mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-22 05:57:18 +02:00
evals: split touchfiles into pure data + selection logic + facade
touchfiles.ts listed ITSELF in GLOBAL_TOUCHFILES, so adding one test's dep entry forced the full ~$38 / 30-45min suite — measured on 21.9% of recent commits (42/192). The self-reference existed because data and logic shared a file: any edit COULD be a selection-logic change. Now: touchfiles-data.ts (the four maps, literals only, zero imports — the future map-diff target), test-selection.ts (matchGlob/detectBase Branch/getChangedFiles/selectTests), and touchfiles.ts as a re-export facade so all ~12 import sites are untouched. GLOBAL_TOUCHFILES drops the self-ref, adds test-selection.ts (logic stays maximally conservative), and TEMPORARILY adds touchfiles-data.ts until the map-diff change lands. New free test pins the literal-only property (comment-aware state-machine scan with a self-test) and facade export parity (===), so neither can silently rot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6d109d3232
commit
587217e2ec
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Diff-based test selection for E2E and LLM-judge evals — the LOGIC half.
|
||||
*
|
||||
* Each test declares which source files it depends on ("touchfiles") in
|
||||
* ./touchfiles-data.ts (literals only — see the note there). The test runner
|
||||
* checks `git diff` and only runs tests whose dependencies were modified.
|
||||
* Override with EVALS_ALL=1 to run everything.
|
||||
*
|
||||
* Import sites should keep using the ./touchfiles facade, which re-exports
|
||||
* both this module and the data module.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
import { GLOBAL_TOUCHFILES } from './touchfiles-data';
|
||||
|
||||
// --- Glob matching ---
|
||||
|
||||
/**
|
||||
* Match a file path against a glob pattern.
|
||||
* Supports:
|
||||
* ** — match any number of path segments
|
||||
* * — match within a single segment (no /)
|
||||
*/
|
||||
export function matchGlob(file: string, pattern: string): boolean {
|
||||
const regexStr = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
||||
.replace(/\*/g, '[^/]*')
|
||||
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
||||
return new RegExp(`^${regexStr}$`).test(file);
|
||||
}
|
||||
|
||||
// --- Base branch detection ---
|
||||
|
||||
/**
|
||||
* Detect the base branch by trying refs in order.
|
||||
* Returns the first valid ref, or null if none found.
|
||||
*/
|
||||
export function detectBaseBranch(cwd: string): string | null {
|
||||
for (const ref of ['origin/main', 'origin/master', 'main', 'master']) {
|
||||
const result = spawnSync('git', ['rev-parse', '--verify', ref], {
|
||||
cwd, stdio: 'pipe', timeout: 3000,
|
||||
});
|
||||
if (result.status === 0) return ref;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of files changed between base branch and HEAD.
|
||||
*/
|
||||
export function getChangedFiles(baseBranch: string, cwd: string): string[] {
|
||||
const result = spawnSync('git', ['diff', '--name-only', `${baseBranch}...HEAD`], {
|
||||
cwd, stdio: 'pipe', timeout: 5000,
|
||||
});
|
||||
if (result.status !== 0) return [];
|
||||
return result.stdout.toString().trim().split('\n').filter(Boolean);
|
||||
}
|
||||
|
||||
// --- Test selection ---
|
||||
|
||||
/**
|
||||
* Select tests to run based on changed files.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. If any changed file matches a global touchfile → run ALL tests
|
||||
* 2. Otherwise, for each test, check if any changed file matches its patterns
|
||||
* 3. Return selected + skipped lists with reason
|
||||
*/
|
||||
export function selectTests(
|
||||
changedFiles: string[],
|
||||
touchfiles: Record<string, string[]>,
|
||||
globalTouchfiles: string[] = GLOBAL_TOUCHFILES,
|
||||
): { selected: string[]; skipped: string[]; reason: string } {
|
||||
const allTestNames = Object.keys(touchfiles);
|
||||
|
||||
// Global touchfile hit → run all
|
||||
for (const file of changedFiles) {
|
||||
if (globalTouchfiles.some(g => matchGlob(file, g))) {
|
||||
return { selected: allTestNames, skipped: [], reason: `global: ${file}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Per-test matching
|
||||
const selected: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
for (const [testName, patterns] of Object.entries(touchfiles)) {
|
||||
const hit = changedFiles.some(f => patterns.some(p => matchGlob(f, p)));
|
||||
(hit ? selected : skipped).push(testName);
|
||||
}
|
||||
|
||||
return { selected, skipped, reason: 'diff' };
|
||||
}
|
||||
Reference in New Issue
Block a user