返回 DeepSeek-Reasonix
turnruntime_test.go
根目录 / internal / agent / turnruntime_test.go
1 package agent
2
3 import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "testing"
8 )
9
10 // beginRunTurn opens with `a.turn = turnRuntime{}`, so a new field starts the
11 // next turn zeroed. That holds only while the type stays assignable: one mutex
12 // or atomic makes it a vet copylocks error, and the lifetime degrades to
13 // whatever the call sites happen to reset. sessionRuntime already pays that
14 // price; this layer must not.
15 func TestTurnRuntimeStaysAssignable(t *testing.T) {
16 fset := token.NewFileSet()
17 file, err := parser.ParseFile(fset, "turnruntime.go", nil, 0)
18 if err != nil {
19 t.Fatalf("parse turnruntime.go: %v", err)
20 }
21 var fields int
22 ast.Inspect(file, func(n ast.Node) bool {
23 spec, ok := n.(*ast.TypeSpec)
24 if !ok || spec.Name.Name != "turnRuntime" {
25 return true
26 }
27 st, ok := spec.Type.(*ast.StructType)
28 if !ok {
29 return false
30 }
31 for _, field := range st.Fields.List {
32 fields += max(len(field.Names), 1)
33 sel, ok := unwrapStar(field.Type).(*ast.SelectorExpr)
34 if !ok {
35 continue
36 }
37 pkg, ok := sel.X.(*ast.Ident)
38 if !ok {
39 continue
40 }
41 switch qualified := pkg.Name + "." + sel.Sel.Name; {
42 case pkg.Name == "atomic", qualified == "sync.Mutex", qualified == "sync.RWMutex":
43 t.Errorf("turnRuntime.%s is a %s; beginRunTurn's single assignment stops compiling and the reset guarantee is lost",
44 fieldName(field), qualified)
45 }
46 }
47 return false
48 })
49 if fields == 0 {
50 t.Fatal("turnRuntime has no fields; the guard would pass vacuously")
51 }
52 }
53
54 func unwrapStar(expr ast.Expr) ast.Expr {
55 if star, ok := expr.(*ast.StarExpr); ok {
56 return star.X
57 }
58 return expr
59 }
60
61 func fieldName(field *ast.Field) string {
62 if len(field.Names) == 0 {
63 return "<embedded>"
64 }
65 return field.Names[0].Name
66 }
67
68 // The tool path must reach turn state through its parameter. Reading a.turn
69 // inside it would compile and behave identically today — and would silently
70 // stop being true the moment a turn is not the agent's current one, which is
71 // exactly what the parameter exists to prevent.
72 func TestToolPathTakesTheTurnAsAParameter(t *testing.T) {
73 for _, name := range []string{"execute_one.go", "execute_batch.go"} {
74 fset := token.NewFileSet()
75 file, err := parser.ParseFile(fset, name, nil, 0)
76 if err != nil {
77 t.Fatalf("parse %s: %v", name, err)
78 }
79 ast.Inspect(file, func(n ast.Node) bool {
80 sel, ok := n.(*ast.SelectorExpr)
81 if !ok || sel.Sel.Name != "turn" {
82 return true
83 }
84 if recv, ok := sel.X.(*ast.Ident); ok && recv.Name == "a" {
85 t.Errorf("%s:%d: reads a.turn; the tool path takes *turnRuntime as a parameter",
86 name, fset.Position(sel.Pos()).Line)
87 }
88 return true
89 })
90 }
91 }
92
93 func TestBeginRunTurnReplacesTheWholeTurn(t *testing.T) {
94 fset := token.NewFileSet()
95 file, err := parser.ParseFile(fset, "run_loop.go", nil, 0)
96 if err != nil {
97 t.Fatalf("parse run_loop.go: %v", err)
98 }
99 var replaced bool
100 ast.Inspect(file, func(n ast.Node) bool {
101 fn, ok := n.(*ast.FuncDecl)
102 if !ok || fn.Name.Name != "beginRunTurn" {
103 return true
104 }
105 ast.Inspect(fn, func(inner ast.Node) bool {
106 assign, ok := inner.(*ast.AssignStmt)
107 if !ok || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
108 return true
109 }
110 sel, ok := assign.Lhs[0].(*ast.SelectorExpr)
111 if !ok || sel.Sel.Name != "turn" {
112 return true
113 }
114 if lit, ok := assign.Rhs[0].(*ast.CompositeLit); ok {
115 if ident, ok := lit.Type.(*ast.Ident); ok && ident.Name == "turnRuntime" {
116 if len(lit.Elts) != 0 {
117 t.Error("beginRunTurn seeds fields in the replacement literal; keep it empty so nothing is carried by accident")
118 }
119 replaced = true
120 }
121 }
122 return true
123 })
124 return false
125 })
126 if !replaced {
127 t.Error("beginRunTurn no longer replaces a.turn wholesale; a per-field reset can forget a field")
128 }
129 }
130
130 lines GO