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); } }