mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
A prompt visible only in a seat's pane must appear as needs-input across the API and CLI, and quoted approval text above a working Claude composer must let coordination messages through, while an actual waiting prompt still refuses them. Both regressions escaped to users (#1946, #1955). Adds one installed `prompt-state` scenario using adopted scripted panes, the real daemon, CLI and tmux path, actual timer wakes, and a read-only check that no hook supplied the activity. It checks that attention clears on a working pane, that both quoted-text forms admit a send and a wake, and that a real dialog refuses both; the refused wake is tied to the watchdog's own attempt record and refusal reason. No production source or provider calls. Validation: the installed case passes at this head in about 21 s in its disposable container (peak memory not sampled). On rebuilt historical sources it fails at the named assertions: the pane-only attention check on32633a3d, and the quoted-selector working send on the parent ofdb863940. The scenarios use controlled pane layouts and don't prove native runtime behaviour.
129 lines
6.5 KiB
Bash
129 lines
6.5 KiB
Bash
#!/usr/bin/env bash
|
|
# All daemon/seat work runs inside the existing disposable testbed.
|
|
# Default: GitHub CI. --remote: explicitly selected SSH Docker executor.
|
|
set -euo pipefail
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
REMOTE=false
|
|
CASES=(fixture library)
|
|
MODES=(healthy lost-baton healthy)
|
|
# Stub-script scenarios run once each, healthy only: their planted failures are
|
|
# product-code mutations, kept as per-PR evidence outside CI.
|
|
PASSING_CASES=(transcript capture daemon-restart refocus-active refocus-parked-off prompt-state)
|
|
EFFECT_CASES=(refocus-parked)
|
|
OUT="$REPO_ROOT/dist/pr-scenarios"
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--remote) REMOTE=true; shift ;;
|
|
--case)
|
|
case "${2:-}" in
|
|
fixture|library) CASES=("$2"); PASSING_CASES=(); EFFECT_CASES=();;
|
|
refocus-parked) CASES=(); PASSING_CASES=(); EFFECT_CASES=("$2");;
|
|
transcript|capture|daemon-restart|refocus-active|refocus-parked-off|prompt-state) CASES=(); PASSING_CASES=("$2"); EFFECT_CASES=();;
|
|
*) echo 'Expected --case fixture|library|transcript|capture|daemon-restart|refocus-parked|refocus-active|refocus-parked-off|prompt-state' >&2; exit 2;;
|
|
esac
|
|
shift 2 ;;
|
|
--mode)
|
|
case "${2:-}" in healthy|lost-baton|seeded) MODES=("$2");; *) echo 'Expected --mode healthy|lost-baton|seeded' >&2; exit 2;; esac
|
|
shift 2 ;;
|
|
--out) test -n "${2:-}" || exit 2; OUT="$2"; shift 2 ;;
|
|
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
# The passing-only cases have no lost-baton form.
|
|
if [ "${MODES[*]}" = lost-baton ]; then
|
|
test "${#CASES[@]}" -gt 0 || { echo 'the passing-only cases run healthy only' >&2; exit 2; }
|
|
PASSING_CASES=()
|
|
EFFECT_CASES=()
|
|
fi
|
|
EFFECT_MODES=(healthy seeded healthy)
|
|
if [ "${MODES[*]}" = healthy ]; then EFFECT_MODES=(healthy); fi
|
|
if [ "${MODES[*]}" = seeded ]; then
|
|
test "${#EFFECT_CASES[@]}" -gt 0 || { echo "seeded mode needs an effect case" >&2; exit 2; }
|
|
CASES=(); PASSING_CASES=(); EFFECT_MODES=(seeded)
|
|
fi
|
|
if "$REMOTE"; then
|
|
case "${DOCKER_HOST:-}" in ssh://?*) ;; *) echo '--remote requires explicit DOCKER_HOST=ssh://...' >&2; exit 2;; esac
|
|
test -z "${DOCKER_CONTEXT:-}" || { echo 'Unset DOCKER_CONTEXT so it cannot override the selected DOCKER_HOST' >&2; exit 2; }
|
|
else
|
|
test "${GITHUB_ACTIONS:-}" = true || { echo 'Use GitHub CI or --remote with the prepared disposable SSH executor' >&2; exit 2; }
|
|
test "$(uname -s)" = Linux
|
|
fi
|
|
mkdir -p "$OUT"
|
|
OUT="$(cd "$OUT" && pwd -P)"
|
|
TARGET_PLATFORM="$(node scripts/scenario-executor.mjs platform "$OUT/docker-server.json")"
|
|
|
|
# build-testbed-image performs the stock package build, clean target install,
|
|
# and daemon-load effect proof, then records the image inputs. It never pushes.
|
|
bash scripts/build-testbed-image.sh "$OUT/image"
|
|
SHA="$(git rev-parse HEAD)"
|
|
BASE="openrig-testbed:$SHA"
|
|
IMAGE="openrig-pr-scenarios:$SHA"
|
|
CONTEXT="$(mktemp -d)"
|
|
CONTAINER=""
|
|
CONTAINER_CID=""
|
|
IMAGE_ID=""
|
|
cleanup() {
|
|
if [ -n "$CONTAINER" ]; then
|
|
node scripts/scenario-executor.mjs settle "$CONTAINER_CID" "$CONTAINER" "$IMAGE_ID" >/dev/null || echo "Cleanup incomplete; retained container name: $CONTAINER" >&2
|
|
fi
|
|
rm -rf "$CONTEXT"
|
|
}
|
|
trap cleanup EXIT
|
|
cp docker/testbed/Dockerfile.scenarios "$CONTEXT/Dockerfile"
|
|
cp -R packages/daemon/test/fixtures/scenarios "$CONTEXT/scenarios"
|
|
cp -R packages/test-system/scenarios "$CONTEXT/library"
|
|
# esbuild already ships in the lockfile through tsx. Bundle the existing helper
|
|
# closure (including YAML) so the container needs no source tree or dev install.
|
|
node_modules/.bin/esbuild packages/test-system/ci/run.mjs --bundle --platform=node \
|
|
--format=esm --banner:js='import { createRequire as nodeRequire } from "node:module"; const require = nodeRequire(import.meta.url);' \
|
|
--outfile="$CONTEXT/runner.mjs" --metafile="$OUT/runner-inputs.json"
|
|
docker build --network none --platform "$TARGET_PLATFORM" --build-arg TESTBED_IMAGE="$BASE" -t "$IMAGE" "$CONTEXT"
|
|
docker image inspect "$IMAGE" > "$OUT/image-inspect.json"
|
|
IMAGE_ID="$(docker image inspect --format '{{.Id}}' "$IMAGE")"
|
|
|
|
attempt=0
|
|
run_case() { # run_case <case> <mode>
|
|
local scenario="$1" mode="$2" status=0 LOG
|
|
attempt=$((attempt + 1))
|
|
LOG="$OUT/$attempt-$scenario-$mode.log"
|
|
CONTAINER_CID="$OUT/$attempt-$scenario-$mode.cid"
|
|
test ! -e "$CONTAINER_CID" || { echo "Retained cidfile: $CONTAINER_CID" >&2; exit 2; }
|
|
CONTAINER="openrig-pr-${SHA:0:12}-$attempt-$$"
|
|
printf '%s\n' "$CONTAINER" > "$OUT/$attempt-$scenario-$mode.container-name.txt"
|
|
# Fresh writable scratch only. No mounts, host networking, credentials or Docker
|
|
# socket; the container is non-root, resource bounded and removed even on failure.
|
|
node scripts/scenario-executor.mjs timeout 300 docker run --name "$CONTAINER" --cidfile "$CONTAINER_CID" --platform "$TARGET_PLATFORM" --network none \
|
|
--read-only --tmpfs /tmp:rw,exec,nosuid,nodev,size=512m,mode=1777 \
|
|
--cap-drop ALL --security-opt no-new-privileges --pids-limit 256 \
|
|
--memory 2g --memory-swap 2g --cpus 2 "$IMAGE" node /opt/openrig-testbed/runner.mjs "$mode" "$scenario" \
|
|
> "$LOG" 2>&1 || status=$?
|
|
printf '%s\n' "$status" > "$OUT/$attempt-$scenario-$mode.exit-code.txt"
|
|
cat "$LOG"
|
|
node scripts/scenario-executor.mjs timeout 30 docker inspect "$CONTAINER" > "$OUT/$attempt-$scenario-$mode.container.json"
|
|
node scripts/scenario-executor.mjs settle "$CONTAINER_CID" "$CONTAINER" "$IMAGE_ID" >/dev/null || { echo "Cleanup incomplete; retained container name: $CONTAINER" >&2; CONTAINER=""; exit 2; }
|
|
CONTAINER=""
|
|
node --input-type=module - "$mode" "$status" "$LOG" "$scenario" <<'JS'
|
|
import { readFileSync } from 'node:fs';
|
|
import { PASSING_CASES, readReport, verifyPassingRun, verifyRun } from './packages/test-system/ci/result.mjs';
|
|
const [mode, status, log, scenario] = process.argv.slice(2);
|
|
const report = readReport(readFileSync(log, 'utf8'));
|
|
if (Object.hasOwn(PASSING_CASES, scenario)) {
|
|
verifyPassingRun(Number(status), report, scenario);
|
|
console.log(`${scenario}/${mode}: passing-only scenario passed`);
|
|
} else {
|
|
verifyRun(mode, Number(status), report, scenario);
|
|
console.log(`${scenario}/${mode}: ${mode === 'healthy' ? 'healthy scenario passed' : 'seeded regression caught at the expected assertion'}`);
|
|
}
|
|
JS
|
|
}
|
|
for scenario in ${CASES[@]+"${CASES[@]}"}; do
|
|
for mode in "${MODES[@]}"; do run_case "$scenario" "$mode"; done
|
|
done
|
|
for scenario in ${EFFECT_CASES[@]+"${EFFECT_CASES[@]}"}; do
|
|
for mode in "${EFFECT_MODES[@]}"; do run_case "$scenario" "$mode"; done
|
|
done
|
|
for scenario in ${PASSING_CASES[@]+"${PASSING_CASES[@]}"}; do
|
|
run_case "$scenario" healthy
|
|
done
|