mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
FinalizeStage forwards state.Prune.MidLoopCompacted into MaybeSummarize so a mid-loop compaction gets persisted (TruncateHistory + IncrementCompaction) instead of being thrown away and redone next turn. Nothing asserted the flag value: TestFinalizeStage_MaybeSummarize_Called binds it as `_ bool`, so hardcoding `false` at the call site keeps the whole suite green while silently restoring the per-turn re-compaction loop and stalling the cumulative compaction count episodic depends on. Adds two cases driving the real NewDefaultPipeline().Run(): - a history large enough to force prune-stage compaction must reach MaybeSummarize with midLoopCompacted=true - a small history must compact nothing and report false, so the flag cannot simply be pinned true Mutation-verified: with the finalize call site hardcoded to false the new test fails while the pre-existing pipeline and agent suites still pass. Test-only change. Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
134 lines
5.3 KiB
Go
134 lines
5.3 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
)
|
|
|
|
// This file closes the last uncovered link in the anti-recompaction-loop chain.
|
|
//
|
|
// The chain has three hops:
|
|
// 1. prune_stage / final_request_guard compact mid-loop and set
|
|
// state.Prune.MidLoopCompacted — covered by
|
|
// TestPruneStage_Compaction_PreservesPending + stages_test.go.
|
|
// 2. finalize_stage forwards that flag into deps.MaybeSummarize — THIS FILE.
|
|
// 3. maybeSummarize lowers its threshold and PERSISTS the compaction — covered by
|
|
// internal/agent/loop_maybe_summarize_pressure_test.go.
|
|
//
|
|
// Hop 2 was effectively untested: TestFinalizeStage_MaybeSummarize_Called binds
|
|
// the flag as `_ bool` and never asserts it, so hardcoding `false` at the
|
|
// finalize call site would keep every existing test green while silently
|
|
// restoring the unbounded re-compaction loop (each turn recompacts from scratch
|
|
// because nothing is ever written back to the session store).
|
|
//
|
|
// These tests drive the REAL NewDefaultPipeline().Run() end to end — no stage is
|
|
// invoked directly — so the flag has to survive the actual run wiring.
|
|
|
|
// pressureE2EDeps builds deps for a full-pipeline run whose token math forces a
|
|
// mid-loop compaction, capturing what finalize hands to MaybeSummarize.
|
|
//
|
|
// Token math (mirrors TestPruneStage_Compaction_PreservesPending):
|
|
//
|
|
// budget = ContextWindow(1000) - overhead(0) - MaxTokens(100) - ReserveTokens(0) = 900
|
|
// history = 50 msgs * 100 tokens = 5000 > 900
|
|
//
|
|
// PruneMessages deliberately reduces nothing, forcing fall-through to
|
|
// CompactMessages, which is the path that sets MidLoopCompacted.
|
|
func pressureE2EDeps(gotFlag *bool, gotCalls *int, compact bool) PipelineDeps {
|
|
deps := PipelineDeps{
|
|
Config: PipelineConfig{
|
|
MaxIterations: 2,
|
|
ContextWindow: 1000,
|
|
MaxTokens: 100,
|
|
},
|
|
TokenCounter: &mockTokenCounter{countPerMessage: 100},
|
|
CallLLM: func(_ context.Context, _ *RunState, _ providers.ChatRequest) (*providers.ChatResponse, error) {
|
|
// No tool calls -> ThinkStage returns BreakLoop, so the run reaches
|
|
// finalize after exactly one iteration.
|
|
return &providers.ChatResponse{Content: "final answer", FinishReason: "stop"}, nil
|
|
},
|
|
MaybeSummarize: func(_ context.Context, _ string, midLoopCompacted bool) {
|
|
*gotCalls++
|
|
*gotFlag = midLoopCompacted
|
|
},
|
|
}
|
|
if compact {
|
|
deps.PruneMessages = func(msgs []providers.Message, _ int) ([]providers.Message, PruneStats) {
|
|
return msgs, PruneStats{} // no reduction — force the compaction path
|
|
}
|
|
deps.CompactMessages = func(_ context.Context, _ []providers.Message, _ string) ([]providers.Message, error) {
|
|
return []providers.Message{{Role: "user", Content: "[compacted summary]"}}, nil
|
|
}
|
|
}
|
|
return deps
|
|
}
|
|
|
|
func pressureE2EState(historyMsgs int) *RunState {
|
|
state := defaultState()
|
|
history := make([]providers.Message, historyMsgs)
|
|
for i := range history {
|
|
history[i] = providers.Message{Role: "user", Content: "msg"}
|
|
}
|
|
state.Messages.SetHistory(history)
|
|
return state
|
|
}
|
|
|
|
// A full pipeline run that compacts mid-loop must tell post-turn summarization
|
|
// about it, so the compaction gets persisted to the session store instead of
|
|
// being thrown away and redone next turn.
|
|
func TestPipelineE2E_MidLoopCompaction_PropagatesPressureToMaybeSummarize(t *testing.T) {
|
|
t.Parallel()
|
|
var gotFlag bool
|
|
var calls int
|
|
deps := pressureE2EDeps(&gotFlag, &calls, true)
|
|
state := pressureE2EState(50) // 5000 tokens >> budget 900 -> compaction fires
|
|
|
|
if _, err := NewDefaultPipeline(deps).Run(context.Background(), state); err != nil {
|
|
t.Fatalf("pipeline Run() error: %v", err)
|
|
}
|
|
|
|
// Precondition: the run really did compact mid-loop (otherwise this test
|
|
// would vacuously pass on a false flag).
|
|
if !state.Prune.MidLoopCompacted {
|
|
t.Fatal("MidLoopCompacted = false, want true (pipeline should have compacted mid-loop)")
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("MaybeSummarize called %d times, want 1", calls)
|
|
}
|
|
// The assertion that TestFinalizeStage_MaybeSummarize_Called cannot make.
|
|
if !gotFlag {
|
|
t.Error("MaybeSummarize received midLoopCompacted=false, want true — " +
|
|
"mid-loop compaction would not be persisted, re-introducing the re-compaction loop")
|
|
}
|
|
}
|
|
|
|
// The mirror case: a run that never compacts must NOT claim pressure, otherwise
|
|
// every ordinary turn would truncate its session history early (over-compaction,
|
|
// which is what makes an agent lose context and "get dumber").
|
|
func TestPipelineE2E_NoCompaction_ReportsNoPressure(t *testing.T) {
|
|
t.Parallel()
|
|
var gotFlag bool
|
|
var calls int
|
|
// compact=false: no CompactMessages wired, and a tiny history that fits the
|
|
// budget anyway, so no mid-loop compaction can occur.
|
|
deps := pressureE2EDeps(&gotFlag, &calls, false)
|
|
state := pressureE2EState(2) // 200 tokens < budget 900
|
|
|
|
if _, err := NewDefaultPipeline(deps).Run(context.Background(), state); err != nil {
|
|
t.Fatalf("pipeline Run() error: %v", err)
|
|
}
|
|
|
|
if state.Prune.MidLoopCompacted {
|
|
t.Fatal("MidLoopCompacted = true, want false (history fits the budget)")
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("MaybeSummarize called %d times, want 1", calls)
|
|
}
|
|
if gotFlag {
|
|
t.Error("MaybeSummarize received midLoopCompacted=true on a run that never compacted — " +
|
|
"would lower the summarize threshold and over-compact healthy sessions")
|
|
}
|
|
}
|