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.
Files changed (47) hide show
  1. package/bin/cass.characterization.test.ts +400 -379
  2. package/bin/eval-gate.test.ts +23 -0
  3. package/bin/eval-gate.ts +21 -0
  4. package/bin/swarm.ts +485 -151
  5. package/dist/bin/swarm.js +2120 -971
  6. package/dist/cass-tools.d.ts +1 -2
  7. package/dist/cass-tools.d.ts.map +1 -1
  8. package/dist/compaction-hook.d.ts +50 -6
  9. package/dist/compaction-hook.d.ts.map +1 -1
  10. package/dist/dashboard.d.ts +5 -6
  11. package/dist/dashboard.d.ts.map +1 -1
  12. package/dist/eval-capture.d.ts +20 -10
  13. package/dist/eval-capture.d.ts.map +1 -1
  14. package/dist/eval-capture.js +54 -21
  15. package/dist/eval-learning.d.ts +5 -5
  16. package/dist/eval-learning.d.ts.map +1 -1
  17. package/dist/hive.d.ts +7 -0
  18. package/dist/hive.d.ts.map +1 -1
  19. package/dist/hive.js +61 -24
  20. package/dist/hivemind-tools.d.ts +479 -0
  21. package/dist/hivemind-tools.d.ts.map +1 -0
  22. package/dist/index.d.ts +31 -98
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1018 -467
  25. package/dist/observability-health.d.ts +87 -0
  26. package/dist/observability-health.d.ts.map +1 -0
  27. package/dist/observability-tools.d.ts +5 -1
  28. package/dist/observability-tools.d.ts.map +1 -1
  29. package/dist/planning-guardrails.d.ts +24 -5
  30. package/dist/planning-guardrails.d.ts.map +1 -1
  31. package/dist/plugin.js +1006 -475
  32. package/dist/query-tools.d.ts +23 -5
  33. package/dist/query-tools.d.ts.map +1 -1
  34. package/dist/regression-detection.d.ts +58 -0
  35. package/dist/regression-detection.d.ts.map +1 -0
  36. package/dist/swarm-orchestrate.d.ts +3 -3
  37. package/dist/swarm-orchestrate.d.ts.map +1 -1
  38. package/dist/swarm-prompts.d.ts +4 -4
  39. package/dist/swarm-prompts.d.ts.map +1 -1
  40. package/dist/swarm-prompts.js +165 -74
  41. package/dist/swarm-research.d.ts +0 -2
  42. package/dist/swarm-research.d.ts.map +1 -1
  43. package/dist/tool-availability.d.ts +1 -1
  44. package/dist/tool-availability.d.ts.map +1 -1
  45. package/examples/commands/swarm.md +7 -7
  46. package/global-skills/swarm-coordination/SKILL.md +6 -6
  47. package/package.json +6 -3
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * CASS Binary Characterization Tests
3
+ * CASS Inhouse Implementation Characterization Tests
4
4
  *
5
- * These tests capture the CURRENT behavior of the CASS binary tools.
6
- * They document WHAT the binary DOES, not what it SHOULD do.
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: Enable safe refactoring during ADR-010 (CASS inhousing).
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 { $ } from "bun";
22
- import {
23
- cassStatsBaseline,
24
- cassSearchBaseline,
25
- cassHealthHumanBaseline,
26
- cassStatsHumanBaseline,
27
- cassViewBaseline,
28
- cassErrorBaseline,
29
- type CassStatsResponse,
30
- type CassSearchResponse,
31
- } from "../evals/fixtures/cass-baseline.ts";
32
-
33
- describe("CASS Binary - cass stats", () => {
34
- test("JSON output structure matches baseline", async () => {
35
- // CHARACTERIZATION: This documents the actual JSON structure
36
- const result = await $`cass stats --json`.quiet().json();
37
-
38
- // Verify top-level structure
39
- expect(result).toHaveProperty("by_agent");
40
- expect(result).toHaveProperty("conversations");
41
- expect(result).toHaveProperty("date_range");
42
- expect(result).toHaveProperty("db_path");
43
- expect(result).toHaveProperty("messages");
44
- expect(result).toHaveProperty("top_workspaces");
45
-
46
- // Verify by_agent structure
47
- expect(Array.isArray(result.by_agent)).toBe(true);
48
- if (result.by_agent.length > 0) {
49
- const firstAgent = result.by_agent[0];
50
- expect(firstAgent).toHaveProperty("agent");
51
- expect(firstAgent).toHaveProperty("count");
52
- expect(typeof firstAgent.agent).toBe("string");
53
- expect(typeof firstAgent.count).toBe("number");
54
- }
55
-
56
- // Verify date_range structure
57
- expect(result.date_range).toHaveProperty("newest");
58
- expect(result.date_range).toHaveProperty("oldest");
59
- expect(typeof result.date_range.newest).toBe("string");
60
- expect(typeof result.date_range.oldest).toBe("string");
61
-
62
- // Verify top_workspaces structure
63
- expect(Array.isArray(result.top_workspaces)).toBe(true);
64
- if (result.top_workspaces.length > 0) {
65
- const firstWorkspace = result.top_workspaces[0];
66
- expect(firstWorkspace).toHaveProperty("count");
67
- expect(firstWorkspace).toHaveProperty("workspace");
68
- expect(typeof firstWorkspace.count).toBe("number");
69
- expect(typeof firstWorkspace.workspace).toBe("string");
70
- }
71
-
72
- // Verify numeric fields are numbers
73
- expect(typeof result.conversations).toBe("number");
74
- expect(typeof result.messages).toBe("number");
75
- });
76
-
77
- test("human-readable output format matches baseline", async () => {
78
- // CHARACTERIZATION: This documents the actual human-readable format
79
- const result = await $`cass stats`.quiet().text();
80
-
81
- // Verify presence of key sections (order matters)
82
- expect(result).toContain("CASS Index Statistics");
83
- expect(result).toContain("Database:");
84
- expect(result).toContain("Totals:");
85
- expect(result).toContain("Conversations:");
86
- expect(result).toContain("Messages:");
87
- expect(result).toContain("By Agent:");
88
- expect(result).toContain("Top Workspaces:");
89
- expect(result).toContain("Date Range:");
90
-
91
- // Verify format patterns
92
- expect(result).toMatch(/Conversations: \d+/);
93
- expect(result).toMatch(/Messages: \d+/);
94
- expect(result).toMatch(/\w+: \d+/); // Agent counts
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 Binary - cass search", () => {
99
- test("JSON output structure matches baseline", async () => {
100
- // CHARACTERIZATION: This documents the actual search response structure
101
- const result = await $`cass search "test" --limit 2 --json`.quiet().json();
102
-
103
- // Verify top-level structure
104
- expect(result).toHaveProperty("count");
105
- expect(result).toHaveProperty("cursor");
106
- expect(result).toHaveProperty("hits");
107
- expect(result).toHaveProperty("hits_clamped");
108
- expect(result).toHaveProperty("limit");
109
- expect(result).toHaveProperty("max_tokens");
110
- expect(result).toHaveProperty("offset");
111
- expect(result).toHaveProperty("query");
112
- expect(result).toHaveProperty("request_id");
113
- expect(result).toHaveProperty("total_matches");
114
-
115
- // Verify types
116
- expect(typeof result.count).toBe("number");
117
- expect(typeof result.hits_clamped).toBe("boolean");
118
- expect(typeof result.limit).toBe("number");
119
- expect(typeof result.offset).toBe("number");
120
- expect(typeof result.query).toBe("string");
121
- expect(typeof result.total_matches).toBe("number");
122
- expect(Array.isArray(result.hits)).toBe(true);
123
-
124
- // Verify hit structure (if any hits returned)
125
- if (result.hits.length > 0) {
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 Binary - cass health", () => {
185
- test("health check returns status indicator", async () => {
186
- // CHARACTERIZATION: Health check outputs status with timing
187
- const result = await $`cass health`.quiet().text();
188
-
189
- // Should contain status indicator (✓ or ✗)
190
- const hasHealthyIndicator = result.includes("✓ Healthy");
191
- const hasUnhealthyIndicator = result.includes("✗");
192
-
193
- expect(hasHealthyIndicator || hasUnhealthyIndicator).toBe(true);
194
-
195
- // Should include timing information
196
- if (hasHealthyIndicator) {
197
- expect(result).toMatch(/\(\d+ms\)/);
198
- }
199
- });
200
-
201
- test("health check may include staleness note", async () => {
202
- // CHARACTERIZATION: May warn about stale index
203
- const result = await $`cass health`.quiet().text();
204
-
205
- // If index is stale, should mention it
206
- // This is conditional - test just verifies format if present
207
- if (result.includes("stale")) {
208
- expect(result).toContain("Note:");
209
- expect(result).toMatch(/older than \d+s/);
210
- }
211
- });
212
-
213
- test("health check exits with code 0 when healthy", async () => {
214
- // CHARACTERIZATION: Exit code 0 = healthy
215
- const proc = Bun.spawn(["cass", "health"], {
216
- stdout: "pipe",
217
- stderr: "pipe",
218
- });
219
-
220
- const exitCode = await proc.exited;
221
- // Exit code 0 means healthy (even if stale)
222
- // Exit code 3 means missing index
223
- expect([0, 3]).toContain(exitCode);
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 Binary - cass view", () => {
228
- test("view output includes file path header", async () => {
229
- // CHARACTERIZATION: View starts with file path
230
- // We can only test this if session files exist
231
- const sessionFiles = await $`ls ~/.config/swarm-tools/sessions/*.jsonl`
232
- .quiet()
233
- .text()
234
- .catch(() => "");
235
-
236
- if (sessionFiles.trim()) {
237
- const firstFile = sessionFiles.split("\n")[0].trim();
238
- const result = await $`cass view ${firstFile} -n 1`.quiet().text();
239
-
240
- expect(result).toContain(`File: ${firstFile}`);
241
- expect(result).toContain("Line: 1");
242
- expect(result).toContain("context:");
243
- expect(result).toContain("----------------------------------------");
244
- }
245
- });
246
-
247
- test("view output shows line numbers with content", async () => {
248
- // CHARACTERIZATION: Lines prefixed with numbers
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 Binary - Error handling", () => {
283
- test("invalid arguments return usage error with hints", async () => {
284
- // CHARACTERIZATION: Helpful error messages with examples
285
- const proc = Bun.spawn(["cass", "stats", "--invalid-flag"], {
286
- stdout: "pipe",
287
- stderr: "pipe",
288
- });
289
-
290
- await proc.exited;
291
- const stderr = await new Response(proc.stderr).text();
292
-
293
- // Should contain helpful error information
294
- expect(stderr).toBeTruthy();
295
- // Typically shows usage or suggests --help
296
- expect(stderr.toLowerCase()).toMatch(/usage|help|invalid|unexpected/);
297
- });
298
-
299
- test("error responses include exit codes", async () => {
300
- // CHARACTERIZATION: Exit codes documented in baseline
301
- // Code 2 = usage error
302
- // Code 3 = missing file/db
303
- // Code 0 = success
304
-
305
- const procInvalidArg = Bun.spawn(["cass", "stats", "--invalid-flag"], {
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 Binary - Robot mode documentation", () => {
322
- test("--robot-help provides machine-readable documentation", async () => {
323
- // CHARACTERIZATION: Robot help is designed for AI agents
324
- const result = await $`cass --robot-help`.quiet().text();
325
-
326
- expect(result).toContain("cass --robot-help (contract v1)");
327
- expect(result).toContain("QUICKSTART (for AI agents):");
328
- expect(result).toContain("TIME FILTERS:");
329
- expect(result).toContain("WORKFLOW:");
330
- expect(result).toContain("OUTPUT:");
331
- expect(result).toContain("Exit codes:");
332
- });
333
-
334
- test("robot-docs subcommand exists", async () => {
335
- // CHARACTERIZATION: robot-docs provides detailed docs for AI
336
- const result = await $`cass robot-docs commands`.quiet().text();
337
-
338
- // Should return command documentation (exact format may vary)
339
- expect(result.length).toBeGreaterThan(0);
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 Binary - Flag behavior", () => {
344
- test("--json flag produces machine-readable output", async () => {
345
- // CHARACTERIZATION: --json enables JSON mode
346
- const result = await $`cass stats --json`.quiet().text();
347
-
348
- // Should be valid JSON
349
- expect(() => JSON.parse(result)).not.toThrow();
350
- });
351
-
352
- test("--json and --robot are equivalent", async () => {
353
- // CHARACTERIZATION: Both flags enable robot mode
354
- const jsonResult = await $`cass search "test" --limit 1 --json`
355
- .quiet()
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 binary behaviors:
397
+ * These tests document the following inhouse CASS behaviors:
379
398
  *
380
- * 1. JSON Output Structure:
381
- * - cass stats: by_agent[], conversations, date_range, messages, top_workspaces[]
382
- * - cass search: count, cursor, hits[], limit, offset, query, total_matches
383
- * - Hit objects: agent, content, created_at, line_number, score, source_path, etc.
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. Human-Readable Output:
386
- * - Formatted tables with headers
387
- * - Numeric statistics
388
- * - Date ranges
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. Error Handling:
391
- * - Exit code 0 = success
392
- * - Exit code 2 = usage error
393
- * - Exit code 3 = missing file/db
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. Robot Mode:
397
- * - --json and --robot flags are equivalent
398
- * - --robot-help provides AI-friendly documentation
399
- * - robot-docs subcommand for detailed docs
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. Search Behavior:
402
- * - Query parameter echoed in response
403
- * - Limit parameter controls max hits
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. View Behavior:
407
- * - File path header
408
- * - Line numbers with > indicator for target
409
- * - Context window (default 5 lines)
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. Health Check:
413
- * - Status indicator ( or )
414
- * - Timing in milliseconds
415
- * - Optional staleness warning
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 implementing the inhouse version:
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 exit codes consistent
442
+ * - Keep agent discovery patterns consistent
422
443
  */