Skip to content

Commit 4c4010d

Browse files
committed
test: move guard E2E from CI to a local runner script
The GitHub Actions job was too slow to keep (a ~735 MB ONNX model download plus two image builds per run). docker/piguard-e2e.sh now provisions the same stack locally (build-if-missing, health wait, env-gated test, always-tear-down) with an optional --linux flag that runs the test binary inside a container for full socket-mode coverage (the host socket subtest skips on macOS because unix sockets do not cross the Docker Desktop VM boundary). Verified locally: host run passes, container run passes all 6 subtests including socket mode.
1 parent 597a439 commit 4c4010d

4 files changed

Lines changed: 123 additions & 102 deletions

File tree

.github/workflows/piguard-e2e.yml

Lines changed: 0 additions & 96 deletions
This file was deleted.

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,10 @@ ODEK_E2E=true go test -v -count=1 ./cmd/odek/ -run "TestMCPE2E_"
236236
# Sandbox integration tests (requires Docker)
237237
go test -v -count=1 ./cmd/odek/ -run "TestSandbox"
238238

239-
# PIGuard sidecar E2E (requires the docker piguard stack running;
240-
# runs in CI via .github/workflows/piguard-e2e.yml)
241-
ODEK_E2E_GUARD=1 go test -v -count=1 ./internal/guard/ -run "TestE2E_"
239+
# PIGuard sidecar E2E (local only — too heavy for CI; provisions the
240+
# docker stack, runs the env-gated test, tears down. Use --linux on macOS
241+
# for full socket-mode coverage.)
242+
docker/piguard-e2e.sh
242243

243244
# Fuzz soaks (extractJSON, SKILL.md parsing, session loading)
244245
go test -fuzz=FuzzExtractJSON -fuzztime=30s ./internal/memory/extended/

docker/piguard-e2e.sh

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#!/usr/bin/env bash
2+
# Local E2E runner for the PIGuard prompt-injection sidecar.
3+
#
4+
# Builds (if needed) and starts the daemon + gateway exactly like the
5+
# docker-compose stack, then runs the env-gated E2E test in
6+
# internal/guard/piguard_e2e_test.go against them, and tears everything
7+
# down afterwards.
8+
#
9+
# Why a script and not CI: the stack is heavy (a ~735 MB ONNX model plus
10+
# two image builds), which made the GitHub Actions job too slow to keep.
11+
# Run this before merging changes that touch internal/guard or the
12+
# docker piguard stack.
13+
#
14+
# Requirements: Docker, Go. First run needs the model exported once:
15+
# docker/piguard/download-model.sh
16+
#
17+
# Usage:
18+
# docker/piguard-e2e.sh build images if missing, run E2E, tear down
19+
# docker/piguard-e2e.sh --build force image rebuild
20+
# docker/piguard-e2e.sh --linux also run the test binary inside a Linux
21+
# container (full socket-mode coverage;
22+
# on macOS the host socket subtest skips
23+
# because unix sockets do not cross the
24+
# Docker Desktop VM boundary)
25+
set -euo pipefail
26+
27+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
28+
DOCKER_DIR="${REPO_ROOT}/docker"
29+
MODEL_DIR="${DOCKER_DIR}/piguard/models"
30+
SOCK_DIR="/tmp/piguard-e2e"
31+
NETWORK="piguard-e2e"
32+
BUILD=0
33+
LINUX=0
34+
for arg in "$@"; do
35+
case "$arg" in
36+
--build) BUILD=1 ;;
37+
--linux) LINUX=1 ;;
38+
*) echo "unknown flag: $arg" >&2; exit 2 ;;
39+
esac
40+
done
41+
42+
cleanup() {
43+
docker rm -f piguard-e2e-gateway piguard-e2e-daemon >/dev/null 2>&1 || true
44+
docker network rm "${NETWORK}" >/dev/null 2>&1 || true
45+
rm -rf "${SOCK_DIR}"
46+
}
47+
trap cleanup EXIT
48+
49+
docker info >/dev/null 2>&1 || { echo "docker daemon is not running" >&2; exit 1; }
50+
51+
if [ ! -f "${MODEL_DIR}/model.onnx" ]; then
52+
echo "PIGuard model not found in ${MODEL_DIR}." >&2
53+
echo "Run ${DOCKER_DIR}/piguard/download-model.sh first (one-time, ~735 MB)." >&2
54+
exit 1
55+
fi
56+
57+
if [ "${BUILD}" = 1 ] || ! docker image inspect piguard:local >/dev/null 2>&1; then
58+
(cd "${DOCKER_DIR}" && docker compose --profile restricted build piguard)
59+
fi
60+
if [ "${BUILD}" = 1 ] || ! docker image inspect piguard-gateway:local >/dev/null 2>&1; then
61+
(cd "${DOCKER_DIR}" && docker compose --profile restricted build piguard-gateway)
62+
fi
63+
64+
cleanup # remove leftovers from a previous run
65+
mkdir -p "${SOCK_DIR}"
66+
docker network create "${NETWORK}" >/dev/null
67+
docker run -d --name piguard-e2e-daemon --network "${NETWORK}" \
68+
-v "${MODEL_DIR}:/models:ro" \
69+
-v "${SOCK_DIR}:/run/piguard" \
70+
piguard:local \
71+
--socket=/run/piguard/piguard.sock --model-dir=/models \
72+
--max-batch=32 --batch-wait=5ms >/dev/null
73+
docker run -d --name piguard-e2e-gateway --network "${NETWORK}" \
74+
-p 127.0.0.1:18080:8080 \
75+
-v "${SOCK_DIR}:/run/piguard" \
76+
piguard-gateway:local \
77+
--addr=:8080 --socket=/run/piguard/piguard.sock >/dev/null
78+
79+
echo "Waiting for gateway health..."
80+
healthy=0
81+
for _ in $(seq 1 90); do
82+
if curl -fsS http://127.0.0.1:18080/healthz >/dev/null 2>&1; then
83+
healthy=1
84+
break
85+
fi
86+
sleep 2
87+
done
88+
if [ "${healthy}" != 1 ]; then
89+
echo "=== daemon logs ===" >&2; docker logs piguard-e2e-daemon >&2 || true
90+
echo "=== gateway logs ===" >&2; docker logs piguard-e2e-gateway >&2 || true
91+
echo "gateway never became healthy" >&2
92+
exit 1
93+
fi
94+
95+
export ODEK_E2E_GUARD=1
96+
export PIGUARD_URL="http://127.0.0.1:18080/detect"
97+
export PIGUARD_SOCKET="${SOCK_DIR}/piguard.sock"
98+
99+
echo "Running guard E2E (host)..."
100+
(cd "${REPO_ROOT}" && go test ./internal/guard -run E2E -count=1 -v)
101+
102+
if [ "${LINUX}" = 1 ]; then
103+
echo "Running guard E2E inside a Linux container (socket mode covered)..."
104+
(cd "${REPO_ROOT}" && CGO_ENABLED=0 GOOS=linux go test -c -o /tmp/piguard-e2e/guard.test ./internal/guard)
105+
docker run --rm --network "${NETWORK}" \
106+
-e ODEK_E2E_GUARD=1 \
107+
-e PIGUARD_URL="http://piguard-e2e-gateway:8080/detect" \
108+
-e PIGUARD_SOCKET=/run/piguard/piguard.sock \
109+
-v "${SOCK_DIR}:/run/piguard" \
110+
-v /tmp/piguard-e2e:/out \
111+
debian:bookworm-slim /out/guard.test -test.run E2E -test.v
112+
fi
113+
114+
echo "PIGuard E2E passed."

internal/guard/piguard_e2e_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import (
1515
// default http://127.0.0.1:18080/detect), and
1616
// - the daemon's Unix socket is reachable on the host (env PIGUARD_SOCKET,
1717
// default /tmp/piguard-e2e/piguard.sock); the socket subtest is skipped
18-
// when the socket file does not exist.
18+
// when the socket file does not exist or is not connectable (on macOS,
19+
// unix sockets do not cross the Docker Desktop VM boundary).
1920
//
20-
// See .github/workflows/piguard-e2e.yml for how the stack is provisioned
21-
// in CI.
21+
// docker/piguard-e2e.sh provisions the stack, runs this test, and tears it
22+
// down. It was kept out of CI (the ~735 MB model + image builds make the
23+
// job too slow); run it locally before merging guard changes.
2224
const (
2325
e2eDefaultURL = "http://127.0.0.1:18080/detect"
2426
e2eDefaultSocket = "/tmp/piguard-e2e/piguard.sock"

0 commit comments

Comments
 (0)