diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 17:53:46 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 17:53:46 +0900 |
| commit | 09914c6ba15214d5ec05c106d5d11fd14a86f532 (patch) | |
| tree | 11f265a8d9e223f0b4b90ffadefc1ba0791569c1 /packages/core/tests | |
| parent | 8d70db66d3f0046cdef5fbce2ce5a86eab0959ef (diff) | |
| download | dispatch-09914c6ba15214d5ec05c106d5d11fd14a86f532.tar.gz dispatch-09914c6ba15214d5ec05c106d5d11fd14a86f532.zip | |
harden(search_code): defensive arg coercion, per-line truncation, rerun-safe pkg
Address findings from a second independent (Gemini) review covering the tool
and the packaging:
- Robustness (was: crash): non-string params from a model hallucination (e.g.
include_ext: ["ts","go"]) threw 'x.trim is not a function' and killed the
tool call. Add an asString() coercion for all string params (query, path,
include_ext, exclude_pattern, only); non-strings now no-op or return the
graceful 'query is required' error.
- Output bound: cap each rendered snippet line at 500 chars (MAX_LINE_CHARS,
mirrors read-file.ts) so a matched minified/generated line can't bloat the
payload. (Total output is already bounded by the universal truncator.)
- packaging/PKGBUILD: make the cs clone rerun-safe (rm -rf before clone) so
makepkg -e / repeat runs don't abort on 'destination path already exists';
add conflicts=('cs') to the code-search package for a clean pacman error vs.
the unrelated AUR 'cs' that also owns /usr/bin/cs (no provides — different
program).
Not changed (verified): path containment, the -- flag-injection guard, and the
deterministic pinned Docker build were all confirmed solid by the review.
Tests: +2 (wrong-type params don't crash; long-line truncation). Full suite
605 pass, biome + tsc green.
Diffstat (limited to 'packages/core/tests')
| -rw-r--r-- | packages/core/tests/tools/search-code.test.ts | 48 |
1 files changed, 48 insertions, 0 deletions
diff --git a/packages/core/tests/tools/search-code.test.ts b/packages/core/tests/tools/search-code.test.ts index d43158a..00eee4c 100644 --- a/packages/core/tests/tools/search-code.test.ts +++ b/packages/core/tests/tools/search-code.test.ts @@ -63,6 +63,28 @@ describe("search_code tool", () => { 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" }); @@ -181,6 +203,32 @@ describe("search_code tool", () => { } }); + 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 { |
