test(ubi-runner): prove the quota wait countdown; CHANGELOG entry for the free-tier timing repairs (GSTA-128)

The existing WAIT=3 quota case passes under both the old wall-clock deadline
and the new countdown because its fake usage read is instant. The new case
slows the usage read to 1.2 s and asserts the printed 2 s budget (origin/main
prints 1 s), then stubs sleep for a 70 s wait to see the 30 s steps and the
release when the peer VM disappears. /ship Step 13 adds the [Unreleased]
CHANGELOG entry; the train re-heads it at the slot.

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-27 15:09:55 +00:00
1 parent b811ee2dbb
commit 660bfb1fab
2 files changed
+48 -1

No files matched your search

+15
View File
@@ -1,5 +1,20 @@
# Changelog
## [Unreleased]
**Three free-tier tests measured something other than what they assert, and the Ubicloud quota wait shaved its own budget.** The daily flaky pass (GSTA-128) traced the free tier's open timing reds to measurement defects, not product ones: `quota_preflight` in `scripts/ubicloud/ubi-runner.sh` pinned its deadline to the wall clock before the first usage call, so a slow CLI spawn on a loaded runner printed "waiting up to 2s more" where the test expects the full 3 s; the pregate-remote teardown test read the `.down` file as soon as it existed, before the stub's `echo` had written the line; and the design-lint linearity check timed one wall-clock run of the 8000-label lint against a 500 ms floor on a shared runner. Under eight busy loops on four cores the quota case went from 8 of 12 green to 12 of 12. No ms bound, threshold, trial count or retry changed.
### Fixed
- **`scripts/ubicloud/ubi-runner.sh`**: the quota wait budget is a countdown of the seconds spent sleeping for room. Usage reads between sleeps do not eat it, the first log line names the whole `UBI_QUOTA_WAIT`, `UBI_QUOTA_WAIT=0` still refuses on the first poll, and the wait still releases when usage drops.
### Tests
- `test/ubicloud-pregate-remote.test.ts` waits for the written teardown line (`exit …` or `term …` ending in a newline), not for the file to exist.
- `test/design-lint-fifth-cycle.test.ts` measures the lint's own CPU time, fastest of three runs, against the unchanged `max(2.5 × t4000, 500 ms)` bound. A quadratic regression at 8000 labels would still measure about 690 ms CPU, above the floor, so the check keeps catching what it was written for.
- `test/ubicloud-runner.test.ts` attaches the runner's stderr to the hung-setup watchdog assertion, so the next red of that case names its cause. A new quota case proves the countdown. A usage read that takes 1.2 s keeps the printed 2 s budget (origin/main prints 1 s), a 70 s wait sleeps 30 s twice, and the wait releases when the peer VM disappears.
### Gate integrity
- Four hunks listed, three read, none a relaxation. The teardown wait is stricter, the watchdog assertion is unchanged with a message added, and the linearity measure keeps its bound (measurement above).
## [1.91.277.0] - 2026-10-27
**office-hours stops carrying its own copy of the shared AskUserQuestion subsections.** After the shared-preamble hoist, office-hours still inlined whole copies of the shared `### When AskUserQuestion is unavailable or a call fails` and `### Format` subsections, because each held one office-hours addition: the `Q<N>` open-question form. A session's first office-hours invocation read those subsections twice, once from the shared file and once from SKILL.md. The `Q<N>` form and its routing sentence now render as office-hours' own `### Open-ended questions` subsection at the end of the AskUserQuestion section, so the shared subsections apply as they are and office-hours' SKILL.md drops 6,328 bytes (its parity union goes from 160,952 to 154,624, under the pre-hoist 1.18 cap).
+33 -1
View File
@@ -346,7 +346,7 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
' *\'"create"\'*) if [ -n ' + JSON.stringify(opts.refuse ?? '') + ' ]; then printf \'%s\' ' + JSON.stringify(opts.refuse ?? '') + ' > "$out"; printf 400; else echo "$loc $name" >> "$listed"; : > "$out"; printf 200; fi ;;\n' +
' *destroy*) : > "$out"; grep -v " $name$" "$listed" > "$listed.tmp"; mv "$listed.tmp" "$listed"; printf 200 ;;\n' +
' *\'"list"\'*) cat "$listed" > "$out"; printf 200 ;;\n' +
' *\'"show"\'*) if grep -q " $name$" "$listed"; then printf \'state: running\\nsize: standard-16\\nip4: 192.0.2.1\\n\' > "$out"; printf 200; else : > "$out"; printf 404; fi ;;\n' +
' *\'"show"\'*) [ -z "${FAKE_SHOW_DELAY:-}" ] || sleep "$FAKE_SHOW_DELAY"; if grep -q " $name$" "$listed"; then printf \'state: running\\nsize: standard-16\\nip4: 192.0.2.1\\n\' > "$out"; printf 200; else : > "$out"; printf 404; fi ;;\n' +
' *) : > "$out"; printf 200 ;;\n' +
'esac\n');
writeFileSync(join(bin, 'ssh'), '#!/usr/bin/env bash\n' +
@@ -570,6 +570,38 @@ describe('ubi-runner run, verify and quota against a fake API and a fake ssh', (
} finally { rmSync(f.root, { recursive: true, force: true }); }
}, 90_000);
// Value: protects=the quota wait budget counts seconds slept, not usage reads: a slow usage read keeps the printed budget, a wait above 30 s counts down in 30 s steps, and the wait releases when room appears; fails_when=the budget goes back to a wall-clock deadline taken before the first usage read (the first log line shrinks to 1s), the step cap or the decrement is lost, or room found mid-wait still refuses; why_new=the WAIT=3 case above passes under both the old deadline and the new countdown because its fake usage read is instant and it never sees a second step; seam=none (the fixture's bin/ is already first on PATH, so sleep and curl are stubbed there); no_claim=real elapsed time of the long wait (the 70 s case stubs sleep)
test('quota wait: a slow usage read does not shave the printed budget; a long wait counts down in 30s steps and releases when room appears', () => {
const slow = fakeVm({ listed: 'eu-central-h1 ubirun-peer-9999999999-cccc\n' });
try {
writeFileSync(join(slow.state, 'quota-max'), '31\n');
const started = Date.now();
const r = run(slow, ['up'], { UBI_QUOTA_WAIT: '2', FAKE_SHOW_DELAY: '1.2' });
const err = r.stderr.toString();
expect(r.exitCode, err).toBe(1);
expect(err).toMatch(/quota: 16 of 31 vCPUs in use and 16 needed; waiting up to 2s more for room/);
expect(err).toContain('create refused before any request: vCPU quota exhausted');
expect(Date.now() - started).toBeGreaterThanOrEqual(2000);
expect(slow.calls()).not.toContain('"create"');
} finally { rmSync(slow.root, { recursive: true, force: true }); }
const f = fakeVm({ listed: 'eu-central-h1 ubirun-peer-9999999999-cccc\n' });
try {
writeFileSync(join(f.state, 'quota-max'), '31\n');
const sleepLog = join(f.root, 'sleep.log');
writeFileSync(join(f.root, 'bin/sleep'), '#!/usr/bin/env bash\necho "$1" >> ' + JSON.stringify(sleepLog) + '\n[ "$(wc -l < ' + JSON.stringify(sleepLog) + ')" -ne 2 ] || : > ' + JSON.stringify(join(f.root, 'listed')) + '\n');
chmodSync(join(f.root, 'bin/sleep'), 0o755);
const r = run(f, ['up'], { UBI_QUOTA_WAIT: '70' });
const err = r.stderr.toString();
expect(r.exitCode, err).toBe(0);
expect(err).toMatch(/waiting up to 70s more for room/);
expect(err).toMatch(/waiting up to 40s more for room/);
expect(err).not.toContain('quota preflight refused');
expect(readFileSync(sleepLog, 'utf8').split('\n').slice(0, 2)).toEqual(['30', '30']);
expect(f.calls()).toContain('"create"');
} finally { rmSync(f.root, { recursive: true, force: true }); }
}, 90_000);
test('reap reports a failed list and a failed destroy as failures; UBI_TTL_HOURS rejects leading zeros and more than three digits', () => {
const f = fakeVm({ listed: 'eu-central-h1 ubirun-other-1000000000-bbbb-t1\n' });
try {