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
120 lines
4.6 KiB
JavaScript
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);
|
|
});
|
|
});
|