* docs(test-standards): enforce no-source-grep rule with CI linter + update CONTRIBUTING.md
Adds scripts/lint-no-source-grep.cjs — a static linter that detects readFileSync
on .cjs source files in tests without an allow-test-rule annotation. Wires it
into CI as a new lint-tests job in test.yml and as npm run lint:tests.
Resolves all 9 existing violations across the test suite:
- Rewrites workspace routing tests (3) as behavioral runGsdTools calls that
verify each command is router-recognized (exit != "Unknown init workflow")
- Adds allow-test-rule annotations with explanatory comments to 7 legitimate
structural tests: architectural invariants (locking, orphan-worktree),
structural regression guards (milestone-regex-global), docs-parity
(config-field-docs), integration-test-input (copilot-install), and
structural-implementation-guards (bug-1891, discuss-mode)
Updates CONTRIBUTING.md Testing Standards section with:
- "Prohibited: Source-Grep Tests" section with the before/after pattern,
root cause analysis of why it breaks (commit 990c3e64), and CI reference
- allow-test-rule exemption table (6 recognized categories with when-to-use)
- "CI Test Quality Checks" table showing lint-tests job and local run command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve CodeRabbit findings on PR #2700
- CONTRIBUTING.md: "four recognized categories" → "six" (table has 6 rows)
- workspace.test.cjs: use positional args in routing tests (no --name flag)
- lint-no-source-grep.cjs: add source-dir guard to READ_WITH_INLINE_CJS_RE
(mirrors CJS_PATH_CONST_RE's protection against false positives on temp files)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): tighten allow-test-rule and add recursive test discovery
- ALLOW_ANNOTATION now requires at least one non-whitespace char after the
colon so bare '// allow-test-rule:' cannot bypass the lint gate
- findTestFiles() recurses into subdirectories so nested *.test.cjs files
are covered if the tests/ tree ever grows subdirs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
65 lines
2.5 KiB
JavaScript
65 lines
2.5 KiB
JavaScript
// allow-test-rule: structural-implementation-guard
|
|
// gsd-tools.cjs @file: resolution is a low-level stdout interception that cannot be
|
|
// exercised end-to-end via runGsdTools without a real workflow that emits @file: output.
|
|
// These structural tests guard the interception wiring until a behavioral integration
|
|
// test suite for the full @file: path is added.
|
|
|
|
/**
|
|
* Regression tests for bug #1891
|
|
*
|
|
* gsd-tools.cjs must transparently resolve @file: references in stdout
|
|
* so that workflows never see the @file: prefix. This eliminates the
|
|
* bash-specific `if [[ "$INIT" == @file:* ]]` check that breaks on
|
|
* PowerShell and other non-bash shells.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test, before } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const GSD_TOOLS_SRC = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
|
|
|
describe('bug #1891: @file: resolution in gsd-tools.cjs', () => {
|
|
let src;
|
|
|
|
before(() => {
|
|
src = fs.readFileSync(GSD_TOOLS_SRC, 'utf-8');
|
|
});
|
|
|
|
test('main() intercepts stdout and resolves @file: references', () => {
|
|
// The non-pick path should have @file: resolution, just like the --pick path
|
|
assert.ok(
|
|
src.includes("captured.startsWith('@file:')") ||
|
|
src.includes('captured.startsWith(\'@file:\')'),
|
|
'main() should check for @file: prefix in captured output'
|
|
);
|
|
});
|
|
|
|
test('@file: resolution reads file content via readFileSync', () => {
|
|
// Verify the resolution reads the actual file
|
|
assert.ok(
|
|
src.includes("readFileSync(captured.slice(6)") ||
|
|
src.includes('readFileSync(captured.slice(6)'),
|
|
'@file: resolution should read file at the path after the prefix'
|
|
);
|
|
});
|
|
|
|
test('stdout interception wraps runCommand in the non-pick path', () => {
|
|
// The main function should intercept fs.writeSync for fd=1
|
|
// in BOTH the pick path AND the normal path
|
|
const mainFunc = src.slice(src.indexOf('async function main()'));
|
|
const pickInterception = mainFunc.indexOf('// When --pick is active');
|
|
const fileResolution = mainFunc.indexOf('@file:');
|
|
|
|
// There should be at least two @file: resolution points:
|
|
// one in the --pick path and one in the normal path
|
|
const firstAt = mainFunc.indexOf("'@file:'");
|
|
const secondAt = mainFunc.indexOf("'@file:'", firstAt + 1);
|
|
assert.ok(secondAt > firstAt,
|
|
'Both --pick and normal paths should resolve @file: references');
|
|
});
|
|
});
|