mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 03:00:26 +01:00
test: eight flaky-test repairs and five failure diagnostics from the 2026-11-01 flaky pass (GSTA-240)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
83ae96e086
commit
fbbec20967
20 files changed
+164
-29
No files matched your search
@@ -111,7 +111,8 @@ test.each([
|
||||
expect(facts.receipts.filter((receipt: any) => receipt.outcome === 'timeout')).toHaveLength(3 - scenario.passed);
|
||||
expect(facts.receipts.every((receipt: any) => receipt.timeoutMs === 240_000 && !receipt.answered)).toBe(true);
|
||||
expect(facts.active).toBe(0); expect(facts.answers).toBe(0); expect(facts.pendingTimers).toBe(0);
|
||||
expect(facts.leftovers).toEqual([]); expect(facts.ownedStateLeftovers).toEqual([]);
|
||||
expect(facts.leftovers).toEqual([]);
|
||||
expect(facts.ownedStateLeftovers, JSON.stringify({ fixtures: facts.fixtures, inputs: facts.inputs.map((input: any) => [input.id, input.config, input.state]), events: facts.events.filter((event: any) => ['query-close', 'capture-settle', 'cleanup'].includes(event.kind)) })).toEqual([]);
|
||||
expect(facts.events.filter((event: any) => event.kind === 'query-close')).toHaveLength(scenario.queries);
|
||||
expect(facts.events.filter((event: any) => event.kind === 'cleanup')).toHaveLength(3);
|
||||
}, 20_000);
|
||||
|
||||
@@ -1316,7 +1316,9 @@ describe('Autoplan phase entry through the gbrain :user render (#2569)', () => {
|
||||
edit(rewriteSectionBase(fs.readFileSync(path.join(ROOT, 'autoplan/sections', name), 'utf8'), linkRoot)));
|
||||
return root;
|
||||
}
|
||||
function withEnv<T>(env: Record<string, string | undefined>, work: () => T): T {
|
||||
/** The state-root chain the case does not name is cleared, so an ambient GSTACK_STATE_ROOT (every gstack launch file exports one) cannot pick the root. */
|
||||
function withEnv<T>(given: Record<string, string | undefined>, work: () => T): T {
|
||||
const env = { GSTACK_STATE_ROOT: undefined, GSTACK_STATE_DIR: undefined, CLAUDE_PLUGIN_DATA: undefined, ...given };
|
||||
const previous = Object.fromEntries(Object.keys(env).map(k => [k, process.env[k]]));
|
||||
for (const [k, v] of Object.entries(env)) if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
||||
try { return work(); }
|
||||
|
||||
@@ -62,7 +62,7 @@ describe.skipIf(process.platform !== 'linux')('bootstrap paid-shard cleanup inte
|
||||
fs.writeFileSync(path.join(root, 'controller.stdout.log'), controller.stdout ?? '', { mode: 0o600 });
|
||||
fs.writeFileSync(path.join(root, 'controller.stderr.log'), controller.stderr ?? '', { mode: 0o600 });
|
||||
expect(controller.error).toBeUndefined();
|
||||
expect(controller.status).toBe(0);
|
||||
expect(controller.status, `controller exited ${controller.status} (signal ${controller.signal}); stderr tail: ${(controller.stderr ?? '').slice(-600).replace(/\s*\n\s*/g, ' | ')}`).toBe(0);
|
||||
const outcome = JSON.parse(fs.readFileSync(outcomePath, 'utf8'));
|
||||
if (scenario === 'ack-failure') {
|
||||
expect(controller.stdout).toContain('(fail) qa-bootstrap');
|
||||
|
||||
@@ -247,6 +247,31 @@ describe('probe lock recovery and failure', () => {
|
||||
expect(calls(f)).toBe(1);
|
||||
}));
|
||||
|
||||
test('a lock released while a waiter reads its age is not abandoned; the next owner\'s fresh lock survives', withFixture((f) => {
|
||||
const sig = sigOf(f);
|
||||
const lock = path.join(f.locks, sig);
|
||||
const once = path.join(f.home, 'vanish-once');
|
||||
const real = JSON.stringify(Bun.which('stat'));
|
||||
fs.mkdirSync(lock, { recursive: true });
|
||||
fs.writeFileSync(path.join(f.home, 'bin/stat'), `#!/usr/bin/env bash
|
||||
if [ "$3" = "$RACE_LOCK" ] && [ -f "$RACE_ONCE" ]; then
|
||||
case "$1" in
|
||||
-c) rmdir "$RACE_LOCK"; [ "$RACE_PHASE" = early ] && mkdir "$RACE_LOCK"; exit 1 ;;
|
||||
-f) ${real} "$@"; rc=$?; rm -f "$RACE_ONCE"; [ -d "$RACE_LOCK" ] || mkdir "$RACE_LOCK"; exit "$rc" ;;
|
||||
esac
|
||||
fi
|
||||
exec ${real} "$@"
|
||||
`, { mode: 0o755 });
|
||||
for (const phase of ['early', 'late']) {
|
||||
fs.writeFileSync(once, 'armed');
|
||||
const r = probe(f, { _GSTACK_CODEX_LOCK_WAIT: '1', RACE_LOCK: lock, RACE_ONCE: once, RACE_PHASE: phase });
|
||||
expect(fs.existsSync(once)).toBe(false);
|
||||
expect(r.stdout).toContain('MODEL_PROBE_INCONCLUSIVE (another probe of this model still holds its lock)');
|
||||
expect(fs.existsSync(lock)).toBe(true);
|
||||
expect(calls(f)).toBe(0);
|
||||
}
|
||||
}));
|
||||
|
||||
test('an unreadable cache or uncreatable lock is unverified and launches no paid probe', withFixture((f) => {
|
||||
fs.mkdirSync(f.cache);
|
||||
const dir = probe(f);
|
||||
|
||||
@@ -74,6 +74,14 @@ describe('gstack-evidence run', () => {
|
||||
expect(fs.readFileSync(rec.log_path, 'utf-8')).toContain('ok');
|
||||
});
|
||||
|
||||
test('the lane command sees its own log path as GSTACK_EVIDENCE_LOG (the free runner\'s home tripwire excludes exactly that file)', () => {
|
||||
const r = run(['run', '--label', 'focused', '--', 'printf "%s" "$GSTACK_EVIDENCE_LOG"']);
|
||||
expect(r.status).toBe(0);
|
||||
const rec = records().pop();
|
||||
expect(path.isAbsolute(rec.log_path)).toBe(true);
|
||||
expect(r.stdout).toBe(rec.log_path);
|
||||
});
|
||||
|
||||
test('propagates a failing exit code and records it', () => {
|
||||
const r = run(['run', '--label', 'tests', '--', 'exit 3']);
|
||||
expect(r.status).toBe(3);
|
||||
|
||||
@@ -78,6 +78,34 @@ describe('free-suite home-write tripwire (#2895)', () => {
|
||||
expect(failure).toContain('only');
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === 'win32')('the wrapping gstack-evidence lane log is excluded by its exact path; a foreign write beside it still trips', async () => {
|
||||
const home = fakeHome();
|
||||
const logs = path.join(home, '.gstack/projects/p/logs');
|
||||
fs.mkdirSync(logs, { recursive: true });
|
||||
const laneLog = path.join(logs, 'focused.log');
|
||||
fs.writeFileSync(laneLog, 'lane start\n');
|
||||
const unexcluded = guardFreeHome(['test/a.test.ts'], { HOME: home });
|
||||
const options = (script: string, name: string) => ({ env: { ...process.env, HOME: home, GSTACK_EVIDENCE_LOG: laneLog }, quiet: true, log: () => {},
|
||||
logFilePath: path.join(home, `${name}.log`),
|
||||
commandFor: () => ({ command: process.execPath, args: ['-e', `${script}console.log('Ran 1 tests across 1 files. [1.00ms]')`] }) });
|
||||
const grow = `require('fs').appendFileSync(${JSON.stringify(laneLog)}, 'lane output\\n');`;
|
||||
const errors: string[] = [];
|
||||
const spy = spyOn(console, 'error').mockImplementation((...args: unknown[]) => { errors.push(args.join(' ')); });
|
||||
try {
|
||||
expect((await runFreeShard(['test/a.test.ts'], 1, 1, options(grow, 'lane'))).status).toBe('passed');
|
||||
expect(unexcluded.verify()).toContain('~/.gstack/projects/p/logs/focused.log');
|
||||
const foreign = `${grow}require('fs').writeFileSync(${JSON.stringify(path.join(logs, 'other.log'))}, 'x');`;
|
||||
expect((await runFreeShard(['test/a.test.ts'], 1, 1, options(foreign, 'foreign'))).status).toBe('failed');
|
||||
expect(errors.join('\n')).toContain('~/.gstack/projects/p/logs/other.log');
|
||||
expect(errors.join('\n')).not.toContain('focused.log');
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
for (const retained of errors.flatMap(line => /retained (\S+)$/.exec(line)?.[1] ?? [])) {
|
||||
if (/^gstack-free-shard-/.test(path.basename(retained))) fs.rmSync(retained, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a concurrent phase guard names no shard and points at attribution', () => {
|
||||
const home = fakeHome();
|
||||
const guard = guardFreeHome(['test/a.test.ts', 'test/b.test.ts'], { HOME: home }, { kind: 'concurrent', shards: 3 });
|
||||
|
||||
@@ -1460,8 +1460,13 @@ describe('scan: option-like bases and page-controlled inline ignores', () => {
|
||||
fs.mkdirSync(many, { recursive: true });
|
||||
for (let i = 0; i < 1100; i++) fs.writeFileSync(path.join(many, `f${i}.css`), 'a{}');
|
||||
try {
|
||||
const r = run(['scan', '--format', 'gstack', 'src/many'], { env: { IMPECCABLE_BIN: FAKE, IMPECCABLE_FAKE_SLEEP_MS: '1000', GSTACK_DESIGN_DETECT_TIMEOUT_MS: '1500' } });
|
||||
const log = path.join(SANDBOX, 'argv-many.log');
|
||||
fs.rmSync(log, { force: true });
|
||||
const r = run(['scan', '--format', 'gstack', 'src/many'], { env: { IMPECCABLE_BIN: FAKE, IMPECCABLE_FAKE_LOG: log } });
|
||||
expect(r.code).toBe(2); // a directory target is one batch (the fake reports findings): the budget test needs files
|
||||
const calls = fs.readFileSync(log, 'utf-8').trim().split('\n').map(l => JSON.parse(l).argv as string[]);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].some(x => x.endsWith(path.join('src', 'many')))).toBe(true);
|
||||
const files = fs.readdirSync(many).map(f => path.join('src', 'many', f));
|
||||
const r2 = run(['scan', '--format', 'gstack', ...files], { env: { IMPECCABLE_BIN: FAKE, IMPECCABLE_FAKE_SLEEP_MS: '1000', GSTACK_DESIGN_DETECT_TIMEOUT_MS: '1500' } });
|
||||
expect(r2.err).toMatch(/DETECT_TIMEOUT: whole-scan budget 7500ms exceeded, \d+ of 11 batches not run/);
|
||||
|
||||
@@ -17,7 +17,7 @@ const ROOT = path.resolve(import.meta.dir, '../..');
|
||||
export const SETTLEMENT_MODE_GROUPS = {
|
||||
lifecycle: ['success', 'failure', 'timeout', 'cancel', 'cancel-force', 'cancel-cold-probes', 'settle-cold-probes', 'settle-slow-exit', 'cancel-vanishing-record'],
|
||||
ownership: ['endpoint-mix', 'full-state-mix', 'terminal-mix', 'chromium-mix', 'replaced-pid', 'stale-child-start', 'replaced-start',
|
||||
'exit-environment-race', 'unavailable-environment', 'directory-remove-failure'],
|
||||
'exit-environment-race', 'unavailable-environment', 'directory-remove-failure', 'exited-child-race'],
|
||||
} as const;
|
||||
|
||||
const OWNERSHIP_ACTOR = `
|
||||
@@ -50,6 +50,7 @@ if (role === 'leaf') {
|
||||
}));
|
||||
await Bun.sleep(50);
|
||||
const owned = [identity(process.pid), ...children.map(child => identity(child.pid))];
|
||||
if (mode === 'exited-child-race') await new Promise(resolve => { children[0].once('exit', resolve); children[0].kill('SIGKILL'); });
|
||||
const server = Bun.serve({hostname:'127.0.0.1', port:0, fetch() {
|
||||
fs.appendFileSync(file('.http'), 'request\\n'); return new Response('ok');
|
||||
}});
|
||||
|
||||
@@ -32,6 +32,31 @@ export const BASE_POLICY = {
|
||||
stamp_sensitive_gates: ['digest'],
|
||||
};
|
||||
|
||||
function fixtureGit(root: string) {
|
||||
const home = path.join(root, 'home');
|
||||
const work = path.join(root, 'work');
|
||||
const env = { GSTACK_HOME: home, GSTACK_STATE_DIR: home, HOME: home, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
|
||||
const git = (...args: string[]): string => {
|
||||
const r = gitArgvIn(work, args, 30_000, { ...process.env, ...env });
|
||||
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr?.toString()}`);
|
||||
return r.stdout?.toString().trim() ?? '';
|
||||
};
|
||||
return { env, git };
|
||||
}
|
||||
|
||||
/**
|
||||
* A private copy of a built fixture (origin, clone, home, and any files under its root), for a test file that
|
||||
* would otherwise rebuild the same multi-second fixture in every test. Only the origin remote holds an absolute path.
|
||||
*/
|
||||
export function copyFixtureRepo(src: FixtureRepo, prefix = 'gstack-restamp-fx-'): FixtureRepo {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
fs.cpSync(src.root, root, { recursive: true, preserveTimestamps: true });
|
||||
const { env, git } = fixtureGit(root);
|
||||
const origin = path.join(root, 'origin.git');
|
||||
git('remote', 'set-url', 'origin', origin);
|
||||
return { root, work: path.join(root, 'work'), origin, home: env.HOME, git, env };
|
||||
}
|
||||
|
||||
export function makeFixtureRepo(opts: { policy?: Record<string, unknown> | null; version?: string; prefix?: string } = {}): FixtureRepo {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), opts.prefix ?? 'gstack-restamp-fx-'));
|
||||
const origin = path.join(root, 'origin.git');
|
||||
@@ -39,13 +64,8 @@ export function makeFixtureRepo(opts: { policy?: Record<string, unknown> | null;
|
||||
const home = path.join(root, 'home');
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
fs.mkdirSync(work, { recursive: true });
|
||||
const env = { GSTACK_HOME: home, GSTACK_STATE_DIR: home, HOME: home, GIT_CONFIG_GLOBAL: path.join(root, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1' };
|
||||
const { env, git } = fixtureGit(root);
|
||||
fs.writeFileSync(env.GIT_CONFIG_GLOBAL, '[user]\n\tname = t\n\temail = t@test\n[commit]\n\tgpgsign = false\n[init]\n\tdefaultBranch = main\n');
|
||||
const git = (...args: string[]): string => {
|
||||
const r = gitArgvIn(work, args, 30_000, { ...process.env, ...env });
|
||||
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr?.toString()}`);
|
||||
return r.stdout?.toString().trim() ?? '';
|
||||
};
|
||||
spawnSync('git', ['init', '-q', '--bare', origin], { timeout: 30_000, env: { ...process.env, ...env } });
|
||||
git('init', '-q', '-b', 'main');
|
||||
const version = opts.version ?? '1.2.3.0';
|
||||
|
||||
@@ -312,7 +312,8 @@ describe('Office Hours real session runner with fake processes', () => {
|
||||
expect(captured.output).toBe('captured output');
|
||||
const diagnostic = JSON.parse(fs.readFileSync(path.join(dir, '.gstack/test-transcripts/drain-failure.json'), 'utf8'));
|
||||
expect(diagnostic.stderr).toContain('fixture auth failure');
|
||||
expect(running(Number(fs.readFileSync(path.join(dir, 'child.pid'), 'utf8')))).toBe(false);
|
||||
const child = fs.readFileSync(path.join(dir, 'child.pid'), 'utf8').trim();
|
||||
expect(running(Number(child)), `held-pipe child after the runner returned (group ${fs.readFileSync(path.join(dir, 'parent.pid'), 'utf8').trim()}): ${spawnSync('ps', ['-o', 'pid=,ppid=,pgid=,stat=,etime=', '-p', child], { encoding: 'utf8', timeout: 5_000 }).stdout.trim() || 'gone now'}`).toBe(false);
|
||||
});
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
* the base branch and every other open PR, and a lookup that cannot finish is
|
||||
* `unknown`, which withholds `REGATE: not-needed`.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { BASE_POLICY, cleanup, makeFixtureRepo, makePr, recordAndBundle, restamp, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
import { BASE_POLICY, cleanup, copyFixtureRepo, makeFixtureRepo, makePr, recordAndBundle, restamp, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
|
||||
const repos: FixtureRepo[] = [];
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
@@ -39,10 +39,13 @@ console.error("gh shim: unsupported "+a.join(" ")); process.exit(1)' "${file}" "
|
||||
}
|
||||
const ghCalls = (repo: FixtureRepo) => fs.readFileSync(path.join(repo.root, 'gh-regate', 'calls.log'), 'utf8');
|
||||
|
||||
type Lanes = Array<{ label: string; command: string }>;
|
||||
const TESTS_LANE: Lanes = [{ label: 'tests', command: 'true' }];
|
||||
const DIGEST_LANES: Lanes = [...TESTS_LANE, { label: 'digest', command: 'cat generated/digest.md' }];
|
||||
|
||||
/** pr-a adds migration v240, is stamped 1.2.4.0 and gated (lanes recorded and bundled); then restamped to 1.2.5.0. */
|
||||
function gatedThenRestamped(lanes: Array<{ label: string; command: string }> = [{ label: 'tests', command: 'true' }]) {
|
||||
function buildGated(lanes: Lanes) {
|
||||
const repo = makeFixtureRepo({ prefix: 'pregate-regate-', policy: POLICY });
|
||||
repos.push(repo);
|
||||
makePr(repo, 'pr-a', { expects: '1.2.4.0' });
|
||||
fs.mkdirSync(path.join(repo.work, 'migrations'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, 'migrations', 'v240-add-index.sql'), 'create index a;\n');
|
||||
@@ -54,7 +57,20 @@ function gatedThenRestamped(lanes: Array<{ label: string; command: string }> = [
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
|
||||
repo.git('commit', '-qam', 'stamp 1.2.5.0');
|
||||
return { repo, head, bundle, current: repo.git('rev-parse', 'HEAD') };
|
||||
return { repo, head, current: repo.git('rev-parse', 'HEAD') };
|
||||
}
|
||||
|
||||
// Each fixture costs four gstack bin runs (two restamps, an evidence run and a bundle); build each lane set once
|
||||
// and give every test its own copy instead of rebuilding it per test.
|
||||
const templates = new Map<Lanes, ReturnType<typeof buildGated>>();
|
||||
beforeAll(() => { for (const lanes of [TESTS_LANE, DIGEST_LANES]) templates.set(lanes, buildGated(lanes)); }, 60_000);
|
||||
afterAll(() => { for (const t of templates.values()) cleanup(t.repo); });
|
||||
|
||||
function gatedThenRestamped(lanes: Lanes = TESTS_LANE) {
|
||||
const { repo: template, head, current } = templates.get(lanes)!;
|
||||
const repo = copyFixtureRepo(template, 'pregate-regate-');
|
||||
repos.push(repo);
|
||||
return { repo, head, bundle: path.join(repo.root, 'bundle.json'), current };
|
||||
}
|
||||
const regate = (repo: FixtureRepo, args: string[], env: Record<string, string>) => runBin(repo, 'gstack-pregate', ['regate', '--base', 'main', ...args], env);
|
||||
|
||||
@@ -97,7 +113,7 @@ describe('gstack-pregate regate: evidence, stamps and lane verdicts', () => {
|
||||
});
|
||||
|
||||
test('a stamp-sensitive lane reruns, so a regate is needed; a code edit is not stamp-only and names the file and line', () => {
|
||||
const { repo, head, bundle } = gatedThenRestamped([{ label: 'tests', command: 'true' }, { label: 'digest', command: 'cat generated/digest.md' }]);
|
||||
const { repo, head, bundle } = gatedThenRestamped(DIGEST_LANES);
|
||||
const env = ghShim(repo, { list: [] });
|
||||
const r = regate(repo, ['--against', head, '--bundle', bundle], env);
|
||||
expect(r.status).toBe(EXIT.fail);
|
||||
|
||||
@@ -284,7 +284,8 @@ describe('gstack-session-update lock identity + TTL (#2613)', () => {
|
||||
const r = runScript(base, install, state);
|
||||
expect(r.status).toBe(0);
|
||||
const log = await waitForLog(state, /UP_TO_DATE|UPDATING/);
|
||||
expect(log).toMatch(/UP_TO_DATE|UPDATING/);
|
||||
const lockPid = fs.existsSync(path.join(lockDir, 'pid')) ? fs.readFileSync(path.join(lockDir, 'pid'), 'utf8').trim() : 'no lock';
|
||||
expect(log, `planted dead pid ${dead.pid} (alive now: ${isAlive(dead.pid)}); lock pid after 15 s: ${lockPid} (alive: ${isAlive(Number(lockPid))}); script exit ${r.status}, stderr: ${r.stderr}`).toMatch(/UP_TO_DATE|UPDATING/);
|
||||
} finally {
|
||||
fs.rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -171,7 +171,10 @@ describe('shard engine: process lifecycle', () => {
|
||||
});
|
||||
expect(paid.status).toBe('timed-out');
|
||||
expect(summaryExitCode(summarize([paid]))).toBe(1);
|
||||
expect(await waitDead(Number(fs.readFileSync(pidFile, 'utf8')))).toBe(true);
|
||||
const paidChild = Number(fs.readFileSync(pidFile, 'utf8'));
|
||||
const paidChildDead = await waitDead(paidChild);
|
||||
const procStat = (pid: number) => { try { return fs.readFileSync(`/proc/${pid}/stat`, 'utf8').trim(); } catch { return 'no /proc entry'; } };
|
||||
expect(paidChildDead, `paid grandchild ${paidChild} of group ${paid.groupPid} 2 s after the timeout kill: ${procStat(paidChild)}`).toBe(true);
|
||||
}, 30_000);
|
||||
|
||||
test('win32 semantics: no process group and never a negative-pid kill', async () => {
|
||||
|
||||
@@ -308,7 +308,7 @@ describe('Claude Code child launch', () => {
|
||||
const file = ho.out.match(/^HANDOFF: (\S+)/m)![1]!;
|
||||
const argsFile = path.join(h.root, 'claude-args.txt');
|
||||
const fake = path.join(h.root, 'fake-claude');
|
||||
fs.writeFileSync(fake, `#!/bin/sh\nprintf '%s\\n' "$@" > "${argsFile}"\n`, { mode: 0o755 });
|
||||
fs.writeFileSync(fake, `#!/bin/sh\nprintf '%s\\n' "$@" > "${argsFile}.tmp" && mv "${argsFile}.tmp" "${argsFile}"\n`, { mode: 0o755 });
|
||||
const r = h.sb(['launch', '--in', file], { GSTACK_CLAUDE_BIN: fake });
|
||||
expect(r.code).toBe(0);
|
||||
const session = h.field(r.out, 'CHILD', 'session');
|
||||
|
||||
@@ -35,13 +35,16 @@ const result = (out: string) => line(out, 'GSTACK_RESULT:');
|
||||
const next = (out: string) => line(out, 'NEXT:');
|
||||
const remoteRefs = (fx: TrainFixture) => fx.repo.git('ls-remote', 'origin');
|
||||
|
||||
// fails_when=coordinatorClone copies origin.git/objects file by file again, so an entry that appears or vanishes there mid-copy (the detached maintenance.lock every push into the fixture origin leaves) fails the clone, as in two Free Tests reds on 2026-10-28 (GSTA-139 ledger); why_new=no other test clones while origin's objects directory changes; seam=a dangling symlink planted under objects/, which a local copy refuses on every run; no_claim=which process removed the loose object those runs named
|
||||
// fails_when=coordinatorClone copies origin.git/objects file by file again, so an entry that appears or vanishes there mid-copy (the detached maintenance.lock every push into the fixture origin leaves) fails the clone, as in two Free Tests reds on 2026-10-28 (GSTA-139 ledger); why_new=no other test clones while origin's objects directory changes; seam=a dangling symlink planted under objects/ beside a held maintenance.lock, which a local copy refuses on every run; the seam has its own name because the fixture push's own detached maintenance can hold objects/maintenance.lock at that moment (EEXIST in Free Tests 41288253305 and 41288417894, 2026-11-01); no_claim=which process removed the loose object those runs named
|
||||
test('the coordinator clone reads origin through git, never by copying its objects directory (GSTA-139)', () => {
|
||||
const fx = make();
|
||||
fs.symlinkSync(path.join(fx.repo.root, 'gone'), path.join(fx.repo.origin, 'objects', 'maintenance.lock'));
|
||||
const objects = path.join(fx.repo.origin, 'objects');
|
||||
fs.writeFileSync(path.join(objects, 'maintenance.lock'), '');
|
||||
fs.symlinkSync(path.join(fx.repo.root, 'gone'), path.join(objects, 'gstack-dangling-seam'));
|
||||
const coord = coordinatorClone(fx.repo);
|
||||
expect(spawnSync('git', ['-C', coord, 'rev-parse', 'origin/main'], { encoding: 'utf8', timeout: 30_000, env: { ...process.env, ...fx.repo.env } }).stdout.trim()).toBe(fx.repo.git('rev-parse', 'origin/main'));
|
||||
expect(fs.existsSync(path.join(coord, '.git', 'objects', 'maintenance.lock'))).toBe(false);
|
||||
expect(fs.readdirSync(path.join(coord, '.git', 'objects'))).not.toContain('gstack-dangling-seam');
|
||||
});
|
||||
|
||||
describe('the train command surface', () => {
|
||||
|
||||
Reference in new issue
Block a user