mirror of
https://github.com/bendtherules/opencode-plugin-compaction-prompt.git
synced 2026-08-18 21:52:46 +00:00
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.
267 lines
8.4 KiB
TypeScript
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");
|
|
});
|
|
});
|