refactor(smoke-tests): split Program.cs into grouped partial files

This commit is contained in:
Jacob Schmidt 2026-02-26 20:15:43 -06:00
parent d1e4989303
commit 1e28ae9e25
10 changed files with 2229 additions and 2200 deletions

View File

@ -0,0 +1,19 @@
global using System.ComponentModel.DataAnnotations;
global using System.IO.Compression;
global using System.Security.Cryptography;
global using System.Text.Json;
global using Journal.Core;
global using Journal.Core.Dtos;
global using Journal.Core.Models;
global using Journal.Core.Repositories;
global using Journal.Core.Services.Ai;
global using Journal.Core.Services.Config;
global using Journal.Core.Services.Database;
global using Journal.Core.Services.Entries;
global using Journal.Core.Services.Fragments;
global using Journal.Core.Services.Logging;
global using Journal.Core.Services.Speech;
global using Journal.Core.Services.Sidecar;
global using Journal.Core.Services.Lists;
global using Journal.Core.Services.Todos;
global using Journal.Core.Services.Vault;

View File

@ -0,0 +1,342 @@
internal static partial class Program
{
static async Task TestEntryAiHealthDefaultAsync()
{
var entry = NewEntry();
var response = await entry.HandleCommandAsync("""{"action":"ai.health"}""");
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for ai.health.");
var data = doc.RootElement.GetProperty("data");
Assert(data.GetProperty("Enabled").GetBoolean() is false, "Expected AI disabled by default.");
Assert(string.Equals(data.GetProperty("Provider").GetString(), "none", StringComparison.OrdinalIgnoreCase), "Expected default provider 'none'.");
}
static async Task TestEntryAiSummarizeEntryDisabledAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "ai.summarize_entry",
payload = new
{
content = "sample entry"
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.summarize_entry.");
var data = doc.RootElement.GetProperty("data").GetString() ?? "";
Assert(data.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled provider message for ai.summarize_entry.");
}
static async Task TestEntryAiSummarizeAllDisabledAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "ai.summarize_all",
payload = new
{
entries = new[] { "entry one", "entry two" }
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.summarize_all.");
var data = doc.RootElement.GetProperty("data").GetString() ?? "";
Assert(data.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled provider message for ai.summarize_all.");
}
static async Task TestEntryAiChatDisabledAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "ai.chat",
payload = new
{
prompt = "hello cloud"
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.chat.");
var data = doc.RootElement.GetProperty("data").GetString() ?? "";
Assert(data.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled provider message for ai.chat.");
}
static async Task TestEntryAiEmbedDisabledAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "ai.embed",
payload = new
{
content = "embedding source text"
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.embed.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected ai.embed response to be a JSON array.");
Assert(data.GetArrayLength() == 0, "Expected disabled ai.embed to return an empty vector.");
}
static async Task TestEntrySpeechDevicesListDisabledAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "speech.devices.list",
payload = new { }
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for speech.devices.list when disabled.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Object, "Expected speech.devices.list data to be an object.");
}
static async Task TestEntrySpeechTranscribeDisabledAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "speech.transcribe",
payload = new
{
text = "fixture transcript",
engine = "whisper"
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for speech.transcribe when disabled.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Object, "Expected speech.transcribe data to be an object.");
var warning = data.TryGetProperty("Warning", out var warningNode) ? warningNode.GetString() ?? "" : "";
Assert(warning.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled speech warning.");
}
static async Task TestPythonSidecarAiServiceJsonLineAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-ai-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var scriptPath = Path.Combine(root, "fake_ai_sidecar.py");
File.WriteAllText(scriptPath, """
import json, sys
request = json.loads(sys.stdin.readline())
action = request.get("action", "")
print("DEBUG prelude")
if action == "health":
print(json.dumps({"ok": True, "data": {"provider": "python-sidecar", "healthy": True, "message": "ok"}}))
elif action == "summarize_entry":
payload = request.get("payload") or {}
print(json.dumps({"ok": True, "data": "ENTRY::" + str(payload.get("content", ""))}))
elif action == "summarize_all":
payload = request.get("payload") or {}
entries = payload.get("entries") or []
print(json.dumps({"ok": True, "data": "ALL::" + str(len(entries))}))
elif action == "chat":
payload = request.get("payload") or {}
print(json.dumps({"ok": True, "data": "CHAT::" + str(payload.get("prompt", ""))}))
elif action == "embed":
payload = request.get("payload") or {}
text = str(payload.get("content", ""))
print(json.dumps({"ok": True, "data": [float(len(text)), 2.5, -1.0]}))
else:
print(json.dumps({"ok": False, "error": "unknown action"}))
""");
try
{
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
IAiService service = new PythonSidecarAiService(config);
var health = await service.HealthAsync();
Assert(health.Enabled, "Expected enabled=true for python-sidecar health.");
Assert(health.Healthy, "Expected healthy=true from fake sidecar health.");
var one = await service.SummarizeEntryAsync("hello");
Assert(one == "ENTRY::hello", "Unexpected summarize_entry response.");
var all = await service.SummarizeAllAsync(["a", "b", "c"]);
Assert(all == "ALL::3", "Unexpected summarize_all response.");
var chat = await service.ChatAsync("hello");
Assert(chat == "CHAT::hello", "Unexpected chat response.");
var vector = await service.EmbedAsync("hello");
Assert(vector.Count == 3, "Unexpected embed vector length.");
Assert(Math.Abs(vector[0] - 5d) < 0.0001d, "Unexpected embed vector first value.");
Assert(Math.Abs(vector[1] - 2.5d) < 0.0001d, "Unexpected embed vector second value.");
Assert(Math.Abs(vector[2] + 1.0d) < 0.0001d, "Unexpected embed vector third value.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestPythonSidecarAiServiceErrorAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-ai-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var scriptPath = Path.Combine(root, "fake_ai_sidecar_error.py");
File.WriteAllText(scriptPath, """
import json, sys
_ = json.loads(sys.stdin.readline())
print(json.dumps({"ok": False, "error": "simulated failure"}))
""");
try
{
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
IAiService service = new PythonSidecarAiService(config);
try
{
_ = await service.SummarizeEntryAsync("hello");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("simulated failure", StringComparison.OrdinalIgnoreCase))
{
return;
}
throw new InvalidOperationException("Expected summarize_entry to surface sidecar error.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestPythonSidecarSpeechServiceNoDevicesAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var scriptPath = Path.Combine(root, "fake_speech_sidecar_nodes.py");
File.WriteAllText(scriptPath, """
import json, sys
request = json.loads(sys.stdin.readline())
action = request.get("action", "")
if action == "speech.devices.list":
print(json.dumps({"ok": True, "data": {"devices": [], "warning": "no devices"}}))
elif action == "speech.transcribe":
payload = request.get("payload") or {}
print(json.dumps({"ok": True, "data": {"text": payload.get("text", ""), "engine": payload.get("engine", "whisper")}}))
else:
print(json.dumps({"ok": False, "error": "unknown action"}))
""");
try
{
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
ISpeechBridgeService service = new PythonSidecarSpeechService(config);
var devices = await service.ListDevicesAsync();
Assert(devices.Devices.Count == 0, "Expected empty devices list.");
Assert((devices.Warning ?? "").Contains("no devices", StringComparison.OrdinalIgnoreCase), "Expected no-devices warning.");
var transcript = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture text", Engine: "whisper"));
Assert(transcript.Text == "fixture text", "Expected passthrough transcript text.");
Assert(transcript.Engine == "whisper", "Expected passthrough transcript engine.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestPythonSidecarSpeechServiceErrorAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var scriptPath = Path.Combine(root, "fake_speech_sidecar_error.py");
File.WriteAllText(scriptPath, """
import json, sys
request = json.loads(sys.stdin.readline())
action = request.get("action", "")
if action == "speech.transcribe":
print(json.dumps({"ok": False, "error": "engine unavailable"}))
else:
print(json.dumps({"ok": True, "data": {"devices": []}}))
""");
try
{
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
ISpeechBridgeService service = new PythonSidecarSpeechService(config);
try
{
_ = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture", Engine: "faster-whisper"));
}
catch (InvalidOperationException ex) when (ex.Message.Contains("engine unavailable", StringComparison.OrdinalIgnoreCase))
{
return;
}
throw new InvalidOperationException("Expected speech transcribe to surface sidecar engine error.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestPythonSidecarSpeechServiceTimeoutAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var scriptPath = Path.Combine(root, "fake_speech_sidecar_timeout.py");
File.WriteAllText(scriptPath, """
import json, sys, time
request = json.loads(sys.stdin.readline())
payload = request.get("payload") or {}
sleep_ms = int(payload.get("simulate_delay_ms") or 0)
time.sleep(max(0, sleep_ms) / 1000.0)
print(json.dumps({"ok": True, "data": {"text": "", "engine": "whisper"}}))
""");
try
{
var config = BuildAiConfig(scriptPath, timeoutMs: 100);
ISpeechBridgeService service = new PythonSidecarSpeechService(config);
try
{
_ = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture", SimulateDelayMs: 500));
}
catch (TimeoutException)
{
return;
}
throw new InvalidOperationException("Expected speech transcribe timeout path.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
}

View File

@ -0,0 +1,249 @@
internal static partial class Program
{
static Task TestDatabaseKeyDerivationMatchesPythonAsync()
{
var service = NewDatabaseService();
var keyHex = Convert.ToHexString(service.DeriveDatabaseKey("vault-pass-123")).ToLowerInvariant();
var expected = "6a9de08e13357aa8f14e7eb0ccde119e7b4d277c60aaaca6493d9a1e1eaa5b04";
Assert(keyHex == expected, "Database key derivation should match Python PBKDF2 fixture.");
return Task.CompletedTask;
}
static Task TestDatabaseSchemaParityAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var service = NewDatabaseService();
var schemaPath = service.WriteSchemaBootstrap(root);
var statements = service.GetSchemaStatements();
var tableNames = new HashSet<string>(statements.Keys, StringComparer.OrdinalIgnoreCase);
Assert(tableNames.Contains("entries"), "Schema should contain entries table.");
Assert(tableNames.Contains("sections"), "Schema should contain sections table.");
Assert(tableNames.Contains("fragments"), "Schema should contain fragments table.");
Assert(tableNames.Contains("tags"), "Schema should contain tags table.");
Assert(tableNames.Contains("fragment_tags"), "Schema should contain fragment_tags table.");
Assert(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;
}
static async Task TestEntryDatabaseStatusAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "db.status",
payload = new
{
password = "vault-pass-123",
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.status.");
var data = doc.RootElement.GetProperty("data");
Assert(data.TryGetProperty("DatabasePath", out var databasePath), "Expected DatabasePath in db.status payload.");
Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty DatabasePath.");
Assert(data.TryGetProperty("KeyDerivation", out var keyDerivation), "Expected KeyDerivation in db.status payload.");
Assert(string.Equals(keyDerivation.GetString(), "PBKDF2-HMAC-SHA256", StringComparison.Ordinal), "Expected PBKDF2-HMAC-SHA256 key derivation.");
Assert(data.TryGetProperty("SchemaTables", out var schemaTables), "Expected SchemaTables list in db.status payload.");
Assert(schemaTables.ValueKind == JsonValueKind.Array && schemaTables.GetArrayLength() >= 5, "Expected schema table list in db.status payload.");
Assert(data.TryGetProperty("SchemaBootstrapPath", out var schemaBootstrapPath), "Expected SchemaBootstrapPath in db.status payload.");
Assert(schemaBootstrapPath.ValueKind == JsonValueKind.String && File.Exists(schemaBootstrapPath.GetString()), "Expected db.status to emit existing schema bootstrap file path.");
Assert(data.TryGetProperty("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in db.status payload.");
Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected SQLCipher runtime-ready status in db.status payload.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntryDatabaseInitializeSchemaAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "db.initialize_schema",
payload = new
{
password = "vault-pass-123",
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.initialize_schema.");
var data = doc.RootElement.GetProperty("data");
Assert(data.TryGetProperty("schemaPath", out var schemaPath), "Expected schemaPath in db.initialize_schema response.");
Assert(schemaPath.ValueKind == JsonValueKind.String, "Expected string schemaPath value.");
var resolvedPath = schemaPath.GetString() ?? "";
Assert(File.Exists(resolvedPath), "db.initialize_schema should write schema bootstrap file.");
var schemaText = File.ReadAllText(resolvedPath);
Assert(schemaText.Contains("CREATE TABLE IF NOT EXISTS entries", StringComparison.OrdinalIgnoreCase), "schema bootstrap should include entries table.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntryDatabaseHydrateWorkspaceAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-db-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
File.WriteAllText(Path.Combine(root, "2026-02-20.md"), "one");
File.WriteAllText(Path.Combine(root, "2026-02-21.md"), "two");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "db.hydrate_workspace",
payload = new
{
password = "vault-pass-123",
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for db.hydrate_workspace.");
var data = doc.RootElement.GetProperty("data");
Assert(data.TryGetProperty("DatabasePath", out var databasePath), "Expected DatabasePath in hydrate payload.");
Assert(databasePath.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(databasePath.GetString()), "Expected non-empty DatabasePath.");
Assert(data.TryGetProperty("SchemaBootstrapPath", out var schemaPath), "Expected SchemaBootstrapPath in hydrate payload.");
Assert(schemaPath.ValueKind == JsonValueKind.String && File.Exists(schemaPath.GetString()), "Expected hydrate to write schema bootstrap file.");
Assert(data.TryGetProperty("EntryFilesProcessed", out var filesProcessed), "Expected EntryFilesProcessed in hydrate payload.");
Assert(filesProcessed.ValueKind == JsonValueKind.Number && filesProcessed.GetInt32() == 2, "Expected hydrate to count markdown files in workspace.");
Assert(data.TryGetProperty("RuntimeReady", out var runtimeReady), "Expected RuntimeReady in hydrate payload.");
Assert(runtimeReady.ValueKind == JsonValueKind.True, "Expected RuntimeReady=true when SQLCipher runtime hydration succeeds.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static Task TestConfigServiceParityKeysAsync()
{
IJournalConfigService config = new JournalConfigService();
var current = config.Current;
Assert(!string.IsNullOrWhiteSpace(current.ProjectRoot), "Config ProjectRoot should not be empty.");
Assert(!string.IsNullOrWhiteSpace(current.AppDirectory), "Config AppDirectory should not be empty.");
Assert(!string.IsNullOrWhiteSpace(current.DataDirectory), "Config DataDirectory should not be empty.");
Assert(!string.IsNullOrWhiteSpace(current.VaultDirectory), "Config VaultDirectory should not be empty.");
Assert(current.MonthlyVaultFormat == "%Y-%m.vault", "Config MonthlyVaultFormat should match Python format token.");
Assert(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.EmbeddingApiUrl == "http://127.0.0.1:8086/v1/embeddings", "Config EmbeddingApiUrl default mismatch.");
Assert(current.SpeechRecognitionEngine == "whisper", "Config SpeechRecognitionEngine default mismatch.");
Assert(current.WhisperModelSize == "base", "Config WhisperModelSize default mismatch.");
Assert(current.AiProvider == "none", "Config AiProvider default mismatch.");
Assert(current.PythonExecutable == "python", "Config PythonExecutable default mismatch.");
Assert(current.AiSidecarTimeoutMs == 45000, "Config AiSidecarTimeoutMs default mismatch.");
Assert(current.PythonAiSidecarPath.EndsWith(Path.Combine("journal", "ai", "sidecar.py"), StringComparison.OrdinalIgnoreCase), "Config PythonAiSidecarPath default mismatch.");
return Task.CompletedTask;
}
static async Task TestEntryConfigGetAsync()
{
var entry = NewEntry();
var response = await entry.HandleCommandAsync("""{"action":"config.get"}""");
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for config.get.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Object, "Expected object payload for config.get.");
Assert(data.TryGetProperty("DataDirectory", out var dataDirectory), "Expected DataDirectory in config payload.");
Assert(dataDirectory.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(dataDirectory.GetString()), "Expected non-empty DataDirectory value.");
Assert(data.TryGetProperty("MonthlyVaultFormat", out var monthlyVaultFormat), "Expected MonthlyVaultFormat in config payload.");
Assert(monthlyVaultFormat.GetString() == "%Y-%m.vault", "Expected Python-compatible MonthlyVaultFormat value.");
Assert(data.TryGetProperty("LlamaCppUrl", out _), "Expected LlamaCppUrl in config payload.");
Assert(data.TryGetProperty("SpeechRecognitionEngine", out _), "Expected SpeechRecognitionEngine in config payload.");
}
static Task TestLogRedactorScrubsSensitiveFieldsAsync()
{
var payload = JsonSerializer.SerializeToElement(new
{
password = "vault-pass-123",
content = "private journal body",
prompt = "private ai prompt",
nested = new
{
token = "abc123"
}
});
var redacted = LogRedactor.RedactPayload(payload);
var serialized = JsonSerializer.Serialize(redacted);
Assert(!serialized.Contains("vault-pass-123", StringComparison.Ordinal), "Password should be redacted.");
Assert(!serialized.Contains("private journal body", StringComparison.Ordinal), "Entry content should be redacted.");
Assert(!serialized.Contains("private ai prompt", StringComparison.Ordinal), "Prompt should be redacted.");
Assert(!serialized.Contains("abc123", StringComparison.Ordinal), "Nested token should be redacted.");
Assert(serialized.Contains("[REDACTED]", StringComparison.Ordinal), "Redacted marker should be present.");
return Task.CompletedTask;
}
static Task TestLogRedactorPreservesNonSensitiveFieldsAsync()
{
var payload = JsonSerializer.SerializeToElement(new
{
action = "entries.save",
mode = "Daily",
filePath = "E:/journal/2026-02-24.md"
});
var redacted = LogRedactor.RedactPayload(payload);
var serialized = JsonSerializer.Serialize(redacted);
Assert(serialized.Contains("entries.save", StringComparison.Ordinal), "Non-sensitive action field should be preserved.");
Assert(serialized.Contains("Daily", StringComparison.Ordinal), "Non-sensitive mode field should be preserved.");
Assert(serialized.Contains("2026-02-24.md", StringComparison.Ordinal), "Non-sensitive path field should be preserved.");
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,565 @@
internal static partial class Program
{
static async Task TestEntryUnknownActionAsync()
{
var entry = NewEntry();
var response = await entry.HandleCommandAsync("""{"action":"unknown.action"}""");
using var doc = JsonDocument.Parse(response);
Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false.");
Assert(doc.RootElement.GetProperty("error").GetString()!.Contains("Unknown action"), "Expected unknown action error.");
}
static async Task TestEntryInvalidJsonAsync()
{
var entry = NewEntry();
var response = await entry.HandleCommandAsync("{\"action\":\"fragments.list\"");
using var doc = JsonDocument.Parse(response);
Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false.");
Assert(doc.RootElement.GetProperty("error").GetString()!.Contains("Invalid command JSON"), "Expected invalid JSON error.");
}
static async Task TestEntryGetMissingReturnsNullDataAsync()
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "fragments.get",
id = Guid.NewGuid().ToString(),
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true.");
Assert(doc.RootElement.GetProperty("data").ValueKind == JsonValueKind.Null, "Expected data=null for missing fragment.");
}
static async Task TestEntryCreateMissingPayloadAsync()
{
var entry = NewEntry();
var response = await entry.HandleCommandAsync("""{"action":"fragments.create"}""");
using var doc = JsonDocument.Parse(response);
Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false.");
Assert(doc.RootElement.GetProperty("error").GetString()!.Contains("payload", StringComparison.OrdinalIgnoreCase), "Expected payload validation error.");
}
static async Task TestEntryEntriesSaveMergeAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var filePath = Path.Combine(root, "2026-02-22.md");
File.WriteAllText(filePath, """
Date: 2026-02-22
## Summary
old summary text
""");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
filePath,
mode = "Daily",
content = """
Date: 2026-02-22
## Summary
new summary text
## Reflection
new reflection text
"""
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save.");
var saved = File.ReadAllText(filePath);
Assert(saved.Contains("new summary text", StringComparison.Ordinal), "Expected merged file to contain new summary text.");
Assert(!saved.Contains("old summary text", StringComparison.Ordinal), "Expected merged file to replace old summary section.");
Assert(saved.Contains("new reflection text", StringComparison.Ordinal), "Expected merged file to contain new reflection section.");
var 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))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntryEntriesLoadAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var filePath = Path.Combine(root, "2026-02-22.md");
var content = """
Date: 2026-02-22
## Summary
hello world
""";
File.WriteAllText(filePath, content);
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "entries.load",
payload = new
{
filePath
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load.");
var data = doc.RootElement.GetProperty("data");
var entryDto = data.GetProperty("Entry");
Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load.");
Assert(entryDto.GetProperty("RawContent").GetString() == content, "Expected raw content from entries.load.");
Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntryEntriesListAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "c");
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "a");
File.WriteAllText(Path.Combine(root, "ignore.txt"), "x");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "entries.list",
payload = new
{
dataDirectory = root
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected entries.list data array.");
Assert(data.GetArrayLength() == 2, "Expected entries.list to return only markdown files.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Expected entries.list sort order by file name.");
Assert(data[1].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected entries.list sort order by file name.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesMatchesRawContentAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "## Summary\nAlpha line\ncommon token");
File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "## Summary\nbeta line\nCOMMON token");
File.WriteAllText(Path.Combine(root, "2026-02-03.md"), "## Summary\ngamma only");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
query = "common token",
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
Assert(data.GetArrayLength() == 2, "Expected two entries matching query across raw content.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesWithoutQueryReturnsAllAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), "one");
File.WriteAllText(Path.Combine(root, "2026-02-02.md"), "two");
File.WriteAllText(Path.Combine(root, "ignore.txt"), "not markdown");
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries without query.");
var data = doc.RootElement.GetProperty("data");
Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
Assert(data.GetArrayLength() == 2, "Expected all markdown files to be returned when query is omitted.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesDateRangeFilterAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
WriteSearchFixtureFiles(root);
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
startDate = "2026-02-02",
endDate = "2026-02-28",
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for date-range filtered search.");
var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 1, "Expected one result for filtered date range.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-05.md", "Date-range result mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesSectionFilterAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
WriteSearchFixtureFiles(root);
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
query = "focus area",
section = "Reflection",
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for section-scoped search.");
var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 1, "Expected one section-scoped result.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Section filter result mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesTagTypeFilterAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
WriteSearchFixtureFiles(root);
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
tags = new[] { "stress" },
types = new[] { "!TRIGGER" },
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for fragment tag/type filtered search.");
var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 1, "Expected one result for fragment tag/type filters.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Tag/type filter result mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesCheckboxFilterAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
WriteSearchFixtureFiles(root);
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
@checked = new[] { "med taken" },
@unchecked = new[] { "drink water" },
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for checkbox filtered search.");
var data = doc.RootElement.GetProperty("data");
Assert(data.GetArrayLength() == 2, "Expected OR-style checkbox match across checked/unchecked filters.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntrySearchEntriesRejectsInvalidDateAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-search-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
WriteSearchFixtureFiles(root);
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
dataDirectory = root,
startDate = "2026/02/01",
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false for invalid date format.");
var error = doc.RootElement.GetProperty("error").GetString() ?? "";
Assert(error.Contains("invalid startdate value", StringComparison.OrdinalIgnoreCase), "Expected invalid startDate error.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static Task TestSidecarSearchCliFilteredAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(dataDir);
try
{
WriteSearchFixtureFiles(dataDir);
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
var (exitCode, stdout, stderr) = CaptureConsole(() => cli.RunSearchCommand(
[
"common",
"--data-dir", dataDir,
"--start-date", "2026-02-01",
"--end-date", "2026-02-28",
"--tag", "stress",
"--type", "!TRIGGER",
"--checked", "med taken",
"--section", "Summary"
]));
Assert(exitCode == 0, "Expected search CLI command to succeed.");
Assert(string.IsNullOrWhiteSpace(stderr), "Expected no stderr output for successful search CLI command.");
Assert(stdout.Contains("--- 2026-02-01 ---", StringComparison.Ordinal), "Expected matching entry header in search CLI output.");
Assert(!stdout.Contains("--- 2026-02-05 ---", StringComparison.Ordinal), "Unexpected non-matching entry in filtered search CLI output.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestSidecarSearchCliEmptyDataAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(dataDir);
try
{
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand(["--data-dir", dataDir]));
Assert(exitCode == 0, "Expected search CLI command to return success for empty data directory.");
Assert(stdout.Contains("No decrypted journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestEntrySavePayloadFileNameDeserializationAsync()
{
// Simulate what DeserializePayload does: parse JSON to JsonElement, then Deserialize<T>
var json = """{"content":"hello","mode":"Overwrite","fileName":"My Custom Name"}""";
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var element = JsonSerializer.Deserialize<JsonElement>(json);
var payload = element.Deserialize<EntrySavePayload>(options);
Assert(payload is not null, "Payload should not be null.");
Assert(payload!.Content == "hello", "Content should be deserialized.");
Assert(payload.Mode == "Overwrite", "Mode should be deserialized.");
Assert(payload.FileName == "My Custom Name", "FileName should be deserialized from camelCase JSON via JsonElement.");
Assert(payload.FilePath is null, "FilePath should be null when not provided.");
return Task.CompletedTask;
}
static async Task TestEntrySaveWithFileNameAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
// Use EntryFileService directly to test the full save path with fileName
var service = new EntryFileService(new DiskEntryFileRepository());
var payload = new EntrySavePayload(
Content: "# Custom Entry\n\nHello world",
FilePath: null,
Mode: "Overwrite",
FileName: "My Custom Name");
var result = service.SaveEntry(payload, root);
var expectedPath = Path.GetFullPath(Path.Combine(root, "My Custom Name.md"));
Assert(result.FilePath == expectedPath, $"Expected path '{expectedPath}' but got '{result.FilePath}'.");
Assert(File.Exists(expectedPath), "Custom-named file should exist on disk.");
Assert(File.ReadAllText(expectedPath).Contains("Hello world"), "File content should match.");
// Also test via Entry.HandleCommandAsync with JSON to verify full deserialization chain
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
content = "# Second Entry",
mode = "Overwrite",
fileName = "Another Custom Name"
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save with fileName.");
var savedFilePath = doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
Assert(savedFilePath.Contains("Another Custom Name.md", StringComparison.OrdinalIgnoreCase),
$"Expected file path to contain 'Another Custom Name.md' but got '{savedFilePath}'.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
}

View File

@ -0,0 +1,232 @@
internal static partial class Program
{
static Task TestCreateTrimsAsync()
{
var service = NewService();
var created = service.Create(new CreateFragmentDto(" !TRIGGER ", " stomach drop ", [" stress ", "", " body "]));
Assert(created.Type == "!TRIGGER", "Type should be trimmed.");
Assert(created.Description == "stomach drop", "Description should be trimmed.");
Assert(created.Tags.Count == 2, "Expected two normalized tags.");
Assert(created.Tags[0] == "stress" && created.Tags[1] == "body", "Tags should be trimmed and preserved.");
return Task.CompletedTask;
}
static Task TestUpdateAcceptsTypeAsync()
{
var service = NewService();
var created = service.Create(new CreateFragmentDto("!TRIGGER", "one"));
var ok = service.Update(created.Id, new UpdateFragmentDto(Type: " !FLASHBACK ", Description: " two ", Tags: [" memory "]));
Assert(ok, "Expected update to succeed.");
var updated = service.GetById(created.Id);
Assert(updated is not null, "Updated fragment should exist.");
Assert(updated!.Type == "!FLASHBACK", "Updated type should be trimmed and stored.");
Assert(updated.Description == "two", "Updated description should be trimmed and stored.");
Assert(updated.Tags.Count == 1 && updated.Tags[0] == "memory", "Updated tags should be normalized.");
return Task.CompletedTask;
}
static Task TestUpdateRejectsWhitespaceTypeAsync()
{
var service = NewService();
var created = service.Create(new CreateFragmentDto("!TRIGGER", "desc"));
try
{
_ = service.Update(created.Id, new UpdateFragmentDto(Type: " "));
}
catch (ValidationException)
{
return Task.CompletedTask;
}
throw new InvalidOperationException("Expected ValidationException for whitespace type update.");
}
static Task TestFileRepositoryPersistsAsync()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "journal-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(tempRoot, "data");
Directory.CreateDirectory(dataDir);
const string password = "smoke-test-password";
try
{
// Set up encrypted DB session
var configService = new JournalConfigService();
var dbService = new JournalDatabaseService(configService);
// First session: create a fragment
using var session1 = new DatabaseSessionService(dbService);
session1.SetPassword(password, dataDir);
var repo1 = new SqliteFragmentRepository(session1);
var service1 = new FragmentService(repo1);
var created = service1.Create(new CreateFragmentDto("!TRIGGER", "persist me", ["tag1"]));
// Second session: verify persistence
using var session2 = new DatabaseSessionService(dbService);
session2.SetPassword(password, dataDir);
var repo2 = new SqliteFragmentRepository(session2);
var service2 = new FragmentService(repo2);
var loaded = service2.GetById(created.Id);
Assert(loaded is not null, "Expected fragment to persist across repository instances.");
Assert(loaded!.Description == "persist me", "Persisted fragment description mismatch.");
Assert(loaded.Tags.Count == 1 && loaded.Tags[0] == "tag1", "Persisted tags mismatch.");
}
finally
{
if (Directory.Exists(tempRoot))
Directory.Delete(tempRoot, recursive: true);
}
return Task.CompletedTask;
}
static Task TestJournalEntryModelAsync()
{
var fragment = new Fragment("!TRIGGER", "test fragment");
var section = new ParsedSection(
"Summary",
content: ["line one", "- [x] completed thing"],
checkboxes: new Dictionary<string, bool> { ["completed thing"] = true });
var entry = new JournalEntry(
date: "2026-02-22",
fragments: [fragment],
rawContent: "raw markdown content",
sections: new Dictionary<string, ParsedSection> { ["Summary"] = section });
Assert(entry.Date == "2026-02-22", "JournalEntry date mismatch.");
Assert(entry.RawContent == "raw markdown content", "JournalEntry raw content mismatch.");
Assert(entry.Fragments.Count == 1, "JournalEntry fragment count mismatch.");
Assert(entry.Sections.Count == 1, "JournalEntry section count mismatch.");
Assert(entry.GetSection("Summary").Contains("line one"), "JournalEntry section content mismatch.");
Assert(entry.GetCheckboxState("Summary", "completed thing") is true, "JournalEntry checkbox state mismatch.");
Assert(entry.GetCheckboxState("Summary", "missing") is null, "JournalEntry checkbox should return null when missing.");
return Task.CompletedTask;
}
static Task TestMergeOverwritesMeaningfulSectionAsync()
{
var current = new JournalEntry(
date: "2026-02-22",
sections: new Dictionary<string, ParsedSection>
{
["Summary"] = new ParsedSection("Summary", ["old content"])
});
var incoming = new JournalEntry(
date: "2026-02-22",
sections: new Dictionary<string, ParsedSection>
{
["Summary"] = new ParsedSection(
"Summary",
[" ", "new content line"],
new Dictionary<string, bool> { ["new check"] = true }),
["Reflection"] = new ParsedSection("Reflection", ["reflective note"])
});
current.MergeWith(incoming);
Assert(current.GetSection("Summary").Contains("new content line"), "Meaningful section update should overwrite existing section.");
Assert(!current.GetSection("Summary").Contains("old content"), "Old section content should be replaced.");
Assert(current.GetCheckboxState("Summary", "new check") is true, "Overwritten section checkbox state should come from incoming section.");
Assert(current.GetSection("Reflection").Contains("reflective note"), "Meaningful new section should be added.");
return Task.CompletedTask;
}
static Task TestMergeIgnoresWhitespaceOnlySectionAsync()
{
var current = new JournalEntry(
date: "2026-02-22",
sections: new Dictionary<string, ParsedSection>
{
["Summary"] = new ParsedSection("Summary", ["keep existing"])
});
var incoming = new JournalEntry(
date: "2026-02-22",
sections: new Dictionary<string, ParsedSection>
{
["Summary"] = new ParsedSection("Summary", [" ", "\t", ""])
});
current.MergeWith(incoming);
Assert(current.GetSection("Summary").Contains("keep existing"), "Whitespace-only section update should be ignored.");
return Task.CompletedTask;
}
static Task TestMergeAppendsNonDuplicateFragmentsAsync()
{
var current = new JournalEntry(
date: "2026-02-22",
fragments:
[
new Fragment("!TRIGGER", "duplicate description")
]);
var incoming = new JournalEntry(
date: "2026-02-22",
fragments:
[
new Fragment("!NOTE", "duplicate description"),
new Fragment("!NOTE", "new description")
]);
current.MergeWith(incoming);
Assert(current.Fragments.Count == 2, "Expected only one new fragment to be appended.");
Assert(current.Fragments.Count(fragment => fragment.Description == "duplicate description") == 1, "Duplicate description should not be appended.");
Assert(current.Fragments.Any(fragment => fragment.Description == "new description"), "New fragment description should be appended.");
return Task.CompletedTask;
}
static Task TestToMarkdownCanonicalSectionOrderAsync()
{
var entry = new JournalEntry(
date: "2026-02-22",
sections: new Dictionary<string, ParsedSection>
{
["Reflection"] = new ParsedSection("Reflection", ["reflection body"]),
["Summary"] = new ParsedSection("Summary", ["summary body"])
});
var markdown = entry.ToMarkdown();
var summaryIdx = markdown.IndexOf("## Summary", StringComparison.Ordinal);
var reflectionIdx = markdown.IndexOf("## Reflection", StringComparison.Ordinal);
Assert(summaryIdx >= 0, "Summary header should be emitted.");
Assert(reflectionIdx >= 0, "Reflection header should be emitted.");
Assert(summaryIdx < reflectionIdx, "Sections should be emitted in canonical order.");
return Task.CompletedTask;
}
static Task TestToMarkdownFragmentFormattingAsync()
{
var fragment = new Fragment("!TRIGGER", "fragment body")
{
Time = default,
Tags = ["stress", "body"]
};
var entry = new JournalEntry(
date: "2026-02-22",
fragments: [fragment]);
var markdown = entry.ToMarkdown();
Assert(markdown.Contains("# Fragments\n", StringComparison.Ordinal), "Fragments header should be present.");
Assert(markdown.Contains("!TRIGGER #stress #body\nfragment body\n", StringComparison.Ordinal), "Fragment block format should match parity shape.");
Assert(markdown.Contains("**Date:** 2026-02-22", StringComparison.Ordinal), "Date frontmatter line should be present.");
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,153 @@
internal static partial class Program
{
static Task TestParserExtractsBoldDateAsync()
{
var content = """
---
type: journal
---
**Date:** 2026-02-22
## Summary
hello
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-01");
Assert(entry.Date == "2026-02-22", "Parser should read date from **Date:** marker.");
return Task.CompletedTask;
}
static Task TestParserExtractsPlainDateAsync()
{
var content = """
Date: 2026-02-23
## Summary
hello
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-01");
Assert(entry.Date == "2026-02-23", "Parser should read date from Date: marker.");
return Task.CompletedTask;
}
static Task TestParserFallsBackToFileStemAsync()
{
var content = """
## Summary
no explicit date
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-24");
Assert(entry.Date == "2026-02-24", "Parser should fall back to file stem when no date marker is present.");
return Task.CompletedTask;
}
static Task TestParserCapturesSectionsAsync()
{
var content = """
Date: 2026-02-25
## Summary
line one
line two
### Events / Triggers - Work
trigger line
## reflection notes
anchor line
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-01");
Assert(entry.Sections.ContainsKey("Summary"), "Parser should capture Summary section.");
Assert(entry.Sections.ContainsKey("Events / Triggers"), "Parser should capture Events / Triggers section.");
Assert(entry.Sections.ContainsKey("Reflection"), "Parser should match canonical section title by substring.");
Assert(entry.GetSection("Summary").Contains("line one"), "Summary section content mismatch.");
Assert(entry.GetSection("Events / Triggers").Contains("trigger line"), "Events / Triggers section content mismatch.");
Assert(entry.GetSection("Reflection").Contains("anchor line"), "Reflection section content mismatch.");
return Task.CompletedTask;
}
static Task TestParserIgnoresNonCanonicalHeadersAsync()
{
var content = """
## Summary
keep this
## Totally Custom Header
should not be captured
### Events / Triggers
keep this too
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-01");
Assert(entry.GetSection("Summary").Contains("keep this"), "Summary section should be captured.");
Assert(!entry.GetSection("Summary").Contains("should not be captured"), "Non-canonical section content should not bleed into previous section.");
Assert(entry.GetSection("Events / Triggers").Contains("keep this too"), "Canonical section after custom header should be captured.");
return Task.CompletedTask;
}
static Task TestParserCapturesCheckboxStatesAsync()
{
var content = """
## Summary
- [x] took medication
- [ ] drank water
* [X] wrote reflection
## Events / Triggers
- [ ] talked to manager
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-01");
Assert(entry.GetCheckboxState("Summary", "took medication") is true, "Expected checked state for '- [x]' checkbox.");
Assert(entry.GetCheckboxState("Summary", "drank water") is false, "Expected unchecked state for '- [ ]' checkbox.");
Assert(entry.GetCheckboxState("Summary", "wrote reflection") is true, "Expected checked state for '* [X]' checkbox.");
Assert(entry.GetCheckboxState("Events / Triggers", "talked to manager") is false, "Expected unchecked state in Events / Triggers section.");
Assert(entry.GetCheckboxState("Summary", "missing item") is null, "Missing checkbox text should return null.");
return Task.CompletedTask;
}
static Task TestParserCapturesMultilineFragmentsAsync()
{
var content = """
Date: 2026-02-26
## Summary
text
!TRIGGER @2026-02-26T10:15:00Z #stress #body
first line
second line
!NOTE #daily
short note
""";
var entry = JournalParser.ParseJournalContent(content, "2026-02-01");
Assert(entry.Fragments.Count == 2, "Expected two parsed fragments.");
Assert(entry.Fragments[0].Type == "!TRIGGER", "First fragment type mismatch.");
Assert(entry.Fragments[0].Description == "first line\nsecond line", "First fragment multiline description mismatch.");
Assert(entry.Fragments[0].Tags.Count == 2, "First fragment tag count mismatch.");
Assert(entry.Fragments[0].Tags[0] == "stress" && entry.Fragments[0].Tags[1] == "body", "First fragment tags mismatch.");
Assert(entry.Fragments[1].Type == "!NOTE", "Second fragment type mismatch.");
Assert(entry.Fragments[1].Description == "short note", "Second fragment description mismatch.");
Assert(entry.Fragments[1].Tags.Count == 1 && entry.Fragments[1].Tags[0] == "daily", "Second fragment tags mismatch.");
return Task.CompletedTask;
}
static Task TestParserFragmentBoundaryBehaviorAsync()
{
var content = """
!TRIGGER #a
line one
!NOTE this starts another fragment header
line two
""";
var fragments = JournalParser.ParseFragments(content);
Assert(fragments.Count == 1, "Expected one parsed fragment because second boundary line is not a valid fragment header.");
Assert(fragments[0].Description == "line one", "First fragment boundary capture mismatch.");
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,169 @@
internal static partial class Program
{
static FragmentService NewService()
{
IFragmentRepository repo = new InMemoryFragmentRepository();
return new FragmentService(repo);
}
static Entry NewEntry()
{
var dbService = new JournalDatabaseService(new JournalConfigService());
var session = new DatabaseSessionService(dbService);
return new Entry(
NewService(),
new EntrySearchService(),
new VaultStorageService(new VaultCryptoService()),
dbService,
session,
new JournalConfigService(),
new DisabledAiService("none"),
new DisabledSpeechBridgeService("none"),
new EntryFileService(new DiskEntryFileRepository()),
new ListService(new SqliteListRepository(session)),
new TodoService(new SqliteTodoRepository(session)),
new CommandLogger());
}
static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(new JournalConfigService());
static Dictionary<string, string> ReadVaultEntryTexts(string vaultPath, string password)
{
var crypto = new VaultCryptoService();
var encrypted = File.ReadAllBytes(vaultPath);
var zipBytes = crypto.DecryptData(encrypted, password);
using var stream = new MemoryStream(zipBytes);
using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var entry in archive.Entries)
{
if (string.IsNullOrEmpty(entry.Name))
continue;
using var reader = new StreamReader(entry.Open());
result[entry.Name] = reader.ReadToEnd();
}
return result;
}
static byte[] CreateZipBytes(Dictionary<string, string> files)
{
using var stream = new MemoryStream();
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var (name, content) in files)
{
var entry = archive.CreateEntry(name);
using var writer = new StreamWriter(entry.Open());
writer.Write(content);
}
}
return stream.ToArray();
}
static void WriteSearchFixtureFiles(string root)
{
File.WriteAllText(Path.Combine(root, "2026-02-01.md"), """
Date: 2026-02-01
## Summary
Alpha common
## Reflection
focus area
- [x] med taken
!TRIGGER #stress
fragment one
""");
File.WriteAllText(Path.Combine(root, "2026-02-05.md"), """
Date: 2026-02-05
## Summary
Beta common
## Reflection
other notes
- [ ] drink water
!NOTE #daily
fragment two
""");
File.WriteAllText(Path.Combine(root, "2026-03-01.md"), """
Date: 2026-03-01
## Summary
Gamma unique
## Reflection
nothing related
!NOTE #other
fragment three
""");
}
static (int ExitCode, string Stdout, string Stderr) CaptureConsole(Func<int> action)
{
var originalOut = Console.Out;
var originalError = Console.Error;
using var stdout = new StringWriter();
using var stderr = new StringWriter();
try
{
Console.SetOut(stdout);
Console.SetError(stderr);
var exitCode = action();
return (exitCode, stdout.ToString(), stderr.ToString());
}
finally
{
Console.SetOut(originalOut);
Console.SetError(originalError);
}
}
static JournalConfig BuildAiConfig(string sidecarScriptPath, int timeoutMs)
{
var baseConfig = new JournalConfigService().Current;
var pythonExe = Environment.GetEnvironmentVariable("JOURNAL_PYTHON_EXE");
if (string.IsNullOrWhiteSpace(pythonExe))
pythonExe = "python";
return baseConfig with
{
AiProvider = "python-sidecar",
PythonExecutable = pythonExe,
PythonAiSidecarPath = sidecarScriptPath,
AiSidecarTimeoutMs = timeoutMs
};
}
static async Task<List<TransportFixture>> LoadTransportFixturesAsync()
{
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "transport_cases.json");
if (!File.Exists(path))
throw new FileNotFoundException($"Transport fixture file not found: {path}");
var json = await File.ReadAllTextAsync(path);
return JsonSerializer.Deserialize<List<TransportFixture>>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? [];
}
static JsonValueKind ParseValueKind(string value) => value.Trim().ToLowerInvariant() switch
{
"array" => JsonValueKind.Array,
"object" => JsonValueKind.Object,
"null" => JsonValueKind.Null,
"string" => JsonValueKind.String,
"number" => JsonValueKind.Number,
"true" => JsonValueKind.True,
"false" => JsonValueKind.False,
_ => throw new InvalidOperationException($"Unsupported JsonValueKind '{value}' in transport fixture.")
};
static void Assert(bool condition, string message)
{
if (!condition)
throw new InvalidOperationException(message);
}
}

View File

@ -0,0 +1,48 @@
internal static partial class Program
{
static async Task TestTransportFixturesAsync()
{
var fixtures = await LoadTransportFixturesAsync();
Assert(fixtures.Count > 0, "Transport fixtures should not be empty.");
foreach (var fixture in fixtures)
{
var entry = NewEntry();
var response = await entry.HandleCommandAsync(fixture.Request);
Assert(!response.Contains('\n') && !response.Contains('\r'), $"Fixture '{fixture.Name}' returned multiline output.");
using var doc = JsonDocument.Parse(response);
var ok = doc.RootElement.GetProperty("ok").GetBoolean();
Assert(ok == fixture.ExpectOk, $"Fixture '{fixture.Name}' expected ok={fixture.ExpectOk} but got ok={ok}.");
if (fixture.ExpectOk)
{
Assert(doc.RootElement.TryGetProperty("data", out var data), $"Fixture '{fixture.Name}' expected data field.");
if (!string.IsNullOrWhiteSpace(fixture.DataKind))
{
var expectedKind = ParseValueKind(fixture.DataKind!);
Assert(data.ValueKind == expectedKind, $"Fixture '{fixture.Name}' expected data kind {expectedKind} but got {data.ValueKind}.");
}
continue;
}
Assert(doc.RootElement.TryGetProperty("error", out var error), $"Fixture '{fixture.Name}' expected error field.");
if (!string.IsNullOrWhiteSpace(fixture.ErrorContains))
{
var message = error.GetString() ?? "";
Assert(message.Contains(fixture.ErrorContains!, StringComparison.OrdinalIgnoreCase), $"Fixture '{fixture.Name}' expected error containing '{fixture.ErrorContains}'.");
}
}
}
}
sealed class TransportFixture
{
public string Name { get; init; } = "";
public string Request { get; init; } = "";
public bool ExpectOk { get; init; }
public string? DataKind { get; init; }
public string? ErrorContains { get; init; }
}

View File

@ -0,0 +1,436 @@
internal static partial class Program
{
static Task TestVaultCryptoRoundtripAsync()
{
var crypto = new VaultCryptoService();
var plaintext = "sample vault payload";
var payload = crypto.EncryptData(System.Text.Encoding.UTF8.GetBytes(plaintext), "vault-pass-123");
Assert(payload.Length == VaultCryptoService.SaltSize + VaultCryptoService.NonceSize + VaultCryptoService.TagSize + plaintext.Length, "Vault payload length should match salt+nonce+tag+ciphertext layout.");
var decrypted = crypto.DecryptData(payload, "vault-pass-123");
Assert(System.Text.Encoding.UTF8.GetString(decrypted) == plaintext, "Vault roundtrip decrypt should return original plaintext.");
return Task.CompletedTask;
}
static Task TestVaultCryptoDecryptsPythonFixtureAsync()
{
var crypto = new VaultCryptoService();
var payload = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODwABAgMEBQYHCAkKC6AErhDEMERBl7OFkG4L4oZ2JZckS0VzhxaZoVLckF7VXE+NIYXILsJ8f1I=");
var expectedPlaintext = Convert.FromBase64String("dmF1bHQgcGF5bG9hZCBleGFtcGxlCmxpbmUy");
var decrypted = crypto.DecryptData(payload, "vault-pass-123");
Assert(decrypted.SequenceEqual(expectedPlaintext), "C# decrypt should match Python-generated payload plaintext.");
return Task.CompletedTask;
}
static Task TestVaultKeyDerivationMatchesPythonAsync()
{
var crypto = new VaultCryptoService();
var salt = Enumerable.Range(0, VaultCryptoService.SaltSize).Select(i => (byte)i).ToArray();
var key = crypto.DeriveKey("vault-pass-123", salt);
var expectedKeyHex = "b29f523f28bf178f6815c6ca9ee2a588d79b3bd9a822c92a2f0dde5bc853bb52";
var actualKeyHex = Convert.ToHexString(key).ToLowerInvariant();
Assert(actualKeyHex == expectedKeyHex, "Derived key should match Python PBKDF2 fixture key.");
return Task.CompletedTask;
}
static Task TestVaultMonthlyFilenameParityAsync()
{
IVaultStorageService vaultStorage = new VaultStorageService(new VaultCryptoService());
var name = vaultStorage.GetMonthlyVaultFileName(new DateTime(2026, 2, 7));
Assert(name == "2026-02.vault", "Monthly vault filename must match yyyy-MM.vault format.");
return Task.CompletedTask;
}
static Task TestVaultLoadClearsAndExtractsAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
File.WriteAllText(Path.Combine(dataDir, "old_file.md"), "stale");
var zipBytes = CreateZipBytes(new Dictionary<string, string>
{
["2026-02-01.md"] = "hello from vault"
});
var crypto = new VaultCryptoService();
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
File.WriteAllBytes(Path.Combine(vaultDir, "2026-02.vault"), encrypted);
IVaultStorageService storage = new VaultStorageService(crypto);
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
Assert(ok, "Expected vault load success with correct password.");
Assert(!File.Exists(Path.Combine(dataDir, "old_file.md")), "Data directory should be cleared before extraction.");
var extractedPath = Path.Combine(dataDir, "2026-02-01.md");
Assert(File.Exists(extractedPath), "Expected markdown file extracted from vault archive.");
Assert(File.ReadAllText(extractedPath) == "hello from vault", "Extracted file content mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestVaultLoadWrongPasswordPreservesVaultAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
var zipBytes = CreateZipBytes(new Dictionary<string, string>
{
["2026-02-01.md"] = "hello from vault"
});
var crypto = new VaultCryptoService();
var encrypted = crypto.EncryptData(zipBytes, "vault-pass-123");
var vaultPath = Path.Combine(vaultDir, "2026-02.vault");
File.WriteAllBytes(vaultPath, encrypted);
var before = File.ReadAllBytes(vaultPath);
IVaultStorageService storage = new VaultStorageService(crypto);
var ok = storage.LoadAllVaults("wrong-password", vaultDir, dataDir);
var after = File.ReadAllBytes(vaultPath);
Assert(!ok, "Expected vault load failure with wrong password.");
Assert(before.SequenceEqual(after), "Vault file bytes should remain unchanged on wrong password.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestVaultLoadLegacyInitVaultHandlingAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
var legacyPath = Path.Combine(vaultDir, "_init_vault.vault");
File.WriteAllBytes(legacyPath, [1, 2, 3, 4]);
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
Assert(ok, "Legacy-only vault directory should still be treated as successful load state.");
Assert(!File.Exists(legacyPath), "Legacy _init_vault.vault should be removed during load.");
Assert(Directory.Exists(dataDir), "Data directory should exist after load workflow.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestVaultCurrentMonthSaveOptimizedAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb one");
File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two");
File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan one");
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
var now = new DateTime(2026, 2, 22, 12, 0, 0, DateTimeKind.Utc);
var firstSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
Assert(firstSaved, "Expected first current-month save to write vault data.");
var febVaultPath = Path.Combine(vaultDir, "2026-02.vault");
var janVaultPath = Path.Combine(vaultDir, "2026-01.vault");
Assert(File.Exists(febVaultPath), "Expected current-month vault file to be created.");
Assert(!File.Exists(janVaultPath), "Current-month save should not write non-current month vault files.");
var entries = ReadVaultEntryTexts(febVaultPath, "vault-pass-123");
Assert(entries.Count == 2, "Current-month vault should include only current-month markdown files.");
Assert(entries.ContainsKey("2026-02-01.md"), "Missing first current-month entry in vault archive.");
Assert(entries.ContainsKey("2026-02-18.md"), "Missing second current-month entry in vault archive.");
Assert(!entries.ContainsKey("2026-01-31.md"), "Current-month vault must not include previous-month files.");
var beforeSkipBytes = File.ReadAllBytes(febVaultPath);
var secondSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
var afterSkipBytes = File.ReadAllBytes(febVaultPath);
Assert(!secondSaved, "Expected unchanged current-month save to skip write.");
Assert(beforeSkipBytes.SequenceEqual(afterSkipBytes), "Vault bytes should remain unchanged when save is skipped.");
File.WriteAllText(Path.Combine(dataDir, "2026-02-18.md"), "feb two changed");
var thirdSaved = storage.SaveCurrentMonthVault("vault-pass-123", vaultDir, dataDir, now);
Assert(thirdSaved, "Expected save to run after current-month file change.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestVaultRebuildAllVaultsAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
File.WriteAllText(Path.Combine(dataDir, "2026-01-31.md"), "jan body");
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "feb body");
File.WriteAllText(Path.Combine(dataDir, "not-a-journal.md"), "should be ignored");
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
var janVaultPath = Path.Combine(vaultDir, "2026-01.vault");
var febVaultPath = Path.Combine(vaultDir, "2026-02.vault");
Assert(File.Exists(janVaultPath), "Expected January vault from rebuild flow.");
Assert(File.Exists(febVaultPath), "Expected February vault from rebuild flow.");
var janEntries = ReadVaultEntryTexts(janVaultPath, "vault-pass-123");
var febEntries = ReadVaultEntryTexts(febVaultPath, "vault-pass-123");
Assert(janEntries.Count == 1 && janEntries.ContainsKey("2026-01-31.md"), "January vault contents mismatch.");
Assert(febEntries.Count == 1 && febEntries.ContainsKey("2026-02-01.md"), "February vault contents mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestVaultClearDataDirectoryAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(dataDir);
Directory.CreateDirectory(Path.Combine(dataDir, "nested"));
try
{
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "decrypted content");
File.WriteAllText(Path.Combine(dataDir, "journal_cache.db"), "cache");
File.WriteAllText(Path.Combine(dataDir, "nested", "tmp.txt"), "temp");
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
storage.ClearDataDirectory(dataDir);
Assert(Directory.Exists(dataDir), "Data directory should be recreated after cleanup.");
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after cleanup.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static async Task TestEntryVaultLoadAllEmptyAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
try
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "vault.load_all",
payload = new
{
password = "vault-pass-123",
vaultDirectory = vaultDir,
dataDirectory = dataDir,
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for empty vault directory load.");
Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected vault.load_all data=true for empty vault directory.");
Assert(Directory.Exists(dataDir), "Expected data directory to be created by load workflow.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static async Task TestEntryVaultClearDataDirectoryAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-smoke", Guid.NewGuid().ToString("N"));
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(dataDir);
File.WriteAllText(Path.Combine(dataDir, "tmp.md"), "x");
try
{
var entry = NewEntry();
var request = JsonSerializer.Serialize(new
{
action = "vault.clear_data_directory",
payload = new
{
dataDirectory = dataDir,
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for clear_data_directory.");
Assert(doc.RootElement.GetProperty("data").GetBoolean(), "Expected clear_data_directory result=true.");
Assert(Directory.Exists(dataDir), "Expected data directory to exist after clear.");
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Expected data directory to be empty after clear.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
static Task TestSidecarVaultCliLoadAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
try
{
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
var exitCode = cli.RunVaultCommand(["load", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
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.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestSidecarVaultCliSaveAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
File.WriteAllText(Path.Combine(dataDir, "2026-02-22.md"), "entry body");
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService()), new EntrySearchService(), new JournalConfigService());
var exitCode = cli.RunVaultCommand(["save", "--password", "vault-pass-123", "--vault-dir", vaultDir, "--data-dir", dataDir]);
Assert(exitCode == 0, "Expected vault save CLI command to succeed.");
Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault file to be written by save CLI command.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestVaultCustomEntryRoundtripAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-vault-smoke", Guid.NewGuid().ToString("N"));
var vaultDir = Path.Combine(root, "vault");
var dataDir = Path.Combine(root, "data");
Directory.CreateDirectory(vaultDir);
Directory.CreateDirectory(dataDir);
try
{
// Create both date-named and custom-named entries
File.WriteAllText(Path.Combine(dataDir, "2026-02-01.md"), "date entry");
File.WriteAllText(Path.Combine(dataDir, "My Custom Entry.md"), "custom entry body");
File.WriteAllText(Path.Combine(dataDir, "Work Notes.md"), "work notes body");
// Rebuild vaults (simulates app close)
IVaultStorageService storage = new VaultStorageService(new VaultCryptoService());
storage.RebuildAllVaults("vault-pass-123", vaultDir, dataDir);
// Verify custom vault was created
var customVaultPath = Path.Combine(vaultDir, "_custom_entries.vault");
Assert(File.Exists(customVaultPath), "Expected _custom_entries.vault to be created.");
Assert(File.Exists(Path.Combine(vaultDir, "2026-02.vault")), "Expected monthly vault for date entry.");
// Clear data directory (simulates app close step 2)
storage.ClearDataDirectory(dataDir);
Assert(!Directory.EnumerateFileSystemEntries(dataDir).Any(), "Data directory should be empty after clear.");
// Load vaults (simulates app restart)
var ok = storage.LoadAllVaults("vault-pass-123", vaultDir, dataDir);
Assert(ok, "Expected vault load to succeed.");
// Verify all entries are restored
Assert(File.Exists(Path.Combine(dataDir, "2026-02-01.md")), "Date entry should be restored from vault.");
Assert(File.Exists(Path.Combine(dataDir, "My Custom Entry.md")), "Custom entry should be restored from vault.");
Assert(File.Exists(Path.Combine(dataDir, "Work Notes.md")), "Second custom entry should be restored from vault.");
Assert(File.ReadAllText(Path.Combine(dataDir, "My Custom Entry.md")) == "custom entry body", "Custom entry content mismatch.");
Assert(File.ReadAllText(Path.Combine(dataDir, "Work Notes.md")) == "work notes body", "Second custom entry content mismatch.");
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
}

File diff suppressed because it is too large Load Diff