| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "reasonix/internal/provider" |
| 10 | "reasonix/internal/tool" |
| 11 | ) |
| 12 | |
| 13 | type genShiftTool struct { |
| 14 | gen string |
| 15 | } |
| 16 | |
| 17 | func (g *genShiftTool) Name() string { return "shift" } |
| 18 | func (g *genShiftTool) Description() string { return "" } |
| 19 | func (g *genShiftTool) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) } |
| 20 | func (g *genShiftTool) ReadOnly() bool { return true } |
| 21 | func (g *genShiftTool) Execute(context.Context, json.RawMessage) (string, error) { |
| 22 | return "", nil |
| 23 | } |
| 24 | func (g *genShiftTool) ClassifyCall(json.RawMessage) tool.CallClass { |
| 25 | return tool.CallClass{Known: true, ReadOnly: true, ParallelSafe: true, Generation: g.gen} |
| 26 | } |
| 27 | |
| 28 | func TestDispatchGenerationGateBlocksStaleClassification(t *testing.T) { |
| 29 | target := &genShiftTool{gen: "v1"} |
| 30 | a := &Agent{} |
| 31 | a.turn.loop.setDispatchClasses(map[string]tool.CallClass{ |
| 32 | "c1": {Known: true, ReadOnly: true, ParallelSafe: true, Generation: "v1"}, |
| 33 | }) |
| 34 | plan := &toolCallPlan{ |
| 35 | call: provider.ToolCall{ID: "c1", Name: "shift", Arguments: `{}`}, |
| 36 | tool: target, |
| 37 | } |
| 38 | if _, blocked := a.applyDispatchGenerationGate(plan); blocked { |
| 39 | t.Fatal("matching generation must dispatch") |
| 40 | } |
| 41 | target.gen = "v2" |
| 42 | out, blocked := a.applyDispatchGenerationGate(plan) |
| 43 | if !blocked || !strings.Contains(out.output, "generation changed") { |
| 44 | t.Fatalf("stale generation outcome = %+v", out) |
| 45 | } |
| 46 | } |
| 47 |