@reddb-io/red-skills 4.4.0 → 4.5.0
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/bin/build-info.mjs +87 -0
- package/bin/cli-frontdoor.mjs +14 -0
- package/bin/red-skills-brain.mjs +17 -22
- package/bin/red-skills-content.mjs +17 -0
- package/bin/red-skills-hook.mjs +45 -0
- package/bin/red-skills-memory.mjs +17 -22
- package/bin/red-skills-redskilled-mcp.mjs +2 -0
- package/bin/red-skills-resource.mjs +21 -0
- package/bin/runtime-compatibility.mjs +17 -0
- package/bin/runtime-paths.mjs +13 -0
- package/dist/code-nav.bundle.min.mjs +1 -1
- package/dist/opencode-host.bundle.min.mjs +1 -1
- package/dist/redskilled-mcp.bundle.min.mjs +36 -36
- package/dist/redskilled.bundle.min.mjs +234 -230
- package/dist/release.bundle.min.mjs +1 -1
- package/dist/rsp-core.bundle.min.mjs +1 -1
- package/dist/rsp.bundle.min.mjs +1 -1
- package/dist/statusline.bundle.min.mjs +1 -1
- package/package.json +9 -4
- package/runtime/hook-routes.json +125 -0
- package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
- package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
- package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
- package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
- package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
- package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
- package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
- package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
- package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
- package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
- package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
- package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
- package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
- package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
- package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
- package/scripts/check-skills-boundary.mjs +32 -0
package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for lib/git-command-classifier.sh (issue #60).
|
|
3
|
+
#
|
|
4
|
+
# Locked to "main", verifies the minimal classifier:
|
|
5
|
+
# block — checkout/switch to a branch other than the lock target
|
|
6
|
+
# allow — switching back to the lock target, file-level `checkout -- <path>`,
|
|
7
|
+
# bare checkout, `git worktree add`, and non-git/non-checkout commands
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
LIB="$(dirname "$HERE")/lib/git-command-classifier.sh"
|
|
13
|
+
|
|
14
|
+
# shellcheck source=../lib/git-command-classifier.sh
|
|
15
|
+
source "$LIB"
|
|
16
|
+
|
|
17
|
+
pass=0
|
|
18
|
+
fail=0
|
|
19
|
+
|
|
20
|
+
expect_verdict() {
|
|
21
|
+
local label="$1" lock="$2" cmd="$3" expected="$4"
|
|
22
|
+
local actual; actual="$(classify_git_command "$lock" "$cmd")"
|
|
23
|
+
if [[ "$actual" == "$expected" ]]; then
|
|
24
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
25
|
+
else
|
|
26
|
+
printf 'FAIL %s\n cmd: %q\n expected: %q\n actual: %q\n' \
|
|
27
|
+
"$label" "$cmd" "$expected" "$actual"
|
|
28
|
+
fail=$((fail + 1))
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
LOCK="main"
|
|
33
|
+
|
|
34
|
+
# ===========================================================================
|
|
35
|
+
# BLOCK — leaving the locked branch
|
|
36
|
+
# ===========================================================================
|
|
37
|
+
expect_verdict "block/checkout other" "$LOCK" "git checkout feature" "block"
|
|
38
|
+
expect_verdict "block/switch other" "$LOCK" "git switch feature" "block"
|
|
39
|
+
expect_verdict "block/checkout slash branch" "$LOCK" "git checkout feature/x" "block"
|
|
40
|
+
expect_verdict "block/checkout -b new" "$LOCK" "git checkout -b shiny" "block"
|
|
41
|
+
expect_verdict "block/switch -c new" "$LOCK" "git switch -c shiny" "block"
|
|
42
|
+
expect_verdict "block/switch dash previous" "$LOCK" "git switch -" "block"
|
|
43
|
+
expect_verdict "block/compound command" "$LOCK" "cd repo && git checkout other" "block"
|
|
44
|
+
expect_verdict "block/checkout --track" "$LOCK" "git checkout --track origin/x" "block"
|
|
45
|
+
|
|
46
|
+
# global options before the subcommand must not bypass the lock
|
|
47
|
+
expect_verdict "block/-C path checkout" "$LOCK" "git -C /repo checkout other" "block"
|
|
48
|
+
expect_verdict "block/-C path switch" "$LOCK" "git -C /repo switch other" "block"
|
|
49
|
+
expect_verdict "block/-C path checkout -b" "$LOCK" "git -C /repo checkout -b shiny" "block"
|
|
50
|
+
expect_verdict "block/-c config checkout" "$LOCK" "git -c core.x=1 checkout other" "block"
|
|
51
|
+
expect_verdict "block/--git-dir eq switch" "$LOCK" "git --git-dir=/r/.git switch o" "block"
|
|
52
|
+
expect_verdict "block/--git-dir arg switch" "$LOCK" "git --git-dir /r/.git switch o" "block"
|
|
53
|
+
expect_verdict "block/--no-pager checkout" "$LOCK" "git --no-pager checkout other" "block"
|
|
54
|
+
expect_verdict "block/-C compound" "$LOCK" "cd x && git -C /r checkout other" "block"
|
|
55
|
+
expect_verdict "block/-C reset --hard" "$LOCK" "git -C /repo reset --hard" "block"
|
|
56
|
+
expect_verdict "block/-C stash" "$LOCK" "git -C /repo stash" "block"
|
|
57
|
+
|
|
58
|
+
# ===========================================================================
|
|
59
|
+
# ALLOW — same-branch / legitimate operations
|
|
60
|
+
# ===========================================================================
|
|
61
|
+
expect_verdict "allow/checkout lock target" "$LOCK" "git checkout main" "allow"
|
|
62
|
+
expect_verdict "allow/switch lock target" "$LOCK" "git switch main" "allow"
|
|
63
|
+
expect_verdict "allow/file restore --" "$LOCK" "git checkout -- src/app.ts" "allow"
|
|
64
|
+
expect_verdict "allow/file restore -- multi" "$LOCK" "git checkout -- a.txt b.txt" "allow"
|
|
65
|
+
expect_verdict "allow/bare checkout" "$LOCK" "git checkout" "allow"
|
|
66
|
+
expect_verdict "allow/worktree add" "$LOCK" "git worktree add ../wt branch" "allow"
|
|
67
|
+
expect_verdict "allow/non-checkout git" "$LOCK" "git status" "allow"
|
|
68
|
+
expect_verdict "allow/non-git command" "$LOCK" "ls -la" "allow"
|
|
69
|
+
expect_verdict "allow/commit" "$LOCK" "git commit -m wip" "allow"
|
|
70
|
+
|
|
71
|
+
# global options before the subcommand: legitimate operations still allowed
|
|
72
|
+
expect_verdict "allow/-C checkout lock" "$LOCK" "git -C /repo checkout main" "allow"
|
|
73
|
+
expect_verdict "allow/-C status" "$LOCK" "git -C /repo status" "allow"
|
|
74
|
+
expect_verdict "allow/-C file restore" "$LOCK" "git -C /repo checkout -- a.ts" "allow"
|
|
75
|
+
expect_verdict "allow/-C worktree add" "$LOCK" "git -C /repo worktree add ../w" "allow"
|
|
76
|
+
|
|
77
|
+
# slash lock target: switching back to it is allowed, elsewhere blocked
|
|
78
|
+
expect_verdict "allow/slash lock back" "feature/x" "git switch feature/x" "allow"
|
|
79
|
+
expect_verdict "block/slash other" "feature/x" "git switch feature/y" "block"
|
|
80
|
+
|
|
81
|
+
# ===========================================================================
|
|
82
|
+
# BLOCK — the work-loss family (issue #61)
|
|
83
|
+
# ===========================================================================
|
|
84
|
+
expect_verdict "block/stash bare" "$LOCK" "git stash" "block"
|
|
85
|
+
expect_verdict "block/stash push" "$LOCK" "git stash push" "block"
|
|
86
|
+
expect_verdict "block/stash push paths" "$LOCK" "git stash push -m x src/" "block"
|
|
87
|
+
expect_verdict "block/stash save" "$LOCK" "git stash save wip" "block"
|
|
88
|
+
expect_verdict "block/clean -f" "$LOCK" "git clean -f" "block"
|
|
89
|
+
expect_verdict "block/clean -fd" "$LOCK" "git clean -fd" "block"
|
|
90
|
+
expect_verdict "block/clean -xfd" "$LOCK" "git clean -xfd" "block"
|
|
91
|
+
expect_verdict "block/clean --force" "$LOCK" "git clean --force" "block"
|
|
92
|
+
expect_verdict "block/reset --hard" "$LOCK" "git reset --hard" "block"
|
|
93
|
+
expect_verdict "block/reset --hard HEAD~1" "$LOCK" "git reset --hard HEAD~1" "block"
|
|
94
|
+
expect_verdict "block/checkout dot" "$LOCK" "git checkout ." "block"
|
|
95
|
+
expect_verdict "block/restore dot" "$LOCK" "git restore ." "block"
|
|
96
|
+
expect_verdict "block/checkout dashdash dot" "$LOCK" "git checkout -- ." "block"
|
|
97
|
+
expect_verdict "block/family compound" "$LOCK" "cd repo && git reset --hard" "block"
|
|
98
|
+
|
|
99
|
+
# ===========================================================================
|
|
100
|
+
# ALLOW — non-destructive members of the same command families
|
|
101
|
+
# ===========================================================================
|
|
102
|
+
expect_verdict "allow/stash list" "$LOCK" "git stash list" "allow"
|
|
103
|
+
expect_verdict "allow/stash show" "$LOCK" "git stash show" "allow"
|
|
104
|
+
expect_verdict "allow/clean dry-run" "$LOCK" "git clean -n" "allow"
|
|
105
|
+
expect_verdict "allow/clean --dry-run" "$LOCK" "git clean --dry-run" "allow"
|
|
106
|
+
expect_verdict "allow/reset soft" "$LOCK" "git reset --soft HEAD~1" "allow"
|
|
107
|
+
expect_verdict "allow/reset mixed default" "$LOCK" "git reset HEAD file.txt" "allow"
|
|
108
|
+
expect_verdict "allow/restore staged" "$LOCK" "git restore --staged f.txt" "allow"
|
|
109
|
+
expect_verdict "allow/restore single file" "$LOCK" "git restore src/app.ts" "allow"
|
|
110
|
+
|
|
111
|
+
# ===========================================================================
|
|
112
|
+
# PRIMARY BRANCH GUARD — dev.lock.primary-branch (issue #396 / ADR 0043)
|
|
113
|
+
# Table: command, cwd-is-primary, flag-state => allow/block.
|
|
114
|
+
# ===========================================================================
|
|
115
|
+
expect_primary_guard() {
|
|
116
|
+
local label="$1" cmd="$2" primary="$3" flag="$4" expected="$5"
|
|
117
|
+
local actual="allow"
|
|
118
|
+
if [[ "$primary" == "primary" && "$flag" == "on" ]]; then
|
|
119
|
+
actual="$(classify_primary_branch_switch_guard "$cmd")"
|
|
120
|
+
fi
|
|
121
|
+
if [[ "$actual" == "$expected" ]]; then
|
|
122
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
123
|
+
else
|
|
124
|
+
printf 'FAIL %s\n cmd: %q\n primary: %q\n flag: %q\n expected: %q\n actual: %q\n' \
|
|
125
|
+
"$label" "$cmd" "$primary" "$flag" "$expected" "$actual"
|
|
126
|
+
fail=$((fail + 1))
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect_primary_guard "primary/on/switch other blocked" "git switch other" primary on block
|
|
131
|
+
expect_primary_guard "primary/on/checkout other blocked" "git checkout other" primary on block
|
|
132
|
+
expect_primary_guard "primary/on/switch -b new blocked" "git switch -b new" primary on block
|
|
133
|
+
expect_primary_guard "primary/on/checkout -b new blocked" "git checkout -b new" primary on block
|
|
134
|
+
expect_primary_guard "primary/on/compound switch blocked" "cd repo && git switch other" primary on block
|
|
135
|
+
expect_primary_guard "primary/on/-C switch blocked" "git -C /repo switch other" primary on block
|
|
136
|
+
expect_primary_guard "primary/on/-C checkout -b blocked" "git -C /repo checkout -b new" primary on block
|
|
137
|
+
expect_primary_guard "primary/on/switch dash blocked" "git switch -" primary on block
|
|
138
|
+
expect_primary_guard "primary/on/commit allowed" "git commit -m wip" primary on allow
|
|
139
|
+
expect_primary_guard "primary/on/worktree add allowed" "git worktree add ../wt other" primary on allow
|
|
140
|
+
expect_primary_guard "primary/on/status allowed" "git status" primary on allow
|
|
141
|
+
expect_primary_guard "primary/on/checkout path allowed" "git checkout -- src/app.ts" primary on allow
|
|
142
|
+
expect_primary_guard "primary/on/checkout dot allowed" "git checkout ." primary on allow
|
|
143
|
+
expect_primary_guard "primary/off/switch allowed" "git switch other" primary off allow
|
|
144
|
+
expect_primary_guard "worktree/on/switch allowed" "git switch other" worktree on allow
|
|
145
|
+
expect_primary_guard "worktree/off/switch allowed" "git switch other" worktree off allow
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# issue #1024: block git reset (any form) and git stash (all subcommands),
|
|
149
|
+
# plus git rebase --autostash, in the primary checkout. Parallel human WIP
|
|
150
|
+
# lives there; these commands have destroyed in-progress work before.
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
expect_primary_guard "primary/on/reset bare blocked" "git reset" primary on block
|
|
153
|
+
expect_primary_guard "primary/on/reset --hard blocked" "git reset --hard" primary on block
|
|
154
|
+
expect_primary_guard "primary/on/reset --hard HEAD~1 blocked" "git reset --hard HEAD~1" primary on block
|
|
155
|
+
expect_primary_guard "primary/on/reset --soft blocked" "git reset --soft HEAD~1" primary on block
|
|
156
|
+
expect_primary_guard "primary/on/reset --mixed blocked" "git reset --mixed" primary on block
|
|
157
|
+
expect_primary_guard "primary/on/reset pathspec blocked" "git reset HEAD file.txt" primary on block
|
|
158
|
+
expect_primary_guard "primary/on/-C reset blocked" "git -C /repo reset --hard" primary on block
|
|
159
|
+
expect_primary_guard "primary/on/compound reset blocked" "cd repo && git reset --hard" primary on block
|
|
160
|
+
|
|
161
|
+
expect_primary_guard "primary/on/stash bare blocked" "git stash" primary on block
|
|
162
|
+
expect_primary_guard "primary/on/stash push blocked" "git stash push" primary on block
|
|
163
|
+
expect_primary_guard "primary/on/stash push paths blocked" "git stash push -m x src/" primary on block
|
|
164
|
+
expect_primary_guard "primary/on/stash save blocked" "git stash save wip" primary on block
|
|
165
|
+
expect_primary_guard "primary/on/stash pop blocked" "git stash pop" primary on block
|
|
166
|
+
expect_primary_guard "primary/on/stash apply blocked" "git stash apply" primary on block
|
|
167
|
+
expect_primary_guard "primary/on/stash list blocked" "git stash list" primary on block
|
|
168
|
+
expect_primary_guard "primary/on/stash show blocked" "git stash show" primary on block
|
|
169
|
+
expect_primary_guard "primary/on/-C stash blocked" "git -C /repo stash" primary on block
|
|
170
|
+
|
|
171
|
+
expect_primary_guard "primary/on/rebase autostash blocked" "git rebase --autostash" primary on block
|
|
172
|
+
expect_primary_guard "primary/on/rebase autostash arg blocked" "git rebase --autostash origin/main" primary on block
|
|
173
|
+
expect_primary_guard "primary/on/rebase main autostash blocked" "git rebase main --autostash" primary on block
|
|
174
|
+
|
|
175
|
+
# rebase without --autostash is not the work-loss vector this rule targets
|
|
176
|
+
expect_primary_guard "primary/on/rebase plain allowed" "git rebase main" primary on allow
|
|
177
|
+
expect_primary_guard "primary/on/rebase --no-autostash allowed" "git rebase --no-autostash main" primary on allow
|
|
178
|
+
|
|
179
|
+
# worktrees stay exempt (scope), flag-off stays inert — for reset/stash too
|
|
180
|
+
expect_primary_guard "worktree/on/reset allowed" "git reset --hard" worktree on allow
|
|
181
|
+
expect_primary_guard "worktree/on/stash allowed" "git stash" worktree on allow
|
|
182
|
+
expect_primary_guard "primary/off/reset allowed" "git reset --hard" primary off allow
|
|
183
|
+
expect_primary_guard "primary/off/stash allowed" "git stash" primary off allow
|
|
184
|
+
|
|
185
|
+
echo
|
|
186
|
+
echo "summary: $pass passed, $fail failed"
|
|
187
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for lib/lock-store.sh (issue #60).
|
|
3
|
+
#
|
|
4
|
+
# Sources the module directly (no globals) and exercises all four entry points
|
|
5
|
+
# against temp lock files:
|
|
6
|
+
# - write/read round-trip, including a branch name with a slash
|
|
7
|
+
# - absent file => unlocked (read rc 1, no output; is_locked rc 1)
|
|
8
|
+
# - clear removes the file and is idempotent
|
|
9
|
+
# - write is atomic and overwrites (relock to a new target)
|
|
10
|
+
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
|
|
13
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
LIB="$(dirname "$HERE")/lib/lock-store.sh"
|
|
15
|
+
|
|
16
|
+
# shellcheck source=../lib/lock-store.sh
|
|
17
|
+
source "$LIB"
|
|
18
|
+
|
|
19
|
+
pass=0
|
|
20
|
+
fail=0
|
|
21
|
+
|
|
22
|
+
expect_eq() {
|
|
23
|
+
local label="$1" expected="$2" actual="$3"
|
|
24
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
25
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
26
|
+
else
|
|
27
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
28
|
+
fail=$((fail + 1))
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ===========================================================================
|
|
33
|
+
# absent file => unlocked
|
|
34
|
+
# ===========================================================================
|
|
35
|
+
tmpdir="$(mktemp -d)"
|
|
36
|
+
lock="$tmpdir/.red/tmp/branch-lock.yaml"
|
|
37
|
+
|
|
38
|
+
out="$(lock_store_read "$lock")"; rc=$?
|
|
39
|
+
expect_eq "absent/read rc 1" "1" "$rc"
|
|
40
|
+
expect_eq "absent/read silent" "" "$out"
|
|
41
|
+
lock_store_is_locked "$lock"; rc=$?
|
|
42
|
+
expect_eq "absent/is_locked rc 1" "1" "$rc"
|
|
43
|
+
|
|
44
|
+
# ===========================================================================
|
|
45
|
+
# write / read round-trip (parent dir created, slash branch preserved)
|
|
46
|
+
# ===========================================================================
|
|
47
|
+
lock_store_write "$lock" "feature/branch-lock"; rc=$?
|
|
48
|
+
expect_eq "write/rc 0" "0" "$rc"
|
|
49
|
+
expect_eq "write/file exists" "1" "$([[ -f "$lock" ]] && echo 1 || echo 0)"
|
|
50
|
+
expect_eq "write/read back" "feature/branch-lock" "$(lock_store_read "$lock")"
|
|
51
|
+
lock_store_is_locked "$lock"; rc=$?
|
|
52
|
+
expect_eq "write/is_locked rc 0" "0" "$rc"
|
|
53
|
+
# stored content is a single line (no trailing whitespace in the value)
|
|
54
|
+
expect_eq "write/single line" "1" "$(wc -l < "$lock" | tr -d ' ')"
|
|
55
|
+
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
# write overwrites (relock to a new target)
|
|
58
|
+
# ===========================================================================
|
|
59
|
+
lock_store_write "$lock" "main"
|
|
60
|
+
expect_eq "relock/read new target" "main" "$(lock_store_read "$lock")"
|
|
61
|
+
|
|
62
|
+
# missing arg => rc 2, file untouched
|
|
63
|
+
lock_store_write "$lock" "" 2>/dev/null; rc=$?
|
|
64
|
+
expect_eq "write/missing branch rc 2" "2" "$rc"
|
|
65
|
+
expect_eq "write/untouched after bad" "main" "$(lock_store_read "$lock")"
|
|
66
|
+
|
|
67
|
+
# ===========================================================================
|
|
68
|
+
# clear removes the file and is idempotent
|
|
69
|
+
# ===========================================================================
|
|
70
|
+
lock_store_clear "$lock"; rc=$?
|
|
71
|
+
expect_eq "clear/rc 0" "0" "$rc"
|
|
72
|
+
expect_eq "clear/file gone" "0" "$([[ -f "$lock" ]] && echo 1 || echo 0)"
|
|
73
|
+
lock_store_clear "$lock"; rc=$?
|
|
74
|
+
expect_eq "clear/idempotent rc 0" "0" "$rc"
|
|
75
|
+
lock_store_is_locked "$lock"; rc=$?
|
|
76
|
+
expect_eq "clear/is_locked rc 1" "1" "$rc"
|
|
77
|
+
|
|
78
|
+
# ===========================================================================
|
|
79
|
+
# an empty lock file reads as unlocked (defensive: half-written / truncated)
|
|
80
|
+
# ===========================================================================
|
|
81
|
+
mkdir -p "$(dirname "$lock")"; : > "$lock"
|
|
82
|
+
out="$(lock_store_read "$lock")"; rc=$?
|
|
83
|
+
expect_eq "empty/read rc 1" "1" "$rc"
|
|
84
|
+
expect_eq "empty/read silent" "" "$out"
|
|
85
|
+
|
|
86
|
+
rm -rf "$tmpdir"
|
|
87
|
+
|
|
88
|
+
echo
|
|
89
|
+
echo "summary: $pass passed, $fail failed"
|
|
90
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for lib/scope-resolver.sh (issue #60).
|
|
3
|
+
#
|
|
4
|
+
# The lock enforces in the primary checkout and is exempt in registered
|
|
5
|
+
# .red/tmp worktree lanes plus legacy .red/tmp/work-* worktrees. Verifies both
|
|
6
|
+
# scope_is_worktree and its inverse scope_should_enforce across primary roots,
|
|
7
|
+
# worktree roots, and paths nested inside a worktree.
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
LIB="$(dirname "$HERE")/lib/scope-resolver.sh"
|
|
13
|
+
|
|
14
|
+
# shellcheck source=../lib/scope-resolver.sh
|
|
15
|
+
source "$LIB"
|
|
16
|
+
|
|
17
|
+
pass=0
|
|
18
|
+
fail=0
|
|
19
|
+
|
|
20
|
+
expect_eq() {
|
|
21
|
+
local label="$1" expected="$2" actual="$3"
|
|
22
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
23
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
24
|
+
else
|
|
25
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
26
|
+
fail=$((fail + 1))
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rc_of() { "$@"; echo $?; }
|
|
31
|
+
|
|
32
|
+
# ===========================================================================
|
|
33
|
+
# primary checkout => enforce (not a worktree)
|
|
34
|
+
# ===========================================================================
|
|
35
|
+
PRIMARY="/home/dev/Work/reddb.io/red-skills"
|
|
36
|
+
expect_eq "primary/is_worktree rc 1" "1" "$(rc_of scope_is_worktree "$PRIMARY")"
|
|
37
|
+
expect_eq "primary/should_enforce rc 0" "0" "$(rc_of scope_should_enforce "$PRIMARY")"
|
|
38
|
+
|
|
39
|
+
# a path that merely contains '.red/tmp' but is not a work-* worktree still enforces
|
|
40
|
+
NEAR="/home/dev/Work/reddb.io/red-skills/.red/tmp"
|
|
41
|
+
expect_eq "near/is_worktree rc 1" "1" "$(rc_of scope_is_worktree "$NEAR")"
|
|
42
|
+
|
|
43
|
+
# ===========================================================================
|
|
44
|
+
# registered worktree lane root => exempt
|
|
45
|
+
# ===========================================================================
|
|
46
|
+
WT="/home/dev/Work/reddb.io/red-skills/.red/tmp/worktrees/manual/fix-docs"
|
|
47
|
+
expect_eq "worktree/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$WT")"
|
|
48
|
+
expect_eq "worktree/should_enforce rc 1" "1" "$(rc_of scope_should_enforce "$WT")"
|
|
49
|
+
|
|
50
|
+
# legacy .red/tmp/work-* remains exempt during migration
|
|
51
|
+
WTROOT="/home/dev/Work/reddb.io/red-skills/.red/tmp/work-abc-i1"
|
|
52
|
+
expect_eq "worktree-root/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$WTROOT")"
|
|
53
|
+
|
|
54
|
+
# ===========================================================================
|
|
55
|
+
# nested path inside a worktree => still exempt
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
NESTED="/home/dev/Work/reddb.io/red-skills/.red/tmp/worktrees/manual/fix-docs/plugins/dev"
|
|
58
|
+
expect_eq "nested/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$NESTED")"
|
|
59
|
+
expect_eq "nested/should_enforce rc 1" "1" "$(rc_of scope_should_enforce "$NESTED")"
|
|
60
|
+
|
|
61
|
+
echo
|
|
62
|
+
echo "summary: $pass passed, $fail failed"
|
|
63
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for branch-lock-session-start.sh (issue #62).
|
|
3
|
+
#
|
|
4
|
+
# The SessionStart hook offers — once, at session start — to lock the agent to
|
|
5
|
+
# the current branch. It must:
|
|
6
|
+
# - emit an instruction (exit 0, JSON on stdout) in the primary checkout when
|
|
7
|
+
# the branch is unlocked, naming the current branch;
|
|
8
|
+
# - stay silent (no stdout) inside an /afk worktree (scope exemption);
|
|
9
|
+
# - stay silent when a lock is already present (nothing to offer);
|
|
10
|
+
# - stay silent on a detached HEAD (no branch to lock).
|
|
11
|
+
#
|
|
12
|
+
# Exercises the pure decision/text helpers sourced from the hook script, plus an
|
|
13
|
+
# end-to-end run of the hook against throwaway git repos.
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
HOOK="$(dirname "$HERE")/branch-lock-session-start.sh"
|
|
19
|
+
|
|
20
|
+
# Source the script for its pure helpers (the script guards main on direct run).
|
|
21
|
+
# shellcheck source=../branch-lock-session-start.sh
|
|
22
|
+
source "$HOOK"
|
|
23
|
+
|
|
24
|
+
pass=0
|
|
25
|
+
fail=0
|
|
26
|
+
|
|
27
|
+
expect_eq() {
|
|
28
|
+
local label="$1" expected="$2" actual="$3"
|
|
29
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
30
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
31
|
+
else
|
|
32
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
33
|
+
fail=$((fail + 1))
|
|
34
|
+
fi
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect_contains() {
|
|
38
|
+
local label="$1" needle="$2" haystack="$3"
|
|
39
|
+
if [[ "$haystack" == *"$needle"* ]]; then
|
|
40
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
41
|
+
else
|
|
42
|
+
printf 'FAIL %s\n missing: %q\n in: %q\n' "$label" "$needle" "$haystack"
|
|
43
|
+
fail=$((fail + 1))
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
rc_of() { "$@"; echo $?; }
|
|
48
|
+
|
|
49
|
+
# ===========================================================================
|
|
50
|
+
# session_start_should_prompt <root> <lockfile> <branch>
|
|
51
|
+
# prompt only when: primary checkout AND unlocked AND a real branch.
|
|
52
|
+
# ===========================================================================
|
|
53
|
+
PRIMARY="/home/dev/Work/reddb.io/red-skills"
|
|
54
|
+
WT="/home/dev/Work/reddb.io/red-skills/.red/tmp/work-abc-i1/worktree"
|
|
55
|
+
ABSENT_LOCK="$(mktemp -u)" # path that does not exist => unlocked
|
|
56
|
+
|
|
57
|
+
expect_eq "primary+unlocked+branch => prompt (rc 0)" \
|
|
58
|
+
"0" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "main")"
|
|
59
|
+
|
|
60
|
+
expect_eq "worktree => no prompt (rc 1)" \
|
|
61
|
+
"1" "$(rc_of session_start_should_prompt "$WT" "$ABSENT_LOCK" "main")"
|
|
62
|
+
|
|
63
|
+
expect_eq "detached HEAD => no prompt (rc 1)" \
|
|
64
|
+
"1" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "HEAD")"
|
|
65
|
+
|
|
66
|
+
expect_eq "empty branch => no prompt (rc 1)" \
|
|
67
|
+
"1" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "")"
|
|
68
|
+
|
|
69
|
+
PRESENT_LOCK="$(mktemp)"; printf 'main\n' > "$PRESENT_LOCK"
|
|
70
|
+
expect_eq "already locked => no prompt (rc 1)" \
|
|
71
|
+
"1" "$(rc_of session_start_should_prompt "$PRIMARY" "$PRESENT_LOCK" "feature/x")"
|
|
72
|
+
rm -f "$PRESENT_LOCK"
|
|
73
|
+
|
|
74
|
+
# ===========================================================================
|
|
75
|
+
# session_start_prompt_text <branch> — names the branch, instructs to ask.
|
|
76
|
+
# ===========================================================================
|
|
77
|
+
TEXT="$(session_start_prompt_text "feature/login")"
|
|
78
|
+
expect_contains "prompt text names the branch" "feature/login" "$TEXT"
|
|
79
|
+
expect_contains "prompt text says to ask the user" "ask" "$TEXT"
|
|
80
|
+
expect_contains "prompt text points at /branch-lock" "/branch-lock" "$TEXT"
|
|
81
|
+
|
|
82
|
+
# ===========================================================================
|
|
83
|
+
# End-to-end: run the hook against throwaway repos.
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
mk_repo() {
|
|
86
|
+
local dir="$1"
|
|
87
|
+
git init -q "$dir"
|
|
88
|
+
git -C "$dir" config user.email t@t.t
|
|
89
|
+
git -C "$dir" config user.name t
|
|
90
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
run_hook() { CLAUDE_PROJECT_DIR="$1" bash "$HOOK" <<<'{"hook_event_name":"SessionStart"}'; }
|
|
94
|
+
|
|
95
|
+
# primary checkout, unlocked => emits JSON with additionalContext naming branch
|
|
96
|
+
TMP_PRIMARY="$(mktemp -d)/red-skills"
|
|
97
|
+
mk_repo "$TMP_PRIMARY"
|
|
98
|
+
git -C "$TMP_PRIMARY" branch -m work-branch
|
|
99
|
+
OUT="$(run_hook "$TMP_PRIMARY")"
|
|
100
|
+
expect_contains "e2e primary: emits SessionStart event" "SessionStart" "$OUT"
|
|
101
|
+
expect_contains "e2e primary: names the branch" "work-branch" "$OUT"
|
|
102
|
+
expect_contains "e2e primary: valid additionalContext" "additionalContext" "$OUT"
|
|
103
|
+
if command -v jq >/dev/null 2>&1; then
|
|
104
|
+
CTX="$(jq -r '.hookSpecificOutput.additionalContext' <<<"$OUT" 2>/dev/null)"
|
|
105
|
+
expect_contains "e2e primary: JSON parses, branch in context" "work-branch" "$CTX"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# already locked => silent (empty stdout)
|
|
109
|
+
mkdir -p "$TMP_PRIMARY/.red/tmp"
|
|
110
|
+
printf 'work-branch\n' > "$TMP_PRIMARY/.red/tmp/branch-lock.yaml"
|
|
111
|
+
OUT_LOCKED="$(run_hook "$TMP_PRIMARY")"
|
|
112
|
+
expect_eq "e2e locked: silent" "" "$OUT_LOCKED"
|
|
113
|
+
|
|
114
|
+
# worktree => silent even when unlocked
|
|
115
|
+
TMP_WT="$(mktemp -d)/red-skills/.red/tmp/work-zzz-i1/worktree"
|
|
116
|
+
mkdir -p "$(dirname "$TMP_WT")"
|
|
117
|
+
mk_repo "$TMP_WT"
|
|
118
|
+
OUT_WT="$(run_hook "$TMP_WT")"
|
|
119
|
+
expect_eq "e2e worktree: silent" "" "$OUT_WT"
|
|
120
|
+
|
|
121
|
+
echo
|
|
122
|
+
echo "summary: $pass passed, $fail failed"
|
|
123
|
+
[ "$fail" -eq 0 ]
|