1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
96
97
98
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
|
import { spawnSync } from "node:child_process";
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtemp as mkdtempP, rm as rmP, writeFile as writeFileP } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createSearchCodeTool } from "../../src/tools/search-code.js";
// A tiny stub that impersonates `cs`: it ignores its args and prints whatever
// JSON we put in the CS_STUB_OUTPUT env var. This makes JSON→text formatting
// tests fully deterministic without needing a real cs binary in CI.
function writeStub(dir: string, body: string): string {
const stubPath = join(dir, "cs-stub.sh");
writeFileSync(stubPath, body, { mode: 0o755 });
chmodSync(stubPath, 0o755);
return stubPath;
}
const ECHO_ENV_STUB = `#!/usr/bin/env bash
printf '%s' "$CS_STUB_OUTPUT"
`;
// A stub that writes to stderr and exits non-zero, impersonating a cs failure
// (bad flag, invalid regex, etc.).
const FAIL_STUB = `#!/usr/bin/env bash
echo "cs: simulated failure on stderr" >&2
exit 3
`;
describe("search_code tool", () => {
let workDir: string;
const savedBin = process.env.DISPATCH_CS_BIN;
const savedStubOut = process.env.CS_STUB_OUTPUT;
beforeEach(async () => {
workDir = await mkdtempP(join(tmpdir(), "dispatch-cs-test-"));
});
afterEach(async () => {
await rmP(workDir, { recursive: true, force: true });
if (savedBin === undefined) delete process.env.DISPATCH_CS_BIN;
else process.env.DISPATCH_CS_BIN = savedBin;
if (savedStubOut === undefined) delete process.env.CS_STUB_OUTPUT;
else process.env.CS_STUB_OUTPUT = savedStubOut;
});
it("exposes the expected name and schema", () => {
const tool = createSearchCodeTool(workDir);
expect(tool.name).toBe("search_code");
expect(tool.description).toContain("cs");
// query is required; a representative set of optional knobs exist.
const shape = (tool.parameters as unknown as { shape: Record<string, unknown> }).shape;
expect(shape.query).toBeDefined();
expect(shape.path).toBeDefined();
expect(shape.only).toBeDefined();
expect(shape.result_limit).toBeDefined();
});
it("requires a non-empty query", async () => {
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: " " });
expect(out).toMatch(/^Error:/);
expect(out).toContain("query is required");
});
it("does not crash when params are the wrong type (model hallucination)", async () => {
const tool = createSearchCodeTool(workDir);
// A non-string query must be rejected gracefully, not throw.
const q = await tool.execute({ query: ["a", "b"] as unknown as string });
expect(q).toMatch(/^Error:/);
expect(q).toContain("query is required");
// A non-string include_ext (array) must not throw "x.trim is not a function".
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
process.env.DISPATCH_CS_BIN = writeStub(stubDir, ECHO_ENV_STUB);
process.env.CS_STUB_OUTPUT = "null";
const out = await tool.execute({
query: "x",
include_ext: ["ts", "go"] as unknown as string,
exclude_pattern: { a: 1 } as unknown as string,
});
expect(out).toBe("No matches found.");
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
it("rejects a path outside the working directory", async () => {
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "anything", path: "../../etc" });
expect(out).toMatch(/^Error:/);
expect(out).toContain("outside the working directory");
});
it("rejects a path that points at a file, not a directory", async () => {
await writeFileP(join(workDir, "a-file.ts"), "const x = 1;\n");
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "x", path: "a-file.ts" });
expect(out).toMatch(/^Error:/);
expect(out).toContain("is a file, not a directory");
});
it("rejects a path that does not exist", async () => {
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "x", path: "no/such/dir" });
expect(out).toMatch(/^Error:/);
expect(out).toContain("does not exist");
});
it("returns an actionable error when the cs binary is missing", async () => {
process.env.DISPATCH_CS_BIN = "/nonexistent/path/to/cs-binary-xyz";
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "anything" });
expect(out).toMatch(/^Error:/);
expect(out).toContain("requires the 'cs'");
expect(out).toContain("DISPATCH_CS_BIN");
});
it("reports no matches when cs outputs null", async () => {
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
process.env.DISPATCH_CS_BIN = writeStub(stubDir, ECHO_ENV_STUB);
process.env.CS_STUB_OUTPUT = "null";
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "nothinghere" });
expect(out).toBe("No matches found.");
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
it("formats cs JSON results into readable per-file blocks", async () => {
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
const csJson = JSON.stringify([
{
filename: "web-search.ts",
location: join(workDir, "packages/core/src/tools/web-search.ts"),
score: 5.24,
language: "TypeScript",
total_lines: 106,
lines: [
{ line_number: 7, content: "" },
{
line_number: 8,
content: "export function createWebSearchTool(): ToolDefinition {",
match_positions: [[16, 35]],
},
{ line_number: 9, content: "\treturn {" },
],
},
{
filename: "index.ts",
location: join(workDir, "packages/core/src/index.ts"),
score: 1.1,
language: "TypeScript",
lines: [{ line_number: 113, content: 'export { createWebSearchTool } from "./web.js";' }],
},
]);
process.env.DISPATCH_CS_BIN = writeStub(stubDir, ECHO_ENV_STUB);
process.env.CS_STUB_OUTPUT = csJson;
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "createWebSearchTool" });
expect(out).toContain("Found matches in 2 files");
// Paths are rendered relative to the workdir.
expect(out).toContain("packages/core/src/tools/web-search.ts [TypeScript] (score 5.24)");
expect(out).not.toContain(workDir);
// Matched line is marked with '>'; line numbers + content present.
expect(out).toContain("> 8: export function createWebSearchTool(): ToolDefinition {");
expect(out).toContain(" 7: ");
expect(out).toContain("packages/core/src/index.ts [TypeScript] (score 1.10)");
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
it("renders cs 'content'-shape (prose) results instead of a bare header", async () => {
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
// cs's snippet mode emits `content` + `matchlocations` and no `lines`.
const csJson = JSON.stringify([
{
filename: "notes.md",
location: join(workDir, "docs/notes.md"),
score: 0.42,
language: "Markdown",
content: "Some heading\nthe orchestration paragraph that matched",
matchlocations: [[13, 26]],
},
]);
process.env.DISPATCH_CS_BIN = writeStub(stubDir, ECHO_ENV_STUB);
process.env.CS_STUB_OUTPUT = csJson;
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "orchestration" });
expect(out).toContain("docs/notes.md [Markdown] (score 0.42)");
// The snippet text must be present, not a bare header.
expect(out).toContain("the orchestration paragraph that matched");
expect(out).not.toContain("no snippet available");
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
it("truncates an excessively long snippet line", async () => {
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
const longContent = `const x = "${"Z".repeat(5000)}";`;
const csJson = JSON.stringify([
{
filename: "big.ts",
location: join(workDir, "big.ts"),
score: 1,
language: "TypeScript",
lines: [{ line_number: 1, content: longContent, match_positions: [[10, 14]] }],
},
]);
process.env.DISPATCH_CS_BIN = writeStub(stubDir, ECHO_ENV_STUB);
process.env.CS_STUB_OUTPUT = csJson;
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "x" });
expect(out).toContain("line truncated");
// No single output line should approach the raw 5k length.
const longest = Math.max(...out.split("\n").map((l) => l.length));
expect(longest).toBeLessThan(700);
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
it("surfaces raw output when cs returns unparseable JSON", async () => {
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
process.env.DISPATCH_CS_BIN = writeStub(stubDir, ECHO_ENV_STUB);
process.env.CS_STUB_OUTPUT = "this is not json";
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "x" });
expect(out).toMatch(/^Error:/);
expect(out).toContain("could not parse cs output");
expect(out).toContain("this is not json");
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
it("reports an error (not 'No matches') when cs exits non-zero", async () => {
const stubDir = await mkdtempP(join(tmpdir(), "dispatch-cs-stub-"));
try {
process.env.DISPATCH_CS_BIN = writeStub(stubDir, FAIL_STUB);
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "x" });
expect(out).toMatch(/^Error:/);
expect(out).toContain("exited with code 3");
// stderr from cs is surfaced to the caller.
expect(out).toContain("simulated failure on stderr");
expect(out).not.toContain("No matches found");
} finally {
await rmP(stubDir, { recursive: true, force: true });
}
});
// ── Live integration: only runs when a real `cs` binary is available. ──
const liveCsBin = findRealCs();
describe.runIf(liveCsBin)("live cs binary", () => {
it("finds a real match and ranks the defining file", async () => {
process.env.DISPATCH_CS_BIN = liveCsBin as string;
// Seed a small tree with a clear match.
await writeFileP(
join(workDir, "alpha.ts"),
"export function findTheNeedle() {\n return 42;\n}\n",
);
await writeFileP(join(workDir, "beta.ts"), "const x = 1;\n// nothing relevant here\n");
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "findTheNeedle" });
expect(out).toContain("alpha.ts");
expect(out).toContain("findTheNeedle");
expect(out).not.toContain("Error:");
});
it("treats a dash-leading query as a search term, not a cs flag", async () => {
process.env.DISPATCH_CS_BIN = liveCsBin as string;
// A literal token beginning with '-' must not be parsed as a flag.
await writeFileP(join(workDir, "dash.ts"), "const dashToken = 1;\n");
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "-dashToken" });
// Whether or not cs ranks a hit, it must NOT error out on flag parsing.
expect(out).not.toContain("unknown shorthand flag");
expect(out).not.toMatch(/^Error: cs exited/);
});
it("renders snippet lines for prose (markdown) matches", async () => {
process.env.DISPATCH_CS_BIN = liveCsBin as string;
await writeFileP(
join(workDir, "doc.md"),
"# Title\n\nThis paragraph mentions widgetronics in prose.\n",
);
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "widgetronics" });
expect(out).toContain("doc.md");
// The matching prose text must be shown, not just a bare header.
expect(out).toContain("widgetronics");
expect(out).not.toContain("no snippet available");
});
it("widens the snippet window when context is given", async () => {
process.env.DISPATCH_CS_BIN = liveCsBin as string;
const body = Array.from({ length: 21 }, (_, i) => `line ${i + 1}`);
body[10] = "const findContextTarget = 1;";
await writeFileP(join(workDir, "ctx.ts"), `${body.join("\n")}\n`);
const tool = createSearchCodeTool(workDir);
const countSnippetLines = (s: string) =>
s.split("\n").filter((l) => /^\s+>?\s*\d+:/.test(l)).length;
const narrow = await tool.execute({
query: "findContextTarget",
context: 0,
result_limit: 1,
});
const wide = await tool.execute({
query: "findContextTarget",
context: 6,
result_limit: 1,
});
expect(countSnippetLines(wide)).toBeGreaterThan(countSnippetLines(narrow));
});
it("returns 'No matches found.' for a query with no hits", async () => {
process.env.DISPATCH_CS_BIN = liveCsBin as string;
await writeFileP(join(workDir, "alpha.ts"), "export const a = 1;\n");
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "zzz_nonexistent_token_qqq" });
expect(out).toBe("No matches found.");
});
it("tags .luau files as Luau", async () => {
process.env.DISPATCH_CS_BIN = liveCsBin as string;
await writeFileP(join(workDir, "mod.luau"), "function Mod.doThing()\n\treturn 1\nend\n");
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "doThing" });
expect(out).toContain("mod.luau");
expect(out).toContain("[Luau]");
});
});
// ── Luau declaration detection: needs a cs built with the Luau patch
// (docker/cs/luau-declarations.patch). Skipped on an unpatched/older cs. ──
const luauCsBin = findLuauCapableCs(liveCsBin);
describe.runIf(luauCsBin)("live cs binary (Luau declaration patch)", () => {
// A small Luau module exercising every declaration form the patch adds.
const LUAU_MODULE = [
"local Mod = {}",
"",
"export type StuntResult = {",
"\tscore: number,",
"}",
"",
"type LaunchConfig = StuntResult",
"",
"function Mod.getDefaults(): LaunchConfig",
"\treturn { score = 0 }",
"end",
"",
"local function helperThing(x: number): number",
"\treturn x + 1",
"end",
"",
"Mod.live = Mod.getDefaults()",
"local used = helperThing(1)",
"",
].join("\n");
beforeEach(async () => {
process.env.DISPATCH_CS_BIN = luauCsBin as string;
await writeFileP(join(workDir, "Mod.luau"), LUAU_MODULE);
});
it("detects `function Mod.x` declarations in .luau files", async () => {
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "getDefaults", only: "declarations" });
expect(out).toContain("Mod.luau");
expect(out).toContain("function Mod.getDefaults");
expect(out).not.toContain("No matches found");
});
it("detects `local function` declarations in .luau files", async () => {
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "helperThing", only: "declarations" });
expect(out).toContain("Mod.luau");
expect(out).toContain("local function helperThing");
});
it("detects `type` / `export type` declarations in .luau files", async () => {
const tool = createSearchCodeTool(workDir);
const exportType = await tool.execute({ query: "StuntResult", only: "declarations" });
expect(exportType).toContain("export type StuntResult");
const aliasType = await tool.execute({ query: "LaunchConfig", only: "declarations" });
expect(aliasType).toContain("type LaunchConfig");
});
it("excludes declaration lines when only=usages", async () => {
const tool = createSearchCodeTool(workDir);
const out = await tool.execute({ query: "getDefaults", only: "usages" });
// The call site is a usage; the `function Mod.getDefaults` definition is not.
expect(out).toContain("Mod.live = Mod.getDefaults()");
expect(out).not.toContain("function Mod.getDefaults");
});
});
// ── Fuzzy mid-word matching: needs a cs built with the fuzzy patch
// (docker/cs/fuzzy-distance.patch). Skipped on an unpatched/older cs. ──
const fuzzyCsBin = findFuzzyCapableCs(liveCsBin);
describe.runIf(fuzzyCsBin)("live cs binary (fuzzy edit-distance patch)", () => {
beforeEach(() => {
process.env.DISPATCH_CS_BIN = fuzzyCsBin as string;
});
it("matches a mid-word deletion within distance 1", async () => {
await writeFileP(
join(workDir, "phys.ts"),
"export function computeSlipAngle() {\n\treturn 0;\n}\n",
);
const tool = createSearchCodeTool(workDir);
// "computSlipAngle" drops the 'e' mid-word — edit distance 1.
const out = await tool.execute({ query: "computSlipAngle~1" });
expect(out).toContain("phys.ts");
expect(out).toContain("computeSlipAngle");
expect(out).not.toBe("No matches found.");
});
it("matches a mid-word insertion within distance 1", async () => {
await writeFileP(join(workDir, "tire.ts"), "const tireFriction = 1;\n");
const tool = createSearchCodeTool(workDir);
// "tireFricction" has an extra 'c' — edit distance 1.
const out = await tool.execute({ query: "tireFricction~1" });
expect(out).toContain("tire.ts");
expect(out).toContain("tireFriction");
});
});
});
/**
* Locate a usable `cs` binary for live tests. Honors DISPATCH_CS_TEST_BIN, then
* a `cs` on PATH. Returns null when none is runnable, so the live suite is
* skipped rather than failing in environments without cs.
*/
function findRealCs(): string | null {
const candidates = [process.env.DISPATCH_CS_TEST_BIN, "cs"].filter(Boolean) as string[];
for (const bin of candidates) {
try {
const res = spawnSync(bin, ["--version"], { stdio: "ignore" });
if (res.status === 0) return bin;
} catch {
// try next
}
}
return null;
}
/**
* Probe a `cs` binary against a throwaway corpus and return its trimmed stdout
* (or "" on any failure). Used by the capability gates below so patch-dependent
* live tests run only on a cs that actually has the patch — and skip (not fail)
* on an unpatched/older binary.
*/
function probeCs(bin: string, files: Record<string, string>, args: string[]): string {
let dir: string | undefined;
try {
dir = mkdtempSync(join(tmpdir(), "dispatch-cs-probe-"));
for (const [name, body] of Object.entries(files)) {
writeFileSync(join(dir, name), body);
}
const res = spawnSync(bin, ["-f", "json", "--dir", dir, ...args], {
encoding: "utf8",
});
if (res.status !== 0 || !res.stdout) return "";
return res.stdout.trim();
} catch {
return "";
} finally {
if (dir) rmSync(dir, { recursive: true, force: true });
}
}
/**
* Return the cs binary only if it recognises Luau declarations (i.e. was built
* with docker/cs/luau-declarations.patch): a `--only-declarations` search for a
* top-level `function` in a .luau file yields a result. Otherwise null → skip.
*/
function findLuauCapableCs(bin: string | null): string | null {
if (!bin) return null;
const out = probeCs(bin, { "probe.luau": "function Probe.thing()\n\treturn 1\nend\n" }, [
"--only-declarations",
"--",
"thing",
]);
return out !== "" && out !== "null" ? bin : null;
}
/**
* Return the cs binary only if its fuzzy matcher honours mid-word edits (i.e.
* was built with docker/cs/fuzzy-distance.patch): a distance-1 deletion matches.
* Otherwise null → skip.
*/
function findFuzzyCapableCs(bin: string | null): string | null {
if (!bin) return null;
const out = probeCs(bin, { "probe.txt": "const x = computeSlipAngle;\n" }, [
"--",
"computSlipAngle~1",
]);
return out !== "" && out !== "null" ? bin : null;
}
|