Files
rohitg00--agentmemory/test/context-injection.test.ts
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Waiting to run
CI / test (20, ubuntu-latest) (push) Waiting to run
CI / test (22, macos-latest) (push) Waiting to run
CI / test (22, ubuntu-latest) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

129 lines
4.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { spawn } from "node:child_process";
import { join } from "node:path";
const HOOKS_DIR = join(import.meta.dirname, "..", "plugin", "scripts");
// Spawns a compiled plugin hook as a subprocess, feeds it JSON on stdin,
// and returns { stdout, stderr, exitCode, tookMs }. The test is about
// making sure the hook writes NOTHING to stdout when context injection is
// disabled — which is what Claude Code reads to decide whether to prepend
// memory context to the next tool turn.
function runHook(
scriptName: string,
stdin: string,
env: Record<string, string>,
): Promise<{
stdout: string;
stderr: string;
exitCode: number | null;
tookMs: number;
}> {
return new Promise((resolve, reject) => {
const start = Date.now();
const child = spawn(
process.execPath,
[join(HOOKS_DIR, scriptName)],
{
env: {
// Start from a clean slate — don't leak test-runner env into
// the hook. Only pass PATH and anything explicitly set by the
// test case.
PATH: process.env["PATH"] ?? "",
...env,
},
stdio: ["pipe", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", reject);
child.on("close", (exitCode) => {
resolve({ stdout, stderr, exitCode, tookMs: Date.now() - start });
});
child.stdin.write(stdin);
child.stdin.end();
});
}
describe("pre-tool-use hook — context injection gate (#143)", () => {
it("writes nothing to stdout when AGENTMEMORY_INJECT_CONTEXT is unset (default)", async () => {
const payload = JSON.stringify({
session_id: "ses_test",
tool_name: "Read",
tool_input: { file_path: "src/foo.ts" },
});
// No AGENTMEMORY_* env vars at all — simulates a fresh Claude Pro
// install with no ~/.agentmemory/.env overrides.
const result = await runHook("pre-tool-use.mjs", payload, {});
expect(result.stdout).toBe("");
expect(result.exitCode).toBe(0);
});
it("writes nothing to stdout when AGENTMEMORY_INJECT_CONTEXT=false explicitly", async () => {
const payload = JSON.stringify({
session_id: "ses_test",
tool_name: "Edit",
tool_input: { file_path: "src/foo.ts", old_string: "a", new_string: "b" },
});
const result = await runHook("pre-tool-use.mjs", payload, {
AGENTMEMORY_INJECT_CONTEXT: "false",
});
expect(result.stdout).toBe("");
expect(result.exitCode).toBe(0);
});
it("exits fast when disabled (no stdin consumption, no network fetch)", async () => {
// The disabled path must not open stdin or reach for fetch — it
// should return immediately. A 250ms budget is generous enough to
// account for Node startup on CI while still catching any accidental
// fetch round-trip or stdin buffering.
const result = await runHook("pre-tool-use.mjs", "", {});
expect(result.tookMs).toBeLessThan(1000);
expect(result.stdout).toBe("");
});
it("when AGENTMEMORY_INJECT_CONTEXT=true, hook still runs but safely errors on unreachable backend", async () => {
// Opt-in path. We point at a port that's guaranteed closed so the
// fetch fails fast; the hook must still exit cleanly (the whole
// point of the try/catch is not to break Claude Code) and must not
// echo anything to stdout when the fetch fails.
const payload = JSON.stringify({
session_id: "ses_test",
tool_name: "Read",
tool_input: { file_path: "src/foo.ts" },
});
const result = await runHook("pre-tool-use.mjs", payload, {
AGENTMEMORY_INJECT_CONTEXT: "true",
AGENTMEMORY_URL: "http://127.0.0.1:1",
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe("");
});
});
describe("session-start hook — context injection gate (#143)", () => {
it("registers the session but writes nothing to stdout when AGENTMEMORY_INJECT_CONTEXT is unset", async () => {
// Session registration POST will fail against the unreachable URL,
// but the hook's try/catch must swallow that cleanly — Claude Code
// must never see an error at session start.
const payload = JSON.stringify({
session_id: "ses_test",
cwd: "/tmp/fake-project",
});
const result = await runHook("session-start.mjs", payload, {
AGENTMEMORY_URL: "http://127.0.0.1:1",
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe("");
});
});