@windyroad/architect 0.20.3 → 0.20.4
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/package.json +6 -2
- package/agents/test/architect-codex-eval-contract.bats +0 -33
- package/agents/test/architect-needs-direction-verdict.bats +0 -64
- package/agents/test/architect-output-formatting.bats +0 -26
- package/agents/test/architect-performance-review.bats +0 -76
- package/agents/test/architect-pre-edit-review-mode.bats +0 -50
- package/agents/test/architect-unratified-dependency-verdict.bats +0 -55
- package/hooks/test/architect-compendium-update-entry.bats +0 -299
- package/hooks/test/architect-detect-once-per-session.bats +0 -106
- package/hooks/test/architect-detect-scope.bats +0 -56
- package/hooks/test/architect-enforce-scope.bats +0 -124
- package/hooks/test/architect-gate.bats +0 -102
- package/hooks/test/architect-mark-reviewed-verdict-grep.bats +0 -113
- package/hooks/test/architect-mark-reviewed.bats +0 -26
- package/hooks/test/architect-marker-only-exempt.bats +0 -183
- package/hooks/test/architect-no-stop-hook.bats +0 -17
- package/hooks/test/architect-oversight-marker-discipline.bats +0 -245
- package/hooks/test/architect-oversight-nudge.bats +0 -75
- package/hooks/test/architect-project-root.bats +0 -81
- package/hooks/test/architect-readme-pairing-check.bats +0 -126
- package/hooks/test/architect-slide-marker.bats +0 -92
- package/hooks/test/codex-agent-completion.bats +0 -64
- package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
- package/hooks/test/substance-aware-drift.bats +0 -243
- package/scripts/test/check-amends-backreference.bats +0 -68
- package/scripts/test/codex-pack-and-agent.bats +0 -86
- package/scripts/test/detect-unoversighted.bats +0 -139
- package/scripts/test/generate-decisions-compendium.bats +0 -210
- package/scripts/test/is-decision-unconfirmed.bats +0 -143
- package/skills/capture-adr/test/capture-adr.bats +0 -362
- package/skills/create-adr/test/create-adr-adr-044-contract.bats +0 -185
- package/skills/create-adr/test/create-adr-codex-eval-contract.bats +0 -34
- package/skills/create-adr/test/create-adr-decision-boundary.bats +0 -49
- package/skills/create-adr/test/create-adr-id-collision-guard.bats +0 -35
- package/skills/create-adr/test/create-adr-next-id-origin-lookup.bats +0 -45
- package/skills/create-adr/test/create-adr-rename-restage-reminder.bats +0 -38
- package/skills/create-adr/test/create-adr-substance-confirm-pattern.bats +0 -169
- package/skills/review-design/test/review-design-codex-contract.bats +0 -22
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Behavioural tests for architect-readme-pairing-check.sh (RFC-014 Story B,
|
|
4
|
-
# ADR-078 Phase 1 / Option 9). Exercises the hook against a real staged git
|
|
5
|
-
# index; asserts on its PreToolUse allow/deny decision (exit code + deny JSON).
|
|
6
|
-
# Behavioural — no grep on hook source (feedback_behavioural_tests).
|
|
7
|
-
|
|
8
|
-
setup() {
|
|
9
|
-
HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/architect-readme-pairing-check.sh"
|
|
10
|
-
REPO="$(mktemp -d)"
|
|
11
|
-
cd "$REPO"
|
|
12
|
-
git init -q
|
|
13
|
-
git config user.email t@e.x
|
|
14
|
-
git config user.name t
|
|
15
|
-
mkdir -p docs/decisions
|
|
16
|
-
echo "# compendium" > docs/decisions/README.md
|
|
17
|
-
echo "# adr 049" > docs/decisions/049-x.proposed.md
|
|
18
|
-
git add -A && git commit -q -m init
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
teardown() {
|
|
22
|
-
cd /
|
|
23
|
-
rm -rf "$REPO"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Run the hook with a synthetic `git commit` Bash PreToolUse payload.
|
|
27
|
-
run_commit_hook() {
|
|
28
|
-
local cmd="${1:-git commit -m wip}"
|
|
29
|
-
echo "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"$cmd\"}}" | bash "$HOOK"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@test "denies commit when an ADR body is staged without README (criterion 1)" {
|
|
33
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
34
|
-
git add docs/decisions/049-x.proposed.md
|
|
35
|
-
run run_commit_hook
|
|
36
|
-
[ "$status" -eq 2 ]
|
|
37
|
-
[[ "$output" == *"deny"* ]]
|
|
38
|
-
[[ "$output" == *"049-x.proposed.md"* ]]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@test "permits commit when ADR body AND README are both staged (criterion 2)" {
|
|
42
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
43
|
-
echo "# compendium refreshed" > docs/decisions/README.md
|
|
44
|
-
git add docs/decisions/049-x.proposed.md docs/decisions/README.md
|
|
45
|
-
run run_commit_hook
|
|
46
|
-
[ "$status" -eq 0 ]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@test "permits commit when only README is staged (compendium-only edit) (criterion 3)" {
|
|
50
|
-
echo "# compendium refreshed" > docs/decisions/README.md
|
|
51
|
-
git add docs/decisions/README.md
|
|
52
|
-
run run_commit_hook
|
|
53
|
-
[ "$status" -eq 0 ]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@test "permits commit when no ADR-touching change is staged (criterion 4)" {
|
|
57
|
-
echo "x" > unrelated.txt
|
|
58
|
-
git add unrelated.txt
|
|
59
|
-
run run_commit_hook
|
|
60
|
-
[ "$status" -eq 0 ]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@test "deny message names the unpaired ADR file + recovery directive (criterion 5)" {
|
|
64
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
65
|
-
git add docs/decisions/049-x.proposed.md
|
|
66
|
-
run run_commit_hook
|
|
67
|
-
[ "$status" -eq 2 ]
|
|
68
|
-
[[ "$output" == *"049-x.proposed.md"* ]]
|
|
69
|
-
[[ "$output" == *"wr-architect-generate-decisions-compendium"* ]]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@test "allows non-commit Bash commands silently" {
|
|
73
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
74
|
-
git add docs/decisions/049-x.proposed.md
|
|
75
|
-
run run_commit_hook "git status"
|
|
76
|
-
[ "$status" -eq 0 ]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
@test "RISK_BYPASS token permits an intentional split" {
|
|
80
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
81
|
-
git add docs/decisions/049-x.proposed.md
|
|
82
|
-
run run_commit_hook "git commit -m 'wip RISK_BYPASS: architect-compendium-deferred'"
|
|
83
|
-
[ "$status" -eq 0 ]
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
# --- P366 leading-token detection regression guards ---
|
|
87
|
-
# These exercise the shared command_invokes_git_commit helper that the hook
|
|
88
|
-
# now sources (replacing inline awk). Permit-path-only coverage is what let
|
|
89
|
-
# the original BSD-awk `\b` bug hide; these are the deny-path / mention-path
|
|
90
|
-
# guards the ticket asks for.
|
|
91
|
-
|
|
92
|
-
@test "denies 'cd <repo> && git commit' with an unpaired ADR (P366 cd-prefix)" {
|
|
93
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
94
|
-
git add docs/decisions/049-x.proposed.md
|
|
95
|
-
run run_commit_hook "cd $REPO && git commit -m wip"
|
|
96
|
-
[ "$status" -eq 2 ]
|
|
97
|
-
[[ "$output" == *"deny"* ]]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@test "denies 'VAR=1 git commit' with an unpaired ADR (P366 env-prefix)" {
|
|
101
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
102
|
-
git add docs/decisions/049-x.proposed.md
|
|
103
|
-
run run_commit_hook "GIT_AUTHOR_NAME=x git commit -m wip"
|
|
104
|
-
[ "$status" -eq 2 ]
|
|
105
|
-
[[ "$output" == *"deny"* ]]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@test "permits a command that merely MENTIONS 'git commit' as a substring (P366 mention-path)" {
|
|
109
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
110
|
-
git add docs/decisions/049-x.proposed.md
|
|
111
|
-
run run_commit_hook "grep -r 'git commit' docs/"
|
|
112
|
-
[ "$status" -eq 0 ]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
@test "permits 'git commit-tree' plumbing (P366 token-boundary)" {
|
|
116
|
-
echo "# adr 049 edited" > docs/decisions/049-x.proposed.md
|
|
117
|
-
git add docs/decisions/049-x.proposed.md
|
|
118
|
-
run run_commit_hook "git commit-tree HEAD"
|
|
119
|
-
[ "$status" -eq 0 ]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
@test "registered in hooks.json as PreToolUse Bash (criterion 6)" {
|
|
123
|
-
HOOKS_JSON="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/hooks.json"
|
|
124
|
-
run jq -e '.hooks.PreToolUse[] | select(.matcher=="Bash") | .hooks[] | select(.command | test("architect-readme-pairing-check"))' "$HOOKS_JSON"
|
|
125
|
-
[ "$status" -eq 0 ]
|
|
126
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Hook-level integration tests for architect-slide-marker.sh (P111).
|
|
4
|
-
# Verifies that the PostToolUse:Agent|Bash hook correctly wires session_id
|
|
5
|
-
# extraction + slide_marker_on_subprocess_return for the architect markers.
|
|
6
|
-
|
|
7
|
-
setup() {
|
|
8
|
-
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
9
|
-
HOOK="$HOOKS_DIR/architect-slide-marker.sh"
|
|
10
|
-
TEST_SESSION="bats-arch-slide-$$-${BATS_TEST_NUMBER}"
|
|
11
|
-
REVIEW_MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
12
|
-
PLAN_MARKER="/tmp/architect-plan-reviewed-${TEST_SESSION}"
|
|
13
|
-
rm -f "$REVIEW_MARKER" "$PLAN_MARKER"
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
teardown() {
|
|
17
|
-
rm -f "$REVIEW_MARKER" "$PLAN_MARKER"
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
_backdate() {
|
|
21
|
-
local file="$1" seconds="$2"
|
|
22
|
-
local stamp
|
|
23
|
-
stamp=$(date -v-${seconds}S +%Y%m%d%H%M.%S 2>/dev/null \
|
|
24
|
-
|| date -d "${seconds} seconds ago" +%Y%m%d%H%M.%S 2>/dev/null)
|
|
25
|
-
touch -t "$stamp" "$file"
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@test "hook: slides architect-reviewed marker on subprocess return" {
|
|
29
|
-
touch "$REVIEW_MARKER"
|
|
30
|
-
_backdate "$REVIEW_MARKER" 60
|
|
31
|
-
BEFORE=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
32
|
-
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Agent","tool_response":{"content":[]}}' | "$HOOK"
|
|
33
|
-
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
34
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@test "hook: slides architect-plan-reviewed marker too" {
|
|
38
|
-
touch "$PLAN_MARKER"
|
|
39
|
-
_backdate "$PLAN_MARKER" 60
|
|
40
|
-
BEFORE=$(stat -c%Y "$PLAN_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$PLAN_MARKER")
|
|
41
|
-
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Agent","tool_response":{"content":[]}}' | "$HOOK"
|
|
42
|
-
AFTER=$(stat -c%Y "$PLAN_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$PLAN_MARKER")
|
|
43
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@test "hook: skips slide when tool_response.is_error=true" {
|
|
47
|
-
touch "$REVIEW_MARKER"
|
|
48
|
-
_backdate "$REVIEW_MARKER" 60
|
|
49
|
-
BEFORE=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
50
|
-
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Bash","tool_response":{"is_error":true,"content":[]}}' | "$HOOK"
|
|
51
|
-
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
52
|
-
[ "$BEFORE" = "$AFTER" ]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@test "hook: no-op when no marker exists (never creates)" {
|
|
56
|
-
[ ! -f "$REVIEW_MARKER" ]
|
|
57
|
-
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Agent","tool_response":{"content":[]}}' | "$HOOK"
|
|
58
|
-
[ ! -f "$REVIEW_MARKER" ]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@test "hook: exits 0 when session_id is missing" {
|
|
62
|
-
run bash -c 'echo "{}" | '"$HOOK"
|
|
63
|
-
[ "$status" -eq 0 ]
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
@test "hook: P111 reproduction — long subprocess does not cause parent marker expiry on return" {
|
|
67
|
-
touch "$REVIEW_MARKER"
|
|
68
|
-
# Marker is 50 minutes old, well within default 60-min TTL but close.
|
|
69
|
-
# Without the slide on subprocess return, a 15-min subprocess would push
|
|
70
|
-
# the next PreToolUse check past TTL. With the slide, the next check sees
|
|
71
|
-
# a fresh marker.
|
|
72
|
-
_backdate "$REVIEW_MARKER" 3000
|
|
73
|
-
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Bash","tool_input":{"command":"claude -p ..."},"tool_response":{"content":[{"type":"text","text":"OK"}]}}' | "$HOOK"
|
|
74
|
-
NOW=$(date +%s)
|
|
75
|
-
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
76
|
-
AGE=$((NOW - AFTER))
|
|
77
|
-
[ "$AGE" -lt 5 ]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
@test "hook: P213 — slides on Skill tool_name (Agent|Bash|Skill matcher expansion)" {
|
|
81
|
-
# Confirms hooks.json matcher expansion (Agent|Bash → Agent|Bash|Skill)
|
|
82
|
-
# composes correctly with the existing hook script. The hook script does
|
|
83
|
-
# not filter on tool_name; the matcher in hooks.json gates which tool
|
|
84
|
-
# completions fire the script. This test asserts the script handles the
|
|
85
|
-
# Skill-shaped tool_response identically to Agent|Bash.
|
|
86
|
-
touch "$REVIEW_MARKER"
|
|
87
|
-
_backdate "$REVIEW_MARKER" 60
|
|
88
|
-
BEFORE=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
89
|
-
echo '{"session_id":"'"$TEST_SESSION"'","tool_name":"Skill","tool_input":{"skill":"wr-risk-scorer:assess-release"},"tool_response":{"content":[{"type":"text","text":"OK"}]}}' | "$HOOK"
|
|
90
|
-
AFTER=$(stat -c%Y "$REVIEW_MARKER" 2>/dev/null || /usr/bin/stat -f%m "$REVIEW_MARKER")
|
|
91
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
92
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
setup() {
|
|
4
|
-
HOOK_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
5
|
-
TMP="$(mktemp -d)"
|
|
6
|
-
export TMPDIR="$TMP/runtime"
|
|
7
|
-
export CLAUDE_PROJECT_DIR="$TMP/project"
|
|
8
|
-
mkdir -p "$TMPDIR" "$CLAUDE_PROJECT_DIR/docs/decisions"
|
|
9
|
-
SESSION="architect-codex-$$"
|
|
10
|
-
TARGET="agent-target"
|
|
11
|
-
MARKER="/tmp/architect-reviewed-$SESSION"
|
|
12
|
-
HASH="$MARKER.hash"
|
|
13
|
-
PLAN="/tmp/architect-plan-reviewed-$SESSION"
|
|
14
|
-
rm -f "$MARKER" "$HASH" "$PLAN"
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
teardown() {
|
|
18
|
-
rm -f "$MARKER" "$HASH" "$PLAN"
|
|
19
|
-
rm -rf "$TMP"
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
dispatch() {
|
|
23
|
-
printf '%s' "$1" | node "$HOOK_DIR/codex-agent-completion.mjs"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
spawn() {
|
|
27
|
-
printf '{"session_id":"%s","cwd":"%s","tool_name":"multi_agent_v1__spawn_agent","tool_input":{"agent_type":"%s"},"tool_response":{"agent_id":"%s"}}' "$SESSION" "$CLAUDE_PROJECT_DIR" "$1" "$TARGET"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
close() {
|
|
31
|
-
printf '{"session_id":"%s","cwd":"%s","tool_name":"multi_agent_v1__close_agent","tool_input":{"target":"%s"},"tool_response":{"previous_status":{"completed":"%s"}}}' "$SESSION" "$CLAUDE_PROJECT_DIR" "$TARGET" "$1"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@test "matched Codex architect PASS creates review markers" {
|
|
35
|
-
dispatch "$(spawn wr-architect:agent)"
|
|
36
|
-
dispatch "$(close '**Architecture Review: PASS**')"
|
|
37
|
-
[ -f "$MARKER" ]
|
|
38
|
-
[ -f "$HASH" ]
|
|
39
|
-
[ -f "$PLAN" ]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@test "issues and malformed output fail closed" {
|
|
43
|
-
dispatch "$(spawn wr-architect:agent)"
|
|
44
|
-
dispatch "$(close '**Architecture Review: ISSUES FOUND**')"
|
|
45
|
-
[ ! -e "$MARKER" ]
|
|
46
|
-
dispatch "$(spawn wr-architect:agent)"
|
|
47
|
-
dispatch "$(close 'review complete')"
|
|
48
|
-
[ ! -e "$MARKER" ]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@test "unmatched close and non-architect target do not unlock" {
|
|
52
|
-
dispatch "$(close '**Architecture Review: PASS**')"
|
|
53
|
-
[ ! -e "$MARKER" ]
|
|
54
|
-
dispatch "$(spawn default)"
|
|
55
|
-
dispatch "$(close '**Architecture Review: PASS**')"
|
|
56
|
-
[ ! -e "$MARKER" ]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
@test "reused target clears stale architect identity" {
|
|
60
|
-
dispatch "$(spawn wr-architect:agent)"
|
|
61
|
-
dispatch "$(spawn default)"
|
|
62
|
-
dispatch "$(close '**Architecture Review: PASS**')"
|
|
63
|
-
[ ! -e "$MARKER" ]
|
|
64
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Tests for slide_marker_on_subprocess_return helper (P111).
|
|
4
|
-
#
|
|
5
|
-
# Behavioural contract:
|
|
6
|
-
# - Slides an existing marker forward (touch) on PostToolUse:Agent|Bash
|
|
7
|
-
# completion, treating subprocess wall-clock as continuous parent-session
|
|
8
|
-
# work for TTL purposes.
|
|
9
|
-
# - Never CREATES a marker (creating requires a real gate review).
|
|
10
|
-
# - Skips slide on subprocess error (tool_response.is_error=true) so a failed
|
|
11
|
-
# subprocess does NOT extend the parent's trust window (ADR-009 amendment).
|
|
12
|
-
# - No-op when no marker exists or session_id is empty (fail-safe).
|
|
13
|
-
|
|
14
|
-
setup() {
|
|
15
|
-
HOOKS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
16
|
-
source "$HOOKS_DIR/lib/gate-helpers.sh"
|
|
17
|
-
|
|
18
|
-
TEST_SESSION="bats-slide-$$-${BATS_TEST_NUMBER}"
|
|
19
|
-
MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
20
|
-
rm -f "$MARKER"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
teardown() {
|
|
24
|
-
rm -f "$MARKER"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
# Helper: backdate file mtime by N seconds (portable between macOS and Linux)
|
|
28
|
-
_backdate() {
|
|
29
|
-
local file="$1" seconds="$2"
|
|
30
|
-
local stamp
|
|
31
|
-
stamp=$(date -v-${seconds}S +%Y%m%d%H%M.%S 2>/dev/null \
|
|
32
|
-
|| date -d "${seconds} seconds ago" +%Y%m%d%H%M.%S 2>/dev/null)
|
|
33
|
-
touch -t "$stamp" "$file"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@test "slide: existing marker is touched on success response" {
|
|
37
|
-
touch "$MARKER"
|
|
38
|
-
_backdate "$MARKER" 60
|
|
39
|
-
BEFORE=$(_mtime "$MARKER")
|
|
40
|
-
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
41
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
42
|
-
AFTER=$(_mtime "$MARKER")
|
|
43
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@test "slide: long-running subprocess does NOT cause parent marker expiry on return (P111 reproduction)" {
|
|
47
|
-
# Simulate the P111 failure mode: parent's marker is set, then a long
|
|
48
|
-
# subprocess runs (we backdate the marker to simulate elapsed wall-clock),
|
|
49
|
-
# then PostToolUse fires with a successful tool_response. The marker mtime
|
|
50
|
-
# must be refreshed so the parent's NEXT PreToolUse gate check (which
|
|
51
|
-
# compares NOW - mtime against TTL) sees a fresh marker.
|
|
52
|
-
touch "$MARKER"
|
|
53
|
-
# Marker is 50 minutes old — under default 60-min TTL but close to expiry.
|
|
54
|
-
# Without the slide on subprocess return, a subsequent 15-min subprocess
|
|
55
|
-
# would push the mtime past TTL and the next PreToolUse would deny.
|
|
56
|
-
_backdate "$MARKER" 3000
|
|
57
|
-
BEFORE=$(_mtime "$MARKER")
|
|
58
|
-
_HOOK_INPUT='{"tool_response":{"content":[{"type":"text","text":"OK"}]}}'
|
|
59
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
60
|
-
AFTER=$(_mtime "$MARKER")
|
|
61
|
-
NOW=$(date +%s)
|
|
62
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
63
|
-
# And the new mtime is approximately NOW (within 5 seconds of slide call)
|
|
64
|
-
AGE=$((NOW - AFTER))
|
|
65
|
-
[ "$AGE" -lt 5 ]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
@test "slide: does NOT touch marker when tool_response.is_error=true" {
|
|
69
|
-
touch "$MARKER"
|
|
70
|
-
_backdate "$MARKER" 60
|
|
71
|
-
BEFORE=$(_mtime "$MARKER")
|
|
72
|
-
_HOOK_INPUT='{"tool_response":{"is_error":true,"content":[]}}'
|
|
73
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
74
|
-
AFTER=$(_mtime "$MARKER")
|
|
75
|
-
[ "$BEFORE" = "$AFTER" ]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
@test "slide: no-op when marker does not exist (never creates)" {
|
|
79
|
-
[ ! -f "$MARKER" ]
|
|
80
|
-
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
81
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
82
|
-
[ ! -f "$MARKER" ]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@test "slide: no-op when marker path argument is empty" {
|
|
86
|
-
_HOOK_INPUT='{"tool_response":{"content":[]}}'
|
|
87
|
-
run slide_marker_on_subprocess_return ""
|
|
88
|
-
[ "$status" -eq 0 ]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@test "slide: malformed hook input is fail-safe (no slide)" {
|
|
92
|
-
touch "$MARKER"
|
|
93
|
-
_backdate "$MARKER" 60
|
|
94
|
-
BEFORE=$(_mtime "$MARKER")
|
|
95
|
-
_HOOK_INPUT='not valid json'
|
|
96
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
97
|
-
AFTER=$(_mtime "$MARKER")
|
|
98
|
-
# Fail-safe: when the hook input cannot be parsed, treat as error and skip
|
|
99
|
-
[ "$BEFORE" = "$AFTER" ]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@test "slide: triggers correctly on Skill tool_response shape (P213, ADR-009 2026-06-08 amendment)" {
|
|
103
|
-
# hooks.json matcher expansion Agent|Bash → Agent|Bash|Skill (P213 Option D)
|
|
104
|
-
# widens slide-marker coverage to PostToolUse:Skill completions (e.g. the
|
|
105
|
-
# /wr-risk-scorer:assess-* sibling assessor SKILLs run as long subprocesses
|
|
106
|
-
# by the AFK orchestrator). The Skill tool_response shape is identical to
|
|
107
|
-
# Agent|Bash (Claude Code's uniform PostToolUse contract), so the matcher-
|
|
108
|
-
# agnostic helper composes without code changes. This test documents that
|
|
109
|
-
# contract explicitly so a future hook_input shape divergence regression
|
|
110
|
-
# surfaces here rather than at the gate-denial site.
|
|
111
|
-
touch "$MARKER"
|
|
112
|
-
_backdate "$MARKER" 60
|
|
113
|
-
BEFORE=$(_mtime "$MARKER")
|
|
114
|
-
_HOOK_INPUT='{"tool_name":"Skill","tool_response":{"content":[{"type":"text","text":"OK"}]}}'
|
|
115
|
-
slide_marker_on_subprocess_return "$MARKER"
|
|
116
|
-
AFTER=$(_mtime "$MARKER")
|
|
117
|
-
[ "$AFTER" -gt "$BEFORE" ]
|
|
118
|
-
}
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Behavioural tests for ADR-009 amendment 2026-06-06: substance-aware drift +
|
|
4
|
-
# atomic verdict-write. Closes P353 (hash-marker brittleness umbrella) +
|
|
5
|
-
# P303 (architect-gate multi-ADR deadlock drift-relock facet).
|
|
6
|
-
#
|
|
7
|
-
# Cases ratified by the user 2026-06-06:
|
|
8
|
-
# (a) Trivial edit (whitespace, CRLF, trailing newline) does NOT re-trigger.
|
|
9
|
-
# (b) Substantive edit (new content, changed line) DOES re-trigger.
|
|
10
|
-
# (c) Atomic write — marker + hash file land together on PASS, with the
|
|
11
|
-
# marker's mtime current and the hash file's content equal to the
|
|
12
|
-
# substance hash of the policy content.
|
|
13
|
-
# (d) Conservative fallback — semantic edits beyond the documented
|
|
14
|
-
# whitespace normalisation (e.g. single-numeral change) ARE treated as
|
|
15
|
-
# substantive (re-review fires).
|
|
16
|
-
|
|
17
|
-
setup() {
|
|
18
|
-
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
19
|
-
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
20
|
-
source "$SCRIPT_DIR/lib/architect-gate.sh"
|
|
21
|
-
|
|
22
|
-
TEST_SESSION="bats-arch-substance-$$-${BATS_TEST_NUMBER}"
|
|
23
|
-
TEST_DIR=$(mktemp -d -t substance-aware-drift.XXXXXX)
|
|
24
|
-
mkdir -p "$TEST_DIR/docs/decisions"
|
|
25
|
-
CLAUDE_PROJECT_DIR="$TEST_DIR"
|
|
26
|
-
export CLAUDE_PROJECT_DIR
|
|
27
|
-
|
|
28
|
-
MARKER="/tmp/architect-reviewed-${TEST_SESSION}"
|
|
29
|
-
HASH_FILE="${MARKER}.hash"
|
|
30
|
-
rm -f "$MARKER" "$HASH_FILE"
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
teardown() {
|
|
34
|
-
rm -f "$MARKER" "$HASH_FILE"
|
|
35
|
-
rm -rf "$TEST_DIR"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# Helper: install ADR content, then store the substance hash + marker as the
|
|
39
|
-
# architect-mark-reviewed.sh PASS path would.
|
|
40
|
-
_install_and_mark() {
|
|
41
|
-
local body="$1"
|
|
42
|
-
printf '%s' "$body" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
43
|
-
local hash
|
|
44
|
-
hash=$(_substance_hash_path "$TEST_DIR/docs/decisions")
|
|
45
|
-
_atomic_mark_with_hash "$MARKER" "$hash"
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# ---------------------------------------------------------------------------
|
|
49
|
-
# Case (a) — trivial whitespace / line-ending edits do NOT re-trigger
|
|
50
|
-
# ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
@test "substance-aware: trailing-whitespace edit does NOT re-trigger drift" {
|
|
53
|
-
_install_and_mark "# ADR-123
|
|
54
|
-
|
|
55
|
-
A line of policy content.
|
|
56
|
-
"
|
|
57
|
-
# Add trailing spaces to the same line.
|
|
58
|
-
printf '%s' "# ADR-123
|
|
59
|
-
|
|
60
|
-
A line of policy content.
|
|
61
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
62
|
-
|
|
63
|
-
run check_architect_gate "$TEST_SESSION"
|
|
64
|
-
[ "$status" -eq 0 ]
|
|
65
|
-
[ -f "$MARKER" ]
|
|
66
|
-
[ -f "$HASH_FILE" ]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
@test "substance-aware: CRLF→LF edit does NOT re-trigger drift" {
|
|
70
|
-
_install_and_mark "# ADR-123
|
|
71
|
-
|
|
72
|
-
A line of policy content.
|
|
73
|
-
"
|
|
74
|
-
# Convert the same content to CRLF line endings.
|
|
75
|
-
printf '# ADR-123\r\n\r\nA line of policy content.\r\n' \
|
|
76
|
-
> "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
77
|
-
|
|
78
|
-
run check_architect_gate "$TEST_SESSION"
|
|
79
|
-
[ "$status" -eq 0 ]
|
|
80
|
-
[ -f "$MARKER" ]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@test "substance-aware: extra trailing newlines do NOT re-trigger drift" {
|
|
84
|
-
_install_and_mark "# ADR-123
|
|
85
|
-
|
|
86
|
-
A line of policy content.
|
|
87
|
-
"
|
|
88
|
-
# Add multiple trailing newlines.
|
|
89
|
-
printf '%s' "# ADR-123
|
|
90
|
-
|
|
91
|
-
A line of policy content.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
96
|
-
|
|
97
|
-
run check_architect_gate "$TEST_SESSION"
|
|
98
|
-
[ "$status" -eq 0 ]
|
|
99
|
-
[ -f "$MARKER" ]
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
# ---------------------------------------------------------------------------
|
|
103
|
-
# Case (b) — substantive content edits DO re-trigger
|
|
104
|
-
# ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
@test "substance-aware: new-word edit DOES re-trigger drift" {
|
|
107
|
-
_install_and_mark "# ADR-123
|
|
108
|
-
|
|
109
|
-
A line of policy content.
|
|
110
|
-
"
|
|
111
|
-
# Real content change.
|
|
112
|
-
printf '%s' "# ADR-123
|
|
113
|
-
|
|
114
|
-
A different line of policy content.
|
|
115
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
116
|
-
|
|
117
|
-
run check_architect_gate "$TEST_SESSION"
|
|
118
|
-
[ "$status" -ne 0 ]
|
|
119
|
-
[ ! -f "$MARKER" ]
|
|
120
|
-
[ ! -f "$HASH_FILE" ]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@test "substance-aware: added-paragraph edit DOES re-trigger drift" {
|
|
124
|
-
_install_and_mark "# ADR-123
|
|
125
|
-
|
|
126
|
-
A line of policy content.
|
|
127
|
-
"
|
|
128
|
-
printf '%s' "# ADR-123
|
|
129
|
-
|
|
130
|
-
A line of policy content.
|
|
131
|
-
|
|
132
|
-
A whole new paragraph of substantive change.
|
|
133
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
134
|
-
|
|
135
|
-
run check_architect_gate "$TEST_SESSION"
|
|
136
|
-
[ "$status" -ne 0 ]
|
|
137
|
-
[ ! -f "$MARKER" ]
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@test "substance-aware: new file in docs/decisions DOES re-trigger drift" {
|
|
141
|
-
_install_and_mark "# ADR-123
|
|
142
|
-
|
|
143
|
-
Content.
|
|
144
|
-
"
|
|
145
|
-
# Adding a NEW ADR file is a substantive change.
|
|
146
|
-
printf '%s' "# ADR-124
|
|
147
|
-
|
|
148
|
-
New ADR content.
|
|
149
|
-
" > "$TEST_DIR/docs/decisions/124-new.proposed.md"
|
|
150
|
-
|
|
151
|
-
run check_architect_gate "$TEST_SESSION"
|
|
152
|
-
[ "$status" -ne 0 ]
|
|
153
|
-
[ ! -f "$MARKER" ]
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
# ---------------------------------------------------------------------------
|
|
157
|
-
# Case (c) — atomic write persists the PASS marker reliably
|
|
158
|
-
# ---------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
@test "atomic-write: PASS write lands marker + hash together" {
|
|
161
|
-
printf '%s' "# ADR-123
|
|
162
|
-
|
|
163
|
-
Policy content.
|
|
164
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
165
|
-
local hash
|
|
166
|
-
hash=$(_substance_hash_path "$TEST_DIR/docs/decisions")
|
|
167
|
-
|
|
168
|
-
run _atomic_mark_with_hash "$MARKER" "$hash"
|
|
169
|
-
[ "$status" -eq 0 ]
|
|
170
|
-
[ -f "$MARKER" ]
|
|
171
|
-
[ -f "$HASH_FILE" ]
|
|
172
|
-
# The stored hash equals the substance hash of the policy content.
|
|
173
|
-
[ "$(cat "$HASH_FILE")" = "$hash" ]
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
@test "atomic-write: failed hash-write leaves neither file (no half-state)" {
|
|
177
|
-
# Point the marker into a non-existent directory to force mv failure.
|
|
178
|
-
local bad_marker="/tmp/does-not-exist-bats-$$/architect-reviewed-${TEST_SESSION}"
|
|
179
|
-
run _atomic_mark_with_hash "$bad_marker" "deadbeef"
|
|
180
|
-
[ "$status" -ne 0 ]
|
|
181
|
-
[ ! -f "$bad_marker" ]
|
|
182
|
-
[ ! -f "${bad_marker}.hash" ]
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
@test "atomic-write: PASS write enables a subsequent gate check to allow" {
|
|
186
|
-
printf '%s' "# ADR-123
|
|
187
|
-
|
|
188
|
-
Policy content.
|
|
189
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
190
|
-
local hash
|
|
191
|
-
hash=$(_substance_hash_path "$TEST_DIR/docs/decisions")
|
|
192
|
-
_atomic_mark_with_hash "$MARKER" "$hash"
|
|
193
|
-
|
|
194
|
-
run check_architect_gate "$TEST_SESSION"
|
|
195
|
-
[ "$status" -eq 0 ]
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# ---------------------------------------------------------------------------
|
|
199
|
-
# Case (d) — conservative boundary: semantic edits beyond whitespace
|
|
200
|
-
# normalisation ARE treated as substantive
|
|
201
|
-
# ---------------------------------------------------------------------------
|
|
202
|
-
|
|
203
|
-
@test "conservative: single-numeral change DOES re-trigger drift" {
|
|
204
|
-
_install_and_mark "# ADR-123
|
|
205
|
-
|
|
206
|
-
Threshold is 5 minutes.
|
|
207
|
-
"
|
|
208
|
-
# A single-numeral change is semantic — conservative boundary re-reviews.
|
|
209
|
-
printf '%s' "# ADR-123
|
|
210
|
-
|
|
211
|
-
Threshold is 6 minutes.
|
|
212
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
213
|
-
|
|
214
|
-
run check_architect_gate "$TEST_SESSION"
|
|
215
|
-
[ "$status" -ne 0 ]
|
|
216
|
-
[ ! -f "$MARKER" ]
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
@test "conservative: frontmatter-key change DOES re-trigger drift" {
|
|
220
|
-
_install_and_mark "---
|
|
221
|
-
status: proposed
|
|
222
|
-
date: 2026-06-06
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
# ADR-123
|
|
226
|
-
|
|
227
|
-
Body.
|
|
228
|
-
"
|
|
229
|
-
# Frontmatter date bump — conservative boundary re-reviews.
|
|
230
|
-
printf '%s' "---
|
|
231
|
-
status: proposed
|
|
232
|
-
date: 2026-06-07
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
# ADR-123
|
|
236
|
-
|
|
237
|
-
Body.
|
|
238
|
-
" > "$TEST_DIR/docs/decisions/123-test.proposed.md"
|
|
239
|
-
|
|
240
|
-
run check_architect_gate "$TEST_SESSION"
|
|
241
|
-
[ "$status" -ne 0 ]
|
|
242
|
-
[ ! -f "$MARKER" ]
|
|
243
|
-
}
|