diff --git a/index.test.ts b/index.test.ts index 7296aa0..c37a166 100644 --- a/index.test.ts +++ b/index.test.ts @@ -9,7 +9,7 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { CompactClient } from "@morphllm/morphsdk"; +import { CompactClient, WarpGrepClient } from "@morphllm/morphsdk"; // These are internal to the plugin but duplicated here for testing. const EXISTING_CODE_MARKER = "// ... existing code ..."; @@ -991,3 +991,69 @@ describe("ToolContext path resolution", () => { } }); }); + +describe("formatWarpGrepResult edge cases", () => { + async function executeSearch(fakeResult: unknown): Promise { + const original = WarpGrepClient.prototype.execute; + WarpGrepClient.prototype.execute = function* () { + return fakeResult; + } as any; + + try { + const { default: MorphPlugin } = await importPluginWithEnv({ + MORPH_API_KEY: "sk-test-key", + }); + const hooks = await MorphPlugin(makePluginInput("/tmp/morph-warpgrep-test")); + return (await hooks.tool.warpgrep_codebase_search.execute( + { search_term: "auth flow" }, + makeToolContext("/tmp/morph-warpgrep-test"), + )) as string; + } finally { + WarpGrepClient.prototype.execute = original; + } + } + + test("contexts with implausible file paths are rejected; valid paths from every OS render normally", async () => { + // Implausible: bare letters, empty strings, whitespace, no separators or extensions + for (const file of ["C", "", " ", "noextension"]) { + const result = await executeSearch({ + success: true, + contexts: [{ file, content: "", lines: "*" }], + }); + expect(result).toContain("malformed"); + expect(result).not.toContain(" }], + }); + expect(result).toContain("Relevant context found:"); + expect(result).toContain(` { + for (const error of [undefined, null, ""]) { + const result = await executeSearch({ success: false, error }); + expect(result).toMatch(/^Search failed:/); + expect(result).not.toContain("undefined"); + expect(result).not.toContain("null"); + } + + const result = await executeSearch({ success: false, error: "timeout after 60s" }); + expect(result).toBe("Search failed: timeout after 60s"); + }); +}); diff --git a/index.ts b/index.ts index fc3b5b5..0190aa7 100644 --- a/index.ts +++ b/index.ts @@ -327,22 +327,41 @@ function buildMorphSystemRoutingHint(): string | null { return lines.length > 1 ? lines.join("\n") : null; } +/** + * Minimal check for a plausible file path on any OS: + * - at least one path separator (/ or \) OR a dot-extension + * - not empty / whitespace-only + */ +const PLAUSIBLE_PATH_RE = /[/\\]|\.[\w]+$/; + +function isValidContext(ctx: { file: string; content: string }): boolean { + return Boolean(ctx.file) && PLAUSIBLE_PATH_RE.test(ctx.file) && ctx.content.length > 0; +} + /** * Format WarpGrep results for tool output */ function formatWarpGrepResult(result: WarpGrepResult): string { if (!result.success) { - return `Search failed: ${result.error}`; + return `Search failed: ${result.error || "search returned no error details."}`; } if (!result.contexts || result.contexts.length === 0) { return "No relevant code found. Try rephrasing your search term."; } + const valid = result.contexts.filter(isValidContext); + + if (valid.length === 0) { + const sample = result.contexts.slice(0, 3).map((c) => c.file); + return `Search returned malformed file contexts (file values: ${JSON.stringify(sample)}). +Fallback: use \`grep\` + \`read\` for local code search.`; + } + const parts: string[] = []; parts.push("Relevant context found:"); - for (const ctx of result.contexts) { + for (const ctx of valid) { const rangeStr = !ctx.lines || ctx.lines === "*" ? "*" @@ -352,7 +371,7 @@ function formatWarpGrepResult(result: WarpGrepResult): string { parts.push("\nFile contents:\n"); - for (const ctx of result.contexts) { + for (const ctx of valid) { const rangeStr = !ctx.lines || ctx.lines === "*" ? "" diff --git a/package.json b/package.json index 021b426..de61246 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "ci": "bun test && bun run build && bun run typecheck" }, "dependencies": { - "@morphllm/morphsdk": "^0.2.148", + "@morphllm/morphsdk": "0.2.160", "@opencode-ai/plugin": "latest", "@opencode-ai/sdk": "^1.2.22" },