chore(claude-do): Mission Control: allgemeiner Session-Handoff an eine frische
## Ziel Jede Mission-Control-Session (Ad-hoc UND Task-Session) soll ihre Arbeit an eine frische Session uebergeben koennen: "gib das ab an eine neue Session". Die alte Kachel wird geschlossen, an derselben Position oeffnet eine frische Session im selben cwd, die mit einem Brief startet, den die abgebende Session selbst geschrieben hat. Motivation: eine lange Session mit vollem/verrottetem Kontex ClaudeDo-Task: 8b846deb-ad10-4243-9272-9aea2a01ec93
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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<InvalidOperationException>(() =>
|
||||
BuildSut(Request(paneId)).HandoffSession("brief", CancellationToken.None));
|
||||
Assert.Empty(_hubContext.Proxy.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandoffSession_EmptyBrief_Throws()
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user