Refactor smoke tests for SQLCipher-first backend contract

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

View File

@ -11,165 +11,108 @@ internal static partial class Program
static Task TestDatabaseSchemaParityAsync()
{
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<string>(statements.Keys, StringComparer.OrdinalIgnoreCase);
try
{
var service = NewDatabaseService();
var schemaPath = service.WriteSchemaBootstrap(root);
var statements = service.GetSchemaStatements();
var tableNames = new HashSet<string>(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;
}
}

View File

@ -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<T>
var json = """{"content":"hello","mode":"Overwrite","fileName":"My Custom Name"}""";
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var element = JsonSerializer.Deserialize<JsonElement>(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<string> SaveEntryForTestAsync(Entry entry, string fileStem, string content)
{
var request = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
content,
mode = "Overwrite",
fileName = fileStem
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test seed entries.save.");
return doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
}
private static async Task<string> LoadEntryForTestAsync(Entry entry, string filePath)
{
var request = JsonSerializer.Serialize(new
{
action = "entries.load",
payload = new
{
filePath
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test entries.load.");
return doc.RootElement.GetProperty("data").GetProperty("Entry").GetProperty("RawContent").GetString() ?? "";
}
private static async Task SeedSearchFixtureEntriesAsync(Entry entry)
{
await SaveEntryForTestAsync(entry, "2026-02-01", """
Date: 2026-02-01
## Summary
Alpha line
common token
## Reflection
focus area
- [x] med taken
!TRIGGER #stress
fragment one
""");
await SaveEntryForTestAsync(entry, "2026-02-05", """
Date: 2026-02-05
## Summary
beta line
COMMON token
## Reflection
other notes
- [ ] drink water
!NOTE #daily
fragment two
""");
await SaveEntryForTestAsync(entry, "2026-03-01", """
Date: 2026-03-01
## Summary
gamma only
## Reflection
nothing related
!NOTE #other
fragment three
""");
}
}

View File

@ -53,20 +53,19 @@ internal static partial class Program
try
{
// 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);

View File

@ -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<string, string> ReadVaultEntryTexts(string vaultPath, string password)
{

View File

@ -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<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);
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<string, string>
{
["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<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);
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);
}
}

View File

@ -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),