返回 DeepSeek-Reasonix
contract_lock_test.go
根目录 / internal / tool / contract_lock_test.go
1 package tool
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8 )
9
10 // blockingReadOnlyTool lets a test park ContractEntries inside the per-tool
11 // ReadOnly callback at a controlled point.
12 type blockingReadOnlyTool struct {
13 name string
14 entered chan<- struct{}
15 release <-chan struct{}
16 }
17
18 func (t *blockingReadOnlyTool) Name() string { return t.name }
19 func (t *blockingReadOnlyTool) Description() string { return "blocking test tool" }
20 func (t *blockingReadOnlyTool) Schema() json.RawMessage {
21 return json.RawMessage(`{"type":"object","properties":{}}`)
22 }
23 func (t *blockingReadOnlyTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
24 return "ok", nil
25 }
26 func (t *blockingReadOnlyTool) ReadOnly() bool {
27 close(t.entered)
28 <-t.release
29 return true
30 }
31
32 // TestContractEntriesDoesNotHoldRegistryLockAcrossToolCallbacks is the
33 // deterministic regression for the AB-BA deadlock fixed in ContractEntries:
34 // with ContractEntries parked INSIDE a tool's ReadOnly callback, a registry
35 // writer (Add) must still complete. Under the pre-fix code ContractEntries
36 // held the registry read lock across ReadOnly, so this interleaving
37 // deadlocked (lazy MCP placeholders take the spawn mutex in ReadOnly while
38 // the spawn's trySwap needs the registry write lock).
39 func TestContractEntriesDoesNotHoldRegistryLockAcrossToolCallbacks(t *testing.T) {
40 reg := NewRegistry()
41 entered := make(chan struct{})
42 release := make(chan struct{})
43 reg.Add(&blockingReadOnlyTool{name: "blocking_tool", entered: entered, release: release})
44
45 entriesCh := make(chan []ContractEntry, 1)
46 go func() {
47 entriesCh <- reg.ContractEntries()
48 }()
49
50 select {
51 case <-entered:
52 case <-time.After(5 * time.Second):
53 t.Fatal("ContractEntries never reached the ReadOnly callback")
54 }
55
56 // The write must go through while the callback is still parked: the
57 // callback must not hold any registry lock.
58 addDone := make(chan struct{})
59 go func() {
60 reg.Add(&blockingReadOnlyTool{name: "writer_tool", entered: make(chan struct{}, 1), release: make(chan struct{})})
61 close(addDone)
62 }()
63 select {
64 case <-addDone:
65 case <-time.After(5 * time.Second):
66 t.Fatal("deadlock: registry writer blocked while ContractEntries was parked inside ReadOnly")
67 }
68
69 close(release)
70 entries := <-entriesCh
71 if len(entries) != 1 || entries[0].Name != "blocking_tool" || !entries[0].ReadOnly {
72 t.Fatalf("ContractEntries returned %+v, want one read-only blocking_tool", entries)
73 }
74 }
75
75 lines GO