docs(test-standards): enforce no-source-grep rule with CI linter + CONTRIBUTING.md (#2700)
* 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>
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
// 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
|
||||
*
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// allow-test-rule: docs-parity
|
||||
// Extracts CONFIG_DEFAULTS keys from core.cjs source to verify planning-config.md
|
||||
// stays in sync. The canonical list of defaults lives in source; there is no runtime
|
||||
// API to enumerate them. Source inspection is the only practical parity check here.
|
||||
|
||||
/**
|
||||
* Verify planning-config.md documents all config fields from source code.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// allow-test-rule: integration-test-input
|
||||
// Reads verify.cjs as real test fixture input to the convertClaudeToCopilotContent()
|
||||
// function under test. The file is not inspected for string presence; it is the
|
||||
// input whose *transformation* is being asserted. This is the correct level of testing
|
||||
// for format-conversion functions where a real source file is the canonical test case.
|
||||
|
||||
/**
|
||||
* GSD Tools Tests - Copilot Install Plumbing
|
||||
*
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// allow-test-rule: structural-implementation-guard
|
||||
// init.cjs cmdInitPlanPhase must expose text_mode in its returned flags object.
|
||||
// The behavioral alternative (run plan-phase init and inspect JSON output) is
|
||||
// fragile across runtime variations. Structural inspection guards the contract
|
||||
// until a stable behavioral API test is in place.
|
||||
|
||||
/**
|
||||
* Discuss Mode Config Tests
|
||||
*
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// allow-test-rule: architectural-invariant
|
||||
// state.cjs locking must use Atomics.wait() (not a spin-loop) and register an exit
|
||||
// handler. These are implementation primitives, not string literals — behavioral tests
|
||||
// cannot verify which sleep primitive was chosen. Source inspection is the right level.
|
||||
|
||||
/**
|
||||
* Regression tests for locking bugs #1909, #1916, #1925, #1927.
|
||||
*
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// allow-test-rule: structural-regression-guard
|
||||
// milestone.cjs must use replace()+compare, not test()+replace(), to avoid regex
|
||||
// lastIndex corruption with global flags. A behavioral test cannot distinguish which
|
||||
// pattern was used — it can only observe wrong output after multiple calls, which is
|
||||
// fragile. Structural inspection locks the correct fix in place.
|
||||
|
||||
/**
|
||||
* Regression tests for regex global state bug in milestone.cjs
|
||||
*
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// allow-test-rule: architectural-invariant
|
||||
// verify.cjs must contain the W017 warning code and the worktree list invocation.
|
||||
// These checks guard the existence of the detection feature, not its text output.
|
||||
// Behavioral tests cover the detection flow; structural tests guard the implementation contract.
|
||||
|
||||
/**
|
||||
* GSD Tools Tests - Orphan/Stale Worktree Detection (W017)
|
||||
*
|
||||
|
||||
@@ -11,7 +11,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
const { runGsdTools, createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { runGsdTools, createTempProject, createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { detectChildRepos } = require('../get-shit-done/bin/lib/init.cjs');
|
||||
|
||||
// ─── detectChildRepos ────────────────────────────────────────────────────────
|
||||
@@ -368,30 +368,37 @@ describe('workspace command files', () => {
|
||||
// ─── Routing in gsd-tools ───────────────────────────────────────────────────
|
||||
|
||||
describe('workspace routing in gsd-tools', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => { tmpDir = createTempProject(); });
|
||||
afterEach(() => { cleanup(tmpDir); });
|
||||
|
||||
// Behavioral routing tests: verify each command is recognized by the router
|
||||
// (does not return "Unknown init workflow: ..."). The exact command output is
|
||||
// covered by the functional tests above; these guard against routing deletions.
|
||||
|
||||
test('init new-workspace is routed correctly', () => {
|
||||
const toolsContent = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
'utf8'
|
||||
const result = runGsdTools('init new-workspace test-ws', tmpDir);
|
||||
const stderr = result.error || '';
|
||||
assert.ok(
|
||||
!stderr.includes('Unknown init workflow'),
|
||||
`init new-workspace must be a recognized command; got: ${stderr}`
|
||||
);
|
||||
assert.ok(toolsContent.includes("case 'new-workspace'"));
|
||||
assert.ok(toolsContent.includes('cmdInitNewWorkspace'));
|
||||
});
|
||||
|
||||
test('init list-workspaces is routed correctly', () => {
|
||||
const toolsContent = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
'utf8'
|
||||
);
|
||||
assert.ok(toolsContent.includes("case 'list-workspaces'"));
|
||||
assert.ok(toolsContent.includes('cmdInitListWorkspaces'));
|
||||
const result = runGsdTools('init list-workspaces', tmpDir);
|
||||
assert.ok(result.success, `init list-workspaces should succeed: ${result.error}`);
|
||||
const parsed = JSON.parse(result.output);
|
||||
assert.ok(Array.isArray(parsed.workspaces), 'list-workspaces must return a workspaces array');
|
||||
});
|
||||
|
||||
test('init remove-workspace is routed correctly', () => {
|
||||
const toolsContent = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
'utf8'
|
||||
const result = runGsdTools('init remove-workspace nonexistent-ws', tmpDir);
|
||||
const stderr = result.error || '';
|
||||
assert.ok(
|
||||
!stderr.includes('Unknown init workflow'),
|
||||
`init remove-workspace must be a recognized command; got: ${stderr}`
|
||||
);
|
||||
assert.ok(toolsContent.includes("case 'remove-workspace'"));
|
||||
assert.ok(toolsContent.includes('cmdInitRemoveWorkspace'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user