Merge origin/main (v1.91.105.0) into capy/next-wave; restamp 1.91.106.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-16 20:47:35 +00:00
commit e9182b1289
20 files changed
+1779 -1116

No files matched your search

+46 -1
View File
@@ -1,6 +1,6 @@
# Changelog
## [1.91.102.0] - 2026-10-14
## [1.91.106.0] - 2026-10-16
**The GBRA-40 field list is fixed: install and check agree on one Bun, `rec` approves an autoplan gate in one word, and pregate never sits silent.** Eighteen Capy threads upgraded to v1.91.101.0 and reported 21 problems (`GBRA-40`, October 14). Each gstack item was reproduced with a test that failed first; the plan went through `/autoplan` (two gate revisions, Claude and GPT-6.1 Sol voices per phase) and two independent code reviews. Two items are Capy's (skills not mounted on some crew threads, Drive copies not re-synced) and are handed off, not worked around.
@@ -27,6 +27,51 @@
- New tests: `test/bun-resolve.test.ts`, `test/autoplan-checkpoint.test.ts`, `test/autoplan-pairing.test.ts` (with this run's real pairs as fixtures), `test/pregate-supervise.test.ts`, `test/pregate-remote.test.ts`; many existing suites gained regressions.
- The ship parity union cap is 1.6488 (measured), and the Capy skill's eager budget is 2601 tokens for its new autoplan checklist.
## [1.91.105.0] - 2026-10-16
**The free suite stops needing its retry pass.
The health report tells the truth.**
Six of 46 main runs last week needed the free suite's serial retry pass to go green. Five test files caused it, and three shared one cause: since git 2.29, every `git commit` starts a background `git maintenance run` that creates and deletes `.git/objects/maintenance.lock` after the commit has returned, so a test that lists, watches or deletes `.git` right afterwards sees a file that is there one moment and gone the next. Those fixtures now turn auto-maintenance off, and the free runner does it for every test child as a backstop. The other two flakes were a fixed-count poll and a smoke test that called live GitHub. `gstack-timeline-read` now refuses filters it cannot apply instead of returning everything.
### The numbers that matter
Measured with the repo's own reproducer (`scripts/ship-measure.ts free`, flaky retry off), before on 47d639a and after on this branch. Commands and per-file counts are in the PR body.
| Measurement | Before | After |
|---|---|---|
| The five ledger files, 16-vCPU Ubicloud VM (`bun run test:ubicloud --diagnostic`) | 197/200 reruns clean | 600/600 |
| The three git fixtures under commit load, local runner env | 37/200 clean | 200/200 |
| Next-version smoke test, live GitHub calls | one `gh api` per open PR, one after another (27 PRs at 1.2 s: 32.6 s, past its 30 s limit) | none (runs in about 60 ms) |
| Weekly health: unseeded free test files | 164 (enforced check failing) | 0 |
| Weekly health: Windows wall population | dispatch recordings on main ("n/a (0 runs)" on Oct 12) | 78 successful PR runs, 9.2 min p50 (7 days to 2026-10-15) |
The headline number is the first row: three of 200 clean-tree reruns failed before, none of 600 after.
### What this means for you
When the free suite goes red now, it is far more likely to be your change. Read the failure, not the retry ledger.
### Itemized changes
#### Fixed
- **If you run `gstack-timeline-read --since` or `--branch` without a value,** you get exit 2 and `--since must be a relative time like "7 days ago" (got no value); accepted: ...` (or `--branch must be a branch name (got flag '--limit')`) instead of a crash.
- **If you pass `--since yesterday` or `--since "2 weeks"`,** you get exit 2 naming what it got, instead of your whole unfiltered history. Accepted forms: `7 days ago` (minute, hour, day, week, month), `7d` / `24h` / `2w`, and ISO dates `2026-10-01` or `2026-10-01T10:00:00Z`.
- **If you pass an argument `gstack-timeline-read` does not know,** it exits 2 with the usage line instead of ignoring it. `--help` prints the usage line.
#### Changed
- **If you read the weekly test-health report,** "Free-suite and Windows wall" now measures successful Windows runs on pull requests, says so (`windows-free-tests (successful PR runs) 9.2 min (78 runs)`), and prints `no successful PR runs in window` when there are none. It used to average the dispatch-only duration recordings on main.
- **If the health report says the duration seed is stale,** its fix line now names what the refresh needs (`UBICLOUD_API_KEY`, then `bun run test:ubicloud --record-durations`, which copies the seed back into your checkout) and tells contributors without the key to leave it to a maintainer. It no longer offers a CI recording, which does not exist.
#### For contributors
- The free runner appends `maintenance.auto=false` and `gc.auto=0` to every shard child's `GIT_CONFIG_*` pairs (`test/test-free-shards-git-maintenance.test.ts`). Fixtures that build their own git environment set the keys themselves; `docs/TESTING_INTERNALS.md#flake-ledger` has the rule and a recipe for reproducing a ledger entry from a CI run's `free-plan` artifact.
- The zombie publication-lock test synchronizes on a PID line its Python parent prints after `os.waitid(..., WNOWAIT)`, and the next-version smoke test runs the real CLI against a fake `gh` that logs and checks every call.
- `scripts/free-test-durations.json` is refreshed from a Ubicloud recording of this tree.
- No community contributions in this release.
## [1.91.101.0] - 2026-10-13
**The field wave's residual gaps are closed.** v1.91.96.0 through v1.91.100.0 shipped the install-honesty and unattended-`/autoplan` wave (`docs/designs/GSTACK_FIELD_WAVE_2026_10_12.md`); running that release on a Capy machine left six gaps, each reproduced here with a test that failed first.
+62 -3
View File
@@ -88,9 +88,6 @@ Left open by the office-hours tier fix (#2801, #879, #1049, #1958, #1723, #1651
follows the carried `SESSION_TIER` through Phase 6 (first run gets the
introduction, second run asks about the earlier assignment). No office-hours
quality judge exists either. **Effort:** M. **Priority:** P2.
- **`gstack-timeline-read --since`/`--branch` without a value** — both still
read an unbound `$2` under `set -u` and crash with exit 1, the class #1723
fixed for `--limit`. **Effort:** S. **Priority:** P3.
### P3: Oct 8 fix wave, safety hooks follow-ups (filed 2026-10-08)
@@ -5394,3 +5391,65 @@ skills were told is fenced.
Guard both writes. Effort S.
**Priority:** P3. **Depends on:** none.
### P1–P2: fix wave 2 follow-ups (filed 2026-10-15)
From the free-suite health wave (flaky-pass fixes, #3154 seed refresh). Owner
Garry for each; revisit 2026-10-29 unless a date says otherwise.
- **`gstack-next-version` fetches claimed slots one PR at a time** —
`GH_API_CONCURRENCY = 10` (bin/gstack-next-version:90), but its async workers
call `spawnSync`, so the per-PR `gh api` calls run serially: 8 stubbed PRs at
0.5 s each took 4160 ms instead of about 500 ms (4108 ms on a rerun), and 27
open PRs at about 1.2 s each pass the 30 s that callers allow, which is how the
old live smoke test failed. Every `/ship` slot allocation pays this. Fix:
async spawn with bounded overlap, verified by a delayed fake `gh`. Changing the
CLI selects 108 paid cases and 24 judges, so it ships as its own priced PR on
Garry's call. **Effort:** S. **Priority:** P1.
- **Standing Linux duration-seed recorder** — the seed refresh in this wave
keeps CEO-10 green for days, not weeks: main gains about 15 free test files a
day against a limit of 5 unseeded, so the weekly health run goes red again
until a recorder lands. Design: a scheduled `free-durations-record.yml` that
records on the standard runner and updates one seed PR before the Monday
health run, or the `free-plan` job consuming the latest recording artifact once
#3190 (owner of `free-tests.yml`) lands. Agree the design with the owner of
`free-tests.yml` before 2026-10-29. **Effort:** M. **Priority:** P1.
- **PR full-fallback and recorded spend (audit next step)** — 2026-10-15:
full-fallback 45.9% of PR runs, $73.52 recorded PR-lane spend per merged PR,
push-to-verdict p50 11.4 min (target 3–5 min). The pre-change spend baseline
was never recorded, so the 50% reduction verdict is unavailable
(docs/test-audit-2026-10.md, "Post-merge follow-up"). Next step, from the
audit's selection replay: touchfile mappings for `bin/`, `lib/`, `hosts/` and
`make-pdf/`, so a branch touching them stops selecting every case. Candidate
headline for the next wave. **Effort:** M. **Priority:** P1.
- **Free flaky-pass target follow-ups (2026-10-22 and 2026-10-29)** — read
main's `flake-ledger-*` artifacts (`bun run test:health --since-days 7`) and
check the free-suite flaky-pass rate is under 5% of main runs; a file still in
the ledger gets a named cause or a new entry here. **Effort:** S. **Priority:** P1.
- **`createQAFunctionalFixture` sets git auto-maintenance off itself** — its
repos inherit the free runner's backstop keys in CI, but a direct `bun test`
run still races `.git/objects/maintenance.lock` during observer setup.
The helper selects 22 paid cases, so this is a priced follow-up.
**Effort:** S. **Priority:** P2.
- **`test:ubicloud --diagnostic` cannot give clean counts for two ledger
shards** — `ship-measure free` reruns set a private `HOME` under `/tmp` and
`GSTACK_STATE_ROOT`, so `browse/test/path-validation.test.ts` (its upload
fixture builds an "outside" directory under `os.userInfo().homedir`, which
Bun takes from `HOME`) and `browse/test/pair-agent-tunnel-eval.test.ts` (its
denial log follows the inherited state root, not its fake `HOME`) fail 30/30
in diagnostic mode while passing in the normal suite. Ledger shards that hold
them can never report `passed = n`. **Effort:** S. **Priority:** P2.
- **`test/plan-seed-submission.test.ts` reads its fake CLI's events after a
fixed 50 ms sleep** — "actual PTY launcher carries placeholder styling into
owned seed submission: xterm-256color" sends `/plan-eng-review\r`, sleeps
50 ms and expects the `slash` event already in `events.jsonl`; under the
16-worker duration recording on 2026-10-15 the event had not landed
(`['paste', 'enter', 'end_turn']`). Wait for the CLI's own write instead of a
timer (docs/TESTING_INTERNALS.md#flake-ledger). **Effort:** S. **Priority:** P2.
- **Split the free files the 2026-10-15 seed put over 60 s** —
`test/setup-install-registry.test.ts` (82 s), `test/host-renders-prune.test.ts`
(64 s) and `test/setup-codex-scope.test.ts` (60.4 s) are allowlisted with this
todo in `scripts/free-test-seed-allowlist.json`; `test/brain-sync.test.ts`
(57 s) and `test/setup-codex-scope-namespaces.test.ts` (57 s) are next. Split
each by describe block, rerun `bun run test:ubicloud --record-durations` and
delete the entries. **Effort:** S each. **Priority:** P2.
+1 -1
View File
@@ -1 +1 @@
1.91.102.0
1.91.106.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.102.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.106.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,
+69 -20
View File
@@ -1,26 +1,78 @@
#!/usr/bin/env bash
# gstack-timeline-read — read and format project timeline
# Usage: gstack-timeline-read [--since "7 days ago"] [--limit N] [--branch NAME]
# Usage: gstack-timeline-read [--since "7 days ago"|7d|YYYY-MM-DD] [--limit N] [--branch NAME]
#
# Session timeline: local-only, never sent anywhere.
# Reads ~/.gstack/projects/$SLUG/timeline.jsonl, filters, formats.
# Exit 0 silently if no timeline file exists or nothing matches. Missing bun
# exits 127 and a failed read exits non-zero with its stderr (#2790). A missing
# or non-positive-integer --limit exits 2 before any read (#1723).
# or non-positive-integer --limit, a missing or unparseable --since, a missing
# --branch, or an unknown argument exits 2 before any read (#1723); --help
# prints the usage line and exits 0.
set -euo pipefail
USAGE='Usage: gstack-timeline-read [--since "7 days ago"|7d|YYYY-MM-DD] [--limit N] [--branch NAME]'
SINCE_FORMS='accepted: "<N> minute|hour|day|week|month[s] ago", "<N>h|d|w", or an ISO date YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ'
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
gstack_state_root_select
GSTACK_HOME="$_gstack_sr_root"
SINCE=""
SINCE_MS=0
LIMIT=20
BRANCH=""
missing_value() { # remaining-arg-count next-arg -> why the flag has no usable value, or nothing
if [[ $1 -lt 2 ]]; then echo "got no value"
elif [[ -z "$2" ]]; then echo "got an empty value"
elif [[ "$2" == --* ]]; then echo "got flag '$2'"
fi
return 0
}
days_from_civil() { # year month day (UTC) -> days since 1970-01-01
local y=$((10#$1)) m=$((10#$2)) d=$((10#$3))
[[ $m -le 2 ]] && y=$((y - 1))
local era=$((y / 400)) yoe doy mp
yoe=$((y - era * 400)); mp=$(((m + 9) % 12)); doy=$(((153 * mp + 2) / 5 + d - 1))
echo $((era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468))
}
since_cutoff_ms() { # one --since value -> cutoff epoch ms, or nothing when unparseable
local now n unit seconds lower
now=$(date -u +%s)
lower=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
if [[ "$lower" =~ ^([1-9][0-9]{0,5})[[:space:]]*(minute|hour|day|week|month)s?[[:space:]]+ago$ ]]; then
n=${BASH_REMATCH[1]}; unit=${BASH_REMATCH[2]}
elif [[ "$lower" =~ ^([1-9][0-9]{0,5})([hdw])$ ]]; then
n=${BASH_REMATCH[1]}
case ${BASH_REMATCH[2]} in h) unit=hour ;; d) unit=day ;; w) unit=week ;; esac
elif [[ "$1" =~ ^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])(T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])Z)?$ ]]; then
seconds=$(( $(days_from_civil "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}") * 86400 ))
[[ -n "${BASH_REMATCH[4]}" ]] && seconds=$((seconds + 10#${BASH_REMATCH[5]} * 3600 + 10#${BASH_REMATCH[6]} * 60 + 10#${BASH_REMATCH[7]}))
echo $((seconds * 1000)); return 0
else
return 0
fi
case $unit in minute) seconds=60 ;; hour) seconds=3600 ;; day) seconds=86400 ;; week) seconds=604800 ;; month) seconds=2592000 ;; esac
echo $(((now - n * seconds) * 1000))
}
while [[ $# -gt 0 ]]; do
case "$1" in
--since) SINCE="$2"; shift 2 ;;
--help|-h) echo "$USAGE"; exit 0 ;;
--since)
problem=$(missing_value $# "${2-}")
if [[ -n "$problem" ]]; then
echo "gstack-timeline-read: --since must be a relative time like \"7 days ago\" ($problem); $SINCE_FORMS" >&2
exit 2
fi
SINCE_MS=$(since_cutoff_ms "$2")
if [[ -z "$SINCE_MS" ]]; then
echo "gstack-timeline-read: --since must be a relative time like \"7 days ago\" (got '$2'); $SINCE_FORMS" >&2
exit 2
fi
shift 2 ;;
--limit)
if [[ $# -lt 2 ]]; then
echo "gstack-timeline-read: --limit must be a positive integer (got no value); example: --limit 20" >&2
@@ -31,8 +83,17 @@ while [[ $# -gt 0 ]]; do
exit 2
fi
LIMIT="$2"; shift 2 ;;
--branch) BRANCH="$2"; shift 2 ;;
*) shift ;;
--branch)
problem=$(missing_value $# "${2-}")
if [[ -n "$problem" ]]; then
echo "gstack-timeline-read: --branch must be a branch name ($problem)" >&2
exit 2
fi
BRANCH="$2"; shift 2 ;;
*)
echo "gstack-timeline-read: unknown argument '$1'" >&2
echo "$USAGE" >&2
exit 2 ;;
esac
done
@@ -47,23 +108,11 @@ command -v bun >/dev/null 2>&1 || {
exit 127
}
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE="$SINCE" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun -e "
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE_MS="$SINCE_MS" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const since = process.env.GSTACK_TIMELINE_SINCE || '';
const branch = process.env.GSTACK_TIMELINE_BRANCH || '';
const limit = Number(process.env.GSTACK_TIMELINE_LIMIT);
let sinceMs = 0;
if (since) {
// Parse relative time like '7 days ago'
const match = since.match(/(\d+)\s*(day|hour|minute|week|month)s?\s*ago/i);
if (match) {
const n = parseInt(match[1]);
const unit = match[2].toLowerCase();
const ms = { minute: 60000, hour: 3600000, day: 86400000, week: 604800000, month: 2592000000 };
sinceMs = Date.now() - n * (ms[unit] || 86400000);
}
}
const sinceMs = Number(process.env.GSTACK_TIMELINE_SINCE_MS);
const entries = [];
for (const line of lines) {
@@ -6,7 +6,8 @@ import { spawnSync } from 'node:child_process';
import { acquireAgentStateLock, agentRecordPath, readAgentStartTime, writeAgentRecord, type AgentRecord } from '../src/terminal-agent-control';
const roots: string[] = [];
const children: ReturnType<typeof Bun.spawn>[] = [];
const children: Bun.Subprocess[] = [];
let unfinished: (() => Promise<Error>) | undefined;
const directory = () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'publication-lock-'));
roots.push(root);
@@ -31,6 +32,8 @@ async function fixture(dead = true) {
}
afterEach(async () => {
if (unfinished) console.error((await unfinished()).message);
unfinished = undefined;
for (const child of children.splice(0)) {
if (child.exitCode === null) try { child.kill('SIGKILL'); } catch {}
await child.exited;
@@ -51,25 +54,36 @@ describe('owned terminal-agent publication lock recovery', () => {
test.skipIf(process.platform !== 'linux')('a dead zombie with the exact recorded birth cannot retain its publication lock', async () => {
const root = directory();
const ready = path.join(root, 'zombie-pid');
const python = [
'import os,time',
'pid=os.fork()',
'if pid==0: os._exit(0)',
`with open(${JSON.stringify(ready)},'w') as f: f.write(str(pid))`,
'os.waitid(os.P_PID,pid,os.WEXITED|os.WNOWAIT)',
'print(pid,flush=True)',
'time.sleep(30)',
].join('\n');
const parent = Bun.spawn(['python3', '-c', python], { stdio: ['ignore', 'ignore', 'ignore'] });
const parent = Bun.spawn(['python3', '-c', python], { stdio: ['ignore', 'pipe', 'pipe'] });
children.push(parent);
for (let n = 0; n < 300 && !fs.existsSync(ready); n++) await Bun.sleep(10);
expect(fs.existsSync(ready)).toBe(true);
const pid = Number(fs.readFileSync(ready, 'utf8'));
let state = '';
for (let n = 0; n < 300; n++) {
state = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').match(/^\d+ \(.*\) ([A-Z])/u)?.[1] || '';
if (state === 'Z') break;
await Bun.sleep(10);
const failure = async (reason: string) => {
if (parent.exitCode === null) parent.kill('SIGKILL');
const status = await parent.exited;
const stderr = await new Response(parent.stderr).text();
return new Error(`${reason}; zombie parent exit status ${status}; stderr tail: ${stderr.slice(-500) || '(empty)'}`);
};
unfinished = () => failure('zombie parent never reported its exited child');
const reader = parent.stdout.getReader();
let line = '';
while (!line.includes('\n')) {
const chunk = await reader.read();
if (chunk.done) throw await failure(`zombie parent closed stdout before a full PID line (got ${JSON.stringify(line)})`);
line += new TextDecoder().decode(chunk.value);
}
reader.releaseLock();
unfinished = undefined;
const reported = line.slice(0, line.indexOf('\n'));
if (!/^[1-9]\d*$/.test(reported)) throw await failure(`zombie parent reported a malformed PID ${JSON.stringify(reported)}`);
const pid = Number(reported);
const state = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').match(/^\d+ \(.*\) ([A-Z])/u)?.[1] || '';
expect(state).toBe('Z');
expect(() => process.kill(pid, 0)).not.toThrow();
const record: AgentRecord = { pid, gen: 'zombie-generation', startedAt: Date.now(),
+33 -4
View File
@@ -1133,15 +1133,44 @@ Free-suite flake rules:
daemon without `CI=true` launches Chromium sandboxed.
- Handshakes with a runner wait for the runner's own observable effect (its
progress line, a coalesced read count), not for a fixture side file or a timer.
- Fixtures that commit and then list, watch or remove a repository's `.git`
turn git auto-maintenance off themselves (`maintenance.auto=false` and
`gc.auto=0` in the repo config, or in the explicit git env they build). Since
git 2.29, `git commit` starts a detached `git maintenance run --auto` that
creates and deletes `.git/objects/maintenance.lock` after the commit has
returned; a listing counts it, a watcher's `lstat` misses it, and Bun's
`rmSync(..., { recursive: true, force: true })` returns without an error while
leaving the directory behind. `runFreeShard` appends the same two keys to
every shard child's `GIT_CONFIG_*` pairs as a backstop, so it covers fixtures
that inherit the environment, and `bun run scripts/ship-measure.ts free
--backend local` measures with that environment; a fixture with its own
replacement env never sees the backstop.
To reproduce a ledger entry, take the shard's exact file list from the CI run's
`free-plan` artifact, then rerun it with the flaky retry off on a CI-mirrored VM:
```bash
gh run download <run-id> -R garrytan/gstack -n free-plan -D /tmp/free-plan
files=$(jq -r --argjson s <shard> '.shards[] | select(.shard == $s) | .files | join(",")' /tmp/free-plan/free-plan.json)
bun run test:ubicloud --diagnostic --files "$files" --reruns 30
```
The ledger entry names the shard (`flake-ledger-<shard>`). Read the counts from
the run's `free-measurement.json` (`completed`, `passed`, `capHit`): with a
before-fix failure rate p, a fix needs at least 3/p clean completed reruns of
the same command, so zero failures would have under a 5% chance if nothing had
changed. Cut or unstarted reruns are incomplete, not passes.
### Free suite duration seed
`scripts/free-test-durations.json` drives shard packing; files missing from it
pack at the seed's 99th percentile. Acceptable seeds come from
pack at the seed's 99th percentile. Acceptable seeds come only from
`bun run test:ubicloud --record-durations` (each file timed in its own child on a
VM with the CI lane's environment) or a CI recording. A 4-core laptop or
small-sandbox recording is not acceptable: browser and display tests skip or slow
down there, so the seed misstates them.
VM with the CI lane's environment; it needs `UBICLOUD_API_KEY` and copies the
refreshed seed back into the checkout). A 4-core laptop or small-sandbox
recording is not acceptable: browser and display tests skip or slow down there,
so the seed misstates them. Contributors without `UBICLOUD_API_KEY` leave the
refresh to a maintainer.
- `--ci-plan` warns in the job summary, with the refresh command, when more than 5
files are unseeded; it never fails a PR. `--ci-verify` lists shards that ran
+31
View File
@@ -274,3 +274,34 @@ The 4 remaining shouting pins are enum or marker tokens (`PLAN MODE EXCEPTION
ALWAYS RUN`, `[CRITICAL]`, `ALWAYS-ON` CLI output), not emphasis. Most remaining
sentence pins sit in QA caller, detector-helper and workflow files that other
work in flight owns.
## Post-merge follow-up (2026-10-15)
The "post-merge audit metrics" TODO, from a no-spend run of
`bun run test:health --since-days 7` (window 2026-10-08T22:14Z to
2026-10-15T22:14Z). The 2026-10-12 figures are the weekly health run in issue
#3154 (run 38234556073), kept as dated history.
| Metric | 2026-10-12 | 2026-10-15 | Target | Verdict |
|---|---:|---:|---|---|
| PR full-fallback rate | 45.0% (58/129 runs) | 45.9% (39/85 runs) | none numeric; the replay above names the cause | open: branches touching `bin/`, `lib/`, `hosts/` or `make-pdf/` still select every case |
| Recorded PR-lane spend per merged PR | $75.09 (46 merged) | $73.52 (33 merged) | down at least 50% from the pre-change baseline | unavailable: baseline not recorded (below) |
| Cancelled slice-minutes per week | 1,939 (44 runs) | 1,545 (33 runs) | none numeric | reported |
| PR push-to-verdict | p50 12.0 min, p95 1,160.7 min (122 runs) | p50 11.4 min, p95 107.9 min (80 runs) | 3–5 minutes for typical PR checks (CONTRIBUTING.md) | missed |
| Free-suite flaky-pass rate | 13.0% (6/46 main runs) | 6.1% (2/33 main runs; `terminal-agent-publication-lock` 2/33, over the 5% per-file check) | under 5% of main runs | missed; fix wave 2 repairs the five ledger files |
**Spend baseline.** The pre-change window was recomputed with the exported
`collectHealth` at `now = 2026-10-04T00:00Z`, seven days, through a client that
drops runs created and PRs merged after that instant (`listWorkflowRuns` has no
upper bound of its own). The window holds 12 merged PRs but no
`trial-outcomes-pr-*` artifacts and no paid-plan manifest with `prCoverage`:
the recorder that feeds this metric shipped with the audit, so the PR lanes
before it wrote nothing it can read (their other artifacts are retained until
2026-11-02 and later). Baseline not recorded; verdict unavailable. The P1 in
TODOS.md is opened on the absolute figures.
**Census reliability (not yet due).** Due after the fourth scheduled census
after the audit merge (2026-10-26). Interim: periodic 2.00 expected failed
trials per run (6/538 over 3 runs; target at most 1.0), gate census 0.33 (1/308
over 3 runs; target at most 0.5). The TODO keeps its due date; the census
repair stays with #3153 and #3200.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.102",
"version": "1.91.106",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -1,6 +1,9 @@
{
"entries": [
{"file": "test/host-conformance.test.ts", "reason": "real ./setup per host and layout; the time is setup's own skill-linking loop, not fixture copies (shared fixture landed in W6c)"},
{"file": "test/shared-libs-stage-actor.test.ts", "reason": "re-runs the paid shared-libs runner body against git fixture repos per test; owned by the paid-harness value-bar review"}
{"file": "test/shared-libs-stage-actor.test.ts", "reason": "re-runs the paid shared-libs runner body against git fixture repos per test; owned by the paid-harness value-bar review"},
{"file": "test/setup-install-registry.test.ts", "reason": "82s in the 2026-10-15 recording (58s on 2026-10-04): 11 cases each run the real ./setup, gstack-relink, gstack-config and gstack-uninstall against throwaway checkouts", "todo": "split by describe block so the packer can balance it, then rerun bun run test:ubicloud --record-durations (TODOS.md, fix wave 2 follow-ups)"},
{"file": "test/host-renders-prune.test.ts", "reason": "64s in the 2026-10-15 recording (unseeded before): every case runs a real ./setup or the gstack-upgrade migration over host render dirs", "todo": "split by describe block so the packer can balance it, then rerun bun run test:ubicloud --record-durations (TODOS.md, fix wave 2 follow-ups)"},
{"file": "test/setup-codex-scope.test.ts", "reason": "60.4s in the 2026-10-15 recording (48s on 2026-10-04): Codex install scope through the real ./setup", "todo": "split by describe block so the packer can balance it, then rerun bun run test:ubicloud --record-durations (TODOS.md, fix wave 2 follow-ups)"}
]
}
+19
View File
@@ -1490,6 +1490,24 @@ function logFreeRecovery(log: (line: string) => void, outcome: FreeShardOutcome,
}
}
/**
* Backstop for fixtures that commit and then list, watch or remove `.git`:
* git's detached auto-maintenance child creates and deletes
* `.git/objects/maintenance.lock` after `git commit` has returned. Appends
* `maintenance.auto=false` and `gc.auto=0` after any inherited GIT_CONFIG_*
* pairs; a malformed inherited count is left untouched so git still rejects it.
*/
export function disableGitAutoMaintenance(env: NodeJS.ProcessEnv): void {
const inherited = (env.GIT_CONFIG_COUNT ?? '').trim();
if (inherited !== '' && !/^\d+$/.test(inherited)) return;
const count = inherited === '' ? 0 : Number(inherited);
env[`GIT_CONFIG_KEY_${count}`] = 'maintenance.auto';
env[`GIT_CONFIG_VALUE_${count}`] = 'false';
env[`GIT_CONFIG_KEY_${count + 1}`] = 'gc.auto';
env[`GIT_CONFIG_VALUE_${count + 1}`] = '0';
env.GIT_CONFIG_COUNT = String(count + 2);
}
/** One line per shard, printed after the run: `[test:free] shard i/N: M files, XXs, pass|fail|timed-out`. */
function shardEpilogue(outcome: FreeShardOutcome, totalShards: number): string {
return `[test:free] shard ${outcome.shard}/${totalShards}: ${outcome.files.length} files, `
@@ -1559,6 +1577,7 @@ export async function runFreeShard(
// shards and prior daemons replace each other's state; override inherited state.
env.BROWSE_STATE_FILE = path.join(stateDir, '.gstack', 'browse.json');
env.GSTACK_FREE_SHARD_ID = randomUUID();
disableGitAutoMaintenance(env);
const home = options.homeGuard ? options.homeGuard(files, env, stateDir) : guardFreeHome(files, env);
const startedAt = Date.now();
+8 -7
View File
@@ -138,7 +138,7 @@ export function flakeCheck(window: FlakeWindow | Error): Check {
export function unseededCheck(root: string): Check {
const base = { id: 'unseeded-free-files', title: 'Free test files missing from the duration seed (CEO-10)',
fix: 'Refresh the seed: `bun run test:ubicloud --record-durations` (or a CI recording; a 4-core laptop recording is not acceptable), then commit scripts/free-test-durations.json (docs/TESTING_INTERNALS.md#free-suite-duration-seed).' };
fix: 'Refresh the seed with UBICLOUD_API_KEY set: `bun run test:ubicloud --record-durations` times every free file on a CI-mirrored VM and copies scripts/free-test-durations.json back into this checkout; commit it (docs/TESTING_INTERNALS.md#free-suite-duration-seed). Without UBICLOUD_API_KEY, leave the refresh to a maintainer: a 4-core laptop or sandbox recording skips or slows browser and display tests, so it is not accepted.' };
const durations = loadFreeTestDurations(root);
if (!durations) return { ...base, status: 'unavailable', detail: 'unavailable: scripts/free-test-durations.json is missing or unreadable; next: refresh the seed' };
const unseeded = unseededFreeFiles(collectFreeTestFiles(root), durations);
@@ -243,12 +243,13 @@ export function collectHealth(inputs: HealthInputs): HealthReport {
detail: window.files.slice(0, 15).map(file => `${file.file}: ${file.flakyRuns}/${window.runs} runs (${pct(file.rate)})${file.failing ? ' FAIL > 5%' : ''}`) };
}));
metrics.push(metric('free-wall', 'Free-suite and Windows wall', 'p50 of run_started_at to updated_at for successful main runs of free-tests.yml and windows-free-tests.yml', () => {
const wall = (workflow: string) => {
const walls = listWorkflowRuns(client, repo, workflow, { since, branch: 'main' }).filter(run => run.conclusion === 'success').map(run => span(run.startedAt, run.updatedAt)!);
return `${minutes(percentile(walls, 0.5))} (${walls.length} runs)`;
};
return { value: `free-tests ${wall('free-tests.yml')}; windows-free-tests ${wall('windows-free-tests.yml')}` };
metrics.push(metric('free-wall', 'Free-suite and Windows wall', 'p50 of run_started_at to updated_at for successful main runs of free-tests.yml and successful pull_request runs of windows-free-tests.yml (its workflow_dispatch duration recordings excluded)', () => {
const walls = (workflow: string, filter: { branch?: string; event?: string }) =>
listWorkflowRuns(client, repo, workflow, { since, ...filter }).filter(run => run.conclusion === 'success').map(run => span(run.startedAt, run.updatedAt)!);
const free = walls('free-tests.yml', { branch: 'main' });
const windows = walls('windows-free-tests.yml', { event: 'pull_request' });
const windowsValue = windows.length ? `${minutes(percentile(windows, 0.5))} (${windows.length} runs)` : 'no successful PR runs in window';
return { value: `free-tests ${minutes(percentile(free, 0.5))} (${free.length} runs); windows-free-tests (successful PR runs) ${windowsValue}` };
}));
metrics.push(metric('free-serial', 'Free-suite serial time', 'sum of scripts/free-test-durations.json (the committed seed)', () => {
+1
View File
@@ -95,6 +95,7 @@ describe('seed ceiling and shrink-only allowlist', () => {
});
test('the committed seed holds the ceiling and the allowlist only shrinks', () => {
expect(SEED_ALLOWLIST_FILE).toBe('scripts/free-test-seed-allowlist.json');
const seed = JSON.parse(fs.readFileSync(path.join(ROOT, FREE_TEST_DURATIONS_FILE), 'utf8')) as { durations: Record<string, number> };
const allowlist = parseSeedAllowlist(fs.readFileSync(path.join(ROOT, SEED_ALLOWLIST_FILE), 'utf8'));
const explicitBase = process.env.GSTACK_FREE_SEED_BASE;
+5 -2
View File
@@ -21,6 +21,7 @@ function repo() {
fs.mkdirSync(path.join(dir, 'handbook'), { recursive: true });
git(root, 'init', '-q', '-b', 'main', dir);
git(dir, 'config', 'user.email', 't@t'); git(dir, 'config', 'user.name', 't'); git(dir, 'config', 'commit.gpgsign', 'false');
git(dir, 'config', 'maintenance.auto', 'false'); git(dir, 'config', 'gc.auto', '0');
fs.writeFileSync(path.join(dir, 'app.ts'), 'export const a = 1;\n');
fs.writeFileSync(path.join(dir, 'handbook', 'guide.md'), '# Guide\n');
git(dir, 'add', '.'); git(dir, 'commit', '-qm', 'base');
@@ -38,7 +39,7 @@ describe('gstack-docs-candidate', () => {
test('snapshot records base, HEAD, index, NUL-safe path lists and content hashes without touching the repository', () => {
const { dir, record } = repo();
const gitDir = path.join(dir, '.git');
const before = fs.readdirSync(gitDir, { recursive: true }).length;
const before = fs.readdirSync(gitDir, { recursive: true }) as string[];
const indexBefore = fs.readFileSync(path.join(gitDir, 'index'));
const result = run(dir, 'snapshot', '--out', record, '--audit-id', 'a1', '--mode', 'edit', '--base', 'main', '--docs', 'handbook');
expect(result.status, result.stderr).toBe(0);
@@ -51,7 +52,9 @@ describe('gstack-docs-candidate', () => {
expect(saved.hashes['app.ts']).toBe(git(dir, 'hash-object', 'app.ts'));
expect(saved.index).toHaveLength(2);
expect(fs.statSync(record).mode & 0o777).toBe(0o600);
expect(fs.readdirSync(gitDir, { recursive: true }).length).toBe(before);
const after = fs.readdirSync(gitDir, { recursive: true }) as string[];
const changed = [...before.filter(entry => !after.includes(entry)).map(entry => `-${entry}`), ...after.filter(entry => !before.includes(entry)).map(entry => `+${entry}`)];
expect(after.length, `.git entries changed during snapshot: ${changed.join(', ') || '(same names)'}`).toBe(before.length);
expect(fs.readFileSync(path.join(gitDir, 'index')).equals(indexBefore)).toBe(true);
});
+83 -28
View File
@@ -932,35 +932,90 @@ describe("width pinned on failed base read (3-digit repos)", () => {
describe("integration (smoke)", () => {
const SMOKE_SCRIPT = join(import.meta.dir, "..", "bin", "gstack-next-version");
// Bumps timeout to 30s — the test spawns a real `bun run` subprocess that
// does a `gh pr list` against the live GitHub API to inspect claimed slots.
// Network latency makes 5s tight on developer machines.
const FAKE_OWNER = "fixture-owner";
const CLAIM = { pr: 41, branch: "fixture/claim", version: "1.6.4.0", url: "https://github.com/fixture-owner/repo/pull/41" };
// A named fake `gh` first on PATH answers every call the CLI makes in this
// checkout and logs each argv; anything else exits 2 so a fixture gap cannot
// silently change the path. The live GitHub API is never called.
function fakeGh(dir: string): { calls: () => string[][]; glabCalls: () => string } {
const log = join(dir, "gh-calls.log");
const glabLog = join(dir, "glab-calls.log");
const prList = JSON.stringify([{ number: CLAIM.pr, headRefName: CLAIM.branch, headRepositoryOwner: { login: FAKE_OWNER }, url: CLAIM.url, isDraft: false }]);
const content = Buffer.from(`${CLAIM.version}\n`).toString("base64");
writeFileSync(join(dir, "gh"), [
"#!/bin/sh",
`printf '%s\\037' "$@" >> '${log}'; printf '\\n' >> '${log}'`,
'case "$*" in',
' "auth status") exit 0 ;;',
' "pr view --json number -q .number") exit 1 ;;',
` "pr list --state open --base main --limit 200 --json number,headRefName,headRepositoryOwner,url,isDraft") printf '%s\\n' '${prList}'; exit 0 ;;`,
` "repo view --json owner -q .owner.login") echo '${FAKE_OWNER}'; exit 0 ;;`,
` "api repos/{owner}/{repo}/contents/VERSION?ref=${encodeURIComponent(CLAIM.branch)} -q .content") echo '${content}'; exit 0 ;;`,
"esac",
`echo "FAKE_GH_UNHANDLED $*" >> '${log}'`,
'echo "FAKE_GH_UNHANDLED $*" >&2',
"exit 2",
"",
].join("\n"), { mode: 0o755 });
writeFileSync(join(dir, "glab"), `#!/bin/sh\necho "$*" >> '${glabLog}'\nexit 1\n`, { mode: 0o755 });
return {
calls: () => readFileSync(log, "utf8").trimEnd().split("\n").map((line) => line.split("\x1f").filter(Boolean)),
glabCalls: () => { try { return readFileSync(glabLog, "utf8"); } catch { return ""; } },
};
}
test("CLI runs against real repo and emits parseable JSON", async () => {
const proc = Bun.spawnSync([
"bun",
"run",
"./bin/gstack-next-version",
"--base",
"main",
"--bump",
"patch",
"--current-version",
"1.6.3.0",
"--workspace-root",
"null", // skip sibling scan in CI
], { timeout: 30_000 });
const out = new TextDecoder().decode(proc.stdout);
const parsed = JSON.parse(out);
expect(parsed).toHaveProperty("version");
expect(parseVersion(parsed.version)).not.toBeNull();
expect(parsed).toHaveProperty("bump", "patch");
expect(parsed).toHaveProperty("host");
expect(["github", "gitlab", "unknown"]).toContain(parsed.host);
expect(parsed).toHaveProperty("claimed");
expect(Array.isArray(parsed.claimed)).toBe(true);
expect(parsed).toHaveProperty("siblings");
expect(parsed.siblings).toEqual([]); // --workspace-root null disabled scanning
expect(parsed).toHaveProperty("version_path", "VERSION"); // default when no config + no flag
const stubDir = mkdtempSync(join(tmpdir(), "nextver-smoke-gh-"));
try {
const gh = fakeGh(stubDir);
const proc = Bun.spawnSync([
"bun",
"run",
"./bin/gstack-next-version",
"--base",
"main",
"--bump",
"patch",
"--current-version",
"1.6.3.0",
"--workspace-root",
"null", // skip sibling scan in CI
], { timeout: 30_000, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH ?? ""}` } });
const stderr = new TextDecoder().decode(proc.stderr);
expect(proc.exitCode, stderr).toBe(0);
const out = new TextDecoder().decode(proc.stdout);
const parsed = JSON.parse(out);
expect(parsed).toHaveProperty("version");
expect(parseVersion(parsed.version)).not.toBeNull();
expect(parsed).toHaveProperty("bump", "patch");
expect(parsed).toHaveProperty("host");
expect(["github", "gitlab", "unknown"]).toContain(parsed.host);
expect(parsed).toHaveProperty("claimed");
expect(Array.isArray(parsed.claimed)).toBe(true);
expect(parsed).toHaveProperty("siblings");
expect(parsed.siblings).toEqual([]); // --workspace-root null disabled scanning
expect(parsed).toHaveProperty("version_path", "VERSION"); // default when no config + no flag
const remote = execFileSync("git", ["remote", "get-url", "origin"], { cwd: join(import.meta.dir, ".."), encoding: "utf8", timeout: 10_000 }).trim();
expect(gh.calls()).toEqual([
...(remote.includes("github.com") ? [] : [["auth", "status"]]),
["pr", "view", "--json", "number", "-q", ".number"],
["pr", "list", "--state", "open", "--base", "main", "--limit", "200", "--json", "number,headRefName,headRepositoryOwner,url,isDraft"],
["repo", "view", "--json", "owner", "-q", ".owner.login"],
["api", `repos/{owner}/{repo}/contents/VERSION?ref=${encodeURIComponent(CLAIM.branch)}`, "-q", ".content"],
]);
expect(gh.calls().flat().join(" ")).not.toContain("FAKE_GH_UNHANDLED");
expect(stderr).not.toContain("FAKE_GH_UNHANDLED");
expect(gh.glabCalls()).toBe("");
expect(parsed.host).toBe("github");
expect(parsed.offline).toBe(false);
expect(parsed.fallback).toBeNull();
expect(parsed.warnings).toEqual([]);
expect(parsed.claimed).toEqual([CLAIM]); // the canned PR's owner matches the canned repo owner
expect(parsed.version).toBe("1.6.5.0"); // the canned claim on 1.6.4.0 is skipped
} finally {
rmSync(stubDir, { recursive: true, force: true });
}
}, 30_000); // Headroom over the 4-5s wall time of the spawned process under load
test("CLI runs with --version-path and surfaces it in JSON output", async () => {
+10 -3
View File
@@ -18,6 +18,7 @@ async function exercise(mode: 'success' | 'max-turns' | 'first-timeout' | 'secon
const scratch = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'legacy-design-free-')));
const home = path.join(scratch, 'home'); fs.mkdirSync(home);
const env = { PATH: process.env.PATH ?? '', HOME: home, GIT_CONFIG_NOSYSTEM: '1',
GIT_CONFIG_COUNT: '2', GIT_CONFIG_KEY_0: 'maintenance.auto', GIT_CONFIG_VALUE_0: 'false', GIT_CONFIG_KEY_1: 'gc.auto', GIT_CONFIG_VALUE_1: '0',
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) };
const callbacks: Array<{ name: string; fn: () => Promise<void>; timeout: number }> = [];
@@ -25,6 +26,8 @@ async function exercise(mode: 'success' | 'max-turns' | 'first-timeout' | 'secon
const recordBudgets: number[] = [];
let launched = false, sdkSignal: AbortSignal | undefined;
let inputVerified = false, saved = '', resultError: unknown;
const removals: string[] = [];
const survivors = () => `surviving fixture dirs: ${dirs.filter(dir => fs.existsSync(dir)).join(', ') || '(none)'}; removals: ${removals.join('; ') || '(none)'}`;
const owned = (p: string) => {
const full = path.resolve(p);
expect(full.startsWith(scratch + path.sep)).toBe(true);
@@ -43,13 +46,17 @@ async function exercise(mode: 'success' | 'max-turns' | 'first-timeout' | 'secon
},
copyFileSync(a: string, b: string) { owned(b); fs.copyFileSync(a, b); },
cpSync(a: string, b: string, opts: any) { owned(b); fs.cpSync(a, b, opts); },
rmSync(p: string, opts: any) { owned(p); fs.rmSync(p, opts); },
rmSync(p: string, opts: any) {
owned(p);
try { fs.rmSync(p, opts); } catch (error) { removals.push(`${p}: ${error}`); throw error; }
if (fs.existsSync(p)) removals.push(`${p}: rmSync returned but the path survived`);
},
};
const args = {
ROOT, fs: localFs, path, os: { tmpdir: () => scratch }, expect,
CAPTURE_MS, CAPTURE_LONG_MS, OFFICE_HOURS_BUN_GRACE_MS, resolveEvalModel, runId: 'free-legacy-design',
evalCollector: { addTest(entry: any) {
expect(dirs.every(dir => !fs.existsSync(dir))).toBe(true);
expect(dirs.every(dir => !fs.existsSync(dir)), survivors()).toBe(true);
rows.push(entry);
} },
runRecordedOfficeHoursAttempt: (options: Parameters<typeof runRecordedOfficeHoursAttempt>[0]) => {
@@ -134,7 +141,7 @@ async function exercise(mode: 'success' | 'max-turns' | 'first-timeout' | 'secon
expect(callbacks).toHaveLength(1); expect(callbacks[0]!.timeout).toBe(CAPTURE_LONG_MS);
try { await callbacks[0]!.fn(); } catch (error) { resultError = error; }
expect(inputVerified).toBe(true); expect(dirs).toHaveLength(1);
expect(dirs.every(dir => !fs.existsSync(dir))).toBe(true);
expect(dirs.every(dir => !fs.existsSync(dir)), survivors()).toBe(true);
expect(recordBudgets).toEqual([CAPTURE_LONG_MS - OFFICE_HOURS_BUN_GRACE_MS]);
expect(sdkSignal?.aborted).toBe(true);
return { resultError, rows, saved };
@@ -0,0 +1,106 @@
/**
* The free runner's git auto-maintenance backstop: every shard child gets
* maintenance.auto=false and gc.auto=0 appended to its inherited GIT_CONFIG_*
* pairs, so a fixture commit cannot leave a detached `git maintenance run`
* creating and deleting .git/objects/maintenance.lock under a later listing,
* watch or removal. Fixtures that build their own git env set the keys
* themselves (docs/TESTING_INTERNALS.md#flake-ledger).
*/
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawnSync } from 'node:child_process';
import { disableGitAutoMaintenance, runFreeShard } from '../scripts/test-free-shards';
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
const scratch = () => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'free-git-maintenance-')));
roots.push(root);
return root;
};
const gitConfigEnv = (env: NodeJS.ProcessEnv) => Object.fromEntries(Object.entries(env).filter(([key]) => key.startsWith('GIT_CONFIG_')).sort());
const cleanGitEnv = (home: string): NodeJS.ProcessEnv => ({
PATH: process.env.PATH ?? '', HOME: home, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: path.join(home, 'no-global-gitconfig'),
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
});
/** The GIT_CONFIG_* entries the real runFreeShard child received. */
async function childGitConfig(env: NodeJS.ProcessEnv | undefined): Promise<NodeJS.ProcessEnv> {
const out = path.join(scratch(), 'child-env.json');
const probe = `require('fs').writeFileSync(${JSON.stringify(out)}, JSON.stringify(Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith('GIT_CONFIG_')))));`
+ `console.log('Ran 1 tests across 1 files. [1.00ms]');`;
const outcome = await runFreeShard(['probe'], 1, 1, { env, quiet: true, log: () => {},
commandFor: () => ({ command: process.execPath, args: ['-e', probe] }) });
expect(outcome.status).toBe('passed');
return gitConfigEnv(JSON.parse(fs.readFileSync(out, 'utf8')));
}
const OFF = { maintenance: ['maintenance.auto', 'false'], gc: ['gc.auto', '0'] } as const;
const appended = (start: number) => ({
[`GIT_CONFIG_KEY_${start}`]: OFF.maintenance[0], [`GIT_CONFIG_VALUE_${start}`]: OFF.maintenance[1],
[`GIT_CONFIG_KEY_${start + 1}`]: OFF.gc[0], [`GIT_CONFIG_VALUE_${start + 1}`]: OFF.gc[1],
});
describe('free runner: git auto-maintenance off for every shard child', () => {
test('absent and zero inherited GIT_CONFIG_COUNT both start the pairs at index 0', async () => {
const home = scratch();
expect(await childGitConfig(cleanGitEnv(home))).toEqual(gitConfigEnv({ ...cleanGitEnv(home), GIT_CONFIG_COUNT: '2', ...appended(0) }));
expect(await childGitConfig({ ...cleanGitEnv(home), GIT_CONFIG_COUNT: '0' }))
.toEqual(gitConfigEnv({ ...cleanGitEnv(home), GIT_CONFIG_COUNT: '2', ...appended(0) }));
});
test('a nonzero inherited count keeps its pairs and appends after them', async () => {
const env = { ...cleanGitEnv(scratch()), GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'user.name', GIT_CONFIG_VALUE_0: 'Fixture' };
expect(await childGitConfig(env)).toEqual(gitConfigEnv({ ...env, GIT_CONFIG_COUNT: '3', ...appended(1) }));
});
test('an explicit options.env and the default process.env are both covered, and neither caller object changes', async () => {
const env = { ...cleanGitEnv(scratch()), GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'core.autocrlf', GIT_CONFIG_VALUE_0: 'false' };
const envBefore = structuredClone(env);
const processBefore = gitConfigEnv(process.env);
await childGitConfig(env);
expect(env).toEqual(envBefore);
const inherited = Number(process.env.GIT_CONFIG_COUNT || '0');
expect(await childGitConfig(undefined)).toEqual({ ...processBefore, GIT_CONFIG_COUNT: String(inherited + 2), ...appended(inherited) });
expect(gitConfigEnv(process.env)).toEqual(processBefore);
});
test('a malformed inherited count is passed through untouched and git still rejects it', async () => {
const env = { ...cleanGitEnv(scratch()), GIT_CONFIG_COUNT: 'abc', GIT_CONFIG_KEY_0: 'user.name', GIT_CONFIG_VALUE_0: 'Fixture' };
const child = await childGitConfig(env);
expect(child).toEqual(gitConfigEnv(env));
const result = spawnSync('git', ['config', '--get', 'user.name'], { env: { ...env, ...child }, encoding: 'utf8', timeout: 10_000 });
expect(result.status, result.stderr).not.toBe(0);
expect(result.stderr).toContain('GIT_CONFIG_COUNT');
});
test('a traced commit in the runner child env starts no git maintenance run; the default env does', async () => {
const root = scratch();
const base = cleanGitEnv(root);
const traced = (env: NodeJS.ProcessEnv, name: string) => {
const repo = path.join(root, name);
const git = (...args: string[]) => {
const result = spawnSync('git', args, { cwd: repo, env: { ...env, GIT_TRACE: '1' }, encoding: 'utf8', timeout: 10_000 });
expect(result.status, result.stderr).toBe(0);
return result.stderr;
};
fs.mkdirSync(repo);
git('init', '-q', '-b', 'main');
git('config', 'user.email', 't@t'); git('config', 'user.name', 't'); git('config', 'commit.gpgsign', 'false');
fs.writeFileSync(path.join(repo, 'a.txt'), 'a\n');
git('add', 'a.txt');
return git('commit', '-qm', 'traced').split('\n').filter(line => /maintenance run|gc --auto/.test(line));
};
expect(traced(base, 'defaults').length, 'control: the default env must show the auto-maintenance child').toBeGreaterThan(0);
const runnerEnv = { ...base, ...(await childGitConfig(base)) };
expect(traced(runnerEnv, 'runner')).toEqual([]);
});
test('disableGitAutoMaintenance mutates only the env it is given', () => {
const env: NodeJS.ProcessEnv = { GIT_CONFIG_COUNT: ' 2 ', GIT_CONFIG_KEY_0: 'a.b', GIT_CONFIG_VALUE_0: '1', GIT_CONFIG_KEY_1: 'c.d', GIT_CONFIG_VALUE_1: '2' };
disableGitAutoMaintenance(env);
expect(env).toEqual({ GIT_CONFIG_KEY_0: 'a.b', GIT_CONFIG_VALUE_0: '1', GIT_CONFIG_KEY_1: 'c.d', GIT_CONFIG_VALUE_1: '2', GIT_CONFIG_COUNT: '4', ...appended(2) });
});
});
+20 -3
View File
@@ -149,6 +149,10 @@ describe('enforced checks and the tracking issue (DX-9)', () => {
const failing = unseededCheck(freeRoot(2, 6));
expect(failing).toMatchObject({ status: 'fail', detail: expect.stringContaining('stale seed: 6 unseeded file(s) > 5') });
expect(failing.fix).toContain('bun run test:ubicloud --record-durations');
expect(failing.fix).toContain('UBICLOUD_API_KEY');
expect(failing.fix).toContain('copies scripts/free-test-durations.json back into this checkout');
expect(failing.fix).toContain('leave the refresh to a maintainer');
expect(failing.fix).not.toContain('CI recording');
expect(unseededCheck(freeRoot(2, 5))).toMatchObject({ status: 'pass', detail: expect.stringContaining('5 unseeded file(s) (limit 5)') });
});
@@ -162,7 +166,8 @@ describe('enforced checks and the tracking issue (DX-9)', () => {
});
describe('collectHealth over recorded fixture data', () => {
function fixture() {
function fixture(windowsRuns: unknown[] = [run(70, { event: 'pull_request', head_branch: 'feature' }),
run(71, { event: 'workflow_dispatch', run_started_at: day(2), updated_at: day(1) })]) {
const prTrials = formatTrialOutcomes([trialRecord('gate-a', 'passed', { tier: 'gate', lane: 'gate/pr', cost_usd: 2.5 }),
trialRecord('gate-b', 'passed', { tier: 'gate', lane: 'gate/pr', execution: 'reused', cost_usd: 0 })]);
const census = formatTrialOutcomes([
@@ -184,7 +189,7 @@ describe('collectHealth over recorded fixture data', () => {
[/workflows\/free-tests\.yml\/runs/, () => ({ workflow_runs: [run(60), run(61, { conclusion: 'failure' })] })],
[/runs\/60\/artifacts/, () => ({ artifacts: [{ id: 61, name: 'flake-ledger-2', size_in_bytes: 10 }] })],
[/runs\/61\/artifacts/, () => ({ artifacts: [] })],
[/workflows\/windows-free-tests\.yml\/runs/, () => ({ workflow_runs: [run(70)] })],
[/workflows\/windows-free-tests\.yml\/runs/, () => ({ workflow_runs: windowsRuns })],
], {
11: { 'manifest.json': JSON.stringify({ prCoverage: { mode: 'full-fallback' } }) },
12: { 'trial-outcomes.jsonl': prTrials },
@@ -207,7 +212,7 @@ describe('collectHealth over recorded fixture data', () => {
expect(value('census-redispatch').value).toBe('0 of 1 census(es) re-dispatched; longest paid slice job 20.0 min');
expect(value('quarantine-evidence').value).toContain('insufficient evidence (not the same as healthy)');
expect(value('free-flaky-pass-rate').value).toStartWith('50.0% (1/2 main runs');
expect(value('free-wall').value).toBe('free-tests 0.0 min (1 runs); windows-free-tests 0.0 min (1 runs)');
expect(value('free-wall').value).toBe('free-tests 0.0 min (1 runs); windows-free-tests (successful PR runs) 0.0 min (1 runs)');
expect(value('free-serial').value).toBe('3 s over 3 files');
for (const entry of report.metrics.filter(entry => entry.id !== 'net-complexity')) {
expect(entry.rule.length).toBeGreaterThan(10);
@@ -218,6 +223,18 @@ describe('collectHealth over recorded fixture data', () => {
expect(formatHealth(report)).toContain('rule: share of evals.yml pull_request runs');
});
test('the Windows wall counts only successful pull_request runs and says so when there are none', () => {
const wall = (windowsRuns: unknown[]) => collectHealth({ client: fixture(windowsRuns), repo: 'o/r', now: NOW, sinceDays: 7, cacheDir: tmp('health-cache-'), root: freeRoot(3, 0) })
.metrics.find(entry => entry.id === 'free-wall')!;
const pr = (id: number, minutes: number, extra: Record<string, unknown> = {}) => run(id, { event: 'pull_request', head_branch: `pr-${id}`,
run_started_at: day(1), updated_at: new Date(Date.parse(day(1)) + minutes * 60_000).toISOString(), ...extra });
expect(wall([pr(70, 6), pr(71, 7), pr(74, 8), pr(72, 30, { conclusion: 'failure' }), run(73, { event: 'workflow_dispatch', run_started_at: day(3), updated_at: day(1) })]).value)
.toBe('free-tests 0.0 min (1 runs); windows-free-tests (successful PR runs) 7.0 min (3 runs)');
expect(wall([run(73, { event: 'workflow_dispatch', run_started_at: day(3), updated_at: day(1) })]).value)
.toBe('free-tests 0.0 min (1 runs); windows-free-tests (successful PR runs) no successful PR runs in window');
expect(wall([]).rule).toContain('workflow_dispatch duration recordings excluded');
});
test('a rate-limited API degrades every remote metric to a named unavailable reason, never 0, and keeps the issue open', () => {
const client = fakeClient([[/./, () => { throw new Error('gh api failed: HTTP 403: API rate limit exceeded for installation ID 1'); }]]);
const report = collectHealth({ client, repo: 'o/r', now: NOW, sinceDays: 7, cacheDir: tmp('health-cache-'), root: freeRoot(3, 0) });
+60
View File
@@ -222,4 +222,64 @@ describe('gstack-timeline-read', () => {
});
}
}
// --since/--branch used to read an unbound $2 (exit 1), and an unparseable
// --since silently returned unfiltered history.
const badFilters: Array<[string, string[], string]> = [
['--since missing value', ['--since'], '--since must be a relative time like "7 days ago" (got no value)'],
['--since empty value', ['--since', ''], '--since must be a relative time like "7 days ago" (got an empty value)'],
['--since followed by a flag', ['--since', '--limit', '5'], "(got flag '--limit')"],
['--since yesterday', ['--since', 'yesterday'], "--since must be a relative time like \"7 days ago\" (got 'yesterday')"],
['--since 2 weeks', ['--since', '2 weeks'], "(got '2 weeks')"],
['--since 7days', ['--since', '7days'], "(got '7days')"],
['--since impossible month', ['--since', '2026-13-01'], "(got '2026-13-01')"],
['--since unanchored', ['--since', 'about 7 days ago, roughly'], "(got 'about 7 days ago, roughly')"],
['--branch missing value', ['--branch'], '--branch must be a branch name (got no value)'],
['--branch empty value', ['--branch', ''], '--branch must be a branch name (got an empty value)'],
['--branch followed by a flag', ['--branch', '--limit', '5'], "--branch must be a branch name (got flag '--limit')"],
['unknown flag', ['--bogus'], "unknown argument '--bogus'"],
['stray argument', ['stray'], "unknown argument 'stray'"],
];
for (const history of ['empty', 'populated'] as const) {
for (const [label, args, message] of badFilters) {
test(`${label} exits 2 before any read on ${history} history`, () => {
if (history === 'populated') {
runLog(JSON.stringify({ skill: 'review', event: 'completed', branch: 'main', ts: '2026-03-28T10:00:00Z' }));
}
const result = runReadStatus(args);
expect(result.status, result.stderr).toBe(2);
expect(result.stdout).toBe('');
expect(result.stderr).toContain(message);
if (label.startsWith('--since')) expect(result.stderr).toContain('accepted: "<N> minute|hour|day|week|month[s] ago", "<N>h|d|w", or an ISO date');
if (label === 'unknown flag' || label === 'stray argument') expect(result.stderr).toContain('Usage: gstack-timeline-read');
});
}
test(`--help prints the usage line and exits 0 on ${history} history`, () => {
if (history === 'populated') {
runLog(JSON.stringify({ skill: 'review', event: 'completed', branch: 'main', ts: '2026-03-28T10:00:00Z' }));
}
const result = runReadStatus(['--help']);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe('Usage: gstack-timeline-read [--since "7 days ago"|7d|YYYY-MM-DD] [--limit N] [--branch NAME]');
});
const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10);
for (const since of ['7 days ago', '3 Hours ago', '7d', '24h', '1w', yesterday, `${yesterday}T00:00:00Z`]) {
test(`--since '${since}' keeps recent events and drops old ones on ${history} history`, () => {
if (history === 'populated') {
runLog(JSON.stringify({ skill: 'review', event: 'completed', branch: 'main', ts: '2020-01-01T10:00:00Z' }));
runLog(JSON.stringify({ skill: 'ship', event: 'completed', branch: 'main', ts: new Date(Date.now() - 3_600_000).toISOString() }));
}
const result = runReadStatus(['--since', since]);
expect(result.status, result.stderr).toBe(0);
expect(result.stderr).toBe('');
if (history === 'empty') expect(result.stdout).toBe('');
else {
expect(result.stdout).toContain('/ship completed');
expect(result.stdout).not.toContain('/review completed');
}
});
}
}
});