* 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>
132 lines
5.8 KiB
JavaScript
132 lines
5.8 KiB
JavaScript
// 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)
|
|
*
|
|
* Tests for feat/worktree-health-w017-2167:
|
|
* - W017 code exists in verify.cjs (structural)
|
|
* - No false positives on projects without linked worktrees
|
|
* - Adding the check does not regress baseline health status
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { runGsdTools, createTempGitProject, cleanup } = require('./helpers.cjs');
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function writeMinimalProjectMd(tmpDir) {
|
|
const sections = ['## What This Is', '## Core Value', '## Requirements'];
|
|
const content = sections.map(s => `${s}\n\nContent here.\n`).join('\n');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'PROJECT.md'),
|
|
`# Project\n\n${content}`
|
|
);
|
|
}
|
|
|
|
function writeMinimalRoadmap(tmpDir) {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
'# Roadmap\n\n### Phase 1: Setup\n'
|
|
);
|
|
}
|
|
|
|
function writeMinimalStateMd(tmpDir) {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
'# Session State\n\n## Current Position\n\nPhase: 1\n'
|
|
);
|
|
}
|
|
|
|
function writeValidConfigJson(tmpDir) {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'config.json'),
|
|
JSON.stringify({
|
|
model_profile: 'balanced',
|
|
commit_docs: true,
|
|
workflow: { nyquist_validation: true, ai_integration_phase: true },
|
|
}, null, 2)
|
|
);
|
|
}
|
|
|
|
function setupHealthyProject(tmpDir) {
|
|
writeMinimalProjectMd(tmpDir);
|
|
writeMinimalRoadmap(tmpDir);
|
|
writeMinimalStateMd(tmpDir);
|
|
writeValidConfigJson(tmpDir);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-setup'), { recursive: true });
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 1. Structural: W017 code exists in verify.cjs
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('W017: structural presence', () => {
|
|
test('verify.cjs contains W017 warning code', () => {
|
|
const verifyPath = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'verify.cjs');
|
|
const source = fs.readFileSync(verifyPath, 'utf-8');
|
|
assert.ok(source.includes("'W017'"), 'verify.cjs should contain W017 warning code');
|
|
});
|
|
|
|
test('verify.cjs contains worktree list --porcelain invocation', () => {
|
|
const verifyPath = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'verify.cjs');
|
|
const source = fs.readFileSync(verifyPath, 'utf-8');
|
|
assert.ok(
|
|
source.includes('worktree') && source.includes('--porcelain'),
|
|
'verify.cjs should invoke git worktree list --porcelain'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 2. No worktrees = no W017
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('W017: no false positives', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempGitProject();
|
|
setupHealthyProject(tmpDir);
|
|
});
|
|
|
|
afterEach(() => cleanup(tmpDir));
|
|
|
|
test('no W017 when project has no linked worktrees', () => {
|
|
const result = runGsdTools('validate health --raw', tmpDir);
|
|
assert.ok(result.success, `validate health should succeed: ${result.error || ''}`);
|
|
const parsed = JSON.parse(result.output);
|
|
|
|
// Collect all warning codes
|
|
const warningCodes = (parsed.warnings || []).map(w => w.code);
|
|
assert.ok(!warningCodes.includes('W017'), `W017 should not fire when no linked worktrees exist, got warnings: ${JSON.stringify(warningCodes)}`);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 3. Clean project still reports healthy
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('W017: no regression on healthy projects', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempGitProject();
|
|
setupHealthyProject(tmpDir);
|
|
});
|
|
|
|
afterEach(() => cleanup(tmpDir));
|
|
|
|
test('validate health still reports healthy on a clean project', () => {
|
|
const result = runGsdTools('validate health --raw', tmpDir);
|
|
assert.ok(result.success, `validate health should succeed: ${result.error || ''}`);
|
|
const parsed = JSON.parse(result.output);
|
|
assert.equal(parsed.status, 'healthy', `Expected healthy status, got ${parsed.status}. Errors: ${JSON.stringify(parsed.errors)}. Warnings: ${JSON.stringify(parsed.warnings)}`);
|
|
});
|
|
});
|