fix(claude-do): Run reporting: token accounting + populate empty result

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
This commit is contained in:
Mika Kuns
2026-06-01 16:01:11 +02:00
parent 5170914a7a
commit 4c6e6594dc
2 changed files with 64 additions and 1 deletions

View File

@@ -79,4 +79,45 @@ public sealed class StreamAnalyzerTests
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);
}
}