mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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.
206 lines
7.4 KiB
JavaScript
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:');
|
|
});
|
|
});
|