v1.92.71.0 test(free): end four non-subject waits early and drop a duplicated test block (GSTA-151 D) (#3582)

* test(free): end four non-subject waits early and drop a duplicated test block

setup's _wait_with_deadline polls every 0.1 s on the SECONDS clock; pregate-supervise and gstack-detach wait on pids and go-files instead of fixed sleeps; qa-probe-gates loses its duplicated C2 block; context-identity runs one restore.

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

* test(setup): mark the sub-second wait bound as the contract (gstack-timing-ok)

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

* docs(changelog): unreleased entry for the free-suite wait pass (GSTA-151 D)

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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-02 09:57:10 +00:00
1 parent cd788faec7
commit 69c14b17d5
10 files changed
+50 -54

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
# Changelog
## [1.92.71.0] - 2026-11-02
**Four free-suite waits now end when the thing they wait for happens: the six files involved spend 18–29 s less in summed per-file runtime, with every assertion intact.** `./setup` returns from a finished child up to ~0.9 s sooner too.
### Changed
- **`setup` `_wait_with_deadline`**: polls every 0.1 s and tracks the deadline on bash's `SECONDS` clock instead of sleeping a whole second per poll. A child that exits at once no longer costs a second. The kill still lands no earlier than the deadline, the exit codes (the child's status, or 124 after the kill) are unchanged, and it runs under bash 3.2.
### Tests
- `test/setup-playwright-best-effort.test.ts`: a child that exits with 3 after 50 ms returns 3 in under 500 ms (the old loop took 1,008 ms); the deadline and kill cases are unchanged. The file drops from ~20.8 s to ~5.9 s on a CI-mirror VM.
- `test/pregate-supervise.test.ts`: the leftover-child case waits for that child's pid to exit (bounded at 3 s) instead of a fixed 2.5 s sleep.
- `test/gstack-detach.test.ts`: the detach and lock bodies wait on files the test writes (bounded at ~10 s, also written in `finally`) instead of `sleep 2` and `sleep 3`.
- `test/qa-probe-gates.test.ts`: the duplicated "judge-named QA workflow gaps (C2)" block is gone (3 tests that repeated the first block).
- `test/context-identity.test.ts`: one `restore(mono)` call serves both assertions.
## [1.92.70.0] - 2026-11-02
**Five free-test files stop rebuilding the same git repositories for every test: they spend 17–24 s less in summed per-file runtime, with every assertion intact.** Each test still gets its own private copy of the fixture.
+1 -1
View File
@@ -1 +1 @@
1.92.70.0
1.92.71.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.92.70.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.92.71.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.92.70",
"version": "1.92.71",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+4 -4
View File
@@ -1539,15 +1539,15 @@ _kill_tree() {
# poll the PID and SIGKILL the whole probe tree past the deadline. Returns
# the probe's exit code, or 124 on timeout.
_wait_with_deadline() {
local pid="$1" deadline_s="$2" waited=0
local pid="$1" deadline_s="$2" end
end=$((SECONDS + deadline_s))
while kill -0 "$pid" 2>/dev/null; do
if [ "$waited" -ge "$deadline_s" ]; then
if [ "$SECONDS" -gt "$end" ]; then
_kill_tree "$pid"
wait "$pid" 2>/dev/null || true
return 124
fi
sleep 1
waited=$((waited + 1))
sleep 0.1
done
wait "$pid"
}
+3 -2
View File
@@ -353,8 +353,9 @@ describe('context-restore offers newer checkpoints it used to skip (#3065)', ()
const sibling = repo('elsewhere', 'git@github.com:acme/elsewhere.git');
const outside = save(sibling, 'sibling work', '20261002-100000');
const below = save(pkg, 'pkg feature', '20261001-100000');
expect(restore(mono).out).not.toContain(outside);
expect(restore(mono).lines).toContain(`BELOW_CWD ${below}`);
const r = restore(mono);
expect(r.out).not.toContain(outside);
expect(r.lines).toContain(`BELOW_CWD ${below}`);
// 200 newer checkpoints of another project push the below-cwd one out of the read window.
const dir = path.dirname(outside);
+10 -4
View File
@@ -25,6 +25,7 @@ function waitFor(pred: () => boolean, ms: number): boolean {
}
return pred();
}
const GO_FILE_WAIT = 'i=0; while [ ! -e "$1" ] && [ "$i" -lt 100 ]; do sleep 0.1; i=$((i + 1)); done; [ -e "$1" ]';
function logHas(p: string, needle: string): boolean {
try { return fs.readFileSync(p, 'utf-8').includes(needle); } catch { return false; }
}
@@ -33,19 +34,21 @@ describe('gstack-detach', () => {
test('detaches (different pgid), returns immediately, completes, writes EXIT sentinel', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gd-'));
const log = path.join(dir, 'run.log');
const go = path.join(dir, 'go');
try {
const t0 = Date.now();
const r = spawnSync(DETACH, ['--log', log, '--', 'bash', '-c', 'sleep 2; echo body-ran'], { encoding: 'utf-8', timeout: 10000 });
const r = spawnSync(DETACH, ['--log', log, '--', 'bash', '-c', GO_FILE_WAIT + ' && echo body-ran', 'body', go], { encoding: 'utf-8', timeout: 10000 });
const elapsed = Date.now() - t0;
expect(r.status).toBe(0);
expect(r.stdout).toContain(`gstack-detach LOG ${log}`);
expect(elapsed).toBeLessThan(1500); // non-blocking
fs.writeFileSync(go, '');
expect(waitFor(() => logHas(log, '### gstack-detach EXIT=0 ###'), 8000)).toBe(true);
expect(logHas(log, 'body-ran')).toBe(true); // ran to completion after launcher returned
const m = fs.readFileSync(log, 'utf-8').match(/pgid=(\d+)/);
expect(m).not.toBeNull();
expect(m![1]).not.toBe(ownPgid()); // detached into its own group
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
} finally { fs.writeFileSync(go, ''); fs.rmSync(dir, { recursive: true, force: true }); }
}, 15000);
test('default log is run-scoped under ~/.gstack-dev/eval-runs (no shared /tmp)', () => {
@@ -127,17 +130,20 @@ describe('gstack-detach', () => {
const lock = `gstack-detach-test-${process.pid}`;
const logA = path.join(dir, 'a.log');
const logB = path.join(dir, 'b.log');
const release = path.join(dir, 'release');
try {
// First holds the lock for ~3s; second must wait then acquire.
spawnSync(DETACH, ['--log', logA, '--lock', lock, '--', 'sleep', '3'], { encoding: 'utf-8', timeout: 10000, env: { ...process.env, HOME: dir } });
// First holds the lock until the test writes the release file; second must wait then acquire.
spawnSync(DETACH, ['--log', logA, '--lock', lock, '--', 'bash', '-c', GO_FILE_WAIT, 'holder', release], { encoding: 'utf-8', timeout: 10000, env: { ...process.env, HOME: dir } });
waitFor(() => logHas(logA, "ACQUIRED"), 4000);
spawnSync(DETACH, ['--log', logB, '--lock', lock, '--', 'echo', 'second-ran'], { encoding: 'utf-8', timeout: 10000, env: { ...process.env, HOME: dir } });
// Second should report WAITING (first still holds it) then ACQUIRE after release.
expect(waitFor(() => logHas(logB, 'WAITING for lock'), 4000)).toBe(true);
fs.writeFileSync(release, '');
expect(waitFor(() => logHas(logB, '### gstack-detach EXIT=0 ###'), 12000)).toBe(true);
expect(logHas(logB, 'second-ran')).toBe(true);
expect(fs.existsSync(path.join(dir, '.gstack', 'locks', `${lock}.lock`))).toBe(true);
} finally {
fs.writeFileSync(release, '');
fs.rmSync(dir, { recursive: true, force: true });
}
}, 20000);
+4 -2
View File
@@ -226,9 +226,11 @@ describe('gstack-pregate: the test stage never hangs silently', () => {
expect(pregate(during, []).status).toBe(EXIT.fail);
expect(lanes(during).detail).toContain('working tree changed during the check');
const after = makeFixtureRepo(); repos.push(after);
commit(after, { 'test/leftover.test.js': "const test = require('node:test');\nconst { spawn } = require('node:child_process');\ntest('leaves a child', () => { spawn('sh', ['-c', 'sleep 2; echo changed >> README.md'], { stdio: 'ignore' }).unref(); });\n" });
commit(after, { 'test/leftover.test.js': "const test = require('node:test');\nconst { spawn } = require('node:child_process');\ntest('leaves a child', () => { const c = spawn('sh', ['-c', 'sleep 2; echo changed >> README.md'], { stdio: 'ignore' }); require('node:fs').writeFileSync(require('node:path').join(__dirname, '..', '..', 'leftover.pid'), String(c.pid)); c.unref(); });\n" });
expect(pregate(after, []).status).toBe(EXIT.ok);
await Bun.sleep(2500);
const until = Date.now() + 3000;
const leftover = await pidFrom(path.join(after.root, 'leftover.pid'), 3000).catch(() => 0);
if (leftover) await goneSoon(leftover, Math.max(0, until - Date.now()));
expect(fs.readFileSync(path.join(after.work, 'README.md'), 'utf8')).toBe('# fixture\n');
}, 90_000);
-39
View File
@@ -344,42 +344,3 @@ describe('judge-named QA workflow gaps (C2)', () => {
}
});
});
// Lane S C2: gaps the qa-only/review/ship workflow judges named in their rationales.
describe('judge-named QA workflow gaps (C2)', () => {
const ctx = (skillName: string) => ({ host: 'claude' as const, skillName, tmplPath: '', paths: HOST_PATHS.claude });
test('Browser Quick leaves time for its six page probes, and the method states the same budget', () => {
for (const skillName of ['qa', 'qa-only']) {
const seconds = Number(/Browser Quick: SECONDS=(\d+)\./.exec(generateQAExploratory(ctx(skillName)))?.[1]);
// homepage + top 5 targets, each a checkpoint write plus one guarded command: 30 s left ~5 s per probe.
expect(seconds / 6).toBeGreaterThanOrEqual(20);
const patterns = fs.readFileSync(path.join(import.meta.dir, '..', 'qa', 'sections', 'qa-patterns.md'), 'utf8');
expect(patterns).toContain(`${seconds / 60} minutes: homepage + top 5 navigation targets`);
}
});
test('PROBE_DIR names its source line, and stopping goes to the final report steps', () => {
for (const skillName of ['qa', 'qa-only']) {
const text = generateQAExploratory(ctx(skillName));
const layout = text.indexOf('mixed standalone runs use REPORT_DIR/browser and REPORT_DIR/functional');
const probeDir = text.indexOf("PROBE_DIR: this surface's owned probe directory per the line above.");
expect(layout).toBeGreaterThan(-1);
expect(probeDir).toBeGreaterThan(layout);
expect(text.slice(layout, probeDir).split('\n')).toHaveLength(3);
expect(text).toContain('write the report (§4), not a checkpoint.');
const final = text.indexOf('## 4. Final report');
expect(final).toBeGreaterThan(-1);
expect(text.indexOf('annotations.json', final)).toBeLessThan(text.indexOf('materialize PROBE_DIR', final));
}
});
test('/review and /ship: smoke rechecks after smoke expiry are not-run, before step 4c revalidation', () => {
for (const skillName of ['review', 'ship']) {
const text = generateQAReview(ctx(skillName));
const rule = text.indexOf('Post-expiry smoke rechecks are not-run.');
expect(rule).toBeGreaterThan(-1);
expect(rule).toBeLessThan(text.indexOf('c. Re-review changed or uncertain coverage and repeat step 3 for affected checks.'));
}
});
});
+12
View File
@@ -271,6 +271,18 @@ describe('setup: Chromium bootstrap block executes best-effort', () => {
expect(fs.readFileSync(path.join(r.tmp, 'mark'), 'utf-8')).toContain('killed');
}, 15_000);
test('_wait_with_deadline returns a quickly exiting child\'s status without waiting out a whole second', () => {
const t0 = Date.now();
const r = runBashScript([
extractFn('_kill_tree'), extractFn('_wait_with_deadline'),
'(sleep 0.05; exit 3) &',
'_wait_with_deadline $! 90 || echo "RC=$?"',
].join('\n'), { timeout: 10_000 });
expect(r.stdout).toBe('RC=3\n');
// gstack-timing-ok: a sub-second return is the contract; the old 1 s poll took 1,008 ms, this path takes ~0.2 s
expect(Date.now() - t0).toBeLessThan(500);
});
test('non-numeric timeout knob falls back to the default instead of erroring', () => {
const r = runBlock({ probe: 'fail', bunx: 'exit 3', env: { GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT: 'soon' } });
expect(r.status).toBe(0);