feat(worker): persistent streaming Claude session + live session registry

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.
This commit is contained in:
Mika Kuns
2026-06-26 08:56:19 +02:00
parent 10342bc562
commit d8a043fae7
8 changed files with 623 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
using ClaudeDo.Worker.Runner.Interfaces;
namespace ClaudeDo.Worker.Tests.Infrastructure;
public sealed class FakeClaudeStreamTransport : IClaudeStreamTransport
{
public List<string> Written { get; } = [];
public bool Killed { get; private set; }
public bool Started { get; private set; }
public event Func<string, Task>? LineReceived;
public event Func<string, Task>? StderrReceived;
public Task StartAsync(IReadOnlyList<string> args, string workingDirectory, CancellationToken ct)
{
Started = true;
return Task.CompletedTask;
}
public Task WriteLineAsync(string jsonLine, CancellationToken ct)
{
Written.Add(jsonLine);
return Task.CompletedTask;
}
public void Kill() => Killed = true;
public Task WaitForExitAsync() => Task.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
// Test helper: push a simulated stdout line to all LineReceived subscribers.
public async Task PushLineAsync(string line)
{
var handler = LineReceived;
if (handler is not null)
await handler(line);
}
// Test helper: push a simulated stderr line.
public async Task PushStderrAsync(string line)
{
var handler = StderrReceived;
if (handler is not null)
await handler(line);
}
}