diff --git a/test/coverage-audit-evidence.test.ts b/test/coverage-audit-evidence.test.ts index b76329b5b..caad2efc2 100644 --- a/test/coverage-audit-evidence.test.ts +++ b/test/coverage-audit-evidence.test.ts @@ -1282,5 +1282,10 @@ describe('coverage audit native evidence: an escaped grep over two literal files expect(reads(capture.call.command, capture.call.output.replace(nonce, nonce.slice(1) + '0'))).toMatchObject({ sourceRead: false }); expect(reads(capture.call.command.replace('cat -n test/billing.test.ts', 'cat -n test/other.test.ts'))).toMatchObject({ testsRead: false }); }); + + test('a quoted grep operand with a space stays one literal file operand', () => { + expect(reads(capture.call.command.replace('review/SKILL.md review/sections/review-army.md', 'review/SKILL.md "docs/design notes.md"'))).toEqual(both); + expect(reads(capture.call.command.replace('review/SKILL.md review/sections/review-army.md', '"docs/design notes.md" -f/tmp/foreign'))).toEqual(neither); + }); }); diff --git a/test/helpers/coverage-audit-evidence.ts b/test/helpers/coverage-audit-evidence.ts index bbb20f71c..bbd79df13 100644 --- a/test/helpers/coverage-audit-evidence.ts +++ b/test/helpers/coverage-audit-evidence.ts @@ -80,8 +80,9 @@ function stagePieces(stage: string[], scope: ShellScope, alone: boolean): Piece[ if (!name || (discarded && name === 'echo')) return undefined; // Backslashes are data only in these closed grep/awk display patterns. const grepRange = /^grep\s+-n(?:\s+-i)?(?:\s+-B\d{1,4})?(?:\s+-A\d{1,4})?\s+(?:"(?:[^"\\$`]|\\[|.])*"|'(?:[^'\\$`]|\\[|.])*')\s+(.+)$/.exec(text); - // One or more literal file operands, none of them an option. - const displayGrep = Boolean(grepRange?.[1]!.split(' ').every(operand => literal(operand) !== undefined && !literal(operand)!.startsWith('-'))); + // One or more literal file operands (the tokens after the quoted pattern), none of them an option. + const grepOperands = grepRange ? words.slice(words.findIndex((word, index) => index > 0 && /^["']/.test(word)) + 1) : []; + const displayGrep = grepOperands.length > 0 && grepOperands.every(operand => literal(operand) !== undefined && !literal(operand)!.startsWith('-')); const awkRange = /^awk\s+'\/(?:[^/\\]|\\[./|])*\/,\/(?:[^/\\]|\\[./|])*\/'\s+(.+)$/.exec(text) ?? /^awk\s+'\/(?:[^/\\]|\\[./|])*\/\{f=1\} f&&\/(?:[^/\\]|\\[./|])*\/\{exit\} f'\s+(.+)$/.exec(text); const displayAwk = Boolean(awkRange && literal(awkRange[1]!) && !literal(awkRange[1]!)!.startsWith('-')); diff --git a/test/helpers/ship-budget-harness.ts b/test/helpers/ship-budget-harness.ts index a6eb8ba8c..3f1b53853 100644 --- a/test/helpers/ship-budget-harness.ts +++ b/test/helpers/ship-budget-harness.ts @@ -68,6 +68,7 @@ export function budgetHarness(): Harness { const run = (bin: string, args: string[], extraEnv: NodeJS.ProcessEnv = {}): Run => { const argv = bin === REVIEW_LOG_BIN ? ['bash', bin] : [process.execPath, ...bunIsolationArgs(), bin]; const started = Date.now(); + abnormal = ''; const r = spawnSync(argv[0], [...argv.slice(1), ...args], { cwd: repo, env: { ...env, ...extraEnv }, encoding: 'utf8', timeout: 60_000 }); const result = { code: r.status ?? -1, out: r.stdout ?? '', err: r.stderr ?? '' }; if (r.status !== null) return result; @@ -79,6 +80,7 @@ export function budgetHarness(): Harness { abnormal += `; children of ${r.pid}: ${children.stdout?.trim() || (children.error ? `unavailable (${children.error.message})` : 'none')}`; } result.err += `\n[harness] ${abnormal}`; + process.stderr.write(`[harness] ${abnormal}\n`); return result; }; let records = 0; diff --git a/test/helpers/third-party-actions.ts b/test/helpers/third-party-actions.ts index ffc19714e..0eccf99d3 100644 --- a/test/helpers/third-party-actions.ts +++ b/test/helpers/third-party-actions.ts @@ -10,7 +10,7 @@ * predicate's own coordinate clause holds no negation: "Aside is not installed but can be downloaded at aside.com" * counts, "Aside is not something that can be downloaded" and "... but cannot be downloaded" never do. */ -const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?*_-]*(?:Aside|it)\b(?:[^.!?;:,\n]*?\b(?:but|and|yet)\b)?(?:(?!\b(?:not|never|no)\b|n['\u2019]t\b)[^.!?;:,\n])*?\b(?:can|may)\s+be\s+downloaded\s+(?:at|from)\s+aside\.com\b`; +const ASIDE_DOWNLOAD_PITCH = String.raw`(?:^|[.!?;:]\s*|\n)\s*(?:[>*-]\s*)*["\u201C]?download (?:it|Aside) at aside\.com\b|(?:^|[.!?;:,\u2014\u2013\n])[\s>*_-]*(?*_-]*(?:Aside|it)\b(?:(?=(?(?:(?!\b(?:can|may)\s+be\s+downloaded\b)[^.!?;:,\n])*\b(?:but|and|yet)\b))\k)?(?:(?!\b(?:not|never|no)\b|n['\u2019]t\b)[^.!?;:,\n])*?\b(?:can|may)\s+be\s+downloaded\s+(?:at|from)\s+aside\.com\b`; export const asideDownloadPitch = new RegExp(ASIDE_DOWNLOAD_PITCH, 'im'); export const asideDownloadPitchCount = (text: string): number => [...text.matchAll(new RegExp(ASIDE_DOWNLOAD_PITCH, 'gim'))].length;