Refactor smoke tests for SQLCipher-first backend contract
This commit is contained in:
parent
941cafba39
commit
4fd3c5b5f1
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user