返回 DeepSeek-Reasonix
conflict_collect_test.go
根目录 / internal / extension / conflict_collect_test.go
1 package extension
2
3 import (
4 "context"
5 "errors"
6 "strings"
7 "testing"
8 )
9
10 // TestConflictCollectRecordsAndResolves: under ConflictCollect a same-tier
11 // multi-source dispute is recorded on the snapshot diagnostics while the
12 // ordinary winner rules still resolve it, and the build succeeds.
13 func TestConflictCollectRecordsAndResolves(t *testing.T) {
14 b := NewBuilder().WithConflictPolicy(ConflictCollect)
15 b.AddContributor(
16 staticContributor("a", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pa", "plugin"), Payload: "from-a"}),
17 staticContributor("b", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pb", "plugin"), Payload: "from-b"}),
18 )
19 snap, _, err := b.Build(context.Background())
20 if err != nil {
21 t.Fatalf("ConflictCollect Build failed: %v", err)
22 }
23 winners := snap.Catalog().Get(KindCommand, "deploy")
24 if len(winners) != 1 {
25 t.Fatalf("catalog holds %d deploy commands, want 1 winner", len(winners))
26 }
27 if winners[0].Payload != "from-a" {
28 t.Fatalf("winner = %v, want the first-registered contribution", winners[0].Payload)
29 }
30 diags := snap.Diagnostics()
31 if len(diags) != 1 {
32 t.Fatalf("diagnostics = %v, want exactly one conflict entry", diags)
33 }
34 for _, want := range []string{"command", `"deploy"`, "pa", "pb"} {
35 if !strings.Contains(diags[0], want) {
36 t.Fatalf("diagnostic %q must contain %q (kind, canonical ID, both sources)", diags[0], want)
37 }
38 }
39 }
40
41 // TestConflictFailRemainsDefault: the same dispute stays a hard ConflictError
42 // without the collect opt-in.
43 func TestConflictFailRemainsDefault(t *testing.T) {
44 b := NewBuilder()
45 b.AddContributor(
46 staticContributor("a", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pa", "plugin"), Payload: "a"}),
47 staticContributor("b", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pb", "plugin"), Payload: "b"}),
48 )
49 _, _, err := b.Build(context.Background())
50 var conflict *ConflictError
51 if !errors.As(err, &conflict) {
52 t.Fatalf("Build error = %v, want *ConflictError", err)
53 }
54 }
55
56 // TestConflictCollectCrossTierIsNotADispute: shadowing across tiers is the
57 // ordinary winner rule, not a conflict — nothing lands on diagnostics.
58 func TestConflictCollectCrossTierIsNotADispute(t *testing.T) {
59 b := NewBuilder().WithConflictPolicy(ConflictCollect)
60 b.AddContributor(
61 staticContributor("plugin", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pa", "plugin"), Payload: "from-plugin"}),
62 staticContributor("project", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopeProject, "", "project"), Payload: "from-project"}),
63 )
64 snap, _, err := b.Build(context.Background())
65 if err != nil {
66 t.Fatalf("Build failed: %v", err)
67 }
68 winners := snap.Catalog().Get(KindCommand, "deploy")
69 if len(winners) != 1 || winners[0].Payload != "from-project" {
70 t.Fatalf("winner = %+v, want the project-tier contribution", winners)
71 }
72 if diags := snap.Diagnostics(); len(diags) != 0 {
73 t.Fatalf("cross-tier shadowing recorded diagnostics: %v", diags)
74 }
75 }
76
77 // TestConflictCollectAdditiveKindsNeverConflict: hooks accumulate; an
78 // identical ID from two sources is not a dispute.
79 func TestConflictCollectAdditiveKindsNeverConflict(t *testing.T) {
80 b := NewBuilder().WithConflictPolicy(ConflictCollect)
81 b.AddContributor(
82 staticContributor("a", Contribution{Kind: KindHook, ID: "PreToolUse#0", Source: src(ScopeProject, "", "project"), Payload: "hook-a"}),
83 staticContributor("b", Contribution{Kind: KindHook, ID: "PreToolUse#0", Source: src(ScopeGlobal, "", "global"), Payload: "hook-b"}),
84 )
85 snap, _, err := b.Build(context.Background())
86 if err != nil {
87 t.Fatalf("Build failed: %v", err)
88 }
89 if got := snap.Catalog().Get(KindHook, "PreToolUse#0"); len(got) != 2 {
90 t.Fatalf("additive hooks = %d, want both surviving", len(got))
91 }
92 if diags := snap.Diagnostics(); len(diags) != 0 {
93 t.Fatalf("additive kinds recorded diagnostics: %v", diags)
94 }
95 }
96
97 // TestConflictCollectSameSourceCollapses: duplicates from a single source
98 // resolve to the first registration without a diagnostic, mirroring today's
99 // first-root-wins discovery.
100 func TestConflictCollectSameSourceCollapses(t *testing.T) {
101 b := NewBuilder().WithConflictPolicy(ConflictCollect)
102 b.AddContributor(staticContributor("a",
103 Contribution{Kind: KindSkill, ID: "review", Source: src(ScopeProject, "", "project"), Payload: "first"},
104 Contribution{Kind: KindSkill, ID: "review", Source: src(ScopeProject, "", "project"), Payload: "second"},
105 ))
106 snap, _, err := b.Build(context.Background())
107 if err != nil {
108 t.Fatalf("Build failed: %v", err)
109 }
110 winners := snap.Catalog().Get(KindSkill, "review")
111 if len(winners) != 1 || winners[0].Payload != "first" {
112 t.Fatalf("winner = %+v, want the first registration", winners)
113 }
114 if diags := snap.Diagnostics(); len(diags) != 0 {
115 t.Fatalf("same-source duplicates recorded diagnostics: %v", diags)
116 }
117 }
118
119 // TestConflictCollectMultipleDisputesAllRecorded: every disputed ID lands on
120 // diagnostics in one pass, and each winner still resolves (here into the
121 // provider-visible tool schemas).
122 func TestConflictCollectMultipleDisputesAllRecorded(t *testing.T) {
123 b := NewBuilder().WithConflictPolicy(ConflictCollect)
124 b.AddContributor(
125 staticContributor("a",
126 Contribution{Kind: KindTool, ID: "read_file", Source: src(ScopePlugin, "pa", "plugin"), Payload: schemaPayload("read_file", "a read")},
127 Contribution{Kind: KindTool, ID: "write_file", Source: src(ScopePlugin, "pa", "plugin"), Payload: schemaPayload("write_file", "a write")},
128 ),
129 staticContributor("b",
130 Contribution{Kind: KindTool, ID: "read_file", Source: src(ScopePlugin, "pb", "plugin"), Payload: schemaPayload("read_file", "b read")},
131 Contribution{Kind: KindTool, ID: "write_file", Source: src(ScopePlugin, "pb", "plugin"), Payload: schemaPayload("write_file", "b write")},
132 ),
133 )
134 snap, _, err := b.Build(context.Background())
135 if err != nil {
136 t.Fatalf("ConflictCollect Build failed: %v", err)
137 }
138 if diags := snap.Diagnostics(); len(diags) != 2 {
139 t.Fatalf("diagnostics = %v, want one entry per disputed tool", diags)
140 }
141 // Contributor "a" registered first at the top tier, so its schemas win.
142 for _, s := range snap.ToolSchemas() {
143 if !strings.HasPrefix(s.Description, "a ") {
144 t.Fatalf("tool %s winner description = %q, want contributor a's schema", s.Name, s.Description)
145 }
146 }
147 }
148
149 // TestConflictCollectSlotConflictStillFails: replacement-slot disputes are
150 // not shadowing, so ConflictCollect does not downgrade them.
151 func TestConflictCollectSlotConflictStillFails(t *testing.T) {
152 b := NewBuilder().WithConflictPolicy(ConflictCollect)
153 b.AddContributor(
154 staticContributor("a", Contribution{
155 Kind: KindStrategy, ID: "strat-a",
156 Source: src(ScopePlugin, "pa", "plugin"),
157 Payload: claimPayload{body: "a", slots: []Slot{SlotSystemPrompt}},
158 }),
159 staticContributor("b", Contribution{
160 Kind: KindStrategy, ID: "strat-b",
161 Source: src(ScopePlugin, "pb", "plugin"),
162 Payload: claimPayload{body: "b", slots: []Slot{SlotSystemPrompt}},
163 }),
164 )
165 _, _, err := b.Build(context.Background())
166 if err == nil {
167 t.Fatal("ConflictCollect Build succeeded, want SlotConflictError")
168 }
169 var slotConflict *SlotConflictError
170 if !errors.As(err, &slotConflict) {
171 t.Fatalf("Build error = %v, want *SlotConflictError", err)
172 }
173 }
174
175 // TestSnapshotDiagnosticsImmutable: the accessor returns a copy.
176 func TestSnapshotDiagnosticsImmutable(t *testing.T) {
177 b := NewBuilder().WithConflictPolicy(ConflictCollect)
178 b.AddContributor(
179 staticContributor("a", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pa", "plugin"), Payload: "a"}),
180 staticContributor("b", Contribution{Kind: KindCommand, ID: "deploy", Source: src(ScopePlugin, "pb", "plugin"), Payload: "b"}),
181 )
182 snap, _, err := b.Build(context.Background())
183 if err != nil {
184 t.Fatalf("Build failed: %v", err)
185 }
186 diags := snap.Diagnostics()
187 if len(diags) != 1 {
188 t.Fatalf("diagnostics = %v, want one entry", diags)
189 }
190 diags[0] = "mutated"
191 if snap.Diagnostics()[0] == "mutated" {
192 t.Fatal("Diagnostics returned the snapshot's internal slice")
193 }
194 }
195
195 lines GO