opencode-swarm-plugin 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,473 @@
1
+ /**
2
+ * Tests for mandate promotion engine
3
+ */
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import {
7
+ evaluateBatchPromotions,
8
+ evaluatePromotion,
9
+ formatPromotionResult,
10
+ getStatusChanges,
11
+ groupByTransition,
12
+ shouldPromote,
13
+ } from "./mandate-promotion";
14
+ import { DEFAULT_MANDATE_DECAY_CONFIG } from "./schemas/mandate";
15
+ import type { MandateEntry, MandateScore } from "./schemas/mandate";
16
+
17
+ // ============================================================================
18
+ // Test Helpers
19
+ // ============================================================================
20
+
21
+ function createMockEntry(
22
+ id: string,
23
+ status: "candidate" | "established" | "mandate" | "rejected" = "candidate",
24
+ ): MandateEntry {
25
+ return {
26
+ id,
27
+ content: "Test mandate content",
28
+ content_type: "idea",
29
+ author_agent: "TestAgent",
30
+ created_at: new Date().toISOString(),
31
+ status,
32
+ tags: [],
33
+ };
34
+ }
35
+
36
+ function createMockScore(
37
+ mandate_id: string,
38
+ net_votes: number,
39
+ vote_ratio: number,
40
+ raw_upvotes: number = Math.max(0, net_votes),
41
+ raw_downvotes: number = 0,
42
+ ): MandateScore {
43
+ return {
44
+ mandate_id,
45
+ net_votes,
46
+ vote_ratio,
47
+ decayed_score: net_votes * vote_ratio,
48
+ last_calculated: new Date().toISOString(),
49
+ raw_upvotes,
50
+ raw_downvotes,
51
+ decayed_upvotes: raw_upvotes,
52
+ decayed_downvotes: raw_downvotes,
53
+ };
54
+ }
55
+
56
+ // ============================================================================
57
+ // shouldPromote Tests
58
+ // ============================================================================
59
+
60
+ describe("shouldPromote", () => {
61
+ it("candidate stays candidate with insufficient votes", () => {
62
+ const score = createMockScore("m1", 1, 1.0);
63
+ const result = shouldPromote(score, "candidate");
64
+ expect(result).toBe("candidate");
65
+ });
66
+
67
+ it("candidate → established at threshold (net_votes >= 2)", () => {
68
+ const score = createMockScore("m1", 2, 1.0);
69
+ const result = shouldPromote(score, "candidate");
70
+ expect(result).toBe("established");
71
+ });
72
+
73
+ it("candidate → established above threshold", () => {
74
+ const score = createMockScore("m1", 3, 0.9);
75
+ const result = shouldPromote(score, "candidate");
76
+ expect(result).toBe("established");
77
+ });
78
+
79
+ it("established stays established with insufficient mandate votes", () => {
80
+ const score = createMockScore("m1", 3, 0.8);
81
+ const result = shouldPromote(score, "established");
82
+ expect(result).toBe("established");
83
+ });
84
+
85
+ it("established stays established with low vote ratio", () => {
86
+ const score = createMockScore("m1", 6, 0.6); // net_votes OK, but ratio < 0.7
87
+ const result = shouldPromote(score, "established");
88
+ expect(result).toBe("established");
89
+ });
90
+
91
+ it("established → mandate at threshold (net >= 5, ratio >= 0.7)", () => {
92
+ const score = createMockScore("m1", 5, 0.7);
93
+ const result = shouldPromote(score, "established");
94
+ expect(result).toBe("mandate");
95
+ });
96
+
97
+ it("established → mandate above threshold", () => {
98
+ const score = createMockScore("m1", 10, 0.9);
99
+ const result = shouldPromote(score, "established");
100
+ expect(result).toBe("mandate");
101
+ });
102
+
103
+ it("mandate stays mandate (no demotion)", () => {
104
+ const score = createMockScore("m1", 3, 0.5); // Degraded score
105
+ const result = shouldPromote(score, "mandate");
106
+ expect(result).toBe("mandate");
107
+ });
108
+
109
+ it("candidate → rejected with negative votes", () => {
110
+ const score = createMockScore("m1", -3, 0.2);
111
+ const result = shouldPromote(score, "candidate");
112
+ expect(result).toBe("rejected");
113
+ });
114
+
115
+ it("established → rejected with negative votes", () => {
116
+ const score = createMockScore("m1", -4, 0.1);
117
+ const result = shouldPromote(score, "established");
118
+ expect(result).toBe("rejected");
119
+ });
120
+
121
+ it("rejected stays rejected (permanent)", () => {
122
+ const score = createMockScore("m1", 5, 0.9); // Even with good score
123
+ const result = shouldPromote(score, "rejected");
124
+ expect(result).toBe("rejected");
125
+ });
126
+
127
+ it("uses custom config thresholds", () => {
128
+ const score = createMockScore("m1", 3, 0.6);
129
+ const customConfig = {
130
+ ...DEFAULT_MANDATE_DECAY_CONFIG,
131
+ establishedNetVotesThreshold: 3,
132
+ mandateNetVotesThreshold: 3,
133
+ mandateVoteRatioThreshold: 0.6,
134
+ };
135
+ const result = shouldPromote(score, "candidate", customConfig);
136
+ expect(result).toBe("established");
137
+
138
+ const result2 = shouldPromote(score, "established", customConfig);
139
+ expect(result2).toBe("mandate");
140
+ });
141
+ });
142
+
143
+ // ============================================================================
144
+ // evaluatePromotion Tests
145
+ // ============================================================================
146
+
147
+ describe("evaluatePromotion", () => {
148
+ it("returns correct promotion result for candidate → established", () => {
149
+ const entry = createMockEntry("m1", "candidate");
150
+ const score = createMockScore("m1", 2, 1.0);
151
+ const result = evaluatePromotion(entry, score);
152
+
153
+ expect(result.mandate_id).toBe("m1");
154
+ expect(result.previous_status).toBe("candidate");
155
+ expect(result.new_status).toBe("established");
156
+ expect(result.promoted).toBe(true);
157
+ expect(result.reason).toContain("Promoted to established");
158
+ expect(result.score).toEqual(score);
159
+ });
160
+
161
+ it("returns correct promotion result for established → mandate", () => {
162
+ const entry = createMockEntry("m1", "established");
163
+ const score = createMockScore("m1", 5, 0.7);
164
+ const result = evaluatePromotion(entry, score);
165
+
166
+ expect(result.mandate_id).toBe("m1");
167
+ expect(result.previous_status).toBe("established");
168
+ expect(result.new_status).toBe("mandate");
169
+ expect(result.promoted).toBe(true);
170
+ expect(result.reason).toContain("Promoted to mandate");
171
+ });
172
+
173
+ it("returns correct promotion result for candidate → rejected", () => {
174
+ const entry = createMockEntry("m1", "candidate");
175
+ const score = createMockScore("m1", -3, 0.2);
176
+ const result = evaluatePromotion(entry, score);
177
+
178
+ expect(result.mandate_id).toBe("m1");
179
+ expect(result.previous_status).toBe("candidate");
180
+ expect(result.new_status).toBe("rejected");
181
+ expect(result.promoted).toBe(true);
182
+ expect(result.reason).toContain("Rejected due to negative consensus");
183
+ });
184
+
185
+ it("returns correct result for no status change", () => {
186
+ const entry = createMockEntry("m1", "candidate");
187
+ const score = createMockScore("m1", 1, 0.8);
188
+ const result = evaluatePromotion(entry, score);
189
+
190
+ expect(result.mandate_id).toBe("m1");
191
+ expect(result.previous_status).toBe("candidate");
192
+ expect(result.new_status).toBe("candidate");
193
+ expect(result.promoted).toBe(false);
194
+ expect(result.reason).toContain("Remains candidate");
195
+ });
196
+
197
+ it("returns correct result for mandate staying mandate", () => {
198
+ const entry = createMockEntry("m1", "mandate");
199
+ const score = createMockScore("m1", 3, 0.5); // Degraded
200
+ const result = evaluatePromotion(entry, score);
201
+
202
+ expect(result.mandate_id).toBe("m1");
203
+ expect(result.previous_status).toBe("mandate");
204
+ expect(result.new_status).toBe("mandate");
205
+ expect(result.promoted).toBe(false);
206
+ expect(result.reason).toContain("Remains mandate");
207
+ });
208
+
209
+ it("returns correct result for rejected staying rejected", () => {
210
+ const entry = createMockEntry("m1", "rejected");
211
+ const score = createMockScore("m1", 10, 0.95); // Good score
212
+ const result = evaluatePromotion(entry, score);
213
+
214
+ expect(result.mandate_id).toBe("m1");
215
+ expect(result.previous_status).toBe("rejected");
216
+ expect(result.new_status).toBe("rejected");
217
+ expect(result.promoted).toBe(false);
218
+ expect(result.reason).toContain("Remains rejected");
219
+ });
220
+ });
221
+
222
+ // ============================================================================
223
+ // Decay Effect Tests
224
+ // ============================================================================
225
+
226
+ describe("decay affects promotion timing", () => {
227
+ it("net_votes can decay below promotion threshold", () => {
228
+ // Entry was established with 2.5 decayed net_votes
229
+ const entry = createMockEntry("m1", "established");
230
+ const score = createMockScore("m1", 1.5, 0.8); // Decayed below threshold
231
+ const result = evaluatePromotion(entry, score);
232
+
233
+ // Stays established (no demotion) even though decayed below candidate→established threshold
234
+ expect(result.new_status).toBe("established");
235
+ expect(result.promoted).toBe(false);
236
+ });
237
+
238
+ it("vote_ratio decay prevents mandate promotion", () => {
239
+ const entry = createMockEntry("m1", "established");
240
+ // High net_votes but low ratio due to decay
241
+ const score = createMockScore("m1", 6, 0.65, 10, 4); // ratio < 0.7
242
+ const result = evaluatePromotion(entry, score);
243
+
244
+ expect(result.new_status).toBe("established");
245
+ expect(result.promoted).toBe(false);
246
+ expect(result.reason).toContain("below mandate threshold");
247
+ });
248
+
249
+ it("fresh votes can push over mandate threshold", () => {
250
+ const entry = createMockEntry("m1", "established");
251
+ const score = createMockScore("m1", 5.1, 0.75, 8, 3); // Fresh votes
252
+ const result = evaluatePromotion(entry, score);
253
+
254
+ expect(result.new_status).toBe("mandate");
255
+ expect(result.promoted).toBe(true);
256
+ });
257
+ });
258
+
259
+ // ============================================================================
260
+ // Utility Functions Tests
261
+ // ============================================================================
262
+
263
+ describe("formatPromotionResult", () => {
264
+ it("formats promoted result with arrow", () => {
265
+ const entry = createMockEntry("m1", "candidate");
266
+ const score = createMockScore("m1", 2, 1.0);
267
+ const result = evaluatePromotion(entry, score);
268
+ const formatted = formatPromotionResult(result);
269
+
270
+ expect(formatted).toContain("[m1]");
271
+ expect(formatted).toContain("candidate → established");
272
+ });
273
+
274
+ it("formats no-change result without arrow", () => {
275
+ const entry = createMockEntry("m1", "candidate");
276
+ const score = createMockScore("m1", 1, 0.8);
277
+ const result = evaluatePromotion(entry, score);
278
+ const formatted = formatPromotionResult(result);
279
+
280
+ expect(formatted).toContain("[m1]");
281
+ expect(formatted).toContain("candidate");
282
+ expect(formatted).not.toContain("→");
283
+ });
284
+ });
285
+
286
+ describe("evaluateBatchPromotions", () => {
287
+ it("evaluates multiple entries", () => {
288
+ const entries = new Map([
289
+ ["m1", createMockEntry("m1", "candidate")],
290
+ ["m2", createMockEntry("m2", "established")],
291
+ ["m3", createMockEntry("m3", "mandate")],
292
+ ]);
293
+
294
+ const scores = new Map([
295
+ ["m1", createMockScore("m1", 2, 1.0)], // Will promote
296
+ ["m2", createMockScore("m2", 5, 0.7)], // Will promote
297
+ ["m3", createMockScore("m3", 10, 0.9)], // Stays mandate
298
+ ]);
299
+
300
+ const results = evaluateBatchPromotions(entries, scores);
301
+
302
+ expect(results).toHaveLength(3);
303
+ expect(results[0].new_status).toBe("established");
304
+ expect(results[1].new_status).toBe("mandate");
305
+ expect(results[2].new_status).toBe("mandate");
306
+ });
307
+
308
+ it("skips entries without scores", () => {
309
+ const entries = new Map([
310
+ ["m1", createMockEntry("m1", "candidate")],
311
+ ["m2", createMockEntry("m2", "established")],
312
+ ]);
313
+
314
+ const scores = new Map([
315
+ ["m1", createMockScore("m1", 2, 1.0)],
316
+ // m2 has no score
317
+ ]);
318
+
319
+ const results = evaluateBatchPromotions(entries, scores);
320
+
321
+ expect(results).toHaveLength(1);
322
+ expect(results[0].mandate_id).toBe("m1");
323
+ });
324
+ });
325
+
326
+ describe("getStatusChanges", () => {
327
+ it("filters to only promoted entries", () => {
328
+ const results = [
329
+ {
330
+ mandate_id: "m1",
331
+ previous_status: "candidate" as const,
332
+ new_status: "established" as const,
333
+ score: createMockScore("m1", 2, 1.0),
334
+ promoted: true,
335
+ reason: "Promoted",
336
+ },
337
+ {
338
+ mandate_id: "m2",
339
+ previous_status: "candidate" as const,
340
+ new_status: "candidate" as const,
341
+ score: createMockScore("m2", 1, 0.8),
342
+ promoted: false,
343
+ reason: "No change",
344
+ },
345
+ {
346
+ mandate_id: "m3",
347
+ previous_status: "established" as const,
348
+ new_status: "mandate" as const,
349
+ score: createMockScore("m3", 5, 0.7),
350
+ promoted: true,
351
+ reason: "Promoted",
352
+ },
353
+ ];
354
+
355
+ const changes = getStatusChanges(results);
356
+
357
+ expect(changes).toHaveLength(2);
358
+ expect(changes[0].mandate_id).toBe("m1");
359
+ expect(changes[1].mandate_id).toBe("m3");
360
+ });
361
+ });
362
+
363
+ describe("groupByTransition", () => {
364
+ it("groups results by transition type", () => {
365
+ const results = [
366
+ {
367
+ mandate_id: "m1",
368
+ previous_status: "candidate" as const,
369
+ new_status: "established" as const,
370
+ score: createMockScore("m1", 2, 1.0),
371
+ promoted: true,
372
+ reason: "Promoted",
373
+ },
374
+ {
375
+ mandate_id: "m2",
376
+ previous_status: "candidate" as const,
377
+ new_status: "established" as const,
378
+ score: createMockScore("m2", 3, 1.0),
379
+ promoted: true,
380
+ reason: "Promoted",
381
+ },
382
+ {
383
+ mandate_id: "m3",
384
+ previous_status: "established" as const,
385
+ new_status: "mandate" as const,
386
+ score: createMockScore("m3", 5, 0.7),
387
+ promoted: true,
388
+ reason: "Promoted",
389
+ },
390
+ {
391
+ mandate_id: "m4",
392
+ previous_status: "candidate" as const,
393
+ new_status: "candidate" as const,
394
+ score: createMockScore("m4", 1, 0.8),
395
+ promoted: false,
396
+ reason: "No change",
397
+ },
398
+ ];
399
+
400
+ const grouped = groupByTransition(results);
401
+
402
+ expect(grouped.size).toBe(3);
403
+ expect(grouped.get("candidate→established")).toHaveLength(2);
404
+ expect(grouped.get("established→mandate")).toHaveLength(1);
405
+ expect(grouped.get("candidate")).toHaveLength(1);
406
+ });
407
+
408
+ it("uses status name for no-change transitions", () => {
409
+ const results = [
410
+ {
411
+ mandate_id: "m1",
412
+ previous_status: "mandate" as const,
413
+ new_status: "mandate" as const,
414
+ score: createMockScore("m1", 10, 0.9),
415
+ promoted: false,
416
+ reason: "Stays",
417
+ },
418
+ {
419
+ mandate_id: "m2",
420
+ previous_status: "rejected" as const,
421
+ new_status: "rejected" as const,
422
+ score: createMockScore("m2", -5, 0.1),
423
+ promoted: false,
424
+ reason: "Stays",
425
+ },
426
+ ];
427
+
428
+ const grouped = groupByTransition(results);
429
+
430
+ expect(grouped.size).toBe(2);
431
+ expect(grouped.get("mandate")).toHaveLength(1);
432
+ expect(grouped.get("rejected")).toHaveLength(1);
433
+ });
434
+ });
435
+
436
+ // ============================================================================
437
+ // Edge Cases
438
+ // ============================================================================
439
+
440
+ describe("edge cases", () => {
441
+ it("handles exact threshold values", () => {
442
+ const entry1 = createMockEntry("m1", "candidate");
443
+ const score1 = createMockScore("m1", 2.0, 1.0); // Exact threshold
444
+ const result1 = evaluatePromotion(entry1, score1);
445
+ expect(result1.new_status).toBe("established");
446
+
447
+ const entry2 = createMockEntry("m2", "established");
448
+ const score2 = createMockScore("m2", 5.0, 0.7); // Exact threshold
449
+ const result2 = evaluatePromotion(entry2, score2);
450
+ expect(result2.new_status).toBe("mandate");
451
+ });
452
+
453
+ it("handles zero votes", () => {
454
+ const entry = createMockEntry("m1", "candidate");
455
+ const score = createMockScore("m1", 0, 0);
456
+ const result = evaluatePromotion(entry, score);
457
+ expect(result.new_status).toBe("candidate");
458
+ });
459
+
460
+ it("handles negative vote ratio edge case", () => {
461
+ const entry = createMockEntry("m1", "established");
462
+ const score = createMockScore("m1", 5, 0.2, 1, 4); // Low ratio
463
+ const result = evaluatePromotion(entry, score);
464
+ expect(result.new_status).toBe("established"); // ratio < 0.7
465
+ });
466
+
467
+ it("rejects at exact rejection threshold", () => {
468
+ const entry = createMockEntry("m1", "candidate");
469
+ const score = createMockScore("m1", -3, 0.1);
470
+ const result = evaluatePromotion(entry, score);
471
+ expect(result.new_status).toBe("rejected");
472
+ });
473
+ });