From dccb79d892f9b7250f56422a10c9cb6949a19f90 Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 31 Aug 2026 08:04:17 +0800 Subject: [PATCH 1/2] fix(review): readable bilingual disclosures for lint deferrals and caller prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review body's disclosure sentences carried two readability defects, both visible on PR #10567's posted round-1 body: - The deferred-checker line stuttered: script-lint's deferral/skip reasons ended with an "— not linted" tail written for a standalone context, the gate spliced them under a "the executable-script lint —" prefix, and the body wrapped the result in a sentence that already opens "Not linted:". Posted: "Not linted (tool limitation, not a blocker): the executable-script lint — ... — not linted." The reasons drop the tail, and the disclosure drops the circular prefix — the wrapper names the fact once, the path and reason carry the rest. - The body's Chinese half presented untranslated English as its translation. Two legs: - The deferral disclosure is machine-built from the report, so it can carry a real translation: `scriptLintGate` now returns bilingual disclosure pairs, the report schema gains an optional `reasonZh` (the actionlint deferral supplies it), and an older CLI's report without one falls back to the English reason in both halves. - Caller-prose "Not reviewed" entries stay untranslatable by construction, and the Chinese label now says so — `未审查(原文为英文):` — instead of presenting an all-English sentence as a translation; the payload keeps its own English full stop rather than closing an English sentence with `。`. --- .../commands/review/compose-review.test.ts | 106 +++++++++++++----- .../cli/src/commands/review/compose-review.ts | 42 +++++-- .../src/commands/review/script-lint.test.ts | 7 ++ .../cli/src/commands/review/script-lint.ts | 25 ++++- 4 files changed, 137 insertions(+), 43 deletions(-) diff --git a/packages/cli/src/commands/review/compose-review.test.ts b/packages/cli/src/commands/review/compose-review.test.ts index 9eb2c7d6894..21e150539f1 100644 --- a/packages/cli/src/commands/review/compose-review.test.ts +++ b/packages/cli/src/commands/review/compose-review.test.ts @@ -1537,7 +1537,8 @@ describe('composeReview — event caps (round-7 Critical #2: caps must reach eve // prefix filter must not let it swallow a DIFFERENT reverse-audit scope // reported with its own reason — a whiffed chunk from the rounds that // DID run is exactly what a partially-run audit still owes the author. - const plan = coveredPlan(); + // han: the caller-prose zh assertion below needs the Chinese half rendered. + const plan = coveredPlan(['verify', 'reverse-audit'], { han: true }); writeBudgetStop( plan, { @@ -1547,20 +1548,31 @@ describe('composeReview — event caps (round-7 Critical #2: caps must reach eve }, 3, ); - const r = composeReview( - base({ - planPath: plan, - unreviewedDimensions: [ - "reverse audit — chunk 2's auditor returned nothing substantive twice", - ], - }), - ); + // Not base(): its planPath default runs coveredPlan() again on the same + // path and would overwrite the han-stamped plan. + const r = composeReview({ + planPath: plan, + env: ENV, + modelId: MODEL, + criticalsInline: 0, + suggestionsInline: 0, + unreviewedDimensions: [ + "reverse audit — chunk 2's auditor returned nothing substantive twice", + ], + }); expect(r.body).toContain( 'Not reviewed: reverse audit — stopped before round 3 by the review time budget.', ); expect(r.body).toContain( "Not reviewed: reverse audit — chunk 2's auditor returned nothing substantive twice.", ); + // Caller prose is untranslatable by construction, and the Chinese half + // SAYS so — an unmarked all-English sentence under 中文说明 read as a + // broken translation (#10567's posted body). The payload keeps its own + // English full stop. + expect(r.body).toContain( + "未审查(原文为英文):reverse audit — chunk 2's auditor returned nothing substantive twice.", + ); // The marker's own disclosure still renders exactly once. expect(r.body.split('review time budget').length - 1).toBe(1); }); @@ -6819,8 +6831,38 @@ describe('scriptLintGate — the deterministic gate reads the report', () => { expect(g.criticals).toEqual([]); expect(g.unreviewed).toEqual([]); expect(g.disclosed).toHaveLength(1); - expect(g.disclosed[0]).toContain('.github/workflows/ci.yml'); - expect(g.disclosed[0]).toContain('source mapping not yet supported'); + expect(g.disclosed[0].en).toContain('.github/workflows/ci.yml'); + expect(g.disclosed[0].en).toContain('source mapping not yet supported'); + // No "the executable-script lint" prefix: the body wraps this in a + // sentence that already opens "Not linted:", and the prefix rendered as + // "Not linted: the executable-script lint" — a lint not linted. + expect(g.disclosed[0].en).not.toContain('executable-script lint'); + // No `reasonZh` in this report — the Chinese half falls back to the + // English reason rather than dropping the sentence. + expect(g.disclosed[0].zh).toContain('source mapping not yet supported'); + }); + + it('a deferred entry with a reasonZh renders it in the Chinese half', () => { + const p = writePlan({ + files: [{ path: '.github/workflows/ci.yml', kind: 'source' }], + }); + writeReport({ + deferred: [ + { + path: '.github/workflows/ci.yml', + tool: 'actionlint', + reason: 'source mapping not yet supported', + reasonZh: '尚未支持源映射', + }, + ], + }); + const g = scriptLintGate(p); + expect(g.disclosed).toHaveLength(1); + expect(g.disclosed[0].en).toContain('source mapping not yet supported'); + expect(g.disclosed[0].zh).toContain('尚未支持源映射'); + expect(g.disclosed[0].zh).not.toContain('source mapping not yet supported'); + // Both halves still carry the code-span path. + expect(g.disclosed[0].zh).toContain('.github/workflows/ci.yml'); }); it('ignores a cosmetic (style) or pre-existing (inDiff:false) finding', () => { @@ -6873,12 +6915,14 @@ describe('scriptLintGate — the deterministic gate reads the report', () => { }); const g = scriptLintGate(p); expect(g.disclosed).toHaveLength(1); - const d = g.disclosed[0]; - expect(d).not.toContain('\n'); // newline stripped — cannot forge a body line - expect(d).not.toContain('`pwn`'); // the PR's own backticks stripped — cannot break out - // `@acme-team` sits INSIDE a code span (backtick … no backtick … backtick), so - // it is inert as a GitHub mention — the whole path rendered as one code span. - expect(d).toMatch(/`[^`\n]*@acme-team[^`\n]*`/); + // BOTH halves post — the Chinese one is not exempt from neutralisation. + for (const d of [g.disclosed[0].en, g.disclosed[0].zh]) { + expect(d).not.toContain('\n'); // newline stripped — cannot forge a body line + expect(d).not.toContain('`pwn`'); // the PR's own backticks stripped — cannot break out + // `@acme-team` sits INSIDE a code span (backtick … no backtick … backtick), so + // it is inert as a GitHub mention — the whole path rendered as one code span. + expect(d).toMatch(/`[^`\n]*@acme-team[^`\n]*`/); + } }); it('report prose cannot smuggle live comment grammar into a disclosure', () => { @@ -6903,12 +6947,13 @@ describe('scriptLintGate — the deterministic gate reads the report', () => { errored: [{ path: 'deploy.sh', tool: 'shellcheck' }], }); const g = scriptLintGate(p); - for (const line of [...g.disclosed, ...g.unreviewed]) { + const disclosedHalves = g.disclosed.flatMap((d) => [d.en, d.zh]); + for (const line of [...disclosedHalves, ...g.unreviewed]) { expect(line).not.toContain(''); } - expect(g.disclosed[0]).toContain('qwen-review-deferred'); - expect(g.disclosed[0]).toContain('mapping unsupported'); + expect(g.disclosed[0].en).toContain('qwen-review-deferred'); + expect(g.disclosed[0].en).toContain('mapping unsupported'); }); it.each([ @@ -6965,7 +7010,7 @@ describe('scriptLintGate — the deterministic gate reads the report', () => { expect(g.disclosed).toHaveLength(1); expect(g.unreviewed[0]).toContain('42'); expect(g.unreviewed[1]).toContain('undefined errored'); - expect(g.disclosed[0]).toContain('[object Object]'); + expect(g.disclosed[0].en).toContain('[object Object]'); }); it('reports an errored checker as unreviewed (fail closed)', () => { @@ -7053,8 +7098,9 @@ describe('composeReview — the script-lint gate wired to the verdict', () => { // verifier (['reverse-audit']) to prove a finding stands with none. function gateReadyPlan( step45Keys: string[] = ['verify', 'reverse-audit'], + planOpts: Parameters[1] = {}, ): string { - const p = coveredPlan(step45Keys); + const p = coveredPlan(step45Keys, planOpts); const planObj = JSON.parse(readFileSync(p, 'utf8')); planObj.worktreePath = '.qwen/tmp/review-pr-1'; writeFileSync(p, JSON.stringify(planObj)); @@ -7206,7 +7252,9 @@ describe('composeReview — the script-lint gate wired to the verdict', () => { // but MUST be surfaced in the body so the reader knows that shell went unlinted. // The gate reads the report as the sole authority, so the deferral is disclosed // from the report itself; the plan stays fully covered so the Approve stands. - const p = gateReadyPlan(); + // han: the Chinese half only renders for a han-audience PR, and this test + // pins that half's sentence too. + const p = gateReadyPlan(['verify', 'reverse-audit'], { han: true }); writeGateReport({ deferred: [ { @@ -7224,8 +7272,16 @@ describe('composeReview — the script-lint gate wired to the verdict', () => { modelId: MODEL, }); expect(r.event).toBe('APPROVE'); - expect(r.body).toContain('.github/workflows/ci.yml'); - expect(r.body).toContain('source mapping not yet supported'); + // The whole composed sentence, both halves — pinned against the stutter + // #10567's posted body carried ("Not linted: the executable-script lint — + // … — not linted"): the wrapper says "Not linted" once, then path and + // reason, nothing else. + expect(r.body).toContain( + 'Not linted (tool limitation, not a blocker): `.github/workflows/ci.yml` — source mapping not yet supported.', + ); + expect(r.body).toContain( + '未检查(工具限制,非阻断):`.github/workflows/ci.yml`——source mapping not yet supported。', + ); // the clean-approve copy is still there — the disclosure augments, it doesn't replace expect(r.body).toContain('No issues found. LGTM! ✅'); }); diff --git a/packages/cli/src/commands/review/compose-review.ts b/packages/cli/src/commands/review/compose-review.ts index db6c4af8f94..7831835d0ba 100644 --- a/packages/cli/src/commands/review/compose-review.ts +++ b/packages/cli/src/commands/review/compose-review.ts @@ -3470,8 +3470,9 @@ function composeReviewBody( bodyCriticals.push(...ownAfterGateDedup); const modelBodyCriticals = [...bodyCriticals]; // input's, captured before the gate // Disclosed-but-non-capping notes from the gate (a deferred checker). Rendered - // in the body on every verdict, but never fed into the cap. - const gateDisclosed: string[] = []; + // in the body on every verdict, but never fed into the cap. Bilingual pairs — + // see `scriptLintGate` for why this channel can afford a real translation. + const gateDisclosed: Array<{ en: string; zh: string }> = []; // Test Plan rulings. Disclosed on every verdict and counted toward nothing — // see `testPlanGate` for why this one neither blocks nor caps. const testPlanNotes: string[] = []; @@ -4878,12 +4879,17 @@ function composeReviewBody( }); } for (const d of explainedCaller) { - // Caller prose, untranslatable by construction — quoted as-is in both, - // its comment grammar inert. + // Caller prose, untranslatable by construction — quoted as-is in both + // halves, its comment grammar inert. The Chinese label SAYS so: without + // the parenthetical, the 中文说明 block presented an all-English sentence + // as its translation (#10567's posted body), and the reader is left + // wondering whether the translation machinery broke. The payload keeps + // its own English full stop — closing an English sentence with "。" is + // the other half of that mismatch. const disclosed = stripCommentGrammar(d); notReviewedParts.push({ en: `Not reviewed: ${disclosed}.`, - zh: `未审查:${disclosed}。`, + zh: `未审查(原文为英文):${disclosed}.`, }); } // Budget-gap disclosures, one BOUNDED sentence for all of them. Four @@ -5180,8 +5186,8 @@ function composeReviewBody( ? [ { trim: 2, - en: `Not linted (tool limitation, not a blocker): ${gateDisclosed.join('; ')}.`, - zh: `未检查(工具限制,非阻断):${gateDisclosed.join('; ')}。`, + en: `Not linted (tool limitation, not a blocker): ${gateDisclosed.map((g) => g.en).join('; ')}.`, + zh: `未检查(工具限制,非阻断):${gateDisclosed.map((g) => g.zh).join(';')}。`, }, ] : []; @@ -6232,14 +6238,17 @@ function structurallyValidReport(report: unknown): boolean { export function scriptLintGate(planPath: string): { criticals: string[]; unreviewed: string[]; - disclosed: string[]; + disclosed: Array<{ en: string; zh: string }>; } { const criticals: string[] = []; const unreviewed: string[] = []; // Disclosed-but-NOT-capping: a `deferred` checker (actionlint) is a known tool // limitation, not a finding and not an unrun-checker gap — the reader is told a // workflow's embedded shell was not linted, but the verdict is not capped on it. - const disclosed: string[] = []; + // Bilingual, unlike the capping lists: these strings are machine-built from the + // report (no model prose), so the body's Chinese half can carry a real + // translation instead of the English line verbatim. + const disclosed: Array<{ en: string; zh: string }> = []; let plan: { prNumber?: unknown; files?: unknown; @@ -6354,10 +6363,19 @@ export function scriptLintGate(planPath: string): { // A deferred checker (actionlint) is disclosed but does not cap — the reader is // told the workflow's embedded shell was not linted, without making every // workflow PR un-Approvable on a checker we deliberately decline to run. + // No "the executable-script lint —" prefix here: the body's own wrapper opens + // with "Not linted:", and naming the lint after that header rendered as + // "Not linted: the executable-script lint" — a sentence about not running a + // lint on a lint (#10567's posted body). The path and reason carry the facts. for (const d of report.deferred ?? []) { - disclosed.push( - `the executable-script lint — ${mdField(d.path)}: ${stripCommentGrammar(d.reason ?? `${d.tool} deferred`)}`, - ); + const reason = stripCommentGrammar(d.reason ?? `${d.tool} deferred`); + disclosed.push({ + en: `${mdField(d.path)} — ${reason}`, + // An older CLI's report has no `reasonZh`; English both halves beats a + // half-empty sentence. Its comment grammar goes inert like the reason's — + // the report is agent-rewritable prose either way. + zh: `${mdField(d.path)}——${d.reasonZh ? stripCommentGrammar(d.reasonZh) : reason}`, + }); } return { criticals, unreviewed, disclosed }; } diff --git a/packages/cli/src/commands/review/script-lint.test.ts b/packages/cli/src/commands/review/script-lint.test.ts index 2c83a6eaa92..829580a136e 100644 --- a/packages/cli/src/commands/review/script-lint.test.ts +++ b/packages/cli/src/commands/review/script-lint.test.ts @@ -172,6 +172,13 @@ describe('runScriptLint — graceful degradation and scoping', () => { expect(r.deferred).toHaveLength(1); expect(r.deferred[0].tool).toBe('actionlint'); expect(r.deferred[0].reason).toContain('not yet supported'); + // The reason must not restate "not linted" — the body renders it under a + // wrapper that already opens with "Not linted:", and a tail here posted + // the phrase twice in one sentence (#10567's posted body). + expect(r.deferred[0].reason).not.toContain('not linted'); + // The Chinese half of that wrapper sentence renders `reasonZh` — without + // it the 中文说明 block carried the English reason verbatim. + expect(r.deferred[0].reasonZh).toContain('actionlint'); expect(r.ok).toBe(true); }); diff --git a/packages/cli/src/commands/review/script-lint.ts b/packages/cli/src/commands/review/script-lint.ts index 5c01feca826..7e37663194c 100644 --- a/packages/cli/src/commands/review/script-lint.ts +++ b/packages/cli/src/commands/review/script-lint.ts @@ -93,8 +93,19 @@ export interface ScriptLintReport { * capping — actionlint is installed on ~15% of PRs (every workflow change), and * capping all of them on a checker we choose not to run would make them * un-Approvable forever, which "install the tool" cannot fix. + * + * `reasonZh` is the same reason for the review body's Chinese half — the + * deferral is the one lint outcome rendered inside a translated sentence, + * and without it that sentence carried the English reason verbatim. + * Optional so a report written by an older CLI still renders (English both + * halves). */ - deferred: Array<{ path: string; tool: LintTool; reason: string }>; + deferred: Array<{ + path: string; + tool: LintTool; + reason: string; + reasonZh?: string; + }>; /** * True when every applicable linter ran cleanly **and** no finding on a changed * line is above `style` — `info`/`warning`/`error` all count against it (the @@ -608,7 +619,7 @@ export function runScriptLint( skipped.push({ path, tool: byName, - reason: 'path resolves outside the worktree — not linted', + reason: 'path resolves outside the worktree', }); } continue; @@ -623,11 +634,13 @@ export function runScriptLint( if (byName) { // Reason does NOT lead with the path — the gate prefixes `${path}:` when // it discloses, and leading with it here would print the path twice. + // Nor does it end with "not linted": the body renders every reason + // under a header that already says so ("Not reviewed:" / "Not + // linted:"), and a tail here posted the phrase twice in one sentence. skipped.push({ path, tool: byName, - reason: - 'not a regular file (symlink/fifo) or unreadable — not linted', + reason: 'not a regular file (symlink/fifo) or unreadable', }); } continue; @@ -647,8 +660,8 @@ export function runScriptLint( deferred.push({ path, tool, - reason: - 'actionlint embedded-shell source mapping is not yet supported — not linted', + reason: 'actionlint embedded-shell source mapping is not yet supported', + reasonZh: 'actionlint 对 workflow 内嵌 shell 的源映射尚未支持', }); continue; } From dbf0be211a5f8ce55490d936d3f3301376ba8b3e Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 31 Aug 2026 18:34:48 +0800 Subject: [PATCH 2/2] test(review): pin the bilingual deferral disclosures the review asked for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five review-suggested pins, each verified by re-running its witness mutation: - The comment-grammar fixture now carries a malicious reasonZh, so the stripCommentGrammar(d.reasonZh) leg is exercised against a live marker (deleting the call previously survived the suite). - The pipeline reasonZh is pinned to its Chinese literal — a toContain('actionlint') fragment was satisfied by the English reason too. - Both skipped reasons get not.toContain('not linted') pins; re-appending either tail previously shipped green. - A two-entry deferred fixture pins the en '; ' and zh ';' joins and the reasonZh-carrying branch end-to-end — every prior fixture held one entry, so no join separator was ever observable, and the only full-sentence zh pin exercised the English-fallback branch. --- .../commands/review/compose-review.test.ts | 48 +++++++++++++++++++ .../commands/review/script-lint.mock.test.ts | 4 ++ .../src/commands/review/script-lint.test.ts | 13 ++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/review/compose-review.test.ts b/packages/cli/src/commands/review/compose-review.test.ts index 21e150539f1..1201dd7d219 100644 --- a/packages/cli/src/commands/review/compose-review.test.ts +++ b/packages/cli/src/commands/review/compose-review.test.ts @@ -6939,6 +6939,11 @@ describe('scriptLintGate — the deterministic gate reads the report', () => { path: '.github/workflows/ci.yml', tool: 'actionlint', reason: 'mapping unsupported here', + // The zh half renders THIS leg when present — without a marker + // here, the loop below sanitised zh via the already-stripped + // English fallback and the `stripCommentGrammar(d.reasonZh)` call + // was never exercised: deleting it survived the whole suite. + reasonZh: '映射不支持 这里', }, ], skipped: [ @@ -6954,6 +6959,9 @@ describe('scriptLintGate — the deterministic gate reads the report', () => { } expect(g.disclosed[0].en).toContain('qwen-review-deferred'); expect(g.disclosed[0].en).toContain('mapping unsupported'); + // Same invariant on the zh half: the text survives, the grammar is inert. + expect(g.disclosed[0].zh).toContain('qwen-review-deferred'); + expect(g.disclosed[0].zh).toContain('映射不支持'); }); it.each([ @@ -7285,6 +7293,46 @@ describe('composeReview — the script-lint gate wired to the verdict', () => { // the clean-approve copy is still there — the disclosure augments, it doesn't replace expect(r.body).toContain('No issues found. LGTM! ✅'); }); + + it('two deferred entries join per language, the reasonZh branch rendered whole', () => { + // Every other deferred fixture holds ONE entry, and a one-element join + // emits no separator — so the en '; ' join, the zh full-width ';' join + // and the reasonZh-carrying branch (the primary case: every report the + // current CLI writes carries `reasonZh`) were pinned by nothing; the + // single-entry test above reaches only the English-fallback branch. Two + // entries, one translated and one not, pin both composed sentences + // whole, separators included. + const p = gateReadyPlan(['verify', 'reverse-audit'], { han: true }); + writeGateReport({ + deferred: [ + { + path: '.github/workflows/ci.yml', + tool: 'actionlint', + reason: 'source mapping not yet supported', + reasonZh: '尚未支持源映射', + }, + { + path: '.github/workflows/release.yml', + tool: 'actionlint', + reason: 'source mapping not yet supported', + }, + ], + }); + const r = composeReview({ + criticalsInline: 0, + suggestionsInline: 0, + planPath: p, + env: ENV, + modelId: MODEL, + }); + expect(r.event).toBe('APPROVE'); + expect(r.body).toContain( + 'Not linted (tool limitation, not a blocker): `.github/workflows/ci.yml` — source mapping not yet supported; `.github/workflows/release.yml` — source mapping not yet supported.', + ); + expect(r.body).toContain( + '未检查(工具限制,非阻断):`.github/workflows/ci.yml`——尚未支持源映射;`.github/workflows/release.yml`——source mapping not yet supported。', + ); + }); }); describe('testPlanGate — Test Plan rulings, disclosed but never capping', () => { diff --git a/packages/cli/src/commands/review/script-lint.mock.test.ts b/packages/cli/src/commands/review/script-lint.mock.test.ts index aa73127bf55..e926c171597 100644 --- a/packages/cli/src/commands/review/script-lint.mock.test.ts +++ b/packages/cli/src/commands/review/script-lint.mock.test.ts @@ -283,6 +283,9 @@ describe('runScriptLint — refuses a path that escapes the worktree', () => { expect(r.checked).toEqual([]); expect(r.skipped).toHaveLength(1); expect(r.skipped[0].reason).toContain('outside the worktree'); + // The body's "Not reviewed:" wrapper already says the file went + // unchecked — a "not linted" tail here posted the phrase twice. + expect(r.skipped[0].reason).not.toContain('not linted'); }); it('refuses a path whose ANCESTOR is a symlink out of the worktree (lexical is not enough)', () => { @@ -306,6 +309,7 @@ describe('runScriptLint — refuses a path that escapes the worktree', () => { expect(r.checked).toEqual([]); expect(r.skipped).toHaveLength(1); expect(r.skipped[0].reason).toContain('outside the worktree'); + expect(r.skipped[0].reason).not.toContain('not linted'); }); }); diff --git a/packages/cli/src/commands/review/script-lint.test.ts b/packages/cli/src/commands/review/script-lint.test.ts index 829580a136e..cfe0c8f0074 100644 --- a/packages/cli/src/commands/review/script-lint.test.ts +++ b/packages/cli/src/commands/review/script-lint.test.ts @@ -177,8 +177,13 @@ describe('runScriptLint — graceful degradation and scoping', () => { // the phrase twice in one sentence (#10567's posted body). expect(r.deferred[0].reason).not.toContain('not linted'); // The Chinese half of that wrapper sentence renders `reasonZh` — without - // it the 中文说明 block carried the English reason verbatim. - expect(r.deferred[0].reasonZh).toContain('actionlint'); + // it the 中文说明 block carried the English reason verbatim. Pinned to the + // literal: a `toContain('actionlint')` fragment was satisfied by the + // ENGLISH reason too, so the exact defect this field exists to fix — an + // English string posing as the Chinese half — shipped green under it. + expect(r.deferred[0].reasonZh).toBe( + 'actionlint 对 workflow 内嵌 shell 的源映射尚未支持', + ); expect(r.ok).toBe(true); }); @@ -198,6 +203,10 @@ describe('runScriptLint — graceful degradation and scoping', () => { expect(r.skipped).toHaveLength(1); expect(r.skipped[0].tool).toBe('shellcheck'); expect(r.skipped[0].reason).toContain('not a regular file'); + // The body renders this under "Not reviewed:" — a "not linted" tail here + // posted the phrase twice in one sentence (the deferred reason's pin + // above guards its own copy; this guards the skipped one). + expect(r.skipped[0].reason).not.toContain('not linted'); }); it('checks nothing when no executable file changed', () => {