Files
gstack/test/pr-dependencies.test.ts
T
Garry Tanandcapy-ai[bot] a669dafcb4 v1.91.129.0 fix(pr-selection): scan shebang and .ts imports, fail closed on scan errors, report cost per run (#3257)
* fix(pr-selection): scan shebang and .ts imports, fail closed on scan errors, classify repo config safely

A1-1: strip a leading #! before scanImports at every site, treat bun/node
shebang scripts as code, pick the loader by extension (the TSX loader
misread 15 .ts files), and restore the full gate when any other import
scan fails. Broadens selection only: 0 (file, case) pairs lost vs main,
41 files move from no-consumer to consumed.

A1-2: FREE_ONLY_PR_FILES gains six repository-config files, capy/ is
derivable, and a full-gate file is labelled 'full-gate input' instead of
'real unknown dependency'. The replay MISS check honours the same audited
free-only exemptions as the selector.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(test-health): report gate cases and known cost per PR run, lead with cost per merged PR (A1-3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(testing): correct the PR fallback section, document the lockfile decision (A1-3, A1-4)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(harness): record the A1 test-pr-profile.ts change as non-behavioral (selection only)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(release): v1.91.127.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Revert "chore(release): v1.91.127.0"

This reverts commit 79e3ef6c63.

* chore(release): v1.91.129.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-18 01:22:15 +00:00

329 lines
20 KiB
TypeScript

/**
* Derived PR-lane dependencies (scripts/pr-dependencies.ts) and the
* fail-closed full-gate list (PR_FULL_GATE_FILES): unmapped files select the
* gate cases whose reference closure reaches them, underivable files and
* global inputs restore the full gate, and every fallback names its fix.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { deriveDependencies, derivedDependencies, referenceText } from '../scripts/pr-dependencies';
import {
DERIVABLE_PREFIXES, PERIODIC_ONLY_PR_FILES, PR_FULL_GATE_FILES, PR_PROFILE_MAPS, selectPrProfile, unknownFileLabel,
} from '../scripts/test-pr-profile';
import { buildRunManifest, computePaidCaseSelection } from '../scripts/test-paid-shards';
import { E2E_TIERS } from './helpers/touchfiles-data';
import { matchGlob } from './helpers/test-selection';
const ROOT = path.resolve(import.meta.dir, '..');
const real = derivedDependencies(PR_PROFILE_MAPS, ROOT);
const gate = Object.keys(E2E_TIERS).filter(id => E2E_TIERS[id] === 'gate');
const FIX = 'docs/TESTING_INTERNALS.md#pr-paid-lane-fallback';
function fixtureRepo(files: Record<string, string>): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pr-deps-'));
for (const [file, text] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
}
for (const args of [['init', '-q'], ['add', '-A']]) {
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', timeout: 10_000 });
if (result.status !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr}`);
}
return root;
}
describe('reference derivation on a fixture tree', () => {
const root = fixtureRepo({
'test/skill-e2e-alpha.test.ts': "import { help } from './helpers/alpha-helper';\nimport { other } from './skill-e2e-beta.test';\n",
'test/skill-e2e-beta.test.ts': "export const other = 'lib/beta-only.ts';\n",
'test/helpers/alpha-helper.ts': "import { x } from '../../lib/alpha';\n// prose about lib/commented.ts is not a reference\nexport const help = x;\n",
'test/helpers/touchfiles-data.ts': "export const MAP = { a: ['lib/listed-only.ts'] };\n",
'test/helpers/joined.ts': "export const p = path.join(ROOT, 'lib', 'joined.ts');\n", 'lib/joined.ts': '',
'lib/alpha.ts': "import './../test/helpers/joined';\nexport const x = 1;\n", 'lib/beta-only.ts': '', 'lib/commented.ts': '', 'lib/listed-only.ts': '', 'lib/bin-lib.ts': '', 'lib/unused.ts': '',
'alpha/SKILL.md.tmpl': '{{GREETING}}\nRun `gstack-tool` then open design/dist/design.\n',
'bin/gstack-tool': "#!/bin/sh\n# mentions lib/commented.ts only in a comment\nbun run \"$ROOT/lib/bin-lib.ts\"\n",
'scripts/build.sh': 'bun build --compile design/src/cli.ts --outfile design/dist/design\n',
'design/src/cli.ts': "import './render';\n", 'design/src/render.ts': '',
'scripts/resolvers/index.ts': "import { greet } from './greeting';\nexport const RESOLVERS = {\n GREETING: greet,\n};\n",
'scripts/resolvers/greeting.ts': "import { tone } from './tone';\nexport const greet = () => tone;\n", 'scripts/resolvers/tone.ts': 'export const tone = 1;\n',
'hosts/acme.ts': '', 'hosts/zeta.ts': '',
'package.json': '{"scripts":{}}\n',
});
const maps = { e2eTouchfiles: { alpha: ['test/skill-e2e-alpha.test.ts', 'alpha/**'], beta: ['test/skill-e2e-beta.test.ts'] }, judgeTouchfiles: {}, globalTouchfiles: [] };
const derived = deriveDependencies(maps, root);
const consumers = (file: string) => [...derived.e2e.get(file) ?? []].sort();
test('imports, bin invocations, compiled binaries and template placeholders all reach their files', () => {
expect(consumers('lib/alpha.ts')).toEqual(['alpha']);
expect(consumers('lib/joined.ts')).toEqual(['alpha']);
expect(consumers('bin/gstack-tool')).toEqual(['alpha']);
expect(consumers('lib/bin-lib.ts')).toEqual(['alpha']);
expect(consumers('design/src/render.ts')).toEqual(['alpha']);
expect(consumers('scripts/resolvers/tone.ts')).toEqual(['alpha']);
expect(derived.trace('alpha', 'scripts/resolvers/tone.ts')).toEqual(['alpha/SKILL.md.tmpl', 'scripts/resolvers/greeting.ts', 'scripts/resolvers/tone.ts']);
});
test('comments, selection data and other paid test files are not references', () => {
expect(consumers('lib/commented.ts')).toEqual([]);
expect(consumers('lib/listed-only.ts')).toEqual([]);
expect(consumers('lib/beta-only.ts')).toEqual(['beta']);
expect(consumers('lib/unused.ts')).toEqual([]);
expect(referenceText('a.ts', ' // lib/x.ts\nconst y = "lib/z.ts"; // trailing lib/w.ts\n')).toContain('lib/z.ts');
expect(referenceText('a.ts', ' // lib/x.ts\n')).not.toContain('lib/x.ts');
expect(referenceText('bin/x', '# lib/x.ts\necho lib/y.ts\n')).toBe('\necho lib/y.ts\n');
expect(referenceText('data.json', '{"a":"lib/x.ts"}')).toBe('');
});
test('trace names the shortest chain: a template that invokes a bin directly wins over a long import path to it', () => {
const repo = fixtureRepo({
'test/skill-e2e-gamma.test.ts': '', 'gamma/SKILL.md.tmpl': 'Run bin/gstack-target.\n', 'gamma/sections/a.md.tmpl': 'See lib/one.ts.\n',
'lib/one.ts': "import './two';\n", 'lib/two.ts': "import './three';\n", 'lib/three.ts': "export const x = 'bin/gstack-target';\n",
'bin/gstack-target': '#!/bin/sh\n', 'package.json': '{}',
});
const traced = deriveDependencies({ e2eTouchfiles: { gamma: ['test/skill-e2e-gamma.test.ts', 'gamma/**'] }, judgeTouchfiles: {}, globalTouchfiles: [] }, repo);
expect(traced.trace('gamma', 'bin/gstack-target')).toEqual(['gamma/SKILL.md.tmpl', 'bin/gstack-target']);
expect(traced.trace('gamma', 'lib/three.ts')).toEqual(['gamma/sections/a.md.tmpl', 'lib/one.ts', 'lib/two.ts', 'lib/three.ts']);
});
test('a host config feeds only cases whose test names the host', () => {
const named = deriveDependencies({ ...maps, e2eTouchfiles: { ...maps.e2eTouchfiles, beta: ['test/skill-e2e-beta.test.ts'] } },
fixtureRepo({ 'test/skill-e2e-beta.test.ts': "run({ host: 'acme' });\n", 'hosts/acme.ts': '', 'hosts/zeta.ts': '', 'scripts/build.sh': '', 'scripts/resolvers/index.ts': '', 'package.json': '{}' }));
expect([...named.e2e.get('hosts/acme.ts') ?? []]).toEqual(['beta']);
expect(named.e2e.get('hosts/zeta.ts')).toBeUndefined();
});
});
describe('shebang files and import scan failures (A1-1)', () => {
const tree = (extra: Record<string, string> = {}) => fixtureRepo({
'test/skill-e2e-alpha.test.ts': "import '../lib/entry';\n", 'test/skill-e2e-beta.test.ts': '',
'lib/entry.ts': '#!/usr/bin/env bun\nimport { y } from \'./typed\';\nexport const x = y;\n',
'lib/typed.ts': 'export const y = <T,>(v: T) => v as unknown as T;\nimport \'./typed-dep\';\n', 'lib/typed-dep.ts': '',
'alpha/SKILL.md.tmpl': 'Run `gstack-sb`.\n', 'bin/gstack-sb': '#!/usr/bin/env -S bun run\nimport { z } from \'../lib/sb-dep\';\n', 'lib/sb-dep.ts': '',
'bin/gstack-sh': '#!/bin/sh\nimport ../lib/sh-only.ts\n', 'lib/sh-only.ts': '',
'scripts/build.sh': '', 'scripts/resolvers/index.ts': '', 'package.json': '{}', ...extra,
});
const maps = { e2eTouchfiles: { alpha: ['test/skill-e2e-alpha.test.ts', 'alpha/**'], beta: ['test/skill-e2e-beta.test.ts'] }, judgeTouchfiles: {}, globalTouchfiles: [] as string[] };
const profileMaps = { ...maps, tiers: { alpha: 'gate', beta: 'gate' } as const };
const pick = (derived: ReturnType<typeof deriveDependencies>, changedFiles: string[]) =>
selectPrProfile({ maps: profileMaps, profile: [], selectedE2E: [], selectedJudges: [], changedFiles, derived });
test('a shebang .ts file, a bun-shebang extensionless bin and a .ts generic arrow are all import-scanned', () => {
const derived = deriveDependencies(maps, tree());
expect([...derived.e2e.get('lib/typed.ts') ?? []]).toEqual(['alpha']);
expect([...derived.e2e.get('lib/typed-dep.ts') ?? []]).toEqual(['alpha']);
expect([...derived.e2e.get('lib/sb-dep.ts') ?? []]).toEqual(['alpha']);
expect(derived.e2e.get('lib/sh-only.ts')).toBeUndefined();
expect(derived.scanErrors.size).toBe(0);
expect(pick(derived, ['lib/sb-dep.ts'])).toMatchObject({ mode: 'dependents', e2e: ['alpha'] });
});
test('lib/fs-utils.ts selects the cases of bin/gstack-redact-prepush, which imports it', () => {
const bin = gate.filter(id => real.e2e.get('bin/gstack-redact-prepush')?.has(id));
expect(bin.length).toBeGreaterThan(0);
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: ['lib/fs-utils.ts'], packageVersionOnly: true });
expect(result.coverage?.mode).toBe('dependents');
for (const id of bin) expect(result.selection.e2e, id).toContain(id);
expect(real.scanErrors.size).toBe(0);
expect(real.global.has('test/helpers/bootstrap-retention.ts')).toBe(true);
});
test('any other scan error fails closed at every site: case closures, the global walk and deleted-file referencers', () => {
const broken = "import { from './nowhere';\n";
const inClosure = deriveDependencies(maps, tree({ 'lib/typed-dep.ts': broken }));
expect([...inClosure.scanErrors.keys()]).toEqual(['lib/typed-dep.ts']);
const closed = pick(inClosure, ['lib/sb-dep.ts']);
expect(closed).toMatchObject({ mode: 'full-fallback', e2e: ['alpha', 'beta'], unknownFiles: ['lib/sb-dep.ts'] });
expect(closed.unknownFileLabels[0]).toMatchObject({ label: 'import scan failed' });
expect(closed.unknownFileLabels[0]!.fix).toContain('lib/typed-dep.ts');
const inGlobal = deriveDependencies({ ...maps, globalTouchfiles: ['lib/runner.ts'] }, tree({ 'lib/runner.ts': broken }));
expect([...inGlobal.scanErrors.keys()]).toEqual(['lib/runner.ts']);
expect(pick(inGlobal, ['lib/sb-dep.ts']).mode).toBe('full-fallback');
const unreached = deriveDependencies(maps, tree({ 'lib/orphan.ts': broken }));
expect(unreached.scanErrors.size).toBe(0);
expect(pick(unreached, ['lib/gone.ts'])).toMatchObject({ mode: 'full-fallback', unknownFiles: ['lib/gone.ts'] });
expect([...unreached.scanErrors.keys()]).toEqual(['lib/orphan.ts']);
});
});
describe('safe classification (A1-2)', () => {
test('capy/support.env derives to the bins that read it instead of restoring the full gate', () => {
for (const bin of ['bin/gstack-install-verdict.sh', 'bin/gstack-doctor-check.sh']) {
for (const id of real.e2e.get(bin) ?? []) expect(real.e2e.get('capy/support.env')?.has(id), `${bin} ${id}`).toBe(true);
}
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: ['capy/support.env'], packageVersionOnly: true });
expect(result.coverage?.unknownFiles).toEqual([]);
expect(result.coverage?.mode).toBe('dependents');
expect(DERIVABLE_PREFIXES).toContain('capy/');
});
test('repository config read only by free lanes or GitHub stays in the PR profile', () => {
const files = ['.github/dependabot.yml', '.osv-scanner.toml', 'setup.ps1', '.gitlab-ci.yml', '.github/ISSUE_TEMPLATE/bug_report.md', '.gstack/ship-policy.example.json'];
for (const file of files) expect(real.tracked.has(file), file).toBe(true);
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: files, packageVersionOnly: true });
expect(result.coverage?.mode).toBe('pr');
expect(result.selection).toEqual({ e2e: [], judges: [] });
});
test('a full-gate file is never labelled a real unknown dependency', () => {
for (const file of ['.github/workflows/evals.yml', 'test-setup.ts', '.gitattributes']) {
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [file], packageVersionOnly: true });
expect(result.coverage?.mode, file).toBe('full-fallback');
for (const label of result.coverage!.unknownFileLabels) {
expect(label.label, file).toBe('full-gate input');
expect(label.fix).toContain('PR_FULL_GATE_FILES');
}
expect(unknownFileLabel(file).label).toBe('full-gate input');
}
expect(unknownFileLabel('ETHOS.md').label).toBe('real unknown dependency');
});
});
describe('derived dependencies of this checkout', () => {
test('a bin script a skill template invokes selects that skill\'s cases', () => {
const consumers = real.e2e.get('bin/gstack-next-version') ?? new Set();
expect(consumers.has('review-sql-injection')).toBe(true);
expect(real.trace('review-sql-injection', 'bin/gstack-next-version')?.[0]).toBe('review/SKILL.md.tmpl');
});
test('a compiled design source reaches exactly the cases whose closure names the design binary', () => {
const consumers = [...real.e2e.get('design/src/evolve.ts') ?? []];
expect(consumers.length).toBeGreaterThan(0);
expect(consumers.length).toBeLessThan(Object.keys(E2E_TIERS).length);
for (const id of consumers.slice(0, 5)) expect(real.trace(id, 'design/src/evolve.ts')?.some(file => file === 'design/src/cli.ts' || file.startsWith('design/src/'))).toBe(true);
});
test('the paid runner\'s import closure is global, and only that', () => {
for (const file of ['scripts/test-paid-shards.ts', 'scripts/e2e-shard-reuse.ts', 'test/helpers/session-runner.ts', 'test/helpers/eval-store.ts', 'scripts/lib/shard-engine.ts']) {
expect(real.global.has(file), file).toBe(true);
}
expect(real.global.has('bin/gstack-next-version')).toBe(false);
expect(real.global.size).toBeLessThan(120);
});
});
describe('PR selection with derived dependencies', () => {
const select = (changedFiles: string[]) => computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles, packageVersionOnly: true });
test('an unmapped file with derived consumers runs every dependent gate case and nothing else', () => {
const result = select(['design/src/evolve.ts']);
expect(result.coverage?.mode).toBe('dependents');
const expected = gate.filter(id => real.e2e.get('design/src/evolve.ts')?.has(id)).sort();
expect(result.selection.e2e).toEqual(expected);
expect(result.coverage?.derivedFiles).toEqual([{ file: 'design/src/evolve.ts', e2e: real.e2e.get('design/src/evolve.ts')!.size, judges: real.judges.get('design/src/evolve.ts')?.size ?? 0 }]);
const manifest = buildRunManifest({ tier: 'gate', profile: 'pr', sliceCount: 2, evalsAll: false, env: {}, changedFiles: ['design/src/evolve.ts'] });
expect(manifest.prCoverage?.mode).toBe('dependents');
expect(manifest.entries.some(entry => entry.status === 'planned')).toBe(true);
});
test('a tracked file no paid case reaches is exempt; periodic-only workflows never run the PR gate', () => {
const result = select(['scripts/typecheck-test.ts', ...PERIODIC_ONLY_PR_FILES]);
expect(result.coverage?.mode).toBe('pr');
expect(result.coverage?.noConsumerFiles).toEqual(['scripts/typecheck-test.ts']);
expect(result.selection).toEqual({ e2e: [], judges: [] });
});
test('a paid-runner import selects every case, as a global touchfile does', () => {
const viaClosure = select(['scripts/e2e-shard-reuse.ts']);
const viaGlobal = select(['scripts/test-paid-shards.ts']);
expect(viaClosure.coverage?.mode).toBe('pr');
expect(viaClosure.selection).toEqual(viaGlobal.selection);
});
test('derivation is skipped for fixture maps unless passed explicitly', () => {
const result = selectPrProfile({ maps: { ...PR_PROFILE_MAPS, e2eTouchfiles: { ...PR_PROFILE_MAPS.e2eTouchfiles } }, selectedE2E: [], selectedJudges: [], changedFiles: ['design/src/evolve.ts'] });
expect(result.mode).toBe('full-fallback');
});
});
describe('the fail-closed full-gate list', () => {
const tracked = [...real.tracked];
test('every entry matches a tracked file', () => {
const stale = PR_FULL_GATE_FILES.filter(pattern => !tracked.some(file => matchGlob(file, pattern)));
expect(stale.map(pattern => `${pattern}: matches no tracked file. Fix: delete it from PR_FULL_GATE_FILES in scripts/test-pr-profile.ts (${FIX})`)).toEqual([]);
});
test('every entry restores the full gate even when a touchfile or a derived reference also names it', () => {
for (const pattern of PR_FULL_GATE_FILES) {
const file = tracked.find(candidate => matchGlob(candidate, pattern))!;
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [file], packageVersionOnly: false });
expect({ file, mode: result.coverage?.mode }).toEqual({ file, mode: 'full-fallback' });
expect(result.selection.e2e).toEqual(gate.sort());
}
});
test('the setup, preamble, eval policy and PR workflow inputs are on it', () => {
for (const file of ['setup', 'scripts/resolvers/preamble.ts', 'test/helpers/periodic-exclude-data.ts', '.github/workflows/evals.yml', 'bun.lock']) {
expect(PR_FULL_GATE_FILES.some(pattern => matchGlob(file, pattern)), file).toBe(true);
}
});
test('a file the derivation cannot place restores the full gate with its fix', () => {
for (const file of ['ETHOS.md', 'conductor.json']) {
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [file], packageVersionOnly: true });
expect(result.coverage?.mode, file).toBe('full-fallback');
const label = result.coverage!.unknownFileLabels[0]!;
expect(label.file).toBe(file);
expect(label.fix).toContain(file);
expect(label.fix).toContain('touchfiles-data.ts');
expect(label.fix).toContain('DERIVABLE_PREFIXES');
}
expect(DERIVABLE_PREFIXES).not.toContain('test/fixtures/');
});
});
describe('deleted files (paths absent from the head tree)', () => {
const without = (file: string) => ({ ...real, tracked: new Set([...real.tracked].filter(other => other !== file)) });
const pick = (file: string, derived = without(file)) => selectPrProfile({ selectedE2E: [], selectedJudges: [], changedFiles: [file], derived });
test('a deleted file nothing references and no touchfile matches has no dependents', () => {
const result = pick('scripts/retired-tool-nobody-calls.ts', real);
expect(result.mode).toBe('pr');
expect(result.noConsumerFiles).toEqual(['scripts/retired-tool-nobody-calls.ts']);
expect(result.e2e).toEqual([]);
});
test('a deleted file a touchfile matches selects that touchfile\'s cases', () => {
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: ['browse/src/retired-module.ts'], packageVersionOnly: true });
expect(result.coverage?.unknownFiles).toEqual([]);
expect(result.coverage?.mode).toBe('pr');
expect(result.selection.e2e).toContain('browse-basic');
});
test('a deleted file live code still references selects the referencing files\' dependents', () => {
const result = pick('bin/gstack-next-version');
expect(result.mode).toBe('dependents');
expect(result.e2e).toContain('review-sql-injection');
expect(result.derivedFiles[0]?.file).toBe('bin/gstack-next-version');
expect(real.referencers('bin/gstack-next-version')).toContain('review/SKILL.md.tmpl');
});
test('a deleted file a full-gate input still references, or a deleted full-gate entry, restores the full gate', () => {
expect(real.referencers('bin/gstack-install-registry.sh')).toContain('setup');
const referenced = pick('bin/gstack-install-registry.sh');
expect(referenced.mode).toBe('full-fallback');
expect(referenced.unknownFileLabels[0]).toMatchObject({ file: 'bin/gstack-install-registry.sh', label: 'deleted but still referenced' });
expect(referenced.unknownFileLabels[0]!.fix).toContain('setup');
expect(pick('bunfig.toml').mode).toBe('full-fallback');
});
test('comments and data files never count as references to a deleted path', () => {
const root = fixtureRepo({
'test/skill-e2e-alpha.test.ts': "// lib/gone.ts used to live here\nimport './helpers/h';\n",
'test/helpers/h.ts': "export const data = 'x';\n", 'data/list.json': '["lib/gone.ts"]',
'scripts/build.sh': '', 'scripts/resolvers/index.ts': '', 'package.json': '{}',
});
const derived = deriveDependencies({ e2eTouchfiles: { alpha: ['test/skill-e2e-alpha.test.ts'] }, judgeTouchfiles: {}, globalTouchfiles: [] }, root);
expect(derived.referencers('lib/gone.ts')).toEqual([]);
const relative = fixtureRepo({ 'test/skill-e2e-alpha.test.ts': "import './helpers/gone';\n", 'scripts/build.sh': '', 'scripts/resolvers/index.ts': '', 'package.json': '{}' });
expect(deriveDependencies({ e2eTouchfiles: { alpha: ['test/skill-e2e-alpha.test.ts'] }, judgeTouchfiles: {}, globalTouchfiles: [] }, relative)
.referencers('test/helpers/gone.ts')).toEqual(['test/skill-e2e-alpha.test.ts']);
});
});