journal/Journal.SmokeTests/Program.Shared.cs
Jacob Schmidt 192e6e3891 feat: add AI coaching, conversation persistence, and LLamaSharp integration
- Add Journal.AI project with LLamaSharp-based AI service (Phi-3 model)
- Implement coach sessions (daily check-in, evening review, weekly review)
- Add conversation CRUD with SQLCipher persistence
- AI chat with full conversation history for context-aware replies
- Frontend: CoachPanel, AI stores, conversation stores, side panel UI
- Conversation list with create, rename, and delete support
- Fix Phi-3 output quality (system prompt leaking, token cleanup, JSON filtering)
- Fix CREATEDRAFT kind override in coach sessions

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-03-01 16:07:59 -06:00

211 lines
6.8 KiB
C#

internal static partial class Program
{
static FragmentService NewService()
{
IFragmentRepository repo = new InMemoryFragmentRepository();
return new FragmentService(repo);
}
static Entry NewEntry(bool unlocked = true, string password = "vault-pass-123", string? root = null)
{
var config = NewConfigService(root);
var dbService = new JournalDatabaseService(config);
var session = new DatabaseSessionService(dbService);
if (unlocked)
session.SetPassword(password);
var entryRepo = new SqliteEntryFileRepository(session);
return new Entry(
NewService(),
new EntrySearchService(entryRepo),
new VaultStorageService(new VaultCryptoService(), dbService),
dbService,
session,
config,
new DisabledAiService("none"),
new DisabledSpeechBridgeService("none"),
new DisabledS2TService(),
new EntryFileService(entryRepo),
new ListService(new SqliteListRepository(session)),
new TodoService(new SqliteTodoRepository(session)),
new DisabledCoachService(),
new ConversationService(new SqliteConversationRepository(session)),
new CommandLogger());
}
static Entry NewLockedEntry() => NewEntry(unlocked: false);
static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(NewConfigService());
static IJournalConfigService NewConfigService(string? root = null, string? databaseFilename = null)
{
var baseConfig = new JournalConfigService().Current;
var normalizedRoot = Path.GetFullPath(root ?? Path.Combine(Path.GetTempPath(), "journal-smoke", Guid.NewGuid().ToString("N")));
var appDirectory = Path.Combine(normalizedRoot, "journal");
var vaultDirectory = Path.Combine(appDirectory, "vault");
var logDirectory = Path.Combine(normalizedRoot, "logs");
Directory.CreateDirectory(vaultDirectory);
Directory.CreateDirectory(logDirectory);
var config = baseConfig with
{
ProjectRoot = normalizedRoot,
AppDirectory = appDirectory,
VaultDirectory = vaultDirectory,
LogDirectory = logDirectory,
PidFile = Path.Combine(logDirectory, "nicegui_server.pid"),
ServerControlFile = Path.Combine(logDirectory, "server_control.action"),
DatabaseFilename = databaseFilename ?? "journal_cache.db"
};
return new FixedConfigService(config);
}
private sealed class FixedConfigService(JournalConfig config) : IJournalConfigService
{
public JournalConfig Current => config;
}
static Dictionary<string, string> ReadVaultEntryTexts(string vaultPath, string password)
{
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);
}
}