@onlooker-community/ecosystem 0.9.0 → 0.14.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/.claude-plugin/marketplace.json +39 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.github/copilot-instructions.md +46 -0
- package/.github/workflows/coverage.yml +78 -0
- package/.github/workflows/release.yml +24 -8
- package/.github/workflows/test.yml +3 -0
- package/.markdownlintignore +3 -0
- package/.release-please-manifest.json +4 -1
- package/CHANGELOG.md +44 -0
- package/README.md +57 -13
- package/config.json +6 -1
- package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
- package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
- package/docs/adr/003-ulid-over-uuid.md +40 -0
- package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
- package/docs/architecture.md +117 -0
- package/hooks/hooks.json +4 -0
- package/package.json +13 -7
- package/plugins/archivist/.claude-plugin/plugin.json +14 -0
- package/plugins/archivist/CHANGELOG.md +8 -0
- package/plugins/archivist/README.md +105 -0
- package/plugins/archivist/config.json +18 -0
- package/plugins/archivist/hooks/hooks.json +35 -0
- package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
- package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
- package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
- package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
- package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
- package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
- package/plugins/echo/.claude-plugin/plugin.json +14 -0
- package/plugins/echo/CHANGELOG.md +24 -0
- package/plugins/echo/README.md +110 -0
- package/plugins/echo/config.json +15 -0
- package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
- package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
- package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
- package/plugins/echo/hooks/hooks.json +15 -0
- package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
- package/plugins/echo/scripts/lib/echo-config.sh +108 -0
- package/plugins/echo/scripts/lib/echo-events.sh +74 -0
- package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
- package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
- package/plugins/tribunal/CHANGELOG.md +10 -0
- package/plugins/tribunal/README.md +134 -0
- package/plugins/tribunal/agents/tribunal-actor.md +35 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
- package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
- package/plugins/tribunal/config.json +50 -0
- package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
- package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
- package/plugins/tribunal/hooks/hooks.json +15 -0
- package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
- package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
- package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
- package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
- package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
- package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
- package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
- package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
- package/release-please-config.json +43 -5
- package/scripts/coverage/bash-coverage.mjs +169 -0
- package/scripts/coverage/format-comment.mjs +120 -0
- package/scripts/coverage/run-coverage.mjs +151 -0
- package/scripts/hooks/agent-spawn-tracker.sh +4 -4
- package/scripts/hooks/prompt-rule-injector.sh +122 -0
- package/scripts/lib/onlooker-event.mjs +82 -10
- package/scripts/lib/portable-lock.sh +48 -0
- package/scripts/lib/prompt-rules.sh +207 -0
- package/scripts/lib/tool-history.sh +7 -8
- package/scripts/lib/validate-path.sh +4 -0
- package/scripts/lint/check-manifests.mjs +314 -0
- package/scripts/lint/check-references.mjs +311 -0
- package/skills/list-prompt-rules/SKILL.md +15 -0
- package/test/bats/archivist-config-files.bats +60 -0
- package/test/bats/archivist-config.bats +54 -0
- package/test/bats/archivist-inject.bats +73 -0
- package/test/bats/archivist-project-key.bats +75 -0
- package/test/bats/archivist-storage.bats +119 -0
- package/test/bats/archivist-ulid.bats +36 -0
- package/test/bats/config.bats +10 -10
- package/test/bats/echo-config.bats +90 -0
- package/test/bats/echo-events.bats +121 -0
- package/test/bats/echo-project-key.bats +115 -0
- package/test/bats/echo-stop-hook.bats +101 -0
- package/test/bats/echo-ulid.bats +38 -0
- package/test/bats/portable-lock.bats +62 -0
- package/test/bats/prompt-rules.bats +269 -0
- package/test/bats/read-chunk-tracking.bats +73 -0
- package/test/bats/tool-history-tracker.bats +1 -0
- package/test/bats/tribunal-aggregate.bats +77 -0
- package/test/bats/tribunal-config.bats +86 -0
- package/test/bats/tribunal-events.bats +209 -0
- package/test/bats/tribunal-gate.bats +95 -0
- package/test/bats/tribunal-jury.bats +80 -0
- package/test/bats/tribunal-rubric.bats +119 -0
- package/test/bats/tribunal-stop-hook.bats +73 -0
- package/test/bats/tribunal-verdict.bats +71 -0
- package/test/bats/validate-path.bats +1 -1
- package/test/fixtures/hook-inputs/post-tool-use-read-chunked.json +15 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
- package/test/helpers/setup.bash +9 -0
- package/test/node/check-manifests.test.mjs +173 -0
- package/test/node/check-references.test.mjs +279 -0
- package/test/node/coverage.test.mjs +143 -0
- package/test/node/schema-events.test.mjs +41 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/echo-project-key.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "non-git directory returns empty key" {
|
|
13
|
+
local d="${BATS_TEST_TMPDIR}/non-git"
|
|
14
|
+
mkdir -p "$d"
|
|
15
|
+
run echo_project_key "$d"
|
|
16
|
+
[ "$status" -eq 0 ]
|
|
17
|
+
[ -z "$output" ]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@test "git repo without remote falls back to repo-root hash" {
|
|
21
|
+
local d="${BATS_TEST_TMPDIR}/local-only-repo"
|
|
22
|
+
mkdir -p "$d"
|
|
23
|
+
git -C "$d" init -q
|
|
24
|
+
git -C "$d" config user.email t@example.com
|
|
25
|
+
git -C "$d" config user.name "Test"
|
|
26
|
+
|
|
27
|
+
local k1 k2
|
|
28
|
+
k1=$(echo_project_key "$d")
|
|
29
|
+
k2=$(echo_project_key "$d")
|
|
30
|
+
[ -n "$k1" ]
|
|
31
|
+
[ "${#k1}" -eq 12 ]
|
|
32
|
+
[ "$k1" = "$k2" ]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@test "git repo with remote uses remote hash" {
|
|
36
|
+
local a="${BATS_TEST_TMPDIR}/clone-a"
|
|
37
|
+
local b="${BATS_TEST_TMPDIR}/clone-b"
|
|
38
|
+
mkdir -p "$a" "$b"
|
|
39
|
+
for d in "$a" "$b"; do
|
|
40
|
+
git -C "$d" init -q
|
|
41
|
+
git -C "$d" config user.email t@example.com
|
|
42
|
+
git -C "$d" config user.name "Test"
|
|
43
|
+
git -C "$d" remote add origin git@github.com:org/proj.git
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
local ka kb
|
|
47
|
+
ka=$(echo_project_key "$a")
|
|
48
|
+
kb=$(echo_project_key "$b")
|
|
49
|
+
[ -n "$ka" ]
|
|
50
|
+
[ "$ka" = "$kb" ]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test "different remotes yield different keys" {
|
|
54
|
+
local a="${BATS_TEST_TMPDIR}/proj-a"
|
|
55
|
+
local b="${BATS_TEST_TMPDIR}/proj-b"
|
|
56
|
+
mkdir -p "$a" "$b"
|
|
57
|
+
for d in "$a" "$b"; do
|
|
58
|
+
git -C "$d" init -q
|
|
59
|
+
git -C "$d" config user.email t@example.com
|
|
60
|
+
git -C "$d" config user.name "Test"
|
|
61
|
+
done
|
|
62
|
+
git -C "$a" remote add origin git@github.com:org/proj-a.git
|
|
63
|
+
git -C "$b" remote add origin git@github.com:org/proj-b.git
|
|
64
|
+
|
|
65
|
+
local ka kb
|
|
66
|
+
ka=$(echo_project_key "$a")
|
|
67
|
+
kb=$(echo_project_key "$b")
|
|
68
|
+
[ -n "$ka" ]
|
|
69
|
+
[ -n "$kb" ]
|
|
70
|
+
[ "$ka" != "$kb" ]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "echo_test_id_for_path returns 16 hex chars" {
|
|
74
|
+
local tid
|
|
75
|
+
tid=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
76
|
+
[ -n "$tid" ]
|
|
77
|
+
[ "${#tid}" -eq 16 ]
|
|
78
|
+
[[ "$tid" =~ ^[0-9a-f]{16}$ ]]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "echo_test_id_for_path is stable across calls" {
|
|
82
|
+
local a b
|
|
83
|
+
a=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
84
|
+
b=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
85
|
+
[ "$a" = "$b" ]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@test "echo_test_id_for_path differs for different paths" {
|
|
89
|
+
local a b
|
|
90
|
+
a=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-standard.md")
|
|
91
|
+
b=$(echo_test_id_for_path "plugins/tribunal/agents/tribunal-judge-adversarial.md")
|
|
92
|
+
[ "$a" != "$b" ]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@test "echo_project_repo_root returns empty for non-git dir" {
|
|
96
|
+
local d="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
97
|
+
mkdir -p "$d"
|
|
98
|
+
local r
|
|
99
|
+
r=$(echo_project_repo_root "$d")
|
|
100
|
+
[ -z "$r" ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "echo_project_repo_root returns repo root for subdir" {
|
|
104
|
+
local d="${BATS_TEST_TMPDIR}/myrepo"
|
|
105
|
+
mkdir -p "${d}/sub/dir"
|
|
106
|
+
git -C "$d" init -q
|
|
107
|
+
git -C "$d" config user.email t@example.com
|
|
108
|
+
git -C "$d" config user.name "Test"
|
|
109
|
+
|
|
110
|
+
local r expected
|
|
111
|
+
r=$(echo_project_repo_root "${d}/sub/dir")
|
|
112
|
+
# Resolve symlinks — on macOS BATS_TEST_TMPDIR may differ from git's toplevel.
|
|
113
|
+
expected=$(cd "$d" && pwd -P)
|
|
114
|
+
[ "$r" = "$expected" ]
|
|
115
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the Echo Stop hook's gating behavior. Does not invoke claude -p
|
|
4
|
+
# (the hook bails before reaching the eval loop when preconditions fail).
|
|
5
|
+
# Tests verify: disabled-by-default, no-git, no-watched-changes, recursion
|
|
6
|
+
# guard, untracked file detection, and stdout silence.
|
|
7
|
+
|
|
8
|
+
setup() {
|
|
9
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
10
|
+
setup_test_env
|
|
11
|
+
|
|
12
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
13
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
14
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/echo-stop-gate.sh"
|
|
15
|
+
|
|
16
|
+
REPO="${BATS_TEST_TMPDIR}/repo"
|
|
17
|
+
mkdir -p "$REPO"
|
|
18
|
+
git -C "$REPO" init -q
|
|
19
|
+
git -C "$REPO" config user.email test@example.com
|
|
20
|
+
git -C "$REPO" config user.name test
|
|
21
|
+
(cd "$REPO" && printf 'initial\n' > README.md && git add README.md && git commit -q -m init)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_make_input() {
|
|
25
|
+
local cwd="${1:-$REPO}" sid="${2:-test-session}"
|
|
26
|
+
jq -n --arg cwd "$cwd" --arg sid "$sid" '{cwd: $cwd, session_id: $sid}'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@test "hook exits 0 silently when echo.enabled is false (default)" {
|
|
30
|
+
local input
|
|
31
|
+
input=$(_make_input)
|
|
32
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
33
|
+
[ "$status" -eq 0 ]
|
|
34
|
+
[ -z "$output" ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "hook exits 0 when cwd is not a git repo" {
|
|
38
|
+
local non_repo="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
39
|
+
mkdir -p "$non_repo"
|
|
40
|
+
local input
|
|
41
|
+
input=$(_make_input "$non_repo")
|
|
42
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
43
|
+
[ "$status" -eq 0 ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "hook exits 0 when enabled but no watched files changed" {
|
|
47
|
+
mkdir -p "${REPO}/.claude"
|
|
48
|
+
printf '%s\n' '{"echo":{"enabled":true}}' > "${REPO}/.claude/settings.json"
|
|
49
|
+
local input
|
|
50
|
+
input=$(_make_input)
|
|
51
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
52
|
+
[ "$status" -eq 0 ]
|
|
53
|
+
[ -z "$output" ]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@test "recursion guard: ECHO_NESTED=1 causes immediate exit 0" {
|
|
57
|
+
mkdir -p "${REPO}/.claude"
|
|
58
|
+
printf '%s\n' '{"echo":{"enabled":true}}' > "${REPO}/.claude/settings.json"
|
|
59
|
+
local input
|
|
60
|
+
input=$(_make_input)
|
|
61
|
+
# Export ECHO_NESTED into the subshell that runs the hook.
|
|
62
|
+
run bash -c "printf '%s' '$input' | ECHO_NESTED=1 ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
63
|
+
[ "$status" -eq 0 ]
|
|
64
|
+
[ -z "$output" ]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "hook never prints to stdout (Stop contract)" {
|
|
68
|
+
local input
|
|
69
|
+
input=$(_make_input)
|
|
70
|
+
run bash -c "printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
71
|
+
[ "$status" -eq 0 ]
|
|
72
|
+
[ -z "$output" ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "untracked watched file is detected when enabled and claude missing" {
|
|
76
|
+
mkdir -p "${REPO}/.claude" "${REPO}/plugins/myplugin/agents"
|
|
77
|
+
printf '%s\n' '{"echo":{"enabled":true,"watch_paths":["plugins/*/agents/*.md"]}}' \
|
|
78
|
+
> "${REPO}/.claude/settings.json"
|
|
79
|
+
printf '%s\n' '# New agent' > "${REPO}/plugins/myplugin/agents/new-agent.md"
|
|
80
|
+
local input
|
|
81
|
+
input=$(_make_input)
|
|
82
|
+
# claude is not present in the test env PATH → hook reaches _done after the
|
|
83
|
+
# `command -v claude` guard. Exit 0 with no output confirms the file was
|
|
84
|
+
# at least detected (it passed the _done guards before the claude check).
|
|
85
|
+
run bash -c "PATH=/usr/bin:/bin printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
86
|
+
[ "$status" -eq 0 ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "files under plugins/echo are excluded even if they match watch_paths" {
|
|
90
|
+
mkdir -p "${REPO}/.claude" "${REPO}/plugins/echo/agents"
|
|
91
|
+
printf '%s\n' '{"echo":{"enabled":true,"watch_paths":["plugins/*/agents/*.md"]}}' \
|
|
92
|
+
> "${REPO}/.claude/settings.json"
|
|
93
|
+
printf '%s\n' '# Echo self' > "${REPO}/plugins/echo/agents/self.md"
|
|
94
|
+
local input
|
|
95
|
+
input=$(_make_input)
|
|
96
|
+
# With echo's own file as the only change, no watched files remain after
|
|
97
|
+
# exclude filtering — hook exits before the claude guard (no output).
|
|
98
|
+
run bash -c "PATH=/usr/bin:/bin printf '%s' '$input' | ONLOOKER_DIR='$ONLOOKER_DIR' '$HOOK'"
|
|
99
|
+
[ "$status" -eq 0 ]
|
|
100
|
+
[ -z "$output" ]
|
|
101
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/echo"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/echo-ulid.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "echo_ulid returns a 26-char crockford base32 string" {
|
|
13
|
+
local id
|
|
14
|
+
id=$(echo_ulid)
|
|
15
|
+
[ "${#id}" -eq 26 ]
|
|
16
|
+
[[ "$id" =~ ^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$ ]]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "two ULIDs minted apart are lexicographically ordered or equal" {
|
|
20
|
+
local a b
|
|
21
|
+
a=$(echo_ulid)
|
|
22
|
+
sleep 0.01
|
|
23
|
+
b=$(echo_ulid)
|
|
24
|
+
[[ "$a" < "$b" ]] || [ "$a" = "$b" ]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "many ULIDs are unique" {
|
|
28
|
+
local seen="${BATS_TEST_TMPDIR}/ulids.txt"
|
|
29
|
+
: > "$seen"
|
|
30
|
+
local i
|
|
31
|
+
for ((i = 0; i < 50; i++)); do
|
|
32
|
+
printf '%s\n' "$(echo_ulid)" >> "$seen"
|
|
33
|
+
done
|
|
34
|
+
local total unique
|
|
35
|
+
total=$(wc -l < "$seen" | tr -d ' ')
|
|
36
|
+
unique=$(sort -u "$seen" | wc -l | tr -d ' ')
|
|
37
|
+
[ "$total" = "$unique" ]
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
setup_test_env
|
|
7
|
+
# shellcheck disable=SC1091
|
|
8
|
+
source "${REPO_ROOT}/scripts/lib/portable-lock.sh"
|
|
9
|
+
LOCK="${BATS_TEST_TMPDIR}/test.lock"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "lock_acquire succeeds on an unlocked path" {
|
|
13
|
+
run lock_acquire "$LOCK" 1
|
|
14
|
+
[ "$status" -eq 0 ]
|
|
15
|
+
[ -d "${LOCK}.d" ]
|
|
16
|
+
lock_release "$LOCK"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "lock_acquire on a held lock blocks until released" {
|
|
20
|
+
lock_acquire "$LOCK" 1
|
|
21
|
+
# Start a background releaser after 200ms.
|
|
22
|
+
( sleep 0.2; lock_release "$LOCK" ) &
|
|
23
|
+
local releaser=$!
|
|
24
|
+
# Second acquire should succeed once the releaser fires.
|
|
25
|
+
run lock_acquire "$LOCK" 2
|
|
26
|
+
wait $releaser
|
|
27
|
+
[ "$status" -eq 0 ]
|
|
28
|
+
lock_release "$LOCK"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "lock_acquire returns 1 when timeout elapses with the lock still held" {
|
|
32
|
+
mkdir "${LOCK}.d"
|
|
33
|
+
run lock_acquire "$LOCK" 1
|
|
34
|
+
[ "$status" -eq 1 ]
|
|
35
|
+
rmdir "${LOCK}.d"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "lock_release is a no-op when the lock is not held" {
|
|
39
|
+
run lock_release "$LOCK"
|
|
40
|
+
[ "$status" -eq 0 ]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "concurrent appenders do not interleave writes" {
|
|
44
|
+
local out="${BATS_TEST_TMPDIR}/concurrent.txt"
|
|
45
|
+
: >"$out"
|
|
46
|
+
local n=20
|
|
47
|
+
local i
|
|
48
|
+
for ((i = 0; i < n; i++)); do
|
|
49
|
+
(
|
|
50
|
+
lock_acquire "$LOCK" 5 || exit 1
|
|
51
|
+
# Write a 100-char marker so any byte-level interleave is obvious.
|
|
52
|
+
printf '%s\n' "$(printf 'x%.0s' {1..100})" >>"$out"
|
|
53
|
+
lock_release "$LOCK"
|
|
54
|
+
) &
|
|
55
|
+
done
|
|
56
|
+
wait
|
|
57
|
+
# All lines should be exactly 100 bytes followed by newline.
|
|
58
|
+
local lines
|
|
59
|
+
lines=$(wc -l <"$out" | tr -d ' ')
|
|
60
|
+
[ "$lines" = "$n" ]
|
|
61
|
+
awk 'length($0) != 100 { bad++ } END { exit (bad > 0) }' "$out"
|
|
62
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
load_validate_path
|
|
7
|
+
# shellcheck source=../../scripts/lib/prompt-rules.sh
|
|
8
|
+
source "${REPO_ROOT}/scripts/lib/prompt-rules.sh"
|
|
9
|
+
export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
|
|
10
|
+
|
|
11
|
+
ensure_dir_exists "$ONLOOKER_PROMPT_RULES_SESSIONS_DIR"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
write_global_rules() {
|
|
15
|
+
local body="$1"
|
|
16
|
+
printf '%s\n' "$body" > "$(prompt_rules_global_path)"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
write_project_rules() {
|
|
20
|
+
local project_dir="$1"
|
|
21
|
+
local body="$2"
|
|
22
|
+
mkdir -p "${project_dir}/.claude"
|
|
23
|
+
printf '%s\n' "$body" > "${project_dir}/.claude/prompt-rules.json"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@test "load_merged returns empty array when no rule files exist" {
|
|
27
|
+
local rules
|
|
28
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
29
|
+
[ "$(echo "$rules" | jq 'length')" = "0" ]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "load_merged surfaces global rules when only global exists" {
|
|
33
|
+
write_global_rules '{
|
|
34
|
+
"rules": [
|
|
35
|
+
{"id": "r1", "pattern": "foo", "guidance": "global rule", "enabled": true}
|
|
36
|
+
]
|
|
37
|
+
}'
|
|
38
|
+
local rules
|
|
39
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
40
|
+
[ "$(echo "$rules" | jq 'length')" = "1" ]
|
|
41
|
+
[ "$(echo "$rules" | jq -r '.[0].guidance')" = "global rule" ]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test "load_merged: project overrides global by id" {
|
|
45
|
+
write_global_rules '{
|
|
46
|
+
"rules": [
|
|
47
|
+
{"id": "r1", "pattern": "foo", "guidance": "global version"},
|
|
48
|
+
{"id": "r2", "pattern": "bar", "guidance": "global only"}
|
|
49
|
+
]
|
|
50
|
+
}'
|
|
51
|
+
write_project_rules "$TEST_HOME" '{
|
|
52
|
+
"rules": [
|
|
53
|
+
{"id": "r1", "pattern": "foo", "guidance": "project version"}
|
|
54
|
+
]
|
|
55
|
+
}'
|
|
56
|
+
local rules
|
|
57
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
58
|
+
[ "$(echo "$rules" | jq 'length')" = "2" ]
|
|
59
|
+
[ "$(echo "$rules" | jq -r '.[] | select(.id=="r1") | .guidance')" = "project version" ]
|
|
60
|
+
[ "$(echo "$rules" | jq -r '.[] | select(.id=="r2") | .guidance')" = "global only" ]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "load_merged filters out enabled: false rules" {
|
|
64
|
+
write_global_rules '{
|
|
65
|
+
"rules": [
|
|
66
|
+
{"id": "r1", "pattern": "foo", "guidance": "on"},
|
|
67
|
+
{"id": "r2", "pattern": "bar", "guidance": "off", "enabled": false}
|
|
68
|
+
]
|
|
69
|
+
}'
|
|
70
|
+
local rules
|
|
71
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
72
|
+
[ "$(echo "$rules" | jq 'length')" = "1" ]
|
|
73
|
+
[ "$(echo "$rules" | jq -r '.[0].id')" = "r1" ]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@test "pattern_matches: hit, miss, empty pattern" {
|
|
77
|
+
run prompt_rules_pattern_matches "hello world" "world"
|
|
78
|
+
[ "$status" -eq 0 ]
|
|
79
|
+
run prompt_rules_pattern_matches "hello world" "xyz"
|
|
80
|
+
[ "$status" -eq 1 ]
|
|
81
|
+
run prompt_rules_pattern_matches "hello world" ""
|
|
82
|
+
[ "$status" -eq 1 ]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@test "pattern_matches: invalid ERE returns non-match without leaking stderr" {
|
|
86
|
+
# Unbalanced bracket — bash would normally print "syntax error in regular
|
|
87
|
+
# expression" to stderr and return 2. The helper must swallow both.
|
|
88
|
+
run prompt_rules_pattern_matches "anything" "[unterminated"
|
|
89
|
+
[ "$status" -eq 1 ]
|
|
90
|
+
[ -z "$stderr" ] || [ -z "${stderr// /}" ]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@test "load_merged: tolerates non-array .rules and entries missing id" {
|
|
94
|
+
# Object instead of array, and rule entries with missing/non-string ids.
|
|
95
|
+
write_global_rules '{"rules": "not-an-array"}'
|
|
96
|
+
write_project_rules "$TEST_HOME" '{
|
|
97
|
+
"rules": [
|
|
98
|
+
{"pattern": "no-id"},
|
|
99
|
+
{"id": null, "pattern": "null-id"},
|
|
100
|
+
{"id": 42, "pattern": "non-string-id"},
|
|
101
|
+
{"id": "good", "pattern": "ok", "guidance": "ok"}
|
|
102
|
+
]
|
|
103
|
+
}'
|
|
104
|
+
local rules
|
|
105
|
+
rules=$(prompt_rules_load_merged "$TEST_HOME")
|
|
106
|
+
[ "$(echo "$rules" | jq 'length')" = "1" ]
|
|
107
|
+
[ "$(echo "$rules" | jq -r '.[0].id')" = "good" ]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@test "mark_fired + load_fired round-trip is idempotent" {
|
|
111
|
+
prompt_rules_mark_fired "sess-A" "rule-1"
|
|
112
|
+
prompt_rules_mark_fired "sess-A" "rule-1"
|
|
113
|
+
prompt_rules_mark_fired "sess-A" "rule-2"
|
|
114
|
+
local fired
|
|
115
|
+
fired=$(prompt_rules_load_fired "sess-A")
|
|
116
|
+
[ "$(echo "$fired" | jq 'length')" = "2" ]
|
|
117
|
+
[ "$(echo "$fired" | jq -r '. | sort | join(",")')" = "rule-1,rule-2" ]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test "hook injects additionalContext when prompt matches a rule" {
|
|
121
|
+
write_global_rules '{
|
|
122
|
+
"rules": [
|
|
123
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
124
|
+
]
|
|
125
|
+
}'
|
|
126
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
127
|
+
|
|
128
|
+
run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh' 2>/dev/null"
|
|
129
|
+
[ "$status" -eq 0 ]
|
|
130
|
+
echo "$output" | jq -e \
|
|
131
|
+
'.hookSpecificOutput.hookEventName == "UserPromptSubmit"
|
|
132
|
+
and (.hookSpecificOutput.additionalContext | contains("Skipping hooks"))' >/dev/null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@test "hook outputs nothing when no rule matches" {
|
|
136
|
+
write_global_rules '{
|
|
137
|
+
"rules": [
|
|
138
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
139
|
+
]
|
|
140
|
+
}'
|
|
141
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json"
|
|
142
|
+
|
|
143
|
+
run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh' 2>/dev/null"
|
|
144
|
+
[ "$status" -eq 0 ]
|
|
145
|
+
[ -z "$output" ]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@test "hook fires a rule once per session" {
|
|
149
|
+
write_global_rules '{
|
|
150
|
+
"rules": [
|
|
151
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
152
|
+
]
|
|
153
|
+
}'
|
|
154
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
155
|
+
|
|
156
|
+
# First invocation: injects
|
|
157
|
+
local first_output
|
|
158
|
+
first_output=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
159
|
+
echo "$first_output" | jq -e '.hookSpecificOutput.additionalContext | contains("Skipping hooks")' >/dev/null
|
|
160
|
+
|
|
161
|
+
# Second invocation with same session: no injection
|
|
162
|
+
local second_output
|
|
163
|
+
second_output=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
164
|
+
[ -z "$second_output" ]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@test "hook emits prompt_rule.matched and prompt_rule.applied to events log" {
|
|
168
|
+
write_global_rules '{
|
|
169
|
+
"rules": [
|
|
170
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue."}
|
|
171
|
+
]
|
|
172
|
+
}'
|
|
173
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
174
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
175
|
+
|
|
176
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" >/dev/null 2>&1
|
|
177
|
+
|
|
178
|
+
grep -q '"event_type":"prompt_rule.matched"' "$ONLOOKER_EVENTS_LOG"
|
|
179
|
+
grep -q '"event_type":"prompt_rule.applied"' "$ONLOOKER_EVENTS_LOG"
|
|
180
|
+
grep -q '"rule_id":"rule-no-verify"' "$ONLOOKER_EVENTS_LOG"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@test "hook fires repeatedly when fire_once_per_session is false" {
|
|
184
|
+
write_global_rules '{
|
|
185
|
+
"rules": [
|
|
186
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "Skipping hooks usually masks the real issue.", "fire_once_per_session": false}
|
|
187
|
+
]
|
|
188
|
+
}'
|
|
189
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
190
|
+
|
|
191
|
+
# Both invocations must inject — explicit false should not get coerced to true.
|
|
192
|
+
local first second
|
|
193
|
+
first=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
194
|
+
second=$(cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
195
|
+
|
|
196
|
+
echo "$first" | jq -e '.hookSpecificOutput.additionalContext | contains("Skipping hooks")' >/dev/null
|
|
197
|
+
echo "$second" | jq -e '.hookSpecificOutput.additionalContext | contains("Skipping hooks")' >/dev/null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "hook still emits prompt_rule.matched on subsequent match but not prompt_rule.applied" {
|
|
201
|
+
write_global_rules '{
|
|
202
|
+
"rules": [
|
|
203
|
+
{"id": "rule-no-verify", "pattern": "--no-verify", "guidance": "msg"}
|
|
204
|
+
]
|
|
205
|
+
}'
|
|
206
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
207
|
+
|
|
208
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" >/dev/null 2>&1
|
|
209
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
210
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" >/dev/null 2>&1
|
|
211
|
+
|
|
212
|
+
grep -q '"event_type":"prompt_rule.matched"' "$ONLOOKER_EVENTS_LOG"
|
|
213
|
+
! grep -q '"event_type":"prompt_rule.applied"' "$ONLOOKER_EVENTS_LOG"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@test "hook respects per_turn_max_chars: drops overflowing rules" {
|
|
217
|
+
write_global_rules '{
|
|
218
|
+
"rules": [
|
|
219
|
+
{"id": "r1", "pattern": "no-verify", "guidance": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"},
|
|
220
|
+
{"id": "r2", "pattern": "no-verify", "guidance": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"}
|
|
221
|
+
]
|
|
222
|
+
}'
|
|
223
|
+
# Use a temp plugin root with a tiny char budget
|
|
224
|
+
local tmp_root="${BATS_TEST_TMPDIR}/tiny-budget-root"
|
|
225
|
+
mkdir -p "$tmp_root"
|
|
226
|
+
printf '%s\n' '{"plugin_name":"onlooker","prompt_rules":{"enabled":true,"per_turn_max_chars":120}}' > "$tmp_root/config.json"
|
|
227
|
+
|
|
228
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
229
|
+
local output
|
|
230
|
+
output=$(CLAUDE_PLUGIN_ROOT="$tmp_root" cat "$fixture" | CLAUDE_PLUGIN_ROOT="$tmp_root" "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" 2>/dev/null)
|
|
231
|
+
|
|
232
|
+
# First rule fits (~98 chars); second pushes past 120, gets dropped.
|
|
233
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("AAAA") and (contains("BBBB") | not)' >/dev/null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@test "hook short-circuits when prompt_rules.enabled = false" {
|
|
237
|
+
write_global_rules '{
|
|
238
|
+
"rules": [
|
|
239
|
+
{"id": "r1", "pattern": "no-verify", "guidance": "should not fire"}
|
|
240
|
+
]
|
|
241
|
+
}'
|
|
242
|
+
local tmp_root="${BATS_TEST_TMPDIR}/disabled-root"
|
|
243
|
+
mkdir -p "$tmp_root"
|
|
244
|
+
printf '%s\n' '{"plugin_name":"onlooker","prompt_rules":{"enabled":false}}' > "$tmp_root/config.json"
|
|
245
|
+
|
|
246
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json"
|
|
247
|
+
local output
|
|
248
|
+
output=$(CLAUDE_PLUGIN_ROOT="$tmp_root" "${REPO_ROOT}/scripts/hooks/prompt-rule-injector.sh" < "$fixture" 2>/dev/null)
|
|
249
|
+
[ -z "$output" ]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@test "list_table reports active rules and per-session fire status" {
|
|
253
|
+
write_global_rules '{
|
|
254
|
+
"rules": [
|
|
255
|
+
{"id": "r1", "pattern": "foo", "guidance": "global one"},
|
|
256
|
+
{"id": "r2", "pattern": "bar", "guidance": "global two"}
|
|
257
|
+
]
|
|
258
|
+
}'
|
|
259
|
+
prompt_rules_mark_fired "session-list" "r1"
|
|
260
|
+
|
|
261
|
+
local out
|
|
262
|
+
out=$(prompt_rules_list_table "session-list" "$TEST_HOME")
|
|
263
|
+
echo "$out" | grep -q "active rules: 2"
|
|
264
|
+
echo "$out" | grep -q "id: r1"
|
|
265
|
+
echo "$out" | grep -q "id: r2"
|
|
266
|
+
# r1 is fired, r2 is not
|
|
267
|
+
echo "$out" | grep -A2 "id: r1" | grep -q "fired: yes"
|
|
268
|
+
echo "$out" | grep -A2 "id: r2" | grep -q "fired: no"
|
|
269
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
load_validate_path
|
|
7
|
+
# shellcheck source=../../scripts/lib/onlooker-schema.sh
|
|
8
|
+
source "${REPO_ROOT}/scripts/lib/onlooker-schema.sh"
|
|
9
|
+
# shellcheck source=../../scripts/lib/tool-history.sh
|
|
10
|
+
source "${REPO_ROOT}/scripts/lib/tool-history.sh"
|
|
11
|
+
export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
|
|
12
|
+
|
|
13
|
+
LARGE_FILE="${BATS_TEST_TMPDIR}/large-source.ts"
|
|
14
|
+
# > LARGE_FILE_BYTES_ON_DISK (100_000) for large_file_full_read
|
|
15
|
+
printf '%*s\n' 120000 "" >"$LARGE_FILE"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@test "tool_history maps full Read to read_mode full" {
|
|
19
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/post-tool-use-read.json"
|
|
20
|
+
local record
|
|
21
|
+
record=$(tool_history_build_record "$(cat "$fixture")")
|
|
22
|
+
echo "$record" | jq -e \
|
|
23
|
+
'.payload.read_mode == "full"
|
|
24
|
+
and .payload.path == "/project/src/main.ts"
|
|
25
|
+
and (.payload.large_file_full_read | not)' \
|
|
26
|
+
>/dev/null
|
|
27
|
+
echo "$record" | onlooker_validate_event
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@test "tool_history maps chunked Read to read_mode partial with offset and limit" {
|
|
31
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/post-tool-use-read-chunked.json"
|
|
32
|
+
local record
|
|
33
|
+
record=$(tool_history_build_record "$(cat "$fixture")")
|
|
34
|
+
echo "$record" | jq -e \
|
|
35
|
+
'.payload.read_mode == "partial"
|
|
36
|
+
and .payload.offset == 400
|
|
37
|
+
and .payload.limit == 80
|
|
38
|
+
and .payload.lines_read == 3' \
|
|
39
|
+
>/dev/null
|
|
40
|
+
echo "$record" | onlooker_validate_event
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "tool_history flags large_file_full_read for full read of large on-disk file" {
|
|
44
|
+
local input
|
|
45
|
+
input=$(jq -n \
|
|
46
|
+
--arg path "$LARGE_FILE" \
|
|
47
|
+
--arg content "peek\n" \
|
|
48
|
+
'{
|
|
49
|
+
session_id: "history-session-002",
|
|
50
|
+
hook_event_name: "PostToolUse",
|
|
51
|
+
tool_name: "Read",
|
|
52
|
+
tool_input: {file_path: $path},
|
|
53
|
+
tool_response: {content: $content}
|
|
54
|
+
}')
|
|
55
|
+
local record
|
|
56
|
+
record=$(tool_history_build_record "$input")
|
|
57
|
+
echo "$record" | jq -e \
|
|
58
|
+
'.payload.read_mode == "full"
|
|
59
|
+
and .payload.large_file_full_read == true
|
|
60
|
+
and .payload.file_bytes_on_disk > 100000' \
|
|
61
|
+
>/dev/null
|
|
62
|
+
echo "$record" | onlooker_validate_event
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "tool-history-tracker appends chunked read to session JSONL" {
|
|
66
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/post-tool-use-read-chunked.json"
|
|
67
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/history-session-001.jsonl"
|
|
68
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
69
|
+
|
|
70
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/tool-history-tracker.sh" >/dev/null 2>&1
|
|
71
|
+
|
|
72
|
+
tail -n 1 "$history_file" | jq -e '.payload.read_mode == "partial"' >/dev/null
|
|
73
|
+
}
|
|
@@ -19,6 +19,7 @@ setup() {
|
|
|
19
19
|
'.schema_version == "1.0"
|
|
20
20
|
and .event_type == "tool.file.read"
|
|
21
21
|
and .payload.path == "/project/src/main.ts"
|
|
22
|
+
and .payload.read_mode == "full"
|
|
22
23
|
and .session_id == "history-session-001"' \
|
|
23
24
|
>/dev/null
|
|
24
25
|
echo "$record" | onlooker_validate_event
|