diff --git a/src/headless-uok-status.ts b/src/headless-uok-status.ts index b01fb1751..e15bdf949 100644 --- a/src/headless-uok-status.ts +++ b/src/headless-uok-status.ts @@ -79,6 +79,14 @@ export interface GateHealthEntry { total: number; pass: number; fail: number; + /** + * Manual-attention outcomes (codex audit follow-up). Previously + * counted by getGateRunStats but dropped from the JSON/table output, + * which inflated `total` without an operator-visible bucket. Now + * surfaced as its own column so manual-attention gates are + * distinguishable from pass/fail in the at-a-glance view. + */ + manualAttention: number; retry: number; lastEvaluatedAt: string | null; circuitBreaker: string; @@ -185,8 +193,8 @@ function formatTable(gates: GateHealthEntry[]): string { return "No gate run data found in the last 24h.\n"; } const header = - "| Gate | Scope | Coverage | Pass% | Pass | Fail | Retry | CB | Streak | Last Evaluated |\n" + - "|------|-------|----------|-------|------|------|-------|----|--------|----------------|\n"; + "| Gate | Scope | Coverage | Pass% | Pass | Fail | Manual | Retry | CB | Streak | Last Evaluated |\n" + + "|------|-------|----------|-------|------|------|--------|-------|----|--------|----------------|\n"; const rows = gates .map((g) => { const last = g.lastEvaluatedAt @@ -195,7 +203,7 @@ function formatTable(gates: GateHealthEntry[]): string { .replace("T", " ") .slice(0, 19) : "never"; - return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`; + return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.manualAttention} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`; }) .join("\n"); return `${header}${rows}\n`; @@ -387,6 +395,7 @@ export async function handleUokStatus( total: stats.total ?? 0, pass: stats.pass ?? 0, fail: stats.fail ?? 0, + manualAttention: stats.manualAttention ?? 0, retry: stats.retry ?? 0, // prefer stats window result; fall back to quality_gates last entry lastEvaluatedAt: stats.lastEvaluatedAt ?? runContext.lastEvaluatedAt, diff --git a/src/resources/extensions/sf/tests/headless-uok-status.test.mjs b/src/resources/extensions/sf/tests/headless-uok-status.test.mjs index 899e83ff4..6131104bd 100644 --- a/src/resources/extensions/sf/tests/headless-uok-status.test.mjs +++ b/src/resources/extensions/sf/tests/headless-uok-status.test.mjs @@ -38,8 +38,8 @@ function formatTable(gates) { return "No gate run data found in the last 24h.\n"; } const header = - "| Gate | Scope | Coverage | Pass% | Pass | Fail | Retry | CB | Streak | Last Evaluated |\n" + - "|------|-------|----------|-------|------|------|-------|----|--------|----------------|\n"; + "| Gate | Scope | Coverage | Pass% | Pass | Fail | Manual | Retry | CB | Streak | Last Evaluated |\n" + + "|------|-------|----------|-------|------|------|--------|-------|----|--------|----------------|\n"; const rows = gates .map((g) => { const last = g.lastEvaluatedAt @@ -48,7 +48,7 @@ function formatTable(gates) { .replace("T", " ") .slice(0, 19) : "never"; - return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`; + return `| ${g.id} | ${g.scope} | ${coverageIcon(g.coverageStatus)} | ${passRate(g)} | ${g.pass} | ${g.fail} | ${g.manualAttention ?? 0} | ${g.retry} | ${cbIcon(g.circuitBreaker)} ${g.circuitBreaker} | ${g.failureStreak} | ${last} |`; }) .join("\n"); return `${header}${rows}\n`; @@ -105,6 +105,7 @@ function makeGate(overrides = {}) { total: 10, pass: 8, fail: 2, + manualAttention: 0, retry: 0, lastEvaluatedAt: "2026-05-11T12:00:00.000Z", circuitBreaker: "closed", @@ -172,6 +173,21 @@ describe("formatTable", () => { assert.ok(result.includes("⚠ stale")); }); + it("includes_manual_attention_column_distinct_from_fail", () => { + // Codex audit follow-up: manualAttention used to be counted by + // getGateRunStats but dropped from the rendered output, inflating + // `total` invisibly. Now it's a column between Fail and Retry. + const result = formatTable([ + makeGate({ id: "g-manual", fail: 0, manualAttention: 3 }), + ]); + assert.ok(result.includes("| Manual |"), "header has Manual column"); + // The row contains "| 0 | 3 |" — fail=0, manualAttention=3. + assert.ok( + /\|\s*0\s*\|\s*3\s*\|/.test(result), + "row shows manualAttention=3 distinct from fail=0", + ); + }); + it("includes_separator_row", () => { const result = formatTable([makeGate()]); assert.ok(result.includes("|------|"));