yoke/tests/brute_verdict_sandbox.sh

260 lines
6.2 KiB
Bash
Raw Permalink Normal View History

2026-03-05 09:14:50 +00:00
#!/usr/bin/env bash
# Integration test: brute-mode verdict preservation through Docker sandbox.
#
# Validates that yoke correctly constructs the docker command, volume mounts
# work (container writes to .loop/ are visible on host), and verdict.md
# survives brute retries through the containerized path.
#
# Prerequisites:
# - docker daemon running
# - omp-sandbox:latest image available
2026-03-05 09:14:50 +00:00
# - cargo (to build yoke)
#
# Usage:
# bash tests/brute_verdict_sandbox.sh
set -euo pipefail
TEST_IMAGE="yoke-test-verdict:latest"
TMPDIR_ROOT=""
cleanup() {
if [[ -n "$TMPDIR_ROOT" && -d "$TMPDIR_ROOT" ]]; then
rm -rf "$TMPDIR_ROOT"
fi
docker rmi "$TEST_IMAGE" >/dev/null 2>&1 || true
}
trap cleanup EXIT
# ── Prerequisites ──
if ! command -v docker &>/dev/null; then
echo "SKIP: docker not found on PATH"
exit 0
fi
if ! docker image inspect omp-sandbox:latest &>/dev/null; then
echo "SKIP: omp-sandbox:latest image not found"
2026-03-05 09:14:50 +00:00
exit 0
fi
if ! command -v cargo &>/dev/null; then
echo "SKIP: cargo not found on PATH"
exit 0
fi
# ── Build yoke ──
echo "Building yoke..."
cargo build --quiet
YOKE_BIN="$(pwd)/target/debug/yoke"
if [[ ! -x "$YOKE_BIN" ]]; then
echo "FAIL: yoke binary not found at $YOKE_BIN"
exit 1
fi
# ── Create temp workspace ──
TMPDIR_ROOT="$(mktemp -d)"
PROJECT="$TMPDIR_ROOT/project"
mkdir -p "$PROJECT"
# ── Write mock-omp script ──
2026-03-05 09:14:50 +00:00
cat > "$TMPDIR_ROOT/mock-omp" <<'MOCK'
2026-03-05 09:14:50 +00:00
#!/usr/bin/env bash
set -euo pipefail
# Extract the prompt from -p argument
PROMPT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p) PROMPT="$2"; shift 2 ;;
*) shift ;;
esac
done
if echo "$PROMPT" | grep -q "protocol.md"; then
# ── Agent mode ──
COUNTER_FILE=".loop/.agent-calls"
N=0
if [[ -f "$COUNTER_FILE" ]]; then
N=$(cat "$COUNTER_FILE")
fi
N=$((N + 1))
echo "$N" > "$COUNTER_FILE"
# Witness: snapshot of verdict.md at the moment the agent runs
cp .loop/verdict.md ".loop/.witness-${N}"
# Write STATUS: DONE so plan loop exits
printf 'STATUS: DONE\n\n## Stage 1 — Minimal\nDone.\n' > .loop/notes.md
elif echo "$PROMPT" | grep -q "judge.md"; then
# ── Judge mode ──
COUNTER_FILE=".loop/.judge-calls"
N=0
if [[ -f "$COUNTER_FILE" ]]; then
N=$(cat "$COUNTER_FILE")
fi
N=$((N + 1))
echo "$N" > "$COUNTER_FILE"
if [[ "$N" -eq 1 ]]; then
printf 'VERDICT: FAIL\n\nFeature is broken — step counter never increments.' > .loop/verdict.md
else
printf 'VERDICT: PASS\n\nAll checks passed.' > .loop/verdict.md
fi
fi
exit 0
MOCK
chmod +x "$TMPDIR_ROOT/mock-omp"
2026-03-05 09:14:50 +00:00
# ── Build test Docker image ──
echo "Building test image $TEST_IMAGE..."
docker build -t "$TEST_IMAGE" -f- "$TMPDIR_ROOT" <<'DOCKERFILE'
FROM omp-sandbox:latest
COPY --chmod=755 mock-omp /usr/local/bin/omp
2026-03-05 09:14:50 +00:00
DOCKERFILE
# ── Set up project directory ──
LOOP_DIR="$PROJECT/.loop"
mkdir -p "$LOOP_DIR"
cat > "$LOOP_DIR/protocol.md" <<'EOF'
# Protocol
You are inside an automated loop.
## Files
| File | Access | Purpose |
|---|---|---|
| `.loop/protocol.md` | read | These instructions. |
| `.loop/plan.md` | read | The feature plan. |
| `.loop/judge.md` | read | What the judge tests. |
| `.loop/notes.md` | read+write | Your scratchpad. |
| `.loop/verdict.md` | read | Previous judge verdict. |
| `.loop/guard-results.md` | read | Guard results. |
| `.loop/yoke.conf` | read | Configuration. |
## Per-Iteration Steps
1. Read plan.
2. Read notes.
3. Read verdict.
4. Implement one stage.
5. Update notes with STATUS line.
6. Exit.
## STATUS Signaling
First line of notes.md: `STATUS: IN_PROGRESS` or `STATUS: DONE`.
EOF
cat > "$LOOP_DIR/plan.md" <<'EOF'
## Stage 1 — Minimal
Implement the feature.
EOF
cat > "$LOOP_DIR/judge.md" <<'EOF'
# Judge
Verify the feature works.
## Verdict
Write VERDICT: PASS or VERDICT: FAIL to .loop/verdict.md.
EOF
cat > "$LOOP_DIR/yoke.conf" <<EOF
image $TEST_IMAGE
allow .
EOF
touch "$LOOP_DIR/notes.md"
touch "$LOOP_DIR/verdict.md"
touch "$LOOP_DIR/guard-results.md"
# ── Git init ──
git -C "$PROJECT" init --quiet
echo "seed" > "$PROJECT/dummy.txt"
git -C "$PROJECT" add dummy.txt
GIT_CONFIG_NOSYSTEM=1 \
git -C "$PROJECT" \
-c user.name=test \
-c user.email=test@test \
commit --quiet -m "init"
# ── Run yoke (sandboxed — no --no-sandbox flag) ──
echo "Running yoke in sandbox mode..."
set +e
OUTPUT=$(cd "$PROJECT" && "$YOKE_BIN" run 2>&1)
YOKE_EXIT=$?
set -e
# ── Assertions ──
PASS=true
# 1. witness-1 should be empty: no verdict exists before first agent run
if [[ ! -f "$LOOP_DIR/.witness-1" ]]; then
echo "FAIL: .witness-1 does not exist (agent was never called)"
PASS=false
else
WITNESS_1=$(cat "$LOOP_DIR/.witness-1")
if [[ -n "$WITNESS_1" ]]; then
echo "FAIL: witness-1 should be empty (no prior verdict), got: '$WITNESS_1'"
PASS=false
else
echo "OK: witness-1 is empty (no prior verdict)"
fi
fi
# 2. witness-2 must contain VERDICT: FAIL — agent saw judge's feedback
if [[ ! -f "$LOOP_DIR/.witness-2" ]]; then
echo "FAIL: .witness-2 does not exist (agent was not called a second time)"
echo " Agent calls: $(cat "$LOOP_DIR/.agent-calls" 2>/dev/null || echo 'N/A')"
echo " Judge calls: $(cat "$LOOP_DIR/.judge-calls" 2>/dev/null || echo 'N/A')"
echo " Yoke output:"
echo "$OUTPUT" | sed 's/^/ /'
PASS=false
else
WITNESS_2=$(cat "$LOOP_DIR/.witness-2")
if echo "$WITNESS_2" | grep -q "VERDICT: FAIL"; then
echo "OK: witness-2 contains 'VERDICT: FAIL' (agent saw judge feedback)"
else
echo "FAIL: witness-2 should contain 'VERDICT: FAIL', got: '$WITNESS_2'"
PASS=false
fi
fi
# 3. yoke exits 0 — judge eventually said PASS
if [[ "$YOKE_EXIT" -eq 0 ]]; then
echo "OK: yoke exited 0 (judge said PASS)"
else
echo "FAIL: yoke exited $YOKE_EXIT (expected 0)"
echo " Yoke output:"
echo "$OUTPUT" | sed 's/^/ /'
PASS=false
fi
# ── Summary ──
if [[ "$PASS" == true ]]; then
echo ""
echo "PASS: brute verdict preserved through Docker sandbox"
exit 0
else
echo ""
echo "FAIL: one or more assertions failed"
exit 1
fi