Guard the deps-requiring CLI tests so local npm test is green every…
#20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # REQ-UPSTREAM-CONTRACT-TESTS — the pi upgrade gate. | |
| # | |
| # Asserts the assumptions pi-dispatch pins about pi, and fails the build when one stops holding. | |
| # CONST-PI-VERSION-PINNED makes an upgrade an explicit commit that edits a version string, so this | |
| # workflow is what turns that commit into a gate rather than a hope. | |
| # | |
| # Every assertion below covers a failure that is SILENT. A crash reports itself and needs no test; | |
| # these do not. Each maps to a point where a claim about pi was verified, believed, and later refuted: | |
| # - "appendSystemPrompt composes with file discovery" -> it REPLACES it, via a `??` | |
| # - "createAgentSession reloads the loader you pass" -> only one it built itself | |
| # - "-nc is a flag you remember" -> it is an object you forget to build | |
| # - "pi never throws" -> its own JSDoc says otherwise | |
| # Each was found by reading source, not by running code. These tests are what catch the next one. | |
| name: pi upgrade check | |
| on: | |
| push: | |
| paths: | |
| - "image/**" | |
| - "worker/**" | |
| - "guardrails/**" | |
| - "package.json" | |
| - ".github/workflows/pi-upgrade-check.yml" | |
| pull_request: | |
| paths: | |
| - "image/**" | |
| - "worker/**" | |
| - "guardrails/**" | |
| - "package.json" | |
| - ".github/workflows/pi-upgrade-check.yml" | |
| schedule: | |
| # Weekly: pi ships breaking changes between minors and its HEAD moved within 24h of this | |
| # project's design being written. A pin that is never exercised rots silently. | |
| - cron: "0 6 * * 1" | |
| workflow_dispatch: | |
| jobs: | |
| version-pin: | |
| name: pins are exact (CONST-PI-VERSION-PINNED) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: No floating ranges on pi packages | |
| run: | | |
| # A floating range turns a silent upstream minor into every queued job becoming a | |
| # no-op with no signal, because the queue still reports success. | |
| if grep -rnE '@earendil-works/pi-[a-z-]+@[\^~]|"@earendil-works/pi-[a-z-]+":[[:space:]]*"[\^~]' image/ package.json; then | |
| echo "::error::Floating range on a pi package. CONST-PI-VERSION-PINNED requires an exact pin." | |
| exit 1 | |
| fi | |
| echo "OK: pi pinned exactly" | |
| - name: Base image is pinned by digest | |
| run: | | |
| # Same reasoning, different vendor. A floating base tag is the identical silent break. | |
| if ! grep -qE '^FROM .*@sha256:[0-9a-f]{64}' image/Dockerfile; then | |
| echo "::error::Base image is not digest-pinned in image/Dockerfile." | |
| exit 1 | |
| fi | |
| echo "OK: base pinned by digest" | |
| contract-tests: | |
| name: pinned assumptions still hold (offline, no API key) | |
| runs-on: ubuntu-latest | |
| services: | |
| # A real Valkey backs the queue integration test (enqueue + dedup). AOF is not needed for a | |
| # test run; the point is a genuine Redis-protocol server, not the fake. | |
| valkey: | |
| image: valkey/valkey:8 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "valkey-cli ping" --health-interval 10s --health-timeout 3s --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: ".nvmrc" # pi requires >=22.19.0; engine-strict enforces it | |
| - run: npm ci --no-audit --no-fund | |
| # The scariest assertions in this project are FREE: every trap lives at the resource-loader | |
| # boundary (pure) or at before_agent_start (which fires strictly before any provider HTTP | |
| # call). No API key, no tokens, no flake, no excuse not to run them on every build. | |
| # | |
| # PI_DISPATCH_REQUIRE_*_TESTS=1 turns a skip into a hard failure. A skipped assertion is an | |
| # UNVERIFIED assertion, and "skipped = pass" is precisely the reasoning that lets a | |
| # guardrail-less agent ship green. VALKEY_TEST_URL activates the queue integration test. | |
| - name: Contract tests -- guardrails, -nc, exit codes, env allowlist, queue (all required) | |
| env: | |
| PI_DISPATCH_REQUIRE_LOADER_TESTS: "1" | |
| PI_DISPATCH_REQUIRE_WORKER_TESTS: "1" | |
| VALKEY_TEST_URL: "redis://127.0.0.1:6379" | |
| run: npm test | |
| image: | |
| name: the job image holds its contract | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build | |
| run: docker build -f image/Dockerfile -t pi-job:ci . | |
| # --- INT-SDK-SESSION-OPTIONS --- | |
| # `pi --mode print` does not exist: --mode accepts text|json|rpc, and --print/-p is a separate | |
| # boolean. --help rather than a real prompt, because a real prompt needs a paid API key and a | |
| # contract test that costs money is a contract test that gets disabled. | |
| # --entrypoint is required: ENTRYPOINT is the runner and ignores CMD, so `docker run img pi ...` | |
| # would silently run the runner instead of pi and the assertion would test nothing. | |
| - name: pi -p is still a flag | |
| run: docker run --rm --entrypoint pi pi-job:ci -p --help >/dev/null | |
| # --- CONST-ISOLATION-CONTAINER-PER-JOB --- | |
| # --cap-drop=ALL is the enforcement surface. Read the effective capability set directly rather | |
| # than install libcap just to ask. | |
| - name: No capabilities under --cap-drop=ALL | |
| run: | | |
| caps=$(docker run --rm --cap-drop=ALL --security-opt no-new-privileges \ | |
| --entrypoint sh pi-job:ci -c 'grep ^CapEff /proc/self/status' | awk '{print $2}') | |
| [ "$caps" = "0000000000000000" ] || { echo "::error::Container retains capabilities: $caps"; exit 1; } | |
| echo "OK: CapEff=$caps" | |
| - name: Runs as a non-root user | |
| run: | | |
| uid=$(docker run --rm --entrypoint id pi-job:ci -u) | |
| [ "$uid" != "0" ] || { echo "::error::Job container runs as root."; exit 1; } | |
| # --- INT-CONTAINER-JOB-INPUTS --- | |
| # /job:ro is what makes CONST-ISSUE-TEXT-IS-DATA enforceable by filesystem permission rather | |
| # than by asking nicely. It is a security boundary, so assert the kernel enforces it. | |
| - name: /job is genuinely read-only from inside | |
| run: | | |
| mkdir -p fixture/pi | |
| echo "x" > fixture/pi/APPEND_SYSTEM.md | |
| if docker run --rm --cap-drop=ALL -v "$PWD/fixture:/job:ro" --entrypoint sh pi-job:ci \ | |
| -c 'echo pwned > /job/pi/APPEND_SYSTEM.md' 2>/dev/null; then | |
| echo "::error::/job is writable. The agent can rewrite its own instructions." | |
| exit 1 | |
| fi | |
| echo "OK: /job:ro enforced" | |
| # --- INT-CONTAINER-RUNTIME-CONTRACT --- | |
| # pi lazily creates ~/.pi/agent and writes auth.json on the FIRST credential operation. If the | |
| # dir is root-owned the job dies EACCES at runtime, on a path nothing in the Dockerfile hints | |
| # at. COPY --chown does not fix it (it skips auto-created parents), so assert the real thing. | |
| - name: The agent dir is writable by the runtime user | |
| run: docker run --rm --entrypoint sh pi-job:ci -c 'touch "$HOME/.pi/agent/auth.json" && rm "$HOME/.pi/agent/auth.json"' | |
| - name: The guardrails are baked where the runner reads them | |
| run: | | |
| docker run --rm --entrypoint grep pi-job:ci -q "pi-dispatch-guardrails-v1" /opt/pi-dispatch/HARD_RULES.md \ | |
| || { echo "::error::Guardrails sentinel missing from /opt/pi-dispatch/HARD_RULES.md"; exit 1; } | |
| # --- DES-PLAYWRIGHT-CLI-NOT-CHROME-DEVTOOLS / REQ-FRONTEND-VISUAL-VERIFY --- | |
| # Root-installed Chromium lands in /root/.cache/ms-playwright, invisible to the non-root user | |
| # CONST-ISOLATION-CONTAINER-PER-JOB requires. --shm-size, NOT --ipc=host: Playwright recommends | |
| # the latter, but it shares the HOST IPC namespace with a container running adversarial-input | |
| # agent code. The crash it prevents is caused by a 64MB /dev/shm, so fix that instead. | |
| # | |
| # This asserts REAL rendering, not merely "a PNG exists". An earlier version opened a file:// | |
| # URL -- which playwright-cli BLOCKS by default -- so it screenshotted about:blank and passed | |
| # on a blank page: green and vacuous. Two corrections: (1) navigate over http, which is what a | |
| # frontend job actually does (a dev server), and which is not blocked; (2) diff the screenshot | |
| # against a blank baseline, so a blank or all-tofu page FAILS. A non-empty PNG proves nothing; | |
| # a PNG that differs from about:blank proves the browser painted the page. | |
| # Two assertions that a NON-VACUOUS test needs, both learned the hard way: | |
| # - The page is served over HTTP (a dev server, what a frontend job hits), not file:// -- | |
| # playwright-cli BLOCKS file:// by default, so the old test screenshotted about:blank and | |
| # passed green on a blank page. | |
| # - Correctness is proven by finding a MARKER in the page snapshot, not by diffing the | |
| # screenshot against blank. When the server is down, `open` renders Chrome's own | |
| # connection-refused error page, which ALSO differs from blank -- so a pixel diff passes on | |
| # an error page. The DOM snapshot containing our marker proves the real page loaded. | |
| # - The page is passed via env, not a bind mount: mounts are the one thing that behaves | |
| # differently across host OSes, and this must not depend on that. | |
| # Both directions verified locally: a dead server makes the marker absent and the step fails. | |
| - name: Chromium renders a real page as non-root | |
| run: | | |
| docker run --rm --init --cap-drop=ALL --security-opt no-new-privileges --shm-size=1g \ | |
| -e PAGE='<html><body style="background:#f00"><h1 style="color:#fff">RENDER-CHECK-MARKER</h1></body></html>' \ | |
| --entrypoint sh pi-job:ci -c \ | |
| 'node -e "require(\"http\").createServer((_,r)=>{r.writeHead(200,{\"content-type\":\"text/html\"});r.end(process.env.PAGE)}).listen(8099)" & sleep 1; | |
| playwright-cli open http://localhost:8099 >/dev/null 2>&1; | |
| playwright-cli snapshot 2>&1 | grep -q RENDER-CHECK-MARKER || { echo "page did not load — chromium could not navigate/render"; exit 1; }; | |
| playwright-cli screenshot --filename /tmp/s.png >/dev/null 2>&1; | |
| test -s /tmp/s.png || { echo "no screenshot produced"; exit 1; }' \ | |
| || { echo "::error::Chromium did not render a real page. Check PLAYWRIGHT_BROWSERS_PATH (build+run), PLAYWRIGHT_MCP_BROWSER, PLAYWRIGHT_MCP_SANDBOX, fonts."; exit 1; } | |
| # Fonts: a weaker, independent check. The render diff above already catches a totally blank | |
| # page; this catches "fonts package silently dropped" specifically. Neither proves LEGIBILITY | |
| # (that needs OCR, which is not worth it here) -- honest about what it does and does not show. | |
| - name: Fonts are installed | |
| run: | | |
| n=$(docker run --rm --entrypoint sh pi-job:ci -c 'fc-list | wc -l') | |
| [ "$n" -gt 0 ] || { echo "::error::No fonts. Chromium renders tofu; REQ-FRONTEND-VISUAL-VERIFY degraded."; exit 1; } | |
| echo "OK: $n fonts" |