Runner stack (non-worktree path): IClaudeProcess + ClaudeProcess spawning the CLI with --output-format stream-json, prompt via stdin, parses the final type:"result" line into RunResult. LogWriter appends ndjson to ~/.todo-app/logs/<taskId>.ndjson. TaskRunner orchestrates DB transitions (MarkRunning -> MarkDone/Failed) and pushes TaskStarted/Message/Finished/ Updated via HubBroadcaster. Worktree-backed lists short-circuit with a "Slice E" failure message until git support lands. QueueService (BackgroundService) holds two in-memory slots (_queueSlot + _overrideSlot) guarded by a lock. Uses PeriodicTimer + SemaphoreSlim wake signal so WakeQueue() triggers an instant pickup. RunNow throws InvalidOperationException when override busy; CancelTask cancels the linked CTS which kills the child process tree. WorkerHub extended with GetActive, RunNow (translated to HubException variants), CancelTask, WakeQueue. HubBroadcaster exposes typed push methods. Tests: 26 pass (12 new). QueueServiceTests cover override-busy, schedule-filter, FIFO sequentiality, cancellation, plus a FakeClaudeProcess that blocks on a TCS for deterministic slot-state assertions. MessageParserTests cover result extraction + malformed/non-result lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
54 lines
1.5 KiB
C#
54 lines
1.5 KiB
C#
using ClaudeDo.Worker.Runner;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Runner;
|
|
|
|
public sealed class MessageParserTests
|
|
{
|
|
[Fact]
|
|
public void WellFormed_Result_Line_Extracts_Result()
|
|
{
|
|
var line = """{"type":"result","result":"Hello **world**"}""";
|
|
Assert.True(MessageParser.TryExtractResult(line, out var result));
|
|
Assert.Equal("Hello **world**", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Non_Result_Type_Returns_False()
|
|
{
|
|
var line = """{"type":"assistant","message":"hi"}""";
|
|
Assert.False(MessageParser.TryExtractResult(line, out var result));
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_Type_Property_Returns_False()
|
|
{
|
|
var line = """{"result":"data"}""";
|
|
Assert.False(MessageParser.TryExtractResult(line, out var result));
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Malformed_Json_Returns_False_No_Throw()
|
|
{
|
|
var line = "this is not json {{{";
|
|
Assert.False(MessageParser.TryExtractResult(line, out var result));
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_Line_Returns_False()
|
|
{
|
|
Assert.False(MessageParser.TryExtractResult("", out _));
|
|
Assert.False(MessageParser.TryExtractResult(" ", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void Null_Result_Value_Returns_True_With_Null()
|
|
{
|
|
var line = """{"type":"result","result":null}""";
|
|
Assert.True(MessageParser.TryExtractResult(line, out var result));
|
|
Assert.Null(result);
|
|
}
|
|
}
|