import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { computeVerdict, summarize } from "../src/qa/verdict.js"; import { loadTemplate, loadTemplateForType, listTemplates, mergeTemplates, } from "../src/qa/template.js"; import { runQaTemplate, saveQaArtifact } from "../src/qa/runtime.js"; import type { QaTemplate, QaChecklistResult } from "../src/qa/schema.js"; const PROJECT_TEMPLATES = join(process.cwd(), "qa-templates"); let workDir: string; beforeEach(async () => { workDir = await mkdtemp(join(tmpdir(), "rails-qa-test-")); }); afterEach(async () => { await rm(workDir, { recursive: true, force: true }); }); describe("computeVerdict", () => { const makeCheck = ( passed: boolean, severity: QaChecklistResult["severity"], ): QaChecklistResult => ({ id: "test", kind: "manual", passed, severity, evidence: "", errorMessage: "", reviewerNote: "", durationMs: 0, }); it("APPROVE when all checks pass", () => { expect( computeVerdict({ checks: [makeCheck(true, "major"), makeCheck(true, "minor")], prerequisitesPassed: true, }), ).toBe("APPROVE"); }); it("REQUEST_CHANGES on any major failure", () => { expect( computeVerdict({ checks: [makeCheck(true, "major"), makeCheck(false, "major")], prerequisitesPassed: true, }), ).toBe("REQUEST_CHANGES"); }); it("REQUEST_CHANGES on any critical failure", () => { expect( computeVerdict({ checks: [makeCheck(false, "critical")], prerequisitesPassed: true, }), ).toBe("REQUEST_CHANGES"); }); it("APPROVE_WITH_NITS when only minor issues fail", () => { expect( computeVerdict({ checks: [makeCheck(true, "major"), makeCheck(false, "minor")], prerequisitesPassed: true, }), ).toBe("APPROVE_WITH_NITS"); }); it("APPROVE_WITH_NITS on recommendation only", () => { expect( computeVerdict({ checks: [makeCheck(false, "recommendation")], prerequisitesPassed: true, }), ).toBe("APPROVE_WITH_NITS"); }); it("ABORT on prerequisite failure", () => { expect( computeVerdict({ checks: [makeCheck(true, "major")], prerequisitesPassed: false, }), ).toBe("ABORT"); }); it("NEVER REQUEST_CHANGES for minor-only failures (rule)", () => { const v = computeVerdict({ checks: [ makeCheck(false, "minor"), makeCheck(false, "minor"), makeCheck(false, "recommendation"), ], prerequisitesPassed: true, }); expect(v).not.toBe("REQUEST_CHANGES"); }); it("summarize counts blocking failures correctly", () => { const s = summarize([ makeCheck(true, "major"), makeCheck(false, "major"), makeCheck(false, "minor"), makeCheck(false, "critical"), ]); expect(s.total).toBe(4); expect(s.passed).toBe(1); expect(s.failed).toBe(3); expect(s.blockingFailed).toBe(2); // major + critical }); }); describe("template loader", () => { it("lists shipped templates", async () => { const names = await listTemplates(PROJECT_TEMPLATES); expect(names).toContain("scaffold-v1"); expect(names).toContain("feature-v1"); expect(names).toContain("bugfix-v1"); expect(names).toContain("migration-v1"); expect(names).toContain("refactor-v1"); expect(names).toContain("infra-v1"); }); it("loads scaffold-v1 template", async () => { const t = await loadTemplate("scaffold-v1", PROJECT_TEMPLATES); expect(t.template).toBe("scaffold-v1"); expect(t.appliesTo).toContain("scaffold"); expect(t.requiredChecks.length).toBeGreaterThan(0); }); it("loadTemplateForType maps type → template", async () => { const t = await loadTemplateForType("feature", PROJECT_TEMPLATES); expect(t.template).toBe("feature-v1"); }); it("throws on unknown template", async () => { await expect( loadTemplate("nonexistent", PROJECT_TEMPLATES), ).rejects.toThrow(/not found/); }); it("merges templates", async () => { const base: QaTemplate = { template: "base-v1", version: "v1", appliesTo: ["feature"], requiredChecks: [ { id: "c1", description: "", kind: "file_exists", spec: { path: "a" }, blocking: true, severity: "major", }, ], additionalChecks: [], }; const extra: QaTemplate = { template: "extra-v1", version: "v1", appliesTo: [], requiredChecks: [ { id: "c2", description: "", kind: "file_exists", spec: { path: "b" }, blocking: true, severity: "major", }, ], additionalChecks: [], }; const merged = mergeTemplates(base, extra); expect(merged.requiredChecks).toHaveLength(2); expect(merged.template).toBe("base-v1+extra-v1"); }); }); describe("runtime", () => { it("passes a file_exists check when file present", async () => { await writeFile(join(workDir, "README.md"), "# test"); const template: QaTemplate = { template: "test-v1", version: "v1", appliesTo: ["scaffold"], requiredChecks: [ { id: "readme", description: "", kind: "file_exists", spec: { path: "README.md" }, blocking: true, severity: "major", }, ], additionalChecks: [], }; const artifact = await runQaTemplate({ template, workdir: workDir, sprintId: "S1", }); expect(artifact.verdict).toBe("APPROVE"); expect(artifact.summary.passed).toBe(1); }); it("fails on missing file", async () => { const template: QaTemplate = { template: "test-v1", version: "v1", appliesTo: ["scaffold"], requiredChecks: [ { id: "missing", description: "", kind: "file_exists", spec: { path: "never.txt" }, blocking: true, severity: "major", }, ], additionalChecks: [], }; const artifact = await runQaTemplate({ template, workdir: workDir, sprintId: "S1", }); expect(artifact.verdict).toBe("REQUEST_CHANGES"); expect(artifact.summary.blockingFailed).toBe(1); }); it("manual checks are SKIPPED by default (no resolver)", async () => { const template: QaTemplate = { template: "test-v1", version: "v1", appliesTo: ["feature"], requiredChecks: [ { id: "review", description: "", kind: "manual", spec: { question: "Is it good?" }, blocking: true, severity: "major", }, ], additionalChecks: [], }; const artifact = await runQaTemplate({ template, workdir: workDir, sprintId: "S1", }); expect(artifact.verdict).toBe("APPROVE"); expect(artifact.checks[0]!.evidence).toContain("SKIPPED"); }); it("manual checks use resolver when provided", async () => { const template: QaTemplate = { template: "test-v1", version: "v1", appliesTo: ["feature"], requiredChecks: [ { id: "review", description: "", kind: "manual", spec: { question: "Is it clean?" }, blocking: true, severity: "major", }, ], additionalChecks: [], }; const artifact = await runQaTemplate({ template, workdir: workDir, sprintId: "S1", manualResolver: async () => ({ passed: false, note: "found a TODO" }), }); expect(artifact.verdict).toBe("REQUEST_CHANGES"); expect(artifact.checks[0]!.errorMessage).toBe("found a TODO"); }); it("saves artifact to disk", async () => { const template: QaTemplate = { template: "test-v1", version: "v1", appliesTo: ["feature"], requiredChecks: [], additionalChecks: [], }; const artifact = await runQaTemplate({ template, workdir: workDir, sprintId: "S1", }); const path = await saveQaArtifact(workDir, artifact); expect(path).toContain(artifact.artifactId); }); }); describe("scaffold-v1 on real project", () => { it("loads without error and has expected checks", async () => { const t = await loadTemplate("scaffold-v1", PROJECT_TEMPLATES); const ids = t.requiredChecks.map((c) => c.id); expect(ids).toContain("readme-exists"); expect(ids).toContain("tsconfig-strict"); }); });