Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.3.1 - Unreleased

- Improved OpenCode malformed JSON diagnostics with output length, event kinds, and a bounded preview, thanks @rohitjavvadi.
- Fixed Express route mapping for aliased Router imports that follow block comment banners, thanks @rohitjavvadi.
- Fixed Bun package-manager detection to recognize the text `bun.lock` lockfile, thanks @austinm911.

Expand Down
45 changes: 45 additions & 0 deletions src/provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it } from "vitest";
import { ClawpatchError } from "./errors.js";
import { __testing, extractJson, providerByName } from "./provider.js";
import { safeProviderPreview } from "./provider-json.js";
import { revalidateOutputSchema, reviewOutputSchema } from "./types.js";

// eslint-disable-next-line no-underscore-dangle
Expand Down Expand Up @@ -55,6 +56,10 @@ function expectMalformed(fn: () => unknown, message: RegExp): void {
throw new Error("expected malformed-output");
}

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}

describe("extractJson", () => {
it("parses strict JSON directly", () => {
const input = '{"findings":[],"inspected":{"files":[],"symbols":[],"notes":[]}}';
Expand Down Expand Up @@ -484,6 +489,46 @@ describe("extractOpencodeJson", () => {
expectMalformed(() => extractOpencodeJson(stdout), /no extractable text.*step_finish/u);
});

it("treats whitespace-only opencode text as no extractable text", () => {
const stdout = [
JSON.stringify({ type: "text", part: { text: " \n\t " } }),
JSON.stringify({ type: "step_finish", part: { reason: "stop" } }),
].join("\n");

expectMalformed(() => extractOpencodeJson(stdout), /no extractable text.*text, step_finish/u);
});

it("throws malformed-output with a preview when opencode text is unparsable", () => {
const stdout = [
JSON.stringify({
type: "text",
part: { text: '{"findings": [' },
}),
JSON.stringify({ type: "step_finish", part: { reason: "stop" } }),
].join("\n");

expectMalformed(
() => extractOpencodeJson(stdout),
/unparsable JSON.*text chars=14.*observed event kinds: \[text, step_finish\].*output preview: \{"findings": \[/u,
);
});

it("bounds the opencode unparsable text preview", () => {
const text = `{"findings":["${"x".repeat(300)}`;
const stdout = JSON.stringify({
type: "text",
part: { text },
});
const preview = safeProviderPreview(text);

expect(preview.length).toBe(200);

expectMalformed(
() => extractOpencodeJson(stdout),
new RegExp(`output preview: ${escapeRegExp(preview)}\\)`, "u"),
);
});

it("throws provider-failure for opencode error events", () => {
const stdout = JSON.stringify({
type: "error",
Expand Down
8 changes: 7 additions & 1 deletion src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,13 @@ export function extractOpencodeJson(stdout: string): unknown {
}
const parsed = extractJson(combined);
if (parsed === null) {
throw new ClawpatchError("opencode provider produced unparsable JSON", 8, "malformed-output");
throw new ClawpatchError(
`opencode provider produced unparsable JSON ` +
`(text chars=${combined.length}, observed event kinds: ` +
`[${[...observedKinds].join(", ")}], output preview: ${safeProviderPreview(combined)})`,
8,
"malformed-output",
);
}
return parsed;
}
Expand Down