Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs
T
mika kuns 2ebdadff08 fix(worker): surface the real reason a Claude run failed instead of a generic exit-code message
StreamAnalyzer now reads subtype/terminal_reason/errors from the CLI's result
event, and TaskRunner builds a specific ErrorMarkdown from them: max_turns names
the turn budget and points at set_task_config + requeue, api_error passes
through the provider's own message (which carries the reset time), and any
other terminal_reason is appended to the previous generic text instead of
staying invisible. Falls through to the old "exited with code N and no result"
text when there's no terminal_reason at all (a real crash).
2026-08-05 15:55:09 +02:00

200 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClaudeDo.Worker.Runner;
namespace ClaudeDo.Worker.Tests.Runner;
public sealed class StreamAnalyzerTests
{
[Fact]
public void Extracts_Result_Markdown()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"## Done","session_id":"sess-1"}""");
var result = analyzer.GetResult();
Assert.Equal("## Done", result.ResultMarkdown);
Assert.Equal("sess-1", result.SessionId);
}
[Fact]
public void Extracts_Structured_Output()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"ok","structured_output":{"summary":"all good"},"session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal("ok", result.ResultMarkdown);
Assert.Contains("all good", result.StructuredOutputJson);
}
[Fact]
public void Counts_Assistant_Turns()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"assistant","message":"hi"}""");
analyzer.ProcessLine("""{"type":"assistant","message":"working on it"}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal(2, result.TurnCount);
}
[Fact]
public void Accumulates_Token_Usage()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"stream_event","event":{"type":"message_start","message":{"usage":{"input_tokens":100,"output_tokens":50}}}}""");
analyzer.ProcessLine("""{"type":"stream_event","event":{"type":"message_start","message":{"usage":{"input_tokens":200,"output_tokens":80}}}}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal(300, result.TokensIn);
Assert.Equal(130, result.TokensOut);
}
[Fact]
public void Counts_Api_Retry_Events()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"system","subtype":"api_retry","attempt":1,"error":"rate_limit"}""");
analyzer.ProcessLine("""{"type":"system","subtype":"api_retry","attempt":2,"error":"rate_limit"}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal(2, result.ApiRetryCount);
}
[Fact]
public void Malformed_Json_Is_Ignored()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("not json {{{");
analyzer.ProcessLine("");
analyzer.ProcessLine(" ");
var result = analyzer.GetResult();
Assert.Null(result.ResultMarkdown);
Assert.Equal(0, result.TurnCount);
}
[Fact]
public void No_Result_Event_Returns_Null_Fields()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"assistant","message":"hi"}""");
var result = analyzer.GetResult();
Assert.Null(result.ResultMarkdown);
Assert.Null(result.SessionId);
}
[Fact]
public void Token_Usage_From_Result_Event()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1","usage":{"input_tokens":150,"output_tokens":75,"cache_read_input_tokens":0}}""");
var result = analyzer.GetResult();
Assert.Equal(150, result.TokensIn);
Assert.Equal(75, result.TokensOut);
}
[Fact]
public void Result_Usage_Overrides_Stream_Event_Accumulation()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"stream_event","event":{"type":"message_start","message":{"usage":{"input_tokens":10,"output_tokens":5}}}}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1","usage":{"input_tokens":200,"output_tokens":90}}""");
var result = analyzer.GetResult();
Assert.Equal(200, result.TokensIn);
Assert.Equal(90, result.TokensOut);
}
[Fact]
public void Empty_Result_Falls_Back_To_Structured_Output_Summary()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"","structured_output":{"summary":"Task completed successfully.","data":{}},"session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal("Task completed successfully.", result.ResultMarkdown);
Assert.Contains("summary", result.StructuredOutputJson);
}
[Fact]
public void Empty_Result_Falls_Back_To_Full_Json_When_No_Summary()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"","structured_output":{"output":"42"},"session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Contains("output", result.ResultMarkdown);
Assert.Contains("42", result.ResultMarkdown);
}
[Fact]
public void Collects_Blocked_Markers_From_Assistant_Text()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"working\nCLAUDEDO_BLOCKED: missing API key\nmoving on"}]}}""");
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: cannot reach db"}]}}""");
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal(2, result.Blocks.Count);
Assert.Equal("missing API key", result.Blocks[0]);
Assert.Equal("cannot reach db", result.Blocks[1]);
}
[Fact]
public void Strips_Blocked_Markers_From_Result_Text()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"All set.\nCLAUDEDO_BLOCKED: no creds\nDone.","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.DoesNotContain("CLAUDEDO_BLOCKED", result.ResultMarkdown);
Assert.Single(result.Blocks);
Assert.Equal("no creds", result.Blocks[0]);
}
[Fact]
public void No_Markers_Means_Empty_Blocks()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
Assert.Empty(analyzer.GetResult().Blocks);
}
[Fact]
public void Extracts_Terminal_Reason_And_Errors_For_Max_Turns()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","subtype":"error_max_turns","terminal_reason":"max_turns","errors":["Reached maximum number of turns (30)"],"session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal("max_turns", result.TerminalReason);
Assert.Equal("error_max_turns", result.ResultSubtype);
Assert.Single(result.Errors);
Assert.Equal("Reached maximum number of turns (30)", result.Errors[0]);
}
[Fact]
public void Extracts_Terminal_Reason_And_Result_Text_For_Api_Error()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","terminal_reason":"api_error","result":"You've hit your session limit resets 1pm (Europe/Berlin)","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Equal("api_error", result.TerminalReason);
Assert.Contains("session limit", result.ResultMarkdown);
}
[Fact]
public void No_Terminal_Reason_Fields_Means_Null_And_Empty()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Null(result.TerminalReason);
Assert.Null(result.ResultSubtype);
Assert.Empty(result.Errors);
}
[Fact]
public void Duplicate_Marker_In_Assistant_And_Result_Is_Collected_Once()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"assistant","message":{"content":[{"type":"text","text":"CLAUDEDO_BLOCKED: no creds"}]}}""");
analyzer.ProcessLine("""{"type":"result","result":"summary\nCLAUDEDO_BLOCKED: no creds","session_id":"s1"}""");
var result = analyzer.GetResult();
Assert.Single(result.Blocks);
Assert.Equal("no creds", result.Blocks[0]);
}
}