Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Runner/StreamAnalyzerTests.cs
T

235 lines
9.6 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 Extracts_Permission_Denials_From_Result_Event()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""
{"type":"result","result":"I need permission to edit the file.","session_id":"s1",
"permission_denials":[{"tool_name":"Edit","tool_use_id":"t1","tool_input":{}}]}
""");
var result = analyzer.GetResult();
Assert.Single(result.PermissionDenials);
Assert.Equal("Edit", result.PermissionDenials[0]);
}
[Fact]
public void No_Permission_Denials_Field_Means_Empty_List()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""{"type":"result","result":"done","session_id":"s1"}""");
Assert.Empty(analyzer.GetResult().PermissionDenials);
}
[Fact]
public void Multiple_Permission_Denials_Are_All_Collected()
{
var analyzer = new StreamAnalyzer();
analyzer.ProcessLine("""
{"type":"result","result":"blocked","session_id":"s1","permission_denials":[
{"tool_name":"Edit","tool_use_id":"t1","tool_input":{}},
{"tool_name":"Write","tool_use_id":"t2","tool_input":{}}
]}
""");
var result = analyzer.GetResult();
Assert.Equal(new[] { "Edit", "Write" }, result.PermissionDenials);
}
[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]);
}
}