mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
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>
This commit is contained in:
1 parent
c10c139ed6
commit
4a1da453fa
6 files changed
+153
-22
No files matched your search
+41
-14
@@ -49,6 +49,12 @@ export interface DerivedDependencies {
|
||||
tracked: Set<string>;
|
||||
/** The reference chain from an E2E case's declared inputs to `file` (for fix text and audits), else null. */
|
||||
trace(caseId: string, file: string): string[] | null;
|
||||
/**
|
||||
* Files whose import scan failed for a reason other than a leading `#!`
|
||||
* line (their imports are unknown). PR selection restores the full gate
|
||||
* while this is non-empty; it fills as the derivation reads files.
|
||||
*/
|
||||
scanErrors: Map<string, string>;
|
||||
/**
|
||||
* Tracked files whose reference text still names `missing`, a path absent
|
||||
* from the tree (deleted in the diff): its path or extensionless import
|
||||
@@ -60,8 +66,10 @@ export interface DerivedDependencies {
|
||||
}
|
||||
|
||||
const CODE = /\.(?:[cm]?[jt]s|tsx|jsx)$/;
|
||||
/** An extensionless script Bun or Node runs: its imports are code, not shell. */
|
||||
const CODE_SHEBANG = /^#![^\n]*\b(?:bun|node)\b/;
|
||||
const TEXT = /\.(?:[cm]?[jt]s|tsx|jsx|sh|md|tmpl|json|ya?ml|toml|txt|py)$/;
|
||||
const REPO_PATH = /(?:^|[^\w.@-])((?:bin|lib|scripts|browse|design|make-pdf|hosts|extension|model-overlays|agents|test\/helpers|test\/fixtures)\/[\w.@-]+(?:\/[\w.@-]+)*)/g;
|
||||
const REPO_PATH = /(?:^|[^\w.@-])((?:bin|lib|scripts|browse|design|make-pdf|hosts|extension|model-overlays|agents|capy|test\/helpers|test\/fixtures)\/[\w.@-]+(?:\/[\w.@-]+)*)/g;
|
||||
/** `path.join(ROOT, 'lib', 'x.ts')`: a top-level directory and its entry as separate string literals. */
|
||||
const JOINED_PATH = /['"`](bin|lib|scripts|browse|design|make-pdf|hosts|extension|model-overlays|agents)['"`]\s*,\s*['"`]([\w.@-]+)['"`]/g;
|
||||
const BIN_NAME = /\bgstack-[a-z0-9][a-z0-9-]*[a-z0-9]\b/g;
|
||||
@@ -85,7 +93,26 @@ export const SELECTION_DATA_MODULES: readonly string[] = [
|
||||
];
|
||||
const DEFAULT_JUDGE_OWNER = 'test/skill-llm-eval.test.ts';
|
||||
|
||||
const transpiler = new Bun.Transpiler({ loader: 'tsx' });
|
||||
const transpilers = {
|
||||
ts: new Bun.Transpiler({ loader: 'ts' }), tsx: new Bun.Transpiler({ loader: 'tsx' }),
|
||||
js: new Bun.Transpiler({ loader: 'js' }), jsx: new Bun.Transpiler({ loader: 'jsx' }),
|
||||
};
|
||||
|
||||
/** Code by extension, or an extensionless file whose shebang runs Bun or Node. */
|
||||
export function isCodeFile(file: string, text: string): boolean {
|
||||
return CODE.test(file) || (!/\.[^/]+$/.test(path.posix.basename(file)) && CODE_SHEBANG.test(text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative import specifiers of a code file. A leading `#!` line is blanked
|
||||
* first (`scanImports` rejects it), and each extension gets its own loader
|
||||
* (the TSX loader misreads `.ts` generics and casts). Any other parse error
|
||||
* throws, so callers fail closed.
|
||||
*/
|
||||
export function scanCodeImports(file: string, text: string): string[] {
|
||||
const loader = /\.tsx$/.test(file) ? 'tsx' : /\.jsx$/.test(file) ? 'jsx' : /\.[cm]?js$/.test(file) ? 'js' : 'ts';
|
||||
return transpilers[loader].scanImports(text.replace(/^#![^\n]*/, '')).map(item => item.path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Text a reference scan reads: whole-line comments are prose about other
|
||||
@@ -168,15 +195,17 @@ export function deriveDependencies(maps: DependencyMaps, root: string): DerivedD
|
||||
};
|
||||
const dirs = new Set(tracked.flatMap(file => file.split('/').slice(0, -1).map((_, i, parts) => parts.slice(0, i + 1).join('/'))));
|
||||
|
||||
const scanErrors = new Map<string, string>();
|
||||
const importsOf = (file: string): string[] => {
|
||||
try { return scanCodeImports(file, read(file)); } catch (error) { scanErrors.set(file, String(error)); return []; }
|
||||
};
|
||||
const edges = new Map<string, string[]>();
|
||||
const refs = (file: string): string[] => {
|
||||
if (edges.has(file)) return edges.get(file)!;
|
||||
const raw = read(file);
|
||||
const text = (SELECTION_DATA_MODULES as readonly string[]).includes(file) ? '' : referenceText(file, raw);
|
||||
const out = new Set<string>();
|
||||
if (CODE.test(file)) {
|
||||
try { for (const item of transpiler.scanImports(raw)) { const hit = resolveSpecifier(root, item.path, file); if (hit) out.add(hit); } } catch { /* literal scan below still applies */ }
|
||||
}
|
||||
if (isCodeFile(file, raw)) for (const specifier of importsOf(file)) { const hit = resolveSpecifier(root, specifier, file); if (hit) out.add(hit); }
|
||||
for (const match of text.matchAll(REPO_PATH)) {
|
||||
const ref = match[1]!.replace(/[.]+$/, '');
|
||||
if (trackedSet.has(ref)) out.add(ref);
|
||||
@@ -238,19 +267,17 @@ export function deriveDependencies(maps: DependencyMaps, root: string): DerivedD
|
||||
const file = pending.pop()!;
|
||||
if (global.has(file)) continue;
|
||||
global.add(file);
|
||||
if (!CODE.test(file)) continue;
|
||||
try { for (const item of transpiler.scanImports(read(file))) { const hit = resolveSpecifier(root, item.path, file); if (hit && !global.has(hit)) pending.push(hit); } } catch { /* unparseable: its own entry stays global */ }
|
||||
if (!isCodeFile(file, read(file))) continue;
|
||||
for (const specifier of importsOf(file)) { const hit = resolveSpecifier(root, specifier, file); if (hit && !global.has(hit)) pending.push(hit); }
|
||||
}
|
||||
const importTargets = new Map<string, string[]>();
|
||||
const targetsOf = (file: string): string[] => {
|
||||
if (!importTargets.has(file)) {
|
||||
const targets: string[] = [];
|
||||
if (CODE.test(file) && !(SELECTION_DATA_MODULES as readonly string[]).includes(file)) {
|
||||
try {
|
||||
for (const item of transpiler.scanImports(read(file))) {
|
||||
if (item.path.startsWith('.')) targets.push(path.posix.normalize(path.posix.join(path.posix.dirname(file), item.path)));
|
||||
}
|
||||
} catch { /* literal mentions below still apply */ }
|
||||
if (isCodeFile(file, read(file)) && !(SELECTION_DATA_MODULES as readonly string[]).includes(file)) {
|
||||
for (const specifier of importsOf(file)) {
|
||||
if (specifier.startsWith('.')) targets.push(path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier)));
|
||||
}
|
||||
}
|
||||
importTargets.set(file, targets);
|
||||
}
|
||||
@@ -287,7 +314,7 @@ export function deriveDependencies(maps: DependencyMaps, root: string): DerivedD
|
||||
while (parents.has(chain[0]!)) chain.unshift(parents.get(chain[0]!)!);
|
||||
return chain;
|
||||
};
|
||||
return { e2e, judges: consumers(maps.judgeTouchfiles, DEFAULT_JUDGE_OWNER), everyCase, global, tracked: trackedSet, trace, referencers };
|
||||
return { e2e, judges: consumers(maps.judgeTouchfiles, DEFAULT_JUDGE_OWNER), everyCase, global, tracked: trackedSet, trace, referencers, scanErrors };
|
||||
}
|
||||
|
||||
const cache = new Map<string, DerivedDependencies>();
|
||||
|
||||
@@ -26,7 +26,7 @@ import { packageChangeOnlyVersion } from './test-pr-profile';
|
||||
import { E2E_TIERS, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from '../test/helpers/touchfiles-data';
|
||||
import { matchGlob } from '../test/helpers/test-selection';
|
||||
import { derivedDependencies } from './pr-dependencies';
|
||||
import { PR_PROFILE_MAPS } from './test-pr-profile';
|
||||
import { knownNonBehaviorFile, PR_PROFILE_MAPS } from './test-pr-profile';
|
||||
|
||||
export interface ReplayInput {
|
||||
runId: number;
|
||||
@@ -59,7 +59,9 @@ export interface ReplayRow {
|
||||
/**
|
||||
* Whether a case depends on any changed file under today's dependency graph:
|
||||
* its touchfiles, the globals, and the derived reference closure
|
||||
* (scripts/pr-dependencies.ts, its global runner closure included).
|
||||
* (scripts/pr-dependencies.ts, its global runner closure included). Like
|
||||
* the selector, a touchfile match wins over the audited free-only exemptions,
|
||||
* and an exempt file reaches no case through the derived closure.
|
||||
*/
|
||||
export function caseTouchesDiff(id: string, changedFiles: readonly string[]): boolean {
|
||||
const patterns = [...(E2E_TOUCHFILES[id] ?? []), ...GLOBAL_TOUCHFILES];
|
||||
@@ -67,7 +69,7 @@ export function caseTouchesDiff(id: string, changedFiles: readonly string[]): bo
|
||||
// A path absent from today's tree reaches a case through the live files that still reference it.
|
||||
const via = (file: string) => derived.tracked.has(file) ? [file] : derived.referencers(file);
|
||||
return changedFiles.some(file => patterns.some(pattern => matchGlob(file, pattern))
|
||||
|| via(file).some(source => derived.global.has(source) || !!derived.e2e.get(source)?.has(id)));
|
||||
|| (!knownNonBehaviorFile(file) && via(file).some(source => derived.global.has(source) || !!derived.e2e.get(source)?.has(id))));
|
||||
}
|
||||
|
||||
export function replayRun(input: ReplayInput, oldSelection: readonly string[]): ReplayRow {
|
||||
|
||||
@@ -216,6 +216,12 @@ export const FREE_ONLY_PR_FILES = [
|
||||
'.gstack/generated.json', // gstack's own regen registry: gstack-regen/gstack-pregate read it from origin/<base> of the repo they run in; paid cases run in fixture repos.
|
||||
'.gstack/pregate.json', // gstack's own pre-gate policy: same trust boundary, same reason.
|
||||
'.gstack/ship-policy.json', // gstack's own ship policy: gstack-ship-policy/gstack-restamp read it from origin/<base> of the repo they run in; same reason.
|
||||
'.gstack/ship-policy.example.json', // The example beside it: only .gitignore and docs/ship-policy.md name it.
|
||||
'.github/dependabot.yml', // Dependabot's own config: read by GitHub, and by free test/compare-pr-version.test.ts; dependabot runs skip the paid lane.
|
||||
'.osv-scanner.toml', // Read by .github/workflows/osv-scanner.yml and the free OSV tests (test/osv-*.test.ts).
|
||||
'setup.ps1', // The Windows installer: run only by .github/workflows/windows-setup-e2e.yml; the paid lane is Linux.
|
||||
'.gitlab-ci.yml', // gstack's own GitLab config: skills probe the fixture repo's .gitlab-ci.yml, never this one.
|
||||
'.github/ISSUE_TEMPLATE/**', // Issue templates: read by GitHub and free test/gstack-doctor.test.ts.
|
||||
'scripts/retired-command.ts', // One-release stubs for retired package scripts; no paid case imports it.
|
||||
'careful/bin/check-careful.sh', // /careful PreToolUse hook: covered by test/hook-scripts.test.ts; no paid fixture copies or runs it.
|
||||
// Reporting and launch tools: they read CI history or start a lane, never run inside a paid case.
|
||||
@@ -273,9 +279,9 @@ export const PR_FULL_GATE_FILES = [
|
||||
export const PERIODIC_ONLY_PR_FILES = ['.github/workflows/evals-periodic.yml', '.github/workflows/evals-marathon.yml'] as const;
|
||||
|
||||
/** Directories the reference derivation covers: an unmapped file there with no consumer is consumed by no paid case. */
|
||||
export const DERIVABLE_PREFIXES = ['bin/', 'lib/', 'scripts/', 'browse/', 'design/', 'make-pdf/', 'hosts/', 'extension/', 'model-overlays/', 'agents/', 'test/helpers/'] as const;
|
||||
export const DERIVABLE_PREFIXES = ['bin/', 'lib/', 'scripts/', 'browse/', 'design/', 'make-pdf/', 'hosts/', 'extension/', 'model-overlays/', 'agents/', 'test/helpers/', 'capy/'] as const;
|
||||
|
||||
function knownNonBehaviorFile(file: string): boolean {
|
||||
export function knownNonBehaviorFile(file: string): boolean {
|
||||
// A mapped dependency still wins over these exemptions. New helper,
|
||||
// runtime, dependency, or paid-workflow files are deliberately not exempted.
|
||||
return /^(?:docs\/|(?:README|CONTRIBUTING|ARCHITECTURE|CHANGELOG|TODOS)\.md$|VERSION$)/.test(file)
|
||||
@@ -293,6 +299,9 @@ export const FALLBACK_FIX_ANCHOR = 'docs/TESTING_INTERNALS.md#pr-paid-lane-fallb
|
||||
|
||||
/** Why one changed file restored the full gate, and the edit that narrows it next time (DX-6). */
|
||||
export function unknownFileLabel(file: string): { label: string; fix: string } {
|
||||
if (matches(file, PR_FULL_GATE_FILES)) {
|
||||
return { label: 'full-gate input', fix: `${file} is in PR_FULL_GATE_FILES (scripts/test-pr-profile.ts): every paid case consumes it, so the full gate is intended and no mapping narrows it` };
|
||||
}
|
||||
if (isPaidTestFile(file) || file.startsWith('test/helpers/')) {
|
||||
return { label: 'needs touchfile entry', fix: `register ${file} under the cases that consume it in test/helpers/touchfiles-data.ts` };
|
||||
}
|
||||
@@ -380,6 +389,12 @@ export function selectPrProfile(options: {
|
||||
if (found.e2e.size + found.judges.size > 0) consumers.set(file, found);
|
||||
else noConsumerFiles.push(file);
|
||||
}
|
||||
// A failed import scan leaves some closure unknown: every unmapped file restores the full gate (fail closed).
|
||||
const scanFailed = derived && unmapped.length ? [...derived.scanErrors.keys()].sort() : [];
|
||||
if (scanFailed.length) {
|
||||
consumers.clear(); derivedGlobal.length = 0; noConsumerFiles.length = 0;
|
||||
for (const file of unmapped) deletedLabels.set(file, { label: 'import scan failed', fix: `scripts/pr-dependencies.ts could not scan the imports of ${scanFailed.slice(0, 3).join(', ')}${scanFailed.length > 3 ? ` and ${scanFailed.length - 3} more` : ''}; fix the parse error so ${file}'s consumers can be derived` });
|
||||
}
|
||||
const derivedFiles = [...consumers.keys()];
|
||||
const unknownFiles = unmapped.filter(file => !derivedGlobal.includes(file) && !consumers.has(file) && !noConsumerFiles.includes(file));
|
||||
const sharedInputs = files.filter(file => depends(file, PR_FULL_GATE_FILES));
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('workflow inventory (CEO-20)', () => {
|
||||
|
||||
test('the inventory rule also reads .yaml files', () => {
|
||||
expect(unknownFileLabel('.github/workflows/new-check.yaml').label).toBe('add to FREE_ONLY_PR_FILES');
|
||||
expect(unknownFileLabel('.github/workflows/evals.yml').label).toBe('real unknown dependency');
|
||||
expect(unknownFileLabel('.github/workflows/evals.yml').label).toBe('full-gate input');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('PR selection over the W1a single-file fallbacks', () => {
|
||||
const result = select(['.github/workflows/evals.yml']);
|
||||
expect(result.mode).toBe('full-fallback');
|
||||
expect(result.unknownFileLabels).toEqual([{ file: '.github/workflows/evals.yml', ...unknownFileLabel('.github/workflows/evals.yml') }]);
|
||||
expect(result.reasons[0]).toContain('.github/workflows/evals.yml (real unknown dependency)');
|
||||
expect(result.reasons[0]).toContain('.github/workflows/evals.yml (full-gate input)');
|
||||
});
|
||||
|
||||
test('a mapped free fixture is exempt; a paid-registered fixture selects its cases; an unmapped one falls back', () => {
|
||||
|
||||
@@ -114,7 +114,7 @@ describe('paid/free dependency boundary', () => {
|
||||
expect(result.selection).toEqual({ e2e: [], judges: [] });
|
||||
}
|
||||
// Tracked files outside every mapping and outside the derivable directories restore the full gate.
|
||||
for (const file of ['ETHOS.md', 'SECURITY.md', 'conductor.json', '.osv-scanner.toml']) {
|
||||
for (const file of ['ETHOS.md', 'SECURITY.md', 'conductor.json', 'NOTICE.md']) {
|
||||
const result = computePaidCaseSelection({ profile: 'pr', env: {}, changedFiles: [FREE_ONLY_PR_FILES[0], file] });
|
||||
expect(result.coverage?.mode, file).toBe('full-fallback');
|
||||
expect(result.coverage?.unknownFiles).toContain(file);
|
||||
|
||||
@@ -96,6 +96,93 @@ describe('reference derivation on a fixture tree', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user