mirror of
https://github.com/juxt/allium.git
synced 2026-09-14 13:56:44 +08:00
135 lines
5.6 KiB
JavaScript
135 lines
5.6 KiB
JavaScript
import { execFileSync } from "child_process";
|
|
import { mkdtempSync, mkdirSync, readFileSync, existsSync, rmSync, symlinkSync, writeFileSync } from "fs";
|
|
import path from "path";
|
|
import { tmpdir } from "os";
|
|
|
|
const hook = new URL("./loop-trace.mjs", import.meta.url).pathname;
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(name, cond) {
|
|
if (cond) { console.log(` pass: ${name}`); passed++; }
|
|
else { console.log(` FAIL: ${name}`); failed++; }
|
|
}
|
|
|
|
// Run the hook once (one event) with a synthetic payload and a project root.
|
|
function runHook(event, input, projectRoot) {
|
|
try {
|
|
execFileSync("node", [hook, event], {
|
|
input: JSON.stringify(input),
|
|
encoding: "utf-8",
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
env: { ...process.env, CLAUDE_PROJECT_ROOT: projectRoot },
|
|
});
|
|
} catch {
|
|
// the hook always exits 0; ignore
|
|
}
|
|
}
|
|
|
|
function newProject({ withLoopDir }) {
|
|
const dir = mkdtempSync(path.join(tmpdir(), "allium-timing-"));
|
|
if (withLoopDir) mkdirSync(path.join(dir, ".allium-loop"));
|
|
return dir;
|
|
}
|
|
function timings(dir) {
|
|
const p = path.join(dir, ".allium-loop", "timings.jsonl");
|
|
if (!existsSync(p)) return [];
|
|
return readFileSync(p, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
}
|
|
|
|
console.log("\n── loop-trace hook ──\n");
|
|
|
|
// 1. pre + post with a correlation id → one timing line, right agent, numeric duration.
|
|
{
|
|
const dir = newProject({ withLoopDir: true });
|
|
const payload = { tool_use_id: "abc", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
|
|
runHook("pre", payload, dir);
|
|
runHook("post", payload, dir);
|
|
const t = timings(dir);
|
|
assert("records one timing for a paired pre/post", t.length === 1);
|
|
assert("labels the timing with the subagent type", t[0]?.agent === "allium:weed");
|
|
assert("duration is a non-negative number", typeof t[0]?.duration_ms === "number" && t[0].duration_ms >= 0);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 2. No .allium-loop dir → no-op, nothing written.
|
|
{
|
|
const dir = newProject({ withLoopDir: false });
|
|
const payload = { tool_use_id: "x", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
|
|
runHook("pre", payload, dir);
|
|
runHook("post", payload, dir);
|
|
assert("does nothing when no loop is active", !existsSync(path.join(dir, ".allium-loop", "timings.jsonl")));
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 3. FIFO fallback when there is no correlation id: two calls pair in order.
|
|
{
|
|
const dir = newProject({ withLoopDir: true });
|
|
runHook("pre", { tool_name: "Task", tool_input: { subagent_type: "allium:distill" } }, dir);
|
|
runHook("pre", { tool_name: "Task", tool_input: { subagent_type: "allium:propagate" } }, dir);
|
|
runHook("post", { tool_name: "Task", tool_input: {} }, dir);
|
|
runHook("post", { tool_name: "Task", tool_input: {} }, dir);
|
|
const t = timings(dir);
|
|
assert("pairs two unkeyed calls in FIFO order", t.length === 2 && t[0].agent === "allium:distill" && t[1].agent === "allium:propagate");
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 4. A post with no matching pre writes nothing and does not crash.
|
|
{
|
|
const dir = newProject({ withLoopDir: true });
|
|
runHook("post", { tool_use_id: "orphan", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } }, dir);
|
|
assert("ignores an unpaired post", timings(dir).length === 0);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 5. Malformed stdin is swallowed (no crash, no output).
|
|
{
|
|
const dir = newProject({ withLoopDir: true });
|
|
try {
|
|
execFileSync("node", [hook, "pre"], { input: "not json", encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, CLAUDE_PROJECT_ROOT: dir } });
|
|
assert("survives malformed input", true);
|
|
} catch {
|
|
assert("survives malformed input", false);
|
|
}
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 6. A repository-controlled .allium-loop symlink must not redirect writes.
|
|
{
|
|
const dir = newProject({ withLoopDir: false });
|
|
const outside = mkdtempSync(path.join(tmpdir(), "allium-timing-outside-"));
|
|
symlinkSync(outside, path.join(dir, ".allium-loop"));
|
|
const payload = { tool_use_id: "linked-dir", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
|
|
runHook("pre", payload, dir);
|
|
assert("refuses a symlinked loop directory", !existsSync(path.join(outside, ".timing-pending.json")));
|
|
rmSync(dir, { recursive: true, force: true });
|
|
rmSync(outside, { recursive: true, force: true });
|
|
}
|
|
|
|
// 7. A symlinked pending-state file must not be read or overwritten.
|
|
{
|
|
const dir = newProject({ withLoopDir: true });
|
|
const victim = path.join(dir, "victim-pending.txt");
|
|
writeFileSync(victim, "unchanged");
|
|
symlinkSync(victim, path.join(dir, ".allium-loop", ".timing-pending.json"));
|
|
runHook("pre", { tool_use_id: "linked-pending", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } }, dir);
|
|
assert("refuses a symlinked pending-state file", readFileSync(victim, "utf-8") === "unchanged");
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
// 8. A symlinked timing log must not receive appended data.
|
|
{
|
|
const dir = newProject({ withLoopDir: true });
|
|
const payload = { tool_use_id: "linked-log", tool_name: "Task", tool_input: { subagent_type: "allium:weed" } };
|
|
runHook("pre", payload, dir);
|
|
const victim = path.join(dir, "victim-timings.txt");
|
|
writeFileSync(victim, "unchanged");
|
|
symlinkSync(victim, path.join(dir, ".allium-loop", "timings.jsonl"));
|
|
runHook("post", payload, dir);
|
|
assert("refuses a symlinked timing log", readFileSync(victim, "utf-8") === "unchanged");
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed > 0 ? 1 : 0);
|