mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 21:46:40 +02:00
chore: merge main and resolve conflicts
Ported plan completion audit, coverage gate, and auto-verification resolvers into main's modular resolver pipeline. Updated CHANGELOG version to 0.11.14.0 (main took 0.11.13.0).
This commit is contained in:
@@ -5,11 +5,13 @@
|
||||
* tests across multiple files by category.
|
||||
*/
|
||||
|
||||
import { describe, test, afterAll } from 'bun:test';
|
||||
import { describe, test, beforeAll, afterAll } from 'bun:test';
|
||||
import type { SkillTestResult } from './session-runner';
|
||||
import { EvalCollector, judgePassed } from './eval-store';
|
||||
import type { EvalTestEntry } from './eval-store';
|
||||
import { selectTests, detectBaseBranch, getChangedFiles, E2E_TOUCHFILES, GLOBAL_TOUCHFILES } from './touchfiles';
|
||||
import { WorktreeManager } from '../../lib/worktree';
|
||||
import type { HarvestResult } from '../../lib/worktree';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -234,6 +236,59 @@ export function testConcurrentIfSelected(testName: string, fn: () => Promise<voi
|
||||
(shouldRun ? test.concurrent : test.skip)(testName, fn, timeout);
|
||||
}
|
||||
|
||||
// --- Worktree isolation ---
|
||||
|
||||
let worktreeManager: WorktreeManager | null = null;
|
||||
|
||||
export function getWorktreeManager(): WorktreeManager {
|
||||
if (!worktreeManager) {
|
||||
worktreeManager = new WorktreeManager();
|
||||
worktreeManager.pruneStale();
|
||||
}
|
||||
return worktreeManager;
|
||||
}
|
||||
|
||||
/** Create an isolated worktree for a test. Returns the worktree path. */
|
||||
export function createTestWorktree(testName: string): string {
|
||||
return getWorktreeManager().create(testName);
|
||||
}
|
||||
|
||||
/** Harvest changes and clean up. Call in afterAll(). Returns HarvestResult for eval integration. */
|
||||
export function harvestAndCleanup(testName: string): HarvestResult | null {
|
||||
const mgr = getWorktreeManager();
|
||||
const result = mgr.harvest(testName);
|
||||
if (result) {
|
||||
if (result.isDuplicate) {
|
||||
process.stderr.write(`\n HARVEST [${testName}]: duplicate patch (skipped)\n`);
|
||||
} else {
|
||||
process.stderr.write(`\n HARVEST [${testName}]: ${result.changedFiles.length} files changed\n`);
|
||||
process.stderr.write(` Patch: ${result.patchPath}\n`);
|
||||
process.stderr.write(` ${result.diffStat}\n\n`);
|
||||
}
|
||||
}
|
||||
mgr.cleanup(testName);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: describe block with automatic worktree isolation + harvest.
|
||||
* Any test file can use this to get real repo context instead of a tmpdir.
|
||||
* Note: tests with planted-bug fixtures should NOT use this — they need their fixture repos.
|
||||
*/
|
||||
export function describeWithWorktree(
|
||||
name: string,
|
||||
testNames: string[],
|
||||
fn: (getWorktreePath: () => string) => void,
|
||||
) {
|
||||
describeIfSelected(name, testNames, () => {
|
||||
let worktreePath: string;
|
||||
beforeAll(() => { worktreePath = createTestWorktree(name); });
|
||||
afterAll(() => { harvestAndCleanup(name); });
|
||||
fn(() => worktreePath);
|
||||
});
|
||||
}
|
||||
|
||||
export { judgePassed } from './eval-store';
|
||||
export { EvalCollector } from './eval-store';
|
||||
export type { EvalTestEntry } from './eval-store';
|
||||
export type { HarvestResult } from '../../lib/worktree';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Eval result persistence and comparison.
|
||||
*
|
||||
* EvalCollector accumulates test results, writes them to
|
||||
* ~/.gstack-dev/evals/{version}-{branch}-{tier}-{timestamp}.json,
|
||||
* ~/.gstack/projects/$SLUG/evals/{version}-{branch}-{tier}-{timestamp}.json,
|
||||
* prints a summary table, and auto-compares with the previous run.
|
||||
*
|
||||
* Comparison functions are exported for reuse by the eval:compare CLI.
|
||||
@@ -14,7 +14,32 @@ import * as os from 'os';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
const SCHEMA_VERSION = 1;
|
||||
const DEFAULT_EVAL_DIR = path.join(os.homedir(), '.gstack-dev', 'evals');
|
||||
const LEGACY_EVAL_DIR = path.join(os.homedir(), '.gstack-dev', 'evals');
|
||||
|
||||
/**
|
||||
* Detect project-scoped eval dir via gstack-slug.
|
||||
* Falls back to legacy ~/.gstack-dev/evals/ if slug detection fails.
|
||||
*/
|
||||
export function getProjectEvalDir(): string {
|
||||
try {
|
||||
// Try repo-local gstack-slug first, then global install
|
||||
const localSlug = spawnSync('bash', ['-c', '.claude/skills/gstack/bin/gstack-slug 2>/dev/null || ~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null'], {
|
||||
stdio: 'pipe', timeout: 3000,
|
||||
});
|
||||
const output = localSlug.stdout?.toString().trim();
|
||||
if (output) {
|
||||
const slugMatch = output.match(/^SLUG=(.+)$/m);
|
||||
if (slugMatch && slugMatch[1]) {
|
||||
const dir = path.join(os.homedir(), '.gstack', 'projects', slugMatch[1], 'evals');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
return LEGACY_EVAL_DIR;
|
||||
}
|
||||
|
||||
const DEFAULT_EVAL_DIR = getProjectEvalDir();
|
||||
|
||||
// --- Interfaces ---
|
||||
|
||||
@@ -55,6 +80,13 @@ export interface EvalTestEntry {
|
||||
missed_bugs?: string[];
|
||||
|
||||
error?: string;
|
||||
|
||||
// Worktree harvest data
|
||||
harvest?: {
|
||||
filesChanged: number;
|
||||
patchPath: string;
|
||||
isDuplicate: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EvalResult {
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { getProjectEvalDir } from './eval-store';
|
||||
|
||||
const GSTACK_DEV_DIR = path.join(os.homedir(), '.gstack-dev');
|
||||
const HEARTBEAT_PATH = path.join(GSTACK_DEV_DIR, 'e2e-live.json');
|
||||
const HEARTBEAT_PATH = path.join(GSTACK_DEV_DIR, 'e2e-live.json'); // heartbeat stays global
|
||||
const PROJECT_DIR = path.dirname(getProjectEvalDir()); // ~/.gstack/projects/$SLUG/
|
||||
|
||||
/** Sanitize test name for use as filename: strip leading slashes, replace / with - */
|
||||
export function sanitizeTestName(name: string): string {
|
||||
@@ -144,7 +146,7 @@ export async function runSkillTest(options: {
|
||||
const safeName = testName ? sanitizeTestName(testName) : null;
|
||||
if (runId) {
|
||||
try {
|
||||
runDir = path.join(GSTACK_DEV_DIR, 'e2e-runs', runId);
|
||||
runDir = path.join(PROJECT_DIR, 'e2e-runs', runId);
|
||||
fs.mkdirSync(runDir, { recursive: true });
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ export const GLOBAL_TOUCHFILES = [
|
||||
'scripts/gen-skill-docs.ts',
|
||||
'test/helpers/touchfiles.ts',
|
||||
'browse/test/test-server.ts',
|
||||
'lib/worktree.ts',
|
||||
];
|
||||
|
||||
// --- Base branch detection ---
|
||||
|
||||
Reference in New Issue
Block a user