|
| 1 | +package pipeline |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/nextlevelbuilder/goclaw/internal/providers" |
| 8 | +) |
| 9 | + |
| 10 | +// TestThinkStage_TracksLastUsagePerIteration guards the per-iteration usage |
| 11 | +// snapshot used for the sessions "context used" display and compaction |
| 12 | +// calibration. Regression: SetLastPromptTokens received the RUN-CUMULATIVE |
| 13 | +// TotalUsage.PromptTokens (sum over all think→act→observe iterations), so a |
| 14 | +// tool-heavy run reported e.g. 901K "context" against a 258K window and |
| 15 | +// over-triggered compaction. LastUsage must hold only the FINAL iteration's |
| 16 | +// own usage — the actual size of the last prompt sent to the model. |
| 17 | +func TestThinkStage_TracksLastUsagePerIteration(t *testing.T) { |
| 18 | + t.Parallel() |
| 19 | + |
| 20 | + usages := []providers.Usage{ |
| 21 | + {PromptTokens: 100000, CompletionTokens: 500, TotalTokens: 100500}, |
| 22 | + {PromptTokens: 120000, CompletionTokens: 300, TotalTokens: 120300, CacheReadTokens: 90000}, |
| 23 | + } |
| 24 | + call := 0 |
| 25 | + deps := &PipelineDeps{ |
| 26 | + Config: PipelineConfig{MaxIterations: 10, MaxTokens: 1000}, |
| 27 | + CallLLM: func(_ context.Context, _ *RunState, _ providers.ChatRequest) (*providers.ChatResponse, error) { |
| 28 | + u := usages[call] |
| 29 | + call++ |
| 30 | + return &providers.ChatResponse{FinishReason: "stop", Content: "done", Usage: &u}, nil |
| 31 | + }, |
| 32 | + } |
| 33 | + stage := NewThinkStage(deps) |
| 34 | + state := defaultState() |
| 35 | + |
| 36 | + if err := stage.Execute(context.Background(), state); err != nil { |
| 37 | + t.Fatalf("Execute() 1: %v", err) |
| 38 | + } |
| 39 | + if err := stage.Execute(context.Background(), state); err != nil { |
| 40 | + t.Fatalf("Execute() 2: %v", err) |
| 41 | + } |
| 42 | + |
| 43 | + // TotalUsage accumulates (unchanged behavior). |
| 44 | + if state.Think.TotalUsage.PromptTokens != 220000 { |
| 45 | + t.Errorf("TotalUsage.PromptTokens = %d, want 220000", state.Think.TotalUsage.PromptTokens) |
| 46 | + } |
| 47 | + // LastUsage holds only the final iteration's usage — NOT the sum. |
| 48 | + if state.Think.LastUsage.PromptTokens != 120000 { |
| 49 | + t.Errorf("LastUsage.PromptTokens = %d, want 120000 (final iteration only)", state.Think.LastUsage.PromptTokens) |
| 50 | + } |
| 51 | + if state.Think.LastUsage.CacheReadTokens != 90000 { |
| 52 | + t.Errorf("LastUsage.CacheReadTokens = %d, want 90000", state.Think.LastUsage.CacheReadTokens) |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +// TestThinkStage_LastUsageKeptWhenFinalResponseLacksUsage: a final iteration |
| 57 | +// whose response carries no usage (nil or zero prompt tokens) must not wipe the |
| 58 | +// last usable snapshot. |
| 59 | +func TestThinkStage_LastUsageKeptWhenFinalResponseLacksUsage(t *testing.T) { |
| 60 | + t.Parallel() |
| 61 | + |
| 62 | + responses := []*providers.ChatResponse{ |
| 63 | + {FinishReason: "stop", Content: "a", Usage: &providers.Usage{PromptTokens: 50000, CompletionTokens: 10}}, |
| 64 | + {FinishReason: "stop", Content: "b", Usage: nil}, |
| 65 | + } |
| 66 | + call := 0 |
| 67 | + deps := &PipelineDeps{ |
| 68 | + Config: PipelineConfig{MaxIterations: 10, MaxTokens: 1000}, |
| 69 | + CallLLM: func(_ context.Context, _ *RunState, _ providers.ChatRequest) (*providers.ChatResponse, error) { |
| 70 | + r := responses[call] |
| 71 | + call++ |
| 72 | + return r, nil |
| 73 | + }, |
| 74 | + } |
| 75 | + stage := NewThinkStage(deps) |
| 76 | + state := defaultState() |
| 77 | + |
| 78 | + if err := stage.Execute(context.Background(), state); err != nil { |
| 79 | + t.Fatalf("Execute() 1: %v", err) |
| 80 | + } |
| 81 | + if err := stage.Execute(context.Background(), state); err != nil { |
| 82 | + t.Fatalf("Execute() 2: %v", err) |
| 83 | + } |
| 84 | + |
| 85 | + if state.Think.LastUsage.PromptTokens != 50000 { |
| 86 | + t.Errorf("LastUsage.PromptTokens = %d, want 50000 (kept from iteration 1)", state.Think.LastUsage.PromptTokens) |
| 87 | + } |
| 88 | +} |
0 commit comments