diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb1fe35f..a58561d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.93.34.0] - 2026-11-03 + +**The docsync fault-callback replays and the runShadow deadline test stop failing on loaded remote pregate VMs.** + +### Fixed +- **Docsync fault-callback replays** (`test/helpers/docsync-fault-callbacks.ts` and its three wrappers): each replay in the generated child now has a 30 s limit instead of bun's 5 s default, and each wrapper's limit is that per-replay limit times its replay count plus one. A replay spawns 30 to 40 real actor, hook and git processes. That takes about 1.3 s on CI and up to 4.9 s on an idle 4-core machine, and it ran past 5 s on GSTA-234's remote pregate for #3892. With 8 busy loops on 4 cores, main fails all 12 legacy-control replays at 5 s, and this branch passes all 12 (172 s for the whole wrapper). `test/docsync-report-interface.test.ts` gets the same limits, because its child cases ran 5.5 s and 11.4 s on #3869's remote pregate. +- **`test/autoplan-shadow.test.ts` "one deadline covers the call and its retry"**: the first call now has 1 s of room inside the deadline instead of 300 ms. The time limit allows 2.4 s past the deadline before it would match an unshared retry, up from 0.9 s; the remote failure ran 1.06 s past. The test still requires the retry to be cut (p1 malformed) and the call to end before an unshared retry could finish. + ## [1.93.33.0] - 2026-11-03 - On a checkout whose `origin/HEAD` points at the checked-out branch itself (a PR-branch clone), every base-relative step resolved the base to HEAD and reported zero changes, so review depth, `ready`, pregate and the docs audit passed on an empty diff (GST-774). Base resolution now skips a remote HEAD that names the checked-out branch, its upstream or HEAD's own commit (so a local branch under another name and a detached checkout of the PR branch are caught too), then uses the PR's base ref when `gh` finds one, then the remote's live default branch from `git ls-remote --symref`, then `main` or `master`. A repository whose default branch is `develop` or `trunk` keeps it. When the resolved base equals HEAD or contains it while the branch has commits its real base lacks, `gstack-diff-scope`, `gstack-gate-diff`, `gstack-review-plan`, `gstack-pregate`, `gstack-docs-candidate`, `/ship` policy base resolution and the receipt's depth binding refuse with `BASE_CONTAINS_HEAD` instead of reporting no changes. A pull request's head is refused whenever the base contains it; on the base branch itself, on a fresh branch with only uncommitted work, and on a detached HEAD given an explicit base, the step runs as before. `gstack-ship-queue land` resuming a merge-only PR that GitHub already merged with a true merge commit still lands, its depth check included. See docs/troubleshooting.md#base-contains-head. diff --git a/VERSION b/VERSION index bd584c813..56b8028cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.93.33.0 +1.93.34.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 9170ca05c..f2f1d212b 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.93.33.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.93.34.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, diff --git a/package.json b/package.json index 2444dbcd8..014883f88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.93.33", + "version": "1.93.34", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/autoplan-shadow.test.ts b/test/autoplan-shadow.test.ts index 7af295ee6..db4f550d1 100644 --- a/test/autoplan-shadow.test.ts +++ b/test/autoplan-shadow.test.ts @@ -271,15 +271,15 @@ describe('runShadow: retry, truncation, failures and the deadline', async () => expect(readLedger(path.join(r.out, 'spend.json')).settled.filter(s => s.label === 'shadow').map(s => s.usd)).toEqual([0]); }); test('one deadline covers the call and its retry: a slow malformed reply leaves the retry no time', async () => { - const r = await shadowed('deadline-retry', [{ mode: 'missing', delay_ms: 1200 }, { mode: 'valid', delay_ms: 1200 }], undefined, 1500); - expect(r.ms).toBeLessThan(2400); + const r = await shadowed('deadline-retry', [{ mode: 'missing', delay_ms: 2500 }, { mode: 'valid', delay_ms: 3400 }], undefined, 3500); + expect(r.ms).toBeLessThan(5900); expect(r.outcome.status).toBe('recorded'); expect(ids(r.out).map(x => x.status)).toEqual(['recorded', 'recorded', 'recorded', 'recorded', 'recorded', 'recorded', 'malformed']); - const slow = await shadowed('deadline-call', [{ mode: 'valid', delay_ms: 3000 }], undefined, 800); + const slow = await shadowed('deadline-call', [{ mode: 'valid', delay_ms: 6000 }], undefined, 800); expect(slow.outcome.line).toBe('SHADOW: failed timeout'); - expect(slow.ms).toBeLessThan(2500); + expect(slow.ms).toBeLessThan(5000); expect(readLedger(path.join(slow.out, 'spend.json')).settled.filter(s => s.label === 'shadow').map(s => s.usd)).toEqual(['unknown']); - }); + }, 15_000); test('a publish that throws journals shadow_failed and drops the lease, so a later claim pays no second call', async () => { const f = await fake('publish-throws', [{ mode: 'valid' }]); const out = gateRun(); diff --git a/test/docsync-fault-interface-faults.test.ts b/test/docsync-fault-interface-faults.test.ts index 8c4bd4844..eaa4237fe 100644 --- a/test/docsync-fault-interface-faults.test.ts +++ b/test/docsync-fault-interface-faults.test.ts @@ -3,7 +3,7 @@ import { expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { docsFaultCallbackScript } from './helpers/docsync-fault-callbacks'; +import { callbackBudgetMs, docsFaultCallbackScript } from './helpers/docsync-fault-callbacks'; const CALLBACK_SPLITS = [ ['fault controls', '^ship-docsync-(?!failure )[a-z-]+ rejects ', 14], @@ -21,7 +21,7 @@ for (const [split, pattern, count] of CALLBACK_SPLITS) test(`registered fault ca env: { ...process.env, EVALS: '', EVALS_TIER: '', EVALS_ALL: '', EVALS_RUN_ID: 'free-docsync-faults', HOME: home, USERPROFILE: home, GSTACK_EVAL_DIR: path.join(dir, 'evidence'), GSTACK_HOME: path.join(dir, 'state') }, - stdout: 'pipe', stderr: 'pipe', timeout: 120_000, + stdout: 'pipe', stderr: 'pipe', timeout: callbackBudgetMs(count), }); const output = result.stdout.toString() + result.stderr.toString(); expect(result.exitCode, output).toBe(0); @@ -29,4 +29,4 @@ for (const [split, pattern, count] of CALLBACK_SPLITS) test(`registered fault ca expect(output).toContain(`${36 - count} filtered out`); expect(output).toContain('0 fail'); } finally { fs.rmSync(dir, { recursive: true, force: true }); } -}, 120_000); +}, callbackBudgetMs(count) + 10_000); diff --git a/test/docsync-fault-interface-legacy.test.ts b/test/docsync-fault-interface-legacy.test.ts index d7173a477..b51acfaf7 100644 --- a/test/docsync-fault-interface-legacy.test.ts +++ b/test/docsync-fault-interface-legacy.test.ts @@ -3,7 +3,7 @@ import { expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { docsFaultCallbackScript } from './helpers/docsync-fault-callbacks'; +import { callbackBudgetMs, docsFaultCallbackScript } from './helpers/docsync-fault-callbacks'; const CALLBACK_SPLITS = [ ['legacy controls', '^ship-docsync-failure rejects ', 12], @@ -21,7 +21,7 @@ for (const [split, pattern, count] of CALLBACK_SPLITS) test(`registered fault ca env: { ...process.env, EVALS: '', EVALS_TIER: '', EVALS_ALL: '', EVALS_RUN_ID: 'free-docsync-faults', HOME: home, USERPROFILE: home, GSTACK_EVAL_DIR: path.join(dir, 'evidence'), GSTACK_HOME: path.join(dir, 'state') }, - stdout: 'pipe', stderr: 'pipe', timeout: 120_000, + stdout: 'pipe', stderr: 'pipe', timeout: callbackBudgetMs(count), }); const output = result.stdout.toString() + result.stderr.toString(); expect(result.exitCode, output).toBe(0); @@ -29,4 +29,4 @@ for (const [split, pattern, count] of CALLBACK_SPLITS) test(`registered fault ca expect(output).toContain(`${36 - count} filtered out`); expect(output).toContain('0 fail'); } finally { fs.rmSync(dir, { recursive: true, force: true }); } -}, 120_000); +}, callbackBudgetMs(count) + 10_000); diff --git a/test/docsync-fault-interface.test.ts b/test/docsync-fault-interface.test.ts index 5755cb9ec..fe6c05337 100644 --- a/test/docsync-fault-interface.test.ts +++ b/test/docsync-fault-interface.test.ts @@ -8,7 +8,7 @@ import { DOC_PATH, docsSnapshotArgv, fixtureDocs, gitAt, gitBlobId, repoSnapshot import { DOCS_CHECKPOINT_MARKER, DOCS_SEEDED_AUDIT_ID, docsActorCommand, docsActorHook, docsActorSeeded, installDocsActor, seedDocsFirstAttempt, type DocsActorState } from './helpers/docsync-fault-actor'; import { docsActorVerdict } from './helpers/docsync-fault-eval'; import { extractDocsDispatch, parseDocsCompletion } from './helpers/docsync-contract'; -import { docsFaultCallbackScript } from './helpers/docsync-fault-callbacks'; +import { callbackBudgetMs, docsFaultCallbackScript } from './helpers/docsync-fault-callbacks'; import { docsNativeInterface, docsWriteFailures, observeDocsWrites } from './helpers/docsync-observer'; test.skipIf(process.platform !== 'linux')('actor CLI validates owned state before creating even transient locks', async () => { @@ -485,7 +485,7 @@ for (const [split, pattern, count] of CALLBACK_SPLITS) test(`registered fault ca env: { ...process.env, EVALS: '', EVALS_TIER: '', EVALS_ALL: '', EVALS_RUN_ID: 'free-docsync-faults', HOME: home, USERPROFILE: home, GSTACK_EVAL_DIR: path.join(dir, 'evidence'), GSTACK_HOME: path.join(dir, 'state') }, - stdout: 'pipe', stderr: 'pipe', timeout: 120_000, + stdout: 'pipe', stderr: 'pipe', timeout: callbackBudgetMs(count), }); const output = result.stdout.toString() + result.stderr.toString(); expect(result.exitCode, output).toBe(0); @@ -493,4 +493,4 @@ for (const [split, pattern, count] of CALLBACK_SPLITS) test(`registered fault ca expect(output).toContain(`${36 - count} filtered out`); expect(output).toContain('0 fail'); } finally { fs.rmSync(dir, { recursive: true, force: true }); } -}, 120_000); +}, callbackBudgetMs(count) + 10_000); diff --git a/test/docsync-report-interface.test.ts b/test/docsync-report-interface.test.ts index 1923a0dbb..821094e80 100644 --- a/test/docsync-report-interface.test.ts +++ b/test/docsync-report-interface.test.ts @@ -2,6 +2,7 @@ import { expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { CALLBACK_TEST_MS, callbackBudgetMs } from './helpers/docsync-fault-callbacks'; test('actual registered documentation callbacks stage the generated report contract and consume their builders', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-report-interface-')); @@ -91,13 +92,13 @@ for (const name of ['ship-docsync-completion', 'ship-docsync-current', 'ship-doc await expect(callbacks.get(name)()).rejects.toBe(stopped); expect(launched).toBe(before + 1); expect(fs.existsSync(fixture.home)).toBe(false); - }); + }, ${CALLBACK_TEST_MS}); } `); const result = Bun.spawnSync([process.execPath, 'test', script], { env: { ...process.env, EVALS: '', EVALS_TIER: '', EVALS_ALL: '', EVALS_RUN_ID: 'free-docsync-interface', GSTACK_EVAL_DIR: path.join(dir, 'evidence'), GSTACK_HOME: path.join(dir, 'state') }, - stdout: 'pipe', stderr: 'pipe', timeout: 120_000, + stdout: 'pipe', stderr: 'pipe', timeout: callbackBudgetMs(4), }); const output = result.stdout.toString() + result.stderr.toString(); expect(result.exitCode, output).toBe(0); @@ -106,4 +107,4 @@ for (const name of ['ship-docsync-completion', 'ship-docsync-current', 'ship-doc } finally { fs.rmSync(dir, { recursive: true, force: true }); } -}); +}, callbackBudgetMs(4) + 10_000); diff --git a/test/helpers/docsync-fault-callbacks.ts b/test/helpers/docsync-fault-callbacks.ts index 94804e754..31a158557 100644 --- a/test/helpers/docsync-fault-callbacks.ts +++ b/test/helpers/docsync-fault-callbacks.ts @@ -6,6 +6,9 @@ import * as path from 'node:path'; import { DOCS_CHECKPOINT_MARKER } from './docsync-fault-actor'; +export const CALLBACK_TEST_MS = 30_000; +export const callbackBudgetMs = (count: number): number => (count + 1) * CALLBACK_TEST_MS; + export function docsFaultCallbackScript(root: string): string { return ` import { expect, mock, test } from 'bun:test'; @@ -20,6 +23,7 @@ const observers = { ...await import(observerModule) }; const { CAPTURE_MS, CAPTURE_LONG_MS } = await import(path.join(root, 'test/helpers/eval-budgets.ts')); const { parseDocsCompletion } = await import(path.join(root, 'test/helpers/docsync-contract.ts')); const { docsActorSeeded, DOCS_SEEDED_AUDIT_ID } = await import(path.join(root, 'test/helpers/docsync-fault-actor.ts')); +const CALLBACK_TEST_MS = ${CALLBACK_TEST_MS}; const callbacks = new Map(); let fixture, control = '', launches = 0, recorded, legacyReaudit = false; let returnedResult: SkillTestResult | undefined; @@ -342,7 +346,7 @@ for (const name of names) test(name + ' consumes actual adapter results', async expect(recorded).toEqual({ passed: true }); expect(consumers).toEqual(['logCost', 'recordE2E']); expect(fs.existsSync(fixture.home)).toBe(false); -}); +}, CALLBACK_TEST_MS); test('ship-docsync-failure accepts its remaining changed-input audit and preserves raw output in Markdown', async () => { control = ''; legacyReaudit = true; @@ -353,7 +357,7 @@ test('ship-docsync-failure accepts its remaining changed-input audit and preserv expect(recorded).toEqual({ passed: true }); expect(consumers).toEqual(['logCost', 'recordE2E']); expect(fs.existsSync(fixture.home)).toBe(false); -}); +}, CALLBACK_TEST_MS); for (const [name, mutation] of [ ['ship-docsync-failure', 'false-current'], ['ship-docsync-failure', 'invalid-publication'], ['ship-docsync-failure', 'restore-partial'], ['ship-docsync-failure', 'user-content'], @@ -381,6 +385,6 @@ for (const [name, mutation] of [ expect(recorded).toEqual({ passed: false }); expect(consumers).toEqual(['logCost', 'recordE2E']); expect(fs.existsSync(fixture.home)).toBe(false); -}); +}, CALLBACK_TEST_MS); `; }