using ClaudeMailbox; using ClaudeMailbox.Config; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace ClaudeMailbox.Tests; /// /// Spins up a full ClaudeMailbox WebApplication on an ephemeral port against a temp SQLite file. /// Disposable — removes the DB and stops the host on dispose. /// public sealed class TestHost : IAsyncDisposable { private readonly WebApplication _app; private readonly string _dbPath; public HttpClient Client { get; } public string BaseUrl { get; } public string DbPath => _dbPath; private TestHost(WebApplication app, string dbPath, string baseUrl) { _app = app; _dbPath = dbPath; BaseUrl = baseUrl; Client = new HttpClient { BaseAddress = new Uri(baseUrl) }; } public static async Task StartAsync() { var dbPath = Path.Combine(Path.GetTempPath(), $"claude-mailbox-test-{Guid.NewGuid():N}.db"); var cfg = new DaemonConfig { Port = 0, BindAddress = "127.0.0.1", DbPath = dbPath, }; var builder = ServerHost.CreateBuilder(cfg); builder.WebHost.UseUrls("http://127.0.0.1:0"); var app = builder.Build(); ServerHost.ConfigurePipeline(app); await app.StartAsync(); // Discover the port Kestrel picked. var server = app.Services.GetRequiredService(); var feature = server.Features.Get(); var url = feature?.Addresses.FirstOrDefault() ?? throw new InvalidOperationException("No bound URL after start."); return new TestHost(app, dbPath, url); } public HttpClient NewClientFor(string mailboxName) { var c = new HttpClient { BaseAddress = new Uri(BaseUrl) }; c.DefaultRequestHeaders.Add("X-Mailbox", mailboxName); return c; } public async ValueTask DisposeAsync() { Client.Dispose(); await _app.StopAsync(); await _app.DisposeAsync(); // Allow SQLite handle to release before deleting. GC.Collect(); GC.WaitForPendingFinalizers(); foreach (var ext in new[] { "", "-wal", "-shm" }) { var path = _dbPath + ext; if (File.Exists(path)) { try { File.Delete(path); } catch { /* best-effort cleanup */ } } } } }