summaryrefslogtreecommitdiffhomepage
path: root/packages/core/tests
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 17:53:46 +0900
committerAdam Malczewski <[email protected]>2026-06-02 17:53:46 +0900
commit09914c6ba15214d5ec05c106d5d11fd14a86f532 (patch)
tree11f265a8d9e223f0b4b90ffadefc1ba0791569c1 /packages/core/tests
parent8d70db66d3f0046cdef5fbce2ce5a86eab0959ef (diff)
downloaddispatch-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.ts48
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 {