Files
opencode-plugin-compaction-…/test/index.test.ts
T
bendtherules 9f549acfde fix: correct $ARGUMENTS token in plugin-compaction-init template
OpenCode's argument token is $ARGUMENTS, not $ARGUMENTS$. With the
extra dollar sign, OpenCode did not substitute the token, so the LLM
ended up writing the literal $ARGUMENTS$ line into the memory file
when no arguments were passed.
2026-08-11 12:55:37 +05:30

267 lines
8.4 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
buildInitCommandTemplate,
CompactionPromptPlugin,
} from "../src/index.js";
const temporaryDirectories: string[] = [];
afterEach(async (): Promise<void> => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true })),
);
});
async function createContext(): Promise<{
worktree: string;
logs: Array<unknown>;
client: { app: { log: (input: unknown) => Promise<void> } };
}> {
const worktree = await mkdtemp(
path.join(os.tmpdir(), "opencode-compaction-"),
);
temporaryDirectories.push(worktree);
const logs: Array<unknown> = [];
return {
worktree,
logs,
client: {
app: {
log: async (input: unknown): Promise<void> => {
logs.push(input);
},
},
},
};
}
describe("CompactionPromptPlugin", () => {
test("appends instructions and memory by default", async () => {
const context = await createContext();
await mkdir(path.join(context.worktree, ".opencode"));
await writeFile(
path.join(context.worktree, ".opencode/compaction.md"),
"Keep the active hypothesis.",
);
const hooks = await CompactionPromptPlugin(context as never);
const output = { context: [] as string[] };
await hooks["experimental.session.compacting"]?.(
{ sessionID: "test" },
output,
);
expect(output.context[0]).toContain("Keep the active hypothesis.");
expect(output.context[0]).toContain(
"opencode-plugin-compaction-prompt: Custom compaction done.",
);
});
test("combines custom instructions with a configured memory file", async () => {
const context = await createContext();
await writeFile(
path.join(context.worktree, "project-memory.md"),
"Preserve the active hypothesis.",
);
const hooks = await CompactionPromptPlugin(context as never, {
memoryFile: "project-memory.md",
prompt: "Keep exact file paths.",
completionMarker: "Compaction complete.",
});
const output = { context: ["Existing context"] };
await hooks["experimental.session.compacting"]?.(
{ sessionID: "test" },
output,
);
expect(output.context).toHaveLength(2);
expect(output.context[0]).toBe("Existing context");
expect(output.context[1]).toContain("Keep exact file paths.");
expect(output.context[1]).toContain("Preserve the active hypothesis.");
expect(output.context[1]).toContain("Compaction complete.");
});
test("supports replacing the default prompt", async () => {
const context = await createContext();
const hooks = await CompactionPromptPlugin(context as never, {
mode: "replace",
prompt: "Preserve the current implementation plan.",
});
const output: { context: string[]; prompt?: string } = { context: [] };
await hooks["experimental.session.compacting"]?.(
{ sessionID: "test" },
output,
);
expect(output.prompt).toContain("User Compaction Instructions");
expect(output.prompt).toContain(
"Preserve the current implementation plan.",
);
expect(output.context).toHaveLength(0);
});
test("ignores a missing memory file", async () => {
const context = await createContext();
const hooks = await CompactionPromptPlugin(context as never);
const output = { context: [] as string[] };
await hooks["experimental.session.compacting"]?.(
{ sessionID: "test" },
output,
);
expect(context.logs).toHaveLength(0);
expect(output.context[0]).toContain(
"echo exactly: **opencode-plugin-compaction-prompt: No custom compaction applied.**",
);
});
test("adds a skipped marker when no instructions are provided", async () => {
const context = await createContext();
const hooks = await CompactionPromptPlugin(context as never);
const output: { context: string[]; prompt?: string } = {
context: ["Existing context"],
};
await hooks["experimental.session.compacting"]?.(
{ sessionID: "test" },
output,
);
expect(output.context[0]).toBe("Existing context");
expect(output.context[1]).toContain(
"echo exactly: **opencode-plugin-compaction-prompt: No custom compaction applied.**",
);
expect(output.prompt).toBeUndefined();
});
test("logs non-missing memory file errors without failing compaction", async () => {
const context = await createContext();
await writeFile(path.join(context.worktree, "memory"), "not a directory");
const hooks = await CompactionPromptPlugin(context as never, {
memoryFile: "memory/file.md",
});
const output = { context: [] as string[] };
await hooks["experimental.session.compacting"]?.(
{ sessionID: "test" },
output,
);
expect(context.logs).toHaveLength(1);
expect(output.context[0]).toContain(
"echo exactly: **opencode-plugin-compaction-prompt: No custom compaction applied.**",
);
});
});
describe("buildInitCommandTemplate", () => {
test("substitutes the configured memory file path verbatim", () => {
const template = buildInitCommandTemplate("docs/agent-memory.md");
expect(template).toContain("`docs/agent-memory.md`");
expect(template).not.toContain(".opencode/compaction.md");
});
test("contains the file skeleton and both section headings", () => {
const template = buildInitCommandTemplate(".opencode/compaction.md");
expect(template).toContain("```\nKeep all details related to");
expect(template).toContain("## Keep\n");
expect(template).toContain("## Discard\n");
});
test("leaves $ARGUMENTS unsubstituted for OpenCode to fill in", () => {
const template = buildInitCommandTemplate(".opencode/compaction.md");
expect(template.trimEnd().endsWith("$ARGUMENTS")).toBe(true);
});
});
describe("CompactionPromptPlugin config hook", () => {
test("registers the plugin-compaction-init command by default", async () => {
const context = await createContext();
const hooks = await CompactionPromptPlugin(context as never);
const cfg: {
command?: Record<string, { description: string; template: string }>;
} = {};
await hooks.config?.(cfg as never);
expect(cfg.command?.["plugin-compaction-init"]).toBeDefined();
expect(
cfg.command?.["plugin-compaction-init"].description.length,
).toBeGreaterThan(0);
expect(cfg.command?.["plugin-compaction-init"].template).toContain(
".opencode/compaction.md",
);
expect(cfg.command?.["plugin-compaction-init"].template).toContain(
"## Keep",
);
expect(cfg.command?.["plugin-compaction-init"].template).toContain(
"## Discard",
);
expect(cfg.command?.["plugin-compaction-init"].template).toContain(
"$ARGUMENTS",
);
expect(context.logs).toHaveLength(0);
});
test("uses the configured memoryFile in the registered template", async () => {
const context = await createContext();
const hooks = await CompactionPromptPlugin(context as never, {
memoryFile: "notes/agent.md",
});
const cfg: {
command?: Record<string, { description: string; template: string }>;
} = {};
await hooks.config?.(cfg as never);
expect(cfg.command?.["plugin-compaction-init"].template).toContain(
"`notes/agent.md`",
);
expect(cfg.command?.["plugin-compaction-init"].template).not.toContain(
".opencode/compaction.md",
);
});
test("skips registration and logs a warning when the command is already defined", async () => {
const context = await createContext();
const hooks = await CompactionPromptPlugin(context as never);
const userTemplate = "user-provided template body";
const cfg: {
command?: Record<string, { description: string; template: string }>;
} = {
command: {
"plugin-compaction-init": {
description: "user description",
template: userTemplate,
},
},
};
await hooks.config?.(cfg as never);
expect(cfg.command?.["plugin-compaction-init"].template).toBe(userTemplate);
expect(context.logs).toHaveLength(1);
expect(context.logs[0]).toMatchObject({
body: {
level: "warn",
service: "opencode-plugin-compaction-prompt",
},
});
expect(
(context.logs[0] as { body?: { message?: string } }).body?.message,
).toContain("plugin-compaction-init");
});
});