forge-orkes 0.63.0 → 0.64.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.claude/hooks/forge-release-fold.sh +9 -1
- package/template/.claude/skills/jarvis/SKILL.md +204 -0
- package/template/.forge/checks/forge-board-render.sh +11 -5
- package/template/.forge/checks/forge-jarvis-answer.sh +85 -0
- package/template/.forge/checks/forge-jarvis-awareness.sh +133 -0
- package/template/.forge/checks/forge-jarvis-bridge-clean.sh +133 -0
- package/template/.forge/checks/forge-jarvis-dispatch.sh +198 -0
- package/template/.forge/checks/forge-jarvis-instruments.sh +149 -0
- package/template/.forge/checks/forge-jarvis-notify-logonly.sh +46 -0
- package/template/.forge/checks/forge-jarvis-promote.sh +116 -0
- package/template/.forge/checks/forge-jarvis-prwatch.sh +109 -0
- package/template/.forge/checks/forge-jarvis-relay.sh +242 -0
- package/template/.forge/checks/forge-jarvis.sh +161 -0
- package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
- package/template/.forge/checks/tests/forge-jarvis-awareness.test.sh +118 -0
- package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
- package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
- package/template/.forge/checks/tests/forge-jarvis-notify-logonly.test.sh +63 -0
- package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
- package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
- package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
- package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
- package/template/.forge/templates/project.yml +11 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test for .forge/checks/forge-jarvis-awareness.sh (m-37 B5; FR-203/204, NFR-036).
|
|
3
|
+
#
|
|
4
|
+
# Pins the B5 "what's in play?" surface that shipped untested: the slice glob
|
|
5
|
+
# (--worktree-root seam), per-slice fold recompute + degradation, the board
|
|
6
|
+
# projection parse (including key-order independence) + degradation, the
|
|
7
|
+
# always-present per-machine caveat, and the loud unknown-flag refusal.
|
|
8
|
+
# No real repo/network — pure fixtures via the existing env seams.
|
|
9
|
+
set -u
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
AWARE="$SCRIPT_DIR/../forge-jarvis-awareness.sh"
|
|
13
|
+
[ -f "$AWARE" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$AWARE" >&2
|
|
14
|
+
|
|
15
|
+
ROOT="$(mktemp -d)"
|
|
16
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
17
|
+
|
|
18
|
+
PASSED=0; FAILED=0
|
|
19
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
20
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
21
|
+
|
|
22
|
+
# fixture slice worktrees (the --worktree-root seam globs this dir)
|
|
23
|
+
WT="$ROOT/worktrees"; mkdir -p "$WT/m-41" "$WT/m-42"
|
|
24
|
+
|
|
25
|
+
# stub fold: records invocation, answers a fixed state
|
|
26
|
+
FOLDSTUB="$ROOT/fold.sh"
|
|
27
|
+
cat > "$FOLDSTUB" <<'EOF'
|
|
28
|
+
#!/bin/sh
|
|
29
|
+
echo "called $1" >> "${FOLD_CALLS:-/dev/null}"
|
|
30
|
+
echo "release_state=merged"
|
|
31
|
+
EOF
|
|
32
|
+
chmod +x "$FOLDSTUB"
|
|
33
|
+
|
|
34
|
+
# stub board: two units, one complete one executing — and the executing row uses a
|
|
35
|
+
# DIFFERENT key order (release_state before status) to pin the order-independent parse.
|
|
36
|
+
BOARDSTUB="$ROOT/board.sh"
|
|
37
|
+
cat > "$BOARDSTUB" <<'EOF'
|
|
38
|
+
#!/bin/sh
|
|
39
|
+
[ "$1" = "--emit" ] || exit 2
|
|
40
|
+
printf '{"forge_id": "m-40", "status": "complete", "release_state": "validated"}\n'
|
|
41
|
+
printf '{"forge_id": "m-41", "release_state": "unmerged", "status": "executing"}\n'
|
|
42
|
+
EOF
|
|
43
|
+
chmod +x "$BOARDSTUB"
|
|
44
|
+
|
|
45
|
+
# 1. --dry-run: slices listed, fold NOT invoked
|
|
46
|
+
CALLS="$ROOT/fold-calls"; : > "$CALLS"
|
|
47
|
+
OUT="$(FOLD_CALLS="$CALLS" FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
48
|
+
sh "$AWARE" --dry-run --worktree-root "$WT" 2>&1)"
|
|
49
|
+
if printf '%s' "$OUT" | grep -q 'm-41' && printf '%s' "$OUT" | grep -q '(dry-run)' && [ ! -s "$CALLS" ]; then
|
|
50
|
+
ok '--dry-run lists slices without invoking the fold'
|
|
51
|
+
else
|
|
52
|
+
bad '--dry-run lists slices without invoking the fold' "$OUT"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 2. real run: per-slice fold recompute rendered
|
|
56
|
+
OUT="$(FOLD_CALLS="$CALLS" FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
57
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"
|
|
58
|
+
if printf '%s' "$OUT" | grep -q 'release_state=merged' && grep -q 'called m-41' "$CALLS"; then
|
|
59
|
+
ok 'live run recomputes each slice release_state from the fold'
|
|
60
|
+
else
|
|
61
|
+
bad 'live run recomputes each slice release_state from the fold' "$OUT"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 3. fold missing → unknown, exit 0 (degraded, never fatal)
|
|
65
|
+
OUT="$(FORGE_JARVIS_FOLD="$ROOT/no-such-fold" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
66
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"; RC=$?
|
|
67
|
+
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -q 'release_state=unknown'; then
|
|
68
|
+
ok 'missing fold degrades to release_state=unknown, exit 0'
|
|
69
|
+
else
|
|
70
|
+
bad 'missing fold degrades to release_state=unknown, exit 0' "rc=$RC out=$OUT"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# 4. board projection: in-flight only, key-order-independent field extraction
|
|
74
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
75
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"
|
|
76
|
+
if printf '%s' "$OUT" | grep -q 'm-41 status=executing release_state=unmerged' \
|
|
77
|
+
&& ! printf '%s' "$OUT" | grep -q 'm-40 status=complete'; then
|
|
78
|
+
ok 'board parse: in-flight only, fields extracted regardless of key order'
|
|
79
|
+
else
|
|
80
|
+
bad 'board parse: in-flight only, fields extracted regardless of key order' "$OUT"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# 5. board missing → stated unavailable, exit 0
|
|
84
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$ROOT/no-such-board" \
|
|
85
|
+
sh "$AWARE" --worktree-root "$WT" 2>&1)"; RC=$?
|
|
86
|
+
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -qi 'projection unavailable'; then
|
|
87
|
+
ok 'missing board degrades to a stated unavailable, exit 0'
|
|
88
|
+
else
|
|
89
|
+
bad 'missing board degrades to a stated unavailable, exit 0' "rc=$RC out=$OUT"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 6. per-machine caveat always present (F11)
|
|
93
|
+
if printf '%s' "$OUT" | grep -q 'PER-MACHINE'; then
|
|
94
|
+
ok 'per-machine scope caveat is always stated'
|
|
95
|
+
else
|
|
96
|
+
bad 'per-machine scope caveat is always stated' "$OUT"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# 7. empty worktree root → explicit none-line, exit 0
|
|
100
|
+
EMPTY="$ROOT/empty"; mkdir -p "$EMPTY"
|
|
101
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
102
|
+
sh "$AWARE" --worktree-root "$EMPTY" 2>&1)"; RC=$?
|
|
103
|
+
if [ "$RC" -eq 0 ] && printf '%s' "$OUT" | grep -q '(none on this machine)'; then
|
|
104
|
+
ok 'empty glob renders an explicit none-line, not silence'
|
|
105
|
+
else
|
|
106
|
+
bad 'empty glob renders an explicit none-line, not silence' "rc=$RC out=$OUT"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# 8. unknown flag → loud refusal, exit 2
|
|
110
|
+
OUT="$(sh "$AWARE" --no-such-flag 2>&1)"; RC=$?
|
|
111
|
+
if [ "$RC" -eq 2 ] && printf '%s' "$OUT" | grep -q 'unknown argument'; then
|
|
112
|
+
ok 'unknown flag refused loudly (exit 2)'
|
|
113
|
+
else
|
|
114
|
+
bad 'unknown flag refused loudly (exit 2)' "rc=$RC out=$OUT"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
printf '\nforge-jarvis-awareness.test.sh: %s passed, %s failed\n' "$PASSED" "$FAILED"
|
|
118
|
+
[ "$FAILED" -eq 0 ]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test for .forge/checks/forge-jarvis-bridge-clean.sh (m-37 B5; FR-206; B1 probe ii).
|
|
3
|
+
#
|
|
4
|
+
# Builds a REAL throwaway repo with two REAL bridge worktrees (one "live", one orphaned), then
|
|
5
|
+
# asserts the orphan is reaped from disk AND git's registry while the live one is untouched — and
|
|
6
|
+
# that --dry-run removes nothing and never flags the live one. Liveness is pinned deterministically
|
|
7
|
+
# via FORGE_JARVIS_BRIDGE_LIVECHECK so the test never depends on wall-clock mtime.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
CLEAN="$SCRIPT_DIR/../forge-jarvis-bridge-clean.sh"
|
|
12
|
+
[ -f "$CLEAN" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$CLEAN" >&2
|
|
13
|
+
|
|
14
|
+
ROOT="$(mktemp -d)"
|
|
15
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
16
|
+
|
|
17
|
+
PASSED=0; FAILED=0
|
|
18
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
19
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
20
|
+
|
|
21
|
+
# --- a real repo with two real bridge worktrees ------------------------------------------------
|
|
22
|
+
REPO="$ROOT/repo"; mkdir -p "$REPO"
|
|
23
|
+
git -C "$REPO" init -q
|
|
24
|
+
git -C "$REPO" config user.email t@t.test
|
|
25
|
+
git -C "$REPO" config user.name tester
|
|
26
|
+
printf 'seed\n' > "$REPO/f"; git -C "$REPO" add f; git -C "$REPO" commit -qm init
|
|
27
|
+
|
|
28
|
+
WTR="$REPO/.claude/worktrees"; mkdir -p "$WTR"
|
|
29
|
+
git -C "$REPO" worktree add -q "$WTR/bridge-alive" 2>/dev/null || bad "setup" "could not add bridge-alive worktree"
|
|
30
|
+
git -C "$REPO" worktree add -q "$WTR/bridge-dead" 2>/dev/null || bad "setup" "could not add bridge-dead worktree"
|
|
31
|
+
|
|
32
|
+
# livecheck stub: ONLY bridge-alive is live.
|
|
33
|
+
LIVE="$ROOT/livecheck.sh"
|
|
34
|
+
printf '#!/usr/bin/env sh\ncase "$1" in *bridge-alive*) exit 0 ;; *) exit 1 ;; esac\n' > "$LIVE"
|
|
35
|
+
chmod +x "$LIVE"
|
|
36
|
+
|
|
37
|
+
run() { FORGE_JARVIS_BRIDGE_LIVECHECK="$LIVE" sh "$CLEAN" --worktrees-root "$WTR" "$@" 2>/dev/null; }
|
|
38
|
+
|
|
39
|
+
# 1. dry-run flags the orphan, removes nothing
|
|
40
|
+
out="$(run --dry-run)"
|
|
41
|
+
case "$out" in *bridge-dead*candidate*) ok "dry-run flags bridge-dead as an orphan candidate" ;; *) bad "dry-run candidate" "$out" ;; esac
|
|
42
|
+
[ -d "$WTR/bridge-dead" ] && ok "dry-run removed nothing (bridge-dead still on disk)" || bad "dry-run non-removal" "bridge-dead gone after dry-run"
|
|
43
|
+
|
|
44
|
+
# 2. dry-run never flags the live one (line-scoped: the bridge-alive line must not say "candidate")
|
|
45
|
+
if printf '%s\n' "$out" | grep 'bridge-alive' | grep -q 'candidate'; then
|
|
46
|
+
bad "dry-run live safety" "flagged the live worktree: $out"
|
|
47
|
+
else
|
|
48
|
+
ok "dry-run keeps the live worktree out of candidates"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# 3. real run reaps the orphan, keeps the live one
|
|
52
|
+
out="$(run)"
|
|
53
|
+
[ ! -d "$WTR/bridge-dead" ] && ok "orphan bridge-dead reaped (removed from disk)" || bad "orphan removal" "bridge-dead still present: $out"
|
|
54
|
+
[ -d "$WTR/bridge-alive" ] && ok "live bridge-alive untouched" || bad "live safety" "live worktree was removed"
|
|
55
|
+
|
|
56
|
+
# 4. reaped from git's worktree registry too (not just the directory)
|
|
57
|
+
if git -C "$REPO" worktree list --porcelain 2>/dev/null | grep -q 'bridge-dead'; then
|
|
58
|
+
bad "registry" "bridge-dead still in git worktree list"
|
|
59
|
+
else
|
|
60
|
+
ok "bridge-dead gone from git's worktree registry too"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# 5. re-run is a clean no-op (only the live one remains, and it is kept)
|
|
64
|
+
out="$(run)"
|
|
65
|
+
case "$out" in *'0 reaped'*) ok "re-run reaps nothing (idempotent — only the live worktree left)" ;; *) bad "idempotent" "$out" ;; esac
|
|
66
|
+
|
|
67
|
+
# --- 6+7. LOCK-AWARE liveness — the REAL RC substrate: bridges are git-LOCKED under the server pid.
|
|
68
|
+
# No injected livecheck here, so this exercises default_livecheck's lock-pid path AND unlock-on-reap
|
|
69
|
+
# (the fixtures 1-5 used unlocked worktrees + a stubbed livecheck, which masked both — the gap the
|
|
70
|
+
# FR-207 human-verification walk surfaced against a live RC server).
|
|
71
|
+
REPO2="$ROOT/repo2"; mkdir -p "$REPO2"
|
|
72
|
+
git -C "$REPO2" init -q
|
|
73
|
+
git -C "$REPO2" config user.email t@t.test
|
|
74
|
+
git -C "$REPO2" config user.name tester
|
|
75
|
+
printf 'seed\n' > "$REPO2/f"; git -C "$REPO2" add f; git -C "$REPO2" commit -qm init
|
|
76
|
+
WTR2="$REPO2/.claude/worktrees"; mkdir -p "$WTR2"
|
|
77
|
+
git -C "$REPO2" worktree add -q "$WTR2/bridge-livepid" 2>/dev/null || bad "setup2" "add bridge-livepid"
|
|
78
|
+
git -C "$REPO2" worktree add -q "$WTR2/bridge-deadpid" 2>/dev/null || bad "setup2" "add bridge-deadpid"
|
|
79
|
+
|
|
80
|
+
# a definitely-DEAD pid (background a no-op, reap it, reuse its pid); a definitely-LIVE pid (this shell)
|
|
81
|
+
( : ) & DEADPID=$!; wait "$DEADPID" 2>/dev/null
|
|
82
|
+
LIVEPID=$$
|
|
83
|
+
git -C "$REPO2" worktree lock --reason "claude agent bridge-livepid (pid $LIVEPID start now)" "$WTR2/bridge-livepid" 2>/dev/null || bad "setup2" "lock bridge-livepid"
|
|
84
|
+
git -C "$REPO2" worktree lock --reason "claude agent bridge-deadpid (pid $DEADPID start now)" "$WTR2/bridge-deadpid" 2>/dev/null || bad "setup2" "lock bridge-deadpid"
|
|
85
|
+
|
|
86
|
+
# Both are FRESH (mtime ~now): under the OLD mtime rule both would be "live". The fix classifies by
|
|
87
|
+
# the lock pid, so the dead-pid one must reap (despite fresh mtime) and the live-pid one must stay —
|
|
88
|
+
# AND the reap must succeed against a LOCKED worktree (unlock-before-remove).
|
|
89
|
+
out2="$(sh "$CLEAN" --worktrees-root "$WTR2" 2>/dev/null)"
|
|
90
|
+
[ ! -d "$WTR2/bridge-deadpid" ] && ok "dead-pid lock reaped despite fresh mtime (lock beats mtime; unlock-then-remove worked)" || bad "lock-pid orphan" "bridge-deadpid still present: $out2"
|
|
91
|
+
[ -d "$WTR2/bridge-livepid" ] && ok "live-pid lock kept (the server still owns it)" || bad "lock-pid live" "live-pid worktree was removed: $out2"
|
|
92
|
+
|
|
93
|
+
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
94
|
+
[ "$FAILED" = 0 ]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test suite for .forge/checks/forge-jarvis-dispatch.sh --dry-run (m-37, step 4 B3).
|
|
3
|
+
#
|
|
4
|
+
# Contract under test (handoff-step4-jarvis.md REV2 B3(b)/(c); FR-197/FR-198; acceptance #5):
|
|
5
|
+
# --dry-run prints the exact `nohup env ... runner ...` command and NEVER dispatches:
|
|
6
|
+
# - FORGE_SLICE_NOTIFY_LOG lands INSIDE the slice worktree (<slice>/.forge/runner-work/
|
|
7
|
+
# notify.log), NEVER the invoker's cwd — the clean-main invariant (step 0; acceptance #5).
|
|
8
|
+
# - FORGE_SLICE_NOTIFY points at the log-only channel (forge-jarvis-notify-logonly.sh) —
|
|
9
|
+
# the relay is the one delivery path; the default channel's push stays quiet (probe x).
|
|
10
|
+
# - the runner is dispatched DETACHED (nohup-& — survives Jarvis's death, probe viii).
|
|
11
|
+
# - every runner arg (incl --config carrying model_by_phase_type) is forwarded VERBATIM;
|
|
12
|
+
# the helper carries NO model-selection logic (routing is step 5, FR-198).
|
|
13
|
+
# Refusal: missing --slice → non-zero exit, no command on stdout.
|
|
14
|
+
#
|
|
15
|
+
# Only ever invokes --dry-run (pure resolution — `claude`/the runner need not run); the real
|
|
16
|
+
# runner + channel are resolved relative to the real script dir, so both exist. Slices are
|
|
17
|
+
# throwaway mktemp dirs.
|
|
18
|
+
#
|
|
19
|
+
# Usage: ./.forge/checks/tests/forge-jarvis-dispatch.test.sh
|
|
20
|
+
# RED-phase friendly: a missing/non-executable script fails every case, exits non-zero.
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
DISPATCH="$SCRIPT_DIR/../forge-jarvis-dispatch.sh"
|
|
25
|
+
|
|
26
|
+
[ -f "$DISPATCH" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$DISPATCH" >&2
|
|
27
|
+
|
|
28
|
+
ROOT="$(mktemp -d)"
|
|
29
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
30
|
+
|
|
31
|
+
PASSED=0; FAILED=0
|
|
32
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
33
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
34
|
+
|
|
35
|
+
# --- fixture slice worktree (just a dir — --dry-run needs [ -d ], not a real git worktree) ---
|
|
36
|
+
SLICE="$ROOT/slice-wt"
|
|
37
|
+
mkdir -p "$SLICE"
|
|
38
|
+
LOGWANT="$SLICE/.forge/runner-work/notify.log"
|
|
39
|
+
CHECKS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # resolve — the dispatch emits a canonical path
|
|
40
|
+
CHANWANT="$CHECKS_DIR/forge-jarvis-notify-logonly.sh"
|
|
41
|
+
|
|
42
|
+
# 1. notify-log lands INSIDE the slice worktree (not cwd) — the acceptance #5 core
|
|
43
|
+
out="$(cd "$ROOT" && sh "$DISPATCH" --slice "$SLICE" --dry-run 2>/dev/null)"; rc=$?
|
|
44
|
+
case "$out" in
|
|
45
|
+
*"FORGE_SLICE_NOTIFY_LOG=$LOGWANT "*) ok "notify-log points INTO the slice worktree ($LOGWANT)" ;;
|
|
46
|
+
*) bad "notify-log placement" "rc=$rc out=$out" ;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
# 2. FORGE_SLICE_NOTIFY points at the log-only channel
|
|
50
|
+
case "$out" in
|
|
51
|
+
*"FORGE_SLICE_NOTIFY=$CHANWANT "*) ok "notify channel is the log-only channel (no double-buzz, probe x)" ;;
|
|
52
|
+
*) bad "log-only channel" "rc=$rc out=$out" ;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
# 3. dispatched DETACHED — the command begins with nohup (survives Jarvis's death, probe viii)
|
|
56
|
+
case "$out" in
|
|
57
|
+
"nohup "*) ok "runner dispatched detached (nohup-&)" ;;
|
|
58
|
+
*) bad "nohup detach" "out=$out" ;;
|
|
59
|
+
esac
|
|
60
|
+
|
|
61
|
+
# 4. model map passed through untouched — a caller --config reaches the runner verbatim
|
|
62
|
+
out4="$(cd "$ROOT" && sh "$DISPATCH" --slice "$SLICE" --config /tmp/mymap.yml --dry-run 2>/dev/null)"; rc=$?
|
|
63
|
+
case "$out4" in
|
|
64
|
+
*"--config /tmp/mymap.yml"*) ok "model map: --config forwarded to the runner VERBATIM (FR-198)" ;;
|
|
65
|
+
*) bad "config passthrough" "rc=$rc out=$out4" ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
# 5. --slice forwarded to the runner (the runner needs it too, not just this helper)
|
|
69
|
+
case "$out" in
|
|
70
|
+
*"--slice $SLICE"*) ok "--slice forwarded to the runner" ;;
|
|
71
|
+
*) bad "slice forward" "out=$out" ;;
|
|
72
|
+
esac
|
|
73
|
+
|
|
74
|
+
# 6. NEG-CONTROL (step-0 invariant): run from a DIFFERENT cwd (a stand-in 'main checkout');
|
|
75
|
+
# the log must STILL be the slice's, never <cwd>/notify.log. Without the override the
|
|
76
|
+
# runner's default would drop notify.log in cwd — this is why the override exists (#5).
|
|
77
|
+
FAKEMAIN="$ROOT/fake-main-checkout"
|
|
78
|
+
mkdir -p "$FAKEMAIN"
|
|
79
|
+
out6="$(cd "$FAKEMAIN" && sh "$DISPATCH" --slice "$SLICE" --dry-run 2>/dev/null)"; rc=$?
|
|
80
|
+
case "$out6" in
|
|
81
|
+
*"FORGE_SLICE_NOTIFY_LOG=$FAKEMAIN/notify.log"*)
|
|
82
|
+
bad "step-0 neg-control" "log leaked into cwd: $out6" ;;
|
|
83
|
+
*"FORGE_SLICE_NOTIFY_LOG=$LOGWANT "*)
|
|
84
|
+
ok "step-0 neg-control: dispatched from another cwd, log STILL in the slice (not cwd)" ;;
|
|
85
|
+
*) bad "step-0 neg-control" "rc=$rc out=$out6" ;;
|
|
86
|
+
esac
|
|
87
|
+
|
|
88
|
+
# 7. NEG: missing --slice → clean refusal (non-zero, no command emitted)
|
|
89
|
+
out7="$(cd "$ROOT" && sh "$DISPATCH" --dry-run 2>/dev/null)"; rc=$?
|
|
90
|
+
if [ "$rc" != 0 ] && [ -z "$out7" ]; then
|
|
91
|
+
ok "neg-control: missing --slice → refusal, no command emitted"
|
|
92
|
+
else
|
|
93
|
+
bad "neg-control missing --slice" "rc=$rc out=$out7"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# 8. SOURCE AUDIT: the helper carries NO model-selection logic (routing is step 5, FR-198).
|
|
97
|
+
# Analogue of the launcher's no-token case — grep the script itself, not the output.
|
|
98
|
+
if grep -inE 'choose.*model|select.*model|pick.*model|route.*model|model_by_phase_type[[:space:]]*=' "$DISPATCH" >/dev/null 2>&1; then
|
|
99
|
+
bad "no routing brain" "model-selection logic found in $DISPATCH"
|
|
100
|
+
else
|
|
101
|
+
ok "no model-selection logic in the dispatch helper (routing stays step 5)"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# --- 9-12. WORK-ORDER MATERIALIZATION (FR-197; walk run 4: runner refused 'no slice.yml',
|
|
105
|
+
# refusal visible only in dispatch.out — phone silent). The helper bridges the Forge plan
|
|
106
|
+
# layout (.forge/phases/milestone-{id}/{phase}-{name}/plan-NN.md) to the runner contract
|
|
107
|
+
# (slice.yml + phases/<name>/plan.md), refusing loudly BEFORE the detach when it cannot.
|
|
108
|
+
|
|
109
|
+
# 9. materialize from milestone plans: ordered slice.yml + concatenated plan.md per phase
|
|
110
|
+
MSLICE="$ROOT/mat-wt"
|
|
111
|
+
mkdir -p "$MSLICE/.forge/phases/milestone-77/71-aaa" "$MSLICE/.forge/phases/milestone-77/72-bbb"
|
|
112
|
+
printf 'plan A part1\n' > "$MSLICE/.forge/phases/milestone-77/71-aaa/plan-01.md"
|
|
113
|
+
printf 'plan A part2\n' > "$MSLICE/.forge/phases/milestone-77/71-aaa/plan-02.md"
|
|
114
|
+
printf 'plan B\n' > "$MSLICE/.forge/phases/milestone-77/72-bbb/plan-01.md"
|
|
115
|
+
err9="$(sh "$DISPATCH" --slice "$MSLICE" --materialize-only 2>&1)"; rc=$?
|
|
116
|
+
if [ "$rc" = 0 ] && [ -f "$MSLICE/slice.yml" ] \
|
|
117
|
+
&& grep -q '^ - 71-aaa$' "$MSLICE/slice.yml" && grep -q '^ - 72-bbb$' "$MSLICE/slice.yml" \
|
|
118
|
+
&& grep -q 'plan A part1' "$MSLICE/phases/71-aaa/plan.md" && grep -q 'plan A part2' "$MSLICE/phases/71-aaa/plan.md" \
|
|
119
|
+
&& grep -q 'plan B' "$MSLICE/phases/72-bbb/plan.md"; then
|
|
120
|
+
ok "materializes slice.yml (ordered) + phases/<name>/plan.md (plan-NN concatenated) from milestone plans"
|
|
121
|
+
else
|
|
122
|
+
bad "materialize" "rc=$rc err=$err9 slice.yml=$(cat "$MSLICE/slice.yml" 2>/dev/null)"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# 10. an existing slice.yml is honored untouched (hand-authored work order wins)
|
|
126
|
+
HSLICE="$ROOT/hand-wt"
|
|
127
|
+
mkdir -p "$HSLICE/.forge/phases/milestone-88/1-x"
|
|
128
|
+
printf 'x\n' > "$HSLICE/.forge/phases/milestone-88/1-x/plan-01.md"
|
|
129
|
+
printf 'phases:\n - hand-rolled\n' > "$HSLICE/slice.yml"
|
|
130
|
+
before="$(cat "$HSLICE/slice.yml")"
|
|
131
|
+
sh "$DISPATCH" --slice "$HSLICE" --materialize-only >/dev/null 2>&1; rc=$?
|
|
132
|
+
after="$(cat "$HSLICE/slice.yml")"
|
|
133
|
+
if [ "$rc" = 0 ] && [ "$before" = "$after" ]; then
|
|
134
|
+
ok "existing slice.yml honored untouched (hand-authored wins)"
|
|
135
|
+
else
|
|
136
|
+
bad "hand-authored slice.yml" "rc=$rc before/after differ or refusal"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# 11. NEG: nothing to dispatch (no slice.yml, no milestone plans) → loud SYNCHRONOUS refusal
|
|
140
|
+
ESLICE="$ROOT/empty-wt"
|
|
141
|
+
mkdir -p "$ESLICE"
|
|
142
|
+
err11="$(sh "$DISPATCH" --slice "$ESLICE" --materialize-only 2>&1)"; rc=$?
|
|
143
|
+
if [ "$rc" != 0 ] && [ ! -f "$ESLICE/slice.yml" ] && printf '%s' "$err11" | grep -q 'REFUSED'; then
|
|
144
|
+
ok "neg-control: nothing to dispatch → loud refusal BEFORE the detach (no silent phone)"
|
|
145
|
+
else
|
|
146
|
+
bad "empty refusal" "rc=$rc err=$err11"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# 12. --dry-run stays side-effect-free (reports, but writes no slice.yml)
|
|
150
|
+
DSLICE="$ROOT/dry-wt"
|
|
151
|
+
mkdir -p "$DSLICE/.forge/phases/milestone-99/1-y"
|
|
152
|
+
printf 'y\n' > "$DSLICE/.forge/phases/milestone-99/1-y/plan-01.md"
|
|
153
|
+
sh "$DISPATCH" --slice "$DSLICE" --dry-run >/dev/null 2>&1
|
|
154
|
+
if [ ! -f "$DSLICE/slice.yml" ] && [ ! -d "$DSLICE/phases" ]; then
|
|
155
|
+
ok "--dry-run is side-effect-free (no slice.yml/phases written)"
|
|
156
|
+
else
|
|
157
|
+
bad "dry-run side effects" "dry-run wrote work-order files"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# --- summary -----------------------------------------------------------------
|
|
161
|
+
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
162
|
+
[ "$FAILED" = 0 ]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test for .forge/checks/forge-jarvis-instruments.sh (m-37 B5; FR-205).
|
|
3
|
+
#
|
|
4
|
+
# Builds a notify.log + a relay-markers dir BY HAND in the exact TSV formats the runner's
|
|
5
|
+
# notify channel (-notify-logonly.sh) and the relay (-relay.sh) write, then asserts the four
|
|
6
|
+
# week-zero numbers fall out. No real repo/slices/network needed — pure fixtures.
|
|
7
|
+
#
|
|
8
|
+
# Fixture design (4 pings emitted at :00,:01,:02,:03; only 1-3 relayed):
|
|
9
|
+
# offset 1 ambiguity, relayed +30s → live
|
|
10
|
+
# offset 2 done, relayed +5s → live, host says unmerged (no discrepancy)
|
|
11
|
+
# offset 3 halt, relayed +30min → landed-while-away, DISCREPANCY (payload lied about merge)
|
|
12
|
+
# offset 4 ambiguity, NOT relayed (no marker) → excluded from every number
|
|
13
|
+
# Expected: 3 relayed total; ambiguity=1/done=1/halt=1; latency max 1800s; discrepancy=1;
|
|
14
|
+
# presence 2/3 live, 1 landed while away.
|
|
15
|
+
set -u
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
INSTR="$SCRIPT_DIR/../forge-jarvis-instruments.sh"
|
|
19
|
+
[ -f "$INSTR" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$INSTR" >&2
|
|
20
|
+
|
|
21
|
+
ROOT="$(mktemp -d)"
|
|
22
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
23
|
+
|
|
24
|
+
PASSED=0; FAILED=0
|
|
25
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
26
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
27
|
+
TAB="$(printf '\t')"
|
|
28
|
+
|
|
29
|
+
LOG="$ROOT/notify.log"
|
|
30
|
+
MK="$ROOT/relay-markers"; mkdir -p "$MK"
|
|
31
|
+
|
|
32
|
+
# notify.log — emit-ts <TAB> type <TAB> payload
|
|
33
|
+
printf '2026-07-21T10:00:00Z%sambiguity%sJWT or sessions?\n' "$TAB" "$TAB" >> "$LOG" # 1
|
|
34
|
+
printf '2026-07-21T10:01:00Z%sdone%sPhases complete, PR opened\n' "$TAB" "$TAB" >> "$LOG" # 2
|
|
35
|
+
printf '2026-07-21T10:02:00Z%shalt%sAll done and merged to main!\n' "$TAB" "$TAB" >> "$LOG" # 3
|
|
36
|
+
printf '2026-07-21T10:03:00Z%sambiguity%sretry or fail?\n' "$TAB" "$TAB" >> "$LOG" # 4 (unrelayed)
|
|
37
|
+
|
|
38
|
+
# relay-markers — relay-ts <TAB> offset=N <TAB> relay-text (only 1,2,3 relayed)
|
|
39
|
+
printf '2026-07-21T10:00:30Z%soffset=1%s❓ Needs you (ambiguity): JWT or sessions?\n' "$TAB" "$TAB" > "$MK/1.relayed"
|
|
40
|
+
printf '2026-07-21T10:01:05Z%soffset=2%s✅ Slice done — host: release_state=unmerged. PR opened\n' "$TAB" "$TAB" > "$MK/2.relayed"
|
|
41
|
+
printf '2026-07-21T10:32:00Z%soffset=3%s⚠️ DISCREPANCY — payload claims landed/merged, but host shows release_state=unmerged. 🛑 Slice HALTED (host truth wins). Payload said: All done and merged to main!\n' "$TAB" "$TAB" > "$MK/3.relayed"
|
|
42
|
+
|
|
43
|
+
OUT="$(sh "$INSTR" --log "$LOG" --marker-dir "$MK" --live-window 120 2>/dev/null)"
|
|
44
|
+
|
|
45
|
+
# 1. by type — only relayed pings; offset 4 (ambiguity, unrelayed) excluded
|
|
46
|
+
printf '%s\n' "$OUT" | grep -qE '3 total' && ok "total relayed = 3 (unrelayed offset 4 excluded)" || bad "total" "$OUT"
|
|
47
|
+
printf '%s\n' "$OUT" | grep -qE 'ambiguity[[:space:]]+1' && ok "type: ambiguity=1 (only the relayed one)" || bad "type ambiguity" "$OUT"
|
|
48
|
+
printf '%s\n' "$OUT" | grep -qE 'done[[:space:]]+1' && ok "type: done=1" || bad "type done" "$OUT"
|
|
49
|
+
printf '%s\n' "$OUT" | grep -qE 'halt[[:space:]]+1' && ok "type: halt=1" || bad "type halt" "$OUT"
|
|
50
|
+
|
|
51
|
+
# 2. latency — max is the +30min catch-up ping
|
|
52
|
+
printf '%s\n' "$OUT" | grep -qE 'max 1800s' && ok "latency max = 1800s (the catch-up ping)" || bad "latency max" "$OUT"
|
|
53
|
+
|
|
54
|
+
# 3. discrepancy — the lying halt
|
|
55
|
+
printf '%s\n' "$OUT" | grep -qE 'Discrepancies caught.*: 1' && ok "discrepancy count = 1 (payload claimed a merge the host denied)" || bad "discrepancy" "$OUT"
|
|
56
|
+
|
|
57
|
+
# 4. presence proxy — 1 & 2 within window, 3 landed while away
|
|
58
|
+
printf '%s\n' "$OUT" | grep -qE '2/3 relayed live' && ok "presence proxy: 2/3 live, 1 landed while away" || bad "presence proxy" "$OUT"
|
|
59
|
+
|
|
60
|
+
# 5. the presence line labels itself a proxy (never a measured SLA)
|
|
61
|
+
printf '%s\n' "$OUT" | grep -qiE 'proxy' && ok "presence figure is labelled a proxy, not an SLA" || bad "proxy label" "$OUT"
|
|
62
|
+
|
|
63
|
+
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
64
|
+
[ "$FAILED" = 0 ]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test for .forge/checks/forge-jarvis-notify-logonly.sh (m-37 B3; FR-197).
|
|
3
|
+
#
|
|
4
|
+
# Pins the log-only channel's whole contract: one TSV line per ping at the
|
|
5
|
+
# FORGE_SLICE_NOTIFY_LOG path, field-forging bytes scrubbed from caller input,
|
|
6
|
+
# best-effort exit 0 even when the write fails, and the FORGE_SLICE_WORK_DIR
|
|
7
|
+
# default location. Pure fixtures.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
CHAN="$SCRIPT_DIR/../forge-jarvis-notify-logonly.sh"
|
|
12
|
+
[ -f "$CHAN" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$CHAN" >&2
|
|
13
|
+
|
|
14
|
+
ROOT="$(mktemp -d)"
|
|
15
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
16
|
+
|
|
17
|
+
PASSED=0; FAILED=0
|
|
18
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
19
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
20
|
+
TAB="$(printf '\t')"
|
|
21
|
+
|
|
22
|
+
# 1. appends exactly one TSV line: ts <TAB> type <TAB> payload
|
|
23
|
+
LOG="$ROOT/notify.log"
|
|
24
|
+
FORGE_SLICE_NOTIFY_LOG="$LOG" sh "$CHAN" ambiguity "JWT or sessions?"; RC=$?
|
|
25
|
+
if [ "$RC" -eq 0 ] && [ "$(wc -l < "$LOG" | tr -d ' ')" = 1 ] \
|
|
26
|
+
&& grep -q "${TAB}ambiguity${TAB}JWT or sessions?$" "$LOG"; then
|
|
27
|
+
ok 'one ping → one TSV line (ts, type, payload), exit 0'
|
|
28
|
+
else
|
|
29
|
+
bad 'one ping → one TSV line (ts, type, payload), exit 0' "rc=$RC log=$(cat "$LOG" 2>/dev/null)"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 2. a payload with embedded newline/tab cannot forge lines or fields
|
|
33
|
+
FORGE_SLICE_NOTIFY_LOG="$LOG" sh "$CHAN" done "line1
|
|
34
|
+
line2${TAB}forged-field"
|
|
35
|
+
LINES="$(wc -l < "$LOG" | tr -d ' ')"
|
|
36
|
+
FIELDS="$(tail -1 "$LOG" | awk -F"$TAB" '{print NF}')"
|
|
37
|
+
if [ "$LINES" = 2 ] && [ "$FIELDS" = 3 ]; then
|
|
38
|
+
ok 'newline/tab in payload scrubbed — no forged lines, still 3 TSV fields'
|
|
39
|
+
else
|
|
40
|
+
bad 'newline/tab in payload scrubbed — no forged lines, still 3 TSV fields' "lines=$LINES fields=$FIELDS"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 3. best-effort contract: unwritable log target → still exit 0 (never a slice failure)
|
|
44
|
+
FORGE_SLICE_NOTIFY_LOG="$ROOT/no-perm/notify.log" sh -c 'mkdir -p "'"$ROOT"'/no-perm" && chmod 555 "'"$ROOT"'/no-perm"'
|
|
45
|
+
FORGE_SLICE_NOTIFY_LOG="$ROOT/no-perm/notify.log" sh "$CHAN" done "cannot land"; RC=$?
|
|
46
|
+
chmod 755 "$ROOT/no-perm" 2>/dev/null
|
|
47
|
+
if [ "$RC" -eq 0 ]; then
|
|
48
|
+
ok 'unwritable log → exit 0 (best-effort, never a slice failure)'
|
|
49
|
+
else
|
|
50
|
+
bad 'unwritable log → exit 0 (best-effort, never a slice failure)' "rc=$RC"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# 4. default location: FORGE_SLICE_WORK_DIR/notify.log when NOTIFY_LOG is unset
|
|
54
|
+
WORK="$ROOT/work"; mkdir -p "$WORK"
|
|
55
|
+
( unset FORGE_SLICE_NOTIFY_LOG 2>/dev/null; FORGE_SLICE_WORK_DIR="$WORK" sh "$CHAN" note "hello" )
|
|
56
|
+
if [ -f "$WORK/notify.log" ] && grep -q "${TAB}note${TAB}hello$" "$WORK/notify.log"; then
|
|
57
|
+
ok 'default location is FORGE_SLICE_WORK_DIR/notify.log'
|
|
58
|
+
else
|
|
59
|
+
bad 'default location is FORGE_SLICE_WORK_DIR/notify.log' "$(ls "$WORK" 2>/dev/null)"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
printf '\nforge-jarvis-notify-logonly.test.sh: %s passed, %s failed\n' "$PASSED" "$FAILED"
|
|
63
|
+
[ "$FAILED" -eq 0 ]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture test suite for .forge/checks/forge-jarvis-promote.sh (m-37, step 4 B6; plan 70-01).
|
|
3
|
+
#
|
|
4
|
+
# Contract under test (handoff-step4-jarvis.md REV2 B6; FR-208; NFR-038; operator ruling 3
|
|
5
|
+
# "trigger-not-approve"; acceptance #7 is the manual human gate, not here):
|
|
6
|
+
# - SHOWS host state (the board projection) BEFORE triggering — reuses the B4/B5 host-truth
|
|
7
|
+
# env contract (FORGE_JARVIS_BOARD), degrades cleanly when the board is unavailable.
|
|
8
|
+
# - TRIGGERS the step-2 seam via `gh workflow run promote.yml -f sha=<full-sha>` and nothing
|
|
9
|
+
# else — a real (non-dry-run) run actually invokes gh (proven with a recording stub).
|
|
10
|
+
# - NEG-CONTROL: the helper contains NO Environment-approval path (no --approve, no
|
|
11
|
+
# `gh api ... environments ... approve`, no approve-a-deployment call). Triggering != approving.
|
|
12
|
+
# - refuses a non-full sha (Jarvis never invents/truncates a sha).
|
|
13
|
+
#
|
|
14
|
+
# The board + gh are stubbed via env so no real repo/main/gh state is needed. Each assertion
|
|
15
|
+
# prints WHY. RED-friendly: a missing/broken script fails every case and exits non-zero.
|
|
16
|
+
#
|
|
17
|
+
# Usage: ./.forge/checks/tests/forge-jarvis-promote.test.sh
|
|
18
|
+
set -u
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
PROMOTE="$SCRIPT_DIR/../forge-jarvis-promote.sh"
|
|
22
|
+
[ -f "$PROMOTE" ] || printf 'NOTE: %s missing — RED phase, every case should FAIL\n' "$PROMOTE" >&2
|
|
23
|
+
|
|
24
|
+
ROOT="$(mktemp -d)"
|
|
25
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
26
|
+
|
|
27
|
+
PASSED=0; FAILED=0
|
|
28
|
+
ok() { PASSED=$((PASSED+1)); printf ' ok %s\n' "$1"; }
|
|
29
|
+
bad() { FAILED=$((FAILED+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
|
|
30
|
+
|
|
31
|
+
SHA="0123456789abcdef0123456789abcdef01234567" # a well-formed full 40-hex sha
|
|
32
|
+
|
|
33
|
+
# --- stub board: emit line-oriented JSON with one merged + one complete unit ------------------
|
|
34
|
+
BOARD_OK="$ROOT/board-ok.sh"
|
|
35
|
+
cat > "$BOARD_OK" <<'EOF'
|
|
36
|
+
#!/usr/bin/env sh
|
|
37
|
+
# emulate forge-board-render.sh --emit: one line per unit
|
|
38
|
+
printf '{"forge_id":"m-30","status":"complete","release_state":"merged"}\n'
|
|
39
|
+
printf '{"forge_id":"m-99","status":"complete","release_state":"complete"}\n'
|
|
40
|
+
EOF
|
|
41
|
+
chmod +x "$BOARD_OK"
|
|
42
|
+
|
|
43
|
+
# --- recording gh stub: append its args so we can prove the trigger actually fired ------------
|
|
44
|
+
GH_REC="$ROOT/gh-rec.sh"
|
|
45
|
+
GH_LOG="$ROOT/gh-args.log"
|
|
46
|
+
cat > "$GH_REC" <<EOF
|
|
47
|
+
#!/usr/bin/env sh
|
|
48
|
+
printf '%s\n' "\$*" >> "$GH_LOG"
|
|
49
|
+
exit 0
|
|
50
|
+
EOF
|
|
51
|
+
chmod +x "$GH_REC"
|
|
52
|
+
|
|
53
|
+
# 1. dry-run SHOWS the board state BEFORE the trigger line (show-then-trigger ordering)
|
|
54
|
+
out="$(FORGE_JARVIS_BOARD="$BOARD_OK" FORGE_JARVIS_GH=gh sh "$PROMOTE" "$SHA" --dry-run 2>/dev/null)"
|
|
55
|
+
show_ln="$(printf '%s\n' "$out" | grep -n 'm-30' | head -1 | cut -d: -f1)"
|
|
56
|
+
trig_ln="$(printf '%s\n' "$out" | grep -n 'would run' | head -1 | cut -d: -f1)"
|
|
57
|
+
if [ -n "$show_ln" ] && [ -n "$trig_ln" ] && [ "$show_ln" -lt "$trig_ln" ]; then
|
|
58
|
+
ok "shows host state (merged unit m-30) BEFORE the trigger command"
|
|
59
|
+
else
|
|
60
|
+
bad "show-then-trigger" "show line=$show_ln trigger line=$trig_ln; out: $out"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# 2. dry-run prints the step-2 seam command: gh workflow run promote.yml -f sha=<sha>
|
|
64
|
+
case "$out" in
|
|
65
|
+
*"gh workflow run promote.yml -f sha=$SHA"*) ok "dry-run prints 'gh workflow run promote.yml -f sha=<sha>' (the step-2 seam)" ;;
|
|
66
|
+
*) bad "trigger command" "no 'gh workflow run promote.yml -f sha=$SHA' in: $out" ;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
# 3. a real (non-dry-run) run ACTUALLY invokes gh with the workflow trigger (recording stub)
|
|
70
|
+
: > "$GH_LOG"
|
|
71
|
+
FORGE_JARVIS_BOARD="$BOARD_OK" FORGE_JARVIS_GH="$GH_REC" sh "$PROMOTE" "$SHA" >/dev/null 2>&1
|
|
72
|
+
if grep -qF "workflow run promote.yml -f sha=$SHA" "$GH_LOG"; then
|
|
73
|
+
ok "real run invokes gh: 'workflow run promote.yml -f sha=<sha>' recorded"
|
|
74
|
+
else
|
|
75
|
+
bad "real trigger" "gh stub not called with the trigger args; log: $(cat "$GH_LOG" 2>/dev/null)"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# 4. NEG-CONTROL — the helper has NO Environment-approval path anywhere in the file
|
|
79
|
+
if grep -qiE 'gh api.*environments.*approve|approve.*deployment|--approve' "$PROMOTE"; then
|
|
80
|
+
bad "no-approval neg-control" "an approval path exists in $PROMOTE (triggering must never approve)"
|
|
81
|
+
else
|
|
82
|
+
ok "neg-control: no approval API path in the helper (triggers, never approves)"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# 5. refuses a non-full sha (never invents/truncates one)
|
|
86
|
+
if FORGE_JARVIS_BOARD="$BOARD_OK" sh "$PROMOTE" "abc123" --dry-run >/dev/null 2>&1; then
|
|
87
|
+
bad "short-sha refusal" "a short sha was accepted; promotion needs the full 40-hex sha"
|
|
88
|
+
else
|
|
89
|
+
ok "refuses a non-full (short) sha — containment needs the full deploy sha"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# 6. board unavailable degrades: dry-run still renders + still prints the trigger (exit 0)
|
|
93
|
+
out="$(FORGE_JARVIS_BOARD="$ROOT/does-not-exist.sh" FORGE_JARVIS_GH=gh sh "$PROMOTE" "$SHA" --dry-run 2>/dev/null)"
|
|
94
|
+
rc=$?
|
|
95
|
+
case "$out" in
|
|
96
|
+
*"projection unavailable"*"would run: gh workflow run promote.yml"*) [ "$rc" -eq 0 ] && ok "board-down degrades: show still renders and the trigger still prints (exit 0)" || bad "degrade exit" "rc=$rc" ;;
|
|
97
|
+
*) bad "board-down degrade" "expected a degraded projection note + the trigger; got: $out" ;;
|
|
98
|
+
esac
|
|
99
|
+
|
|
100
|
+
# --- summary -----------------------------------------------------------------
|
|
101
|
+
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
102
|
+
[ "$FAILED" = 0 ]
|