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