Skip to content

Commit 4e4555d

Browse files
committed
Detect malformed WarpGrep Windows results and harden error formatting
Fixes #7 — On Windows, upstream SDK path parsing truncates drive-letter paths (e.g. C:/Users/.../auth.ts → file: 'C'), producing confusing output. This adds detection for malformed contexts and surfaces an actionable error with a workaround suggestion. Also fixes the 'Search failed: undefined' output when the SDK omits an error string. https://claude.ai/code/session_01G5qa1GyHGNwWu2ko6uZ53N
1 parent ac1efae commit 4e4555d

2 files changed

Lines changed: 139 additions & 2 deletions

File tree

index.test.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "node:fs";
1010
import { tmpdir } from "node:os";
1111
import { join } from "node:path";
12-
import { CompactClient } from "@morphllm/morphsdk";
12+
import { CompactClient, WarpGrepClient } from "@morphllm/morphsdk";
1313

1414
// These are internal to the plugin but duplicated here for testing.
1515
const EXISTING_CODE_MARKER = "// ... existing code ...";
@@ -991,3 +991,110 @@ describe("ToolContext path resolution", () => {
991991
}
992992
});
993993
});
994+
995+
// ---------------------------------------------------------------------------
996+
// WarpGrep malformed Windows result detection (Issue #7)
997+
// ---------------------------------------------------------------------------
998+
999+
describe("warpgrep_codebase_search malformed Windows results", () => {
1000+
/**
1001+
* Helper: patch WarpGrepClient.prototype.execute to return a fake result,
1002+
* import the plugin fresh, call the tool, then restore the original.
1003+
*/
1004+
async function executeSearchWithMockedResult(fakeResult: unknown): Promise<string> {
1005+
const original = WarpGrepClient.prototype.execute;
1006+
// The plugin calls warpGrep!.execute() which is an async generator.
1007+
// We mock it to yield nothing and return the fakeResult.
1008+
WarpGrepClient.prototype.execute = function* () {
1009+
return fakeResult;
1010+
} as any;
1011+
1012+
try {
1013+
const { default: MorphPlugin } = await importPluginWithEnv({
1014+
MORPH_API_KEY: "sk-test-key",
1015+
});
1016+
const hooks = await MorphPlugin(makePluginInput("/tmp/morph-warpgrep-test"));
1017+
const result = await hooks.tool.warpgrep_codebase_search.execute(
1018+
{ search_term: "test query" },
1019+
makeToolContext("/tmp/morph-warpgrep-test"),
1020+
);
1021+
return result as string;
1022+
} finally {
1023+
WarpGrepClient.prototype.execute = original;
1024+
}
1025+
}
1026+
1027+
test("malformed result with file:'C' returns actionable Windows error", async () => {
1028+
const result = await executeSearchWithMockedResult({
1029+
success: true,
1030+
contexts: [
1031+
{ file: "C", content: "", lines: "*" },
1032+
],
1033+
});
1034+
1035+
expect(result).toContain("malformed file contexts on Windows");
1036+
expect(result).toContain("`C`");
1037+
expect(result).toContain("upstream SDK");
1038+
expect(result).toContain("grep");
1039+
expect(result).toContain("read");
1040+
});
1041+
1042+
test("multiple malformed contexts still triggers error", async () => {
1043+
const result = await executeSearchWithMockedResult({
1044+
success: true,
1045+
contexts: [
1046+
{ file: "C", content: "", lines: "*" },
1047+
{ file: "D", content: "", lines: "*" },
1048+
],
1049+
});
1050+
1051+
expect(result).toContain("malformed file contexts on Windows");
1052+
});
1053+
1054+
test("missing SDK error string does not produce 'Search failed: undefined'", async () => {
1055+
const result = await executeSearchWithMockedResult({
1056+
success: false,
1057+
error: undefined,
1058+
});
1059+
1060+
expect(result).not.toContain("undefined");
1061+
expect(result).toContain("Search failed");
1062+
expect(result).toContain("no error details");
1063+
});
1064+
1065+
test("explicit SDK error string is preserved", async () => {
1066+
const result = await executeSearchWithMockedResult({
1067+
success: false,
1068+
error: "timeout after 60s",
1069+
});
1070+
1071+
expect(result).toContain("Search failed: timeout after 60s");
1072+
});
1073+
1074+
test("valid search results still format normally", async () => {
1075+
const result = await executeSearchWithMockedResult({
1076+
success: true,
1077+
contexts: [
1078+
{
1079+
file: "src/auth.ts",
1080+
content: "export function login() { return true; }",
1081+
lines: [[1, 5]] as Array<[number, number]>,
1082+
},
1083+
],
1084+
});
1085+
1086+
expect(result).toContain("Relevant context found:");
1087+
expect(result).toContain("src/auth.ts");
1088+
expect(result).toContain("export function login()");
1089+
expect(result).not.toContain("malformed");
1090+
});
1091+
1092+
test("empty contexts returns 'no relevant code' message", async () => {
1093+
const result = await executeSearchWithMockedResult({
1094+
success: true,
1095+
contexts: [],
1096+
});
1097+
1098+
expect(result).toContain("No relevant code found");
1099+
});
1100+
});

index.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,18 +327,48 @@ function buildMorphSystemRoutingHint(): string | null {
327327
return lines.length > 1 ? lines.join("\n") : null;
328328
}
329329

330+
/**
331+
* Check if a single WarpGrep context looks like a truncated Windows drive-letter path.
332+
* e.g. { file: 'C', content: '', lines: '*' }
333+
*/
334+
function isMalformedContext(ctx: { file: string; content: string; lines?: string | unknown }): boolean {
335+
return /^[A-Za-z]$/.test(ctx.file) && !ctx.content && ctx.lines === "*";
336+
}
337+
338+
/**
339+
* Check if a WarpGrep result contains malformed Windows-style contexts.
340+
* Returns true when all (or the majority of) contexts are malformed.
341+
*/
342+
function hasMalformedContexts(contexts: { file: string; content: string; lines?: string | unknown }[]): boolean {
343+
if (contexts.length === 0) return false;
344+
const malformedCount = contexts.filter(isMalformedContext).length;
345+
return malformedCount > 0 && malformedCount >= contexts.length / 2;
346+
}
347+
330348
/**
331349
* Format WarpGrep results for tool output
332350
*/
333351
function formatWarpGrepResult(result: WarpGrepResult): string {
334352
if (!result.success) {
335-
return `Search failed: ${result.error}`;
353+
return `Search failed: ${result.error || "search returned no error details."}`;
336354
}
337355

338356
if (!result.contexts || result.contexts.length === 0) {
339357
return "No relevant code found. Try rephrasing your search term.";
340358
}
341359

360+
if (hasMalformedContexts(result.contexts)) {
361+
const malformedFiles = result.contexts
362+
.filter(isMalformedContext)
363+
.map((ctx) => ctx.file);
364+
console.error(
365+
`[morph-plugin] Malformed WarpGrep contexts detected: ${malformedFiles.length} context(s) with file values: ${JSON.stringify(malformedFiles)}${result.summary ? ` | summary: ${result.summary}` : ""}`,
366+
);
367+
return `Search returned malformed file contexts on Windows (for example \`${malformedFiles[0]}\` instead of a full file path).
368+
This appears to be an upstream SDK Windows path parsing bug.
369+
Temporary workaround: use \`grep\` + \`read\` for local code search until the SDK fix lands.`;
370+
}
371+
342372
const parts: string[] = [];
343373
parts.push("Relevant context found:");
344374

0 commit comments

Comments
 (0)