Compare commits

..

No commits in common. "aa197230a690cdc87dfae1e7b214064eb2b90803" and "941cafba390207c4d21efdee90fb73df824f8cd7" have entirely different histories.

7 changed files with 819 additions and 699 deletions

View File

@ -1,5 +1,6 @@
global using System.ComponentModel.DataAnnotations; global using System.ComponentModel.DataAnnotations;
global using System.IO.Compression; global using System.IO.Compression;
global using System.Security.Cryptography;
global using System.Text.Json; global using System.Text.Json;
global using Journal.Core; global using Journal.Core;
global using Journal.Core.Dtos; global using Journal.Core.Dtos;

View File

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

View File

@ -47,386 +47,507 @@ internal static partial class Program
static async Task TestEntryEntriesSaveMergeAsync() static async Task TestEntryEntriesSaveMergeAsync()
{ {
var entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
var firstPath = await SaveEntryForTestAsync(entry, "2026-02-22", """ Directory.CreateDirectory(root);
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 mergeRequest = JsonSerializer.Serialize(new var entry = NewEntry();
{ var request = JsonSerializer.Serialize(new
action = "entries.save",
payload = new
{ {
filePath = firstPath, action = "entries.save",
mode = "Daily", payload = new
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 mergeResponse = await entry.HandleCommandAsync(mergeRequest); var response = await entry.HandleCommandAsync(request);
using var mergeDoc = JsonDocument.Parse(mergeResponse); using var doc = JsonDocument.Parse(response);
Assert(mergeDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save daily merge."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save.");
var loadedAfterMerge = await LoadEntryForTestAsync(entry, firstPath); var saved = File.ReadAllText(filePath);
Assert(loadedAfterMerge.Contains("new summary text", StringComparison.Ordinal), "Expected merged entry to contain new summary text."); Assert(saved.Contains("new summary text", StringComparison.Ordinal), "Expected merged file to contain new summary text.");
Assert(!loadedAfterMerge.Contains("old summary text", StringComparison.Ordinal), "Expected merged entry to replace old summary section."); Assert(!saved.Contains("old summary text", StringComparison.Ordinal), "Expected merged file to replace old summary section.");
Assert(loadedAfterMerge.Contains("new reflection text", StringComparison.Ordinal), "Expected merged entry to contain new reflection section."); Assert(saved.Contains("new reflection text", StringComparison.Ordinal), "Expected merged file to contain new reflection section.");
var fragmentSaveRequest = JsonSerializer.Serialize(new var fragmentSaveRequest = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{ {
filePath = firstPath, action = "entries.save",
mode = "Fragment", payload = new
content = "!NOTE\nfragment append text" {
} filePath,
}); mode = "Fragment",
content = "!NOTE\nfragment append text"
}
});
var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest); var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest);
using var fragmentDoc = JsonDocument.Parse(fragmentResponse); using var fragmentDoc = JsonDocument.Parse(fragmentResponse);
Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode."); Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode.");
var appended = File.ReadAllText(filePath);
var loadedAfterFragment = await LoadEntryForTestAsync(entry, firstPath); Assert(appended.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved file.");
Assert(loadedAfterFragment.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved entry."); }
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntryEntriesLoadAsync() static async Task TestEntryEntriesLoadAsync()
{ {
var entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
var content = """ Directory.CreateDirectory(root);
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
"""; """;
var filePath = await SaveEntryForTestAsync(entry, "2026-02-22", content); File.WriteAllText(filePath, content);
var request = JsonSerializer.Serialize(new var entry = NewEntry();
{ var request = JsonSerializer.Serialize(new
action = "entries.load",
payload = new
{ {
filePath action = "entries.load",
} payload = new
}); {
filePath
}
});
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 entries.load."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load.");
var data = doc.RootElement.GetProperty("data"); var data = doc.RootElement.GetProperty("data");
var entryDto = data.GetProperty("Entry"); var entryDto = data.GetProperty("Entry");
Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load."); 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(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."); Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntryEntriesListAsync() static async Task TestEntryEntriesListAsync()
{ {
var entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
await SaveEntryForTestAsync(entry, "2026-02-03", "c"); Directory.CreateDirectory(root);
await SaveEntryForTestAsync(entry, "2026-02-01", "a");
var request = JsonSerializer.Serialize(new try
{ {
action = "entries.list", File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "c");
payload = new { } File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "a");
}); File.WriteAllText(Path.Combine(root, "ignore.txt"), "x");
var response = await entry.HandleCommandAsync(request); var entry = NewEntry();
using var doc = JsonDocument.Parse(response); var request = JsonSerializer.Serialize(new
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list."); {
action = "entries.list",
payload = new
{
dataDirectory = root
}
});
var data = doc.RootElement.GetProperty("data"); var response = await entry.HandleCommandAsync(request);
Assert(data.ValueKind == JsonValueKind.Array, "Expected entries.list data array."); using var doc = JsonDocument.Parse(response);
Assert(data.GetArrayLength() == 2, "Expected entries.list to return seeded markdown entries."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
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."); var data = doc.RootElement.GetProperty("data");
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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-template-smoke", Guid.NewGuid().ToString("N"));
await SaveEntryForTestAsync(entry, "2026-02-03", "daily entry"); Directory.CreateDirectory(root);
var saveRequest = JsonSerializer.Serialize(new try
{ {
action = "templates.save", File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "daily entry");
payload = new
var entry = NewEntry();
var saveRequest = JsonSerializer.Serialize(new
{ {
name = "Weekly Review", action = "templates.save",
content = "# Weekly Review\n\n## Wins\n- one" payload = new
} {
}); name = "Weekly Review",
var saveResponse = await entry.HandleCommandAsync(saveRequest); content = "# Weekly Review\n\n## Wins\n- one",
using var saveDoc = JsonDocument.Parse(saveResponse); dataDirectory = root
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 templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? ""; var listTemplatesRequest = JsonSerializer.Serialize(new
Assert(templatePath.EndsWith("Weekly%20Review.template.md", StringComparison.OrdinalIgnoreCase), "Template path should be canonical db://template path.");
var listTemplatesRequest = JsonSerializer.Serialize(new
{
action = "templates.list",
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 action = "templates.list",
} payload = new
}); {
var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest); dataDirectory = root
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() ?? ""; var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest);
Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result."); 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 deleteTemplateRequest = JsonSerializer.Serialize(new var listEntriesRequest = JsonSerializer.Serialize(new
{
action = "templates.delete",
payload = new
{ {
filePath = templatePath action = "entries.list",
} payload = new
}); {
var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest); dataDirectory = root
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."); 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))
Directory.Delete(root, recursive: true);
}
} }
static async Task TestEntrySearchEntriesMatchesRawContentAsync() static async Task TestEntrySearchEntriesMatchesRawContentAsync()
{ {
var entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "## Summary\nAlpha line\ncommon token");
payload = new File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "## Summary\nbeta line\nCOMMON token");
File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "## Summary\ngamma only");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
query = "common token", action = "search.entries",
} 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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "one");
payload = new { } File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "two");
}); File.WriteAllText(Path.Combine(root, "ignore.txt"), "not markdown");
var response = await entry.HandleCommandAsync(request); var entry = NewEntry();
using var doc = JsonDocument.Parse(response); var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
}
});
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries without query."); var response = await entry.HandleCommandAsync(request);
var data = doc.RootElement.GetProperty("data"); using var doc = JsonDocument.Parse(response);
Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
Assert(data.GetArrayLength() == 3, "Expected all seeded markdown entries to be returned when query is omitted."); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries without query.");
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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", WriteSearchFixtureFiles(root);
payload = new var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
startDate = "2026-02-02", action = "search.entries",
endDate = "2026-02-28", payload = new
} {
}); 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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", WriteSearchFixtureFiles(root);
payload = new var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
query = "focus area", action = "search.entries",
section = "Reflection", payload = new
} {
}); 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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", WriteSearchFixtureFiles(root);
payload = new var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
tags = new[] { "stress" }, action = "search.entries",
types = new[] { "!TRIGGER" }, payload = new
} {
}); 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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", WriteSearchFixtureFiles(root);
payload = new var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
@checked = new[] { "med taken" }, action = "search.entries",
@unchecked = new[] { "drink water" }, payload = new
} {
}); 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 entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
await SeedSearchFixtureEntriesAsync(entry); Directory.CreateDirectory(root);
var request = JsonSerializer.Serialize(new try
{ {
action = "search.entries", WriteSearchFixtureFiles(root);
payload = new var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{ {
startDate = "2026/02/01", action = "search.entries",
} 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 config = NewConfigService(root); var dataDir = Path.Combine(root, "data");
var dbService = new JournalDatabaseService(config); Directory.CreateDirectory(dataDir);
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
{ {
entryFiles.SaveEntry(new EntrySavePayload(""" WriteSearchFixtureFiles(dataDir);
Date: 2026-02-01 var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
## 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",
"--start-date", "2026-02-01", "--data-dir", dataDir,
"--end-date", "2026-02-28", "--start-date", "2026-02-01",
"--tag", "stress", "--end-date", "2026-02-28",
"--type", "!TRIGGER", "--tag", "stress",
"--checked", "med taken", "--type", "!TRIGGER",
"--section", "Summary" "--checked", "med taken",
"--section", "Summary"
])); ]));
Assert(exitCode == 0, "Expected search CLI command to succeed."); Assert(exitCode == 0, "Expected search CLI command to succeed.");
@ -436,7 +557,6 @@ non match
} }
finally finally
{ {
session.Dispose();
if (Directory.Exists(root)) if (Directory.Exists(root))
Directory.Delete(root, recursive: true); Directory.Delete(root, recursive: true);
} }
@ -447,27 +567,19 @@ non match
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 config = NewConfigService(root); var dataDir = Path.Combine(root, "data");
var dbService = new JournalDatabaseService(config); Directory.CreateDirectory(dataDir);
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 (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand([])); var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand(["--data-dir", dataDir]));
Assert(exitCode == 0, "Expected search CLI command to return success for empty data store."); Assert(exitCode == 0, "Expected search CLI command to return success for empty data directory.");
Assert(stdout.Contains("No journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message."); Assert(stdout.Contains("No decrypted 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);
} }
@ -477,6 +589,7 @@ non match
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);
@ -494,31 +607,26 @@ non match
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"));
var config = NewConfigService(root); Directory.CreateDirectory(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); var result = service.SaveEntry(payload, root);
Assert(result.FilePath.StartsWith("db://entry/", StringComparison.OrdinalIgnoreCase), "Expected canonical db://entry path."); var expectedPath = Path.GetFullPath(Path.Combine(root, "My Custom Name.md"));
Assert(result.FilePath.Contains("My%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), "Expected escaped custom name in db path."); Assert(result.FilePath == expectedPath, $"Expected path '{expectedPath}' but got '{result.FilePath}'.");
Assert(File.Exists(expectedPath), "Custom-named file should exist on disk.");
var loaded = service.LoadEntry(result.FilePath); Assert(File.ReadAllText(expectedPath).Contains("Hello world"), "File content should match.");
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
{ {
@ -536,87 +644,14 @@ non match
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%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), Assert(savedFilePath.Contains("Another Custom Name.md", StringComparison.OrdinalIgnoreCase),
$"Expected file path to contain 'Another%20Custom%20Name.md' but got '{savedFilePath}'."); $"Expected file path to contain 'Another Custom Name.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,19 +53,20 @@ internal static partial class Program
try try
{ {
var configService = NewConfigService(tempRoot); // Set up encrypted DB session
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); session1.SetPassword(password, dataDir);
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); session2.SetPassword(password, dataDir);
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,64 +6,26 @@ internal static partial class Program
return new FragmentService(repo); return new FragmentService(repo);
} }
static Entry NewEntry(bool unlocked = true, string password = "vault-pass-123", string? root = null) static Entry NewEntry()
{ {
var config = NewConfigService(root); var dbService = new JournalDatabaseService(new JournalConfigService());
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(entryRepo), new EntrySearchService(),
new VaultStorageService(new VaultCryptoService(), dbService), new VaultStorageService(new VaultCryptoService()),
dbService, dbService,
session, session,
config, new JournalConfigService(),
new DisabledAiService("none"), new DisabledAiService("none"),
new DisabledSpeechBridgeService("none"), new DisabledSpeechBridgeService("none"),
new EntryFileService(entryRepo), new EntryFileService(new DiskEntryFileRepository()),
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 Entry NewLockedEntry() => NewEntry(unlocked: false); static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(new JournalConfigService());
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,53 +41,40 @@ internal static partial class Program
static Task TestVaultMonthlyFilenameParityAsync() static Task TestVaultMonthlyFilenameParityAsync()
{ {
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); IVaultStorageService vaultStorage = new VaultStorageService(new VaultCryptoService());
var config = NewConfigService(root); var name = vaultStorage.GetMonthlyVaultFileName(new DateTime(2026, 2, 7));
var dbService = new JournalDatabaseService(config); Assert(name == "2026-02.vault", "Monthly vault filename must match yyyy-MM.vault format.");
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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
var dbPath = dbService.GetDatabasePath(); File.WriteAllText(Path.Combine(dataDir, "old_file.md"), "stale");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
var originalBytes = Guid.NewGuid().ToByteArray();
File.WriteAllBytes(dbPath, originalBytes);
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); var zipBytes = CreateZipBytes(new Dictionary<string, string>
{
["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);
File.WriteAllBytes(dbPath, [1, 2, 3, 4]); IVaultStorageService storage = new VaultStorageService(crypto);
var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
Assert(ok, "Expected vault load success with correct password."); Assert(ok, "Expected vault load success with correct password.");
var loaded = File.ReadAllBytes(dbPath); Assert(!File.Exists(Path.Combine(dataDir, "old_file.md")), "Data directory should be cleared before extraction.");
Assert(loaded.SequenceEqual(originalBytes), "Expected DB file bytes restored from vault snapshot."); var extractedPath = Path.Combine(dataDir, "2026-02-01.md");
Assert(File.Exists(extractedPath), "Expected markdown file extracted from vault archive.");
Assert(File.ReadAllText(extractedPath) == "hello from vault", "Extracted file content mismatch.");
} }
finally finally
{ {
@ -101,21 +88,25 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
var dbPath = dbService.GetDatabasePath(); var zipBytes = CreateZipBytes(new Dictionary<string, string>
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); {
File.WriteAllText(dbPath, "db payload"); ["2026-02-01.md"] = "hello from vault"
});
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); var crypto = new VaultCryptoService();
var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault"); var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
var vaultPath = Path.Combine(vaultDir, "2026-02.vault");
File.WriteAllBytes(vaultPath, encrypted);
var before = File.ReadAllBytes(vaultPath); var before = File.ReadAllBytes(vaultPath);
var ok = storage.LoadAllVaults("wrong-password", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); IVaultStorageService storage = new VaultStorageService(crypto);
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.");
@ -133,19 +124,22 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
var legacyPath = Path.Combine(config.Current.VaultDirectory, "_init_vault.vault"); var legacyPath = Path.Combine(vaultDir, "_init_vault.vault");
File.WriteAllBytes(legacyPath, [1, 2, 3, 4]); File.WriteAllBytes(legacyPath, [1, 2, 3, 4]);
var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbService.GetDatabasePath())!); IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
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 ignored in SQLCipher snapshot mode."); Assert(!File.Exists(legacyPath), "Legacy _init_vault.vault should be removed during load.");
Assert(Directory.Exists(dataDir), "Data directory should exist after load workflow.");
} }
finally finally
{ {
@ -159,25 +153,43 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
var dbPath = dbService.GetDatabasePath(); File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb one");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two");
File.WriteAllText(dbPath, "initial db"); File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan one");
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault"); var now = new DateTime(2026, 2, 22, 12, 0, 0, DateTimeKind.Utc);
Assert(File.Exists(vaultPath), "Expected DB vault snapshot file to be created.");
var firstLength = new FileInfo(vaultPath).Length; var firstSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); Assert(firstSaved, "Expected first current-month save to write vault data.");
var secondLength = new FileInfo(vaultPath).Length;
Assert(firstLength > 0 && secondLength > 0, "Vault snapshot should remain non-empty across repeated rebuilds."); var febVaultPath = Path.Combine(vaultDir, "2026-02.vault");
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
{ {
@ -191,21 +203,30 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!; File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan body");
Directory.CreateDirectory(dbDir); File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb body");
File.WriteAllText(Path.Combine(dbDir, "journal_cache.db"), "primary"); File.WriteAllText(Path.Combine(dataDir, "not-a-journal.md"), "should be ignored");
File.WriteAllText(Path.Combine(dbDir, "analytics.db"), "secondary");
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir); IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected primary DB snapshot vault."); var janVaultPath = Path.Combine(vaultDir, "2026-01.vault");
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_analytics.db.vault")), "Expected secondary DB snapshot vault."); var febVaultPath = Path.Combine(vaultDir, "2026-02.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
{ {
@ -219,22 +240,21 @@ 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 config = NewConfigService(root); var dataDir = Path.Combine(root, "data");
var dbService = new JournalDatabaseService(config); Directory.CreateDirectory(dataDir);
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(Path.Combine(dataDir, "nested"));
var scratchDir = Path.Combine(root, "scratch-data");
Directory.CreateDirectory(scratchDir);
Directory.CreateDirectory(Path.Combine(scratchDir, "nested"));
try try
{ {
File.WriteAllText(Path.Combine(scratchDir, "tmp.md"), "decrypted content"); File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "decrypted content");
File.WriteAllText(Path.Combine(scratchDir, "nested", "tmp.txt"), "temp"); File.WriteAllText(Path.Combine(dataDir, "journal_cache.db"), "cache");
File.WriteAllText(Path.Combine(dataDir, "nested", "tmp.txt"), "temp");
storage.ClearDataDirectory(scratchDir); IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
storage.ClearDataDirectory(dataDir);
Assert(!Directory.Exists(scratchDir), "Non-db scratch directory should be deleted by clear_data_directory."); Assert(Directory.Exists(dataDir), "Data directory should be recreated after cleanup.");
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after cleanup.");
} }
finally finally
{ {
@ -248,28 +268,30 @@ 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"));
Directory.CreateDirectory(root); var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
try try
{ {
var entry = NewLockedEntry(); var entry = NewEntry();
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 = Path.Combine(root, "vault") vaultDirectory = vaultDir,
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
{ {
@ -280,44 +302,52 @@ internal static partial class Program
static async Task TestEntryVaultClearDataDirectoryAsync() static async Task TestEntryVaultClearDataDirectoryAsync()
{ {
var entry = NewEntry(); var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
var request = JsonSerializer.Serialize(new var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(dataDir);
File.WriteAllText(Path.Combine(dataDir, "tmp.md"), "x");
try
{ {
action = "vault.clear_data_directory", var entry = NewEntry();
payload = new { } var request = JsonSerializer.Serialize(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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
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 exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir]); var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory."); var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
var dbDir = Path.Combine(config.Current.VaultDirectory, "db"); Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory.");
Assert(Directory.Exists(dbDir), "Expected db directory to be created by vault load CLI command."); Assert(Directory.Exists(dataDir), "Expected data directory to be created by vault load CLI command.");
} }
finally finally
{ {
@ -331,29 +361,20 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
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
{ {
entryFiles.SaveEntry(new EntrySavePayload("entry body", Mode: "Overwrite", FileName: "2026-02-22")); File.WriteAllText(Path.Combine(dataDir, "2026-02-22.md"), "entry body");
session.CloseConnection();
var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir]); var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
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, "_db_journal_cache.db.vault")), "Expected DB vault snapshot file to be written by save CLI command."); Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault file to be written by save CLI command.");
} }
finally finally
{ {
@ -367,46 +388,41 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var storage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try try
{ {
using (var seedSession = new DatabaseSessionService(dbService)) // Create both date-named and custom-named entries
{ File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "date entry");
seedSession.SetPassword("vault-pass-123"); File.WriteAllText(Path.Combine(dataDir, "My Custom Entry.md"), "custom entry body");
var seedRepo = new SqliteEntryFileRepository(seedSession); File.WriteAllText(Path.Combine(dataDir, "Work Notes.md"), "work notes body");
var seedEntryFiles = new EntryFileService(seedRepo);
seedEntryFiles.SaveEntry(new EntrySavePayload("date entry", Mode: "Overwrite", FileName: "2026-02-01")); // Rebuild vaults (simulates app close)
seedEntryFiles.SaveEntry(new EntrySavePayload("custom entry body", Mode: "Overwrite", FileName: "My Custom Entry")); IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
seedEntryFiles.SaveEntry(new EntrySavePayload("work notes body", Mode: "Overwrite", FileName: "Work Notes")); storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
seedSession.CloseConnection();
}
var dbPath = dbService.GetDatabasePath(); // Verify custom vault was created
var dbDir = Path.GetDirectoryName(dbPath)!; var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault");
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.");
storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir); // Clear data directory (simulates app close step 2)
Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected DB vault snapshot to be created."); storage.ClearDataDirectory(dataDir);
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after clear.");
File.Delete(dbPath); // Load vaults (simulates app restart)
Assert(!File.Exists(dbPath), "DB file should be deleted before restore."); var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
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.");
using (var verifySession = new DatabaseSessionService(dbService)) // Verify all entries are restored
{ Assert(File.Exists(Path.Combine(dataDir, "2026-02-01.md")), "Date entry should be restored from vault.");
verifySession.SetPassword("vault-pass-123"); Assert(File.Exists(Path.Combine(dataDir, "My Custom Entry.md")), "Custom entry should be restored from vault.");
var verifyRepo = new SqliteEntryFileRepository(verifySession); Assert(File.Exists(Path.Combine(dataDir, "Work Notes.md")), "Second custom entry should be restored from vault.");
var verifyEntryFiles = new EntryFileService(verifyRepo); Assert(File.ReadAllText(Path.Combine(dataDir, "My Custom Entry.md")) == "custom entry body", "Custom entry content mismatch.");
var allEntries = verifyEntryFiles.ListEntries(); Assert(File.ReadAllText(Path.Combine(dataDir, "Work Notes.md")) == "work notes body", "Second custom entry content mismatch.");
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
{ {
@ -420,34 +436,22 @@ 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 config = NewConfigService(root); var vaultDir = Path.Combine(root, "vault");
var dbService = new JournalDatabaseService(config); var dataDir = Path.Combine(root, "data");
var vaultStorage = new VaultStorageService(new VaultCryptoService(), dbService); Directory.CreateDirectory(vaultDir);
var session = new DatabaseSessionService(dbService); Directory.CreateDirectory(dataDir);
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 dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!; var zipBytes = CreateZipBytes(new Dictionary<string, string>
Directory.CreateDirectory(dbDir); {
File.WriteAllBytes(dbService.GetDatabasePath(), Guid.NewGuid().ToByteArray()); ["2026-02-01.md"] = "hello from vault"
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
{ {
@ -455,7 +459,8 @@ internal static partial class Program
payload = new payload = new
{ {
password = "wrong-password", password = "wrong-password",
vaultDirectory = config.Current.VaultDirectory vaultDirectory = vaultDir,
dataDirectory = dataDir
} }
}); });
var loadResponse = await entry.HandleCommandAsync(loadRequest); var loadResponse = await entry.HandleCommandAsync(loadRequest);
@ -483,31 +488,89 @@ 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 entry = NewEntry(root: root); var projectRoot = Path.Combine(root, "project");
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 configResponse = await entry.HandleCommandAsync("""{"action":"config.get"}"""); var previousProjectRoot = Environment.GetEnvironmentVariable("JOURNAL_PROJECT_ROOT");
using var configDoc = JsonDocument.Parse(configResponse); var previousDataDir = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR");
var configData = configDoc.RootElement.GetProperty("data"); var previousVaultDir = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR");
var vaultDir = configData.GetProperty("VaultDirectory").GetString() ?? ""; Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", projectRoot);
var dbFilename = configData.GetProperty("DatabaseFilename").GetString() ?? "journal_cache.db"; Environment.SetEnvironmentVariable("JOURNAL_DATA_DIR", dataDir);
Environment.SetEnvironmentVariable("JOURNAL_VAULT_DIR", vaultDir);
var saveTemplateRequest = JsonSerializer.Serialize(new try
{ {
action = "templates.save", var entry = NewEntry();
payload = new var password = "vault-pass-123";
var unlockRequest = JsonSerializer.Serialize(new
{ {
name = "Weekly Review", action = "vault.load_all",
content = "## Wins\n- shipped feature" payload = new
} {
}); password,
var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest); vaultDirectory = vaultDir,
using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse); dataDirectory = dataDir
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 dbVaultPath = Path.Combine(vaultDir, $"_db_{dbFilename}.vault"); var saveTemplateRequest = JsonSerializer.Serialize(new
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.");
if (Directory.Exists(root)) var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault");
Directory.Delete(root, recursive: true); Assert(File.Exists(customVaultPath), "Expected template save to auto-sync custom entries vault.");
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 restores encrypted DB snapshot", TestVaultLoadClearsAndExtractsAsync), ("Vault load clears workspace and extracts decrypted files", TestVaultLoadClearsAndExtractsAsync),
("Vault load wrong password does not modify vault files", TestVaultLoadWrongPasswordPreservesVaultAsync), ("Vault load wrong password does not modify vault files", TestVaultLoadWrongPasswordPreservesVaultAsync),
("Vault load ignores legacy _init_vault.vault in snapshot mode", TestVaultLoadLegacyInitVaultHandlingAsync), ("Vault load ignores and removes legacy _init_vault.vault", TestVaultLoadLegacyInitVaultHandlingAsync),
("Vault rebuild remains repeatable across runs", TestVaultCurrentMonthSaveOptimizedAsync), ("Vault current-month save writes only current month and skips unchanged state", TestVaultCurrentMonthSaveOptimizedAsync),
("Vault rebuild writes encrypted DB snapshot vault files", TestVaultRebuildAllVaultsAsync), ("Vault rebuild saves grouped monthly archives from decrypted 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 initializes SQLCipher schema", TestEntryDatabaseInitializeSchemaAsync), ("Entry db.initialize_schema creates schema in data directory", 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 compatibility command succeeds", TestEntryVaultClearDataDirectoryAsync), ("Entry vault.clear_data_directory removes files", 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 DB snapshot vault with --password", TestSidecarVaultCliSaveAsync), ("Sidecar vault CLI save writes monthly 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),