Monorepo with centralized build props, npm workspaces, LlamaSharp AI, SQLite/SQLCipher storage, Svelte frontend, and unified smoke tests. Co-Authored-By: Oz <oz-agent@warp.dev>
517 lines
22 KiB
C#
517 lines
22 KiB
C#
internal static partial class Program
|
|
{
|
|
static Task TestVaultCryptoRoundtripAsync()
|
|
{
|
|
var crypto = new VaultCryptoService();
|
|
var plaintext = "sample vault payload";
|
|
var payload = crypto.EncryptData(System.Text.Encoding.UTF8.GetBytes(plaintext), "vault-pass-123");
|
|
|
|
Assert(payload.Length == VaultCryptoService.SaltSize + VaultCryptoService.NonceSize + VaultCryptoService.TagSize + plaintext.Length, "Vault payload length should match salt+nonce+tag+ciphertext layout.");
|
|
var decrypted = crypto.DecryptData(payload, "vault-pass-123");
|
|
Assert(System.Text.Encoding.UTF8.GetString(decrypted) == plaintext, "Vault roundtrip decrypt should return original plaintext.");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultCryptoDecryptsPythonFixtureAsync()
|
|
{
|
|
var crypto = new VaultCryptoService();
|
|
|
|
var payload = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODwABAgMEBQYHCAkKC6AErhDEMERBl7OFkG4L4oZ2JZckS0VzhxaZoVLckF7VXE+NIYXILsJ8f1I=");
|
|
var expectedPlaintext = Convert.FromBase64String("dmF1bHQgcGF5bG9hZCBleGFtcGxlCmxpbmUy");
|
|
var decrypted = crypto.DecryptData(payload, "vault-pass-123");
|
|
|
|
Assert(decrypted.SequenceEqual(expectedPlaintext), "C# decrypt should match Python-generated payload plaintext.");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultKeyDerivationMatchesPythonAsync()
|
|
{
|
|
var crypto = new VaultCryptoService();
|
|
var salt = Enumerable.Range(0, VaultCryptoService.SaltSize).Select(i => (byte)i).ToArray();
|
|
var key = crypto.DeriveKey("vault-pass-123", salt);
|
|
var expectedKeyHex = "b29f523f28bf178f6815c6ca9ee2a588d79b3bd9a822c92a2f0dde5bc853bb52";
|
|
var actualKeyHex = Convert.ToHexString(key).ToLowerInvariant();
|
|
|
|
Assert(actualKeyHex == expectedKeyHex, "Derived key should match Python PBKDF2 fixture key.");
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultMonthlyFilenameParityAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
var dbPath = dbService.GetDatabasePath();
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
|
File.WriteAllText(dbPath, "db-bytes");
|
|
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
|
|
var expectedName = $"_db_{Path.GetFileName(dbPath)}.vault";
|
|
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, expectedName)), "Expected DB vault snapshot filename with _db_ prefix and .vault suffix.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultLoadClearsAndExtractsAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
var dbPath = dbService.GetDatabasePath();
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
|
var originalBytes = Guid.NewGuid().ToByteArray();
|
|
File.WriteAllBytes(dbPath, originalBytes);
|
|
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
|
|
File.WriteAllBytes(dbPath, [1, 2, 3, 4]);
|
|
var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
|
|
Assert(ok, "Expected vault load success with correct password.");
|
|
var loaded = File.ReadAllBytes(dbPath);
|
|
Assert(loaded.SequenceEqual(originalBytes), "Expected DB file bytes restored from vault snapshot.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultLoadWrongPasswordPreservesVaultAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
var dbPath = dbService.GetDatabasePath();
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
|
File.WriteAllText(dbPath, "db payload");
|
|
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault");
|
|
var before = File.ReadAllBytes(vaultPath);
|
|
|
|
var ok = storage.LoadAllVaults("wrong-password", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
var after = File.ReadAllBytes(vaultPath);
|
|
|
|
Assert(!ok, "Expected vault load failure with wrong password.");
|
|
Assert(before.SequenceEqual(after), "Vault file bytes should remain unchanged on wrong password.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultLoadLegacyInitVaultHandlingAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
var legacyPath = Path.Combine(config.Current.VaultDirectory, "_init_vault.vault");
|
|
File.WriteAllBytes(legacyPath, [1, 2, 3, 4]);
|
|
|
|
var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbService.GetDatabasePath())!);
|
|
|
|
Assert(ok, "Legacy-only vault directory should still be treated as successful load state.");
|
|
Assert(File.Exists(legacyPath), "Legacy _init_vault.vault should be ignored in SQLCipher snapshot mode.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultCurrentMonthSaveOptimizedAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
var dbPath = dbService.GetDatabasePath();
|
|
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
|
File.WriteAllText(dbPath, "initial db");
|
|
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault");
|
|
Assert(File.Exists(vaultPath), "Expected DB vault snapshot file to be created.");
|
|
|
|
var firstLength = new FileInfo(vaultPath).Length;
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
|
|
var secondLength = new FileInfo(vaultPath).Length;
|
|
|
|
Assert(firstLength > 0 && secondLength > 0, "Vault snapshot should remain non-empty across repeated rebuilds.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultRebuildAllVaultsAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!;
|
|
Directory.CreateDirectory(dbDir);
|
|
File.WriteAllText(Path.Combine(dbDir, "journal_cache.db"), "primary");
|
|
File.WriteAllText(Path.Combine(dbDir, "analytics.db"), "secondary");
|
|
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
|
|
|
|
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected primary DB snapshot vault.");
|
|
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_analytics.db.vault")), "Expected secondary DB snapshot vault.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultClearDataDirectoryAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
var scratchDir = Path.Combine(root, "scratch-data");
|
|
Directory.CreateDirectory(scratchDir);
|
|
Directory.CreateDirectory(Path.Combine(scratchDir, "nested"));
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(scratchDir, "tmp.md"), "decrypted content");
|
|
File.WriteAllText(Path.Combine(scratchDir, "nested", "tmp.txt"), "temp");
|
|
|
|
storage.ClearDataDirectory(scratchDir);
|
|
|
|
Assert(!Directory.Exists(scratchDir), "Non-db scratch directory should be deleted by clear_data_directory.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static async Task TestEntryVaultLoadAllEmptyAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
|
|
try
|
|
{
|
|
var entry = NewLockedEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.load_all",
|
|
payload = new
|
|
{
|
|
password = "vault-pass-123",
|
|
vaultDirectory = Path.Combine(root, "vault")
|
|
}
|
|
});
|
|
|
|
Directory.CreateDirectory(Path.Combine(root, "vault"));
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for empty vault directory load.");
|
|
Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault directory.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestEntryVaultClearDataDirectoryAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.clear_data_directory",
|
|
payload = new { }
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for clear_data_directory.");
|
|
Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected clear_data_directory result=true.");
|
|
}
|
|
|
|
static Task TestSidecarVaultCliLoadAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
|
|
using var session = new DatabaseSessionService(dbService);
|
|
session.SetPassword("vault-pass-123");
|
|
|
|
var repo = new SqliteEntryFileRepository(session);
|
|
var entryFiles = new EntryFileService(repo);
|
|
var searchService = new EntrySearchService(repo);
|
|
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles);
|
|
|
|
var vaultDir = Path.Combine(root, "vault-cli");
|
|
Directory.CreateDirectory(vaultDir);
|
|
|
|
try
|
|
{
|
|
var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir]);
|
|
Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory.");
|
|
|
|
var dbDir = Path.Combine(config.Current.VaultDirectory, "db");
|
|
Assert(Directory.Exists(dbDir), "Expected db directory to be created by vault load CLI command.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestSidecarVaultCliSaveAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
|
|
using var session = new DatabaseSessionService(dbService);
|
|
session.SetPassword("vault-pass-123");
|
|
|
|
var repo = new SqliteEntryFileRepository(session);
|
|
var entryFiles = new EntryFileService(repo);
|
|
var searchService = new EntrySearchService(repo);
|
|
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles);
|
|
|
|
var vaultDir = Path.Combine(root, "vault-cli");
|
|
Directory.CreateDirectory(vaultDir);
|
|
|
|
try
|
|
{
|
|
entryFiles.SaveEntry(new EntrySavePayload("entry body", Mode: "Overwrite", FileName: "2026-02-22"));
|
|
session.CloseConnection();
|
|
|
|
var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir]);
|
|
|
|
Assert(exitCode == 0, "Expected vault save CLI command to succeed.");
|
|
Assert(File.Exists(Path.Combine(vaultDir, "_db_journal_cache.db.vault")), "Expected DB vault snapshot file to be written by save CLI command.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static Task TestVaultCustomEntryRoundtripAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var storage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
|
|
try
|
|
{
|
|
using (var seedSession = new DatabaseSessionService(dbService))
|
|
{
|
|
seedSession.SetPassword("vault-pass-123");
|
|
var seedRepo = new SqliteEntryFileRepository(seedSession);
|
|
var seedEntryFiles = new EntryFileService(seedRepo);
|
|
|
|
seedEntryFiles.SaveEntry(new EntrySavePayload("date entry", Mode: "Overwrite", FileName: "2026-02-01"));
|
|
seedEntryFiles.SaveEntry(new EntrySavePayload("custom entry body", Mode: "Overwrite", FileName: "My Custom Entry"));
|
|
seedEntryFiles.SaveEntry(new EntrySavePayload("work notes body", Mode: "Overwrite", FileName: "Work Notes"));
|
|
seedSession.CloseConnection();
|
|
}
|
|
|
|
var dbPath = dbService.GetDatabasePath();
|
|
var dbDir = Path.GetDirectoryName(dbPath)!;
|
|
|
|
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
|
|
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected DB vault snapshot to be created.");
|
|
|
|
File.Delete(dbPath);
|
|
Assert(!File.Exists(dbPath), "DB file should be deleted before restore.");
|
|
|
|
var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
|
|
Assert(ok, "Expected vault load to succeed.");
|
|
|
|
using (var verifySession = new DatabaseSessionService(dbService))
|
|
{
|
|
verifySession.SetPassword("vault-pass-123");
|
|
var verifyRepo = new SqliteEntryFileRepository(verifySession);
|
|
var verifyEntryFiles = new EntryFileService(verifyRepo);
|
|
var allEntries = verifyEntryFiles.ListEntries();
|
|
Assert(allEntries.Any(e => e.FileName == "2026-02-01.md"), "Date entry should be restored from vault DB snapshot.");
|
|
Assert(allEntries.Any(e => e.FileName == "My Custom Entry.md"), "Custom entry should be restored from vault DB snapshot.");
|
|
Assert(allEntries.Any(e => e.FileName == "Work Notes.md"), "Second custom entry should be restored from vault DB snapshot.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
static async Task TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
|
|
var config = NewConfigService(root);
|
|
var dbService = new JournalDatabaseService(config);
|
|
var vaultStorage = new VaultStorageService(new VaultCryptoService(), dbService);
|
|
var session = new DatabaseSessionService(dbService);
|
|
var repo = new SqliteEntryFileRepository(session);
|
|
var entryFiles = new EntryFileService(repo);
|
|
var entrySearch = new EntrySearchService(repo);
|
|
|
|
var entry = new Entry(
|
|
NewService(),
|
|
entrySearch,
|
|
vaultStorage,
|
|
dbService,
|
|
session,
|
|
config,
|
|
new DisabledAiService("none"),
|
|
new DisabledSpeechBridgeService("none"),
|
|
new DisabledS2TService(),
|
|
entryFiles,
|
|
new ListService(new SqliteListRepository(session)),
|
|
new TodoService(new SqliteTodoRepository(session)),
|
|
new DisabledCoachService(),
|
|
new ConversationService(new SqliteConversationRepository(session)),
|
|
new CommandLogger());
|
|
|
|
try
|
|
{
|
|
var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!;
|
|
Directory.CreateDirectory(dbDir);
|
|
File.WriteAllBytes(dbService.GetDatabasePath(), Guid.NewGuid().ToByteArray());
|
|
vaultStorage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
|
|
|
|
var loadRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "vault.load_all",
|
|
payload = new
|
|
{
|
|
password = "wrong-password",
|
|
vaultDirectory = config.Current.VaultDirectory
|
|
}
|
|
});
|
|
var loadResponse = await entry.HandleCommandAsync(loadRequest);
|
|
using var loadDoc = JsonDocument.Parse(loadResponse);
|
|
Assert(loadDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected vault.load_all response envelope to be ok=true.");
|
|
Assert(!loadDoc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=false with wrong password.");
|
|
|
|
var listRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "lists.list"
|
|
});
|
|
var listResponse = await entry.HandleCommandAsync(listRequest);
|
|
using var listDoc = JsonDocument.Parse(listResponse);
|
|
Assert(!listDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected lists.list to fail while locked.");
|
|
var error = listDoc.RootElement.GetProperty("error").GetString() ?? "";
|
|
Assert(error.Contains("database is locked", StringComparison.OrdinalIgnoreCase), "Expected locked-session error after failed vault.load_all.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestEntryTemplateSaveAutoSyncsVaultAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-entry-vault-sync-smoke", Guid.NewGuid().ToString("N"));
|
|
var entry = NewEntry(root: root);
|
|
|
|
var configResponse = await entry.HandleCommandAsync("""{"action":"config.get"}""");
|
|
using var configDoc = JsonDocument.Parse(configResponse);
|
|
var configData = configDoc.RootElement.GetProperty("data");
|
|
var vaultDir = configData.GetProperty("VaultDirectory").GetString() ?? "";
|
|
var dbFilename = configData.GetProperty("DatabaseFilename").GetString() ?? "journal_cache.db";
|
|
|
|
var saveTemplateRequest = JsonSerializer.Serialize(new
|
|
{
|
|
action = "templates.save",
|
|
payload = new
|
|
{
|
|
name = "Weekly Review",
|
|
content = "## Wins\n- shipped feature"
|
|
}
|
|
});
|
|
var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest);
|
|
using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse);
|
|
Assert(saveTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected templates.save to succeed.");
|
|
|
|
var dbVaultPath = Path.Combine(vaultDir, $"_db_{dbFilename}.vault");
|
|
Assert(File.Exists(dbVaultPath), "Expected template save auto-sync to write DB vault snapshot.");
|
|
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|