Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,18 +436,16 @@ function buildGuardrailHoldFinding(): AdvisoryFinding {
* would-be conclusion. `off`/`block`/unset modes are untouched; non-mode policy (grace, size HOLD, guardrail) is
* preserved as-is, so the would-be verdict still honours newcomer grace and the manual-review holds. PURE. */
function promoteAdvisoryToBlock(policy: GateCheckPolicy): GateCheckPolicy {
// #disposition-redesign: the dry-run "would-be" verdict must reflect the REAL disposition model — a CLOSE is driven by
// the AI reviewer's confidence + genuine hard blockers (secret/CI/banned) ONLY. The advisory signals — missing linked
// issue, readiness/quality, slop, duplicates, manifest policy, self-authored issue — are NEVER close drivers, so they
// are deliberately NOT promoted here. Only the AI sub-gate is promoted, so an `advisory` AI defect still previews its
// would-be close while a missing linked issue or a low readiness score can never render a "close" verdict.
const block = (mode: GateRuleMode | undefined): GateRuleMode | undefined => (mode === "advisory" ? "block" : mode);
return {
...policy,
dryRun: false,
linkedIssueGateMode: block(policy.linkedIssueGateMode),
duplicatePrGateMode: block(policy.duplicatePrGateMode),
qualityGateMode: block(policy.qualityGateMode),
aiReviewGateMode: block(policy.aiReviewGateMode),
slopGateMode: block(policy.slopGateMode),
mergeReadinessGateMode: block(policy.mergeReadinessGateMode),
manifestPolicyGateMode: block(policy.manifestPolicyGateMode),
selfAuthoredLinkedIssueGateMode: block(policy.selfAuthoredLinkedIssueGateMode),
};
}

Expand Down
26 changes: 20 additions & 6 deletions test/unit/gate-check-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,18 +604,32 @@ describe("size + guardrail manual-review HOLD (#gate-size / #gate-guardrail)", (
});

describe("dry-run disposition (#gate-dryrun): would-be verdict without enforcing", () => {
it("posts the real non-enforcing conclusion but exposes the would-be conclusion as displayConclusion", () => {
// advisory linked-issue ⇒ the missing-issue finding does NOT block (posted = success), but promoted to block it WOULD close
const out = evaluateGateCheck(missingIssueAdvisory(), { dryRun: true, linkedIssueGateMode: "advisory" });
expect(out.conclusion).toBe("success"); // POSTED — non-blocking pass
// #disposition-redesign: the dry-run shadow promotes ONLY the AI sub-gate. CLOSE is driven by AI confidence; the
// advisory signals (linked issue, readiness/quality, slop, duplicates) can NEVER drive a would-be close.
const aiDefect = (): Advisory => ({
...missingIssueAdvisory(),
findings: [{ code: "ai_consensus_defect", title: "AI consensus defect", severity: "warning", detail: "both models flagged a real defect", action: "fix it" }],
});
it("an advisory AI defect previews a would-be close (AI sub-gate is the only one promoted)", () => {
const out = evaluateGateCheck(aiDefect(), { dryRun: true, aiReviewGateMode: "advisory" });
expect(out.conclusion).toBe("success"); // POSTED — non-blocking (AI is advisory)
expect(out.displayConclusion).toBe("failure"); // would-be — drives the "close" verdict in the comment
});
it("a MISSING LINKED ISSUE never drives a dry-run close (advisory-only signal)", () => {
const out = evaluateGateCheck(missingIssueAdvisory(), { dryRun: true, linkedIssueGateMode: "advisory" });
expect(out.conclusion).toBe("success");
expect(out.displayConclusion).toBe("success"); // NOT promoted ⇒ no would-be close
});
it("a LOW READINESS score never drives a dry-run close (advisory-only signal)", () => {
const out = evaluateGateCheck({ ...missingIssueAdvisory(), findings: [] }, { dryRun: true, qualityGateMode: "advisory", qualityGateMinScore: 70, readinessScore: 40 });
expect(out.displayConclusion).toBe("success"); // readiness is advisory-only, never promoted to a close
});
it("a clean PR in dry-run shows a would-be PASS (displayConclusion = success)", () => {
const clean = { ...missingIssueAdvisory(), findings: [] };
expect(evaluateGateCheck(clean, { dryRun: true, linkedIssueGateMode: "advisory" }).displayConclusion).toBe("success");
expect(evaluateGateCheck(clean, { dryRun: true, aiReviewGateMode: "advisory" }).displayConclusion).toBe("success");
});
it("outside dry-run, displayConclusion is absent (the verdict falls back to the posted conclusion)", () => {
const out = evaluateGateCheck(missingIssueAdvisory(), { linkedIssueGateMode: "advisory" });
const out = evaluateGateCheck(aiDefect(), { aiReviewGateMode: "advisory" });
expect(out.conclusion).toBe("success");
expect(out.displayConclusion).toBeUndefined();
});
Expand Down
Loading