import { afterEach, describe, expect, test } from "bun:test";
import { createTestEnvironment } from "../../tests/helpers/environment";
import PlannotatorPlugin from "./index";
/**
* OpenCode 1 slash-command interception.
*
* The V1 plugin clears `output.parts` IN PLACE before anything reaches the
* model. Now that the shared markdown stubs carry real instructions ("run the
* plannotator CLI and relay stdout", for OpenCode 2 hosts on the stale
* channels), a regression here would leak those instructions to the OpenCode 1
* model and re-open the #713 class: OpenCode resolves prompt parts over
* "
" and auto-attaches any file path it finds, which on a
* large file blows the context before the annotation UI even opens.
*
* Interception lives on the always-built plugin object; `shouldRegisterSubmitPlan`
* only gates `plugin.tool`, so `workflow: "manual"` must intercept too.
*/
const envKeys = ["PLANNOTATOR_BIN", "PLANNOTATOR_DATA_DIR"] as const;
const environment = createTestEnvironment(envKeys, "plannotator-oc1-intercept-");
afterEach(() => environment.restore());
const COMMANDS = ["plannotator-review", "plannotator-annotate", "plannotator-last"] as const;
function makeClient() {
return {
app: {
log: async () => ({}),
agents: async () => ({ data: [] }),
},
config: { get: async () => ({ data: {} }) },
session: {
messages: async () => ({ data: [] }),
prompt: async () => ({}),
},
};
}
async function interceptionHandler(options: Record) {
const plugin = await PlannotatorPlugin(
{ client: makeClient(), directory: "/project" } as never,
// "cli" keeps the embedded server out of the test; the CLI spawn then fails
// fast against the bogus PLANNOTATOR_BIN below and is swallowed by
// handleCliCommand's own catch.
{ runtime: "cli", ...options } as never,
);
return (plugin as Record)["command.execute.before"] as (
input: Record,
output: { parts: unknown[] },
) => Promise;
}
describe("OpenCode 1 command interception", () => {
for (const workflow of ["plan-agent", "manual"] as const) {
for (const command of COMMANDS) {
test(`${workflow}: /${command} empties output.parts before the model sees it`, async () => {
environment.reset();
process.env.PLANNOTATOR_BIN = "/nonexistent/plannotator-interception-test";
process.env.PLANNOTATOR_DATA_DIR = environment.makeTempDir();
const handler = await interceptionHandler({ workflow });
const parts = [{ type: "text", text: "run the plannotator CLI and relay stdout" }];
const output = { parts };
await handler(
{ command, sessionID: "session-1", arguments: "" },
output,
);
expect(parts.length).toBe(0);
// Mutated in place, never reassigned: the caller holds this exact array
// and ignores anything assigned to output.parts.
expect(output.parts).toBe(parts);
});
}
}
test("an unrelated command keeps its parts untouched", async () => {
environment.reset();
process.env.PLANNOTATOR_BIN = "/nonexistent/plannotator-interception-test";
process.env.PLANNOTATOR_DATA_DIR = environment.makeTempDir();
const handler = await interceptionHandler({ workflow: "plan-agent" });
const output = { parts: [{ type: "text", text: "someone else's command" }] };
await handler({ command: "other-command", sessionID: "session-1", arguments: "" }, output);
expect(output.parts.length).toBe(1);
});
});