@descent-vtt/spec-harness 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +90 -0
- package/README.md +8 -3
- package/bin/spec-harness.js +9 -0
- package/dist/branch.js +2 -0
- package/dist/branch.js.map +1 -1
- package/dist/briefs.js +2 -0
- package/dist/briefs.js.map +1 -1
- package/dist/cli.d.ts +15 -3
- package/dist/cli.js +33 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +6 -2
- package/dist/commands.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/git.d.ts +8 -6
- package/dist/git.js +11 -6
- package/dist/git.js.map +1 -1
- package/dist/probe.d.ts +59 -1
- package/dist/probe.js +88 -1
- package/dist/probe.js.map +1 -1
- package/dist/reader.js +4 -0
- package/dist/reader.js.map +1 -1
- package/dist/sandbox.d.ts +25 -1
- package/dist/sandbox.js +134 -29
- package/dist/sandbox.js.map +1 -1
- package/package.json +3 -3
- package/skills/draft-brief/SKILL.md +10 -2
package/dist/probe.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Probes: a defect, measured before anyone is sent to fix it.\n *\n * A brief that claims a defect proves it with a probe - a test that fails at\n * the base commit, for the reason the brief states, every time it runs - and\n * the round is done when the same probe passes at the head. The probe lives\n * in the brief, as fenced blocks, so the evidence and the claim are one\n * record:\n *\n * ````markdown\n * ```probe\n * id: old-token-still-accepted\n * run: npm test -- tests/probes/rotate.test.ts\n * signature: expected 401, got 200\n * ```\n *\n * ```probe-file tests/probes/rotate.test.ts\n * ...the test...\n * ```\n * ````\n *\n * What \"for the reason the brief states\" means is declared, never inferred\n * from prose: a `signature` the failing output must contain, or a `test` name\n * the JUnit report must show failing (ADR-0007). Without one, a probe could be\n * red because it did not compile, and a red that proves nothing is refused.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { JUnitRead } from './junit.js';\n\nexport interface ProbeSpec {\n readonly id: string;\n /** The command, as a line a shell would run. */\n readonly run: string;\n /** A command to prepare a fresh checkout, such as `npm ci`. */\n readonly setup: string | null;\n /** Text the failing output, or the failing test's message, must contain. */\n readonly signature: string | null;\n /** A JUnit report the command writes, relative to the checkout. */\n readonly junit: string | null;\n /** The failing test's name, or part of it, in the JUnit report. */\n readonly test: string | null;\n readonly runs: number | null;\n readonly timeout: number | null;\n /** 1-based line of the probe block in the brief. */\n readonly line: number;\n}\n\nexport interface ProbeFile {\n readonly path: string;\n readonly content: string;\n readonly line: number;\n}\n\n/** A fenced block as the scanner found it. */\nexport interface CodeBlock {\n readonly info: string;\n readonly content: string;\n /** 1-based line of the opening fence. */\n readonly line: number;\n}\n\nexport interface ProbeSet {\n readonly probes: readonly ProbeSpec[];\n readonly files: readonly ProbeFile[];\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n /** SHA-256 over every probe block and probe file, in order: what the evidence was measured with. */\n readonly hash: string;\n}\n\nconst KEYS = ['id', 'run', 'setup', 'signature', 'junit', 'test', 'runs', 'timeout'];\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && (trimmed.startsWith('\"') || trimmed.startsWith(\"'\")) && trimmed.endsWith(trimmed.charAt(0))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * A probe line without its trailing `# comment`. A `#` counts only after\n * whitespace and outside quotes, so `run: npm test -- -t \"issue #12\"` keeps\n * its argument; a quote counts only where a word starts, so the apostrophe in\n * `it's` opens nothing.\n */\nfunction withoutComment(raw: string): string {\n let quote: string | null = null;\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw.charAt(i);\n const after = i === 0 ? ' ' : raw.charAt(i - 1);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else if ((ch === '\"' || ch === \"'\") && /[\\s:=]/.test(after)) {\n quote = ch;\n } else if (ch === '#' && /\\s/.test(after)) {\n return raw.slice(0, i);\n }\n }\n return raw;\n}\n\n/** Reads the probes and probe files among a brief's fenced blocks. */\nexport function readProbes(blocks: readonly CodeBlock[]): ProbeSet {\n const probes: ProbeSpec[] = [];\n const files: ProbeFile[] = [];\n const problems: { line: number; message: string }[] = [];\n const hash = createHash('sha256');\n for (const block of blocks) {\n const [word, ...rest] = block.info.trim().split(/\\s+/);\n if (word === 'probe-file') {\n const path = rest.join(' ').trim();\n // A backslash is a separator where the probe is written to disk on Windows.\n if (path === '' || /^[\\\\/]/.test(path) || path.split(/[\\\\/]/).includes('..') || /^[A-Za-z]:/.test(path)) {\n problems.push({ line: block.line, message: `a probe-file names \"${path}\"; it must be a path inside the repository` });\n continue;\n }\n files.push({ path, content: block.content, line: block.line });\n hash.update(`probe-file ${path}\\n${block.content}\\n`);\n continue;\n }\n if (word !== 'probe') continue;\n hash.update(`probe\\n${block.content}\\n`);\n const fields = new Map<string, string>();\n let bad = false;\n for (const raw of block.content.split(/\\r?\\n/)) {\n const line = withoutComment(raw).trim();\n if (line === '' || line.startsWith('#')) continue;\n const colon = line.indexOf(':');\n const key = colon < 0 ? '' : line.slice(0, colon).trim();\n if (!KEYS.includes(key)) {\n problems.push({ line: block.line, message: `a probe says \"${line}\"; its keys are ${KEYS.join(', ')}` });\n bad = true;\n continue;\n }\n fields.set(key, unquote(line.slice(colon + 1)));\n }\n if (bad) continue;\n const id = fields.get('id') ?? '';\n const run = fields.get('run') ?? '';\n if (id === '' || run === '') {\n problems.push({ line: block.line, message: 'a probe needs an id and a run command' });\n continue;\n }\n const signature = fields.get('signature') || null;\n const test = fields.get('test') || null;\n const junit = fields.get('junit') || null;\n if (signature === null && test === null) {\n problems.push({\n line: block.line,\n message: `probe ${id} does not say how it fails; give a signature, or a JUnit test name, so that a failure to compile is not taken for the defect`,\n });\n continue;\n }\n if (test !== null && junit === null) {\n problems.push({ line: block.line, message: `probe ${id} names a test but no junit report to find it in` });\n continue;\n }\n const count = (key: string): number | null | string => {\n const value = fields.get(key);\n if (value === undefined) return null;\n const n = Number(value);\n return Number.isInteger(n) && n >= 1 ? n : `probe ${id}: \"${key}\" must be a whole number of at least 1`;\n };\n const runs = count('runs');\n const timeout = count('timeout');\n if (typeof runs === 'string' || typeof timeout === 'string') {\n problems.push({ line: block.line, message: (typeof runs === 'string' ? runs : timeout) as string });\n continue;\n }\n if (probes.some((probe) => probe.id === id)) {\n problems.push({ line: block.line, message: `probe ${id} is declared twice` });\n continue;\n }\n probes.push({ id, run, setup: fields.get('setup') || null, signature, junit, test, runs, timeout, line: block.line });\n }\n return { probes, files, problems, hash: `sha256-${hash.digest('hex')}` };\n}\n\n/** What one run of a probe did. `exitCode` is `null` when it was stopped at its timeout. */\nexport interface ProbeRun {\n readonly exitCode: number | null;\n readonly output: string;\n /** The report when the probe names one: read, unreadable, or `null` when the file was not written. */\n readonly junit: JUnitRead | null;\n}\n\nexport type RunOutcome = 'red' | 'green' | 'wrong-failure' | 'timeout' | 'no-report';\n\nexport interface Classified {\n readonly outcome: RunOutcome;\n /** The evidence: the line or the message that matched, or what was missing. */\n readonly detail: string;\n}\n\nfunction firstLineWith(text: string, needle: string): string {\n const line = text.split(/\\r?\\n/).find((candidate) => candidate.includes(needle));\n return (line ?? needle).trim().slice(0, 200);\n}\n\n/** What one run showed, against what the probe declares. */\nexport function classify(spec: ProbeSpec, run: ProbeRun): Classified {\n if (run.exitCode === null) return { outcome: 'timeout', detail: `stopped after ${spec.timeout ?? 'the configured'} seconds` };\n if (spec.junit !== null) {\n if (run.junit === null) {\n return run.exitCode === 0\n ? { outcome: 'no-report', detail: `the command passed and wrote no ${spec.junit}` }\n : { outcome: 'wrong-failure', detail: `the command failed before writing ${spec.junit}` };\n }\n if (!run.junit.ok) return { outcome: 'no-report', detail: `${spec.junit} cannot be read: ${run.junit.error}` };\n const failing = run.junit.cases.filter((c) => c.outcome === 'failed' || c.outcome === 'errored');\n const matching = failing.filter(\n (c) =>\n (spec.test === null || `${c.classname} ${c.name}`.includes(spec.test)) &&\n (spec.signature === null || c.message.includes(spec.signature) || c.name.includes(spec.signature)),\n );\n const first = matching[0];\n if (first !== undefined) {\n return { outcome: 'red', detail: `${first.name} failed${spec.signature === null ? '' : `: ${firstLineWith(first.message, spec.signature)}`}` };\n }\n if (failing.length === 0 && run.exitCode === 0) return { outcome: 'green', detail: 'every test passed' };\n const other = failing[0];\n return {\n outcome: 'wrong-failure',\n detail: other === undefined ? `the command exited ${run.exitCode} with no failing test` : `${other.name} failed, which is not what the probe declares`,\n };\n }\n if (run.exitCode === 0) return { outcome: 'green', detail: 'the command passed' };\n const signature = spec.signature as string;\n return run.output.includes(signature)\n ? { outcome: 'red', detail: firstLineWith(run.output, signature) }\n : { outcome: 'wrong-failure', detail: `the command exited ${run.exitCode} and its output does not contain \"${signature}\"` };\n}\n\nexport type Verdict =\n /** Red at every run where red was expected: the defect is measured. */\n | 'measured'\n /** Green at every run where green was expected: the defect is gone. */\n | 'fixed'\n /** Green where red was expected: the probe finds no defect. */\n | 'vacuous'\n /** Red where green was expected: the round did not fix it. */\n | 'still-failing'\n /** Runs disagreed with each other. */\n | 'flaky'\n /** It failed for another reason, timed out, or wrote no report: it proves nothing. */\n | 'invalid';\n\n/** The verdict over every run of one probe, at the base (`red` expected) or the head (`green`). */\nexport function verdictOf(expected: 'red' | 'green', runs: readonly Classified[]): Verdict {\n const outcomes = new Set(runs.map((run) => run.outcome));\n if (outcomes.size > 1) return 'flaky';\n const only = runs[0]?.outcome;\n if (only === 'red') return expected === 'red' ? 'measured' : 'still-failing';\n if (only === 'green') return expected === 'green' ? 'fixed' : 'vacuous';\n return 'invalid';\n}\n\nexport interface ProbeResult {\n readonly probe: ProbeSpec;\n readonly at: string;\n readonly commit: string;\n readonly expected: 'red' | 'green';\n readonly runs: readonly Classified[];\n readonly verdict: Verdict;\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The evidence a brief records: a table a reader can check, and what it was measured with. */\nexport function renderEvidence(results: readonly ProbeResult[], hash: string, date: string): string {\n const lines = ['| Probe | At | Runs | Verdict | Evidence |', '| --- | --- | ---: | --- | --- |'];\n for (const result of results) {\n const agreeing = result.runs.filter((run) => run.outcome === result.runs[0]?.outcome).length;\n lines.push(\n `| ${cell(result.probe.id)} | ${result.at} \\`${result.commit.slice(0, 12)}\\` | ${agreeing}/${result.runs.length} | ${result.verdict} | ${cell(result.runs[0]?.detail ?? '')} |`,\n );\n }\n lines.push('', `Measured ${date} by spec-harness with probes \\`${hash}\\`.`);\n return lines.join('\\n');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Probes: a defect, measured before anyone is sent to fix it.\n *\n * A brief that claims a defect proves it with a probe - a test that fails at\n * the base commit, for the reason the brief states, every time it runs - and\n * the round is done when the same probe passes at the head. The probe lives\n * in the brief, as fenced blocks, so the evidence and the claim are one\n * record:\n *\n * ````markdown\n * ```probe\n * id: old-token-still-accepted\n * run: npm test -- tests/probes/rotate.test.ts\n * signature: expected 401, got 200\n * ```\n *\n * ```probe-file tests/probes/rotate.test.ts\n * ...the test...\n * ```\n * ````\n *\n * What \"for the reason the brief states\" means is declared, never inferred\n * from prose: a `signature` the failing output must contain, or a `test` name\n * the JUnit report must show failing (ADR-0007). Without one, a probe could be\n * red because it did not compile, and a red that proves nothing is refused.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { JUnitRead } from './junit.js';\n\nexport interface ProbeSpec {\n readonly id: string;\n /** The command, as a line a shell would run. */\n readonly run: string;\n /** A command to prepare a fresh checkout, such as `npm ci`. */\n readonly setup: string | null;\n /** Text the failing output, or the failing test's message, must contain. */\n readonly signature: string | null;\n /** A JUnit report the command writes, relative to the checkout. */\n readonly junit: string | null;\n /** The failing test's name, or part of it, in the JUnit report. */\n readonly test: string | null;\n readonly runs: number | null;\n readonly timeout: number | null;\n /** 1-based line of the probe block in the brief. */\n readonly line: number;\n}\n\nexport interface ProbeFile {\n readonly path: string;\n readonly content: string;\n readonly line: number;\n}\n\n/** A fenced block as the scanner found it. */\nexport interface CodeBlock {\n readonly info: string;\n readonly content: string;\n /** 1-based line of the opening fence. */\n readonly line: number;\n}\n\nexport interface ProbeSet {\n readonly probes: readonly ProbeSpec[];\n readonly files: readonly ProbeFile[];\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n /** SHA-256 over every probe block and probe file, in order: what the evidence was measured with. */\n readonly hash: string;\n}\n\nconst KEYS = ['id', 'run', 'setup', 'signature', 'junit', 'test', 'runs', 'timeout'];\n\nfunction unquote(value: string): string {\n const trimmed = value.trim();\n if (trimmed.length >= 2 && (trimmed.startsWith('\"') || trimmed.startsWith(\"'\")) && trimmed.endsWith(trimmed.charAt(0))) {\n return trimmed.slice(1, -1);\n }\n return trimmed;\n}\n\n/**\n * A probe line without its trailing `# comment`. A `#` counts only after\n * whitespace and outside quotes, so `run: npm test -- -t \"issue #12\"` keeps\n * its argument; a quote counts only where a word starts, so the apostrophe in\n * `it's` opens nothing.\n */\nfunction withoutComment(raw: string): string {\n let quote: string | null = null;\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw.charAt(i);\n const after = i === 0 ? ' ' : raw.charAt(i - 1);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else if ((ch === '\"' || ch === \"'\") && /[\\s:=]/.test(after)) {\n quote = ch;\n } else if (ch === '#' && /\\s/.test(after)) {\n return raw.slice(0, i);\n }\n }\n return raw;\n}\n\n/** Reads the probes and probe files among a brief's fenced blocks. */\nexport function readProbes(blocks: readonly CodeBlock[]): ProbeSet {\n const probes: ProbeSpec[] = [];\n const files: ProbeFile[] = [];\n const problems: { line: number; message: string }[] = [];\n const hash = createHash('sha256');\n for (const block of blocks) {\n const [word, ...rest] = block.info.trim().split(/\\s+/);\n if (word === 'probe-file') {\n const path = rest.join(' ').trim();\n // A backslash is a separator where the probe is written to disk on Windows.\n if (path === '' || /^[\\\\/]/.test(path) || path.split(/[\\\\/]/).includes('..') || /^[A-Za-z]:/.test(path)) {\n problems.push({ line: block.line, message: `a probe-file names \"${path}\"; it must be a path inside the repository` });\n continue;\n }\n files.push({ path, content: block.content, line: block.line });\n hash.update(`probe-file ${path}\\n${block.content}\\n`);\n continue;\n }\n if (word !== 'probe') continue;\n hash.update(`probe\\n${block.content}\\n`);\n const fields = new Map<string, string>();\n let bad = false;\n for (const raw of block.content.split(/\\r?\\n/)) {\n const line = withoutComment(raw).trim();\n if (line === '' || line.startsWith('#')) continue;\n const colon = line.indexOf(':');\n const key = colon < 0 ? '' : line.slice(0, colon).trim();\n if (!KEYS.includes(key)) {\n problems.push({ line: block.line, message: `a probe says \"${line}\"; its keys are ${KEYS.join(', ')}` });\n bad = true;\n continue;\n }\n fields.set(key, unquote(line.slice(colon + 1)));\n }\n if (bad) continue;\n const id = fields.get('id') ?? '';\n const run = fields.get('run') ?? '';\n if (id === '' || run === '') {\n problems.push({ line: block.line, message: 'a probe needs an id and a run command' });\n continue;\n }\n const signature = fields.get('signature') || null;\n const test = fields.get('test') || null;\n const junit = fields.get('junit') || null;\n if (signature === null && test === null) {\n problems.push({\n line: block.line,\n message: `probe ${id} does not say how it fails; give a signature, or a JUnit test name, so that a failure to compile is not taken for the defect`,\n });\n continue;\n }\n if (test !== null && junit === null) {\n problems.push({ line: block.line, message: `probe ${id} names a test but no junit report to find it in` });\n continue;\n }\n const count = (key: string): number | null | string => {\n const value = fields.get(key);\n if (value === undefined) return null;\n const n = Number(value);\n return Number.isInteger(n) && n >= 1 ? n : `probe ${id}: \"${key}\" must be a whole number of at least 1`;\n };\n const runs = count('runs');\n const timeout = count('timeout');\n if (typeof runs === 'string' || typeof timeout === 'string') {\n problems.push({ line: block.line, message: (typeof runs === 'string' ? runs : timeout) as string });\n continue;\n }\n if (probes.some((probe) => probe.id === id)) {\n problems.push({ line: block.line, message: `probe ${id} is declared twice` });\n continue;\n }\n probes.push({ id, run, setup: fields.get('setup') || null, signature, junit, test, runs, timeout, line: block.line });\n }\n return { probes, files, problems, hash: `sha256-${hash.digest('hex')}` };\n}\n\n/** What one run of a probe did. `exitCode` is `null` when it was stopped at its timeout. */\nexport interface ProbeRun {\n readonly exitCode: number | null;\n readonly output: string;\n /** The report when the probe names one: read, unreadable, or `null` when the file was not written. */\n readonly junit: JUnitRead | null;\n}\n\nexport type RunOutcome = 'red' | 'green' | 'wrong-failure' | 'timeout' | 'no-report';\n\nexport interface Classified {\n readonly outcome: RunOutcome;\n /** The evidence: the line or the message that matched, or what was missing. */\n readonly detail: string;\n /**\n * The end of what the command printed, as {@link endOfOutput} gives it, on\n * a run that proves nothing: it failed for another reason, was stopped at\n * its timeout, or left no report that can be read. `detail` says only that\n * the reason was not the probe's; the reason itself is the command's to\n * give, and this is where it gave it. A red or a green run has none.\n */\n readonly output?: string;\n}\n\n/**\n * npm's `yes` setting as an environment spells it. npm reads the name in any\n * case: `NPM_CONFIG_YES` and `Npm_Config_Yes` did what `npm_config_yes` does\n * under npm 10.9.9, 11.16.0, 11.20.0 and 12.2.0 (ADR-0007).\n */\nconst NPM_YES = /^npm_config_yes$/i;\n\n/**\n * The environment a probe's commands run in: the person's own, with `CI` set\n * as a pipeline sets it, and with npm told to fetch nothing for a command\n * unless the person's environment already says what npm may fetch.\n *\n * A probe's command runs where nothing is installed until its `setup`\n * installs it, with no terminal to be asked on. There `npx <name>` fetches\n * the registry's package of that name and runs it, unasked, and with\n * `npm_config_yes=false` it stops and names the package instead (ADR-0007).\n * A person who set the variable, in any case, has said what they want, and\n * it is left as they set it. Set and empty says nothing: npm fetched with it\n * as with no variable, so the setting is made. The empty one is taken out\n * first and not left beside it: on Windows a name is one variable whatever\n * its case, and of two spellings a command is handed one, the upper case\n * before the lower, which would be the empty one.\n */\nexport function probeEnvironment(own: Readonly<Record<string, string | undefined>>): Record<string, string | undefined> {\n const spelled = Object.keys(own).filter((name) => NPM_YES.test(name));\n if (spelled.some((name) => Boolean(own[name]))) return { ...own, CI: '1' };\n const rest = Object.fromEntries(Object.entries(own).filter(([name]) => !spelled.includes(name)));\n return { ...rest, CI: '1', npm_config_yes: 'false' };\n}\n\n/** How many characters of the end of a command's output a person is shown. */\nconst SHOWN = 2000;\n\n/**\n * The end of what a command printed, as a person is shown it: the last 2,000\n * characters, less what is there for a terminal to obey rather than for a\n * person to read. Written out as it came, an escape sequence in it moves the\n * cursor, clears lines already written or renames the window.\n *\n * - A carriage return ends a line as a line feed does, alone or in front of\n * one: alone it would have the next line written over the last.\n * - A colour is dropped. A command told to colour what it prints, as\n * `npm_config_color=always` tells npm, does so with nobody at a terminal\n * to see it, and a colour says nothing in a report.\n * - Every other control character but the line feed and the tab is written\n * as its escape, as JSON writes it, so that it shows and does nothing.\n *\n * Space at the end is dropped before the 2,000 are counted, so that blank\n * lines do not take the place of the reason above them. Blank lines at the\n * start of what is left are dropped after: `npm run` opens with one.\n */\nexport function endOfOutput(output: string): string {\n return output\n .replace(/\\r\\n?/g, '\\n')\n .replace(/\\u001b\\[[\\d;]*m/g, '')\n .replace(/[^\\P{Cc}\\n\\t]/gu, (control) => `\\\\u${control.charCodeAt(0).toString(16).padStart(4, '0')}`)\n .trimEnd()\n .slice(-SHOWN)\n .replace(/^\\n+/, '');\n}\n\nfunction firstLineWith(text: string, needle: string): string {\n const line = text.split(/\\r?\\n/).find((candidate) => candidate.includes(needle));\n return (line ?? needle).trim().slice(0, 200);\n}\n\n/**\n * What one run showed, against what the probe declares, with the end of the\n * command's output on a run that is neither red nor green.\n */\nexport function classify(spec: ProbeSpec, run: ProbeRun): Classified {\n const judged = judge(spec, run);\n return judged.outcome === 'red' || judged.outcome === 'green' ? judged : { ...judged, output: endOfOutput(run.output) };\n}\n\nfunction judge(spec: ProbeSpec, run: ProbeRun): Classified {\n if (run.exitCode === null) return { outcome: 'timeout', detail: `stopped after ${spec.timeout ?? 'the configured'} seconds` };\n if (spec.junit !== null) {\n if (run.junit === null) {\n return run.exitCode === 0\n ? { outcome: 'no-report', detail: `the command passed and wrote no ${spec.junit}` }\n : { outcome: 'wrong-failure', detail: `the command failed before writing ${spec.junit}` };\n }\n if (!run.junit.ok) return { outcome: 'no-report', detail: `${spec.junit} cannot be read: ${run.junit.error}` };\n const failing = run.junit.cases.filter((c) => c.outcome === 'failed' || c.outcome === 'errored');\n const matching = failing.filter(\n (c) =>\n (spec.test === null || `${c.classname} ${c.name}`.includes(spec.test)) &&\n (spec.signature === null || c.message.includes(spec.signature) || c.name.includes(spec.signature)),\n );\n const first = matching[0];\n if (first !== undefined) {\n return { outcome: 'red', detail: `${first.name} failed${spec.signature === null ? '' : `: ${firstLineWith(first.message, spec.signature)}`}` };\n }\n if (failing.length === 0 && run.exitCode === 0) return { outcome: 'green', detail: 'every test passed' };\n const other = failing[0];\n return {\n outcome: 'wrong-failure',\n detail: other === undefined ? `the command exited ${run.exitCode} with no failing test` : `${other.name} failed, which is not what the probe declares`,\n };\n }\n if (run.exitCode === 0) return { outcome: 'green', detail: 'the command passed' };\n const signature = spec.signature as string;\n return run.output.includes(signature)\n ? { outcome: 'red', detail: firstLineWith(run.output, signature) }\n : { outcome: 'wrong-failure', detail: `the command exited ${run.exitCode} and its output does not contain \"${signature}\"` };\n}\n\nexport type Verdict =\n /** Red at every run where red was expected: the defect is measured. */\n | 'measured'\n /** Green at every run where green was expected: the defect is gone. */\n | 'fixed'\n /** Green where red was expected: the probe finds no defect. */\n | 'vacuous'\n /** Red where green was expected: the round did not fix it. */\n | 'still-failing'\n /** Runs disagreed with each other. */\n | 'flaky'\n /** It failed for another reason, timed out, or wrote no report: it proves nothing. */\n | 'invalid';\n\n/** The verdict over every run of one probe, at the base (`red` expected) or the head (`green`). */\nexport function verdictOf(expected: 'red' | 'green', runs: readonly Classified[]): Verdict {\n const outcomes = new Set(runs.map((run) => run.outcome));\n if (outcomes.size > 1) return 'flaky';\n const only = runs[0]?.outcome;\n if (only === 'red') return expected === 'red' ? 'measured' : 'still-failing';\n if (only === 'green') return expected === 'green' ? 'fixed' : 'vacuous';\n return 'invalid';\n}\n\nexport interface ProbeResult {\n readonly probe: ProbeSpec;\n readonly at: string;\n readonly commit: string;\n readonly expected: 'red' | 'green';\n readonly runs: readonly Classified[];\n readonly verdict: Verdict;\n}\n\nfunction cell(text: string): string {\n return text.replace(/\\|/g, '\\\\|').replace(/\\r?\\n/g, ' ').trim();\n}\n\n/** The evidence a brief records: a table a reader can check, and what it was measured with. */\nexport function renderEvidence(results: readonly ProbeResult[], hash: string, date: string): string {\n const lines = ['| Probe | At | Runs | Verdict | Evidence |', '| --- | --- | ---: | --- | --- |'];\n for (const result of results) {\n const agreeing = result.runs.filter((run) => run.outcome === result.runs[0]?.outcome).length;\n lines.push(\n `| ${cell(result.probe.id)} | ${result.at} \\`${result.commit.slice(0, 12)}\\` | ${agreeing}/${result.runs.length} | ${result.verdict} | ${cell(result.runs[0]?.detail ?? '')} |`,\n );\n }\n lines.push('', `Measured ${date} by spec-harness with probes \\`${hash}\\`.`);\n return lines.join('\\n');\n}\n\n/**\n * What a person is told, beside the table, of each result with a run that\n * proves nothing: which run, what it showed, and the end of what its command\n * printed, where the reason is. The first such run of a result speaks for the\n * rest, as the first run's evidence fills the table's cell. A result whose\n * runs are all red or green has nothing to add to its verdict.\n *\n * It is for the standard error: the table is what a brief records and what a\n * script reads, and a command's output is no part of either.\n */\nexport function renderUnexplained(results: readonly ProbeResult[]): string {\n return results\n .map((result) => {\n const at = result.runs.findIndex((run) => run.output !== undefined);\n const run = result.runs[at];\n if (run?.output === undefined) return '';\n const printed = run.output === '' ? 'it printed nothing' : `its output ended:\\n${run.output}`;\n return `spec-harness: probe ${result.probe.id} is ${result.verdict} at ${result.at}: run ${at + 1} of ${result.runs.length}: ${run.detail}; ${printed}\\n`;\n })\n .join('');\n}\n"]}
|
package/dist/reader.js
CHANGED
|
@@ -6,6 +6,8 @@ import { findEntry, readFrontMatter, scanMarkdown } from './vendor/spec-core/mar
|
|
|
6
6
|
import { splitLines } from './vendor/spec-core/text/index.js';
|
|
7
7
|
/** Section names compare without case, emphasis, a leading number or a trailing colon. */
|
|
8
8
|
export function sameSection(heading, name) {
|
|
9
|
+
// Both names are folded the same way and then compared, so the mutant that
|
|
10
|
+
// folds them to upper case is equivalent.
|
|
9
11
|
const normal = (text) => text
|
|
10
12
|
.replace(/[*_`]/g, '')
|
|
11
13
|
.replace(/^\s*\d+[.)]?\s+/, '')
|
|
@@ -36,6 +38,8 @@ function scanOf(text, cache) {
|
|
|
36
38
|
}
|
|
37
39
|
export function createReader() {
|
|
38
40
|
// Commands read the same brief several times in a row; one scan serves them.
|
|
41
|
+
// It spares a scan and changes no answer, so the mutant that keeps nothing
|
|
42
|
+
// is equivalent.
|
|
39
43
|
let last = null;
|
|
40
44
|
const scan = (text) => {
|
|
41
45
|
const result = scanOf(text, last);
|
package/dist/reader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The document reader, on spec-core's Markdown scanner: nothing inside code\n * or a comment is ever taken for a section, a table, a link or a probe.\n */\n\nimport type { Citation, DocumentReader, SectionTables, UnreadableFrontMatterLine } from './document.js';\nimport type { CodeBlock } from './probe.js';\nimport type { TableView } from './rulings.js';\nimport { findEntry, readFrontMatter, scanMarkdown, type FrontMatter, type MarkdownScan } from './vendor/spec-core/markdown/index.js';\nimport { splitLines } from './vendor/spec-core/text/index.js';\n\n/** Section names compare without case, emphasis, a leading number or a trailing colon. */\nexport function sameSection(heading: string, name: string): boolean {\n // Both names are folded the same way and then compared, so the mutant that\n // folds them to upper case is equivalent.\n const normal = (text: string): string =>\n text\n .replace(/[*_`]/g, '')\n .replace(/^\\s*\\d+[.)]?\\s+/, '')\n .replace(/[:\\s]+$/, '')\n .trim()\n .toLowerCase();\n return normal(heading) === normal(name);\n}\n\n/**\n * The lines of closed YAML front matter spec-core's reader passes over, since\n * none is `key: value`, each with its reason. The reader reports a problem on\n * a line it read too - a key declared twice - and one on the opening line for\n * TOML front matter; neither is a line passed over, and only these are. A\n * byte-order mark, which the reader leaves out, sits on the opening line,\n * never one of these, so the text's own lines give each as written.\n */\nfunction unreadLines(text: string, front: FrontMatter | null): UnreadableFrontMatterLine[] {\n if (front === null) return [];\n const read = new Set(front.entries.map((entry) => entry.line));\n const lines = splitLines(text);\n return front.problems\n .filter((problem) => problem.line > 0 && !read.has(problem.line))\n .map((problem) => ({ line: problem.line + 1, text: lines[problem.line] as string, reason: problem.message }));\n}\n\nfunction scanOf(text: string, cache: { text: string; scan: MarkdownScan } | null): MarkdownScan {\n return cache !== null && cache.text === text ? cache.scan : scanMarkdown(text);\n}\n\nexport function createReader(): DocumentReader {\n // Commands read the same brief several times in a row; one scan serves them.\n // It spares a scan and changes no answer, so the mutant that keeps nothing\n // is equivalent.\n let last: { text: string; scan: MarkdownScan } | null = null;\n const scan = (text: string): MarkdownScan => {\n const result = scanOf(text, last);\n last = { text, scan: result };\n return result;\n };\n\n return {\n codeBlocks(text: string): CodeBlock[] {\n const scanned = scan(text);\n const spaces = (line: string): number => (/^ */.exec(line) as RegExpExecArray)[0].length;\n return scanned.blocks\n .filter((block) => block.kind === 'fenced')\n .map((block) => {\n const first = block.line + 1;\n const last = block.closed ? block.endLine - 1 : block.endLine;\n // A fence in a block quote or a list item: its content is the code\n // without the quote markers and without the fence's own indent, as\n // CommonMark reads it, so a probe file is written as it was meant.\n const indent = spaces(scanned.lines[block.line - 1]?.content ?? '');\n const lines: string[] = [];\n for (let line = first; line <= last; line += 1) {\n const content = scanned.lines[line - 1]?.content ?? '';\n lines.push(content.slice(Math.min(indent, spaces(content))));\n }\n return { info: block.info, content: lines.join('\\n'), line: block.line };\n });\n },\n\n sectionAt(text: string, line: number): string | null {\n let found: string | null = null;\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level >= 2) found = heading.text;\n }\n return found;\n },\n\n sectionsAt(text: string, line: number): string[] {\n const open: { level: number; text: string }[] = [];\n for (const heading of scan(text).headings) {\n if (heading.line > line) break;\n if (heading.level < 2) continue;\n while (open.length > 0 && (open[open.length - 1] as { level: number }).level >= heading.level) open.pop();\n open.push({ level: heading.level, text: heading.text });\n }\n return open.map((section) => section.text);\n },\n\n sectionTables(text: string, section: string): SectionTables {\n const scanned = scan(text);\n const headings = scanned.headings;\n const index = headings.findIndex((heading) => heading.level >= 2 && sameSection(heading.text, section));\n if (index < 0) return { tables: [], headingLine: null, tableEndLine: null };\n const heading = headings[index] as (typeof headings)[number];\n const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);\n const endLine = next === undefined ? scanned.index.lineCount + 1 : next.line;\n const tables = scanned.tables.filter((table) => table.line > heading.endLine && table.line < endLine);\n const views: TableView[] = tables.map((table) => ({\n headers: table.headers.map((cell) => cell.text),\n rows: table.rows.map((row) => ({ cells: row.cells.map((cell) => cell.text), line: row.line })),\n }));\n const lastTable = tables[tables.length - 1];\n return { tables: views, headingLine: heading.endLine, tableEndLine: lastTable === undefined ? null : lastTable.endLine };\n },\n\n citations(text: string): Citation[] {\n return scan(text)\n .links.filter((link) => !link.image && link.form !== 'autolink' && link.form !== 'definition')\n .map((link) => ({ target: link.target, line: link.line }));\n },\n\n titleAndStatus(text: string): {\n title: string | null;\n status: string | null;\n unclosedFrontMatter: boolean;\n unreadableFrontMatter: UnreadableFrontMatterLine[];\n } {\n const scanned = scan(text);\n const title = scanned.headings.find((heading) => heading.level === 1)?.text ?? null;\n const front = readFrontMatter(text);\n let status: string | null = null;\n // The status is read under `status` alone: a key in another language,\n // such as `狀態`, is another key (ADR-0001).\n const entry = findEntry(front, 'status');\n if (entry !== undefined && entry.value.kind === 'scalar') status = entry.value.scalar.text || null;\n // TOML front matter gives no status, closed or not, so closing it would\n // change nothing the context packet shows; only YAML's is worth saying.\n return {\n title,\n status,\n unclosedFrontMatter: scanned.unclosedFrontMatter?.kind === 'yaml',\n unreadableFrontMatter: unreadLines(text, front),\n };\n },\n };\n}\n"]}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -8,8 +8,27 @@
|
|
|
8
8
|
* succeeded, threw, or the process was interrupted (ADR-0003). The person's
|
|
9
9
|
* branch, index, stash and working files are never touched; a failed round is
|
|
10
10
|
* discarded by discarding its worktree, never by resetting theirs.
|
|
11
|
+
*
|
|
12
|
+
* Git forgets a worktree whatever becomes of its directory. A directory that
|
|
13
|
+
* cannot be deleted, as on Windows while something a command started still
|
|
14
|
+
* runs in it, costs the job nothing: its answer stands, the directory is
|
|
15
|
+
* tried again as the process exits, and one still there then is named on the
|
|
16
|
+
* standard error.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Runs `work` in a detached worktree of `repository` at `revision`, and
|
|
20
|
+
* removes the worktree after.
|
|
21
|
+
*
|
|
22
|
+
* Git removes it first, while it is as the job left it: git then forgets the
|
|
23
|
+
* worktree whether or not all of its directory can be deleted. What git left
|
|
24
|
+
* of the directory is deleted next, which is all of it when the job broke the
|
|
25
|
+
* worktree, since git refuses one whose `.git` file is gone. Only then is
|
|
26
|
+
* git asked again, once the directory is gone, when it forgets what it finds
|
|
27
|
+
* missing (`removeWorktree`).
|
|
28
|
+
*
|
|
29
|
+
* A directory that cannot be deleted stays on the list for the exit to try
|
|
30
|
+
* again, and does not take the job's answer with it.
|
|
11
31
|
*/
|
|
12
|
-
/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */
|
|
13
32
|
export declare function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T>;
|
|
14
33
|
export interface CommandRun {
|
|
15
34
|
/** `null` when the command was stopped at its timeout. */
|
|
@@ -26,5 +45,10 @@ export interface CommandRun {
|
|
|
26
45
|
* repository's own brief - `npm test -- x` - which the person approved with
|
|
27
46
|
* the brief, exactly as CI runs the repository's own scripts. Nothing from
|
|
28
47
|
* outside the repository reaches it.
|
|
48
|
+
*
|
|
49
|
+
* The line is run as it is written. Its environment is the person's own as
|
|
50
|
+
* `probeEnvironment` hands it on: `CI` set, and npm told to fetch nothing for
|
|
51
|
+
* a command where neither that environment nor the line says what it may
|
|
52
|
+
* fetch (ADR-0007).
|
|
29
53
|
*/
|
|
30
54
|
export declare function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun>;
|
package/dist/sandbox.js
CHANGED
|
@@ -8,12 +8,19 @@
|
|
|
8
8
|
* succeeded, threw, or the process was interrupted (ADR-0003). The person's
|
|
9
9
|
* branch, index, stash and working files are never touched; a failed round is
|
|
10
10
|
* discarded by discarding its worktree, never by resetting theirs.
|
|
11
|
+
*
|
|
12
|
+
* Git forgets a worktree whatever becomes of its directory. A directory that
|
|
13
|
+
* cannot be deleted, as on Windows while something a command started still
|
|
14
|
+
* runs in it, costs the job nothing: its answer stands, the directory is
|
|
15
|
+
* tried again as the process exits, and one still there then is named on the
|
|
16
|
+
* standard error.
|
|
11
17
|
*/
|
|
12
18
|
import { execFileSync, spawn } from 'node:child_process';
|
|
13
19
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
14
20
|
import { constants, tmpdir } from 'node:os';
|
|
15
21
|
import { join } from 'node:path';
|
|
16
22
|
import { addWorktree, removeWorktree } from './git.js';
|
|
23
|
+
import { probeEnvironment } from './probe.js';
|
|
17
24
|
const live = new Map();
|
|
18
25
|
const running = new Set();
|
|
19
26
|
let installed = false;
|
|
@@ -26,10 +33,36 @@ let ending = false;
|
|
|
26
33
|
* left the tree, and no wait would end it.
|
|
27
34
|
*/
|
|
28
35
|
const SETTLE_MS = 3_000;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a process has this id now. Signal 0 asks and sends nothing, and any
|
|
38
|
+
* answer but "no such process" says one has it: another user's process
|
|
39
|
+
* answers that it may not be signalled.
|
|
40
|
+
*/
|
|
41
|
+
function taken(pid) {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return error.code !== 'ESRCH';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
29
50
|
/**
|
|
30
51
|
* Stops a command with everything it started, forced: SIGKILL to its process
|
|
31
52
|
* group on Linux and macOS, taskkill over its tree on Windows.
|
|
32
53
|
*
|
|
54
|
+
* Both name the command by its id, and the id is the command's only while
|
|
55
|
+
* something holds it (ADR-0003). Node holds the command's shell until it has
|
|
56
|
+
* seen the shell end, and until then the system gives the id to nothing else.
|
|
57
|
+
* After that the id is free on Windows, where a process started seconds later
|
|
58
|
+
* can be given it: taskkill would stop that process with all it started, and
|
|
59
|
+
* could not have found what the command left anyway, since it finds no tree
|
|
60
|
+
* under a root that has ended. So a command whose shell has ended is not
|
|
61
|
+
* stopped there. On Linux and macOS the id names the command's process group
|
|
62
|
+
* as well, and stays out of use while the group has a member: what the shell
|
|
63
|
+
* left in its group is still stopped, unless a process has the id now, which
|
|
64
|
+
* says the group emptied and the id was given out again.
|
|
65
|
+
*
|
|
33
66
|
* `windowsHide` here and below only keeps a console window from opening on
|
|
34
67
|
* Windows, which nothing reads, so its mutants are equivalent.
|
|
35
68
|
*/
|
|
@@ -40,6 +73,15 @@ function stop(child) {
|
|
|
40
73
|
// tick nothing here stops a command.
|
|
41
74
|
if (child.pid === undefined)
|
|
42
75
|
return;
|
|
76
|
+
// Node sets one of the two when it sees the shell end, and lets go of the
|
|
77
|
+
// shell then. Windows reports no signal: there the second is set only once
|
|
78
|
+
// the fallback below has stopped the shell, on the path no test takes.
|
|
79
|
+
const held = child.exitCode === null && child.signalCode === null;
|
|
80
|
+
// Windows is not asked whether a process has the id: one could be given it
|
|
81
|
+
// between the answer and taskkill. Elsewhere `win32` is false already, so
|
|
82
|
+
// the mutants that make it false are equivalent there.
|
|
83
|
+
if (!held && (process.platform === 'win32' || taken(child.pid)))
|
|
84
|
+
return;
|
|
43
85
|
try {
|
|
44
86
|
// False already on Linux and macOS, so the mutants that make it false
|
|
45
87
|
// are equivalent there. On Windows the tests fail without taskkill:
|
|
@@ -52,32 +94,35 @@ function stop(child) {
|
|
|
52
94
|
process.kill(-child.pid, 'SIGKILL');
|
|
53
95
|
}
|
|
54
96
|
catch {
|
|
55
|
-
// Reached when the group
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
97
|
+
// Reached when the group is already gone, and the command's shell with
|
|
98
|
+
// it, as when the command left something outside the group holding its
|
|
99
|
+
// output; on Windows when the shell has only just ended, which taskkill
|
|
100
|
+
// sees before Node does; and there when taskkill cannot be run, where
|
|
101
|
+
// stopping the shell is what is left. No test takes that last path.
|
|
102
|
+
// `kill` goes through Node's own hold on the shell, and does nothing
|
|
103
|
+
// once that is gone.
|
|
59
104
|
child.kill('SIGKILL');
|
|
60
105
|
}
|
|
61
106
|
}
|
|
62
107
|
/**
|
|
63
108
|
* Deletes a worktree's directory, trying again for up to `SETTLE_MS` while
|
|
64
|
-
* something holds it
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* deleted all the same.
|
|
109
|
+
* something holds it, and answers `null` once it is gone, or else why it is
|
|
110
|
+
* not. On Windows a command just stopped lets go of its directory a moment
|
|
111
|
+
* after taskkill returns, and an exit handler, which runs synchronously, has
|
|
112
|
+
* no other way to wait; elsewhere a directory in use is deleted all the same.
|
|
68
113
|
*/
|
|
69
114
|
function discard(directory) {
|
|
70
115
|
const deadline = Date.now() + SETTLE_MS;
|
|
71
116
|
for (;;) {
|
|
72
117
|
try {
|
|
73
118
|
rmSync(directory, { recursive: true, force: true });
|
|
74
|
-
return;
|
|
119
|
+
return null;
|
|
75
120
|
}
|
|
76
121
|
catch (error) {
|
|
77
122
|
// `>=` would differ only in the millisecond the deadline falls on, so
|
|
78
123
|
// that mutant is equivalent.
|
|
79
124
|
if (Date.now() > deadline)
|
|
80
|
-
|
|
125
|
+
return error;
|
|
81
126
|
// The pause only spares the processor between attempts: without it
|
|
82
127
|
// the directory is deleted, or given up on, at the same moment, so
|
|
83
128
|
// that mutant is equivalent.
|
|
@@ -85,28 +130,48 @@ function discard(directory) {
|
|
|
85
130
|
}
|
|
86
131
|
}
|
|
87
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Has git remove the worktree at `directory` with its record of it, as
|
|
135
|
+
* `removeWorktree` does and synchronously, and answers whether git did.
|
|
136
|
+
*/
|
|
137
|
+
function forget(directory, repository) {
|
|
138
|
+
try {
|
|
139
|
+
execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });
|
|
140
|
+
// Asked again about a worktree it has forgotten, git changes nothing, so
|
|
141
|
+
// the mutant that answers false here is equivalent.
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Git refused a worktree its job broke, could not delete all of the
|
|
146
|
+
// directory, holds nothing of it any more, or cannot be run. The answer
|
|
147
|
+
// is read with `!` and nowhere else, so the mutant that answers nothing
|
|
148
|
+
// here is equivalent.
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
88
152
|
/**
|
|
89
153
|
* Stops every command still running, then removes every live worktree:
|
|
90
154
|
* synchronously, the only kind of work an exit handler may do, so at exit
|
|
91
|
-
* nothing waits for the commands to end but `discard`'s retries.
|
|
155
|
+
* nothing waits for the commands to end but `discard`'s retries. A directory
|
|
156
|
+
* that outlasts those is named on the standard error and left: nothing
|
|
157
|
+
* thrown here could be caught, and the next worktree is still to remove.
|
|
92
158
|
*/
|
|
93
159
|
function sweep() {
|
|
94
160
|
for (const child of running)
|
|
95
161
|
stop(child);
|
|
96
162
|
for (const [directory, repository] of live) {
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
discard(directory);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// be done from an exit handler.
|
|
106
|
-
}
|
|
163
|
+
// In the order a job's end takes, for the reasons `withWorktree` gives.
|
|
164
|
+
const forgotten = forget(directory, repository);
|
|
165
|
+
const kept = discard(directory);
|
|
166
|
+
if (kept !== null)
|
|
167
|
+
process.stderr.write(`spec-harness: the temporary worktree at ${directory} could not be deleted and is left there: ${kept.message}\n`);
|
|
168
|
+
// The mutant that always asks again is equivalent, as in `forget`.
|
|
169
|
+
else if (!forgotten)
|
|
170
|
+
forget(directory, repository);
|
|
107
171
|
}
|
|
108
|
-
// A signal handler ends in an exit, which sweeps again over worktrees
|
|
109
|
-
// already gone and forgotten,
|
|
172
|
+
// A signal handler ends in an exit, which sweeps again: over worktrees
|
|
173
|
+
// already gone and forgotten, where keeping them would change nothing, and
|
|
174
|
+
// over one that could not be deleted, which would be named a second time.
|
|
110
175
|
live.clear();
|
|
111
176
|
}
|
|
112
177
|
/**
|
|
@@ -137,10 +202,29 @@ function install() {
|
|
|
137
202
|
// meet Node's default, which ends the process before any 'exit' handler
|
|
138
203
|
// runs, with the worktrees still in place. It runs the same steps again,
|
|
139
204
|
// and the first to finish exits.
|
|
140
|
-
|
|
205
|
+
//
|
|
206
|
+
// SIGBREAK is Windows' own: Ctrl+Break, which GitHub's runner sends there,
|
|
207
|
+
// after Ctrl+C, where it sends SIGTERM elsewhere. Unheard, it ends the
|
|
208
|
+
// process before any 'exit' handler runs, and the commands run on.
|
|
209
|
+
// Elsewhere there is no such signal and nothing calls its listener, so
|
|
210
|
+
// its mutants are equivalent there.
|
|
211
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'])
|
|
141
212
|
process.on(signal, interrupted);
|
|
142
213
|
}
|
|
143
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Runs `work` in a detached worktree of `repository` at `revision`, and
|
|
216
|
+
* removes the worktree after.
|
|
217
|
+
*
|
|
218
|
+
* Git removes it first, while it is as the job left it: git then forgets the
|
|
219
|
+
* worktree whether or not all of its directory can be deleted. What git left
|
|
220
|
+
* of the directory is deleted next, which is all of it when the job broke the
|
|
221
|
+
* worktree, since git refuses one whose `.git` file is gone. Only then is
|
|
222
|
+
* git asked again, once the directory is gone, when it forgets what it finds
|
|
223
|
+
* missing (`removeWorktree`).
|
|
224
|
+
*
|
|
225
|
+
* A directory that cannot be deleted stays on the list for the exit to try
|
|
226
|
+
* again, and does not take the job's answer with it.
|
|
227
|
+
*/
|
|
144
228
|
export async function withWorktree(repository, revision, work) {
|
|
145
229
|
install();
|
|
146
230
|
const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));
|
|
@@ -150,9 +234,14 @@ export async function withWorktree(repository, revision, work) {
|
|
|
150
234
|
return await work(directory);
|
|
151
235
|
}
|
|
152
236
|
finally {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
237
|
+
const forgotten = await removeWorktree(directory, repository);
|
|
238
|
+
if (discard(directory) === null) {
|
|
239
|
+
// Asked again about a worktree it has forgotten, git changes nothing,
|
|
240
|
+
// so the mutant that always asks again is equivalent.
|
|
241
|
+
if (!forgotten)
|
|
242
|
+
await removeWorktree(directory, repository);
|
|
243
|
+
live.delete(directory);
|
|
244
|
+
}
|
|
156
245
|
}
|
|
157
246
|
}
|
|
158
247
|
/**
|
|
@@ -165,6 +254,11 @@ export async function withWorktree(repository, revision, work) {
|
|
|
165
254
|
* repository's own brief - `npm test -- x` - which the person approved with
|
|
166
255
|
* the brief, exactly as CI runs the repository's own scripts. Nothing from
|
|
167
256
|
* outside the repository reaches it.
|
|
257
|
+
*
|
|
258
|
+
* The line is run as it is written. Its environment is the person's own as
|
|
259
|
+
* `probeEnvironment` hands it on: `CI` set, and npm told to fetch nothing for
|
|
260
|
+
* a command where neither that environment nor the line says what it may
|
|
261
|
+
* fetch (ADR-0007).
|
|
168
262
|
*/
|
|
169
263
|
export function runCommand(line, cwd, timeoutSeconds) {
|
|
170
264
|
install();
|
|
@@ -175,7 +269,18 @@ export function runCommand(line, cwd, timeoutSeconds) {
|
|
|
175
269
|
// tests catch on Windows. Everywhere else `detached` is true already, so
|
|
176
270
|
// the mutants that make it true are equivalent there, as are
|
|
177
271
|
// `windowsHide`'s.
|
|
178
|
-
|
|
272
|
+
//
|
|
273
|
+
// It is given no input: nobody is there to type any, and a pipe that is
|
|
274
|
+
// never written to and never closed kept a command that reads its input
|
|
275
|
+
// waiting until the timeout stopped it.
|
|
276
|
+
const child = spawn(line, {
|
|
277
|
+
cwd,
|
|
278
|
+
shell: true,
|
|
279
|
+
windowsHide: true,
|
|
280
|
+
detached: process.platform !== 'win32',
|
|
281
|
+
env: probeEnvironment(process.env),
|
|
282
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
283
|
+
});
|
|
179
284
|
running.add(child);
|
|
180
285
|
let output = '';
|
|
181
286
|
const keep = (chunk) => {
|
package/dist/sandbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Temporary worktrees: somewhere to run a command at a commit without\n * touching the person's own checkout.\n *\n * The family's tools read git and never write it (spec-core ADR-0005). The one\n * exception is this: a detached worktree under the system's temporary\n * directory, added for one job and removed after it, whether the job\n * succeeded, threw, or the process was interrupted (ADR-0003). The person's\n * branch, index, stash and working files are never touched; a failed round is\n * discarded by discarding its worktree, never by resetting theirs.\n */\n\nimport { execFileSync, spawn, type ChildProcess } from 'node:child_process';\nimport { mkdtempSync, rmSync } from 'node:fs';\nimport { constants, tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { addWorktree, removeWorktree } from './git.js';\n\nconst live = new Map<string, string>();\nconst running = new Set<ChildProcess>();\nlet installed = false;\n/** Set once the process is interrupted: from then on a command's end is never answered. */\nlet ending = false;\n\n/**\n * How long the sandbox waits for the commands it stopped to let go of their\n * output and their directory. They were stopped forced, so this is only the\n * time the system takes to end them; whatever still holds either after it has\n * left the tree, and no wait would end it.\n */\nconst SETTLE_MS = 3_000;\n\n/**\n * Stops a command with everything it started, forced: SIGKILL to its process\n * group on Linux and macOS, taskkill over its tree on Windows.\n *\n * `windowsHide` here and below only keeps a console window from opening on\n * Windows, which nothing reads, so its mutants are equivalent.\n */\nfunction stop(child: ChildProcess): void {\n // No pid means the spawn failed and nothing runs. Past the tick it failed\n // in, the kill below throws on the missing id and the fallback finds no\n // process, so the mutant that lets it through is equivalent; within that\n // tick nothing here stops a command.\n if (child.pid === undefined) return;\n try {\n // False already on Linux and macOS, so the mutants that make it false\n // are equivalent there. On Windows the tests fail without taskkill:\n // stopping the shell alone leaves node holding the output. True already\n // on Windows, so the mutant that makes it true is equivalent there;\n // elsewhere there is no taskkill, and the shell alone is stopped.\n if (process.platform === 'win32') execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });\n else process.kill(-child.pid, 'SIGKILL');\n } catch {\n // Reached when the group or the tree is already gone, and the command's\n // shell with it, as when the command left something holding its output;\n // and on Windows when taskkill cannot be run, where stopping the shell is\n // what is left. No test takes that second path.\n child.kill('SIGKILL');\n }\n}\n\n/**\n * Deletes a worktree's directory, trying again for up to `SETTLE_MS` while\n * something holds it. On Windows a command just stopped lets go of its\n * directory a moment after taskkill returns, and an exit handler, which runs\n * synchronously, has no other way to wait; elsewhere a directory in use is\n * deleted all the same.\n */\nfunction discard(directory: string): void {\n const deadline = Date.now() + SETTLE_MS;\n for (;;) {\n try {\n rmSync(directory, { recursive: true, force: true });\n return;\n } catch (error) {\n // `>=` would differ only in the millisecond the deadline falls on, so\n // that mutant is equivalent.\n if (Date.now() > deadline) throw error;\n // The pause only spares the processor between attempts: without it\n // the directory is deleted, or given up on, at the same moment, so\n // that mutant is equivalent.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n }\n}\n\n/**\n * Stops every command still running, then removes every live worktree:\n * synchronously, the only kind of work an exit handler may do, so at exit\n * nothing waits for the commands to end but `discard`'s retries.\n */\nfunction sweep(): void {\n for (const child of running) stop(child);\n for (const [directory, repository] of live) {\n // Deleted first, as at the end of a job, so that git forgets it however\n // the job left it (`removeWorktree` says why).\n discard(directory);\n try {\n execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });\n } catch {\n // Git holds nothing of it any more, or cannot be run; nothing more can\n // be done from an exit handler.\n }\n }\n // A signal handler ends in an exit, which sweeps again over worktrees\n // already gone and forgotten, so a mutant that keeps them changes nothing.\n live.clear();\n}\n\n/**\n * Stops every running command with its tree, waits a bounded time for them to\n * let go of their output, removes the worktrees, and exits as a shell reports\n * a process the signal ended: 128 and the signal's number, the same on every\n * platform Node runs on. The commands lead process groups of their own on\n * Linux and macOS, so the terminal's signal never reached them.\n */\nasync function interrupted(signal: NodeJS.Signals): Promise<void> {\n ending = true;\n const stopping = [...running];\n for (const child of stopping) stop(child);\n await Promise.race([\n Promise.all(stopping.map((child) => new Promise((resolve) => child.once('close', resolve)))),\n new Promise((resolve) => setTimeout(resolve, SETTLE_MS)),\n ]);\n sweep();\n process.exit(128 + constants.signals[signal]);\n}\n\nfunction install(): void {\n if (installed) return;\n installed = true;\n process.once('exit', sweep);\n // `on`, not `once`: a second signal while the first waits would otherwise\n // meet Node's default, which ends the process before any 'exit' handler\n // runs, with the worktrees still in place. It runs the same steps again,\n // and the first to finish exits.\n for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) process.on(signal, interrupted);\n}\n\n/** Runs `work` in a detached worktree of `repository` at `revision`, and removes the worktree after. */\nexport async function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T> {\n install();\n const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));\n live.set(directory, repository);\n try {\n await addWorktree(directory, revision, repository);\n return await work(directory);\n } finally {\n discard(directory);\n await removeWorktree(directory, repository);\n live.delete(directory);\n }\n}\n\nexport interface CommandRun {\n /** `null` when the command was stopped at its timeout. */\n readonly exitCode: number | null;\n readonly output: string;\n}\n\n/**\n * Runs a command line in a directory, output and errors interleaved, stopped\n * with everything it started when it outlives `timeoutSeconds` or the process\n * is interrupted or exits. Once the process is interrupted it never answers:\n * the job waiting on it would otherwise carry on in a worktree being removed.\n *\n * Through the shell, on purpose: a probe's command is a line from the\n * repository's own brief - `npm test -- x` - which the person approved with\n * the brief, exactly as CI runs the repository's own scripts. Nothing from\n * outside the repository reaches it.\n */\nexport function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun> {\n install();\n return new Promise((resolve) => {\n // Elsewhere the command leads a process group of its own, so the timeout\n // can stop the group. Windows has none to signal, and taskkill follows\n // the tree instead; a command detached there loses its output, which the\n // tests catch on Windows. Everywhere else `detached` is true already, so\n // the mutants that make it true are equivalent there, as are\n // `windowsHide`'s.\n const child = spawn(line, { cwd, shell: true, windowsHide: true, detached: process.platform !== 'win32', env: { ...process.env, CI: '1' } });\n running.add(child);\n let output = '';\n const keep = (chunk: Buffer): void => {\n // Enough to find a signature in; a runaway log is not kept whole.\n if (output.length < 4 * 1024 * 1024) output += chunk.toString('utf8');\n };\n // Both streams are piped, so both exist: `?.` only satisfies the type,\n // and its mutants are equivalent.\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n let stopped = false;\n const timer = setTimeout(() => {\n stopped = true;\n stop(child);\n }, timeoutSeconds * 1000);\n child.on('error', (error) => {\n // 'close' follows the error of a spawn that failed, and clears the\n // timer too, so the mutant that drops this call is equivalent.\n clearTimeout(timer);\n resolve({ exitCode: 127, output: `${output}${error.message}\\n` });\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n running.delete(child);\n if (!ending) resolve({ exitCode: stopped ? null : (code ?? 1), output });\n });\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Temporary worktrees: somewhere to run a command at a commit without\n * touching the person's own checkout.\n *\n * The family's tools read git and never write it (spec-core ADR-0005). The one\n * exception is this: a detached worktree under the system's temporary\n * directory, added for one job and removed after it, whether the job\n * succeeded, threw, or the process was interrupted (ADR-0003). The person's\n * branch, index, stash and working files are never touched; a failed round is\n * discarded by discarding its worktree, never by resetting theirs.\n *\n * Git forgets a worktree whatever becomes of its directory. A directory that\n * cannot be deleted, as on Windows while something a command started still\n * runs in it, costs the job nothing: its answer stands, the directory is\n * tried again as the process exits, and one still there then is named on the\n * standard error.\n */\n\nimport { execFileSync, spawn, type ChildProcess } from 'node:child_process';\nimport { mkdtempSync, rmSync } from 'node:fs';\nimport { constants, tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { addWorktree, removeWorktree } from './git.js';\nimport { probeEnvironment } from './probe.js';\n\nconst live = new Map<string, string>();\nconst running = new Set<ChildProcess>();\nlet installed = false;\n/** Set once the process is interrupted: from then on a command's end is never answered. */\nlet ending = false;\n\n/**\n * How long the sandbox waits for the commands it stopped to let go of their\n * output and their directory. They were stopped forced, so this is only the\n * time the system takes to end them; whatever still holds either after it has\n * left the tree, and no wait would end it.\n */\nconst SETTLE_MS = 3_000;\n\n/**\n * Whether a process has this id now. Signal 0 asks and sends nothing, and any\n * answer but \"no such process\" says one has it: another user's process\n * answers that it may not be signalled.\n */\nfunction taken(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== 'ESRCH';\n }\n}\n\n/**\n * Stops a command with everything it started, forced: SIGKILL to its process\n * group on Linux and macOS, taskkill over its tree on Windows.\n *\n * Both name the command by its id, and the id is the command's only while\n * something holds it (ADR-0003). Node holds the command's shell until it has\n * seen the shell end, and until then the system gives the id to nothing else.\n * After that the id is free on Windows, where a process started seconds later\n * can be given it: taskkill would stop that process with all it started, and\n * could not have found what the command left anyway, since it finds no tree\n * under a root that has ended. So a command whose shell has ended is not\n * stopped there. On Linux and macOS the id names the command's process group\n * as well, and stays out of use while the group has a member: what the shell\n * left in its group is still stopped, unless a process has the id now, which\n * says the group emptied and the id was given out again.\n *\n * `windowsHide` here and below only keeps a console window from opening on\n * Windows, which nothing reads, so its mutants are equivalent.\n */\nfunction stop(child: ChildProcess): void {\n // No pid means the spawn failed and nothing runs. Past the tick it failed\n // in, the kill below throws on the missing id and the fallback finds no\n // process, so the mutant that lets it through is equivalent; within that\n // tick nothing here stops a command.\n if (child.pid === undefined) return;\n // Node sets one of the two when it sees the shell end, and lets go of the\n // shell then. Windows reports no signal: there the second is set only once\n // the fallback below has stopped the shell, on the path no test takes.\n const held = child.exitCode === null && child.signalCode === null;\n // Windows is not asked whether a process has the id: one could be given it\n // between the answer and taskkill. Elsewhere `win32` is false already, so\n // the mutants that make it false are equivalent there.\n if (!held && (process.platform === 'win32' || taken(child.pid))) return;\n try {\n // False already on Linux and macOS, so the mutants that make it false\n // are equivalent there. On Windows the tests fail without taskkill:\n // stopping the shell alone leaves node holding the output. True already\n // on Windows, so the mutant that makes it true is equivalent there;\n // elsewhere there is no taskkill, and the shell alone is stopped.\n if (process.platform === 'win32') execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });\n else process.kill(-child.pid, 'SIGKILL');\n } catch {\n // Reached when the group is already gone, and the command's shell with\n // it, as when the command left something outside the group holding its\n // output; on Windows when the shell has only just ended, which taskkill\n // sees before Node does; and there when taskkill cannot be run, where\n // stopping the shell is what is left. No test takes that last path.\n // `kill` goes through Node's own hold on the shell, and does nothing\n // once that is gone.\n child.kill('SIGKILL');\n }\n}\n\n/**\n * Deletes a worktree's directory, trying again for up to `SETTLE_MS` while\n * something holds it, and answers `null` once it is gone, or else why it is\n * not. On Windows a command just stopped lets go of its directory a moment\n * after taskkill returns, and an exit handler, which runs synchronously, has\n * no other way to wait; elsewhere a directory in use is deleted all the same.\n */\nfunction discard(directory: string): Error | null {\n const deadline = Date.now() + SETTLE_MS;\n for (;;) {\n try {\n rmSync(directory, { recursive: true, force: true });\n return null;\n } catch (error) {\n // `>=` would differ only in the millisecond the deadline falls on, so\n // that mutant is equivalent.\n if (Date.now() > deadline) return error as Error;\n // The pause only spares the processor between attempts: without it\n // the directory is deleted, or given up on, at the same moment, so\n // that mutant is equivalent.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n }\n}\n\n/**\n * Has git remove the worktree at `directory` with its record of it, as\n * `removeWorktree` does and synchronously, and answers whether git did.\n */\nfunction forget(directory: string, repository: string): boolean {\n try {\n execFileSync('git', ['worktree', 'remove', '--force', directory], { cwd: repository, stdio: 'ignore', windowsHide: true });\n // Asked again about a worktree it has forgotten, git changes nothing, so\n // the mutant that answers false here is equivalent.\n return true;\n } catch {\n // Git refused a worktree its job broke, could not delete all of the\n // directory, holds nothing of it any more, or cannot be run. The answer\n // is read with `!` and nowhere else, so the mutant that answers nothing\n // here is equivalent.\n return false;\n }\n}\n\n/**\n * Stops every command still running, then removes every live worktree:\n * synchronously, the only kind of work an exit handler may do, so at exit\n * nothing waits for the commands to end but `discard`'s retries. A directory\n * that outlasts those is named on the standard error and left: nothing\n * thrown here could be caught, and the next worktree is still to remove.\n */\nfunction sweep(): void {\n for (const child of running) stop(child);\n for (const [directory, repository] of live) {\n // In the order a job's end takes, for the reasons `withWorktree` gives.\n const forgotten = forget(directory, repository);\n const kept = discard(directory);\n if (kept !== null) process.stderr.write(`spec-harness: the temporary worktree at ${directory} could not be deleted and is left there: ${kept.message}\\n`);\n // The mutant that always asks again is equivalent, as in `forget`.\n else if (!forgotten) forget(directory, repository);\n }\n // A signal handler ends in an exit, which sweeps again: over worktrees\n // already gone and forgotten, where keeping them would change nothing, and\n // over one that could not be deleted, which would be named a second time.\n live.clear();\n}\n\n/**\n * Stops every running command with its tree, waits a bounded time for them to\n * let go of their output, removes the worktrees, and exits as a shell reports\n * a process the signal ended: 128 and the signal's number, the same on every\n * platform Node runs on. The commands lead process groups of their own on\n * Linux and macOS, so the terminal's signal never reached them.\n */\nasync function interrupted(signal: NodeJS.Signals): Promise<void> {\n ending = true;\n const stopping = [...running];\n for (const child of stopping) stop(child);\n await Promise.race([\n Promise.all(stopping.map((child) => new Promise((resolve) => child.once('close', resolve)))),\n new Promise((resolve) => setTimeout(resolve, SETTLE_MS)),\n ]);\n sweep();\n process.exit(128 + constants.signals[signal]);\n}\n\nfunction install(): void {\n if (installed) return;\n installed = true;\n process.once('exit', sweep);\n // `on`, not `once`: a second signal while the first waits would otherwise\n // meet Node's default, which ends the process before any 'exit' handler\n // runs, with the worktrees still in place. It runs the same steps again,\n // and the first to finish exits.\n //\n // SIGBREAK is Windows' own: Ctrl+Break, which GitHub's runner sends there,\n // after Ctrl+C, where it sends SIGTERM elsewhere. Unheard, it ends the\n // process before any 'exit' handler runs, and the commands run on.\n // Elsewhere there is no such signal and nothing calls its listener, so\n // its mutants are equivalent there.\n for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'] as const) process.on(signal, interrupted);\n}\n\n/**\n * Runs `work` in a detached worktree of `repository` at `revision`, and\n * removes the worktree after.\n *\n * Git removes it first, while it is as the job left it: git then forgets the\n * worktree whether or not all of its directory can be deleted. What git left\n * of the directory is deleted next, which is all of it when the job broke the\n * worktree, since git refuses one whose `.git` file is gone. Only then is\n * git asked again, once the directory is gone, when it forgets what it finds\n * missing (`removeWorktree`).\n *\n * A directory that cannot be deleted stays on the list for the exit to try\n * again, and does not take the job's answer with it.\n */\nexport async function withWorktree<T>(repository: string, revision: string, work: (directory: string) => Promise<T>): Promise<T> {\n install();\n const directory = mkdtempSync(join(tmpdir(), 'spec-harness-'));\n live.set(directory, repository);\n try {\n await addWorktree(directory, revision, repository);\n return await work(directory);\n } finally {\n const forgotten = await removeWorktree(directory, repository);\n if (discard(directory) === null) {\n // Asked again about a worktree it has forgotten, git changes nothing,\n // so the mutant that always asks again is equivalent.\n if (!forgotten) await removeWorktree(directory, repository);\n live.delete(directory);\n }\n }\n}\n\nexport interface CommandRun {\n /** `null` when the command was stopped at its timeout. */\n readonly exitCode: number | null;\n readonly output: string;\n}\n\n/**\n * Runs a command line in a directory, output and errors interleaved, stopped\n * with everything it started when it outlives `timeoutSeconds` or the process\n * is interrupted or exits. Once the process is interrupted it never answers:\n * the job waiting on it would otherwise carry on in a worktree being removed.\n *\n * Through the shell, on purpose: a probe's command is a line from the\n * repository's own brief - `npm test -- x` - which the person approved with\n * the brief, exactly as CI runs the repository's own scripts. Nothing from\n * outside the repository reaches it.\n *\n * The line is run as it is written. Its environment is the person's own as\n * `probeEnvironment` hands it on: `CI` set, and npm told to fetch nothing for\n * a command where neither that environment nor the line says what it may\n * fetch (ADR-0007).\n */\nexport function runCommand(line: string, cwd: string, timeoutSeconds: number): Promise<CommandRun> {\n install();\n return new Promise((resolve) => {\n // Elsewhere the command leads a process group of its own, so the timeout\n // can stop the group. Windows has none to signal, and taskkill follows\n // the tree instead; a command detached there loses its output, which the\n // tests catch on Windows. Everywhere else `detached` is true already, so\n // the mutants that make it true are equivalent there, as are\n // `windowsHide`'s.\n //\n // It is given no input: nobody is there to type any, and a pipe that is\n // never written to and never closed kept a command that reads its input\n // waiting until the timeout stopped it.\n const child = spawn(line, {\n cwd,\n shell: true,\n windowsHide: true,\n detached: process.platform !== 'win32',\n env: probeEnvironment(process.env),\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n running.add(child);\n let output = '';\n const keep = (chunk: Buffer): void => {\n // Enough to find a signature in; a runaway log is not kept whole.\n if (output.length < 4 * 1024 * 1024) output += chunk.toString('utf8');\n };\n // Both streams are piped, so both exist: `?.` only satisfies the type,\n // and its mutants are equivalent.\n child.stdout?.on('data', keep);\n child.stderr?.on('data', keep);\n let stopped = false;\n const timer = setTimeout(() => {\n stopped = true;\n stop(child);\n }, timeoutSeconds * 1000);\n child.on('error', (error) => {\n // 'close' follows the error of a spawn that failed, and clears the\n // timer too, so the mutant that drops this call is equivalent.\n clearTimeout(timer);\n resolve({ exitCode: 127, output: `${output}${error.message}\\n` });\n });\n child.on('close', (code) => {\n clearTimeout(timer);\n running.delete(child);\n if (!ending) resolve({ exitCode: stopped ? null : (code ?? 1), output });\n });\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descent-vtt/spec-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The agent-facing member of the spec-* tools: context for a round, a guard on every write, an audit when it ends, probes that prove a defect before it is filed, and rulings a person signs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@descent-vtt/spec-brief": "^0.
|
|
77
|
-
"@descent-vtt/spec-guard": "^0.
|
|
76
|
+
"@descent-vtt/spec-brief": "^0.5.1",
|
|
77
|
+
"@descent-vtt/spec-guard": "^0.19.1",
|
|
78
78
|
"@stryker-mutator/core": "^10.0.0",
|
|
79
79
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
80
80
|
"@types/node": "^22.20.4",
|