mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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.
This commit is contained in:
@@ -6,5 +6,6 @@ These rules apply to `scripts/**`. Follow the repo-root `AGENTS.md` first, then
|
||||
- Use repo-relative paths and environment variables instead of user-specific absolute paths.
|
||||
- For dev-server helpers, default to `https://5chan.localhost`, but allow a branch-scoped `*.5chan.localhost` route when the launcher is avoiding a Portless name collision. Start the Portless HTTPS proxy on port 443 before registering routes so legacy `~/.portless` state on port 1355 is not reused. Respect the existing `PORTLESS=0` fallback instead of hard-coding alternate ports. For USB Android preview, `scripts/start-android-usb.mjs` mirrors bitsocial-web: `adb reverse` plus Vite on `127.0.0.1`, then `am start` VIEW to open the default browser when the port is listening (disable with `ANDROID_USB_OPEN_BROWSER=0`).
|
||||
- Keep shell helpers thin. When logic becomes stateful or cross-platform, prefer a Node script.
|
||||
- `scripts/pw-session.sh` owns the machine-wide Playwright resource lock shared by every worktree and checkout, so its default lock path must stay repository-independent. Keep acquisition atomic, treat `playwright-cli list --all` as the only liveness oracle and leave the lock alone when it cannot be read, require exact-owner release, and close the named browser before normal release; never broaden cleanup to unrelated sessions.
|
||||
- Git and worktree helpers must validate input and default to safe operations.
|
||||
- If a helper deletes local branches automatically, document the exact eligibility checks and keep the behavior conservative.
|
||||
|
||||
Executable
+318
@@ -0,0 +1,318 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
|
||||
# pw-session.sh — shared resource lock for playwright-cli browser sessions.
|
||||
#
|
||||
# Playwright disables normal background throttling, so a hidden 5chan page keeps
|
||||
# doing P2P and rendering work after a check finishes. Agents verifying in
|
||||
# parallel therefore stack whole browser engines on one machine. This wrapper
|
||||
# permits one active Playwright browser at a time and records who holds it.
|
||||
#
|
||||
# The lock is machine-wide, not per-repository: the contended resource is RAM and
|
||||
# CPU, so every checkout that ships this script shares a single slot. Set
|
||||
# PLAYWRIGHT_RESOURCE_LOCK_DIR to isolate a lock (tests, or a deliberate second
|
||||
# slot on a machine with headroom).
|
||||
#
|
||||
# Liveness comes from `playwright-cli list --all`, which reports `status: open`
|
||||
# for a running browser. A lock whose recorded session is no longer open is
|
||||
# stale, and is reclaimed automatically rather than blocking every later
|
||||
# workflow. When playwright-cli cannot be queried the lock is left alone, so a
|
||||
# broken CLI never silently disables the budget.
|
||||
#
|
||||
# PW_SESSION_POLL_SECONDS overrides how often `--wait` re-checks the slot.
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/pw-session.sh open [--wait[=SECONDS]] <session> [playwright-cli open arguments...]
|
||||
./scripts/pw-session.sh close <session>
|
||||
./scripts/pw-session.sh status
|
||||
./scripts/pw-session.sh release <session>
|
||||
|
||||
One browser slot is shared by every worktree and repository on this machine.
|
||||
|
||||
open Acquire the slot, then start the browser. Exits 75 when the slot is
|
||||
held by a live session; --wait polls until it frees (default 300s).
|
||||
A slot whose browser is gone is reclaimed automatically.
|
||||
close Stop the named browser, then release the slot. Always attempts the
|
||||
browser close, even when the lock was already lost, and never
|
||||
releases a slot held by a different session.
|
||||
status Report the holder and whether its browser is still running.
|
||||
release Drop a lock without closing a browser. Normal cleanup uses `close`.
|
||||
EOF
|
||||
}
|
||||
|
||||
playwright_cli="${PLAYWRIGHT_CLI_BIN:-playwright-cli}"
|
||||
lock_root="${XDG_CACHE_HOME:-$HOME/.cache}/bitsocial"
|
||||
lock_dir="${PLAYWRIGHT_RESOURCE_LOCK_DIR:-$lock_root/playwright-session.lock}"
|
||||
owner_file="$lock_dir/owner"
|
||||
started_file="$lock_dir/started-at"
|
||||
workspace_file="$lock_dir/workspace"
|
||||
default_wait_seconds=300
|
||||
poll_seconds="${PW_SESSION_POLL_SECONDS:-5}"
|
||||
|
||||
# Recorded for diagnostics only: the lock is machine-wide, so a checkout outside
|
||||
# a Git worktree is unusual but not an error.
|
||||
workspace="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
|
||||
validate_session() {
|
||||
local session="$1"
|
||||
|
||||
if [[ ! "$session" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$ ]]; then
|
||||
echo "pw-session: session must be 1-40 characters using letters, numbers, '.', '_', or '-'" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
current_owner() {
|
||||
if [ -f "$owner_file" ]; then
|
||||
sed -n '1p' "$owner_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Echoes `live`, `dead`, or `unknown` for a session name. `unknown` means the
|
||||
# browser list could not be read, and callers must treat the lock as held.
|
||||
session_state() {
|
||||
local session="$1" listing line current=''
|
||||
|
||||
if ! listing="$("$playwright_cli" list --all 2>/dev/null)"; then
|
||||
echo unknown
|
||||
return 0
|
||||
fi
|
||||
|
||||
# `playwright-cli list --all` prints a `- <session>:` header per browser,
|
||||
# followed by indented fields including ` - status: open|closed`.
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
'- '*':')
|
||||
current="${line#- }"
|
||||
current="${current%:}"
|
||||
;;
|
||||
' - status: open')
|
||||
if [ "$current" = "$session" ]; then
|
||||
echo live
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done <<<"$listing"
|
||||
|
||||
echo dead
|
||||
}
|
||||
|
||||
write_lock_metadata() {
|
||||
printf '%s\n' "$1" >"$owner_file"
|
||||
date -u '+%Y-%m-%dT%H:%M:%SZ' >"$started_file"
|
||||
printf '%s\n' "$workspace" >"$workspace_file"
|
||||
}
|
||||
|
||||
print_status() {
|
||||
local owner started held_workspace state
|
||||
|
||||
if [ ! -d "$lock_dir" ]; then
|
||||
echo "pw-session: browser slot is available"
|
||||
return 0
|
||||
fi
|
||||
|
||||
owner="$(current_owner)"
|
||||
started="$(sed -n '1p' "$started_file" 2>/dev/null || true)"
|
||||
held_workspace="$(sed -n '1p' "$workspace_file" 2>/dev/null || true)"
|
||||
state="$([ -n "$owner" ] && session_state "$owner" || echo unknown)"
|
||||
|
||||
case "$state" in
|
||||
live) echo "pw-session: browser slot is held" ;;
|
||||
dead) echo "pw-session: browser slot is held by a stale lock" ;;
|
||||
*) echo "pw-session: browser slot is held (browser state unverifiable)" ;;
|
||||
esac
|
||||
|
||||
echo "Session: ${owner:-unknown}"
|
||||
echo "Started: ${started:-unknown}"
|
||||
echo "Workspace: ${held_workspace:-unknown}"
|
||||
echo "Lock: $lock_dir"
|
||||
|
||||
case "$state" in
|
||||
live) echo "Browser: running" ;;
|
||||
dead)
|
||||
echo "Browser: not running — the next 'open' reclaims this slot automatically"
|
||||
;;
|
||||
*)
|
||||
echo "Browser: unverifiable — '$playwright_cli list --all' failed, so the lock is left alone"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Atomically drop a lock we have confirmed is stale. Renaming first means only
|
||||
# one racing reclaimer can win, so a concurrent fresh lock is never deleted.
|
||||
reclaim_stale_lock() {
|
||||
local owner="$1" staged="${lock_dir}.stale.$$"
|
||||
|
||||
if mv "$lock_dir" "$staged" 2>/dev/null; then
|
||||
rm -rf "$staged"
|
||||
echo "pw-session: reclaimed stale slot from '$owner' (its browser is no longer running)" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
acquire() {
|
||||
local session="$1" wait_seconds="$2" owner state reclaims=0
|
||||
|
||||
validate_session "$session"
|
||||
mkdir -p "$(dirname "$lock_dir")"
|
||||
|
||||
SECONDS=0
|
||||
while true; do
|
||||
if mkdir "$lock_dir" 2>/dev/null; then
|
||||
write_lock_metadata "$session"
|
||||
echo "pw-session: acquired browser slot for '$session'"
|
||||
return 0
|
||||
fi
|
||||
|
||||
owner="$(current_owner)"
|
||||
state="$([ -n "$owner" ] && session_state "$owner" || echo dead)"
|
||||
|
||||
# Bounded so an unremovable lock directory fails loudly instead of spinning.
|
||||
if [ "$state" = dead ] && [ "$reclaims" -lt 3 ]; then
|
||||
reclaims=$((reclaims + 1))
|
||||
reclaim_stale_lock "${owner:-unknown}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$state" = dead ]; then
|
||||
print_status >&2
|
||||
echo "pw-session: could not reclaim the stale slot at $lock_dir; remove it by hand" >&2
|
||||
return 75
|
||||
fi
|
||||
|
||||
if [ "$wait_seconds" -gt 0 ] && [ "$SECONDS" -lt "$wait_seconds" ]; then
|
||||
echo "pw-session: slot held by '$owner'; retrying in ${poll_seconds}s (waited ${SECONDS}s of ${wait_seconds}s)" >&2
|
||||
sleep "$poll_seconds"
|
||||
continue
|
||||
fi
|
||||
|
||||
print_status >&2
|
||||
if [ "$wait_seconds" -gt 0 ]; then
|
||||
echo "pw-session: gave up after ${wait_seconds}s; do not bypass the lock" >&2
|
||||
else
|
||||
echo "pw-session: another browser workflow is active; do not bypass the lock" >&2
|
||||
fi
|
||||
return 75
|
||||
done
|
||||
}
|
||||
|
||||
release() {
|
||||
local session="$1" owner
|
||||
|
||||
validate_session "$session"
|
||||
if [ ! -d "$lock_dir" ]; then
|
||||
echo "pw-session: browser slot is already available"
|
||||
return 0
|
||||
fi
|
||||
|
||||
owner="$(current_owner)"
|
||||
if [ "$owner" != "$session" ]; then
|
||||
echo "pw-session: '$session' cannot release the slot held by '${owner:-unknown}'" >&2
|
||||
if [ -n "$owner" ] && [ "$(session_state "$owner")" = dead ]; then
|
||||
echo "pw-session: that lock is stale; the next 'open' reclaims it automatically" >&2
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$owner_file" "$started_file" "$workspace_file"
|
||||
rmdir "$lock_dir"
|
||||
echo "pw-session: released browser slot for '$session'"
|
||||
}
|
||||
|
||||
command="${1:-}"
|
||||
case "$command" in
|
||||
open)
|
||||
shift
|
||||
wait_seconds=0
|
||||
session=''
|
||||
open_args=()
|
||||
|
||||
# `--wait` is accepted anywhere so `open <session> --wait` is not a silent
|
||||
# no-op. `playwright-cli open` has no --wait of its own, so nothing that
|
||||
# belongs to it is swallowed here. The first bare argument is the session;
|
||||
# the rest pass through untouched.
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--wait)
|
||||
wait_seconds="$default_wait_seconds"
|
||||
;;
|
||||
--wait=*)
|
||||
wait_seconds="${1#--wait=}"
|
||||
if [[ ! "$wait_seconds" =~ ^[0-9]+$ ]]; then
|
||||
echo "pw-session: --wait expects a whole number of seconds" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ -z "$session" ]; then
|
||||
session="$1"
|
||||
else
|
||||
open_args+=("$1")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ -z "$session" ]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
acquire "$session" "$wait_seconds"
|
||||
# Guarded expansion: Bash 3.2 (macOS /bin/bash) errors on an empty array
|
||||
# under `set -u`.
|
||||
if ! "$playwright_cli" -s="$session" open ${open_args[@]+"${open_args[@]}"}; then
|
||||
release "$session"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
close)
|
||||
session="${2:-}"
|
||||
if [ -z "$session" ] || [ "$#" -ne 2 ]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
validate_session "$session"
|
||||
|
||||
# Cleanup must always stop the browser, even when the lock was lost, so a
|
||||
# failed workflow cannot strand a running engine.
|
||||
owner="$(current_owner)"
|
||||
close_status=0
|
||||
"$playwright_cli" -s="$session" close || close_status=$?
|
||||
if [ "$close_status" -ne 0 ]; then
|
||||
echo "pw-session: warning: closing browser '$session' exited $close_status" >&2
|
||||
fi
|
||||
|
||||
if [ -z "$owner" ]; then
|
||||
echo "pw-session: browser slot was already free; closed '$session' anyway"
|
||||
elif [ "$owner" = "$session" ]; then
|
||||
release "$session"
|
||||
else
|
||||
echo "pw-session: closed '$session'; left the slot held by '$owner' untouched" >&2
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
if [ "$#" -ne 1 ]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
print_status
|
||||
;;
|
||||
release)
|
||||
session="${2:-}"
|
||||
if [ -z "$session" ] || [ "$#" -ne 2 ]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
release "$session"
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,205 @@
|
||||
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:');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user