Files
5chan/scripts/pw-session.test.js
T
Tommaso Casaburi 1a33f7dc88 chore(agents): add a machine-wide Playwright browser resource budget
Playwright disables normal background throttling, so a hidden 5chan page keeps
doing P2P and rendering work after a check finishes. Agents verifying in
parallel across worktrees stacked whole browser engines on one machine.

Add scripts/pw-session.sh, a wrapper that permits one active Playwright browser
at a time and records who holds it:

- The lock is machine-wide, not per-repository, because the contended resource
  is RAM and CPU. Every worktree and checkout shares one slot.
- Acquisition is an atomic mkdir. Stale locks clear themselves: `open` reclaims
  any slot whose recorded browser is no longer `status: open` in
  `playwright-cli list --all`, so an interrupted workflow cannot strand the
  budget. When that list cannot be read the lock is left alone, so a broken CLI
  never silently disables the budget.
- `open` exits 75 when the slot is busy; `--wait[=SECONDS]` blocks instead.
- `close` always stops the browser, even when the lock was already lost, and
  never releases a slot held by a different session.
- `status` reports the holder and whether its browser is still alive.

Agent policy now runs browser engines and profiler batches sequentially, uses
Chrome/Blink during iteration and the full engine matrix only for final
verification, and never uses `close-all` or `kill-all` while other agents may
own sessions.

Covered by scripts/pw-session.test.js.
2026-08-01 19:21:01 +02:00

206 lines
7.4 KiB
JavaScript

import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
const scriptPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'pw-session.sh');
// A stand-in for playwright-cli so the lock can be exercised without launching
// real browser engines. `list` reports the session recorded in liveSession as
// `status: open`, mirroring the real `playwright-cli list --all` output.
const FAKE_CLI = `#!/bin/bash
state="$(dirname "$0")/live-session"
open_rc="$(dirname "$0")/open-exit-code"
if [ "\${1:-}" = "list" ]; then
live="$(cat "$state")"
[ "$live" = "__UNAVAILABLE__" ] && exit 3
echo "### Browsers"
if [ -n "$live" ]; then
echo "- $live:"
echo " - status: open"
fi
echo "- already-closed:"
echo " - status: closed"
exit 0
fi
session="\${1#-s=}"
case "\${2:-}" in
open)
rc="$(cat "$open_rc")"
[ "$rc" = 0 ] && printf '%s' "$session" >"$state"
exit "$rc"
;;
close)
[ "$(cat "$state")" = "$session" ] && printf '' >"$state"
exit 0
;;
esac
exit 0
`;
let tempDir;
// Status and diagnostics are split across stdout and stderr, so assertions read
// both streams.
const run = (...args) => {
const result = spawnSync(scriptPath, args, {
encoding: 'utf8',
env: {
...process.env,
PLAYWRIGHT_RESOURCE_LOCK_DIR: path.join(tempDir, 'slot.lock'),
PLAYWRIGHT_CLI_BIN: path.join(tempDir, 'fake-playwright-cli'),
PW_SESSION_POLL_SECONDS: '1',
},
});
return { code: result.status, output: `${result.stdout}${result.stderr}` };
};
const lockExists = () => fs.existsSync(path.join(tempDir, 'slot.lock'));
const setLiveSession = (session) => fs.writeFileSync(path.join(tempDir, 'live-session'), session);
const liveSession = () => fs.readFileSync(path.join(tempDir, 'live-session'), 'utf8');
const setOpenExitCode = (code) => fs.writeFileSync(path.join(tempDir, 'open-exit-code'), String(code));
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-session-test-'));
const fakeCli = path.join(tempDir, 'fake-playwright-cli');
fs.writeFileSync(fakeCli, FAKE_CLI, { mode: 0o755 });
setLiveSession('');
setOpenExitCode(0);
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('pw-session.sh', () => {
it('reports an available slot and rejects malformed session names', () => {
expect(run('status').output).toContain('browser slot is available');
const rejected = run('open', 'bad name!');
expect(rejected.code).toBe(1);
expect(rejected.output).toContain('session must be 1-40');
});
it('acquires the slot, records the owner, and blocks a second live acquire with exit 75', () => {
expect(run('open', 'verify-chrome', 'about:blank').output).toContain("acquired browser slot for 'verify-chrome'");
expect(run('status').output).toContain('Browser: running');
const blocked = run('open', 'verify-firefox', 'about:blank');
expect(blocked.code).toBe(75);
expect(blocked.output).toContain('do not bypass the lock');
});
it('refuses to release or steal a slot owned by another session', () => {
run('open', 'verify-chrome', 'about:blank');
const released = run('release', 'verify-firefox');
expect(released.code).toBe(1);
expect(released.output).toContain("cannot release the slot held by 'verify-chrome'");
// Closing a different session must still stop that browser without
// dropping someone else's lock.
expect(run('close', 'verify-firefox').output).toContain("left the slot held by 'verify-chrome' untouched");
expect(lockExists()).toBe(true);
});
it('releases the slot when the owner closes it', () => {
run('open', 'verify-chrome', 'about:blank');
expect(run('close', 'verify-chrome').output).toContain("released browser slot for 'verify-chrome'");
expect(lockExists()).toBe(false);
});
it('reclaims a stale slot whose browser is gone instead of blocking forever', () => {
run('open', 'verify-chrome', 'about:blank');
setLiveSession(''); // the browser died without releasing the lock
const status = run('status');
expect(status.output).toContain('stale lock');
expect(status.output).toContain('reclaims this slot automatically');
const reclaimed = run('open', 'verify-firefox', 'about:blank');
expect(reclaimed.code).toBe(0);
expect(reclaimed.output).toContain("reclaimed stale slot from 'verify-chrome'");
});
it('still stops the browser when the lock was already lost', () => {
run('open', 'verify-chrome', 'about:blank');
fs.rmSync(path.join(tempDir, 'slot.lock'), { recursive: true });
expect(run('close', 'verify-chrome').output).toContain("already free; closed 'verify-chrome' anyway");
expect(liveSession()).toBe('');
});
it('releases the slot when the browser fails to start', () => {
setOpenExitCode(1);
const failed = run('open', 'verify-chrome', 'about:blank');
expect(failed.code).toBe(1);
expect(failed.output).toContain('released browser slot');
expect(lockExists()).toBe(false);
});
it('leaves the lock alone when browser liveness cannot be verified', () => {
run('open', 'verify-chrome', 'about:blank');
setLiveSession('__UNAVAILABLE__'); // playwright-cli list fails
expect(run('status').output).toContain('unverifiable');
// Failing closed matters: a broken CLI must not silently disable the budget.
const blocked = run('open', 'verify-firefox', 'about:blank');
expect(blocked.code).toBe(75);
expect(lockExists()).toBe(true);
});
it('polls for a busy slot with --wait and gives up with exit 75', () => {
run('open', 'verify-chrome', 'about:blank');
const timedOut = run('open', '--wait=2', 'verify-firefox', 'about:blank');
expect(timedOut.code).toBe(75);
expect(timedOut.output).toContain('gave up after 2s');
expect(timedOut.output).toContain('retrying in 1s');
const malformed = run('open', '--wait=soon', 'verify-firefox');
expect(malformed.code).toBe(1);
expect(malformed.output).toContain('whole number of seconds');
});
it('honours --wait after the session name instead of passing it to playwright-cli', () => {
run('open', 'verify-chrome', 'about:blank');
const afterSession = run('open', 'verify-firefox', '--wait=2', 'about:blank');
expect(afterSession.code).toBe(75);
expect(afterSession.output).toContain('gave up after 2s');
});
it('fails loudly instead of spinning when a stale lock cannot be removed', () => {
run('open', 'verify-chrome', 'about:blank');
setLiveSession(''); // stale, but the lock directory is not removable
fs.chmodSync(tempDir, 0o500);
try {
const stuck = run('open', 'verify-firefox', 'about:blank');
expect(stuck.code).toBe(75);
expect(stuck.output).toContain('could not reclaim the stale slot');
} finally {
fs.chmodSync(tempDir, 0o700);
}
});
it('opens with no extra playwright-cli arguments', () => {
// Bash 3.2 errors on an empty array expansion under `set -u`, so the
// no-arguments path needs its own guard.
const opened = run('open', 'verify-chrome');
expect(opened.code).toBe(0);
expect(opened.output).toContain("acquired browser slot for 'verify-chrome'");
});
it('prints usage for an unknown subcommand', () => {
const unknown = run('bogus');
expect(unknown.code).toBe(1);
expect(unknown.output).toContain('Usage:');
});
});