Files
fusion/packages/engine/src/cli-agent/__tests__/hook-scripts.test.ts
2026-06-04 23:39:07 -07:00

122 lines
4.4 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, statSync, existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { rm } from "node:fs/promises";
import {
writeSessionHookScripts,
cleanupSessionHookDir,
buildHookScriptContent,
buildNotifyShimContent,
HOOK_SCRIPT_NAMES,
} from "../hook-scripts.js";
describe("hook-scripts", () => {
let tmpDir: string;
let dir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), "fusion-hook-scripts-"));
dir = join(tmpDir, "session-config");
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
const opts = {
sessionId: "sess-123",
token: "abc123def456token",
endpointUrl: "http://127.0.0.1:4040/api/cli-agent/hooks",
};
describe("buildHookScriptContent", () => {
it("POSTs to the endpoint URL with the token + session headers", () => {
const script = buildHookScriptContent(opts);
expect(script).toContain("http://127.0.0.1:4040/api/cli-agent/hooks");
expect(script).toContain("X-Fusion-Cli-Session-Token: $TOKEN");
expect(script).toContain("X-Fusion-Cli-Session-Id: $SESSION_ID");
expect(script).toContain("abc123def456token");
expect(script).toContain("sess-123");
});
it("uses curl with short timeouts and always exits 0", () => {
const script = buildHookScriptContent(opts);
expect(script).toContain("curl");
expect(script).toContain("--connect-timeout");
expect(script).toContain("--max-time");
// Failure tolerance: the curl line is `|| true` and the script ends `exit 0`.
expect(script).toContain("|| true");
expect(script.trimEnd().endsWith("exit 0")).toBe(true);
});
it("never sets an Origin header (CSRF-safe)", () => {
const script = buildHookScriptContent(opts);
expect(script.toLowerCase()).not.toContain("origin:");
});
it("starts with a sh shebang", () => {
expect(buildHookScriptContent(opts).startsWith("#!/bin/sh")).toBe(true);
});
it("shell-escapes a token containing a single quote", () => {
const script = buildHookScriptContent({ ...opts, token: "to'ken" });
// The single quote is escaped via the '\'' idiom — no raw unbalanced quote.
expect(script).toContain(`'\\''`);
});
});
describe("buildNotifyShimContent", () => {
it("forwards argv[1] (else stdin) and exits 0", () => {
const script = buildNotifyShimContent(opts);
expect(script.startsWith("#!/bin/sh")).toBe(true);
expect(script).toContain('"$1"');
expect(script).toContain("event=notify");
expect(script).toContain("X-Fusion-Cli-Session-Token: $TOKEN");
expect(script.trimEnd().endsWith("exit 0")).toBe(true);
});
});
describe("writeSessionHookScripts", () => {
it("writes both scripts into the dir, marked executable", async () => {
const result = await writeSessionHookScripts({ ...opts, dir });
expect(result.hookScriptPath).toBe(join(dir, HOOK_SCRIPT_NAMES.hook));
expect(result.notifyScriptPath).toBe(join(dir, HOOK_SCRIPT_NAMES.notify));
expect(existsSync(result.hookScriptPath)).toBe(true);
expect(existsSync(result.notifyScriptPath)).toBe(true);
// Owner-executable bit set on both files.
const hookMode = statSync(result.hookScriptPath).mode;
const notifyMode = statSync(result.notifyScriptPath).mode;
expect(hookMode & 0o100).toBe(0o100);
expect(notifyMode & 0o100).toBe(0o100);
const hookContent = await readFile(result.hookScriptPath, "utf8");
expect(hookContent).toContain(opts.endpointUrl);
expect(hookContent).toContain(opts.token);
});
it("creates the dir if it does not exist", async () => {
expect(existsSync(dir)).toBe(false);
await writeSessionHookScripts({ ...opts, dir });
expect(existsSync(dir)).toBe(true);
});
});
describe("cleanupSessionHookDir", () => {
it("removes the dir and its contents", async () => {
await writeSessionHookScripts({ ...opts, dir });
expect(existsSync(dir)).toBe(true);
await cleanupSessionHookDir(dir);
expect(existsSync(dir)).toBe(false);
});
it("is a no-op for a missing dir (never throws)", async () => {
await expect(cleanupSessionHookDir(join(tmpDir, "does-not-exist"))).resolves.toBeUndefined();
});
});
});