Skip to content

Commit a4eacd2

Browse files
Steel-techclaude
andcommitted
ci: require real-browser integration and saved QA on Linux and macOS
Adds a browser job (ubuntu/macos x Node 22/24) that installs pinned agent-browser 0.33.2, installs Chromium, and runs doctor, test:integration and test:qa, so skipped browser coverage can no longer pass CI. The unit job runs npm run validate on Node 20/22/24. makeBrowser now passes the renderer's env to agent-browser, and the e2e test uses a short AGENT_BROWSER_SOCKET_DIR so long checkout paths stay under the macOS Unix socket path limit. Recovered from jig run 6806b9fd, which passed its gates but was held locally by the UI's keep-local delivery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 182a62c commit a4eacd2

5 files changed

Lines changed: 116 additions & 21 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ jobs:
1414
timeout-minutes: 10
1515
strategy:
1616
matrix:
17-
# 20 is the supported floor; 22 exercises live streaming and CDP
18-
# attach mode (global WebSocket client).
19-
node-version: [20, 22]
17+
# 20 is the supported floor; 22/24 support the global WebSocket client.
18+
node-version: [20, 22, 24]
2019
steps:
2120
- name: Check out repository
2221
# v4.2.2
@@ -30,11 +29,66 @@ jobs:
3029
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b
3130
with:
3231
node-version: ${{ matrix.node-version }}
33-
- name: Run tests
34-
run: npm test
35-
- name: Validate shell syntax
36-
run: bash -n scripts/*.sh
32+
- name: Validate source, manifest, shell scripts, and tests
33+
run: npm run validate
3734
- name: Run ShellCheck
3835
run: shellcheck scripts/*.sh
39-
- name: Validate plugin manifest
40-
run: node scripts/check-manifest.mjs
36+
37+
browser:
38+
runs-on: ${{ matrix.os }}
39+
timeout-minutes: 15
40+
strategy:
41+
fail-fast: false
42+
matrix:
43+
os: [ubuntu-latest, macos-latest]
44+
node-version: [22, 24]
45+
steps:
46+
- name: Check out repository
47+
# v4.2.2
48+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
49+
with:
50+
persist-credentials: false
51+
- name: Set up Node.js for agent-browser installation
52+
# agent-browser 0.33.2 requires Node 24 to install its native CLI.
53+
# v4.0.3
54+
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b
55+
with:
56+
node-version: 24
57+
- name: Install pinned agent-browser
58+
run: |
59+
npm install --global --prefix "$RUNNER_TEMP/agent-browser" agent-browser@0.33.2
60+
echo "$RUNNER_TEMP/agent-browser/bin" >> "$GITHUB_PATH"
61+
- name: Install Chromium and platform prerequisites
62+
shell: bash
63+
run: |
64+
if [[ "$RUNNER_OS" == "Linux" ]]; then
65+
agent-browser install --with-deps
66+
else
67+
agent-browser install
68+
fi
69+
- name: Export installed Chromium executable
70+
shell: bash
71+
run: |
72+
python3 - <<'PY' >> "$GITHUB_ENV"
73+
import os
74+
from pathlib import Path
75+
76+
cache = Path.home() / ".agent-browser" / "browsers"
77+
binaries = [p for p in cache.rglob("*")
78+
if p.name in ("chrome", "Google Chrome for Testing")
79+
and p.is_file() and os.access(p, os.X_OK)]
80+
if len(binaries) != 1:
81+
raise SystemExit(f"Expected one installed Chromium executable, found {binaries}")
82+
print(f"HERDR_BROWSER_CHROMIUM={binaries[0]}")
83+
PY
84+
- name: Set up Node.js under test
85+
# v4.0.3
86+
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b
87+
with:
88+
node-version: ${{ matrix.node-version }}
89+
- name: Check browser prerequisites
90+
run: npm run doctor
91+
- name: Require real-browser integration coverage
92+
run: npm run test:integration
93+
- name: Require saved QA browser coverage
94+
run: npm run test:qa

‎README.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ npm run doctor
499499
npm run build
500500
npm run validate
501501
npm run test:integration
502+
npm run test:qa
502503
herdr plugin link .
503504
```
504505

@@ -514,7 +515,15 @@ browser coverage; optional browser tests skip when their prerequisites are
514515
unavailable. `npm run test:integration` requires both an installed Chrome or
515516
Chromium and agent-browser with its engine installed, on Node 22+. It fails
516517
instead of silently skipping either real-browser path. Tests use a local HTTP
517-
fixture and isolated browser sessions.
518+
fixture and isolated browser sessions. `npm run test:qa` requires agent-browser
519+
with its engine installed and runs saved desktop/mobile scenarios, including
520+
assertion failures and browser error evidence. CI installs pinned agent-browser
521+
0.33.2 using Node 24, then tests the plugin on Node 22/24 on Linux and macOS.
522+
523+
Before release, require local validation, both strict browser commands, and the
524+
hosted CI matrix to pass for the reviewed commit. A passing suite with browser
525+
coverage skipped is insufficient. Manually verify Kitty graphics and symbol
526+
rendering in Herdr on macOS and Linux.
518527

519528
For backend choices and the remaining readiness work, see the
520529
[readiness assessment](docs/readiness.md).

‎bin/renderer.mjs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export function formatNetworkFailure({ method, url, status }) {
423423

424424
// --- agent-browser access ---
425425

426-
export function makeBrowser(session, bin = "agent-browser") {
426+
export function makeBrowser(session, bin = "agent-browser", env = process.env) {
427427
// Console rings on noisy pages reach several MB — Node's 1 MiB default
428428
// maxBuffer would throw on every tick and freeze the pane for good.
429429
const maxBuffer = 16 * 1024 * 1024;
@@ -446,7 +446,7 @@ export function makeBrowser(session, bin = "agent-browser") {
446446
const { stdout } = await pExecFile(
447447
bin,
448448
["--session", session, "batch", "--bail", "--json", ...cmds],
449-
{ timeout, maxBuffer },
449+
{ timeout, maxBuffer, env },
450450
);
451451
const arr = parse(stdout, "batch output");
452452
for (const r of arr) {
@@ -464,6 +464,7 @@ export function makeBrowser(session, bin = "agent-browser") {
464464
{
465465
timeout: 10_000,
466466
maxBuffer,
467+
env,
467468
},
468469
);
469470
const parsed = parse(stdout, "output");
@@ -541,6 +542,7 @@ export function makeBrowser(session, bin = "agent-browser") {
541542
const { stdout } = await pExecFile(bin, ["session", "list", "--json"], {
542543
timeout: 10_000,
543544
maxBuffer,
545+
env,
544546
});
545547
// Exact match only: a substring hit (herdr-ws-w2 vs herdr-ws-w22)
546548
// would make the pane attach and auto-create a session it must not.
@@ -592,7 +594,7 @@ export class Renderer {
592594
this.browser =
593595
this.backend === "attach"
594596
? makeCdpBrowser(this.cdpEndpoint)
595-
: makeBrowser(this.session, this.bin);
597+
: makeBrowser(this.session, this.bin, this.env);
596598
// Attach mode observes a browser someone else owns: ownership is never
597599
// claimed, so the quit path can never close a stranger's session.
598600
this.ownershipEnabled = this.backend !== "attach";
@@ -1883,6 +1885,7 @@ export class Renderer {
18831885
try {
18841886
spawnSync(this.bin, ["--session", this.session, "close"], {
18851887
timeout: 2_000,
1888+
env: this.env,
18861889
});
18871890
} catch {
18881891
/* already gone */

‎docs/readiness.md‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,33 @@ own Chrome engine; a second bundled distribution is unnecessary.
3636
- Limit test discovery to this checkout's `tests/` directory; Node 20 otherwise
3737
traverses hidden nested worktrees and runs unrelated, stale test copies.
3838

39+
## Automated readiness gates
40+
41+
CI runs `npm run validate` on Linux with Node 20, 22, and 24. This checks source
42+
and shell syntax, the plugin manifest, ShellCheck, and the complete test suite.
43+
Separate browser jobs run on Linux and macOS with Node 22 and 24. They install
44+
pinned agent-browser 0.33.2 under Node 24, install its Chromium engine (including
45+
Linux system dependencies), and export the installed executable through
46+
`HERDR_BROWSER_CHROMIUM` before switching to the Node version under test.
47+
48+
Every browser job runs `npm run doctor`, `npm run test:integration`, and
49+
`npm run test:qa`. The strict test commands require real local Chromium,
50+
shared-session streaming, and desktop/mobile saved QA coverage; missing
51+
prerequisites and installation failures fail the job instead of skipping that
52+
coverage. Existing tests use local fixtures and clean up their browser sessions.
53+
54+
Readiness requires local validation and both strict browser commands to pass,
55+
plus a passing hosted matrix for the exact commit being reviewed. Configuring
56+
these gates does not establish that they have passed. Before release, manually
57+
verify Kitty graphics and symbol rendering in Herdr on macOS and Linux; hosted
58+
browser tests do not verify terminal display quality or pane interaction.
59+
3960
## Next recommendations, in priority order
4061

41-
1. **Release compatibility matrix.** Exercise macOS and Linux, Node 22/24,
42-
supported agent-browser versions, and Kitty/symbol rendering in Herdr.
43-
Current CI has Node 20/22 but does not install agent-browser, so its passing
44-
status alone cannot prove shared-session integration. Adopting the strict
45-
integration command in hosted CI is a separate workflow change.
62+
1. **Release compatibility checks.** Complete the manual Kitty/symbol checks
63+
in Herdr on macOS and Linux. Hosted CI now covers both platforms on Node
64+
22/24 with agent-browser 0.33.2; expand agent-browser version coverage when
65+
claiming support for additional versions.
4666
2. **CDP transport correctness.** HTTP discovery currently uses `node:http`
4767
even for an HTTPS input and assumes the local debugging port. Implement
4868
actual HTTPS discovery with transport tests before advertising secured

‎tests/renderer.test.mjs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ test("cleanup closes only self-created sessions and removes shot files", () => {
752752
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "hb-close-"));
753753
const stub = path.join(dir, "ab-stub");
754754
const logf = path.join(dir, "log");
755-
fs.writeFileSync(stub, `#!/usr/bin/env bash\necho "$@" >> "${logf}"\n`);
755+
fs.writeFileSync(stub, `#!/bin/sh\necho "$@" >> "${logf}"\n`);
756756
fs.chmodSync(stub, 0o755);
757757
const mk = () => quiet(mkRenderer());
758758
const own = mk();
@@ -1397,13 +1397,20 @@ test("e2e: goLive receives pushed frames and console from a real session", {
13971397
}, async () => {
13981398
assert.ok(hasAgentBrowser && canCdp, "integration requires agent-browser and Node 22+");
13991399
const session = `hb-itest-${process.pid}`;
1400+
// Keep Unix socket paths below macOS's limit even with a long checkout HOME.
1401+
const socketDir = fs.mkdtempSync("/tmp/hb-s-");
1402+
const env = {
1403+
...process.env,
1404+
HERDR_BROWSER_SESSION: session,
1405+
AGENT_BROWSER_SOCKET_DIR: socketDir,
1406+
};
14001407
const { execFile: ef } = await import("node:child_process");
14011408
const ab = (args) =>
14021409
new Promise((res, rej) =>
14031410
ef(
14041411
"agent-browser",
14051412
["--session", session, ...args],
1406-
{ timeout: 15_000 },
1413+
{ timeout: 15_000, env },
14071414
(e, so) => (e ? rej(e) : res(so)),
14081415
),
14091416
);
@@ -1412,7 +1419,7 @@ test("e2e: goLive receives pushed frames and console from a real session", {
14121419
res.end("<!doctype html><title>Browser stream test</title><h1>Local browser fixture</h1><textarea aria-label='Text'></textarea>");
14131420
});
14141421
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
1415-
const r = quiet(mkRenderer({ HERDR_BROWSER_SESSION: session }));
1422+
const r = quiet(mkRenderer(env));
14161423
const streamEvents = {};
14171424
const onStreamMessage = r.onStreamMessage.bind(r);
14181425
r.onStreamMessage = (message) => {
@@ -1472,7 +1479,9 @@ test("e2e: goLive receives pushed frames and console from a real session", {
14721479
} finally {
14731480
r.dropLive();
14741481
await ab(["close"]).catch(() => {});
1482+
r.cleanup();
14751483
await new Promise((resolve) => server.close(resolve));
1484+
fs.rmSync(socketDir, { recursive: true, force: true });
14761485
}
14771486
});
14781487

0 commit comments

Comments
 (0)