From 4fd3c5b5f12448e6bf1b60f16a2dea88fc92495f Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 28 Feb 2026 18:58:44 -0600 Subject: [PATCH] Refactor smoke tests for SQLCipher-first backend contract --- .../Program.DatabaseConfigTests.cs | 202 ++--- Journal.SmokeTests/Program.EntryTests.cs | 769 +++++++++--------- Journal.SmokeTests/Program.FragmentTests.cs | 7 +- Journal.SmokeTests/Program.Shared.cs | 52 +- Journal.SmokeTests/Program.VaultTests.cs | 477 +++++------ Journal.SmokeTests/Program.cs | 14 +- 6 files changed, 701 insertions(+), 820 deletions(-) diff --git a/Journal.SmokeTests/Program.DatabaseConfigTests.cs b/Journal.SmokeTests/Program.DatabaseConfigTests.cs index 2b51b89..5ddcf9d 100644 --- a/Journal.SmokeTests/Program.DatabaseConfigTests.cs +++ b/Journal.SmokeTests/Program.DatabaseConfigTests.cs @@ -11,165 +11,108 @@ internal static partial class Program static Task TestDatabaseSchemaParityAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var service = NewDatabaseService(); + var statements = service.GetSchemaStatements(); + var tableNames = new HashSet(statements.Keys, StringComparer.OrdinalIgnoreCase); - try - { - var service = NewDatabaseService(); - var schemaPath = service.WriteSchemaBootstrap(root); - var statements = service.GetSchemaStatements(); - var tableNames = new HashSet(statements.Keys, StringComparer.OrdinalIgnoreCase); + Assert(tableNames.Contains("entries"), "Schema should contain entries table."); + Assert(tableNames.Contains("sections"), "Schema should contain sections table."); + Assert(tableNames.Contains("fragments"), "Schema should contain fragments table."); + Assert(tableNames.Contains("tags"), "Schema should contain tags table."); + Assert(tableNames.Contains("fragment_tags"), "Schema should contain fragment_tags table."); + Assert(tableNames.Contains("entry_documents"), "Schema should contain entry_documents table."); - Assert(tableNames.Contains("entries"), "Schema should contain entries table."); - Assert(tableNames.Contains("sections"), "Schema should contain sections table."); - Assert(tableNames.Contains("fragments"), "Schema should contain fragments table."); - 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); - } + 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."); return Task.CompletedTask; } static async Task TestEntryDatabaseStatusAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - - try + var entry = NewEntry(unlocked: false); + var request = JsonSerializer.Serialize(new { - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "db.status", + payload = new { - action = "db.status", - payload = new - { - password = "vault-pass-123", - dataDirectory = root - } - }); + password = "vault-pass-123" + } + }); - 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); - } + 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("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in db.status payload."); + Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected SQLCipher runtime-ready status in db.status payload."); } static async Task TestEntryDatabaseInitializeSchemaAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - - try + var entry = NewEntry(unlocked: false); + var request = JsonSerializer.Serialize(new { - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "db.initialize_schema", + payload = new { - action = "db.initialize_schema", - payload = new - { - password = "vault-pass-123", - dataDirectory = root - } - }); + password = "vault-pass-123" + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.initialize_schema."); - var data = doc.RootElement.GetProperty("data"); - Assert(data.TryGetProperty("schemaPath", out var schemaPath), "Expected schemaPath in db.initialize_schema response."); - Assert(schemaPath.ValueKind == JsonValueKind.String, "Expected string schemaPath value."); - var resolvedPath = schemaPath.GetString() ?? ""; - Assert(File.Exists(resolvedPath), "db.initialize_schema should write schema bootstrap file."); - 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); - } + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.initialize_schema."); + + var data = doc.RootElement.GetProperty("data"); + Assert(data.TryGetProperty("initialized", out var initialized), "Expected initialized flag in db.initialize_schema response."); + Assert(initialized.ValueKind == JsonValueKind.True, "Expected initialized=true from db.initialize_schema."); + Assert(data.TryGetProperty("databasePath", out var databasePath), "Expected databasePath in db.initialize_schema response."); + Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty databasePath from db.initialize_schema."); } static async Task TestEntryDatabaseHydrateWorkspaceAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - - try + var entry = NewEntry(unlocked: false); + var request = JsonSerializer.Serialize(new { - File.WriteAllText(Path.Combine(root, "2026-02-20.md"), "one"); - File.WriteAllText(Path.Combine(root, "2026-02-21.md"), "two"); - - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "db.hydrate_workspace", + payload = new { - action = "db.hydrate_workspace", - payload = new - { - password = "vault-pass-123", - dataDirectory = root - } - }); + password = "vault-pass-123" + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.hydrate_workspace."); - var data = doc.RootElement.GetProperty("data"); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.hydrate_workspace."); + var data = doc.RootElement.GetProperty("data"); - 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(data.TryGetProperty("SchemaBootstrapPath", out var schemaPath), "Expected SchemaBootstrapPath in hydrate payload."); - Assert(schemaPath.ValueKind == JsonValueKind.String && File.Exists(schemaPath.GetString()), "Expected hydrate to write schema bootstrap file."); - Assert(data.TryGetProperty("EntryFilesProcessed", out var filesProcessed), "Expected EntryFilesProcessed in hydrate payload."); - 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); - } + 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(data.TryGetProperty("EntryFilesProcessed", out var filesProcessed), "Expected EntryFilesProcessed in hydrate payload."); + Assert(filesProcessed.ValueKind == JsonValueKind.Number && filesProcessed.GetInt32() >= 0, "Expected non-negative EntryFilesProcessed in hydrate payload."); + 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."); } static Task TestConfigServiceParityKeysAsync() { - IJournalConfigService config = new JournalConfigService(); + IJournalConfigService config = NewConfigService(); var current = config.Current; 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.DataDirectory), "Config DataDirectory should not be empty."); Assert(!string.IsNullOrWhiteSpace(current.VaultDirectory), "Config VaultDirectory should not be empty."); - Assert(current.MonthlyVaultFormat == "%Y-%m.vault", "Config MonthlyVaultFormat should match Python format token."); + Assert(!string.IsNullOrWhiteSpace(current.DatabaseFilename), "Config DatabaseFilename should not be empty."); Assert(current.LlamaCppUrl == "http://127.0.0.1:8085/v1/completions", "Config LlamaCppUrl default mismatch."); Assert(current.LlamaCppModel == "qwen/qwen3-4b", "Config LlamaCppModel default mismatch."); @@ -194,10 +137,10 @@ internal static partial class Program var data = doc.RootElement.GetProperty("data"); Assert(data.ValueKind == JsonValueKind.Object, "Expected object payload for config.get."); - Assert(data.TryGetProperty("DataDirectory", out var dataDirectory), "Expected DataDirectory in config payload."); - Assert(dataDirectory.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(dataDirectory.GetString()), "Expected non-empty DataDirectory value."); - Assert(data.TryGetProperty("MonthlyVaultFormat", out var monthlyVaultFormat), "Expected MonthlyVaultFormat in config payload."); - Assert(monthlyVaultFormat.GetString() == "%Y-%m.vault", "Expected Python-compatible MonthlyVaultFormat value."); + Assert(data.TryGetProperty("VaultDirectory", out var vaultDirectory), "Expected VaultDirectory in config payload."); + Assert(vaultDirectory.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(vaultDirectory.GetString()), "Expected non-empty VaultDirectory value."); + Assert(data.TryGetProperty("DatabaseFilename", out var databaseFilename), "Expected DatabaseFilename in config payload."); + Assert(databaseFilename.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databaseFilename.GetString()), "Expected non-empty DatabaseFilename value."); Assert(data.TryGetProperty("LlamaCppUrl", out _), "Expected LlamaCppUrl in config payload."); Assert(data.TryGetProperty("SpeechRecognitionEngine", out _), "Expected SpeechRecognitionEngine in config payload."); } @@ -233,7 +176,7 @@ internal static partial class Program { action = "entries.save", mode = "Daily", - filePath = "E:/journal/2026-02-24.md" + filePath = "db://entry/2026-02-24.md" }); var redacted = LogRedactor.RedactPayload(payload); @@ -246,4 +189,3 @@ internal static partial class Program return Task.CompletedTask; } } - diff --git a/Journal.SmokeTests/Program.EntryTests.cs b/Journal.SmokeTests/Program.EntryTests.cs index ad8da7d..4671959 100644 --- a/Journal.SmokeTests/Program.EntryTests.cs +++ b/Journal.SmokeTests/Program.EntryTests.cs @@ -47,507 +47,386 @@ internal static partial class Program static async Task TestEntryEntriesSaveMergeAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - - try - { - var filePath = Path.Combine(root, "2026-02-22.md"); - File.WriteAllText(filePath, """ + var entry = NewEntry(); + var firstPath = await SaveEntryForTestAsync(entry, "2026-02-22", """ Date: 2026-02-22 ## Summary old summary text """); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + var mergeRequest = JsonSerializer.Serialize(new + { + action = "entries.save", + payload = new { - action = "entries.save", - payload = new - { - filePath, - mode = "Daily", - content = """ + filePath = firstPath, + mode = "Daily", + content = """ Date: 2026-02-22 ## Summary new summary text ## Reflection new reflection text """ - } - }); + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save."); + var mergeResponse = await entry.HandleCommandAsync(mergeRequest); + using var mergeDoc = JsonDocument.Parse(mergeResponse); + Assert(mergeDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save daily merge."); - var saved = File.ReadAllText(filePath); - Assert(saved.Contains("new summary text", StringComparison.Ordinal), "Expected merged file to contain new summary text."); - Assert(!saved.Contains("old summary text", StringComparison.Ordinal), "Expected merged file to replace old summary section."); - Assert(saved.Contains("new reflection text", StringComparison.Ordinal), "Expected merged file to contain new reflection section."); + var loadedAfterMerge = await LoadEntryForTestAsync(entry, firstPath); + Assert(loadedAfterMerge.Contains("new summary text", StringComparison.Ordinal), "Expected merged entry to contain new summary text."); + Assert(!loadedAfterMerge.Contains("old summary text", StringComparison.Ordinal), "Expected merged entry to replace old summary section."); + Assert(loadedAfterMerge.Contains("new reflection text", StringComparison.Ordinal), "Expected merged entry to contain new reflection section."); - var fragmentSaveRequest = JsonSerializer.Serialize(new - { - action = "entries.save", - payload = new - { - filePath, - mode = "Fragment", - content = "!NOTE\nfragment append text" - } - }); - - var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest); - using var fragmentDoc = JsonDocument.Parse(fragmentResponse); - Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode."); - var appended = File.ReadAllText(filePath); - Assert(appended.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved file."); - } - finally + var fragmentSaveRequest = JsonSerializer.Serialize(new { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + action = "entries.save", + payload = new + { + filePath = firstPath, + mode = "Fragment", + content = "!NOTE\nfragment append text" + } + }); + + var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest); + using var fragmentDoc = JsonDocument.Parse(fragmentResponse); + Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode."); + + var loadedAfterFragment = await LoadEntryForTestAsync(entry, firstPath); + Assert(loadedAfterFragment.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved entry."); } static async Task TestEntryEntriesLoadAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - - try - { - var filePath = Path.Combine(root, "2026-02-22.md"); - var content = """ + var entry = NewEntry(); + var content = """ Date: 2026-02-22 ## Summary hello world """; - File.WriteAllText(filePath, content); + var filePath = await SaveEntryForTestAsync(entry, "2026-02-22", content); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new - { - action = "entries.load", - payload = new - { - filePath - } - }); - - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load."); - - var data = doc.RootElement.GetProperty("data"); - var entryDto = data.GetProperty("Entry"); - Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load."); - Assert(entryDto.GetProperty("RawContent").GetString() == content, "Expected raw content from entries.load."); - Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load."); - } - finally + var request = JsonSerializer.Serialize(new { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + action = "entries.load", + payload = new + { + filePath + } + }); + + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load."); + + var data = doc.RootElement.GetProperty("data"); + var entryDto = data.GetProperty("Entry"); + Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load."); + Assert(entryDto.GetProperty("RawContent").GetString() == content, "Expected raw content from entries.load."); + Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load."); } static async Task TestEntryEntriesListAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SaveEntryForTestAsync(entry, "2026-02-03", "c"); + await SaveEntryForTestAsync(entry, "2026-02-01", "a"); - try + var request = JsonSerializer.Serialize(new { - File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "c"); - File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "a"); - File.WriteAllText(Path.Combine(root, "ignore.txt"), "x"); + action = "entries.list", + payload = new { } + }); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new - { - action = "entries.list", - payload = new - { - dataDirectory = root - } - }); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list."); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list."); - - 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); - } + 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 seeded markdown entries."); + 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."); } static async Task TestEntryTemplatesCrudExcludesFromEntriesListAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-template-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SaveEntryForTestAsync(entry, "2026-02-03", "daily entry"); - try + var saveRequest = JsonSerializer.Serialize(new { - File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "daily entry"); - - var entry = NewEntry(); - var saveRequest = JsonSerializer.Serialize(new + action = "templates.save", + payload = new { - action = "templates.save", - payload = new - { - name = "Weekly Review", - content = "# Weekly Review\n\n## Wins\n- one", - dataDirectory = root - } - }); - 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."); + name = "Weekly Review", + content = "# Weekly Review\n\n## Wins\n- one" + } + }); + 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 listTemplatesRequest = JsonSerializer.Serialize(new - { - action = "templates.list", - payload = new - { - dataDirectory = root - } - }); - var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest); - using var listTemplatesDoc = JsonDocument.Parse(listTemplatesResponse); - Assert(listTemplatesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.list."); - var templateItems = listTemplatesDoc.RootElement.GetProperty("data"); - Assert(templateItems.GetArrayLength() == 1, "Expected one template in templates.list."); - Assert(templateItems[0].GetProperty("FileName").GetString() == "Weekly Review.template.md", "Template file name mismatch."); + var templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? ""; + Assert(templatePath.EndsWith("Weekly%20Review.template.md", StringComparison.OrdinalIgnoreCase), "Template path should be canonical db://template path."); - var listEntriesRequest = JsonSerializer.Serialize(new - { - action = "entries.list", - payload = new - { - dataDirectory = root - } - }); - var listEntriesResponse = await entry.HandleCommandAsync(listEntriesRequest); - using var listEntriesDoc = JsonDocument.Parse(listEntriesResponse); - Assert(listEntriesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list."); - var entryItems = listEntriesDoc.RootElement.GetProperty("data"); - Assert(entryItems.GetArrayLength() == 1, "Expected entries.list to exclude template files."); - Assert(entryItems[0].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected only daily entry file in entries.list."); - - var loadTemplateRequest = JsonSerializer.Serialize(new - { - action = "templates.load", - payload = new - { - filePath = templatePath - } - }); - var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest); - using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse); - Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load."); - var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? ""; - Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result."); - - var deleteTemplateRequest = JsonSerializer.Serialize(new - { - action = "templates.delete", - payload = new - { - filePath = templatePath - } - }); - var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest); - using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse); - Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete."); - Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true."); - Assert(!File.Exists(templatePath), "Template file should be deleted."); - } - finally + var listTemplatesRequest = JsonSerializer.Serialize(new { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + 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 + } + }); + var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest); + using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse); + Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load."); + var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? ""; + Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result."); + + var deleteTemplateRequest = JsonSerializer.Serialize(new + { + action = "templates.delete", + payload = new + { + filePath = templatePath + } + }); + var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest); + using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse); + Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete."); + Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true."); } static async Task TestEntrySearchEntriesMatchesRawContentAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "## Summary\nAlpha line\ncommon token"); - 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 + action = "search.entries", + payload = new { - action = "search.entries", - payload = new - { - dataDirectory = root, - query = "common token", - } - }); + query = "common token", + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries."); - var data = doc.RootElement.GetProperty("data"); - Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries."); - Assert(data.GetArrayLength() == 2, "Expected two entries matching query across raw content."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries."); + var data = doc.RootElement.GetProperty("data"); + Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries."); + Assert(data.GetArrayLength() == 2, "Expected two entries matching query across raw content."); } static async Task TestEntrySearchEntriesWithoutQueryReturnsAllAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "one"); - File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "two"); - File.WriteAllText(Path.Combine(root, "ignore.txt"), "not markdown"); + action = "search.entries", + payload = new { } + }); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new - { - action = "search.entries", - payload = new - { - dataDirectory = root, - } - }); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); - - 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); - } + 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() == 3, "Expected all seeded markdown entries to be returned when query is omitted."); } static async Task TestEntrySearchEntriesDateRangeFilterAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - WriteSearchFixtureFiles(root); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "search.entries", + payload = new { - action = "search.entries", - payload = new - { - dataDirectory = root, - startDate = "2026-02-02", - endDate = "2026-02-28", - } - }); + startDate = "2026-02-02", + endDate = "2026-02-28", + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for date-range filtered search."); - var data = doc.RootElement.GetProperty("data"); - 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."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for date-range filtered search."); + var data = doc.RootElement.GetProperty("data"); + 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."); } static async Task TestEntrySearchEntriesSectionFilterAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - WriteSearchFixtureFiles(root); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "search.entries", + payload = new { - action = "search.entries", - payload = new - { - dataDirectory = root, - query = "focus area", - section = "Reflection", - } - }); + query = "focus area", + section = "Reflection", + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for section-scoped search."); - var data = doc.RootElement.GetProperty("data"); - Assert(data.GetArrayLength() == 1, "Expected one section-scoped result."); - Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Section filter result mismatch."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for section-scoped search."); + var data = doc.RootElement.GetProperty("data"); + Assert(data.GetArrayLength() == 1, "Expected one section-scoped result."); + Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Section filter result mismatch."); } static async Task TestEntrySearchEntriesTagTypeFilterAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - WriteSearchFixtureFiles(root); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "search.entries", + payload = new { - action = "search.entries", - payload = new - { - dataDirectory = root, - tags = new[] { "stress" }, - types = new[] { "!TRIGGER" }, - } - }); + tags = new[] { "stress" }, + types = new[] { "!TRIGGER" }, + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for fragment tag/type filtered search."); - var data = doc.RootElement.GetProperty("data"); - 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."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for fragment tag/type filtered search."); + var data = doc.RootElement.GetProperty("data"); + 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."); } static async Task TestEntrySearchEntriesCheckboxFilterAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - WriteSearchFixtureFiles(root); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "search.entries", + payload = new { - action = "search.entries", - payload = new - { - dataDirectory = root, - @checked = new[] { "med taken" }, - @unchecked = new[] { "drink water" }, - } - }); + @checked = new[] { "med taken" }, + @unchecked = new[] { "drink water" }, + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for checkbox filtered search."); - var data = doc.RootElement.GetProperty("data"); - Assert(data.GetArrayLength() == 2, "Expected OR-style checkbox match across checked/unchecked filters."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for checkbox filtered search."); + var data = doc.RootElement.GetProperty("data"); + Assert(data.GetArrayLength() == 2, "Expected OR-style checkbox match across checked/unchecked filters."); } static async Task TestEntrySearchEntriesRejectsInvalidDateAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var entry = NewEntry(); + await SeedSearchFixtureEntriesAsync(entry); - try + var request = JsonSerializer.Serialize(new { - WriteSearchFixtureFiles(root); - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new + action = "search.entries", + payload = new { - action = "search.entries", - payload = new - { - dataDirectory = root, - startDate = "2026/02/01", - } - }); + startDate = "2026/02/01", + } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false for invalid date format."); - var error = doc.RootElement.GetProperty("error").GetString() ?? ""; - Assert(error.Contains("invalid startdate value", StringComparison.OrdinalIgnoreCase), "Expected invalid startDate error."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } + Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false for invalid date format."); + var error = doc.RootElement.GetProperty("error").GetString() ?? ""; + Assert(error.Contains("invalid startdate value", StringComparison.OrdinalIgnoreCase), "Expected invalid startDate error."); } static Task TestSidecarSearchCliFilteredAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + + using var session = new DatabaseSessionService(dbService); + session.SetPassword("vault-pass-123"); + + var repo = new SqliteEntryFileRepository(session); + var entryFiles = new EntryFileService(repo); + var searchService = new EntrySearchService(repo); + var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles); try { - WriteSearchFixtureFiles(dataDir); - var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); + entryFiles.SaveEntry(new EntrySavePayload(""" +Date: 2026-02-01 +## Summary +common +- [x] med taken +!TRIGGER #stress +matched body +""", Mode: "Overwrite", FileName: "2026-02-01")); + entryFiles.SaveEntry(new EntrySavePayload(""" +Date: 2026-02-05 +## Summary +common +- [ ] drink water +!NOTE #daily +non match +""", Mode: "Overwrite", FileName: "2026-02-05")); var (exitCode, stdout, stderr) = CaptureConsole(() => cli.RunSearchCommand( [ "common", - "--data-dir", dataDir, - "--start-date", "2026-02-01", - "--end-date", "2026-02-28", - "--tag", "stress", - "--type", "!TRIGGER", - "--checked", "med taken", - "--section", "Summary" + "--start-date", "2026-02-01", + "--end-date", "2026-02-28", + "--tag", "stress", + "--type", "!TRIGGER", + "--checked", "med taken", + "--section", "Summary" ])); Assert(exitCode == 0, "Expected search CLI command to succeed."); @@ -557,6 +436,7 @@ hello world } finally { + session.Dispose(); if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } @@ -567,19 +447,27 @@ hello world static Task TestSidecarSearchCliEmptyDataAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + + using var session = new DatabaseSessionService(dbService); + session.SetPassword("vault-pass-123"); + + var repo = new SqliteEntryFileRepository(session); + var entryFiles = new EntryFileService(repo); + var searchService = new EntrySearchService(repo); + var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles); try { - var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); - var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand(["--data-dir", dataDir])); + var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand([])); - Assert(exitCode == 0, "Expected search CLI command to return success for empty data directory."); - Assert(stdout.Contains("No decrypted journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message."); + Assert(exitCode == 0, "Expected search CLI command to return success for empty data store."); + Assert(stdout.Contains("No journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message."); } finally { + session.Dispose(); if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } @@ -589,7 +477,6 @@ hello world static Task TestEntrySavePayloadFileNameDeserializationAsync() { - // Simulate what DeserializePayload does: parse JSON to JsonElement, then Deserialize var json = """{"content":"hello","mode":"Overwrite","fileName":"My Custom Name"}"""; var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var element = JsonSerializer.Deserialize(json); @@ -607,26 +494,31 @@ hello world static async Task TestEntrySaveWithFileNameAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + + using var session = new DatabaseSessionService(dbService); + session.SetPassword("vault-pass-123"); + + var repo = new SqliteEntryFileRepository(session); + var service = new EntryFileService(repo); try { - // Use EntryFileService directly to test the full save path with fileName - var service = new EntryFileService(new DiskEntryFileRepository()); var payload = new EntrySavePayload( Content: "# Custom Entry\n\nHello world", FilePath: null, Mode: "Overwrite", FileName: "My Custom Name"); - var result = service.SaveEntry(payload, root); + var result = service.SaveEntry(payload); - var expectedPath = Path.GetFullPath(Path.Combine(root, "My Custom Name.md")); - Assert(result.FilePath == expectedPath, $"Expected path '{expectedPath}' but got '{result.FilePath}'."); - Assert(File.Exists(expectedPath), "Custom-named file should exist on disk."); - Assert(File.ReadAllText(expectedPath).Contains("Hello world"), "File content should match."); + Assert(result.FilePath.StartsWith("db://entry/", StringComparison.OrdinalIgnoreCase), "Expected canonical db://entry path."); + Assert(result.FilePath.Contains("My%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), "Expected escaped custom name in db path."); + + var loaded = service.LoadEntry(result.FilePath); + Assert(loaded.Entry.RawContent.Contains("Hello world", StringComparison.Ordinal), "Stored content should match."); - // Also test via Entry.HandleCommandAsync with JSON to verify full deserialization chain var entry = NewEntry(); var request = JsonSerializer.Serialize(new { @@ -644,14 +536,87 @@ hello world Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save with fileName."); var savedFilePath = doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? ""; - Assert(savedFilePath.Contains("Another Custom Name.md", StringComparison.OrdinalIgnoreCase), - $"Expected file path to contain 'Another Custom Name.md' but got '{savedFilePath}'."); + Assert(savedFilePath.Contains("Another%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), + $"Expected file path to contain 'Another%20Custom%20Name.md' but got '{savedFilePath}'."); } finally { + session.Dispose(); if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } -} + private static async Task 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 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 +"""); + } +} diff --git a/Journal.SmokeTests/Program.FragmentTests.cs b/Journal.SmokeTests/Program.FragmentTests.cs index c0e1f08..55987f5 100644 --- a/Journal.SmokeTests/Program.FragmentTests.cs +++ b/Journal.SmokeTests/Program.FragmentTests.cs @@ -53,20 +53,19 @@ internal static partial class Program try { - // Set up encrypted DB session - var configService = new JournalConfigService(); + var configService = NewConfigService(tempRoot); var dbService = new JournalDatabaseService(configService); // First session: create a fragment using var session1 = new DatabaseSessionService(dbService); - session1.SetPassword(password, dataDir); + session1.SetPassword(password); var repo1 = new SqliteFragmentRepository(session1); var service1 = new FragmentService(repo1); var created = service1.Create(new CreateFragmentDto("!TRIGGER", "persist me", ["tag1"])); // Second session: verify persistence using var session2 = new DatabaseSessionService(dbService); - session2.SetPassword(password, dataDir); + session2.SetPassword(password); var repo2 = new SqliteFragmentRepository(session2); var service2 = new FragmentService(repo2); var loaded = service2.GetById(created.Id); diff --git a/Journal.SmokeTests/Program.Shared.cs b/Journal.SmokeTests/Program.Shared.cs index 21803d9..09f10b1 100644 --- a/Journal.SmokeTests/Program.Shared.cs +++ b/Journal.SmokeTests/Program.Shared.cs @@ -6,26 +6,64 @@ internal static partial class Program return new FragmentService(repo); } - static Entry NewEntry() + static Entry NewEntry(bool unlocked = true, string password = "vault-pass-123", string? root = null) { - var dbService = new JournalDatabaseService(new JournalConfigService()); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); var session = new DatabaseSessionService(dbService); + if (unlocked) + session.SetPassword(password); + + var entryRepo = new SqliteEntryFileRepository(session); + return new Entry( NewService(), - new EntrySearchService(), - new VaultStorageService(new VaultCryptoService()), + new EntrySearchService(entryRepo), + new VaultStorageService(new VaultCryptoService(), dbService), dbService, session, - new JournalConfigService(), + config, new DisabledAiService("none"), new DisabledSpeechBridgeService("none"), - new EntryFileService(new DiskEntryFileRepository()), + new EntryFileService(entryRepo), new ListService(new SqliteListRepository(session)), new TodoService(new SqliteTodoRepository(session)), new CommandLogger()); } - static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(new JournalConfigService()); + static Entry NewLockedEntry() => NewEntry(unlocked: false); + + static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(NewConfigService()); + + static IJournalConfigService NewConfigService(string? root = null, string? databaseFilename = null) + { + var baseConfig = new JournalConfigService().Current; + var normalizedRoot = Path.GetFullPath(root ?? Path.Combine(Path.GetTempPath(), "journal-smoke", Guid.NewGuid().ToString("N"))); + var appDirectory = Path.Combine(normalizedRoot, "journal"); + var vaultDirectory = Path.Combine(appDirectory, "vault"); + var logDirectory = Path.Combine(normalizedRoot, "logs"); + + Directory.CreateDirectory(vaultDirectory); + Directory.CreateDirectory(logDirectory); + + var config = baseConfig with + { + ProjectRoot = normalizedRoot, + AppDirectory = appDirectory, + VaultDirectory = vaultDirectory, + LogDirectory = logDirectory, + PidFile = Path.Combine(logDirectory, "nicegui_server.pid"), + ServerControlFile = Path.Combine(logDirectory, "server_control.action"), + DatabaseFilename = databaseFilename ?? "journal_cache.db" + }; + + return new FixedConfigService(config); + } + + private sealed class FixedConfigService(JournalConfig config) : IJournalConfigService + { + public JournalConfig Current => config; + } static Dictionary ReadVaultEntryTexts(string vaultPath, string password) { diff --git a/Journal.SmokeTests/Program.VaultTests.cs b/Journal.SmokeTests/Program.VaultTests.cs index 795250a..a625d6b 100644 --- a/Journal.SmokeTests/Program.VaultTests.cs +++ b/Journal.SmokeTests/Program.VaultTests.cs @@ -41,40 +41,53 @@ internal static partial class Program static Task TestVaultMonthlyFilenameParityAsync() { - IVaultStorageService vaultStorage = new VaultStorageService(new VaultCryptoService()); - var name = vaultStorage.GetMonthlyVaultFileName(new DateTime(2026, 2, 7)); - Assert(name == "2026-02.vault", "Monthly vault filename must match yyyy-MM.vault format."); + var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); + + try + { + var dbPath = dbService.GetDatabasePath(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + File.WriteAllText(dbPath, "db-bytes"); + + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); + + var expectedName = $"_db_{Path.GetFileName(dbPath)}.vault"; + Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, expectedName)), "Expected DB vault snapshot filename with _db_ prefix and .vault suffix."); + } + finally + { + if (Directory.Exists(root)) + Directory.Delete(root, recursive: true); + } + return Task.CompletedTask; } static Task TestVaultLoadClearsAndExtractsAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); try { - File.WriteAllText(Path.Combine(dataDir, "old_file.md"), "stale"); + var dbPath = dbService.GetDatabasePath(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + var originalBytes = Guid.NewGuid().ToByteArray(); + File.WriteAllBytes(dbPath, originalBytes); - var zipBytes = CreateZipBytes(new Dictionary - { - ["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); + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); - IVaultStorageService storage = new VaultStorageService(crypto); - var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir); + File.WriteAllBytes(dbPath, [1, 2, 3, 4]); + var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); Assert(ok, "Expected vault load success with correct password."); - Assert(!File.Exists(Path.Combine(dataDir, "old_file.md")), "Data directory should be cleared before extraction."); - 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."); + var loaded = File.ReadAllBytes(dbPath); + Assert(loaded.SequenceEqual(originalBytes), "Expected DB file bytes restored from vault snapshot."); } finally { @@ -88,25 +101,21 @@ internal static partial class Program static Task TestVaultLoadWrongPasswordPreservesVaultAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); try { - var zipBytes = CreateZipBytes(new Dictionary - { - ["2026-02-01.md"] = "hello from vault" - }); - var crypto = new VaultCryptoService(); - var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123"); - var vaultPath = Path.Combine(vaultDir, "2026-02.vault"); - File.WriteAllBytes(vaultPath, encrypted); + var dbPath = dbService.GetDatabasePath(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + File.WriteAllText(dbPath, "db payload"); + + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); + var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault"); var before = File.ReadAllBytes(vaultPath); - IVaultStorageService storage = new VaultStorageService(crypto); - var ok = storage.LoadAllVaults("wrong-password", vaultDir, dataDir); + var ok = storage.LoadAllVaults("wrong-password", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); var after = File.ReadAllBytes(vaultPath); Assert(!ok, "Expected vault load failure with wrong password."); @@ -124,22 +133,19 @@ internal static partial class Program static Task TestVaultLoadLegacyInitVaultHandlingAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); try { - var legacyPath = Path.Combine(vaultDir, "_init_vault.vault"); + var legacyPath = Path.Combine(config.Current.VaultDirectory, "_init_vault.vault"); File.WriteAllBytes(legacyPath, [1, 2, 3, 4]); - IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); - var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir); + var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbService.GetDatabasePath())!); Assert(ok, "Legacy-only vault directory should still be treated as successful load state."); - Assert(!File.Exists(legacyPath), "Legacy _init_vault.vault should be removed during load."); - Assert(Directory.Exists(dataDir), "Data directory should exist after load workflow."); + Assert(File.Exists(legacyPath), "Legacy _init_vault.vault should be ignored in SQLCipher snapshot mode."); } finally { @@ -153,43 +159,25 @@ internal static partial class Program static Task TestVaultCurrentMonthSaveOptimizedAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); try { - File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb one"); - File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two"); - File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan one"); + var dbPath = dbService.GetDatabasePath(); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!); + File.WriteAllText(dbPath, "initial db"); - IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); - var now = new DateTime(2026, 2, 22, 12, 0, 0, DateTimeKind.Utc); + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); + var vaultPath = Path.Combine(config.Current.VaultDirectory, $"_db_{Path.GetFileName(dbPath)}.vault"); + Assert(File.Exists(vaultPath), "Expected DB vault snapshot file to be created."); - var firstSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now); - Assert(firstSaved, "Expected first current-month save to write vault data."); + var firstLength = new FileInfo(vaultPath).Length; + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, Path.GetDirectoryName(dbPath)!); + var secondLength = new FileInfo(vaultPath).Length; - var febVaultPath = Path.Combine(vaultDir, "2026-02.vault"); - 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."); + Assert(firstLength > 0 && secondLength > 0, "Vault snapshot should remain non-empty across repeated rebuilds."); } finally { @@ -203,30 +191,21 @@ internal static partial class Program static Task TestVaultRebuildAllVaultsAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); try { - File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan body"); - File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb body"); - File.WriteAllText(Path.Combine(dataDir, "not-a-journal.md"), "should be ignored"); + var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!; + Directory.CreateDirectory(dbDir); + File.WriteAllText(Path.Combine(dbDir, "journal_cache.db"), "primary"); + File.WriteAllText(Path.Combine(dbDir, "analytics.db"), "secondary"); - IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); - storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir); + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir); - var janVaultPath = Path.Combine(vaultDir, "2026-01.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."); + Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected primary DB snapshot vault."); + Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_analytics.db.vault")), "Expected secondary DB snapshot vault."); } finally { @@ -240,21 +219,22 @@ internal static partial class Program static Task TestVaultClearDataDirectoryAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(dataDir); - Directory.CreateDirectory(Path.Combine(dataDir, "nested")); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); + + var scratchDir = Path.Combine(root, "scratch-data"); + Directory.CreateDirectory(scratchDir); + Directory.CreateDirectory(Path.Combine(scratchDir, "nested")); try { - File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "decrypted content"); - File.WriteAllText(Path.Combine(dataDir, "journal_cache.db"), "cache"); - File.WriteAllText(Path.Combine(dataDir, "nested", "tmp.txt"), "temp"); + File.WriteAllText(Path.Combine(scratchDir, "tmp.md"), "decrypted content"); + File.WriteAllText(Path.Combine(scratchDir, "nested", "tmp.txt"), "temp"); - IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); - storage.ClearDataDirectory(dataDir); + storage.ClearDataDirectory(scratchDir); - Assert(Directory.Exists(dataDir), "Data directory should be recreated after cleanup."); - Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after cleanup."); + Assert(!Directory.Exists(scratchDir), "Non-db scratch directory should be deleted by clear_data_directory."); } finally { @@ -268,30 +248,28 @@ internal static partial class Program static async Task TestEntryVaultLoadAllEmptyAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); + Directory.CreateDirectory(root); try { - var entry = NewEntry(); + var entry = NewLockedEntry(); var request = JsonSerializer.Serialize(new { action = "vault.load_all", payload = new { password = "vault-pass-123", - vaultDirectory = vaultDir, - dataDirectory = dataDir, + vaultDirectory = Path.Combine(root, "vault") } }); + Directory.CreateDirectory(Path.Combine(root, "vault")); + var response = await entry.HandleCommandAsync(request); using var doc = JsonDocument.Parse(response); Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for empty vault directory load."); Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault directory."); - Assert(Directory.Exists(dataDir), "Expected data directory to be created by load workflow."); } finally { @@ -302,52 +280,44 @@ internal static partial class Program static async Task TestEntryVaultClearDataDirectoryAsync() { - var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N")); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(dataDir); - File.WriteAllText(Path.Combine(dataDir, "tmp.md"), "x"); - - try + var entry = NewEntry(); + var request = JsonSerializer.Serialize(new { - var entry = NewEntry(); - var request = JsonSerializer.Serialize(new - { - action = "vault.clear_data_directory", - payload = new - { - dataDirectory = dataDir, - } - }); + action = "vault.clear_data_directory", + payload = new { } + }); - var response = await entry.HandleCommandAsync(request); - using var doc = JsonDocument.Parse(response); + var response = await entry.HandleCommandAsync(request); + using var doc = JsonDocument.Parse(response); - Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for clear_data_directory."); - Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected clear_data_directory result=true."); - 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); - } + Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for clear_data_directory."); + Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected clear_data_directory result=true."); } static Task TestSidecarVaultCliLoadAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + + using var session = new DatabaseSessionService(dbService); + session.SetPassword("vault-pass-123"); + + var repo = new SqliteEntryFileRepository(session); + var entryFiles = new EntryFileService(repo); + var searchService = new EntrySearchService(repo); + var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles); + + var vaultDir = Path.Combine(root, "vault-cli"); Directory.CreateDirectory(vaultDir); try { - var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); - var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]); - + var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir]); Assert(exitCode == 0, "Expected vault load CLI command to succeed on empty vault directory."); - Assert(Directory.Exists(dataDir), "Expected data directory to be created by vault load CLI command."); + + var dbDir = Path.Combine(config.Current.VaultDirectory, "db"); + Assert(Directory.Exists(dbDir), "Expected db directory to be created by vault load CLI command."); } finally { @@ -361,20 +331,29 @@ internal static partial class Program static Task TestSidecarVaultCliSaveAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + + using var session = new DatabaseSessionService(dbService); + session.SetPassword("vault-pass-123"); + + var repo = new SqliteEntryFileRepository(session); + var entryFiles = new EntryFileService(repo); + var searchService = new EntrySearchService(repo); + var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles); + + var vaultDir = Path.Combine(root, "vault-cli"); Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); try { - File.WriteAllText(Path.Combine(dataDir, "2026-02-22.md"), "entry body"); + entryFiles.SaveEntry(new EntrySavePayload("entry body", Mode: "Overwrite", FileName: "2026-02-22")); + session.CloseConnection(); - var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService()); - var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]); + var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir]); Assert(exitCode == 0, "Expected vault save CLI command to succeed."); - Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault file to be written by save CLI command."); + Assert(File.Exists(Path.Combine(vaultDir, "_db_journal_cache.db.vault")), "Expected DB vault snapshot file to be written by save CLI command."); } finally { @@ -388,41 +367,46 @@ internal static partial class Program static Task TestVaultCustomEntryRoundtripAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var storage = new VaultStorageService(new VaultCryptoService(), dbService); try { - // Create both date-named and custom-named entries - File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "date entry"); - File.WriteAllText(Path.Combine(dataDir, "My Custom Entry.md"), "custom entry body"); - File.WriteAllText(Path.Combine(dataDir, "Work Notes.md"), "work notes body"); + using (var seedSession = new DatabaseSessionService(dbService)) + { + seedSession.SetPassword("vault-pass-123"); + var seedRepo = new SqliteEntryFileRepository(seedSession); + var seedEntryFiles = new EntryFileService(seedRepo); - // Rebuild vaults (simulates app close) - IVaultStorageService storage = new VaultStorageService(new VaultCryptoService()); - storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir); + seedEntryFiles.SaveEntry(new EntrySavePayload("date entry", Mode: "Overwrite", FileName: "2026-02-01")); + seedEntryFiles.SaveEntry(new EntrySavePayload("custom entry body", Mode: "Overwrite", FileName: "My Custom Entry")); + seedEntryFiles.SaveEntry(new EntrySavePayload("work notes body", Mode: "Overwrite", FileName: "Work Notes")); + seedSession.CloseConnection(); + } - // Verify custom vault was created - 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."); + var dbPath = dbService.GetDatabasePath(); + var dbDir = Path.GetDirectoryName(dbPath)!; - // Clear data directory (simulates app close step 2) - storage.ClearDataDirectory(dataDir); - Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after clear."); + storage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir); + Assert(File.Exists(Path.Combine(config.Current.VaultDirectory, "_db_journal_cache.db.vault")), "Expected DB vault snapshot to be created."); - // Load vaults (simulates app restart) - var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir); + File.Delete(dbPath); + Assert(!File.Exists(dbPath), "DB file should be deleted before restore."); + + var ok = storage.LoadAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir); Assert(ok, "Expected vault load to succeed."); - // Verify all entries are restored - Assert(File.Exists(Path.Combine(dataDir, "2026-02-01.md")), "Date entry should be restored from vault."); - Assert(File.Exists(Path.Combine(dataDir, "My Custom Entry.md")), "Custom entry should be restored from vault."); - Assert(File.Exists(Path.Combine(dataDir, "Work Notes.md")), "Second custom entry should be restored from vault."); - Assert(File.ReadAllText(Path.Combine(dataDir, "My Custom Entry.md")) == "custom entry body", "Custom entry content mismatch."); - Assert(File.ReadAllText(Path.Combine(dataDir, "Work Notes.md")) == "work notes body", "Second custom entry content mismatch."); + using (var verifySession = new DatabaseSessionService(dbService)) + { + verifySession.SetPassword("vault-pass-123"); + var verifyRepo = new SqliteEntryFileRepository(verifySession); + var verifyEntryFiles = new EntryFileService(verifyRepo); + var allEntries = verifyEntryFiles.ListEntries(); + Assert(allEntries.Any(e => e.FileName == "2026-02-01.md"), "Date entry should be restored from vault DB snapshot."); + Assert(allEntries.Any(e => e.FileName == "My Custom Entry.md"), "Custom entry should be restored from vault DB snapshot."); + Assert(allEntries.Any(e => e.FileName == "Work Notes.md"), "Second custom entry should be restored from vault DB snapshot."); + } } finally { @@ -436,22 +420,34 @@ internal static partial class Program static async Task TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N")); - var vaultDir = Path.Combine(root, "vault"); - var dataDir = Path.Combine(root, "data"); - Directory.CreateDirectory(vaultDir); - Directory.CreateDirectory(dataDir); + var config = NewConfigService(root); + var dbService = new JournalDatabaseService(config); + var vaultStorage = new VaultStorageService(new VaultCryptoService(), dbService); + var session = new DatabaseSessionService(dbService); + var repo = new SqliteEntryFileRepository(session); + var entryFiles = new EntryFileService(repo); + var entrySearch = new EntrySearchService(repo); + + var entry = new Entry( + NewService(), + entrySearch, + vaultStorage, + dbService, + session, + config, + new DisabledAiService("none"), + new DisabledSpeechBridgeService("none"), + entryFiles, + new ListService(new SqliteListRepository(session)), + new TodoService(new SqliteTodoRepository(session)), + new CommandLogger()); try { - var zipBytes = CreateZipBytes(new Dictionary - { - ["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); - - var entry = NewEntry(); + var dbDir = Path.GetDirectoryName(dbService.GetDatabasePath())!; + Directory.CreateDirectory(dbDir); + File.WriteAllBytes(dbService.GetDatabasePath(), Guid.NewGuid().ToByteArray()); + vaultStorage.RebuildAllVaults("vault-pass-123", config.Current.VaultDirectory, dbDir); var loadRequest = JsonSerializer.Serialize(new { @@ -459,8 +455,7 @@ internal static partial class Program payload = new { password = "wrong-password", - vaultDirectory = vaultDir, - dataDirectory = dataDir + vaultDirectory = config.Current.VaultDirectory } }); var loadResponse = await entry.HandleCommandAsync(loadRequest); @@ -488,89 +483,31 @@ internal static partial class Program static async Task TestEntryTemplateSaveAutoSyncsVaultAsync() { var root = Path.Combine(Path.GetTempPath(), "journal-entry-vault-sync-smoke", Guid.NewGuid().ToString("N")); - 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 entry = NewEntry(root: root); - var previousProjectRoot = Environment.GetEnvironmentVariable("JOURNAL_PROJECT_ROOT"); - var previousDataDir = Environment.GetEnvironmentVariable("JOURNAL_DATA_DIR"); - var previousVaultDir = Environment.GetEnvironmentVariable("JOURNAL_VAULT_DIR"); - Environment.SetEnvironmentVariable("JOURNAL_PROJECT_ROOT", projectRoot); - Environment.SetEnvironmentVariable("JOURNAL_DATA_DIR", dataDir); - Environment.SetEnvironmentVariable("JOURNAL_VAULT_DIR", vaultDir); + var configResponse = await entry.HandleCommandAsync("""{"action":"config.get"}"""); + using var configDoc = JsonDocument.Parse(configResponse); + var configData = configDoc.RootElement.GetProperty("data"); + var vaultDir = configData.GetProperty("VaultDirectory").GetString() ?? ""; + var dbFilename = configData.GetProperty("DatabaseFilename").GetString() ?? "journal_cache.db"; - try + var saveTemplateRequest = JsonSerializer.Serialize(new { - var entry = NewEntry(); - var password = "vault-pass-123"; - - var unlockRequest = JsonSerializer.Serialize(new + action = "templates.save", + payload = new { - action = "vault.load_all", - payload = new - { - password, - vaultDirectory = vaultDir, - dataDirectory = dataDir - } - }); - 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."); + name = "Weekly Review", + content = "## Wins\n- shipped feature" + } + }); + var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest); + using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse); + Assert(saveTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected templates.save to succeed."); - var saveTemplateRequest = JsonSerializer.Serialize(new - { - action = "templates.save", - payload = new - { - name = "Weekly Review", - content = "## Wins\n- shipped feature", - dataDirectory = dataDir - } - }); - var saveTemplateResponse = await entry.HandleCommandAsync(saveTemplateRequest); - using var saveTemplateDoc = JsonDocument.Parse(saveTemplateResponse); - Assert(saveTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected templates.save to succeed."); + var dbVaultPath = Path.Combine(vaultDir, $"_db_{dbFilename}.vault"); + Assert(File.Exists(dbVaultPath), "Expected template save auto-sync to write DB vault snapshot."); - var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault"); - 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); - } + if (Directory.Exists(root)) + Directory.Delete(root, recursive: true); } } - diff --git a/Journal.SmokeTests/Program.cs b/Journal.SmokeTests/Program.cs index 6e99b60..88d2c88 100644 --- a/Journal.SmokeTests/Program.cs +++ b/Journal.SmokeTests/Program.cs @@ -17,11 +17,11 @@ internal static partial class Program ("Vault crypto decrypts Python payload fixture", TestVaultCryptoDecryptsPythonFixtureAsync), ("Vault key derivation matches Python fixture", TestVaultKeyDerivationMatchesPythonAsync), ("Vault monthly filename matches parity format", TestVaultMonthlyFilenameParityAsync), - ("Vault load clears workspace and extracts decrypted files", TestVaultLoadClearsAndExtractsAsync), + ("Vault load restores encrypted DB snapshot", TestVaultLoadClearsAndExtractsAsync), ("Vault load wrong password does not modify vault files", TestVaultLoadWrongPasswordPreservesVaultAsync), - ("Vault load ignores and removes legacy _init_vault.vault", TestVaultLoadLegacyInitVaultHandlingAsync), - ("Vault current-month save writes only current month and skips unchanged state", TestVaultCurrentMonthSaveOptimizedAsync), - ("Vault rebuild saves grouped monthly archives from decrypted files", TestVaultRebuildAllVaultsAsync), + ("Vault load ignores legacy _init_vault.vault in snapshot mode", TestVaultLoadLegacyInitVaultHandlingAsync), + ("Vault rebuild remains repeatable across runs", TestVaultCurrentMonthSaveOptimizedAsync), + ("Vault rebuild writes encrypted DB snapshot vault files", TestVaultRebuildAllVaultsAsync), ("Vault clear data directory removes decrypted workspace artifacts", TestVaultClearDataDirectoryAsync), ("Parser extracts date from **Date:** marker", TestParserExtractsBoldDateAsync), ("Parser extracts date from Date: marker", TestParserExtractsPlainDateAsync), @@ -50,7 +50,7 @@ internal static partial class Program ("Database key derivation matches Python fixture", TestDatabaseKeyDerivationMatchesPythonAsync), ("Database schema parity tables are created", TestDatabaseSchemaParityAsync), ("Entry db.status returns database compatibility payload", TestEntryDatabaseStatusAsync), - ("Entry db.initialize_schema creates schema in data directory", TestEntryDatabaseInitializeSchemaAsync), + ("Entry db.initialize_schema initializes SQLCipher schema", TestEntryDatabaseInitializeSchemaAsync), ("Entry db.hydrate_workspace returns hydration metadata", TestEntryDatabaseHydrateWorkspaceAsync), ("Config service exposes parity path, vault, AI, and speech settings", TestConfigServiceParityKeysAsync), ("Entry config.get returns config payload", TestEntryConfigGetAsync), @@ -70,10 +70,10 @@ internal static partial class Program ("Python sidecar speech service times out deterministically", TestPythonSidecarSpeechServiceTimeoutAsync), ("Entry vault.load_all succeeds for empty vault directory", TestEntryVaultLoadAllEmptyAsync), ("Entry vault.load_all wrong password keeps database session locked", TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync), - ("Entry vault.clear_data_directory removes files", TestEntryVaultClearDataDirectoryAsync), + ("Entry vault.clear_data_directory compatibility command succeeds", TestEntryVaultClearDataDirectoryAsync), ("Entry templates.save auto-syncs vault and survives reload", TestEntryTemplateSaveAutoSyncsVaultAsync), ("Sidecar vault CLI load succeeds with --password", TestSidecarVaultCliLoadAsync), - ("Sidecar vault CLI save writes monthly vault with --password", TestSidecarVaultCliSaveAsync), + ("Sidecar vault CLI save writes DB snapshot vault with --password", TestSidecarVaultCliSaveAsync), ("Sidecar search CLI returns matching entries with filters", TestSidecarSearchCliFilteredAsync), ("Sidecar search CLI warns when no decrypted entries exist", TestSidecarSearchCliEmptyDataAsync), ("Transport fixtures produce stable envelopes", TestTransportFixturesAsync),