Order 20260926T070037Z-680859139 (.claude). A read-only, zero-model-call check any session starter can call first: exit 0 allows, exit 2 denies with one stderr line naming the rule and what is missing. - R4: PLAN.md holds exactly one gate line, <!-- gate: gjenfortelling=<time>; dekning=komplett; r1-proeve=<time|->; foerste-maaling=<time|->; retning=<name> --> - R3: the Logg table's Bevegelse (ja|nei) and Kvote (N%) columns. Deny at >= 10% of the weekly quota without movement since the last row that moved. - R6: cap 1 live session per repo (claude agents --json --cwd <repo>) until r1-proeve and foerste-maaling are both times, then 2. Night 23:00-07:00 or --night needs r1-proeve. Fail-closed: unmeasured rows, a missing claude binary, a non-JSON listing, duplicate or unknown keys and usage errors all deny. Measured: claude agents --cwd matches the directory a session was STARTED in, not its printed cwd, so --repo must be the main checkout root. Chose NOT to call it from board.sh --dispatch, because a dispatch line is generated before the start (a live count would be stale), and live-session reading is the process axis board.sh refuses by design. session-gate-selftest.sh: 0 -> 97 checks. It was red 91 of 95 before the script existed, then 97/97 green. Ten mutants all die. It is wired into npm test as the sixth suite, and the README badge goes from 1006 to 1103. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
408 lines
18 KiB
Bash
408 lines
18 KiB
Bash
#!/bin/bash
|
|
# session-gate-selftest.sh - prove session-gate.sh against fixture repos.
|
|
# ASCII only, bash 3.2 safe. Writes only under a mktemp directory.
|
|
#
|
|
# Every rule gets at least one repo that must be LET THROUGH beside the repos
|
|
# that must be REFUSED, because a gate proven only by refusals is a gate that
|
|
# refuses everything and proves nothing. The live-session count is fed from a
|
|
# fake `claude` on PATH, so the suite never depends on what happens to be
|
|
# running on the machine - and the fake records its argv, so the real call
|
|
# shape (`agents --json --cwd <physical path>`) is pinned, not assumed.
|
|
set -u
|
|
export LC_ALL=C
|
|
|
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
GATE="$DIR/session-gate.sh"
|
|
|
|
PASS=0; FAIL=0
|
|
check() { if [ "$2" -eq 0 ]; then PASS=$((PASS+1)); echo " ok - $1"; else FAIL=$((FAIL+1)); echo " FAIL - $1"; fi; }
|
|
|
|
T="$(mktemp -d)"
|
|
trap 'chmod -R u+rwx "$T" 2>/dev/null; rm -rf "$T"' EXIT
|
|
T="$(cd -P "$T" && pwd -P)"
|
|
|
|
FAKEBIN="$T/fakebin"
|
|
mkdir -p "$FAKEBIN"
|
|
cat > "$FAKEBIN/claude" <<'FAKE'
|
|
#!/bin/bash
|
|
printf '%s\n' "$*" >> "$FAKE_LOG"
|
|
if [ -n "${FAKE_AGENTS_FILE:-}" ]; then cat "$FAKE_AGENTS_FILE"; fi
|
|
exit "${FAKE_AGENTS_EXIT:-0}"
|
|
FAKE
|
|
chmod +x "$FAKEBIN/claude"
|
|
FAKE_LOG="$T/fake.log"
|
|
|
|
printf '[]\n' > "$T/ag0"
|
|
cat > "$T/ag1" <<'J'
|
|
[
|
|
{
|
|
"pid": 4242,
|
|
"id": "aaaa1111",
|
|
"cwd": "/x/demo/.claude/worktrees/w",
|
|
"kind": "background",
|
|
"startedAt": 1790406122536,
|
|
"sessionId": "aaaa1111-0000-0000-0000-000000000000",
|
|
"name": "demo-w",
|
|
"status": "busy",
|
|
"state": "working"
|
|
}
|
|
]
|
|
J
|
|
printf '[{"id":"a","cwd":"/x/demo","kind":"background","sessionId":"a-1"},{"cwd":"/x/demo","kind":"interactive","sessionId":"b-2"}]\n' > "$T/ag2"
|
|
printf 'hello\n' > "$T/agjunk"
|
|
: > "$T/agempty"
|
|
|
|
# run_gate <args...>: env knobs NOW (clock, default 12:00; NOW_UNSET=1 leaves
|
|
# the variable out), AG (agents fixture, default ag0), AGX (fake exit),
|
|
# GPATH (PATH for the gate, default fakebin first).
|
|
run_gate() {
|
|
: > "$FAKE_LOG"
|
|
if [ "${NOW_UNSET:-0}" = 1 ]; then
|
|
OUT="$(env -u SESSION_GATE_NOW PATH="${GPATH:-$FAKEBIN:/usr/bin:/bin}" FAKE_LOG="$FAKE_LOG" \
|
|
FAKE_AGENTS_FILE="${AG:-$T/ag0}" FAKE_AGENTS_EXIT="${AGX:-0}" \
|
|
/bin/bash "$GATE" "$@" 2>"$T/err")"; RC=$?
|
|
else
|
|
OUT="$(env PATH="${GPATH:-$FAKEBIN:/usr/bin:/bin}" SESSION_GATE_NOW="${NOW-12:00}" FAKE_LOG="$FAKE_LOG" \
|
|
FAKE_AGENTS_FILE="${AG:-$T/ag0}" FAKE_AGENTS_EXIT="${AGX:-0}" \
|
|
/bin/bash "$GATE" "$@" 2>"$T/err")"; RC=$?
|
|
fi
|
|
ERR="$(cat "$T/err")"
|
|
}
|
|
# denied <rule>: exit 2, nothing on stdout, exactly ONE stderr line naming the rule.
|
|
denied() {
|
|
[ "$RC" -eq 2 ] && [ -z "$OUT" ] && [ "$(printf '%s\n' "$ERR" | grep -c .)" -eq 1 ] \
|
|
&& printf '%s' "$ERR" | grep -q "$1"
|
|
}
|
|
# allowed: exit 0, one ALLOW line on stdout, stderr empty.
|
|
allowed() {
|
|
[ "$RC" -eq 0 ] && [ -z "$ERR" ] && printf '%s' "$OUT" | grep -q '^session-gate: ALLOW'
|
|
}
|
|
|
|
GATE_OK='<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
GATE_R1='<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=2026-09-26T08:00; foerste-maaling=-; retning=demo -->'
|
|
GATE_R1M='<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=2026-09-26T08:00; foerste-maaling=2026-09-27T09:00+02:00; retning=demo -->'
|
|
LOG_OK='## Logg
|
|
| Dato | Hva | Bevegelse | Kvote |
|
|
|---|---|---|---|
|
|
| 26.09 | x | ja: first | 3% |
|
|
| 26.09 | y | nei: nothing moved | 4% |'
|
|
|
|
# mkplan <name> <gate-line> <logg-block>: a repo dir holding PLAN.md.
|
|
mkplan() {
|
|
mkdir -p "$T/$1"
|
|
printf '# PLAN - %s\n%s\n\n## Maal\nSomething.\n\n%s\n' "$1" "$2" "$3" > "$T/$1/PLAN.md"
|
|
}
|
|
|
|
echo "session-gate-selftest"
|
|
|
|
# --- 0. CLI, help and the known-positive baseline --------------------------
|
|
run_gate -h
|
|
[ "$RC" -eq 0 ]; check "-h exits 0" $?
|
|
printf '%s' "$OUT" | grep -q 'checks that the fields are filled in, not that they were thought through'
|
|
check "help states the caveat: filled in, not thought through" $?
|
|
printf '%s' "$OUT" | grep -q 'retelling is the only control with real content'
|
|
check "help states the caveat: the retelling is the only control with real content" $?
|
|
printf '%s' "$OUT" | grep -q '0 = allow' && printf '%s' "$OUT" | grep -q '2 = deny'
|
|
check "help documents exit 0 = allow and 2 = deny" $?
|
|
|
|
run_gate
|
|
denied usage; check "no arguments: deny (exit 2, one line)" $?
|
|
run_gate --repo
|
|
denied usage; check "--repo without a value: deny" $?
|
|
run_gate --repo "$T" --bogus
|
|
denied usage; check "unknown argument: deny" $?
|
|
mkplan base "$GATE_OK" "$LOG_OK"
|
|
run_gate --repo "$T/base" --kind other
|
|
denied usage; check "--kind outside build|read: deny" $?
|
|
run_gate --repo "$T/does-not-exist"
|
|
denied repo; check "--repo that does not exist: deny" $?
|
|
|
|
run_gate --repo "$T/base"
|
|
allowed; check "KNOWN-POSITIVE: complete PLAN.md, 0 live sessions, daytime -> allow" $?
|
|
run_gate --repo "$T/base" --kind build
|
|
allowed; check "explicit --kind build on the same repo -> allow" $?
|
|
|
|
LC_ALL=C grep -q '[^ -~ ]' "$GATE"; [ $? -eq 1 ]
|
|
check "session-gate.sh is ASCII-only" $?
|
|
[ -s "$GATE" ] && { grep -v '^[[:space:]]*#' "$GATE" | grep -Eq '(^|[^A-Za-z_-])(jq|python3?|node)([^A-Za-z_-]|$)'; [ $? -eq 1 ]; }
|
|
check "no jq/python/node in executable lines (zero dependencies)" $?
|
|
printf 'x=1\njq .\n' | grep -v '^[[:space:]]*#' | grep -Eq '(^|[^A-Za-z_-])(jq|python3?|node)([^A-Za-z_-]|$)'
|
|
check "KNOWN-POSITIVE: the dependency grep finds a planted jq call" $?
|
|
|
|
# --- 1. PLAN.md itself: fail-closed on absence and unreadability -----------
|
|
mkdir -p "$T/noplan"
|
|
run_gate --repo "$T/noplan"
|
|
denied PLAN.md; check "no PLAN.md: deny, naming PLAN.md" $?
|
|
|
|
mkdir -p "$T/lower"
|
|
printf '%s\n\n%s\n' "$GATE_OK" "$LOG_OK" > "$T/lower/plan.md"
|
|
[ -f "$T/lower/PLAN.md" ] && echo " note - case-insensitive volume: [ -f PLAN.md ] is TRUE for plan.md here"
|
|
run_gate --repo "$T/lower"
|
|
denied PLAN.md; check "only plan.md (wrong case): deny - the name is matched byte for byte" $?
|
|
|
|
mkplan unread "$GATE_OK" "$LOG_OK"
|
|
chmod 000 "$T/unread/PLAN.md"
|
|
run_gate --repo "$T/unread"
|
|
denied PLAN.md; check "unreadable PLAN.md: deny" $?
|
|
chmod 644 "$T/unread/PLAN.md"
|
|
|
|
mkdir -p "$T/plandir/PLAN.md"
|
|
run_gate --repo "$T/plandir"
|
|
denied PLAN.md; check "PLAN.md is a directory: deny" $?
|
|
|
|
mkdir -p "$T/emptyplan"; : > "$T/emptyplan/PLAN.md"
|
|
run_gate --repo "$T/emptyplan"
|
|
denied R4; check "empty PLAN.md: deny on R4 (no gate line)" $?
|
|
|
|
mkdir -p "$T/linked-target"
|
|
printf '%s\n\n%s\n' "$GATE_OK" "$LOG_OK" > "$T/linked-target/real-plan.md"
|
|
ln -s real-plan.md "$T/linked-target/PLAN.md"
|
|
run_gate --repo "$T/linked-target"
|
|
allowed; check "symlinked PLAN.md is read through the link -> allow" $?
|
|
|
|
# --- 2. R4: the retelling and the coverage table --------------------------
|
|
mkplan r4-none "" "$LOG_OK"
|
|
run_gate --repo "$T/r4-none"
|
|
denied R4; check "R4: no gate line: deny" $?
|
|
|
|
mkplan r4-two "$GATE_OK
|
|
$GATE_OK" "$LOG_OK"
|
|
run_gate --repo "$T/r4-two"
|
|
denied R4; check "R4: two gate lines (ambiguous): deny" $?
|
|
|
|
mkplan r4-prose "See the form \`$GATE_OK\` in the docs." "$LOG_OK"
|
|
run_gate --repo "$T/r4-prose"
|
|
denied R4; check "R4: a gate line quoted mid-sentence is prose, not a gate line: deny" $?
|
|
|
|
r4() { # r4 <name> <gate-line>: build and run
|
|
mkplan "$1" "$2" "$LOG_OK"; run_gate --repo "$T/$1"
|
|
}
|
|
r4 r4-nogj '<!-- gate: dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: gjenfortelling missing: deny" $?
|
|
r4 r4-gjdash '<!-- gate: gjenfortelling=-; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: gjenfortelling=- (no yes yet): deny" $?
|
|
r4 r4-gjdate '<!-- gate: gjenfortelling=2026-09-26; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: gjenfortelling with a date but no time: deny" $?
|
|
r4 r4-gjjunk '<!-- gate: gjenfortelling=yes; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: gjenfortelling=yes (not a time): deny" $?
|
|
r4 r4-gjmonth '<!-- gate: gjenfortelling=2026-13-01T10:00; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: gjenfortelling in month 13: deny" $?
|
|
r4 r4-gjhour '<!-- gate: gjenfortelling=2026-09-26T24:10; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: gjenfortelling at hour 24: deny" $?
|
|
r4 r4-dekdel '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=delvis; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: dekning=delvis: deny" $?
|
|
r4 r4-dekcase '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=Komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: dekning=Komplett (exact token only): deny" $?
|
|
r4 r4-dekpre '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett2; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: dekning=komplett2 (prefix is not the token): deny" $?
|
|
r4 r4-nodek '<!-- gate: gjenfortelling=2026-09-26T07:10; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: dekning missing: deny" $?
|
|
r4 r4-noret '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning= -->'
|
|
denied R4; check "R4: retning empty: deny" $?
|
|
r4 r4-retdash '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=- -->'
|
|
denied R4; check "R4: retning=- : deny" $?
|
|
r4 r4-nor1 '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: r1-proeve key missing (must be written, - if not yet): deny" $?
|
|
r4 r4-r1junk '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=ja; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: r1-proeve=ja (not a time or -): deny" $?
|
|
r4 r4-nomaal '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=-; retning=demo -->'
|
|
denied R4; check "R4: foerste-maaling key missing: deny" $?
|
|
r4 r4-typo '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=-; r1-prove=2026-09-26T08:00; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: an unknown key (typo r1-prove beside r1-proeve) is refused, not ignored" $?
|
|
r4 r4-repeat '<!-- gate: gjenfortelling=2026-09-26T07:10; dekning=komplett; r1-proeve=-; r1-proeve=2026-09-26T08:00; foerste-maaling=-; retning=demo -->'
|
|
denied R4; check "R4: a repeated key (which one counts?) is refused" $?
|
|
r4 r4-okz '<!-- gate:gjenfortelling=2026-09-26T07:10:00Z; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=demo -->'
|
|
allowed; check "R4 control: seconds and Z suffix -> allow" $?
|
|
r4 r4-okoff '<!-- gate: gjenfortelling=2026-09-26T07:10+02:00; dekning=komplett; r1-proeve=-; foerste-maaling=-; retning=ny retning 2 -->'
|
|
allowed; check "R4 control: +02:00 offset and a retning with spaces -> allow" $?
|
|
|
|
# --- 3. R3: movement per quota share, over PLAN.md's Logg -----------------
|
|
r3() { # r3 <name> <logg-block>
|
|
mkplan "$1" "$GATE_OK" "$2"; run_gate --repo "$T/$1"
|
|
}
|
|
HDR='## Logg
|
|
| Dato | Hva | Bevegelse | Kvote |
|
|
|---|---|---|---|'
|
|
r3 r3-ten "$HDR
|
|
| 1 | a | ja | 5% |
|
|
| 2 | b | nei | 6% |
|
|
| 3 | c | nei | 4% |"
|
|
denied R3; check "R3: 10% without movement since the last movement: deny (threshold is >= 10)" $?
|
|
printf '%s' "$ERR" | grep -q '10'
|
|
check "R3: the denial states the sum" $?
|
|
r3 r3-nine5 "$HDR
|
|
| 1 | a | nei | 4,5% |
|
|
| 2 | b | nei | 5 % |"
|
|
allowed; check "R3 control: 9.5% (comma decimal, spaced percent) -> allow" $?
|
|
r3 r3-before "$HDR
|
|
| 1 | a | nei | 50% |
|
|
| 2 | b | ja: moved | 1% |
|
|
| 3 | c | nei | 3% |"
|
|
allowed; check "R3 control: rows BEFORE the last movement do not count -> allow" $?
|
|
r3 r3-lastja "$HDR
|
|
| 1 | a | nei | 40% |
|
|
| 2 | b | Ja - moved K1 | 2% |"
|
|
allowed; check "R3 control: last row moved (Ja, any case) -> sum 0 -> allow" $?
|
|
r3 r3-nomove-ever "$HDR
|
|
| 1 | a | nei | 6% |
|
|
| 2 | b | nei | 6% |"
|
|
denied R3; check "R3: no movement ever, 12%: deny (counted from the first row)" $?
|
|
r3 r3-unmeasured "$HDR
|
|
| 1 | a | ja | 1% |
|
|
| 2 | b | nei | ikke maalt |"
|
|
denied R3; check "R3: kvote 'ikke maalt' is unmeasured, not zero: deny" $?
|
|
r3 r3-kdash "$HDR
|
|
| 1 | a | nei | - |"
|
|
denied R3; check "R3: kvote '-': deny" $?
|
|
r3 r3-kneg "$HDR
|
|
| 1 | a | nei | -3% |"
|
|
denied R3; check "R3: negative kvote: deny" $?
|
|
r3 r3-kbare "$HDR
|
|
| 1 | a | nei | 3 |"
|
|
denied R3; check "R3: kvote without % (unit unknown): deny" $?
|
|
r3 r3-bingen "$HDR
|
|
| 1 | a | ingen mot kriteriene | 1% |"
|
|
denied R3; check "R3: bevegelse outside ja|nei ('ingen ...'): deny" $?
|
|
r3 r3-bempty "$HDR
|
|
| 1 | a | | 1% |"
|
|
denied R3; check "R3: bevegelse empty: deny" $?
|
|
r3 r3-bjanei "$HDR
|
|
| 1 | a | janei | 1% |"
|
|
denied R3; check "R3: bevegelse 'janei' (prefix is not the token): deny" $?
|
|
r3 r3-cells "$HDR
|
|
| 1 | a | nei |"
|
|
denied R3; check "R3: a row with fewer cells than the header: deny" $?
|
|
r3 r3-pipe "$HDR
|
|
| 1 | a \`x|y\` | nei | 1% |"
|
|
denied R3; check "R3: a row with more cells than the header: deny" $?
|
|
r3 r3-nolog ""
|
|
denied R3; check "R3: no Logg section at all: deny" $?
|
|
r3 r3-notable "## Logg
|
|
- 26.09 did things, no movement, 3%"
|
|
denied R3; check "R3: Logg section with no table: deny" $?
|
|
r3 r3-nokvote "## Logg
|
|
| Dato | Hva | Bevegelse |
|
|
|---|---|---|
|
|
| 1 | a | ja |"
|
|
denied R3; check "R3: Logg table without a Kvote column: deny" $?
|
|
r3 r3-nobev "## Logg
|
|
| Dato | Hva | Kvote |
|
|
|---|---|---|
|
|
| 1 | a | 1% |"
|
|
denied R3; check "R3: Logg table without a Bevegelse column: deny" $?
|
|
r3 r3-hdronly "$HDR"
|
|
allowed; check "R3 control: header only, no rows yet (first session) -> allow" $?
|
|
r3 r3-twolog "$HDR
|
|
| 1 | a | ja | 1% |
|
|
|
|
## Logg
|
|
| Dato | Hva | Bevegelse | Kvote |
|
|
|---|---|---|---|"
|
|
denied R3; check "R3: two Logg sections (ambiguous): deny" $?
|
|
r3 r3-bullet "$HDR
|
|
| 1 | a | ja | 1% |
|
|
- 2 | b | nei | 30% (a row written as a bullet)"
|
|
denied R3; check "R3: a list item inside Logg (a row the gate cannot read): deny" $?
|
|
r3 r3-after "$HDR
|
|
| 1 | a | ja | 1% |
|
|
|
|
## Next section
|
|
- a bullet that belongs to another section
|
|
| x | y |"
|
|
allowed; check "R3 control: the Logg section ends at the next heading -> allow" $?
|
|
SECT="$(printf '\302\247')"
|
|
r3 r3-para "## $SECT Logg
|
|
| dato | hva | bevegelse | kvoteandel |
|
|
|:---|---|---|---:|
|
|
| 1 | a | nei | 2% |"
|
|
allowed; check "R3 control: '## <section sign> Logg', lower-case headers, Kvoteandel, aligned separator -> allow" $?
|
|
r3 r3-endrlogg "## Endringslogg
|
|
| Dato | Hva | Bevegelse | Kvote |
|
|
|---|---|---|---|
|
|
| 1 | a | ja | 1% |"
|
|
denied R3; check "R3: '## Endringslogg' is not the Logg section: deny" $?
|
|
|
|
# --- 4. R6: concurrency cap and night starts -------------------------------
|
|
mkplan r6 "$GATE_OK" "$LOG_OK"
|
|
AG="$T/ag0" run_gate --repo "$T/r6"
|
|
allowed; check "R6 control: 0 live sessions, cap 1 -> allow" $?
|
|
grep -qx "agents --json --cwd $T/r6" "$FAKE_LOG"
|
|
check "R6: live sessions are read with 'claude agents --json --cwd <repo>'" $?
|
|
|
|
ln -s "$T/r6" "$T/r6-link"
|
|
AG="$T/ag0" run_gate --repo "$T/r6-link"
|
|
grep -qx "agents --json --cwd $T/r6" "$FAKE_LOG"
|
|
check "R6: a symlinked --repo is resolved to its physical path before the query" $?
|
|
|
|
AG="$T/ag1" run_gate --repo "$T/r6"
|
|
denied R6; check "R6: 1 live session, no R1 proof -> cap 1 -> deny" $?
|
|
printf '%s' "$ERR" | grep -q '1'
|
|
check "R6: the denial states the live count" $?
|
|
|
|
mkplan r6-r1 "$GATE_R1" "$LOG_OK"
|
|
AG="$T/ag1" run_gate --repo "$T/r6-r1"
|
|
denied R6; check "R6: R1 proof but no first measurement -> cap still 1 -> deny" $?
|
|
|
|
mkplan r6-r1m "$GATE_R1M" "$LOG_OK"
|
|
AG="$T/ag1" run_gate --repo "$T/r6-r1m"
|
|
allowed; check "R6 control: R1 proof AND first measurement, 1 live -> cap 2 -> allow" $?
|
|
AG="$T/ag2" run_gate --repo "$T/r6-r1m"
|
|
denied R6; check "R6: 2 live (one-line JSON, interactive counts too), cap 2 -> deny" $?
|
|
|
|
AG="$T/ag1" run_gate --repo "$T/r6" --kind read
|
|
allowed; check "R6 control: --kind read is not capped -> allow with 1 live" $?
|
|
[ ! -s "$FAKE_LOG" ]
|
|
check "R6: --kind read does not query live sessions at all" $?
|
|
|
|
AG="$T/agjunk" run_gate --repo "$T/r6"
|
|
denied R6; check "R6: claude agents prints non-JSON: deny (fail-closed)" $?
|
|
AG="$T/agempty" run_gate --repo "$T/r6"
|
|
denied R6; check "R6: claude agents prints nothing: deny (fail-closed)" $?
|
|
AG="$T/ag0" AGX=1 run_gate --repo "$T/r6"
|
|
denied R6; check "R6: claude agents exits 1: deny (fail-closed)" $?
|
|
|
|
NOCL="$T/noclaude"; mkdir -p "$NOCL"
|
|
env PATH="$NOCL:/usr/bin:/bin" /bin/bash -c 'command -v claude' >/dev/null 2>&1; [ $? -ne 0 ]
|
|
check "ground truth: claude is not on the reduced PATH" $?
|
|
GPATH="$NOCL:/usr/bin:/bin" run_gate --repo "$T/r6"
|
|
denied R6; check "R6: claude not on PATH: deny (fail-closed)" $?
|
|
GPATH="$NOCL:/usr/bin:/bin" run_gate --repo "$T/r6" --kind read
|
|
allowed; check "R6 control: --kind read needs no claude binary -> allow" $?
|
|
|
|
NOW=23:00 run_gate --repo "$T/r6"
|
|
denied R6; check "R6 night: 23:00 without R1 proof: deny" $?
|
|
printf '%s' "$ERR" | grep -q 'r1-proeve'
|
|
check "R6 night: the denial names r1-proeve" $?
|
|
NOW=06:59 run_gate --repo "$T/r6"
|
|
denied R6; check "R6 night: 06:59 without R1 proof: deny" $?
|
|
NOW=07:00 run_gate --repo "$T/r6"
|
|
allowed; check "R6 night control: 07:00 is day -> allow" $?
|
|
NOW=22:59 run_gate --repo "$T/r6"
|
|
allowed; check "R6 night control: 22:59 is day -> allow" $?
|
|
NOW=12:00 run_gate --repo "$T/r6" --night
|
|
denied R6; check "R6 night: --night at noon without R1 proof: deny" $?
|
|
NOW=02:00 run_gate --repo "$T/r6" --kind read
|
|
denied R6; check "R6 night: a read session at 02:00 without R1 proof: deny" $?
|
|
NOW=02:00 run_gate --repo "$T/r6-r1"
|
|
allowed; check "R6 night control: R1 proof present -> night start allowed" $?
|
|
NOW=12:00 run_gate --repo "$T/r6-r1" --night
|
|
allowed; check "R6 night control: --night with R1 proof -> allow" $?
|
|
|
|
NOW=noon run_gate --repo "$T/r6"
|
|
denied SESSION_GATE_NOW; check "clock override 'noon' is refused by name, never ignored" $?
|
|
NOW=25:00 run_gate --repo "$T/r6"
|
|
denied SESSION_GATE_NOW; check "clock override 25:00 is refused by name" $?
|
|
NOW= run_gate --repo "$T/r6"
|
|
denied SESSION_GATE_NOW; check "clock override set but empty is refused by name" $?
|
|
NOW_UNSET=1 run_gate --repo "$T/r6-r1m"
|
|
allowed; check "KNOWN-POSITIVE: real clock (override unset), R1 proof -> allow at any hour" $?
|
|
|
|
# --- 5. Rule order: the first failing rule is the one reported ------------
|
|
mkplan order "" "$HDR
|
|
| 1 | a | nei | 50% |"
|
|
AG="$T/ag2" NOW=03:00 run_gate --repo "$T/order"
|
|
denied R4; check "all three rules fail: R4 is reported first (one line)" $?
|
|
|
|
echo "session-gate-selftest: $PASS passed, $FAIL failed (of $((PASS+FAIL)) checks)"
|
|
[ "$FAIL" -eq 0 ] || exit 1
|
|
exit 0
|