journal/Journal.SmokeTests/Program.EntryTests.cs
Jacob Schmidt 0d77300c22 feat: Project Journal backend monorepo
Monorepo with centralized build props, npm workspaces, LlamaSharp AI,
SQLite/SQLCipher storage, Svelte frontend, and unified smoke tests.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-03-02 20:56:26 -06:00

623 lines
24 KiB
C#

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 entry = NewEntry();
var firstPath = await SaveEntryForTestAsync(entry, "2026-02-22", """
Date: 2026-02-22
## Summary
old summary text
""");
var mergeRequest = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
filePath = firstPath,
mode = "Daily",
content = """
Date: 2026-02-22
## Summary
new summary text
## Reflection
new reflection text
"""
}
});
var mergeResponse = await entry.HandleCommandAsync(mergeRequest);
using var mergeDoc = JsonDocument.Parse(mergeResponse);
Assert(mergeDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save daily merge.");
var loadedAfterMerge = await LoadEntryForTestAsync(entry, firstPath);
Assert(loadedAfterMerge.Contains("new summary text", StringComparison.Ordinal), "Expected merged entry to contain new summary text.");
Assert(!loadedAfterMerge.Contains("old summary text", StringComparison.Ordinal), "Expected merged entry to replace old summary section.");
Assert(loadedAfterMerge.Contains("new reflection text", StringComparison.Ordinal), "Expected merged entry to contain new reflection section.");
var fragmentSaveRequest = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
filePath = firstPath,
mode = "Fragment",
content = "!NOTE\nfragment append text"
}
});
var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest);
using var fragmentDoc = JsonDocument.Parse(fragmentResponse);
Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode.");
var loadedAfterFragment = await LoadEntryForTestAsync(entry, firstPath);
Assert(loadedAfterFragment.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved entry.");
}
static async Task TestEntryEntriesLoadAsync()
{
var entry = NewEntry();
var content = """
Date: 2026-02-22
## Summary
hello world
""";
var filePath = await SaveEntryForTestAsync(entry, "2026-02-22", content);
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.");
}
static async Task TestEntryEntriesListAsync()
{
var entry = NewEntry();
await SaveEntryForTestAsync(entry, "2026-02-03", "c");
await SaveEntryForTestAsync(entry, "2026-02-01", "a");
var request = JsonSerializer.Serialize(new
{
action = "entries.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 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 seeded markdown entries.");
Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Expected entries.list sort order by file name.");
Assert(data[1].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected entries.list sort order by file name.");
}
static async Task TestEntryTemplatesCrudExcludesFromEntriesListAsync()
{
var entry = NewEntry();
await SaveEntryForTestAsync(entry, "2026-02-03", "daily entry");
var saveRequest = JsonSerializer.Serialize(new
{
action = "templates.save",
payload = new
{
name = "Weekly Review",
content = "# Weekly Review\n\n## Wins\n- one"
}
});
var saveResponse = await entry.HandleCommandAsync(saveRequest);
using var saveDoc = JsonDocument.Parse(saveResponse);
Assert(saveDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.save.");
var templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
Assert(templatePath.EndsWith("Weekly%20Review.template.md", StringComparison.OrdinalIgnoreCase), "Template path should be canonical db://template path.");
var listTemplatesRequest = JsonSerializer.Serialize(new
{
action = "templates.list",
payload = new { }
});
var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest);
using var listTemplatesDoc = JsonDocument.Parse(listTemplatesResponse);
Assert(listTemplatesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.list.");
var templateItems = listTemplatesDoc.RootElement.GetProperty("data");
Assert(templateItems.GetArrayLength() == 1, "Expected one template in templates.list.");
Assert(templateItems[0].GetProperty("FileName").GetString() == "Weekly Review.template.md", "Template file name mismatch.");
var listEntriesRequest = JsonSerializer.Serialize(new
{
action = "entries.list",
payload = new { }
});
var listEntriesResponse = await entry.HandleCommandAsync(listEntriesRequest);
using var listEntriesDoc = JsonDocument.Parse(listEntriesResponse);
Assert(listEntriesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
var entryItems = listEntriesDoc.RootElement.GetProperty("data");
Assert(entryItems.GetArrayLength() == 1, "Expected entries.list to exclude template files.");
Assert(entryItems[0].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected only daily entry file in entries.list.");
var loadTemplateRequest = JsonSerializer.Serialize(new
{
action = "templates.load",
payload = new
{
filePath = templatePath
}
});
var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest);
using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse);
Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load.");
var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? "";
Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result.");
var deleteTemplateRequest = JsonSerializer.Serialize(new
{
action = "templates.delete",
payload = new
{
filePath = templatePath
}
});
var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest);
using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse);
Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete.");
Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true.");
}
static async Task TestEntrySearchEntriesMatchesRawContentAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
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.");
}
static async Task TestEntrySearchEntriesWithoutQueryReturnsAllAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new { }
});
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() == 3, "Expected all seeded markdown entries to be returned when query is omitted.");
}
static async Task TestEntrySearchEntriesDateRangeFilterAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
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.");
}
static async Task TestEntrySearchEntriesSectionFilterAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
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.");
}
static async Task TestEntrySearchEntriesTagTypeFilterAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
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.");
}
static async Task TestEntrySearchEntriesCheckboxFilterAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
@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.");
}
static async Task TestEntrySearchEntriesRejectsInvalidDateAsync()
{
var entry = NewEntry();
await SeedSearchFixtureEntriesAsync(entry);
var request = JsonSerializer.Serialize(new
{
action = "search.entries",
payload = new
{
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.");
}
static Task TestSidecarSearchCliFilteredAsync()
{
var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
var config = NewConfigService(root);
var dbService = new JournalDatabaseService(config);
using var session = new DatabaseSessionService(dbService);
session.SetPassword("vault-pass-123");
var repo = new SqliteEntryFileRepository(session);
var entryFiles = new EntryFileService(repo);
var searchService = new EntrySearchService(repo);
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles);
try
{
entryFiles.SaveEntry(new EntrySavePayload("""
Date: 2026-02-01
## Summary
common
- [x] med taken
!TRIGGER #stress
matched body
""", Mode: "Overwrite", FileName: "2026-02-01"));
entryFiles.SaveEntry(new EntrySavePayload("""
Date: 2026-02-05
## Summary
common
- [ ] drink water
!NOTE #daily
non match
""", Mode: "Overwrite", FileName: "2026-02-05"));
var (exitCode, stdout, stderr) = CaptureConsole(() => cli.RunSearchCommand(
[
"common",
"--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
{
session.Dispose();
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 config = NewConfigService(root);
var dbService = new JournalDatabaseService(config);
using var session = new DatabaseSessionService(dbService);
session.SetPassword("vault-pass-123");
var repo = new SqliteEntryFileRepository(session);
var entryFiles = new EntryFileService(repo);
var searchService = new EntrySearchService(repo);
var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles);
try
{
var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand([]));
Assert(exitCode == 0, "Expected search CLI command to return success for empty data store.");
Assert(stdout.Contains("No journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message.");
}
finally
{
session.Dispose();
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
return Task.CompletedTask;
}
static Task TestEntrySavePayloadFileNameDeserializationAsync()
{
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"));
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
{
var payload = new EntrySavePayload(
Content: "# Custom Entry\n\nHello world",
FilePath: null,
Mode: "Overwrite",
FileName: "My Custom Name");
var result = service.SaveEntry(payload);
Assert(result.FilePath.StartsWith("db://entry/", StringComparison.OrdinalIgnoreCase), "Expected canonical db://entry path.");
Assert(result.FilePath.Contains("My%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), "Expected escaped custom name in db path.");
var loaded = service.LoadEntry(result.FilePath);
Assert(loaded.Entry.RawContent.Contains("Hello world", StringComparison.Ordinal), "Stored content should match.");
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%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase),
$"Expected file path to contain 'Another%20Custom%20Name.md' but got '{savedFilePath}'.");
}
finally
{
session.Dispose();
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
private static async Task<string> SaveEntryForTestAsync(Entry entry, string fileStem, string content)
{
var request = JsonSerializer.Serialize(new
{
action = "entries.save",
payload = new
{
content,
mode = "Overwrite",
fileName = fileStem
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test seed entries.save.");
return doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
}
private static async Task<string> LoadEntryForTestAsync(Entry entry, string filePath)
{
var request = JsonSerializer.Serialize(new
{
action = "entries.load",
payload = new
{
filePath
}
});
var response = await entry.HandleCommandAsync(request);
using var doc = JsonDocument.Parse(response);
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test entries.load.");
return doc.RootElement.GetProperty("data").GetProperty("Entry").GetProperty("RawContent").GetString() ?? "";
}
private static async Task SeedSearchFixtureEntriesAsync(Entry entry)
{
await SaveEntryForTestAsync(entry, "2026-02-01", """
Date: 2026-02-01
## Summary
Alpha line
common token
## Reflection
focus area
- [x] med taken
!TRIGGER #stress
fragment one
""");
await SaveEntryForTestAsync(entry, "2026-02-05", """
Date: 2026-02-05
## Summary
beta line
COMMON token
## Reflection
other notes
- [ ] drink water
!NOTE #daily
fragment two
""");
await SaveEntryForTestAsync(entry, "2026-03-01", """
Date: 2026-03-01
## Summary
gamma only
## Reflection
nothing related
!NOTE #other
fragment three
""");
}
}