Refactor smoke tests for SQLCipher-first backend contract

This commit is contained in:
Jacob Schmidt 2026-02-28 18:58:44 -06:00
parent 941cafba39
commit 4fd3c5b5f1
6 changed files with 701 additions and 820 deletions

View File

@ -11,165 +11,108 @@ internal static partial class Program
static Task TestDatabaseSchemaParityAsync() static Task TestDatabaseSchemaParityAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); var service = NewDatabaseService();
Directory.CreateDirectory(root); var statements = service.GetSchemaStatements();
var tableNames = new HashSet<string>(statements.Keys, StringComparer.OrdinalIgnoreCase);
try Assert(tableNames.Contains("entries"), "Schema should contain entries table.");
{ Assert(tableNames.Contains("sections"), "Schema should contain sections table.");
var service = NewDatabaseService(); Assert(tableNames.Contains("fragments"), "Schema should contain fragments table.");
var schemaPath = service.WriteSchemaBootstrap(root); Assert(tableNames.Contains("tags"), "Schema should contain tags table.");
var statements = service.GetSchemaStatements(); Assert(tableNames.Contains("fragment_tags"), "Schema should contain fragment_tags table.");
var tableNames = new HashSet<string>(statements.Keys, StringComparer.OrdinalIgnoreCase); Assert(tableNames.Contains("entry_documents"), "Schema should contain entry_documents table.");
Assert(tableNames.Contains("entries"), "Schema should contain entries table."); var fragmentTagsSql = statements["fragment_tags"];
Assert(tableNames.Contains("sections"), "Schema should contain sections table."); Assert(fragmentTagsSql.Contains("PRIMARY KEY (fragment_id, tag_id)", StringComparison.OrdinalIgnoreCase), "fragment_tags should enforce composite primary key parity.");
Assert(tableNames.Contains("fragments"), "Schema should contain fragments table."); Assert(fragmentTagsSql.Contains("FOREIGN KEY (fragment_id) REFERENCES fragments (id)", StringComparison.OrdinalIgnoreCase), "fragment_tags should contain fragment foreign key parity.");
Assert(tableNames.Contains("tags"), "Schema should contain tags table."); Assert(fragmentTagsSql.Contains("FOREIGN KEY (tag_id) REFERENCES tags (id)", StringComparison.OrdinalIgnoreCase), "fragment_tags should contain tag foreign key parity.");
Assert(tableNames.Contains("fragment_tags"), "Schema should contain fragment_tags table.");
Assert(File.Exists(schemaPath), "Schema bootstrap file should be written.");
var fragmentTagsSql = statements["fragment_tags"];
Assert(fragmentTagsSql.Contains("PRIMARY KEY (fragment_id, tag_id)", StringComparison.OrdinalIgnoreCase), "fragment_tags should enforce composite primary key parity.");
Assert(fragmentTagsSql.Contains("FOREIGN KEY (fragment_id) REFERENCES fragments (id)", StringComparison.OrdinalIgnoreCase), "fragment_tags should contain fragment foreign key parity.");
Assert(fragmentTagsSql.Contains("FOREIGN KEY (tag_id) REFERENCES tags (id)", StringComparison.OrdinalIgnoreCase), "fragment_tags should contain tag foreign key parity.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask; return Task.CompletedTask;
} }
static async Task TestEntryDatabaseStatusAsync() static async Task TestEntryDatabaseStatusAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry(unlocked: false);
Directory.CreateDirectory(root); var request = JsonSerializer.Serialize(new
try
{ {
var entry = NewEntry(); action = "db.status",
var request = JsonSerializer.Serialize(new payload = new
{ {
action = "db.status", password = "vault-pass-123"
payload = new }
{ });
password = "vault-pass-123",
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.status."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.status.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.TryGetProperty("DatabasePath", out var databasePath), "Expected DatabasePath in db.status payload."); Assert(data.TryGetProperty("DatabasePath", out var databasePath), "Expected DatabasePath in db.status payload.");
Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty DatabasePath."); Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty DatabasePath.");
Assert(data.TryGetProperty("KeyDerivation", out var keyDerivation), "Expected KeyDerivation in db.status payload."); Assert(data.TryGetProperty("KeyDerivation", out var keyDerivation), "Expected KeyDerivation in db.status payload.");
Assert(string.Equals(keyDerivation.GetString(), "PBKDF2-HMAC-SHA256", StringComparison.Ordinal), "Expected PBKDF2-HMAC-SHA256 key derivation."); Assert(string.Equals(keyDerivation.GetString(), "PBKDF2-HMAC-SHA256", StringComparison.Ordinal), "Expected PBKDF2-HMAC-SHA256 key derivation.");
Assert(data.TryGetProperty("SchemaTables", out var schemaTables), "Expected SchemaTables list in db.status payload."); Assert(data.TryGetProperty("SchemaTables", out var schemaTables), "Expected SchemaTables list in db.status payload.");
Assert(schemaTables.ValueKind == JsonValueKind.Array && schemaTables.GetArrayLength() >= 5, "Expected schema table list in db.status payload."); Assert(schemaTables.ValueKind == JsonValueKind.Array && schemaTables.GetArrayLength() >= 5, "Expected schema table list in db.status payload.");
Assert(data.TryGetProperty("SchemaBootstrapPath", out var schemaBootstrapPath), "Expected SchemaBootstrapPath in db.status payload."); Assert(data.TryGetProperty("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in db.status payload.");
Assert(schemaBootstrapPath.ValueKind == JsonValueKind.String && File.Exists(schemaBootstrapPath.GetString()), "Expected db.status to emit existing schema bootstrap file path."); Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected SQLCipher runtime-ready status in db.status payload.");
Assert(data.TryGetProperty("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in db.status payload.");
Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected SQLCipher runtime-ready status in db.status payload.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntryDatabaseInitializeSchemaAsync() static async Task TestEntryDatabaseInitializeSchemaAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry(unlocked: false);
Directory.CreateDirectory(root); var request = JsonSerializer.Serialize(new
try
{ {
var entry = NewEntry(); action = "db.initialize_schema",
var request = JsonSerializer.Serialize(new payload = new
{ {
action = "db.initialize_schema", password = "vault-pass-123"
payload = new }
{ });
password = "vault-pass-123",
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.initialize_schema."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.initialize_schema.");
var data = doc.RootElement.GetProperty("data");
Assert(data.TryGetProperty("schemaPath", out var schemaPath), "Expected schemaPath in db.initialize_schema response."); var data = doc.RootElement.GetProperty("data");
Assert(schemaPath.ValueKind == JsonValueKind.String, "Expected string schemaPath value."); Assert(data.TryGetProperty("initialized", out var initialized), "Expected initialized flag in db.initialize_schema response.");
var resolvedPath = schemaPath.GetString() ?? ""; Assert(initialized.ValueKind == JsonValueKind.True, "Expected initialized=true from db.initialize_schema.");
Assert(File.Exists(resolvedPath), "db.initialize_schema should write schema bootstrap file."); Assert(data.TryGetProperty("databasePath", out var databasePath), "Expected databasePath in db.initialize_schema response.");
var schemaText = File.ReadAllText(resolvedPath); Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty databasePath from db.initialize_schema.");
Assert(schemaText.Contains("CREATE TABLE IF NOT EXISTS entries", StringComparison.OrdinalIgnoreCase), "schema bootstrap should include entries table.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntryDatabaseHydrateWorkspaceAsync() static async Task TestEntryDatabaseHydrateWorkspaceAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry(unlocked: false);
Directory.CreateDirectory(root); var request = JsonSerializer.Serialize(new
try
{ {
File.WriteAllText(Path.Combine(root, "2026-02-20.md"), "one"); action = "db.hydrate_workspace",
File.WriteAllText(Path.Combine(root, "2026-02-21.md"), "two"); payload = new
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
action = "db.hydrate_workspace", password = "vault-pass-123"
payload = new }
{ });
password = "vault-pass-123",
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.hydrate_workspace."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.hydrate_workspace.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.TryGetProperty("DatabasePath", out var databasePath), "Expected DatabasePath in hydrate payload."); Assert(data.TryGetProperty("DatabasePath", out var databasePath), "Expected DatabasePath in hydrate payload.");
Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty DatabasePath."); Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty DatabasePath.");
Assert(data.TryGetProperty("SchemaBootstrapPath", out var schemaPath), "Expected SchemaBootstrapPath in hydrate payload."); Assert(data.TryGetProperty("EntryFilesProcessed", out var filesProcessed), "Expected EntryFilesProcessed in hydrate payload.");
Assert(schemaPath.ValueKind == JsonValueKind.String && File.Exists(schemaPath.GetString()), "Expected hydrate to write schema bootstrap file."); Assert(filesProcessed.ValueKind == JsonValueKind.Number && filesProcessed.GetInt32() >= 0, "Expected non-negative EntryFilesProcessed in hydrate payload.");
Assert(data.TryGetProperty("EntryFilesProcessed", out var filesProcessed), "Expected EntryFilesProcessed in hydrate payload."); Assert(data.TryGetProperty("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in hydrate payload.");
Assert(filesProcessed.ValueKind == JsonValueKind.Number && filesProcessed.GetInt32() == 2, "Expected hydrate to count markdown files in workspace."); Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected RuntimeReady=true when SQLCipher runtime hydration succeeds.");
Assert(data.TryGetProperty("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in hydrate payload.");
Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected RuntimeReady=true when SQLCipher runtime hydration succeeds.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static Task TestConfigServiceParityKeysAsync() static Task TestConfigServiceParityKeysAsync()
{ {
IJournalConfigService config = new JournalConfigService(); IJournalConfigService config = NewConfigService();
var current = config.Current; var current = config.Current;
Assert(!string.IsNullOrWhiteSpace(current.ProjectRoot), "Config ProjectRoot should not be empty."); Assert(!string.IsNullOrWhiteSpace(current.ProjectRoot), "Config ProjectRoot should not be empty.");
Assert(!string.IsNullOrWhiteSpace(current.AppDirectory), "Config AppDirectory should not be empty."); Assert(!string.IsNullOrWhiteSpace(current.AppDirectory), "Config AppDirectory should not be empty.");
Assert(!string.IsNullOrWhiteSpace(current.DataDirectory), "Config DataDirectory should not be empty.");
Assert(!string.IsNullOrWhiteSpace(current.VaultDirectory), "Config VaultDirectory should not be empty."); Assert(!string.IsNullOrWhiteSpace(current.VaultDirectory), "Config VaultDirectory should not be empty.");
Assert(current.MonthlyVaultFormat == "%Y-%m.vault", "Config MonthlyVaultFormat should match Python format token."); Assert(!string.IsNullOrWhiteSpace(current.DatabaseFilename), "Config DatabaseFilename should not be empty.");
Assert(current.LlamaCppUrl == "http://127.0.0.1:8085/v1/completions", "Config LlamaCppUrl default mismatch."); Assert(current.LlamaCppUrl == "http://127.0.0.1:8085/v1/completions", "Config LlamaCppUrl default mismatch.");
Assert(current.LlamaCppModel == "qwen/qwen3-4b", "Config LlamaCppModel default mismatch."); Assert(current.LlamaCppModel == "qwen/qwen3-4b", "Config LlamaCppModel default mismatch.");
@ -194,10 +137,10 @@ internal static partial class Program
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Object, "Expected object payload for config.get."); Assert(data.ValueKind == JsonValueKind.Object, "Expected object payload for config.get.");
Assert(data.TryGetProperty("DataDirectory", out var dataDirectory), "Expected DataDirectory in config payload."); Assert(data.TryGetProperty("VaultDirectory", out var vaultDirectory), "Expected VaultDirectory in config payload.");
Assert(dataDirectory.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(dataDirectory.GetString()), "Expected non-empty DataDirectory value."); Assert(vaultDirectory.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(vaultDirectory.GetString()), "Expected non-empty VaultDirectory value.");
Assert(data.TryGetProperty("MonthlyVaultFormat", out var monthlyVaultFormat), "Expected MonthlyVaultFormat in config payload."); Assert(data.TryGetProperty("DatabaseFilename", out var databaseFilename), "Expected DatabaseFilename in config payload.");
Assert(monthlyVaultFormat.GetString() == "%Y-%m.vault", "Expected Python-compatible MonthlyVaultFormat value."); Assert(databaseFilename.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databaseFilename.GetString()), "Expected non-empty DatabaseFilename value.");
Assert(data.TryGetProperty("LlamaCppUrl", out _), "Expected LlamaCppUrl in config payload."); Assert(data.TryGetProperty("LlamaCppUrl", out _), "Expected LlamaCppUrl in config payload.");
Assert(data.TryGetProperty("SpeechRecognitionEngine", out _), "Expected SpeechRecognitionEngine in config payload."); Assert(data.TryGetProperty("SpeechRecognitionEngine", out _), "Expected SpeechRecognitionEngine in config payload.");
} }
@ -233,7 +176,7 @@ internal static partial class Program
{ {
action = "entries.save", action = "entries.save",
mode = "Daily", mode = "Daily",
filePath = "E:/journal/2026-02-24.md" filePath = "db://entry/2026-02-24.md"
}); });
var redacted = LogRedactor.RedactPayload(payload); var redacted = LogRedactor.RedactPayload(payload);
@ -246,4 +189,3 @@ internal static partial class Program
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -47,507 +47,386 @@ internal static partial class Program
static async Task TestEntryEntriesSaveMergeAsync() static async Task TestEntryEntriesSaveMergeAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); var firstPath = await SaveEntryForTestAsync(entry, "2026-02-22", """
try
{
var filePath = Path.Combine(root, "2026-02-22.md");
File.WriteAllText(filePath, """
Date: 2026-02-22 Date: 2026-02-22
## Summary ## Summary
old summary text old summary text
"""); """);
var entry = NewEntry(); var mergeRequest = JsonSerializer.Serialize(new
var request = JsonSerializer.Serialize(new {
action = "entries.save",
payload = new
{ {
action = "entries.save", filePath = firstPath,
payload = new mode = "Daily",
{ content = """
filePath,
mode = "Daily",
content = """
Date: 2026-02-22 Date: 2026-02-22
## Summary ## Summary
new summary text new summary text
## Reflection ## Reflection
new reflection text new reflection text
""" """
} }
}); });
var response = await entry.HandleCommandAsync(request); var mergeResponse = await entry.HandleCommandAsync(mergeRequest);
using var doc = JsonDocument.Parse(response); using var mergeDoc = JsonDocument.Parse(mergeResponse);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save."); Assert(mergeDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save daily merge.");
var saved = File.ReadAllText(filePath); var loadedAfterMerge = await LoadEntryForTestAsync(entry, firstPath);
Assert(saved.Contains("new summary text", StringComparison.Ordinal), "Expected merged file to contain new summary text."); Assert(loadedAfterMerge.Contains("new summary text", StringComparison.Ordinal), "Expected merged entry to contain new summary text.");
Assert(!saved.Contains("old summary text", StringComparison.Ordinal), "Expected merged file to replace old summary section."); Assert(!loadedAfterMerge.Contains("old summary text", StringComparison.Ordinal), "Expected merged entry to replace old summary section.");
Assert(saved.Contains("new reflection text", StringComparison.Ordinal), "Expected merged file to contain new reflection section."); Assert(loadedAfterMerge.Contains("new reflection text", StringComparison.Ordinal), "Expected merged entry to contain new reflection section.");
var fragmentSaveRequest = JsonSerializer.Serialize(new var fragmentSaveRequest = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
filePath,
mode = "Fragment",
content = "!NOTE\nfragment append text"
}
});
var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest);
using var fragmentDoc = JsonDocument.Parse(fragmentResponse);
Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode.");
var appended = File.ReadAllText(filePath);
Assert(appended.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved file.");
}
finally
{ {
if (Directory.Exists(root)) action = "entries.save",
Directory.Delete(root, recursive: true); payload = new
} {
filePath = firstPath,
mode = "Fragment",
content = "!NOTE\nfragment append text"
}
});
var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest);
using var fragmentDoc = JsonDocument.Parse(fragmentResponse);
Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode.");
var loadedAfterFragment = await LoadEntryForTestAsync(entry, firstPath);
Assert(loadedAfterFragment.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved entry.");
} }
static async Task TestEntryEntriesLoadAsync() static async Task TestEntryEntriesLoadAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); var content = """
try
{
var filePath = Path.Combine(root, "2026-02-22.md");
var content = """
Date: 2026-02-22 Date: 2026-02-22
## Summary ## Summary
hello world hello world
"""; """;
File.WriteAllText(filePath, content); var filePath = await SaveEntryForTestAsync(entry, "2026-02-22", content);
var entry = NewEntry(); var request = JsonSerializer.Serialize(new
var request = JsonSerializer.Serialize(new
{
action = "entries.load",
payload = new
{
filePath
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load.");
var data = doc.RootElement.GetProperty("data");
var entryDto = data.GetProperty("Entry");
Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load.");
Assert(entryDto.GetProperty("RawContent").GetString() == content, "Expected raw content from entries.load.");
Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load.");
}
finally
{ {
if (Directory.Exists(root)) action = "entries.load",
Directory.Delete(root, recursive: true); payload = new
} {
filePath
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load.");
var data = doc.RootElement.GetProperty("data");
var entryDto = data.GetProperty("Entry");
Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load.");
Assert(entryDto.GetProperty("RawContent").GetString() == content, "Expected raw content from entries.load.");
Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load.");
} }
static async Task TestEntryEntriesListAsync() static async Task TestEntryEntriesListAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SaveEntryForTestAsync(entry, "2026-02-03", "c");
await SaveEntryForTestAsync(entry, "2026-02-01", "a");
try var request = JsonSerializer.Serialize(new
{ {
File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "c"); action = "entries.list",
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "a"); payload = new { }
File.WriteAllText(Path.Combine(root, "ignore.txt"), "x"); });
var entry = NewEntry(); var response = await entry.HandleCommandAsync(request);
var request = JsonSerializer.Serialize(new using var doc = JsonDocument.Parse(response);
{ Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
action = "entries.list",
payload = new
{
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request); var data = doc.RootElement.GetProperty("data");
using var doc = JsonDocument.Parse(response); Assert(data.ValueKind == JsonValueKind.Array, "Expected entries.list data array.");
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list."); Assert(data.GetArrayLength() == 2, "Expected entries.list to return seeded markdown entries.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Expected entries.list sort order by file name.");
var data = doc.RootElement.GetProperty("data"); Assert(data[1].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected entries.list sort order by file name.");
Assert(data.ValueKind == JsonValueKind.Array, "Expected entries.list data array.");
Assert(data.GetArrayLength() == 2, "Expected entries.list to return only markdown files.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Expected entries.list sort order by file name.");
Assert(data[1].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected entries.list sort order by file name.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntryTemplatesCrudExcludesFromEntriesListAsync() static async Task TestEntryTemplatesCrudExcludesFromEntriesListAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-template-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SaveEntryForTestAsync(entry, "2026-02-03", "daily entry");
try var saveRequest = JsonSerializer.Serialize(new
{ {
File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "daily entry"); action = "templates.save",
payload = new
var entry = NewEntry();
var saveRequest = JsonSerializer.Serialize(new
{ {
action = "templates.save", name = "Weekly Review",
payload = new content = "# Weekly Review\n\n## Wins\n- one"
{ }
name = "Weekly Review", });
content = "# Weekly Review\n\n## Wins\n- one", var saveResponse = await entry.HandleCommandAsync(saveRequest);
dataDirectory = root using var saveDoc = JsonDocument.Parse(saveResponse);
} Assert(saveDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.save.");
});
var saveResponse = await entry.HandleCommandAsync(saveRequest);
using var saveDoc = JsonDocument.Parse(saveResponse);
Assert(saveDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.save.");
var templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
Assert(templatePath.EndsWith(".template.md", StringComparison.OrdinalIgnoreCase), "Template file should end with .template.md.");
Assert(File.Exists(templatePath), "Template file should exist.");
var listTemplatesRequest = JsonSerializer.Serialize(new var templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
{ Assert(templatePath.EndsWith("Weekly%20Review.template.md", StringComparison.OrdinalIgnoreCase), "Template path should be canonical db://template path.");
action = "templates.list",
payload = new
{
dataDirectory = root
}
});
var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest);
using var listTemplatesDoc = JsonDocument.Parse(listTemplatesResponse);
Assert(listTemplatesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.list.");
var templateItems = listTemplatesDoc.RootElement.GetProperty("data");
Assert(templateItems.GetArrayLength() == 1, "Expected one template in templates.list.");
Assert(templateItems[0].GetProperty("FileName").GetString() == "Weekly Review.template.md", "Template file name mismatch.");
var listEntriesRequest = JsonSerializer.Serialize(new var listTemplatesRequest = JsonSerializer.Serialize(new
{
action = "entries.list",
payload = new
{
dataDirectory = root
}
});
var listEntriesResponse = await entry.HandleCommandAsync(listEntriesRequest);
using var listEntriesDoc = JsonDocument.Parse(listEntriesResponse);
Assert(listEntriesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
var entryItems = listEntriesDoc.RootElement.GetProperty("data");
Assert(entryItems.GetArrayLength() == 1, "Expected entries.list to exclude template files.");
Assert(entryItems[0].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected only daily entry file in entries.list.");
var loadTemplateRequest = JsonSerializer.Serialize(new
{
action = "templates.load",
payload = new
{
filePath = templatePath
}
});
var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest);
using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse);
Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load.");
var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? "";
Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result.");
var deleteTemplateRequest = JsonSerializer.Serialize(new
{
action = "templates.delete",
payload = new
{
filePath = templatePath
}
});
var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest);
using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse);
Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete.");
Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true.");
Assert(!File.Exists(templatePath), "Template file should be deleted.");
}
finally
{ {
if (Directory.Exists(root)) action = "templates.list",
Directory.Delete(root, recursive: true); payload = new { }
} });
var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest);
using var listTemplatesDoc = JsonDocument.Parse(listTemplatesResponse);
Assert(listTemplatesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.list.");
var templateItems = listTemplatesDoc.RootElement.GetProperty("data");
Assert(templateItems.GetArrayLength() == 1, "Expected one template in templates.list.");
Assert(templateItems[0].GetProperty("FileName").GetString() == "Weekly Review.template.md", "Template file name mismatch.");
var listEntriesRequest = JsonSerializer.Serialize(new
{
action = "entries.list",
payload = new { }
});
var listEntriesResponse = await entry.HandleCommandAsync(listEntriesRequest);
using var listEntriesDoc = JsonDocument.Parse(listEntriesResponse);
Assert(listEntriesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
var entryItems = listEntriesDoc.RootElement.GetProperty("data");
Assert(entryItems.GetArrayLength() == 1, "Expected entries.list to exclude template files.");
Assert(entryItems[0].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected only daily entry file in entries.list.");
var loadTemplateRequest = JsonSerializer.Serialize(new
{
action = "templates.load",
payload = new
{
filePath = templatePath
}
});
var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest);
using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse);
Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load.");
var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? "";
Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result.");
var deleteTemplateRequest = JsonSerializer.Serialize(new
{
action = "templates.delete",
payload = new
{
filePath = templatePath
}
});
var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest);
using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse);
Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete.");
Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true.");
} }
static async Task TestEntrySearchEntriesMatchesRawContentAsync() static async Task TestEntrySearchEntriesMatchesRawContentAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "## Summary\nAlpha line\ncommon token"); action = "search.entries",
File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "## Summary\nbeta line\nCOMMON token"); payload = new
File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "## Summary\ngamma only");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
action = "search.entries", query = "common token",
payload = new }
{ });
dataDirectory = root,
query = "common token",
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries."); Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
Assert(data.GetArrayLength() == 2, "Expected two entries matching query across raw content."); Assert(data.GetArrayLength() == 2, "Expected two entries matching query across raw content.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesWithoutQueryReturnsAllAsync() static async Task TestEntrySearchEntriesWithoutQueryReturnsAllAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "one"); action = "search.entries",
File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "two"); payload = new { }
File.WriteAllText(Path.Combine(root, "ignore.txt"), "not markdown"); });
var entry = NewEntry(); var response = await entry.HandleCommandAsync(request);
var request = JsonSerializer.Serialize(new using var doc = JsonDocument.Parse(response);
{
action = "search.entries",
payload = new
{
dataDirectory = root,
}
});
var response = await entry.HandleCommandAsync(request); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries without query.");
using var doc = JsonDocument.Parse(response); var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries without query."); Assert(data.GetArrayLength() == 3, "Expected all seeded markdown entries to be returned when query is omitted.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
Assert(data.GetArrayLength() == 2, "Expected all markdown files to be returned when query is omitted.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesDateRangeFilterAsync() static async Task TestEntrySearchEntriesDateRangeFilterAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
WriteSearchFixtureFiles(root); action = "search.entries",
var entry = NewEntry(); payload = new
var request = JsonSerializer.Serialize(new
{ {
action = "search.entries", startDate = "2026-02-02",
payload = new endDate = "2026-02-28",
{ }
dataDirectory = root, });
startDate = "2026-02-02",
endDate = "2026-02-28",
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for date-range filtered search."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for date-range filtered search.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 1, "Expected one result for filtered date range."); Assert(data.GetArrayLength() == 1, "Expected one result for filtered date range.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-05.md", "Date-range result mismatch."); Assert(data[0].GetProperty("FileName").GetString() == "2026-02-05.md", "Date-range result mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesSectionFilterAsync() static async Task TestEntrySearchEntriesSectionFilterAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
WriteSearchFixtureFiles(root); action = "search.entries",
var entry = NewEntry(); payload = new
var request = JsonSerializer.Serialize(new
{ {
action = "search.entries", query = "focus area",
payload = new section = "Reflection",
{ }
dataDirectory = root, });
query = "focus area",
section = "Reflection",
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for section-scoped search."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for section-scoped search.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 1, "Expected one section-scoped result."); Assert(data.GetArrayLength() == 1, "Expected one section-scoped result.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Section filter result mismatch."); Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Section filter result mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesTagTypeFilterAsync() static async Task TestEntrySearchEntriesTagTypeFilterAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
WriteSearchFixtureFiles(root); action = "search.entries",
var entry = NewEntry(); payload = new
var request = JsonSerializer.Serialize(new
{ {
action = "search.entries", tags = new[] { "stress" },
payload = new types = new[] { "!TRIGGER" },
{ }
dataDirectory = root, });
tags = new[] { "stress" },
types = new[] { "!TRIGGER" },
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for fragment tag/type filtered search."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for fragment tag/type filtered search.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 1, "Expected one result for fragment tag/type filters."); Assert(data.GetArrayLength() == 1, "Expected one result for fragment tag/type filters.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Tag/type filter result mismatch."); Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Tag/type filter result mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesCheckboxFilterAsync() static async Task TestEntrySearchEntriesCheckboxFilterAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
WriteSearchFixtureFiles(root); action = "search.entries",
var entry = NewEntry(); payload = new
var request = JsonSerializer.Serialize(new
{ {
action = "search.entries", @checked = new[] { "med taken" },
payload = new @unchecked = new[] { "drink water" },
{ }
dataDirectory = root, });
@checked = new[] { "med taken" },
@unchecked = new[] { "drink water" },
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for checkbox filtered search."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for checkbox filtered search.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 2, "Expected OR-style checkbox match across checked/unchecked filters."); Assert(data.GetArrayLength() == 2, "Expected OR-style checkbox match across checked/unchecked filters.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesRejectsInvalidDateAsync() static async Task TestEntrySearchEntriesRejectsInvalidDateAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
Directory.CreateDirectory(root); await SeedSearchFixtureEntriesAsync(entry);
try var request = JsonSerializer.Serialize(new
{ {
WriteSearchFixtureFiles(root); action = "search.entries",
var entry = NewEntry(); payload = new
var request = JsonSerializer.Serialize(new
{ {
action = "search.entries", startDate = "2026/02/01",
payload = new }
{ });
dataDirectory = root,
startDate = "2026/02/01",
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false for invalid date format."); Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false for invalid date format.");
var error = doc.RootElement.GetProperty("error").GetString() ?? ""; var error = doc.RootElement.GetProperty("error").GetString() ?? "";
Assert(error.Contains("invalid startdate value", StringComparison.OrdinalIgnoreCase), "Expected invalid startDate error."); Assert(error.Contains("invalid startdate value", StringComparison.OrdinalIgnoreCase), "Expected invalid startDate error.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static Task TestSidecarSearchCliFilteredAsync() static Task TestSidecarSearchCliFilteredAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data"); var config = NewConfigService(root);
Directory.CreateDirectory(dataDir); 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);
try try
{ {
WriteSearchFixtureFiles(dataDir); entryFiles.SaveEntry(new EntrySavePayload("""
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); Date: 2026-02-01
## Summary
common
- [x] med taken
!TRIGGER #stress
matched body
""", Mode: "Overwrite", FileName: "2026-02-01"));
entryFiles.SaveEntry(new EntrySavePayload("""
Date: 2026-02-05
## Summary
common
- [ ] drink water
!NOTE #daily
non match
""", Mode: "Overwrite", FileName: "2026-02-05"));
var (exitCode, stdout, stderr) = CaptureConsole(() => cli.RunSearchCommand( var (exitCode, stdout, stderr) = CaptureConsole(() => cli.RunSearchCommand(
[ [
"common", "common",
"--data-dir", dataDir, "--start-date", "2026-02-01",
"--start-date", "2026-02-01", "--end-date", "2026-02-28",
"--end-date", "2026-02-28", "--tag", "stress",
"--tag", "stress", "--type", "!TRIGGER",
"--type", "!TRIGGER", "--checked", "med taken",
"--checked", "med taken", "--section", "Summary"
"--section", "Summary"
])); ]));
Assert(exitCode == 0, "Expected search CLI command to succeed."); Assert(exitCode == 0, "Expected search CLI command to succeed.");
@ -557,6 +436,7 @@ hello world
} }
finally finally
{ {
session.Dispose();
if (Directory.Exists(root)) if (Directory.Exists(root))
Directory.Delete(root, recursive: true); Directory.Delete(root, recursive: true);
} }
@ -567,19 +447,27 @@ hello world
static Task TestSidecarSearchCliEmptyDataAsync() static Task TestSidecarSearchCliEmptyDataAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data"); var config = NewConfigService(root);
Directory.CreateDirectory(dataDir); 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);
try try
{ {
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand([]));
var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand(["--data-dir", dataDir]));
Assert(exitCode == 0, "Expected search CLI command to return success for empty data directory."); Assert(exitCode == 0, "Expected search CLI command to return success for empty data store.");
Assert(stdout.Contains("No decrypted journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message."); Assert(stdout.Contains("No journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message.");
} }
finally finally
{ {
session.Dispose();
if (Directory.Exists(root)) if (Directory.Exists(root))
Directory.Delete(root, recursive: true); Directory.Delete(root, recursive: true);
} }
@ -589,7 +477,6 @@ hello world
static Task TestEntrySavePayloadFileNameDeserializationAsync() static Task TestEntrySavePayloadFileNameDeserializationAsync()
{ {
// Simulate what DeserializePayload does: parse JSON to JsonElement, then Deserialize<T>
var json = """{"content":"hello","mode":"Overwrite","fileName":"My Custom Name"}"""; var json = """{"content":"hello","mode":"Overwrite","fileName":"My Custom Name"}""";
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var element = JsonSerializer.Deserialize<JsonElement>(json); var element = JsonSerializer.Deserialize<JsonElement>(json);
@ -607,26 +494,31 @@ hello world
static async Task TestEntrySaveWithFileNameAsync() static async Task TestEntrySaveWithFileNameAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root); 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 service = new EntryFileService(repo);
try try
{ {
// Use EntryFileService directly to test the full save path with fileName
var service = new EntryFileService(new DiskEntryFileRepository());
var payload = new EntrySavePayload( var payload = new EntrySavePayload(
Content: "# Custom Entry\n\nHello world", Content: "# Custom Entry\n\nHello world",
FilePath: null, FilePath: null,
Mode: "Overwrite", Mode: "Overwrite",
FileName: "My Custom Name"); FileName: "My Custom Name");
var result = service.SaveEntry(payload, root); var result = service.SaveEntry(payload);
var expectedPath = Path.GetFullPath(Path.Combine(root, "My Custom Name.md")); Assert(result.FilePath.StartsWith("db://entry/", StringComparison.OrdinalIgnoreCase), "Expected canonical db://entry path.");
Assert(result.FilePath == expectedPath, $"Expected path '{expectedPath}' but got '{result.FilePath}'."); Assert(result.FilePath.Contains("My%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), "Expected escaped custom name in db path.");
Assert(File.Exists(expectedPath), "Custom-named file should exist on disk.");
Assert(File.ReadAllText(expectedPath).Contains("Hello world"), "File content should match."); var loaded = service.LoadEntry(result.FilePath);
Assert(loaded.Entry.RawContent.Contains("Hello world", StringComparison.Ordinal), "Stored content should match.");
// Also test via Entry.HandleCommandAsync with JSON to verify full deserialization chain
var entry = NewEntry(); var entry = NewEntry();
var request = JsonSerializer.Serialize(new var request = JsonSerializer.Serialize(new
{ {
@ -644,14 +536,87 @@ hello world
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save with fileName."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save with fileName.");
var savedFilePath = doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? ""; var savedFilePath = doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
Assert(savedFilePath.Contains("Another Custom Name.md", StringComparison.OrdinalIgnoreCase), Assert(savedFilePath.Contains("Another%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase),
$"Expected file path to contain 'Another Custom Name.md' but got '{savedFilePath}'."); $"Expected file path to contain 'Another%20Custom%20Name.md' but got '{savedFilePath}'.");
} }
finally finally
{ {
session.Dispose();
if (Directory.Exists(root)) if (Directory.Exists(root))
Directory.Delete(root, recursive: true); Directory.Delete(root, recursive: true);
} }
} }
}
private static async Task<string> SaveEntryForTestAsync(Entry entry, string fileStem, string content)
{
var request = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
content,
mode = "Overwrite",
fileName = fileStem
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test seed entries.save.");
return doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
}
private static async Task<string> LoadEntryForTestAsync(Entry entry, string filePath)
{
var request = JsonSerializer.Serialize(new
{
action = "entries.load",
payload = new
{
filePath
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test entries.load.");
return doc.RootElement.GetProperty("data").GetProperty("Entry").GetProperty("RawContent").GetString() ?? "";
}
private static async Task SeedSearchFixtureEntriesAsync(Entry entry)
{
await SaveEntryForTestAsync(entry, "2026-02-01", """
Date: 2026-02-01
## Summary
Alpha line
common token
## Reflection
focus area
- [x] med taken
!TRIGGER #stress
fragment one
""");
await SaveEntryForTestAsync(entry, "2026-02-05", """
Date: 2026-02-05
## Summary
beta line
COMMON token
## Reflection
other notes
- [ ] drink water
!NOTE #daily
fragment two
""");
await SaveEntryForTestAsync(entry, "2026-03-01", """
Date: 2026-03-01
## Summary
gamma only
## Reflection
nothing related
!NOTE #other
fragment three
""");
}
}

View File

@ -53,20 +53,19 @@ internal static partial class Program
try try
{ {
// Set up encrypted DB session var configService = NewConfigService(tempRoot);
var configService = new JournalConfigService();
var dbService = new JournalDatabaseService(configService); var dbService = new JournalDatabaseService(configService);
// First session: create a fragment // First session: create a fragment
using var session1 = new DatabaseSessionService(dbService); using var session1 = new DatabaseSessionService(dbService);
session1.SetPassword(password, dataDir); session1.SetPassword(password);
var repo1 = new SqliteFragmentRepository(session1); var repo1 = new SqliteFragmentRepository(session1);
var service1 = new FragmentService(repo1); var service1 = new FragmentService(repo1);
var created = service1.Create(new CreateFragmentDto("!TRIGGER", "persist me", ["tag1"])); var created = service1.Create(new CreateFragmentDto("!TRIGGER", "persist me", ["tag1"]));
// Second session: verify persistence // Second session: verify persistence
using var session2 = new DatabaseSessionService(dbService); using var session2 = new DatabaseSessionService(dbService);
session2.SetPassword(password, dataDir); session2.SetPassword(password);
var repo2 = new SqliteFragmentRepository(session2); var repo2 = new SqliteFragmentRepository(session2);
var service2 = new FragmentService(repo2); var service2 = new FragmentService(repo2);
var loaded = service2.GetById(created.Id); var loaded = service2.GetById(created.Id);

View File

@ -6,26 +6,64 @@ internal static partial class Program
return new FragmentService(repo); return new FragmentService(repo);
} }
static Entry NewEntry() static Entry NewEntry(bool unlocked = true, string password = "vault-pass-123", string? root = null)
{ {
var dbService = new JournalDatabaseService(new JournalConfigService()); var config = NewConfigService(root);
var dbService = new JournalDatabaseService(config);
var session = new DatabaseSessionService(dbService); var session = new DatabaseSessionService(dbService);
if (unlocked)
session.SetPassword(password);
var entryRepo = new SqliteEntryFileRepository(session);
return new Entry( return new Entry(
NewService(), NewService(),
new EntrySearchService(), new EntrySearchService(entryRepo),
new VaultStorageService(new VaultCryptoService()), new VaultStorageService(new VaultCryptoService(), dbService),
dbService, dbService,
session, session,
new JournalConfigService(), config,
new DisabledAiService("none"), new DisabledAiService("none"),
new DisabledSpeechBridgeService("none"), new DisabledSpeechBridgeService("none"),
new EntryFileService(new DiskEntryFileRepository()), new EntryFileService(entryRepo),
new ListService(new SqliteListRepository(session)), new ListService(new SqliteListRepository(session)),
new TodoService(new SqliteTodoRepository(session)), new TodoService(new SqliteTodoRepository(session)),
new CommandLogger()); new CommandLogger());
} }
static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(new JournalConfigService()); static Entry NewLockedEntry() => NewEntry(unlocked: false);
static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(NewConfigService());
static IJournalConfigService NewConfigService(string? root = null, string? databaseFilename = null)
{
var baseConfig = new JournalConfigService().Current;
var normalizedRoot = Path.GetFullPath(root ?? Path.Combine(Path.GetTempPath(), "journal-smoke", Guid.NewGuid().ToString("N")));
var appDirectory = Path.Combine(normalizedRoot, "journal");
var vaultDirectory = Path.Combine(appDirectory, "vault");
var logDirectory = Path.Combine(normalizedRoot, "logs");
Directory.CreateDirectory(vaultDirectory);
Directory.CreateDirectory(logDirectory);
var config = baseConfig with
{
ProjectRoot = normalizedRoot,
AppDirectory = appDirectory,
VaultDirectory = vaultDirectory,
LogDirectory = logDirectory,
PidFile = Path.Combine(logDirectory, "nicegui_server.pid"),
ServerControlFile = Path.Combine(logDirectory, "server_control.action"),
DatabaseFilename = databaseFilename ?? "journal_cache.db"
};
return new FixedConfigService(config);
}
private sealed class FixedConfigService(JournalConfig config) : IJournalConfigService
{
public JournalConfig Current => config;
}
static Dictionary<string, string> ReadVaultEntryTexts(string vaultPath, string password) static Dictionary<string, string> ReadVaultEntryTexts(string vaultPath, string password)
{ {

View File

@ -41,40 +41,53 @@ internal static partial class Program
static Task TestVaultMonthlyFilenameParityAsync() static Task TestVaultMonthlyFilenameParityAsync()
{ {
IVaultStorageService vaultStorage = new VaultStorageService(new VaultCryptoService()); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var name = vaultStorage.GetMonthlyVaultFileName(new DateTime(2026, 2, 7)); var config = NewConfigService(root);
Assert(name == "2026-02.vault", "Monthly vault filename must match yyyy-MM.vault format."); 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; return Task.CompletedTask;
} }
static Task TestVaultLoadClearsAndExtractsAsync() static Task TestVaultLoadClearsAndExtractsAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir);
try try
{ {
File.WriteAllText(Path.Combine(dataDir, "old_file.md"), "stale"); var dbPath = dbService.GetDatabasePath();
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
var originalBytes = Guid.NewGuid().ToByteArray();
File.WriteAllBytes(dbPath, originalBytes);
var zipBytes = CreateZipBytes(new Dictionary<string, string> storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
{
["2026-02-01.md"] = "hello from vault"
});
var crypto = new VaultCryptoService();
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
File.WriteAllBytes(Path.Combine(vaultDir, "2026-02.vault"), encrypted);
IVaultStorageService storage = new VaultStorageService(crypto); File.WriteAllBytes(dbPath, [1, 2, 3, 4]);
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir); var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
Assert(ok, "Expected vault load success with correct password."); Assert(ok, "Expected vault load success with correct password.");
Assert(!File.Exists(Path.Combine(dataDir, "old_file.md")), "Data directory should be cleared before extraction."); var loaded = File.ReadAllBytes(dbPath);
var extractedPath = Path.Combine(dataDir, "2026-02-01.md"); Assert(loaded.SequenceEqual(originalBytes), "Expected DB file bytes restored from vault snapshot.");
Assert(File.Exists(extractedPath), "Expected markdown file extracted from vault archive.");
Assert(File.ReadAllText(extractedPath) == "hello from vault", "Extracted file content mismatch.");
} }
finally finally
{ {
@ -88,25 +101,21 @@ internal static partial class Program
static Task TestVaultLoadWrongPasswordPreservesVaultAsync() static Task TestVaultLoadWrongPasswordPreservesVaultAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir);
try try
{ {
var zipBytes = CreateZipBytes(new Dictionary<string, string> var dbPath = dbService.GetDatabasePath();
{ Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
["2026-02-01.md"] = "hello from vault" File.WriteAllText(dbPath, "db payload");
});
var crypto = new VaultCryptoService(); storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123"); var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault");
var vaultPath = Path.Combine(vaultDir, "2026-02.vault");
File.WriteAllBytes(vaultPath, encrypted);
var before = File.ReadAllBytes(vaultPath); var before = File.ReadAllBytes(vaultPath);
IVaultStorageService storage = new VaultStorageService(crypto); var ok = storage.LoadAllVaults("wrong-password", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
var ok = storage.LoadAllVaults("wrong-password", vaultDir, dataDir);
var after = File.ReadAllBytes(vaultPath); var after = File.ReadAllBytes(vaultPath);
Assert(!ok, "Expected vault load failure with wrong password."); Assert(!ok, "Expected vault load failure with wrong password.");
@ -124,22 +133,19 @@ internal static partial class Program
static Task TestVaultLoadLegacyInitVaultHandlingAsync() static Task TestVaultLoadLegacyInitVaultHandlingAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir);
try try
{ {
var legacyPath = Path.Combine(vaultDir, "_init_vault.vault"); var legacyPath = Path.Combine(config.Current.VaultDirectory, "_init_vault.vault");
File.WriteAllBytes(legacyPath, [1, 2, 3, 4]); File.WriteAllBytes(legacyPath, [1, 2, 3, 4]);
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbService.GetDatabasePath())!);
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
Assert(ok, "Legacy-only vault directory should still be treated as successful load state."); Assert(ok, "Legacy-only vault directory should still be treated as successful load state.");
Assert(!File.Exists(legacyPath), "Legacy _init_vault.vault should be removed during load."); Assert(File.Exists(legacyPath), "Legacy _init_vault.vault should be ignored in SQLCipher snapshot mode.");
Assert(Directory.Exists(dataDir), "Data directory should exist after load workflow.");
} }
finally finally
{ {
@ -153,43 +159,25 @@ internal static partial class Program
static Task TestVaultCurrentMonthSaveOptimizedAsync() static Task TestVaultCurrentMonthSaveOptimizedAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir);
try try
{ {
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb one"); var dbPath = dbService.GetDatabasePath();
File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two"); Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan one"); File.WriteAllText(dbPath, "initial db");
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
var now = new DateTime(2026, 2, 22, 12, 0, 0, DateTimeKind.Utc); 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 firstSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now); var firstLength = new FileInfo(vaultPath).Length;
Assert(firstSaved, "Expected first current-month save to write vault data."); storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!);
var secondLength = new FileInfo(vaultPath).Length;
var febVaultPath = Path.Combine(vaultDir, "2026-02.vault"); Assert(firstLength > 0 && secondLength > 0, "Vault snapshot should remain non-empty across repeated rebuilds.");
var janVaultPath = Path.Combine(vaultDir, "2026-01.vault");
Assert(File.Exists(febVaultPath), "Expected current-month vault file to be created.");
Assert(!File.Exists(janVaultPath), "Current-month save should not write non-current month vault files.");
var entries = ReadVaultEntryTexts(febVaultPath, "vault-pass-123");
Assert(entries.Count == 2, "Current-month vault should include only current-month markdown files.");
Assert(entries.ContainsKey("2026-02-01.md"), "Missing first current-month entry in vault archive.");
Assert(entries.ContainsKey("2026-02-18.md"), "Missing second current-month entry in vault archive.");
Assert(!entries.ContainsKey("2026-01-31.md"), "Current-month vault must not include previous-month files.");
var beforeSkipBytes = File.ReadAllBytes(febVaultPath);
var secondSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
var afterSkipBytes = File.ReadAllBytes(febVaultPath);
Assert(!secondSaved, "Expected unchanged current-month save to skip write.");
Assert(beforeSkipBytes.SequenceEqual(afterSkipBytes), "Vault bytes should remain unchanged when save is skipped.");
File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two changed");
var thirdSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
Assert(thirdSaved, "Expected save to run after current-month file change.");
} }
finally finally
{ {
@ -203,30 +191,21 @@ internal static partial class Program
static Task TestVaultRebuildAllVaultsAsync() static Task TestVaultRebuildAllVaultsAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir);
try try
{ {
File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan body"); var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!;
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb body"); Directory.CreateDirectory(dbDir);
File.WriteAllText(Path.Combine(dataDir, "not-a-journal.md"), "should be ignored"); File.WriteAllText(Path.Combine(dbDir, "journal_cache.db"), "primary");
File.WriteAllText(Path.Combine(dbDir, "analytics.db"), "secondary");
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
var janVaultPath = Path.Combine(vaultDir, "2026-01.vault"); Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected primary DB snapshot vault.");
var febVaultPath = Path.Combine(vaultDir, "2026-02.vault"); Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_analytics.db.vault")), "Expected secondary DB snapshot vault.");
Assert(File.Exists(janVaultPath), "Expected January vault from rebuild flow.");
Assert(File.Exists(febVaultPath), "Expected February vault from rebuild flow.");
var janEntries = ReadVaultEntryTexts(janVaultPath, "vault-pass-123");
var febEntries = ReadVaultEntryTexts(febVaultPath, "vault-pass-123");
Assert(janEntries.Count == 1 && janEntries.ContainsKey("2026-01-31.md"), "January vault contents mismatch.");
Assert(febEntries.Count == 1 && febEntries.ContainsKey("2026-02-01.md"), "February vault contents mismatch.");
} }
finally finally
{ {
@ -240,21 +219,22 @@ internal static partial class Program
static Task TestVaultClearDataDirectoryAsync() static Task TestVaultClearDataDirectoryAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data"); var config = NewConfigService(root);
Directory.CreateDirectory(dataDir); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(Path.Combine(dataDir, "nested")); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
var scratchDir = Path.Combine(root, "scratch-data");
Directory.CreateDirectory(scratchDir);
Directory.CreateDirectory(Path.Combine(scratchDir, "nested"));
try try
{ {
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "decrypted content"); File.WriteAllText(Path.Combine(scratchDir, "tmp.md"), "decrypted content");
File.WriteAllText(Path.Combine(dataDir, "journal_cache.db"), "cache"); File.WriteAllText(Path.Combine(scratchDir, "nested", "tmp.txt"), "temp");
File.WriteAllText(Path.Combine(dataDir, "nested", "tmp.txt"), "temp");
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); storage.ClearDataDirectory(scratchDir);
storage.ClearDataDirectory(dataDir);
Assert(Directory.Exists(dataDir), "Data directory should be recreated after cleanup."); Assert(!Directory.Exists(scratchDir), "Non-db scratch directory should be deleted by clear_data_directory.");
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after cleanup.");
} }
finally finally
{ {
@ -268,30 +248,28 @@ internal static partial class Program
static async Task TestEntryVaultLoadAllEmptyAsync() static async Task TestEntryVaultLoadAllEmptyAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); Directory.CreateDirectory(root);
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
try try
{ {
var entry = NewEntry(); var entry = NewLockedEntry();
var request = JsonSerializer.Serialize(new var request = JsonSerializer.Serialize(new
{ {
action = "vault.load_all", action = "vault.load_all",
payload = new payload = new
{ {
password = "vault-pass-123", password = "vault-pass-123",
vaultDirectory = vaultDir, vaultDirectory = Path.Combine(root, "vault")
dataDirectory = dataDir,
} }
}); });
Directory.CreateDirectory(Path.Combine(root, "vault"));
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for empty vault directory load."); 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."); Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault directory.");
Assert(Directory.Exists(dataDir), "Expected data directory to be created by load workflow.");
} }
finally finally
{ {
@ -302,52 +280,44 @@ internal static partial class Program
static async Task TestEntryVaultClearDataDirectoryAsync() static async Task TestEntryVaultClearDataDirectoryAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N")); var entry = NewEntry();
var dataDir = Path.Combine(root, "data"); var request = JsonSerializer.Serialize(new
Directory.CreateDirectory(dataDir);
File.WriteAllText(Path.Combine(dataDir, "tmp.md"), "x");
try
{ {
var entry = NewEntry(); action = "vault.clear_data_directory",
var request = JsonSerializer.Serialize(new payload = new { }
{ });
action = "vault.clear_data_directory",
payload = new
{
dataDirectory = dataDir,
}
});
var response = await entry.HandleCommandAsync(request); var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response); using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for clear_data_directory."); 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."); Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected clear_data_directory result=true.");
Assert(Directory.Exists(dataDir), "Expected data directory to exist after clear.");
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Expected data directory to be empty after clear.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static Task TestSidecarVaultCliLoadAsync() static Task TestSidecarVaultCliLoadAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); 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); Directory.CreateDirectory(vaultDir);
try try
{ {
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir]);
var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory."); Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory.");
Assert(Directory.Exists(dataDir), "Expected data directory to be created by vault load CLI command.");
var dbDir = Path.Combine(config.Current.VaultDirectory, "db");
Assert(Directory.Exists(dbDir), "Expected db directory to be created by vault load CLI command.");
} }
finally finally
{ {
@ -361,20 +331,29 @@ internal static partial class Program
static Task TestSidecarVaultCliSaveAsync() static Task TestSidecarVaultCliSaveAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); 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); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
File.WriteAllText(Path.Combine(dataDir, "2026-02-22.md"), "entry body"); entryFiles.SaveEntry(new EntrySavePayload("entry body", Mode: "Overwrite", FileName: "2026-02-22"));
session.CloseConnection();
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir]);
var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
Assert(exitCode == 0, "Expected vault save CLI command to succeed."); Assert(exitCode == 0, "Expected vault save CLI command to succeed.");
Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault file to be written by save CLI command."); Assert(File.Exists(Path.Combine(vaultDir, "_db_journal_cache.db.vault")), "Expected DB vault snapshot file to be written by save CLI command.");
} }
finally finally
{ {
@ -388,41 +367,46 @@ internal static partial class Program
static Task TestVaultCustomEntryRoundtripAsync() static Task TestVaultCustomEntryRoundtripAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var storage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir);
try try
{ {
// Create both date-named and custom-named entries using (var seedSession = new DatabaseSessionService(dbService))
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "date entry"); {
File.WriteAllText(Path.Combine(dataDir, "My Custom Entry.md"), "custom entry body"); seedSession.SetPassword("vault-pass-123");
File.WriteAllText(Path.Combine(dataDir, "Work Notes.md"), "work notes body"); var seedRepo = new SqliteEntryFileRepository(seedSession);
var seedEntryFiles = new EntryFileService(seedRepo);
// Rebuild vaults (simulates app close) seedEntryFiles.SaveEntry(new EntrySavePayload("date entry", Mode: "Overwrite", FileName: "2026-02-01"));
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); seedEntryFiles.SaveEntry(new EntrySavePayload("custom entry body", Mode: "Overwrite", FileName: "My Custom Entry"));
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir); seedEntryFiles.SaveEntry(new EntrySavePayload("work notes body", Mode: "Overwrite", FileName: "Work Notes"));
seedSession.CloseConnection();
}
// Verify custom vault was created var dbPath = dbService.GetDatabasePath();
var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault"); var dbDir = Path.GetDirectoryName(dbPath)!;
Assert(File.Exists(customVaultPath), "Expected _custom_entries.vault to be created.");
Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault for date entry.");
// Clear data directory (simulates app close step 2) storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
storage.ClearDataDirectory(dataDir); Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected DB vault snapshot to be created.");
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after clear.");
// Load vaults (simulates app restart) File.Delete(dbPath);
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir); 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."); Assert(ok, "Expected vault load to succeed.");
// Verify all entries are restored using (var verifySession = new DatabaseSessionService(dbService))
Assert(File.Exists(Path.Combine(dataDir, "2026-02-01.md")), "Date entry should be restored from vault."); {
Assert(File.Exists(Path.Combine(dataDir, "My Custom Entry.md")), "Custom entry should be restored from vault."); verifySession.SetPassword("vault-pass-123");
Assert(File.Exists(Path.Combine(dataDir, "Work Notes.md")), "Second custom entry should be restored from vault."); var verifyRepo = new SqliteEntryFileRepository(verifySession);
Assert(File.ReadAllText(Path.Combine(dataDir, "My Custom Entry.md")) == "custom entry body", "Custom entry content mismatch."); var verifyEntryFiles = new EntryFileService(verifyRepo);
Assert(File.ReadAllText(Path.Combine(dataDir, "Work Notes.md")) == "work notes body", "Second custom entry content mismatch."); 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 finally
{ {
@ -436,22 +420,34 @@ internal static partial class Program
static async Task TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync() static async Task TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault"); var config = NewConfigService(root);
var dataDir = Path.Combine(root, "data"); var dbService = new JournalDatabaseService(config);
Directory.CreateDirectory(vaultDir); var vaultStorage = new VaultStorageService(new VaultCryptoService(), dbService);
Directory.CreateDirectory(dataDir); 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"),
entryFiles,
new ListService(new SqliteListRepository(session)),
new TodoService(new SqliteTodoRepository(session)),
new CommandLogger());
try try
{ {
var zipBytes = CreateZipBytes(new Dictionary<string, string> var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!;
{ Directory.CreateDirectory(dbDir);
["2026-02-01.md"] = "hello from vault" File.WriteAllBytes(dbService.GetDatabasePath(), Guid.NewGuid().ToByteArray());
}); vaultStorage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir);
var crypto = new VaultCryptoService();
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
File.WriteAllBytes(Path.Combine(vaultDir, "2026-02.vault"), encrypted);
var entry = NewEntry();
var loadRequest = JsonSerializer.Serialize(new var loadRequest = JsonSerializer.Serialize(new
{ {
@ -459,8 +455,7 @@ internal static partial class Program
payload = new payload = new
{ {
password = "wrong-password", password = "wrong-password",
vaultDirectory = vaultDir, vaultDirectory = config.Current.VaultDirectory
dataDirectory = dataDir
} }
}); });
var loadResponse = await entry.HandleCommandAsync(loadRequest); var loadResponse = await entry.HandleCommandAsync(loadRequest);
@ -488,89 +483,31 @@ internal static partial class Program
static async Task TestEntryTemplateSaveAutoSyncsVaultAsync() static async Task TestEntryTemplateSaveAutoSyncsVaultAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-entry-vault-sync-smoke", Guid.NewGuid().ToString("N")); var root = Path.Combine(Path.GetTempPath(), "journal-entry-vault-sync-smoke", Guid.NewGuid().ToString("N"));
var projectRoot = Path.Combine(root, "project"); var entry = NewEntry(root: root);
var appDirectory = Path.Combine(projectRoot, "journal");
var vaultDir = Path.Combine(appDirectory, "vault");
var dataDir = Path.Combine(appDirectory, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
var previousProjectRoot = Environment.GetEnvironmentVariable("JOURNAL_PROJECT_ROOT"); var configResponse = await entry.HandleCommandAsync("""{"action":"config.get"}""");
var previousDataDir = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR"); using var configDoc = JsonDocument.Parse(configResponse);
var previousVaultDir = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR"); var configData = configDoc.RootElement.GetProperty("data");
Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", projectRoot); var vaultDir = configData.GetProperty("VaultDirectory").GetString() ?? "";
Environment.SetEnvironmentVariable("JOURNAL_DATA_DIR", dataDir); var dbFilename = configData.GetProperty("DatabaseFilename").GetString() ?? "journal_cache.db";
Environment.SetEnvironmentVariable("JOURNAL_VAULT_DIR", vaultDir);
try var saveTemplateRequest = JsonSerializer.Serialize(new
{ {
var entry = NewEntry(); action = "templates.save",
var password = "vault-pass-123"; payload = new
var unlockRequest = JsonSerializer.Serialize(new
{ {
action = "vault.load_all", name = "Weekly Review",
payload = new content = "## Wins\n- shipped feature"
{ }
password, });
vaultDirectory = vaultDir, var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest);
dataDirectory = dataDir using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse);
} Assert(saveTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected templates.save to succeed.");
});
var unlockResponse = await entry.HandleCommandAsync(unlockRequest);
using var unlockDoc = JsonDocument.Parse(unlockResponse);
Assert(unlockDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected vault.load_all envelope to succeed.");
Assert(unlockDoc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault.");
var saveTemplateRequest = JsonSerializer.Serialize(new var dbVaultPath = Path.Combine(vaultDir, $"_db_{dbFilename}.vault");
{ Assert(File.Exists(dbVaultPath), "Expected template save auto-sync to write DB vault snapshot.");
action = "templates.save",
payload = new
{
name = "Weekly Review",
content = "## Wins\n- shipped feature",
dataDirectory = dataDir
}
});
var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest);
using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse);
Assert(saveTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected templates.save to succeed.");
var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault"); if (Directory.Exists(root))
Assert(File.Exists(customVaultPath), "Expected template save to auto-sync custom entries vault."); Directory.Delete(root, recursive: true);
var entries = ReadVaultEntryTexts(customVaultPath, password);
Assert(entries.ContainsKey("Weekly Review.template.md"), "Expected template file in custom vault archive.");
var clearRequest = JsonSerializer.Serialize(new
{
action = "vault.clear_data_directory",
payload = new
{
dataDirectory = dataDir
}
});
var clearResponse = await entry.HandleCommandAsync(clearRequest);
using var clearDoc = JsonDocument.Parse(clearResponse);
Assert(clearDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected vault.clear_data_directory to succeed.");
var reloadResponse = await entry.HandleCommandAsync(unlockRequest);
using var reloadDoc = JsonDocument.Parse(reloadResponse);
Assert(reloadDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected second vault.load_all envelope to succeed.");
Assert(reloadDoc.RootElement.GetProperty("data").GetBoolean(), "Expected second vault.load_all data=true.");
var restoredTemplatePath = Path.Combine(dataDir, "Weekly Review.template.md");
Assert(File.Exists(restoredTemplatePath), "Expected template to be restored from vault after reload.");
}
finally
{
Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", previousProjectRoot);
Environment.SetEnvironmentVariable("JOURNAL_DATA_DIR", previousDataDir);
Environment.SetEnvironmentVariable("JOURNAL_VAULT_DIR", previousVaultDir);
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
} }

View File

@ -17,11 +17,11 @@ internal static partial class Program
("Vault crypto decrypts Python payload fixture", TestVaultCryptoDecryptsPythonFixtureAsync), ("Vault crypto decrypts Python payload fixture", TestVaultCryptoDecryptsPythonFixtureAsync),
("Vault key derivation matches Python fixture", TestVaultKeyDerivationMatchesPythonAsync), ("Vault key derivation matches Python fixture", TestVaultKeyDerivationMatchesPythonAsync),
("Vault monthly filename matches parity format", TestVaultMonthlyFilenameParityAsync), ("Vault monthly filename matches parity format", TestVaultMonthlyFilenameParityAsync),
("Vault load clears workspace and extracts decrypted files", TestVaultLoadClearsAndExtractsAsync), ("Vault load restores encrypted DB snapshot", TestVaultLoadClearsAndExtractsAsync),
("Vault load wrong password does not modify vault files", TestVaultLoadWrongPasswordPreservesVaultAsync), ("Vault load wrong password does not modify vault files", TestVaultLoadWrongPasswordPreservesVaultAsync),
("Vault load ignores and removes legacy _init_vault.vault", TestVaultLoadLegacyInitVaultHandlingAsync), ("Vault load ignores legacy _init_vault.vault in snapshot mode", TestVaultLoadLegacyInitVaultHandlingAsync),
("Vault current-month save writes only current month and skips unchanged state", TestVaultCurrentMonthSaveOptimizedAsync), ("Vault rebuild remains repeatable across runs", TestVaultCurrentMonthSaveOptimizedAsync),
("Vault rebuild saves grouped monthly archives from decrypted files", TestVaultRebuildAllVaultsAsync), ("Vault rebuild writes encrypted DB snapshot vault files", TestVaultRebuildAllVaultsAsync),
("Vault clear data directory removes decrypted workspace artifacts", TestVaultClearDataDirectoryAsync), ("Vault clear data directory removes decrypted workspace artifacts", TestVaultClearDataDirectoryAsync),
("Parser extracts date from **Date:** marker", TestParserExtractsBoldDateAsync), ("Parser extracts date from **Date:** marker", TestParserExtractsBoldDateAsync),
("Parser extracts date from Date: marker", TestParserExtractsPlainDateAsync), ("Parser extracts date from Date: marker", TestParserExtractsPlainDateAsync),
@ -50,7 +50,7 @@ internal static partial class Program
("Database key derivation matches Python fixture", TestDatabaseKeyDerivationMatchesPythonAsync), ("Database key derivation matches Python fixture", TestDatabaseKeyDerivationMatchesPythonAsync),
("Database schema parity tables are created", TestDatabaseSchemaParityAsync), ("Database schema parity tables are created", TestDatabaseSchemaParityAsync),
("Entry db.status returns database compatibility payload", TestEntryDatabaseStatusAsync), ("Entry db.status returns database compatibility payload", TestEntryDatabaseStatusAsync),
("Entry db.initialize_schema creates schema in data directory", TestEntryDatabaseInitializeSchemaAsync), ("Entry db.initialize_schema initializes SQLCipher schema", TestEntryDatabaseInitializeSchemaAsync),
("Entry db.hydrate_workspace returns hydration metadata", TestEntryDatabaseHydrateWorkspaceAsync), ("Entry db.hydrate_workspace returns hydration metadata", TestEntryDatabaseHydrateWorkspaceAsync),
("Config service exposes parity path, vault, AI, and speech settings", TestConfigServiceParityKeysAsync), ("Config service exposes parity path, vault, AI, and speech settings", TestConfigServiceParityKeysAsync),
("Entry config.get returns config payload", TestEntryConfigGetAsync), ("Entry config.get returns config payload", TestEntryConfigGetAsync),
@ -70,10 +70,10 @@ internal static partial class Program
("Python sidecar speech service times out deterministically", TestPythonSidecarSpeechServiceTimeoutAsync), ("Python sidecar speech service times out deterministically", TestPythonSidecarSpeechServiceTimeoutAsync),
("Entry vault.load_all succeeds for empty vault directory", TestEntryVaultLoadAllEmptyAsync), ("Entry vault.load_all succeeds for empty vault directory", TestEntryVaultLoadAllEmptyAsync),
("Entry vault.load_all wrong password keeps database session locked", TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync), ("Entry vault.load_all wrong password keeps database session locked", TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync),
("Entry vault.clear_data_directory removes files", TestEntryVaultClearDataDirectoryAsync), ("Entry vault.clear_data_directory compatibility command succeeds", TestEntryVaultClearDataDirectoryAsync),
("Entry templates.save auto-syncs vault and survives reload", TestEntryTemplateSaveAutoSyncsVaultAsync), ("Entry templates.save auto-syncs vault and survives reload", TestEntryTemplateSaveAutoSyncsVaultAsync),
("Sidecar vault CLI load succeeds with --password", TestSidecarVaultCliLoadAsync), ("Sidecar vault CLI load succeeds with --password", TestSidecarVaultCliLoadAsync),
("Sidecar vault CLI save writes monthly vault with --password", TestSidecarVaultCliSaveAsync), ("Sidecar vault CLI save writes DB snapshot vault with --password", TestSidecarVaultCliSaveAsync),
("Sidecar search CLI returns matching entries with filters", TestSidecarSearchCliFilteredAsync), ("Sidecar search CLI returns matching entries with filters", TestSidecarSearchCliFilteredAsync),
("Sidecar search CLI warns when no decrypted entries exist", TestSidecarSearchCliEmptyDataAsync), ("Sidecar search CLI warns when no decrypted entries exist", TestSidecarSearchCliEmptyDataAsync),
("Transport fixtures produce stable envelopes", TestTransportFixturesAsync), ("Transport fixtures produce stable envelopes", TestTransportFixturesAsync),