diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d367fdd..f1c69beba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` 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` 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). diff --git a/test/ubicloud-runner.test.ts b/test/ubicloud-runner.test.ts index 1327c2dd0..73ad33fae 100644 --- a/test/ubicloud-runner.test.ts +++ b/test/ubicloud-runner.test.ts @@ -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 {