Files
upstream-atomic-crm/.claude/hooks/test/record-review-verdict.test.mjs
upstream-mirror 93ecc1a6f8
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:48:50 +05:00

120 lines
4.6 KiB
JavaScript

// Tests for record-review-verdict.mjs (vitest, "claude" Node project). A
// reviewer's final contract line (APPROVED / REJECTED) becomes a per-ticket flag
// under <sessionDir>/reviews/<TASK>-<role>, read later by
// block-merger-without-review.mjs. Verdict source here is last_assistant_message.
import { mkdtempSync, existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import { describe, test, expect, beforeAll, afterAll } from "vitest";
import { sanitizePath } from "../lib/paths.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const HOOK = join(HERE, "..", "record-review-verdict.mjs");
const SESSION_ID = "deadbeef-1111-2222-3333-444455556666";
let TMP;
let APP_DIR;
let env;
let reviewsDir;
const run = (payload) =>
spawnSync("node", [HOOK], {
input: JSON.stringify({ session_id: SESSION_ID, ...payload }),
env,
encoding: "utf8",
});
const flag = (name) => join(reviewsDir, name);
beforeAll(() => {
TMP = mkdtempSync(join(tmpdir(), "record-verdict-test-"));
APP_DIR = join(TMP, "app");
const CRM_TMP_ROOT = join(TMP, "scratch");
reviewsDir = join(CRM_TMP_ROOT, sanitizePath(APP_DIR), SESSION_ID, "reviews");
env = { ...process.env, APP_DIR, CRM_TMP_ROOT };
delete env.CLAUDE_AGENT_NAME;
// These tests assert the flag lands under ctx.sessionDir (CRM_TMP_ROOT/...).
// reviewsDir() prefers CHAT_SESSION_DIR when set (managed-launcher path), so a
// CHAT_SESSION_DIR inherited from the ambient env (e.g. CRM Builder's container)
// would redirect the flag and break the assertion — neutralise it here. The
// CHAT_SESSION_DIR-present path is covered by reviews.test.mjs.
delete env.CHAT_SESSION_DIR;
});
afterAll(() => rmSync(TMP, { recursive: true, force: true }));
describe("record-review-verdict", () => {
test("APPROVED writes the per-ticket flag", () => {
run({
agent_type: "quality-reviewer-TASK-007",
last_assistant_message: "APPROVED",
});
expect(existsSync(flag("TASK-007-quality-reviewer"))).toBe(true);
});
test("REJECTED clears a prior flag", () => {
run({
agent_type: "quality-reviewer-TASK-007",
last_assistant_message: "REJECTED: do X",
});
expect(existsSync(flag("TASK-007-quality-reviewer"))).toBe(false);
});
test("records the flag from a final APPROVED after preamble prose", () => {
run({
agent_type: "quality-reviewer-TASK-011",
last_assistant_message: "Part C — integration: present\nAPPROVED",
});
expect(existsSync(flag("TASK-011-quality-reviewer"))).toBe(true);
});
test("multi-line REJECTED (bulleted feedback) clears a prior flag", () => {
// Arrange: a prior APPROVED flag exists for this ticket/role.
run({
agent_type: "quality-reviewer-TASK-009",
last_assistant_message: "APPROVED",
});
expect(existsSync(flag("TASK-009-quality-reviewer"))).toBe(true);
// Act: a re-review rejects using the contract's bulleted-list feedback, so
// the REJECTED keyword is NOT on the final line.
run({
agent_type: "quality-reviewer-TASK-009",
last_assistant_message:
"Findings:\nREJECTED:\n- src/foo.ts: missing null check\n- src/bar.ts: wrong import",
});
// Assert: the stale APPROVED flag is cleared.
expect(existsSync(flag("TASK-009-quality-reviewer"))).toBe(false);
});
test("trailing prose starting with REJECTED does not flip a real APPROVED", () => {
// A chatty reviewer that approves but adds a clarifying sentence which happens
// to start with the keyword must NOT be recorded as REJECTED. The contract
// REJECTED form requires a colon (`REJECTED:`), so this trailing prose is
// ignored and the standalone APPROVED above wins.
run({
agent_type: "quality-reviewer-TASK-010",
last_assistant_message:
"APPROVED\nREJECTED concerns from the first pass are now resolved.",
});
expect(existsSync(flag("TASK-010-quality-reviewer"))).toBe(true);
});
test("standalone APPROVED with a trailing note still approves", () => {
run({
agent_type: "quality-reviewer-TASK-012",
last_assistant_message: "APPROVED\nNice work — all e2e specs present.",
});
expect(existsSync(flag("TASK-012-quality-reviewer"))).toBe(true);
});
test("unknown verdict leaves state untouched (no flag written)", () => {
run({
agent_type: "quality-reviewer-TASK-008",
last_assistant_message: "I am still thinking about it",
});
expect(existsSync(flag("TASK-008-quality-reviewer"))).toBe(false);
});
});