StreamingClaudeSession drives claude --input-format stream-json over a kept- open stdin: sends user messages, interrupts the in-flight turn via the verified control_request protocol, and tracks turn state from result events (treating an interrupt-aborted error_during_execution result as turn-ended). IClaudeStreamTransport abstracts the process I/O so it is unit-tested with a fake (no real claude). LiveSessionRegistry maps taskId -> live session for the hub to route into. Backs the upcoming in-app interactive sessions; autonomous task execution untouched.
93 lines
2.6 KiB
C#
93 lines
2.6 KiB
C#
using ClaudeDo.Worker.Runner;
|
|
using ClaudeDo.Worker.Runner.Interfaces;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Runner;
|
|
|
|
public sealed class LiveSessionRegistryTests
|
|
{
|
|
private sealed class FakeLiveSession : ILiveSession
|
|
{
|
|
public bool StopCalled { get; private set; }
|
|
public bool IsTurnInFlight => false;
|
|
|
|
public Task SendUserMessageAsync(string text, CancellationToken ct) => Task.CompletedTask;
|
|
public Task InterruptAsync(CancellationToken ct) => Task.CompletedTask;
|
|
|
|
public Task StopAsync()
|
|
{
|
|
StopCalled = true;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
}
|
|
|
|
[Fact]
|
|
public void Register_ThenTryGet_ReturnsSession()
|
|
{
|
|
var registry = new LiveSessionRegistry();
|
|
var session = new FakeLiveSession();
|
|
|
|
registry.Register("task-1", session);
|
|
|
|
Assert.True(registry.TryGet("task-1", out var retrieved));
|
|
Assert.Same(session, retrieved);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGet_Missing_ReturnsFalse()
|
|
{
|
|
var registry = new LiveSessionRegistry();
|
|
|
|
Assert.False(registry.TryGet("no-such-task", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Unregister_RemovesSession()
|
|
{
|
|
var registry = new LiveSessionRegistry();
|
|
registry.Register("task-1", new FakeLiveSession());
|
|
registry.Unregister("task-1");
|
|
|
|
Assert.False(registry.TryGet("task-1", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Register_WhenSessionAlreadyExists_StopsPreviousSession()
|
|
{
|
|
var registry = new LiveSessionRegistry();
|
|
var first = new FakeLiveSession();
|
|
var second = new FakeLiveSession();
|
|
|
|
registry.Register("task-1", first);
|
|
registry.Register("task-1", second);
|
|
|
|
// Give the fire-and-forget stop a tick to run.
|
|
await Task.Delay(50);
|
|
|
|
Assert.True(first.StopCalled);
|
|
Assert.True(registry.TryGet("task-1", out var retrieved));
|
|
Assert.Same(second, retrieved);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StopAsync_StopsAndRemovesSession()
|
|
{
|
|
var registry = new LiveSessionRegistry();
|
|
var session = new FakeLiveSession();
|
|
registry.Register("task-1", session);
|
|
|
|
await registry.StopAsync("task-1");
|
|
|
|
Assert.True(session.StopCalled);
|
|
Assert.False(registry.TryGet("task-1", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StopAsync_MissingTask_DoesNotThrow()
|
|
{
|
|
var registry = new LiveSessionRegistry();
|
|
await registry.StopAsync("no-such-task"); // should not throw
|
|
}
|
|
}
|