Skip to content

Commit a316587

Browse files
authored
Merge pull request #97 from levelcodeai/fix/release-gate-linux-timing
fix(test): v1.3.0's release gate failed on Linux — a test helper counted event-loop turns
2 parents 214e67d + 0883179 commit a316587

4 files changed

Lines changed: 81 additions & 27 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,9 @@ jobs:
4040
with:
4141
node-version: "24"
4242
- name: Extension unit tests
43-
# `shopt` is a bash builtin, so pin the shell instead of relying on the runner default (bash on
44-
# Linux/macOS, but pwsh on Windows — where this step would break if the job were ever copied).
45-
# Explicit `shell: bash` also upgrades the default `bash -e` to
46-
# `bash --noprofile --norc -eo pipefail`, so a stray profile file can't perturb the gate either.
47-
shell: bash
48-
# DISCOVERS suites — it used to `cd extensions/levelcode-ai`, so levelcode-updater's tests never
49-
# ran here, including the one guarding the updater's Download button against serving a raw
50-
# .app.zip. Globbing every extension means a new suite is gated the moment it is added, with no
51-
# list here to keep in sync. Requires are file-relative, so running from the repo root is fine.
52-
run: |
53-
shopt -s nullglob
54-
count=0
55-
for t in extensions/*/test/*.test.js; do
56-
echo "── $t"
57-
node "$t" # `-e` (from `shell: bash` above) aborts the job on the first failure
58-
count=$((count + 1))
59-
done
60-
# A zero-match glob would otherwise report success and gate nothing — the exact failure this
61-
# step is fixing. Fail loudly instead.
62-
if [ "$count" -eq 0 ]; then
63-
echo "::error::No suites matched extensions/*/test/*.test.js — the gate would pass vacuously."
64-
exit 1
65-
fi
66-
echo "──────── $count test files passed ────────"
43+
# The gate itself lives in a script, so the pull-request workflow (test.yml) runs exactly what
44+
# this does. It discovers every extensions/*/test/*.test.js and fails if it finds none.
45+
run: ./scripts/test-extensions.sh
6746

6847
build:
6948
name: Build ${{ matrix.arch }}

‎.github/workflows/test.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Extension tests
2+
3+
# The release gate (scripts/test-extensions.sh), on every pull request and on develop.
4+
#
5+
# release.yml runs the same script, but only when a tag is pushed — which made a tag the first time
6+
# the suites ran on Linux. v1.3.0's gate failed there on a suite that had only ever run on a Mac,
7+
# after the pull requests that wrote it had merged. Here a suite that passes on one machine and not
8+
# on the runner is found on the pull request that adds it.
9+
10+
on:
11+
pull_request:
12+
push:
13+
branches: [develop]
14+
15+
permissions:
16+
contents: read
17+
18+
# A newer push to the same branch or pull request supersedes the run before it.
19+
concurrency:
20+
group: extension-tests-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
test:
25+
name: Extension unit tests
26+
runs-on: ubuntu-latest # the runner the release gate uses
27+
steps:
28+
- uses: actions/checkout@v7
29+
- uses: actions/setup-node@v7
30+
with:
31+
node-version: "24" # as release.yml
32+
- name: Extension unit tests
33+
run: ./scripts/test-extensions.sh

‎extensions/levelcode-ai/test/authRetryCallers.test.js‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,23 @@ const { registerInlineComplete } = require('../inlineComplete');
210210

211211
// ── the network: the only stand-in ───────────────────────────────────────────────────────────────
212212
const tick = () => new Promise((resolve) => setImmediate(resolve));
213+
/** How long anything here is given to happen. Reached only by what never does. */
214+
const PATIENCE_MS = 2000;
215+
/**
216+
* Wait for `cond` — by the CLOCK, not by counting turns of the event loop. Inline completion waits
217+
* out its debounce on a real timer, a millisecond even when the setting is 0, and how many turns fit
218+
* into a millisecond is the machine's business: a few dozen on a Mac, well over a thousand on
219+
* Linux, where the release gate runs. A count that was plenty on one ran out on the other before
220+
* the timer had fired.
221+
*/
213222
async function until(cond, what) {
214-
for (let i = 0; i < 400; i++) { if (cond()) { return; } await tick(); }
215-
assert.fail('never happened: ' + what);
223+
const deadline = Date.now() + PATIENCE_MS;
224+
while (!cond()) {
225+
if (Date.now() > deadline) { assert.fail('never happened: ' + what); }
226+
await tick();
227+
}
216228
}
217-
/** Let everything already in motion get as far as it can. */
229+
/** Let everything already in motion get as far as it can. Turns, not time — so only for what no timer stands in the way of. */
218230
async function settle() { for (let i = 0; i < 40; i++) { await tick(); } }
219231
function deferred() { let resolve; const promise = new Promise((r) => { resolve = r; }); return { promise, resolve }; }
220232
function within(promise, ms, what) {

‎scripts/test-extensions.sh‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Run every extension's unit suites. This is the gate: the release workflow runs it before spending
4+
# an hour of macOS build minutes, and every pull request runs it too (.github/workflows/test.yml).
5+
#
6+
# It DISCOVERS suites — extensions/*/test/*.test.js — so a new one is gated the moment it is added,
7+
# with no list to keep in sync. (The gate used to `cd extensions/levelcode-ai`; levelcode-updater's
8+
# suites never ran, including the one guarding the Download button against serving a raw .app.zip.)
9+
# Requires are file-relative, so the suites run from the repo root.
10+
#
11+
# Usage:
12+
# ./scripts/test-extensions.sh
13+
#
14+
set -euo pipefail
15+
shopt -s nullglob
16+
17+
cd "$(dirname "$0")/.."
18+
19+
count=0
20+
for t in extensions/*/test/*.test.js; do
21+
echo "── $t"
22+
node "$t" # `set -e` stops at the first suite that fails
23+
count=$((count + 1))
24+
done
25+
# A glob that matches nothing would otherwise report success and gate nothing. Fail loudly instead.
26+
if [ "$count" -eq 0 ]; then
27+
echo "::error::No suites matched extensions/*/test/*.test.js — the gate would pass vacuously."
28+
exit 1
29+
fi
30+
echo "──────── $count test files passed ────────"

0 commit comments

Comments
 (0)