name: replay-ui # Sanderling fuzzing sanderling's own replay UI. Dispatch-only: it takes minutes # and it is a demo of the product loop, not a merge gate. # # The shape is: produce a real trace, serve it with `sanderling replay`, then run # a spec against that UI. Any violation fails the job. Six of the seven # properties in replay-ui/sanderling/spec.ts are cross-panel agreements that hold # for any trace; the seventh is the stock noUncaughtExceptions. None of them # needs recalibrating when the fixture changes. on: workflow_dispatch: inputs: seed: description: seed for the dogfood run default: "3" max-steps: description: step budget for the dogfood run default: "80" permissions: contents: read jobs: dogfood: timeout-minutes: 45 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod cache: true - name: Set up bun uses: oven-sh/setup-bun@v2 with: bun-version: "1.3.13" # Pinned stable plus the AppArmor sysctl: the same setup ci.yml's browser # job needs to get headless Chrome up on ubuntu-latest. - name: Set up Chrome uses: browser-actions/setup-chrome@v1 with: chrome-version: stable - name: Allow Chrome under unprivileged user namespaces run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 - name: Verify headless Chrome starts run: | chrome --version chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage \ --dump-dom 'data:text/html,ok' # The UI the spec drives is the one embedded in this binary, so the build # has to come after any change to replay-ui/src. - name: Build sanderling run: make sanderling-web # A trace with a violation and uncaught exceptions in it, so the UI has # something to render in every panel the spec looks at. No # --exit-on-violation here: the run is the fixture, and stopping it at the # first violation would leave a four-step trace to fuzz. - name: Record a fixture trace run: | python3 -m http.server 8792 --bind 127.0.0.1 \ --directory test/browser/testdata/throwing & ready="" for _ in $(seq 1 30); do curl -sf http://127.0.0.1:8792/ >/dev/null && { ready=1; break; } sleep 1 done if [ -z "$ready" ]; then echo "the fixture http server never answered on 127.0.0.1:8792" >&2 exit 1 fi ./bin/sanderling test \ --platform web \ --spec test/browser/testdata/throwing/spec.ts \ --bundle-id http://127.0.0.1:8792/ \ --duration 5m --max-steps 25 --seed 7 \ --output runs/fixture - name: Serve the trace with sanderling replay run: | # Flags before the positional argument: Go's flag package stops # parsing at the first non-flag word. ./bin/sanderling replay --port 8793 --no-open runs/fixture & ready="" for _ in $(seq 1 30); do curl -sf http://127.0.0.1:8793/api/runs >/dev/null && { ready=1; break; } sleep 1 done if [ -z "$ready" ]; then echo "sanderling replay never served /api/runs on 127.0.0.1:8793" >&2 exit 1 fi run_id="$(ls runs/fixture | head -1)" echo "RUN_URL=http://127.0.0.1:8793/runs/$run_id/steps/1" >> "$GITHUB_ENV" curl -sf "http://127.0.0.1:8793/runs/$run_id/steps/1" >/dev/null # Inputs go through env rather than into the script text: a `${{ }}` is # substituted before bash ever sees the line, so a seed of `$(id)` would # run as a command. - name: Fuzz the replay UI run: | ./bin/sanderling test \ --platform web \ --spec replay-ui/sanderling/spec.ts \ --bundle-id "$RUN_URL" \ --duration 10m \ --max-steps "$MAX_STEPS" \ --seed "$SEED" \ --exit-on-violation \ --output runs/dogfood env: SEED: ${{ inputs.seed }} MAX_STEPS: ${{ inputs.max-steps }} - name: Summarise if: always() run: | { echo "### replay-ui dogfood" echo echo "- seed \`$SEED\`, budget $MAX_STEPS steps" for dir in runs/dogfood/*/; do [ -f "$dir/trace.jsonl" ] || continue steps=$(wc -l < "$dir/trace.jsonl" | tr -d ' ') violations=$(grep -c '"violations":\[' "$dir/trace.jsonl" || true) echo "- $steps steps recorded, $violations step(s) with violations" done } >> "$GITHUB_STEP_SUMMARY" env: SEED: ${{ inputs.seed }} MAX_STEPS: ${{ inputs.max-steps }} - name: Upload runs if: always() uses: actions/upload-artifact@v4 with: name: replay-ui-runs path: runs/ retention-days: 14