Batch 9 of 17 in the ad hoc timeout literal migration (epic #4445). Replaces every bare numeric timeout/timeoutMs object-literal property in 14 files with a named constant, per eslint-rules/no-adhoc-timeout-literal.cjs. Removes the 14 files from the rule's allowlist. The issue's own guess ("all run a scripts/*.cjs generator or lint script once, BUILD_TIMEOUT_MS class") needed two corrections found by reading every site directly. First, BUILD_TIMEOUT_MS's own doc comment scopes it specifically to scripts/build-hooks.js, which none of this batch's generator/lint-script sites run — a new shared constant, GENERATOR_SCRIPT_TIMEOUT_MS, covers the class instead. Second, no-pending-3212-markers.test.cjs's single site spawns `git ls-files` directly, not a scripts/*.cjs script at all — routed to a second new shared constant, REAL_REPO_GIT_TIMEOUT_MS, promoted once emitted-attribution.test.cjs's own git-plumbing sites were found sharing the same class and value. Isolated Standards-axis review caught a further misclassification: one of REAL_REPO_GIT_TIMEOUT_MS's three emitted-attribution.test.cjs sites actually builds a fresh throwaway temp repo (createTempDir + git init), contradicting that constant's own real-repo-tree-only scope. Fixed with a new file-local FRESH_FIXTURE_GIT_TIMEOUT_MS holding the exact pre-existing value under an honest name, rather than reusing the shared GIT_FIXTURE_TIMEOUT_MS (which would have doubled the bound). emitted-attribution.test.cjs also gets two more file-local constants: HEAVY_REAL_TREE_TEST_TIMEOUT_MS (node:test's own per-test timeout option, not a spawn bound) and BUILD_HOOKS_UNDER_LOAD_TIMEOUT_MS (the same build-hooks.js script as the shared norm, at 4x its bound inside the suite's heaviest test). emitted-ack-trailer.test.cjs gets IMPOSSIBLY_SHORT_GIT_TIMEOUT_MS — the one value in this migration that is deliberately tiny (20ms), used to force a timeout in a negative test, not generous headroom. No src/bin file touched, no numeric value changed anywhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
229 lines
8.7 KiB
JavaScript
229 lines
8.7 KiB
JavaScript
'use strict';
|
|
|
|
// docs-guard-exempt: this file only WRITES synthetic 'docs/...' fixtures into
|
|
// a throwaway temp git repo (writeFile()) to exercise scripts/lint-docs-command-form.cjs's
|
|
// own behavior — it never reads real shipped docs/ content.
|
|
|
|
/**
|
|
* TDD tests for scripts/lint-docs-command-form.cjs (#2903).
|
|
*
|
|
* Uses spawnSync to invoke the guard script against a temporary git repo so
|
|
* we can inject fixtures without touching the real repo. Mirrors the fixture
|
|
* pattern in tests/lint-legacy-dir-name.test.cjs.
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { runNode } = require('./helpers/process-seam.cjs');
|
|
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
|
const { GENERATOR_SCRIPT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
|
|
|
const GUARD_SCRIPT = path.resolve(__dirname, '..', 'scripts', 'lint-docs-command-form.cjs');
|
|
|
|
function createTempRepo() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lint-docs-command-form-test-'));
|
|
gitOrThrow(['init', '--initial-branch=main'], { cwd: dir });
|
|
gitOrThrow(['config', 'user.email', 'test@example.com'], { cwd: dir });
|
|
gitOrThrow(['config', 'user.name', 'Test'], { cwd: dir });
|
|
// Roster source: the guard reads commands/gsd/*.md filenames as valid
|
|
// command names, regardless of tracked/staged status.
|
|
writeFile(dir, 'commands/gsd/plan-phase.md', '# plan-phase\n');
|
|
return dir;
|
|
}
|
|
|
|
function writeFile(dir, relPath, content) {
|
|
const fullPath = path.join(dir, relPath);
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
fs.writeFileSync(fullPath, content, 'utf8');
|
|
}
|
|
|
|
function gitAdd(dir, relPath) {
|
|
gitOrThrow(['add', relPath], { cwd: dir });
|
|
}
|
|
|
|
function cleanup(dir) {
|
|
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- local cleanup in lint test; no helpers import available
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
|
|
/**
|
|
* Returns an object shaped like the raw spawnSync() result (status/stdout/
|
|
* stderr) because every call site in this file was written against that
|
|
* shape; the seam itself returns exitCode, not status, so it is mapped here.
|
|
* 30_000ms: matches the sibling-suite default for guard scripts that shell
|
|
* out to git (the guard runs `git ls-files` over the fixture repo), which
|
|
* is comparable workload to this script's own `git ls-files` invocation.
|
|
*/
|
|
function runGuard(cwd) {
|
|
const r = runNode([GUARD_SCRIPT], {
|
|
cwd,
|
|
env: { ...process.env, GSD_LINT_DOCS_COMMAND_FORM_REPO_ROOT: cwd },
|
|
timeoutMs: GENERATOR_SCRIPT_TIMEOUT_MS,
|
|
});
|
|
return { status: r.exitCode, stdout: r.stdout, stderr: r.stderr };
|
|
}
|
|
|
|
describe('lint-docs-command-form — colon slash form flagged', () => {
|
|
test('exits non-zero and names the file when docs contain /gsd:<cmd>', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'docs/how-to/example.md', 'Run `/gsd:plan-phase` to start.\n');
|
|
gitAdd(dir, 'commands/gsd/plan-phase.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.notEqual(result.status, 0, `expected non-zero exit, got ${result.status}; stdout: ${result.stdout}`);
|
|
assert.ok(result.stderr.includes('docs/how-to/example.md'), `stderr should name the file: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — plugin namespace permitted', () => {
|
|
test('exits 0 when docs contain /gsd-core:<cmd>', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'docs/how-to/example.md', 'Run `/gsd-core:plan-phase` to start.\n');
|
|
gitAdd(dir, 'commands/gsd/plan-phase.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.equal(result.status, 0, `expected exit 0 for /gsd-core:, got ${result.status}; stderr: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — docs/adr exempt', () => {
|
|
test('exits 0 for a fixture under docs/adr/', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'docs/adr/999-example.md', 'Historically we typed `/gsd:plan-phase`.\n');
|
|
gitAdd(dir, 'commands/gsd/plan-phase.md');
|
|
gitAdd(dir, 'docs/adr/999-example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.equal(result.status, 0, `expected exit 0 under docs/adr/, got ${result.status}; stderr: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — source trees never checked', () => {
|
|
test('exits 0 for a fixture under gsd-core/workflows/ (colon form is correct there)', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'gsd-core/workflows/example.md', 'Dispatch `/gsd:plan-phase`.\n');
|
|
gitAdd(dir, 'commands/gsd/plan-phase.md');
|
|
gitAdd(dir, 'gsd-core/workflows/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.equal(result.status, 0, `expected exit 0 under gsd-core/workflows/, got ${result.status}; stderr: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — bare colon form flagged', () => {
|
|
test('exits non-zero when docs contain bare gsd:<cmd>', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'docs/how-to/example.md', 'The command is gsd:plan-phase.\n');
|
|
gitAdd(dir, 'commands/gsd/plan-phase.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.notEqual(result.status, 0, `expected non-zero exit, got ${result.status}; stdout: ${result.stdout}`);
|
|
assert.ok(result.stderr.includes('docs/how-to/example.md'), `stderr should name the file: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — name: frontmatter key citation exempt', () => {
|
|
test('exits 0 when docs quote `name: gsd:next` as a source frontmatter citation', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'commands/gsd/next.md', '---\nname: gsd:next\n---\n');
|
|
writeFile(
|
|
dir,
|
|
'docs/how-to/example.md',
|
|
'Frontmatter:\n- `name: gsd:next` (surfaces as `/gsd-next`)\n',
|
|
);
|
|
gitAdd(dir, 'commands/gsd/next.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.equal(
|
|
result.status,
|
|
0,
|
|
`expected exit 0 for a name: frontmatter citation, got ${result.status}; stderr: ${result.stderr}`,
|
|
);
|
|
assert.ok(result.stdout.includes('0 violations'), `stdout: ${result.stdout}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — name: exemption is narrow, not a blanket hole', () => {
|
|
test('exits non-zero when bare gsd:next appears without a preceding name: key', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'commands/gsd/next.md', '---\nname: gsd:next\n---\n');
|
|
writeFile(dir, 'docs/how-to/example.md', 'Just type gsd:next to run it.\n');
|
|
gitAdd(dir, 'commands/gsd/next.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.notEqual(result.status, 0, `expected non-zero exit, got ${result.status}; stdout: ${result.stdout}`);
|
|
assert.ok(result.stderr.includes('docs/how-to/example.md'), `stderr should name the file: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — case-insensitive detection', () => {
|
|
test('exits non-zero when docs contain /GSD:next (mixed case)', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'commands/gsd/next.md', '---\nname: gsd:next\n---\n');
|
|
writeFile(dir, 'docs/how-to/example.md', 'Run `/GSD:next` to start.\n');
|
|
gitAdd(dir, 'commands/gsd/next.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.notEqual(result.status, 0, `expected non-zero exit, got ${result.status}; stdout: ${result.stdout}`);
|
|
assert.ok(result.stderr.includes('docs/how-to/example.md'), `stderr should name the file: ${result.stderr}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lint-docs-command-form — clean docs tree', () => {
|
|
test('exits 0 on a clean fixture repo with no colon-form commands', () => {
|
|
const dir = createTempRepo();
|
|
try {
|
|
writeFile(dir, 'docs/how-to/example.md', 'Run `/gsd-plan-phase` to start.\n');
|
|
gitAdd(dir, 'commands/gsd/plan-phase.md');
|
|
gitAdd(dir, 'docs/how-to/example.md');
|
|
|
|
const result = runGuard(dir);
|
|
assert.equal(result.status, 0, `expected exit 0, got ${result.status}; stderr: ${result.stderr}`);
|
|
assert.ok(result.stdout.includes('0 violations'), `stdout: ${result.stdout}`);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|