Files
opencode-plugin-compaction-…/test/index.test.ts
T
bendtherules e8c76ea922 feat: auto-register plugin-compaction-init slash command
Adds a custom OpenCode command that scaffolds the compaction memory
file with ## Keep and ## Discard sections of classification rules.
The configured memoryFile path is substituted into both the command
description and the prompt template, and any arguments passed after
the command name are appended as user instructions.
2026-08-11 12:48:11 +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");
});
});