mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
test: guard the reduced suite against new test-of-test files
- test/test-of-test-ratchet.test.ts records the 228 free tests that import only test/ code and fails on a new one, naming the owner test to extend instead; a stale baseline entry fails with the remove instruction. - test/helpers/resolve-repo-path.ts is the one specifier/literal resolver for the ratchet and the touchfile closure invariant, with its own unit tests. - CONTRIBUTING "Test tiers" describes the paid-failure workflow (fix, then one row in the detector's owner test) and the ratchet; TEST_PORTFOLIO gains the detector -> owner-test table and no longer claims an Autoplan chain eval. - TODOS: automatic exclusion policy for chronically red periodic files (P3), the deferred native-completion table collapse, the unused CEO payment seeder; the PTY readiness item is narrowed to the paid runner. - docs/test-audit-2026-09.md collects the triage, security mapping, inventories, selection proof, behavior-commit decisions and retained false positives.
This commit is contained in:
1 parent
67084a5caf
commit
13e4f37201
8 files changed
+848
-26
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as path from 'path';
|
||||
import { directSpecifiers, resolveRepoLiteral, resolveRepoSpecifier } from './resolve-repo-path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '../..');
|
||||
|
||||
describe('resolve-repo-path', () => {
|
||||
test('relative specifiers resolve to repo files, with and without extensions', () => {
|
||||
expect(resolveRepoSpecifier(ROOT, 'test/touchfiles.test.ts', './helpers/touchfiles')).toBe('test/helpers/touchfiles.ts');
|
||||
expect(resolveRepoSpecifier(ROOT, 'test/touchfiles.test.ts', '../lib/eval-model')).toBe('lib/eval-model.ts');
|
||||
expect(resolveRepoSpecifier(ROOT, 'lib/code-intelligence/gbrain-adapter.ts', '../egress-receipt.js')).toBe('lib/egress-receipt.ts');
|
||||
expect(resolveRepoSpecifier(ROOT, 'test/touchfiles.test.ts', './helpers/no-such-module')).toBeNull();
|
||||
});
|
||||
|
||||
test('bare packages and bun:/node: builtins never resolve', () => {
|
||||
for (const specifier of ['bun:test', 'node:fs', 'fs', '@anthropic-ai/sdk', 'ts-morph']) {
|
||||
expect(resolveRepoSpecifier(ROOT, 'test/touchfiles.test.ts', specifier)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test('direct specifiers include imports, re-exports, require and literal dynamic import, not type-only imports', () => {
|
||||
const source = [
|
||||
"import { a } from './a';",
|
||||
"import type { T } from './types';",
|
||||
"export type { U } from './more-types';",
|
||||
"export { b } from '../lib/b';",
|
||||
"import {\n c,\n d,\n} from './multi';",
|
||||
"import './side-effect';",
|
||||
"const e = require('./e');",
|
||||
"const f = await import('./f');",
|
||||
"const g = await import(name);",
|
||||
].join('\n');
|
||||
expect(directSpecifiers(source)).toEqual(['./a', '../lib/b', './multi', './side-effect', './e', './f']);
|
||||
});
|
||||
|
||||
test('path literals resolve only when the repo path exists', () => {
|
||||
expect(resolveRepoLiteral(ROOT, 'bin/gstack-config')).toBe('bin/gstack-config');
|
||||
expect(resolveRepoLiteral(ROOT, 'test/fixtures/')).toBe('test/fixtures');
|
||||
expect(resolveRepoLiteral(ROOT, path.join('test', 'helpers', 'touchfiles.ts'))).toBe('test/helpers/touchfiles.ts');
|
||||
expect(resolveRepoLiteral(ROOT, 'test/fixtures/no-such-fixture.json')).toBeNull();
|
||||
expect(resolveRepoLiteral(ROOT, '/etc/passwd')).toBeNull();
|
||||
expect(resolveRepoLiteral(ROOT, '../outside')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Resolve a module specifier or path literal from a repo file to a repo-relative
|
||||
* path, or null when it names no file in the checkout. Bare packages and
|
||||
* `bun:` / `node:` builtins never resolve. Shared by the touchfile closure
|
||||
* invariant and the test-of-test ratchet.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const isFile = (candidate: string) => fs.existsSync(candidate) && fs.statSync(candidate).isFile();
|
||||
|
||||
/** A relative module specifier (`./x`, `../lib/y.js`) → repo-relative file, or null. */
|
||||
export function resolveRepoSpecifier(root: string, fromFile: string, specifier: string): string | null {
|
||||
if (!specifier.startsWith('.')) return null;
|
||||
const base = path.resolve(path.dirname(path.join(root, fromFile)), specifier);
|
||||
for (const candidate of [base, `${base}.ts`, base.replace(/\.js$/, '.ts'), `${base}.tsx`, path.join(base, 'index.ts')]) {
|
||||
if (isFile(candidate)) return toRelative(root, candidate);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A repo-rooted path literal (`test/fixtures/x.json`, `bin/gstack-x`) → itself when it exists, else null. */
|
||||
export function resolveRepoLiteral(root: string, literal: string): string | null {
|
||||
const clean = literal.replace(/\/+$/, '');
|
||||
if (!clean || path.isAbsolute(clean) || clean.startsWith('..')) return null;
|
||||
return fs.existsSync(path.join(root, clean)) ? clean : null;
|
||||
}
|
||||
|
||||
function toRelative(root: string, absolute: string): string | null {
|
||||
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
||||
return relative.startsWith('..') ? null : relative;
|
||||
}
|
||||
|
||||
/** Direct module specifiers of a source file: static imports/re-exports, require() and literal dynamic import(); `import type` excluded. */
|
||||
export function directSpecifiers(source: string): string[] {
|
||||
const out: string[] = [];
|
||||
const typeOnly = /^\s*(?:import|export)\s+type\s/;
|
||||
for (const match of source.matchAll(/^[ \t]*(?:import|export)\b[^;'"`]*?from\s*(['"])([^'"]+)\1|^[ \t]*import\s*(['"])([^'"]+)\3/gm)) {
|
||||
if (typeOnly.test(match[0])) continue;
|
||||
out.push(match[2] ?? match[4]!);
|
||||
}
|
||||
for (const match of source.matchAll(/\b(?:require|import)\s*\(\s*(['"])([^'"]+)\1\s*\)/g)) out.push(match[2]!);
|
||||
return out;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { matchGlob } from './test-selection';
|
||||
import { resolveRepoLiteral, resolveRepoSpecifier } from './resolve-repo-path';
|
||||
|
||||
export interface ClosureEntry {
|
||||
/** Repo-relative dependency path. */
|
||||
@@ -19,15 +20,6 @@ export interface ClosureEntry {
|
||||
const LITERAL = /test\/(?:helpers|fixtures)\/[A-Za-z0-9_.\-/]+[A-Za-z0-9_]/g;
|
||||
const scanner = new Bun.Transpiler({ loader: 'tsx' });
|
||||
|
||||
function resolveImport(root: string, from: string, specifier: string): string | null {
|
||||
if (!specifier.startsWith('.')) return null;
|
||||
const base = path.resolve(path.dirname(path.join(root, from)), specifier);
|
||||
for (const candidate of [base, `${base}.ts`, base.replace(/\.js$/, '.ts'), path.join(base, 'index.ts')]) {
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return path.relative(root, candidate).split(path.sep).join('/');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Selection itself is diffed by map (diffTouchfileMaps), and test files are never dependencies. */
|
||||
const SELECTION_MODULES = new Set(['test/helpers/touchfiles-data.ts', 'test/helpers/touchfiles.ts', 'test/helpers/test-selection.ts']);
|
||||
const inScope = (file: string) => (file.startsWith('test/helpers/') || file.startsWith('test/fixtures/')) &&
|
||||
@@ -47,15 +39,15 @@ export function paidTestClosure(root: string, testFile: string, boundary: Readon
|
||||
let imports: Array<{ path: string }> = [];
|
||||
try { imports = scanner.scanImports(source); } catch { imports = []; }
|
||||
for (const { path: specifier } of imports) {
|
||||
const target = resolveImport(root, file, specifier);
|
||||
const target = resolveRepoSpecifier(root, file, specifier);
|
||||
if (!target || !inScope(target) || seen.has(target)) continue;
|
||||
seen.set(target, [...chain, target]);
|
||||
queue.push({ file: target, chain: [...chain, target] });
|
||||
}
|
||||
for (const match of source.match(LITERAL) ?? []) {
|
||||
const absolute = path.join(root, match);
|
||||
if (!inScope(match) || !fs.existsSync(absolute)) continue;
|
||||
const literal = fs.statSync(absolute).isDirectory() ? `${match.replace(/\/$/, '')}/**` : match;
|
||||
const found = inScope(match) ? resolveRepoLiteral(root, match) : null;
|
||||
if (!found) continue;
|
||||
const literal = fs.statSync(path.join(root, found)).isDirectory() ? `${found}/**` : found;
|
||||
if (seen.has(literal)) continue;
|
||||
seen.set(literal, [...chain, `"${match}"`]);
|
||||
if (!literal.endsWith('/**')) queue.push({ file: literal, chain: [...chain, `"${match}"`] });
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Recurrence ratchet for tests that exercise only test code.
|
||||
*
|
||||
* A free test counts when none of its direct imports reaches a repo file outside
|
||||
* `test/` and it names no `bin/` path, SKILL.md or `.tmpl` template: it tests a
|
||||
* helper or a replayed capture, not product code. The 2026-09 audit left the
|
||||
* files below; a new one fails here with its path and a suggested owner test.
|
||||
* AGENTS.md: "do not add one spelling or glyph per paid failure".
|
||||
*/
|
||||
import { expect, test } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isPaidTestFile } from './helpers/paid-test-set';
|
||||
import { directSpecifiers, resolveRepoSpecifier } from './helpers/resolve-repo-path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
|
||||
/** Recorded after workstream D. Add a path only with a one-line reason comment, reviewed in the diff. */
|
||||
const BASELINE = [
|
||||
'test/anthropic-preflight.test.ts',
|
||||
'test/artifacts-allowlist-decisions.test.ts',
|
||||
'test/artifacts-init-migration.test.ts',
|
||||
'test/auto-decide-fixture.test.ts',
|
||||
'test/autoplan-artifact-recorder.test.ts',
|
||||
'test/autoplan-artifact-windows-argv.test.ts',
|
||||
'test/autoplan-edit-digests-al.test.ts',
|
||||
'test/autoplan-owned-state.test.ts',
|
||||
'test/autoplan-pending-artifact.test.ts',
|
||||
'test/autoplan-pending-question.test.ts',
|
||||
'test/autoplan-phase-observer.test.ts',
|
||||
'test/autoplan-public-narration.test.ts',
|
||||
'test/benchmark-cli.test.ts',
|
||||
'test/brain-sync-windows-paths.test.ts',
|
||||
'test/build-script-shell-compat.test.ts',
|
||||
'test/builder-profile.test.ts',
|
||||
'test/bun-subprocess-fd-lifetime.test.ts',
|
||||
'test/bun-version-drift.test.ts',
|
||||
'test/carve-guard-completeness.test.ts',
|
||||
'test/carve-plan-fixture.test.ts',
|
||||
'test/carve-section-ordering.test.ts',
|
||||
'test/ceo-barless-submit.test.ts',
|
||||
'test/ceo-count-ad-v2.test.ts',
|
||||
'test/ceo-expansion-auq.test.ts',
|
||||
'test/ceo-expansion-pacing-native.test.ts',
|
||||
'test/ceo-hold-posture-review.test.ts',
|
||||
'test/ceo-mode-expansion-disposition.test.ts',
|
||||
'test/ceo-mode-labels-native.test.ts',
|
||||
'test/ceo-mode-pending-submit.test.ts',
|
||||
'test/ceo-mode-posture-native.test.ts',
|
||||
'test/ceo-mode-prerequisite.test.ts',
|
||||
'test/ceo-mode-routing-fixture.test.ts',
|
||||
'test/ceo-plan-mode-fixture.test.ts',
|
||||
'test/ceo-posture-packet.test.ts',
|
||||
'test/ceo-section-loading-fixture.test.ts',
|
||||
'test/ceo-split-collection.test.ts',
|
||||
'test/ceo-split-question-policy.test.ts',
|
||||
'test/changed-files-union.test.ts',
|
||||
'test/ci-image-cli-pin.test.ts',
|
||||
'test/ci-image-tag-binding.test.ts',
|
||||
'test/claude-provider-keychain.test.ts',
|
||||
'test/code-intelligence-cli.test.ts',
|
||||
'test/codex-carve-fixture.test.ts',
|
||||
'test/codex-eval-recording.test.ts',
|
||||
'test/codex-model-probe.test.ts',
|
||||
'test/codex-resume-flag-semantics.test.ts',
|
||||
'test/cso-ntfs-fixture.test.ts',
|
||||
'test/cso-windows-build-contract.test.ts',
|
||||
'test/dependency-security.test.ts',
|
||||
'test/deps-smoke.test.ts',
|
||||
'test/design-completion-handoff-scored.test.ts',
|
||||
'test/design-daemon-windows-identity.test.ts',
|
||||
'test/design-html-section-completion.test.ts',
|
||||
'test/devex-peer-comparison-calibration.test.ts',
|
||||
'test/diagram-render-drift.test.ts',
|
||||
'test/diff-scope.test.ts',
|
||||
'test/disabled-dated-record-at.test.ts',
|
||||
'test/distill-apply.test.ts',
|
||||
'test/distill-free-text.test.ts',
|
||||
'test/docs-config-keys.test.ts',
|
||||
'test/dx-selected-navigation-ap.test.ts',
|
||||
'test/eng-batching-current-ledger.test.ts',
|
||||
'test/eng-batching-native-replay.test.ts',
|
||||
'test/eng-batching-saved-ledger.test.ts',
|
||||
'test/eng-count-question-policy.test.ts',
|
||||
'test/eng-devex-s-count.test.ts',
|
||||
'test/eng-first-review.test.ts',
|
||||
'test/eng-published-navigation.test.ts',
|
||||
'test/eng-resolution-block-position.test.ts',
|
||||
'test/eng-seeded-completion-ai.test.ts',
|
||||
'test/eng-seeded-coverage.test.ts',
|
||||
'test/eng-semantic-terminal.test.ts',
|
||||
'test/eng-test-plan-edit-approval.test.ts',
|
||||
'test/eval-list-cli.test.ts',
|
||||
'test/evidence.test.ts',
|
||||
'test/explain-level-config.test.ts',
|
||||
'test/extension-pty-inject-invariant.test.ts',
|
||||
'test/founder-resources-optout.test.ts',
|
||||
'test/free-tests-workflow-wiring.test.ts',
|
||||
'test/gbrain-lib-validate-varname.test.ts',
|
||||
'test/gbrain-lib-verify.test.ts',
|
||||
'test/gbrain-refresh-install-render.test.ts',
|
||||
'test/gbrain-repo-policy.test.ts',
|
||||
'test/gbrain-source-worktree-advance.test.ts',
|
||||
'test/gbrain-sync-skip.test.ts',
|
||||
'test/gbrain-sync-voyage-code-3-integration.test.ts',
|
||||
'test/gstack-artifacts-url.test.ts',
|
||||
'test/gstack-codex-session-import.test.ts',
|
||||
'test/gstack-config-cross-project.test.ts',
|
||||
'test/gstack-config-defaults.test.ts',
|
||||
'test/gstack-config-key-locale.test.ts',
|
||||
'test/gstack-config-memorable-key.test.ts',
|
||||
'test/gstack-config-redact-keys.test.ts',
|
||||
'test/gstack-detach.test.ts',
|
||||
'test/gstack-developer-profile.test.ts',
|
||||
'test/gstack-gbrain-mcp-verify.test.ts',
|
||||
'test/gstack-gbrain-source-wireup.test.ts',
|
||||
'test/gstack-home-module-scope.test.ts',
|
||||
'test/gstack-learnings-search.test.ts',
|
||||
'test/gstack-question-log.test.ts',
|
||||
'test/gstack-question-preference.test.ts',
|
||||
'test/gstack-redact-cli.test.ts',
|
||||
'test/gstack-repo-mode.test.ts',
|
||||
'test/gstack-session-kind.test.ts',
|
||||
'test/gstack-slug-parity.test.ts',
|
||||
'test/gstack-slug-sanitize.test.ts',
|
||||
'test/gstack-state-root-override.test.ts',
|
||||
'test/gstack-team-init-hook-schema.test.ts',
|
||||
'test/gstack-upgrade-migration-v1_17_0_0.test.ts',
|
||||
'test/gstack-upgrade-migration-v1_37_0_0.test.ts',
|
||||
'test/gstack-upgrade-migration-v1_40_0_0.test.ts',
|
||||
'test/gstack-upgrade-migration-v1_78_0_0.test.ts',
|
||||
'test/helpers-unit.test.ts',
|
||||
'test/helpers/budget-override.test.ts',
|
||||
'test/helpers/capture-parity-baseline.test.ts',
|
||||
'test/helpers/claude-pty-runner.scope-gate-floor.unit.test.ts',
|
||||
'test/helpers/e2e-gate.unit.test.ts',
|
||||
'test/helpers/eval-store.test.ts',
|
||||
'test/helpers/hermetic-env.test.ts',
|
||||
'test/helpers/run-bin.test.ts',
|
||||
'test/helpers/session-runner.test.ts',
|
||||
'test/helpers/sync-command-capture.test.ts',
|
||||
'test/hooks-windows-paths.test.ts',
|
||||
'test/hostile-path-writers.test.ts',
|
||||
'test/ios-debug-bridge-release-guard.test.ts',
|
||||
'test/ios-qa-stateserver-hardening.test.ts',
|
||||
'test/ios-qa-swift-build.test.ts',
|
||||
'test/jargon-list.test.ts',
|
||||
'test/jsonl-merge.test.ts',
|
||||
'test/learnings-injection.test.ts',
|
||||
'test/llm-judge-frontier.test.ts',
|
||||
'test/memory-cache-injection.test.ts',
|
||||
'test/memory-ingest-include-gitignored.test.ts',
|
||||
'test/migrations-v1.27.0.0.test.ts',
|
||||
'test/native-auto-decide-pty.test.ts',
|
||||
'test/no-suicide-exit.test.ts',
|
||||
'test/osv-config-wiring.test.ts',
|
||||
'test/outside-voice-fixture.test.ts',
|
||||
'test/overlay-measurement.test.ts',
|
||||
'test/overlay-recording-order.test.ts',
|
||||
'test/overlay-sdk-cancel-eof.test.ts',
|
||||
'test/paid-orphan-tripwire.test.ts',
|
||||
'test/parity-baseline-integrity.test.ts',
|
||||
'test/parity-suite.test.ts',
|
||||
'test/pending-question-completion.test.ts',
|
||||
'test/plan-count-artifacts.test.ts',
|
||||
'test/plan-count-checkbox.test.ts',
|
||||
'test/plan-count-clipped-elision.test.ts',
|
||||
'test/plan-count-collection-completion.test.ts',
|
||||
'test/plan-count-completion.test.ts',
|
||||
'test/plan-count-cropped-wrap.test.ts',
|
||||
'test/plan-count-dx-handoff.test.ts',
|
||||
'test/plan-count-empty-review.test.ts',
|
||||
'test/plan-count-file-permission.test.ts',
|
||||
'test/plan-count-history.test.ts',
|
||||
'test/plan-count-long-edit.test.ts',
|
||||
'test/plan-count-native-input.test.ts',
|
||||
'test/plan-count-owned-permission.test.ts',
|
||||
'test/plan-count-pending-exit.test.ts',
|
||||
'test/plan-count-prerequisite.test.ts',
|
||||
'test/plan-count-preview-footer.test.ts',
|
||||
'test/plan-count-timeout.test.ts',
|
||||
'test/plan-count-transcript.test.ts',
|
||||
'test/plan-count-truncated-border.test.ts',
|
||||
'test/plan-count-truncated-question.test.ts',
|
||||
'test/plan-create-combined-permission.test.ts',
|
||||
'test/plan-create-permission.test.ts',
|
||||
'test/plan-create-prepublication.test.ts',
|
||||
'test/plan-design-floor-fixture.test.ts',
|
||||
'test/plan-edit-cropped-permission.test.ts',
|
||||
'test/plan-floor-dx-actor.test.ts',
|
||||
'test/plan-floor-review.test.ts',
|
||||
'test/plan-floor-target.test.ts',
|
||||
'test/plan-mode-evidence.test.ts',
|
||||
'test/plan-pending-question-pty.test.ts',
|
||||
'test/plan-seed-submission.test.ts',
|
||||
'test/plan-skill-read-permission.test.ts',
|
||||
'test/plan-skill-webfetch-permission.test.ts',
|
||||
'test/plan-tune-cathedral.test.ts',
|
||||
'test/pr-title-rewrite.test.ts',
|
||||
'test/pr-title-sync-workflow-safety.test.ts',
|
||||
'test/pty-askuserquestion-single-line.test.ts',
|
||||
'test/pty-numbered-option-indent-native.test.ts',
|
||||
'test/pty-option-selection.test.ts',
|
||||
'test/pty-output-wake.test.ts',
|
||||
'test/pty-screen-session.test.ts',
|
||||
'test/pty-screen-unicode-ap.test.ts',
|
||||
'test/pty-screen.test.ts',
|
||||
'test/pty-skill-seeding-wiring.test.ts',
|
||||
'test/pty-trust-dialog.test.ts',
|
||||
'test/qa-fix-loop-fixture.test.ts',
|
||||
'test/qa-only-capability.test.ts',
|
||||
'test/question-log-hook.test.ts',
|
||||
'test/readme-throughput.test.ts',
|
||||
'test/redact-prepush-rebase-force-push.test.ts',
|
||||
'test/redact-prepush-scan-range.test.ts',
|
||||
'test/review-army-budget.test.ts',
|
||||
'test/review-consensus-lifecycle.test.ts',
|
||||
'test/review-count-markdown.test.ts',
|
||||
'test/review-enum-lifecycle.test.ts',
|
||||
'test/review-n-plus-one-contract.test.ts',
|
||||
'test/session-runner-stream-lifecycle.test.ts',
|
||||
'test/session-runner-timeout.test.ts',
|
||||
'test/setup-browser-hint.test.ts',
|
||||
'test/setup-bun-cmd-and-pipe-bugs.test.ts',
|
||||
'test/setup-conductor-worktree.test.ts',
|
||||
'test/setup-plan-tune-hooks-noninteractive.test.ts',
|
||||
'test/setup-playwright-platform.test.ts',
|
||||
'test/setup-sections-linking.test.ts',
|
||||
'test/setup-timeline-hook-gate.test.ts',
|
||||
'test/shared-libs-plan-actor.test.ts',
|
||||
'test/shared-libs-revalidation-prompt.test.ts',
|
||||
'test/shared-libs-source-reads.test.ts',
|
||||
'test/ship-coverage-audit-af.test.ts',
|
||||
'test/ship-pr-liveness-policy.test.ts',
|
||||
'test/ship-section-fixture.test.ts',
|
||||
'test/skill-budget-regression.test.ts',
|
||||
'test/skill-parser.test.ts',
|
||||
'test/spawnsync-timeout-tripwire.test.ts',
|
||||
'test/tasks-section-jq.test.ts',
|
||||
'test/taste-engine.test.ts',
|
||||
'test/telemetry-repo-strip.test.ts',
|
||||
'test/test-free-shards-capture.test.ts',
|
||||
'test/timeline.test.ts',
|
||||
'test/ubicloud-runner.test.ts',
|
||||
'test/update-check-crash-sentinel.test.ts',
|
||||
'test/verify-gate.test.ts',
|
||||
'test/workflow-concurrency.test.ts',
|
||||
];
|
||||
|
||||
function freeTests(): string[] {
|
||||
return ['test', 'test/helpers'].flatMap(dir => fs.readdirSync(path.join(ROOT, dir)).map(name => `${dir}/${name}`))
|
||||
.filter(file => file.endsWith('.test.ts') && !isPaidTestFile(file)).sort();
|
||||
}
|
||||
|
||||
function testsOnlyTestCode(file: string): { counted: boolean; helper?: string } {
|
||||
const source = fs.readFileSync(path.join(ROOT, file), 'utf8');
|
||||
const targets = directSpecifiers(source).map(specifier => resolveRepoSpecifier(ROOT, file, specifier)).filter(Boolean) as string[];
|
||||
if (targets.some(target => !target.startsWith('test/') && !target.startsWith('node_modules/'))) return { counted: false };
|
||||
if (/(['"`])[^'"`\n]*(?:\bbin\/|SKILL\.md|\.tmpl)[^'"`\n]*\1/.test(source)) return { counted: false };
|
||||
return { counted: true, helper: targets.find(target => target.startsWith('test/helpers/') && !target.endsWith('.test.ts')) };
|
||||
}
|
||||
|
||||
function suggestedOwner(helper?: string): string {
|
||||
if (!helper) return 'the owner test of the production module it should import';
|
||||
const candidates = [helper.replace(/^test\/helpers\/(.+)\.ts$/, 'test/$1.test.ts'), helper.replace(/\.ts$/, '.test.ts'), helper.replace(/\.ts$/, '.unit.test.ts')];
|
||||
return candidates.find(candidate => fs.existsSync(path.join(ROOT, candidate))) ?? `the test that owns ${helper}`;
|
||||
}
|
||||
|
||||
test('no new test exercises only test code', () => {
|
||||
const listed = new Set(BASELINE);
|
||||
const added = freeTests().map(file => ({ file, ...testsOnlyTestCode(file) }))
|
||||
.filter(entry => entry.counted && !listed.has(entry.file));
|
||||
expect(added.map(entry => entry.file), added.length ? [
|
||||
`${added.length} test file(s) over the baseline: ${added.map(entry => entry.file).join(', ')}.`,
|
||||
'They import only test/ code, so they test a helper or a replayed capture rather than product code.',
|
||||
...added.map(entry => `Fix: add the case as a row in ${suggestedOwner(entry.helper)}, or import the production module under test.`),
|
||||
'If the file is genuinely needed, add its path to BASELINE in test/test-of-test-ratchet.test.ts with a one-line reason.',
|
||||
'AGENTS.md: do not add one spelling or glyph per paid failure.',
|
||||
].join('\n') : '').toEqual([]);
|
||||
});
|
||||
|
||||
test('every baseline entry is still a test file', () => {
|
||||
const stale = BASELINE.filter(file => !fs.existsSync(path.join(ROOT, file)));
|
||||
expect(stale, stale.map(file => `remove ${file} from the baseline`).join('\n')).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user