feat(vault): pick a notebook vault folder on first run
Some checks failed
CI / Windows build (push) Has been cancelled

Phase 0 of the file-based storage plan (docs/plans/
2026-06-24-file-based-storage.md). Foundation only — no editor or
DB change yet.

- VaultService (SharedPreferences): stores the vault root path,
  vaultRootValid() = path set AND directory exists.
- VaultSetupScreen: first-run folder picker (file_picker, Windows).
- main.dart gates HomeScreen behind a valid vault, re-prompting if
  the saved folder is gone.
- Settings: a Vault section to change the folder.

Editors still use SQLite; later phases move annotations into
per-file sidecars under the vault. analyze clean, tests green.
This commit is contained in:
2026-06-24 20:45:16 +08:00
parent 875dabcd89
commit 9fcac47ef2
10 changed files with 655 additions and 3 deletions

View File

@@ -0,0 +1,81 @@
// Proves the Phase-0 vault root recording: the path persists through
// SharedPreferences, vaultRootValid() is true ONLY when a path is set AND its
// directory exists, and clearVaultRoot() forgets it. Uses a real temp dir for
// existence checks and a mock SharedPreferences for persistence.
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:badnote/services/vault_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory tempDir;
setUp(() async {
SharedPreferences.setMockInitialValues({});
tempDir = await Directory.systemTemp.createTemp('vault_service_test');
});
tearDown(() async {
if (await tempDir.exists()) {
await tempDir.delete(recursive: true);
}
});
Future<VaultService> makeService() async {
final prefs = await SharedPreferences.getInstance();
return VaultService.forTest(prefs);
}
test('starts with no vault root and is invalid', () async {
final vault = await makeService();
expect(vault.vaultRoot, isNull);
expect(await vault.vaultRootValid(), isFalse);
});
test('setVaultRoot persists the path', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
expect(vault.vaultRoot, tempDir.path);
// Re-read through a fresh service over the SAME prefs store: persisted.
final prefs = await SharedPreferences.getInstance();
expect(prefs.getString(VaultService.vaultRootKey), tempDir.path);
final reopened = VaultService.forTest(prefs);
expect(reopened.vaultRoot, tempDir.path);
});
test('vaultRootValid is true only when set AND the dir exists', () async {
final vault = await makeService();
// Set to an existing dir → valid.
await vault.setVaultRoot(tempDir.path);
expect(await vault.vaultRootValid(), isTrue);
// Point at a path that does not exist → invalid (folder went missing).
final missing = '${tempDir.path}${Platform.pathSeparator}gone';
await vault.setVaultRoot(missing);
expect(vault.vaultRoot, missing);
expect(await vault.vaultRootValid(), isFalse);
});
test('clearVaultRoot forgets the path and becomes invalid', () async {
final vault = await makeService();
await vault.setVaultRoot(tempDir.path);
expect(await vault.vaultRootValid(), isTrue);
await vault.clearVaultRoot();
expect(vault.vaultRoot, isNull);
expect(await vault.vaultRootValid(), isFalse);
});
test('empty stored path is treated as invalid', () async {
final vault = await makeService();
await vault.setVaultRoot('');
expect(await vault.vaultRootValid(), isFalse);
});
}