opencode-swarm-plugin 0.45.7 → 0.47.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.
- package/bin/cass.characterization.test.ts +400 -379
- package/bin/eval-gate.test.ts +23 -0
- package/bin/eval-gate.ts +21 -0
- package/bin/swarm.ts +485 -151
- package/dist/bin/swarm.js +2120 -971
- package/dist/cass-tools.d.ts +1 -2
- package/dist/cass-tools.d.ts.map +1 -1
- package/dist/compaction-hook.d.ts +50 -6
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/dashboard.d.ts +5 -6
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +20 -10
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/eval-capture.js +54 -21
- package/dist/eval-learning.d.ts +5 -5
- package/dist/eval-learning.d.ts.map +1 -1
- package/dist/hive.d.ts +7 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/hive.js +61 -24
- package/dist/hivemind-tools.d.ts +479 -0
- package/dist/hivemind-tools.d.ts.map +1 -0
- package/dist/index.d.ts +31 -98
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -467
- package/dist/observability-health.d.ts +87 -0
- package/dist/observability-health.d.ts.map +1 -0
- package/dist/observability-tools.d.ts +5 -1
- package/dist/observability-tools.d.ts.map +1 -1
- package/dist/planning-guardrails.d.ts +24 -5
- package/dist/planning-guardrails.d.ts.map +1 -1
- package/dist/plugin.js +1006 -475
- package/dist/query-tools.d.ts +23 -5
- package/dist/query-tools.d.ts.map +1 -1
- package/dist/regression-detection.d.ts +58 -0
- package/dist/regression-detection.d.ts.map +1 -0
- package/dist/swarm-orchestrate.d.ts +3 -3
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +4 -4
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-prompts.js +165 -74
- package/dist/swarm-research.d.ts +0 -2
- package/dist/swarm-research.d.ts.map +1 -1
- package/dist/tool-availability.d.ts +1 -1
- package/dist/tool-availability.d.ts.map +1 -1
- package/examples/commands/swarm.md +7 -7
- package/global-skills/swarm-coordination/SKILL.md +6 -6
- package/package.json +6 -3
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* CASS
|
|
3
|
+
* CASS Inhouse Implementation Characterization Tests
|
|
4
4
|
*
|
|
5
|
-
* These tests capture the CURRENT behavior of the CASS
|
|
6
|
-
* They document WHAT the
|
|
5
|
+
* These tests capture the CURRENT behavior of the inhouse CASS implementation.
|
|
6
|
+
* They document WHAT the implementation DOES, not what it SHOULD do.
|
|
7
7
|
*
|
|
8
|
-
* Purpose:
|
|
9
|
-
* Our inhouse implementation must match these behaviors exactly.
|
|
8
|
+
* Purpose: Verify inhouse implementation matches expected behavior after migration from binary.
|
|
10
9
|
*
|
|
11
10
|
* Pattern: Feathers Characterization Testing
|
|
12
11
|
* 1. Write a test you KNOW will fail
|
|
@@ -17,406 +16,428 @@
|
|
|
17
16
|
* DO NOT modify these tests to match desired behavior.
|
|
18
17
|
* These are BASELINE tests - they verify behaviors ARE present.
|
|
19
18
|
*/
|
|
20
|
-
import { describe, test, expect } from "bun:test";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
19
|
+
import { describe, test, expect, beforeAll } from "bun:test";
|
|
20
|
+
import { cassTools } from "../src/cass-tools.ts";
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Test Helpers
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse JSON from tool output (tools return strings)
|
|
28
|
+
*/
|
|
29
|
+
function parseToolJSON(output: string): any {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(output);
|
|
32
|
+
} catch {
|
|
33
|
+
// If not JSON, return as-is
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Tests
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
describe("CASS Inhouse - cass_stats", () => {
|
|
43
|
+
test("returns JSON with SessionStats structure", async () => {
|
|
44
|
+
// CHARACTERIZATION: cass_stats returns JSON string with SessionStats
|
|
45
|
+
const output = await cassTools.cass_stats.execute({});
|
|
46
|
+
const result = parseToolJSON(output);
|
|
47
|
+
|
|
48
|
+
// Verify SessionStats structure
|
|
49
|
+
expect(result).toHaveProperty("total_sessions");
|
|
50
|
+
expect(result).toHaveProperty("total_chunks");
|
|
51
|
+
expect(result).toHaveProperty("by_agent");
|
|
52
|
+
|
|
53
|
+
// Verify types
|
|
54
|
+
expect(typeof result.total_sessions).toBe("number");
|
|
55
|
+
expect(typeof result.total_chunks).toBe("number");
|
|
56
|
+
expect(typeof result.by_agent).toBe("object");
|
|
57
|
+
|
|
58
|
+
// Verify by_agent structure
|
|
59
|
+
if (Object.keys(result.by_agent).length > 0) {
|
|
60
|
+
const firstAgent = Object.entries(result.by_agent)[0][1] as any;
|
|
61
|
+
expect(firstAgent).toHaveProperty("sessions");
|
|
62
|
+
expect(firstAgent).toHaveProperty("chunks");
|
|
63
|
+
expect(typeof firstAgent.sessions).toBe("number");
|
|
64
|
+
expect(typeof firstAgent.chunks).toBe("number");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("numeric fields are non-negative", async () => {
|
|
69
|
+
// CHARACTERIZATION: Counts should be >= 0
|
|
70
|
+
const output = await cassTools.cass_stats.execute({});
|
|
71
|
+
const result = parseToolJSON(output);
|
|
72
|
+
|
|
73
|
+
expect(result.total_sessions).toBeGreaterThanOrEqual(0);
|
|
74
|
+
expect(result.total_chunks).toBeGreaterThanOrEqual(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("CASS Inhouse - cass_health", () => {
|
|
79
|
+
test("returns JSON with IndexHealth structure", async () => {
|
|
80
|
+
// CHARACTERIZATION: cass_health returns JSON with IndexHealth
|
|
81
|
+
const output = await cassTools.cass_health.execute({});
|
|
82
|
+
const result = parseToolJSON(output);
|
|
83
|
+
|
|
84
|
+
// Verify IndexHealth structure
|
|
85
|
+
expect(result).toHaveProperty("healthy");
|
|
86
|
+
expect(result).toHaveProperty("message");
|
|
87
|
+
expect(result).toHaveProperty("total_indexed");
|
|
88
|
+
expect(result).toHaveProperty("stale_count");
|
|
89
|
+
expect(result).toHaveProperty("fresh_count");
|
|
90
|
+
|
|
91
|
+
// Verify types
|
|
92
|
+
expect(typeof result.healthy).toBe("boolean");
|
|
93
|
+
expect(typeof result.message).toBe("string");
|
|
94
|
+
expect(typeof result.total_indexed).toBe("number");
|
|
95
|
+
expect(typeof result.stale_count).toBe("number");
|
|
96
|
+
expect(typeof result.fresh_count).toBe("number");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("healthy=true when total_indexed > 0 and stale_count === 0", async () => {
|
|
100
|
+
// CHARACTERIZATION: Health is determined by indexed count and staleness
|
|
101
|
+
const output = await cassTools.cass_health.execute({});
|
|
102
|
+
const result = parseToolJSON(output);
|
|
103
|
+
|
|
104
|
+
if (result.total_indexed > 0 && result.stale_count === 0) {
|
|
105
|
+
expect(result.healthy).toBe(true);
|
|
106
|
+
expect(result.message).toContain("ready");
|
|
107
|
+
} else {
|
|
108
|
+
expect(result.healthy).toBe(false);
|
|
109
|
+
expect(result.message).toContain("needs");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("includes optional timestamp fields when data exists", async () => {
|
|
114
|
+
// CHARACTERIZATION: oldest_indexed and newest_indexed are optional
|
|
115
|
+
const output = await cassTools.cass_health.execute({});
|
|
116
|
+
const result = parseToolJSON(output);
|
|
117
|
+
|
|
118
|
+
if (result.total_indexed > 0) {
|
|
119
|
+
// When there are indexed files, timestamps should be present
|
|
120
|
+
if (result.oldest_indexed !== undefined) {
|
|
121
|
+
expect(typeof result.oldest_indexed).toBe("string");
|
|
122
|
+
}
|
|
123
|
+
if (result.newest_indexed !== undefined) {
|
|
124
|
+
expect(typeof result.newest_indexed).toBe("string");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("CASS Inhouse - cass_search", () => {
|
|
131
|
+
test("returns formatted string with results", async () => {
|
|
132
|
+
// CHARACTERIZATION: cass_search returns formatted string, not JSON
|
|
133
|
+
const output = await cassTools.cass_search.execute({
|
|
134
|
+
query: "test",
|
|
135
|
+
limit: 2,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Should be a string (not JSON object)
|
|
139
|
+
expect(typeof output).toBe("string");
|
|
140
|
+
|
|
141
|
+
// If results exist, should contain numbered results
|
|
142
|
+
if (!output.includes("No results found")) {
|
|
143
|
+
expect(output).toMatch(/1\./); // Numbered result
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("minimal fields format returns compact output", async () => {
|
|
148
|
+
// CHARACTERIZATION: fields='minimal' returns path:line (agent) format
|
|
149
|
+
const output = await cassTools.cass_search.execute({
|
|
150
|
+
query: "test",
|
|
151
|
+
limit: 2,
|
|
152
|
+
fields: "minimal",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!output.includes("No results found")) {
|
|
156
|
+
// Minimal format: "1. /path/to/file.jsonl:42 (agent)"
|
|
157
|
+
expect(output).toMatch(/\d+\.\s+\S+:\d+\s+\(\w+\)/);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("default format includes score and preview", async () => {
|
|
162
|
+
// CHARACTERIZATION: Default format includes score and content preview
|
|
163
|
+
const output = await cassTools.cass_search.execute({
|
|
164
|
+
query: "test",
|
|
165
|
+
limit: 1,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!output.includes("No results found")) {
|
|
169
|
+
expect(output).toContain("Score:");
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("empty results return helpful message", async () => {
|
|
174
|
+
// CHARACTERIZATION: No results message suggests actions
|
|
175
|
+
const output = await cassTools.cass_search.execute({
|
|
176
|
+
query: "xyzzy-nonexistent-term-99999-abcdef",
|
|
177
|
+
limit: 5,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(output).toContain("No results found");
|
|
181
|
+
expect(output).toContain("Try:");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("limit parameter controls max results", async () => {
|
|
185
|
+
// CHARACTERIZATION: Limit controls result count
|
|
186
|
+
const output = await cassTools.cass_search.execute({
|
|
187
|
+
query: "test",
|
|
188
|
+
limit: 1,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!output.includes("No results found")) {
|
|
192
|
+
const lines = output.split("\n").filter((l) => l.match(/^\d+\./));
|
|
193
|
+
expect(lines.length).toBeLessThanOrEqual(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("agent filter parameter accepted", async () => {
|
|
198
|
+
// CHARACTERIZATION: agent parameter filters by agent type
|
|
199
|
+
const output = await cassTools.cass_search.execute({
|
|
200
|
+
query: "test",
|
|
201
|
+
agent: "claude",
|
|
202
|
+
limit: 2,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Should not throw, result format same as unfiltered
|
|
206
|
+
expect(typeof output).toBe("string");
|
|
207
|
+
});
|
|
96
208
|
});
|
|
97
209
|
|
|
98
|
-
describe("CASS
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const firstHit = result.hits[0];
|
|
127
|
-
expect(firstHit).toHaveProperty("agent");
|
|
128
|
-
expect(firstHit).toHaveProperty("content");
|
|
129
|
-
expect(firstHit).toHaveProperty("created_at");
|
|
130
|
-
expect(firstHit).toHaveProperty("line_number");
|
|
131
|
-
expect(firstHit).toHaveProperty("match_type");
|
|
132
|
-
expect(firstHit).toHaveProperty("score");
|
|
133
|
-
expect(firstHit).toHaveProperty("snippet");
|
|
134
|
-
expect(firstHit).toHaveProperty("source_path");
|
|
135
|
-
expect(firstHit).toHaveProperty("title");
|
|
136
|
-
expect(firstHit).toHaveProperty("workspace");
|
|
137
|
-
|
|
138
|
-
expect(typeof firstHit.agent).toBe("string");
|
|
139
|
-
expect(typeof firstHit.content).toBe("string");
|
|
140
|
-
expect(typeof firstHit.created_at).toBe("number");
|
|
141
|
-
expect(typeof firstHit.line_number).toBe("number");
|
|
142
|
-
expect(typeof firstHit.match_type).toBe("string");
|
|
143
|
-
expect(typeof firstHit.score).toBe("number");
|
|
144
|
-
expect(typeof firstHit.snippet).toBe("string");
|
|
145
|
-
expect(typeof firstHit.source_path).toBe("string");
|
|
146
|
-
expect(typeof firstHit.title).toBe("string");
|
|
147
|
-
expect(typeof firstHit.workspace).toBe("string");
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("query parameter is preserved in response", async () => {
|
|
152
|
-
// CHARACTERIZATION: Query echoed back in response
|
|
153
|
-
const testQuery = "characterization-test-query-12345";
|
|
154
|
-
const result = await $`cass search "${testQuery}" --json`.quiet().json();
|
|
155
|
-
|
|
156
|
-
expect(result.query).toBe(testQuery);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("limit parameter is respected", async () => {
|
|
160
|
-
// CHARACTERIZATION: Limit controls max hits returned
|
|
161
|
-
const result = await $`cass search "test" --limit 3 --json`.quiet().json();
|
|
162
|
-
|
|
163
|
-
expect(result.limit).toBe(3);
|
|
164
|
-
if (result.hits.length > 0) {
|
|
165
|
-
expect(result.hits.length).toBeLessThanOrEqual(3);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("empty results include suggestions field", async () => {
|
|
170
|
-
// CHARACTERIZATION: Empty results return suggestions
|
|
171
|
-
const result =
|
|
172
|
-
await $`cass search "xyzzy-nonexistent-term-99999" --json`.quiet().json();
|
|
173
|
-
|
|
174
|
-
if (result.total_matches === 0) {
|
|
175
|
-
// Empty results may include suggestions (optional feature)
|
|
176
|
-
// Just verify structure if present
|
|
177
|
-
if (result.suggestions) {
|
|
178
|
-
expect(Array.isArray(result.suggestions)).toBe(true);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
});
|
|
210
|
+
describe("CASS Inhouse - cass_view", () => {
|
|
211
|
+
test("returns viewSessionLine formatted output", async () => {
|
|
212
|
+
// CHARACTERIZATION: cass_view uses viewSessionLine format
|
|
213
|
+
// We need a real session file to test this
|
|
214
|
+
// For now, just test error handling
|
|
215
|
+
|
|
216
|
+
const output = await cassTools.cass_view.execute({
|
|
217
|
+
path: "/nonexistent/session.jsonl",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Error case returns JSON
|
|
221
|
+
const result = parseToolJSON(output);
|
|
222
|
+
if (result.error) {
|
|
223
|
+
expect(result).toHaveProperty("error");
|
|
224
|
+
expect(typeof result.error).toBe("string");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("line parameter jumps to specific line", async () => {
|
|
229
|
+
// CHARACTERIZATION: line parameter controls starting line
|
|
230
|
+
const output = await cassTools.cass_view.execute({
|
|
231
|
+
path: "/nonexistent/session.jsonl",
|
|
232
|
+
line: 42,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// For now just verify parameter is accepted
|
|
236
|
+
expect(typeof output).toBe("string");
|
|
237
|
+
});
|
|
182
238
|
});
|
|
183
239
|
|
|
184
|
-
describe("CASS
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
describe("CASS Inhouse - cass_expand", () => {
|
|
241
|
+
test("returns expanded context around line", async () => {
|
|
242
|
+
// CHARACTERIZATION: cass_expand uses viewSessionLine with context
|
|
243
|
+
const output = await cassTools.cass_expand.execute({
|
|
244
|
+
path: "/nonexistent/session.jsonl",
|
|
245
|
+
line: 10,
|
|
246
|
+
context: 5,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Error case returns JSON
|
|
250
|
+
const result = parseToolJSON(output);
|
|
251
|
+
if (result.error) {
|
|
252
|
+
expect(result).toHaveProperty("error");
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("context parameter controls window size", async () => {
|
|
257
|
+
// CHARACTERIZATION: context defaults to 5, can be overridden
|
|
258
|
+
const output = await cassTools.cass_expand.execute({
|
|
259
|
+
path: "/nonexistent/session.jsonl",
|
|
260
|
+
line: 10,
|
|
261
|
+
context: 10,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Parameter accepted
|
|
265
|
+
expect(typeof output).toBe("string");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("CASS Inhouse - cass_index", () => {
|
|
270
|
+
// NOTE: cass_index tests are slow (5s+ timeout) - skip in unit tests
|
|
271
|
+
// These are integration tests that require indexing real files
|
|
272
|
+
test.skip("returns summary string with counts", async () => {
|
|
273
|
+
// CHARACTERIZATION: cass_index returns summary string
|
|
274
|
+
const output = await cassTools.cass_index.execute({});
|
|
275
|
+
|
|
276
|
+
expect(typeof output).toBe("string");
|
|
277
|
+
expect(output).toMatch(/Indexed \d+ sessions/);
|
|
278
|
+
expect(output).toMatch(/\d+ chunks/);
|
|
279
|
+
expect(output).toMatch(/\d+ms/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test.skip("full rebuild flag accepted", async () => {
|
|
283
|
+
// CHARACTERIZATION: full parameter triggers full rebuild
|
|
284
|
+
const output = await cassTools.cass_index.execute({ full: true });
|
|
285
|
+
|
|
286
|
+
expect(typeof output).toBe("string");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test.skip("incremental indexing is default", async () => {
|
|
290
|
+
// CHARACTERIZATION: No full flag = incremental
|
|
291
|
+
const output = await cassTools.cass_index.execute({});
|
|
292
|
+
|
|
293
|
+
expect(typeof output).toBe("string");
|
|
294
|
+
});
|
|
225
295
|
});
|
|
226
296
|
|
|
227
|
-
describe("CASS
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const sessionFiles = await $`ls ~/.config/swarm-tools/sessions/*.jsonl`
|
|
250
|
-
.quiet()
|
|
251
|
-
.text()
|
|
252
|
-
.catch(() => "");
|
|
253
|
-
|
|
254
|
-
if (sessionFiles.trim()) {
|
|
255
|
-
const firstFile = sessionFiles.split("\n")[0].trim();
|
|
256
|
-
const result = await $`cass view ${firstFile} -n 1`.quiet().text();
|
|
257
|
-
|
|
258
|
-
// Target line marked with >
|
|
259
|
-
expect(result).toMatch(/>\s+\d+\s+\|/);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test("view with non-existent file returns error", async () => {
|
|
264
|
-
// CHARACTERIZATION: File not found error structure
|
|
265
|
-
const result = await $`cass view /nonexistent/path.jsonl -n 1 --json`
|
|
266
|
-
.quiet()
|
|
267
|
-
.json()
|
|
268
|
-
.catch((e) => JSON.parse(e.stderr.toString()));
|
|
269
|
-
|
|
270
|
-
expect(result).toHaveProperty("error");
|
|
271
|
-
expect(result.error).toHaveProperty("code");
|
|
272
|
-
expect(result.error).toHaveProperty("kind");
|
|
273
|
-
expect(result.error).toHaveProperty("message");
|
|
274
|
-
expect(result.error).toHaveProperty("retryable");
|
|
275
|
-
|
|
276
|
-
expect(result.error.code).toBe(3);
|
|
277
|
-
expect(result.error.kind).toBe("file-not-found");
|
|
278
|
-
expect(result.error.retryable).toBe(false);
|
|
279
|
-
});
|
|
297
|
+
describe("CASS Inhouse - Error Handling", () => {
|
|
298
|
+
test("errors return JSON with error field", async () => {
|
|
299
|
+
// CHARACTERIZATION: Tool errors return {error: string}
|
|
300
|
+
const output = await cassTools.cass_view.execute({
|
|
301
|
+
path: "/definitely/does/not/exist.jsonl",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const result = parseToolJSON(output);
|
|
305
|
+
expect(result).toHaveProperty("error");
|
|
306
|
+
expect(typeof result.error).toBe("string");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("search errors fall back gracefully", async () => {
|
|
310
|
+
// CHARACTERIZATION: Search with bad agent filter doesn't crash
|
|
311
|
+
const output = await cassTools.cass_search.execute({
|
|
312
|
+
query: "test",
|
|
313
|
+
agent: "nonexistent-agent-type",
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Should return results or no results message, not error
|
|
317
|
+
expect(typeof output).toBe("string");
|
|
318
|
+
});
|
|
280
319
|
});
|
|
281
320
|
|
|
282
|
-
describe("CASS
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
stdout: "pipe",
|
|
307
|
-
stderr: "pipe",
|
|
308
|
-
});
|
|
309
|
-
const exitCodeInvalid = await procInvalidArg.exited;
|
|
310
|
-
expect(exitCodeInvalid).toBe(2); // Usage error
|
|
311
|
-
|
|
312
|
-
const procSuccess = Bun.spawn(["cass", "stats", "--json"], {
|
|
313
|
-
stdout: "pipe",
|
|
314
|
-
stderr: "pipe",
|
|
315
|
-
});
|
|
316
|
-
const exitCodeSuccess = await procSuccess.exited;
|
|
317
|
-
expect([0, 3]).toContain(exitCodeSuccess); // Success or missing index
|
|
318
|
-
});
|
|
321
|
+
describe("CASS Inhouse - Agent Discovery", () => {
|
|
322
|
+
test("indexes multiple agent directories", async () => {
|
|
323
|
+
// CHARACTERIZATION: Indexes from ~/.opencode, ~/.config/swarm-tools, etc.
|
|
324
|
+
const output = await cassTools.cass_stats.execute({});
|
|
325
|
+
const result = parseToolJSON(output);
|
|
326
|
+
|
|
327
|
+
// by_agent should contain different agent types if they exist
|
|
328
|
+
expect(typeof result.by_agent).toBe("object");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("detects agent type from path", async () => {
|
|
332
|
+
// CHARACTERIZATION: Path like ~/.local/share/Claude → agent='claude'
|
|
333
|
+
const output = await cassTools.cass_stats.execute({});
|
|
334
|
+
const result = parseToolJSON(output);
|
|
335
|
+
|
|
336
|
+
// Agent types are detected from paths
|
|
337
|
+
// Possible values: claude, cursor, opencode, opencode-swarm, codex, aider
|
|
338
|
+
if (Object.keys(result.by_agent).length > 0) {
|
|
339
|
+
const agentTypes = Object.keys(result.by_agent);
|
|
340
|
+
for (const agentType of agentTypes) {
|
|
341
|
+
expect(typeof agentType).toBe("string");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
319
345
|
});
|
|
320
346
|
|
|
321
|
-
describe("CASS
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
347
|
+
describe("CASS Inhouse - Staleness Detection", () => {
|
|
348
|
+
test("health check reports stale files", async () => {
|
|
349
|
+
// CHARACTERIZATION: stale_count indicates files needing reindex
|
|
350
|
+
const output = await cassTools.cass_health.execute({});
|
|
351
|
+
const result = parseToolJSON(output);
|
|
352
|
+
|
|
353
|
+
expect(result).toHaveProperty("stale_count");
|
|
354
|
+
expect(typeof result.stale_count).toBe("number");
|
|
355
|
+
expect(result.stale_count).toBeGreaterThanOrEqual(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("fresh_count indicates up-to-date files", async () => {
|
|
359
|
+
// CHARACTERIZATION: fresh_count shows files that don't need reindex
|
|
360
|
+
const output = await cassTools.cass_health.execute({});
|
|
361
|
+
const result = parseToolJSON(output);
|
|
362
|
+
|
|
363
|
+
expect(result).toHaveProperty("fresh_count");
|
|
364
|
+
expect(typeof result.fresh_count).toBe("number");
|
|
365
|
+
expect(result.fresh_count).toBeGreaterThanOrEqual(0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("total_indexed equals fresh_count + stale_count", async () => {
|
|
369
|
+
// CHARACTERIZATION: Counts should add up
|
|
370
|
+
const output = await cassTools.cass_health.execute({});
|
|
371
|
+
const result = parseToolJSON(output);
|
|
372
|
+
|
|
373
|
+
expect(result.total_indexed).toBe(
|
|
374
|
+
result.fresh_count + result.stale_count,
|
|
375
|
+
);
|
|
376
|
+
});
|
|
341
377
|
});
|
|
342
378
|
|
|
343
|
-
describe("CASS
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
.json();
|
|
357
|
-
const robotResult = await $`cass search "test" --limit 1 --robot`
|
|
358
|
-
.quiet()
|
|
359
|
-
.json();
|
|
360
|
-
|
|
361
|
-
// Both should return same structure
|
|
362
|
-
expect(jsonResult).toHaveProperty("hits");
|
|
363
|
-
expect(robotResult).toHaveProperty("hits");
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
test("limit flag controls result count", async () => {
|
|
367
|
-
// CHARACTERIZATION: --limit parameter
|
|
368
|
-
const result = await $`cass search "test" --limit 1 --json`.quiet().json();
|
|
369
|
-
|
|
370
|
-
expect(result.limit).toBe(1);
|
|
371
|
-
expect(result.hits.length).toBeLessThanOrEqual(1);
|
|
372
|
-
});
|
|
379
|
+
describe("CASS Inhouse - Ollama Fallback", () => {
|
|
380
|
+
test("search works even if Ollama unavailable", async () => {
|
|
381
|
+
// CHARACTERIZATION: Graceful degradation to FTS5
|
|
382
|
+
// Hard to test without mocking, but search shouldn't crash
|
|
383
|
+
const output = await cassTools.cass_search.execute({
|
|
384
|
+
query: "test",
|
|
385
|
+
limit: 1,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Should return either results or "No results found"
|
|
389
|
+
expect(typeof output).toBe("string");
|
|
390
|
+
expect(output.length).toBeGreaterThan(0);
|
|
391
|
+
});
|
|
373
392
|
});
|
|
374
393
|
|
|
375
394
|
/**
|
|
376
395
|
* CHARACTERIZATION NOTES:
|
|
377
396
|
*
|
|
378
|
-
* These tests document the following CASS
|
|
397
|
+
* These tests document the following inhouse CASS behaviors:
|
|
379
398
|
*
|
|
380
|
-
* 1.
|
|
381
|
-
* -
|
|
382
|
-
* -
|
|
383
|
-
* -
|
|
399
|
+
* 1. Output Format Changes from Binary:
|
|
400
|
+
* - cass_stats: Returns JSON string with SessionStats structure
|
|
401
|
+
* - cass_health: Returns JSON with healthy boolean + IndexHealth fields
|
|
402
|
+
* - cass_search: Returns formatted string (not JSON), with optional minimal mode
|
|
403
|
+
* - cass_view/cass_expand: Returns viewSessionLine formatted output
|
|
404
|
+
* - cass_index: Returns summary string with counts
|
|
384
405
|
*
|
|
385
|
-
* 2.
|
|
386
|
-
* -
|
|
387
|
-
* -
|
|
388
|
-
* -
|
|
406
|
+
* 2. Data Structures:
|
|
407
|
+
* - SessionStats: { total_sessions, total_chunks, by_agent }
|
|
408
|
+
* - IndexHealth: { healthy, message, total_indexed, stale_count, fresh_count, oldest_indexed?, newest_indexed? }
|
|
409
|
+
* - SearchResult: Formatted string with numbered results, scores, previews
|
|
389
410
|
*
|
|
390
|
-
* 3.
|
|
391
|
-
* -
|
|
392
|
-
* -
|
|
393
|
-
* -
|
|
394
|
-
* - Error objects with code, kind, message, retryable fields
|
|
411
|
+
* 3. Agent Discovery:
|
|
412
|
+
* - Indexes from multiple directories (~/.opencode, ~/.config/swarm-tools, etc.)
|
|
413
|
+
* - Detects agent type from path (claude, cursor, opencode, aider, etc.)
|
|
414
|
+
* - by_agent groups stats by detected agent type
|
|
395
415
|
*
|
|
396
|
-
* 4.
|
|
397
|
-
* -
|
|
398
|
-
* -
|
|
399
|
-
* -
|
|
416
|
+
* 4. Staleness Detection:
|
|
417
|
+
* - stale_count: Files modified since last index
|
|
418
|
+
* - fresh_count: Files up-to-date
|
|
419
|
+
* - total_indexed = fresh_count + stale_count
|
|
420
|
+
* - healthy = total_indexed > 0 && stale_count === 0
|
|
400
421
|
*
|
|
401
|
-
* 5.
|
|
402
|
-
* -
|
|
403
|
-
* -
|
|
404
|
-
* - Empty results may include suggestions
|
|
422
|
+
* 5. Ollama Fallback:
|
|
423
|
+
* - Search falls back to FTS5 if Ollama unavailable
|
|
424
|
+
* - Graceful degradation with warning, no crash
|
|
405
425
|
*
|
|
406
|
-
* 6.
|
|
407
|
-
* -
|
|
408
|
-
* -
|
|
409
|
-
* -
|
|
410
|
-
* - Horizontal separators
|
|
426
|
+
* 6. Error Handling:
|
|
427
|
+
* - Tools return JSON with {error: string} on failure
|
|
428
|
+
* - Search with invalid agent filter returns empty results, not error
|
|
429
|
+
* - View/expand with missing file returns error JSON
|
|
411
430
|
*
|
|
412
|
-
* 7.
|
|
413
|
-
* -
|
|
414
|
-
* -
|
|
415
|
-
* -
|
|
431
|
+
* 7. Removed from Binary Version:
|
|
432
|
+
* - No --json/--robot flags (always returns structured output)
|
|
433
|
+
* - No robot-docs subcommand
|
|
434
|
+
* - No --robot-help flag
|
|
435
|
+
* - No human-readable table format (JSON/formatted strings only)
|
|
436
|
+
* - No exit codes (tools return strings)
|
|
416
437
|
*
|
|
417
|
-
* When
|
|
438
|
+
* When modifying the inhouse implementation:
|
|
418
439
|
* - Match these structures exactly
|
|
419
440
|
* - Preserve field names and types
|
|
420
441
|
* - Maintain error response format
|
|
421
|
-
* - Keep
|
|
442
|
+
* - Keep agent discovery patterns consistent
|
|
422
443
|
*/
|