using ClaudeDo.Worker.External; using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.AspNetCore.Http; namespace ClaudeDo.Worker.Tests.External; public sealed class SessionHandoffMcpToolsTests : IDisposable { private readonly CapturingHubContext _hubContext = new(); private readonly string _paneId = Guid.NewGuid().ToString("n"); public void Dispose() { try { Directory.Delete(InteractiveLaunchSpecService.PaneSessionDir(_paneId), true); } catch { } } private static HttpContext Request(string? paneId, string? sessionId = null) { var http = new DefaultHttpContext(); if (paneId is not null) http.Request.Headers[SessionHandoffMcpTools.PaneHeader] = paneId; if (sessionId is not null) http.Request.Headers[SessionHandoffMcpTools.SessionHeader] = sessionId; return http; } private SessionHandoffMcpTools BuildSut(HttpContext http) => new(new HubBroadcaster(_hubContext), new HttpContextAccessor { HttpContext = http }); [Fact] public async Task HandoffSession_IdentityFromHeaders_WritesBriefWithOrigin_AndBroadcasts() { var result = await BuildSut(Request(_paneId, "sess-1")).HandoffSession(" do the thing ", CancellationToken.None); Assert.True(result.Requested); Assert.StartsWith(InteractiveLaunchSpecService.PaneSessionDir(_paneId), result.BriefPath); Assert.Equal("Handed off from session `sess-1`.\n\ndo the thing\n", File.ReadAllText(result.BriefPath)); var call = Assert.Single(_hubContext.Proxy.Calls); Assert.Equal("SessionHandoffRequested", call.Method); Assert.Equal(new object?[] { _paneId, result.BriefPath }, call.Args); } [Theory] [InlineData(null)] [InlineData("not-a-guid")] [InlineData(@"..\..\evil")] public async Task HandoffSession_MissingOrInvalidPaneHeader_ThrowsWithoutBroadcast(string? paneId) { await Assert.ThrowsAsync(() => BuildSut(Request(paneId)).HandoffSession("brief", CancellationToken.None)); Assert.Empty(_hubContext.Proxy.Calls); } [Fact] public async Task HandoffSession_EmptyBrief_Throws() { await Assert.ThrowsAsync(() => BuildSut(Request(_paneId)).HandoffSession(" ", CancellationToken.None)); Assert.Empty(_hubContext.Proxy.Calls); } // The per-tile entry lists only handoff_session; the plain entry every session has hides it, so // the tool is never duplicated and never offered where it can't identify the tile. [Theory] [InlineData("handoff_session", true, true)] [InlineData("handoff_session", false, false)] [InlineData("get_task", true, false)] [InlineData("get_task", false, true)] public void IsVisible_SplitsToolsByPaneHeader(string tool, bool paneRequest, bool expected) { Assert.Equal(expected, SessionHandoffMcpTools.IsVisible(tool, Request(paneRequest ? _paneId : null))); } }