Files
msd-core/tests/verify-test-quality.test.cjs
Tom Boucher b8c33647d8 refactor(tests): retire output-grep & source-grep via typed surfaces (finish #2974) (#462)
* refactor(#455): implement typed surfaces to retire grep tests

Production surfaces added:
- hooks/managed-hooks-registry.cjs: new CJS module exporting MANAGED_HOOKS
  as a typed array; gsd-check-update-worker.js now requires it instead of
  declaring an inline array
- bin/install.js: elevate inline gsdHooks to module-level GSD_UNINSTALL_HOOKS,
  export it alongside runtimeMap/allRuntimes (already exported)
- scripts/build-hooks.js: export HOOKS_TO_COPY; guard build() behind
  require.main===module so tests can require the file without triggering a build
- get-shit-done/bin/lib/init.cjs: add --json mode to agent-skills command,
  emitting typed IR { agent_type, block, skills_count } for test assertions
- get-shit-done/bin/gsd-tools.cjs: wire --json flag for agent-skills dispatch

Category-B source-grep migrations:
- tests/managed-hooks.test.cjs: require MANAGED_HOOKS from registry, drop fs.readFileSync+regex
- tests/orphaned-hooks.test.cjs: require MANAGED_HOOKS+HOOKS_TO_COPY as typed exports
- tests/hooks-opt-in.test.cjs: replace gsdHooks regex-parse with GSD_UNINSTALL_HOOKS import
- tests/install-minimal-hooks.test.cjs: replace gsdHooks regex-parse with GSD_UNINSTALL_HOOKS
- tests/copilot-install.test.cjs: replace src.includes() checks with typed
  assertions on runtimeMap, allRuntimes, parseRuntimeInput, buildRuntimePromptText
- tests/agent-skills.test.cjs: migrate to --json typed IR assertions

pending-migration-to-typed-ir token cleared (87 of 87 files):
- 78 files already had source-text-is-the-product; removed duplicate token
- 5 files already used typed assertions; reclassified or annotated
- 4 files required individual reclassification to source-text-is-the-product
  or architectural-invariant

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#455): update workflow-guard test to typed GSD_UNINSTALL_HOOKS import; isolate HOME in runtime-launcher (D) test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#455): guard install.js main() behind require.main===module so the typed export is require-safe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#455): document --json typed surfaces for agent-skills, progress, validate context

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#455): add changeset fragment for new --json surfaces

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#455): complete grep migration for files flagged by lint-tests

The branch commit 4e630d99 stripped `allow-test-rule: pending-migration-to-typed-ir`
from ~80 test files without replacing their assertions or adding the correct
exemption annotation. The files were NOT source-grep tests — they read .md
workflow/agent/command/reference files (source-text-is-the-product) or hook
source files for structural invariants (structural-regression-guard). No
assertion logic was changed; only the correct allow-test-rule annotation was
added to each file per CONTRIBUTING.md exception matrix.

73 files: `source-text-is-the-product` — workflow/agent/command/reference .md
7 files:  `structural-regression-guard` — hook .js / bin/install.js structural checks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:39:52 -04:00

210 lines
8.0 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// Tests write synthetic fixture files and apply regex detectors to them.
// The fixture text IS the product being tested (testing linter/detector logic,
// not GSD command JSON output). Migrated from pending-migration-to-typed-ir per #455.
/**
* Tests for the audit_test_quality step in verify-phase.md
*
* Validates that the verifier's test quality audit detects:
* - Disabled tests (it.skip) covering requirements
* - Circular tests (system generating its own expected values)
* - Weak assertions on requirement-linked tests
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { createTempProject, cleanup } = require('./helpers.cjs');
describe('audit_test_quality step', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
describe('disabled test detection', () => {
test('detects it.skip in test files', () => {
const testDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testDir, { recursive: true });
fs.writeFileSync(path.join(testDir, 'parity.test.js'), [
'describe("parity", () => {',
' it.skip("matches PHP output", async () => {',
' expect(result).toBeCloseTo(155.96, 2);',
' });',
'});',
].join('\n'));
const content = fs.readFileSync(path.join(testDir, 'parity.test.js'), 'utf8');
const skipPatterns = /it\.skip|describe\.skip|test\.skip|xit\(|xdescribe\(|xtest\(/g;
const matches = content.match(skipPatterns);
assert.ok(matches, 'Should detect skip patterns');
assert.strictEqual(matches.length, 1);
});
test('detects multiple skip patterns across frameworks', () => {
const testDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testDir, { recursive: true });
fs.writeFileSync(path.join(testDir, 'multi.test.js'), [
'describe.skip("suite", () => {});',
'xit("old jasmine", () => {});',
'test.skip("jest skip", () => {});',
'it.todo("not implemented");',
].join('\n'));
const content = fs.readFileSync(path.join(testDir, 'multi.test.js'), 'utf8');
const skipPatterns = /it\.skip|describe\.skip|test\.skip|xit\(|xdescribe\(|xtest\(|it\.todo|test\.todo/g;
const matches = content.match(skipPatterns);
assert.ok(matches, 'Should detect all skip variants');
assert.strictEqual(matches.length, 4);
});
test('does not flag active tests as skipped', () => {
const testDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testDir, { recursive: true });
fs.writeFileSync(path.join(testDir, 'active.test.js'), [
'describe("active suite", () => {',
' it("does the thing", () => {',
' expect(result).toBe(true);',
' });',
' test("also works", () => {',
' expect(other).toBe(42);',
' });',
'});',
].join('\n'));
const content = fs.readFileSync(path.join(testDir, 'active.test.js'), 'utf8');
const skipPatterns = /it\.skip|describe\.skip|test\.skip|xit\(|xdescribe\(|xtest\(|it\.todo|test\.todo/g;
const matches = content.match(skipPatterns);
assert.strictEqual(matches, null, 'Active tests should not match skip patterns');
});
});
describe('circular test detection', () => {
test('detects script that imports system-under-test and writes fixtures', () => {
const testDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testDir, { recursive: true });
fs.writeFileSync(path.join(testDir, 'captureBaseline.js'), [
'import { CalculationService } from "../server/services/calculationService.js";',
'import { writeFileSync } from "fs";',
'',
'const result = await CalculationService.execute(input);',
'fixture.expectedOutput = result.value;',
'writeFileSync("fixtures/data.json", JSON.stringify(fixture));',
].join('\n'));
const content = fs.readFileSync(path.join(testDir, 'captureBaseline.js'), 'utf8');
const importsSystem = /import.*(?:Service|Engine|Calculator|Controller)/.test(content);
const writesFiles = /writeFileSync|writeFile|fs\.write/.test(content);
assert.ok(importsSystem, 'Should detect system-under-test import');
assert.ok(writesFiles, 'Should detect file writing');
assert.ok(importsSystem && writesFiles, 'Script that imports SUT and writes fixtures is CIRCULAR');
});
test('does not flag test helpers that only read fixtures', () => {
const testDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testDir, { recursive: true });
fs.writeFileSync(path.join(testDir, 'loadFixtures.js'), [
'import { readFileSync } from "fs";',
'export function loadFixture(name) {',
' return JSON.parse(readFileSync(`fixtures/${name}.json`, "utf8"));',
'}',
].join('\n'));
const content = fs.readFileSync(path.join(testDir, 'loadFixtures.js'), 'utf8');
const importsSystem = /import.*(?:Service|Engine|Calculator|Controller)/.test(content);
const writesFiles = /writeFileSync|writeFile|fs\.write/.test(content);
assert.ok(!importsSystem, 'Should not flag read-only helper as importing SUT');
assert.ok(!writesFiles, 'Should not flag read-only helper as writing files');
});
});
describe('assertion strength classification', () => {
test('classifies existence-only assertions as INSUFFICIENT for value requirements', () => {
const assertions = [
'expect(result).toBeDefined()',
'expect(result).not.toBeNull()',
'assert.ok(result)',
];
const existencePattern = /toBeDefined|not\.toBeNull|assert\.ok\(/;
const valuePattern = /toEqual|toBeCloseTo|strictEqual|deepStrictEqual/;
for (const assertion of assertions) {
assert.ok(existencePattern.test(assertion), `"${assertion}" should match existence pattern`);
assert.ok(!valuePattern.test(assertion), `"${assertion}" should NOT match value pattern`);
}
});
test('classifies value assertions as sufficient', () => {
const assertions = [
'expect(result).toBeCloseTo(155.96, 2)',
'expect(result).toEqual({ amount: 100 })',
'assert.strictEqual(result, 42)',
];
const valuePattern = /toEqual|toBeCloseTo|strictEqual|deepStrictEqual/;
for (const assertion of assertions) {
assert.ok(valuePattern.test(assertion), `"${assertion}" should match value pattern`);
}
});
});
describe('provenance classification', () => {
test('fixture with legacy system comment classified as VALID', () => {
const fixture = {
legacyId: 10341,
comment: 'Real PHP fixture - output from legacy system',
dbDependent: true,
expectedOutput: { value: 155.96 },
};
const hasLegacySource = /legacy|php|real|manual|captured from/i.test(fixture.comment || '');
assert.ok(hasLegacySource, 'Comment referencing legacy system = VALID provenance');
});
test('fixture with synthetic/baseline comment classified as SUSPECT', () => {
const fixture = {
legacyId: null,
comment: 'Synthetic offline fixture - computed from known algorithm',
dbDependent: false,
expectedOutput: { value: 1240.68 },
};
const hasSyntheticSource = /synthetic|computed|baseline|generated|captured from engine/i.test(fixture.comment || '');
const hasLegacySource = /legacy|php|real output|manual capture/i.test(fixture.comment || '');
assert.ok(hasSyntheticSource, 'Comment indicating synthetic source detected');
assert.ok(!hasLegacySource, 'Should NOT be classified as legacy source');
});
test('fixture with no comment classified as UNKNOWN', () => {
const fixture = {
expectedOutput: { value: 42 },
};
const hasAnyProvenance = (fixture.comment || '').length > 0;
assert.ok(!hasAnyProvenance, 'No comment = UNKNOWN provenance');
});
});
});