BUNDLE — both fixes live in the Worker run-recording / persistence layer (where a TaskRun is written after an agent finishes), NOT in ExternalMcpService.cs. Keep this disjoint from the MCP-surface bundle so the two can run in parallel without worktree conflicts. The DTO fields (tokensIn, tokensOut, resultMarkdown) already exist and are surfaced by list_runs/get_run — the bug is at write time. 1. ClaudeDo-Task: 49a6060a-5044-4f1b-8665-5cfc064b8a82
124 lines
4.9 KiB
C#
124 lines
4.9 KiB
C#
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);
|
|
}
|
|
}
|