@onlooker-community/ecosystem 0.28.1 → 0.29.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 +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +2 -2
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +2 -0
- package/docs/plugin-catalog.md +125 -0
- package/package.json +3 -3
- package/plugins/compass/.claude-plugin/plugin.json +1 -1
- package/plugins/compass/CHANGELOG.md +7 -0
- package/plugins/compass/README.md +1 -3
- package/plugins/compass/config.json +1 -2
- package/plugins/compass/docs/design.md +1 -2
- package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
- package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
- package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
- package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
- package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
- package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
- package/plugins/inspector/.claude-plugin/plugin.json +14 -0
- package/plugins/inspector/README.md +155 -0
- package/plugins/inspector/config.json +25 -0
- package/plugins/inspector/docs/design.md +286 -0
- package/plugins/inspector/hooks/hooks.json +33 -0
- package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
- package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
- package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
- package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
- package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
- package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
- package/test/bats/archivist-project-key.bats +79 -0
- package/test/bats/archivist-storage.bats +79 -0
- package/test/bats/compact-tracker.bats +125 -0
- package/test/bats/compass-config.bats +65 -0
- package/test/bats/compass-gate.bats +129 -0
- package/test/bats/compass-sanitizer.bats +69 -0
- package/test/bats/compass-symbolic-skip.bats +88 -0
- package/test/bats/compass-transcript.bats +80 -0
- package/test/bats/inspector-config.bats +118 -0
- package/test/bats/inspector-events.bats +156 -0
- package/test/bats/inspector-post-write-hook.bats +164 -0
- package/test/bats/inspector-project-key.bats +68 -0
- package/test/bats/inspector-ulid.bats +34 -0
- package/test/bats/onlooker-schema.bats +111 -0
- package/test/bats/prompt-rules.bats +98 -0
- package/test/bats/session-tracker.bats +260 -0
- package/test/bats/skill-usage-tracker.bats +63 -0
- package/test/bats/task-tracker.bats +102 -0
- package/test/bats/turn-tracker.bats +180 -0
- package/test/bats/validate-path.bats +125 -0
- package/test/bats/worktree-tracker.bats +167 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates every emitted inspector.* event against @onlooker-community/schema.
|
|
4
|
+
#
|
|
5
|
+
# The inspector.* event types ship in @onlooker-community/schema; until the
|
|
6
|
+
# installed version includes them, these tests skip rather than fail. Once the
|
|
7
|
+
# ecosystem's schema dependency is bumped to a release that carries them, they
|
|
8
|
+
# run for real.
|
|
9
|
+
|
|
10
|
+
setup() {
|
|
11
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
12
|
+
setup_test_env
|
|
13
|
+
|
|
14
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/inspector"
|
|
15
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
16
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
17
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
18
|
+
|
|
19
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
20
|
+
export CLAUDE_SESSION_ID="bats-session-$$"
|
|
21
|
+
|
|
22
|
+
# shellcheck disable=SC1091
|
|
23
|
+
source "${PLUGIN_ROOT}/scripts/lib/inspector-events.sh"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_require_inspector_schema() {
|
|
27
|
+
if ! grep -q "inspector.check.passed" \
|
|
28
|
+
"${REPO_ROOT}/node_modules/@onlooker-community/schema/schemas/event.v1.json" 2>/dev/null; then
|
|
29
|
+
skip "installed @onlooker-community/schema has no inspector.* types yet"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_validate_latest_event() {
|
|
34
|
+
local last
|
|
35
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
36
|
+
[ -n "$last" ] || return 1
|
|
37
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
38
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@test "inspector.check.passed validates" {
|
|
42
|
+
_require_inspector_schema
|
|
43
|
+
local p
|
|
44
|
+
p=$(jq -n '{
|
|
45
|
+
file_path: "/repo/src/a.ts",
|
|
46
|
+
file_path_relative: "src/a.ts",
|
|
47
|
+
tool_name: "Edit",
|
|
48
|
+
check_name: "biome",
|
|
49
|
+
check_kind: "lint",
|
|
50
|
+
argv: ["biome", "check", "/repo/src/a.ts"],
|
|
51
|
+
duration_ms: 124,
|
|
52
|
+
project_key: "a1b2c3d4e5f6"
|
|
53
|
+
}')
|
|
54
|
+
inspector_emit_event "inspector.check.passed" "$p"
|
|
55
|
+
run _validate_latest_event
|
|
56
|
+
[ "$status" -eq 0 ]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@test "inspector.check.failed validates" {
|
|
60
|
+
_require_inspector_schema
|
|
61
|
+
local p
|
|
62
|
+
p=$(jq -n '{
|
|
63
|
+
file_path: "/repo/src/a.ts",
|
|
64
|
+
file_path_relative: "src/a.ts",
|
|
65
|
+
tool_name: "Edit",
|
|
66
|
+
check_name: "tsc",
|
|
67
|
+
check_kind: "typecheck",
|
|
68
|
+
argv: ["tsc", "--noEmit"],
|
|
69
|
+
duration_ms: 980,
|
|
70
|
+
exit_code: 2,
|
|
71
|
+
issue_count: 3,
|
|
72
|
+
output_excerpt: "src/a.ts:42:5 - Type error",
|
|
73
|
+
output_truncated: false,
|
|
74
|
+
project_key: "a1b2c3d4e5f6"
|
|
75
|
+
}')
|
|
76
|
+
inspector_emit_event "inspector.check.failed" "$p"
|
|
77
|
+
run _validate_latest_event
|
|
78
|
+
[ "$status" -eq 0 ]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "inspector.check.failed validates with null issue_count" {
|
|
82
|
+
_require_inspector_schema
|
|
83
|
+
local p
|
|
84
|
+
p=$(jq -n '{
|
|
85
|
+
file_path: "/repo/src/a.ts",
|
|
86
|
+
tool_name: "Edit",
|
|
87
|
+
check_name: "mystery-linter",
|
|
88
|
+
check_kind: "lint",
|
|
89
|
+
exit_code: 1,
|
|
90
|
+
issue_count: null
|
|
91
|
+
}')
|
|
92
|
+
inspector_emit_event "inspector.check.failed" "$p"
|
|
93
|
+
run _validate_latest_event
|
|
94
|
+
[ "$status" -eq 0 ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "inspector.check.skipped (per-check) validates" {
|
|
98
|
+
_require_inspector_schema
|
|
99
|
+
local p
|
|
100
|
+
p=$(jq -n '{
|
|
101
|
+
file_path: "/repo/scripts/deploy.sh",
|
|
102
|
+
file_path_relative: "scripts/deploy.sh",
|
|
103
|
+
tool_name: "Edit",
|
|
104
|
+
check_name: "shellcheck",
|
|
105
|
+
check_kind: "lint",
|
|
106
|
+
reason: "tool_missing",
|
|
107
|
+
project_key: "a1b2c3d4e5f6"
|
|
108
|
+
}')
|
|
109
|
+
inspector_emit_event "inspector.check.skipped" "$p"
|
|
110
|
+
run _validate_latest_event
|
|
111
|
+
[ "$status" -eq 0 ]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@test "inspector.check.skipped (whole-file) validates" {
|
|
115
|
+
_require_inspector_schema
|
|
116
|
+
local p
|
|
117
|
+
p=$(jq -n '{
|
|
118
|
+
file_path: "/repo/node_modules/foo/index.js",
|
|
119
|
+
file_path_relative: "node_modules/foo/index.js",
|
|
120
|
+
tool_name: "Write",
|
|
121
|
+
reason: "excluded_path",
|
|
122
|
+
project_key: "a1b2c3d4e5f6"
|
|
123
|
+
}')
|
|
124
|
+
inspector_emit_event "inspector.check.skipped" "$p"
|
|
125
|
+
run _validate_latest_event
|
|
126
|
+
[ "$status" -eq 0 ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@test "inspector.run.completed validates" {
|
|
130
|
+
_require_inspector_schema
|
|
131
|
+
local p
|
|
132
|
+
p=$(jq -n '{
|
|
133
|
+
file_path: "/repo/src/a.ts",
|
|
134
|
+
file_path_relative: "src/a.ts",
|
|
135
|
+
tool_name: "Edit",
|
|
136
|
+
checks_run: 2,
|
|
137
|
+
checks_passed: 1,
|
|
138
|
+
checks_failed: 1,
|
|
139
|
+
checks_skipped: 0,
|
|
140
|
+
duration_ms: 1080,
|
|
141
|
+
project_key: "a1b2c3d4e5f6"
|
|
142
|
+
}')
|
|
143
|
+
inspector_emit_event "inspector.run.completed" "$p"
|
|
144
|
+
run _validate_latest_event
|
|
145
|
+
[ "$status" -eq 0 ]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@test "emission rejects an unknown event type" {
|
|
149
|
+
run inspector_emit_event "inspector.no.such.event" '{"file_path":"x"}'
|
|
150
|
+
[ "$status" -ne 0 ]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@test "inspector_emit_event returns 1 when payload is empty" {
|
|
154
|
+
run inspector_emit_event "inspector.check.passed" ""
|
|
155
|
+
[ "$status" -ne 0 ]
|
|
156
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the Inspector PostToolUse hook's end-to-end behavior.
|
|
4
|
+
# Uses fake check commands (sh -c) so the suite has no external lint deps.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
|
|
10
|
+
# These tests assert on the event log, so they require a schema that
|
|
11
|
+
# accepts inspector.* events. Skip when the installed package is too old.
|
|
12
|
+
if ! grep -q "inspector.check.passed" \
|
|
13
|
+
"${BATS_TEST_DIRNAME}/../../node_modules/@onlooker-community/schema/schemas/event.v1.json" 2>/dev/null; then
|
|
14
|
+
skip "installed @onlooker-community/schema has no inspector.* types yet"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/inspector"
|
|
18
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/inspector-post-write.sh"
|
|
19
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
20
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
21
|
+
|
|
22
|
+
REPO="${BATS_TEST_TMPDIR}/repo"
|
|
23
|
+
mkdir -p "${REPO}/src" "${REPO}/.claude" "${REPO}/node_modules/foo"
|
|
24
|
+
git -C "$REPO" init -q
|
|
25
|
+
git -C "$REPO" remote add origin "https://example.com/test-${BATS_TEST_NUMBER}.git"
|
|
26
|
+
printf 'sample\n' >"${REPO}/src/sample.ts"
|
|
27
|
+
printf 'sample\n' >"${REPO}/src/sample.py"
|
|
28
|
+
printf 'sample\n' >"${REPO}/node_modules/foo/index.ts"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_input() {
|
|
32
|
+
local cwd="${1:-$REPO}" tool="${2:-Edit}" path="${3:-${REPO}/src/sample.ts}"
|
|
33
|
+
jq -n --arg cwd "$cwd" --arg tool "$tool" --arg p "$path" --arg sid "test-${BATS_TEST_NUMBER}" \
|
|
34
|
+
'{cwd:$cwd, session_id:$sid, tool_name:$tool, tool_input:{file_path:$p}}'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_settings() {
|
|
38
|
+
cat >"${REPO}/.claude/settings.json"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_event_count() {
|
|
42
|
+
local et="$1"
|
|
43
|
+
if [[ -f "$ONLOOKER_EVENTS_LOG" ]]; then
|
|
44
|
+
jq -c "select(.event_type == \"$et\")" "$ONLOOKER_EVENTS_LOG" | wc -l | tr -d ' '
|
|
45
|
+
else
|
|
46
|
+
printf '0'
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_run_hook() {
|
|
51
|
+
local input="$1"
|
|
52
|
+
printf '%s' "$input" | ONLOOKER_DIR="$ONLOOKER_DIR" CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" bash "$HOOK"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "exits 0 silently when inspector.enabled is false (default)" {
|
|
56
|
+
run _run_hook "$(_input)"
|
|
57
|
+
[ "$status" -eq 0 ]
|
|
58
|
+
[ -z "$output" ]
|
|
59
|
+
[ ! -f "$ONLOOKER_EVENTS_LOG" ] || [ "$(_event_count inspector.run.completed)" = "0" ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "exits 0 when tool_name is not Write/Edit/MultiEdit" {
|
|
63
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"t","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
64
|
+
run _run_hook "$(_input "$REPO" "Bash")"
|
|
65
|
+
[ "$status" -eq 0 ]
|
|
66
|
+
[ -z "$output" ]
|
|
67
|
+
[ "$(_event_count inspector.run.completed)" = "0" ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@test "recursion guard: INSPECTOR_NESTED=1 causes immediate exit 0" {
|
|
71
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"t","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
72
|
+
run bash -c "printf '%s' '$(_input)' | INSPECTOR_NESTED=1 ONLOOKER_DIR='$ONLOOKER_DIR' CLAUDE_PLUGIN_ROOT='$PLUGIN_ROOT' '$HOOK'"
|
|
73
|
+
[ "$status" -eq 0 ]
|
|
74
|
+
[ -z "$output" ]
|
|
75
|
+
[ "$(_event_count inspector.run.completed)" = "0" ]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@test "files in excluded_paths emit a single .skipped event and produce no agent output" {
|
|
79
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"t","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
80
|
+
run _run_hook "$(_input "$REPO" "Write" "${REPO}/node_modules/foo/index.ts")"
|
|
81
|
+
[ "$status" -eq 0 ]
|
|
82
|
+
[ -z "$output" ]
|
|
83
|
+
[ "$(_event_count inspector.check.skipped)" = "1" ]
|
|
84
|
+
[ "$(_event_count inspector.run.completed)" = "0" ]
|
|
85
|
+
# The skipped reason must be excluded_path.
|
|
86
|
+
[ "$(jq -r 'select(.event_type=="inspector.check.skipped").payload.reason' "$ONLOOKER_EVENTS_LOG")" = "excluded_path" ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "extensions with no configured checks emit a single .skipped with no_extension_match" {
|
|
90
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"t","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
91
|
+
run _run_hook "$(_input "$REPO" "Edit" "${REPO}/src/sample.py")"
|
|
92
|
+
[ "$status" -eq 0 ]
|
|
93
|
+
[ "$(_event_count inspector.check.skipped)" = "1" ]
|
|
94
|
+
[ "$(jq -r 'select(.event_type=="inspector.check.skipped").payload.reason' "$ONLOOKER_EVENTS_LOG")" = "no_extension_match" ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "a passing check emits .passed and silences agent-facing stdout by default" {
|
|
98
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"clean","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
99
|
+
run _run_hook "$(_input)"
|
|
100
|
+
[ "$status" -eq 0 ]
|
|
101
|
+
[ -z "$output" ]
|
|
102
|
+
[ "$(_event_count inspector.check.passed)" = "1" ]
|
|
103
|
+
[ "$(_event_count inspector.run.completed)" = "1" ]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@test "show_clean_runs surfaces the file header for passing checks" {
|
|
107
|
+
echo '{"inspector":{"enabled":true,"show_clean_runs":true,"checks":{".ts":[{"name":"clean","kind":"lint","argv":["true"]}]}}}' | _settings
|
|
108
|
+
run _run_hook "$(_input)"
|
|
109
|
+
[ "$status" -eq 0 ]
|
|
110
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]]
|
|
111
|
+
[[ "$output" == *"✓ clean"* ]]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@test "a failing check emits .failed with exit_code and surfaces issue lines to the agent" {
|
|
115
|
+
cat <<'EOF' | _settings
|
|
116
|
+
{"inspector":{"enabled":true,"checks":{".ts":[
|
|
117
|
+
{"name":"broken","kind":"lint","argv":["sh","-c","echo 'src/sample.ts:1:1 - Bad'; exit 2"]}
|
|
118
|
+
]}}}
|
|
119
|
+
EOF
|
|
120
|
+
run _run_hook "$(_input)"
|
|
121
|
+
[ "$status" -eq 0 ]
|
|
122
|
+
[[ "$output" == *"inspector: src/sample.ts"* ]]
|
|
123
|
+
[[ "$output" == *"✗ broken"* ]]
|
|
124
|
+
[[ "$output" == *"src/sample.ts:1:1 - Bad"* ]]
|
|
125
|
+
[ "$(_event_count inspector.check.failed)" = "1" ]
|
|
126
|
+
[ "$(jq -r 'select(.event_type=="inspector.check.failed").payload.exit_code' "$ONLOOKER_EVENTS_LOG")" = "2" ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@test "a missing tool emits .skipped with tool_missing and produces no agent output" {
|
|
130
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"ghost","kind":"lint","argv":["this-tool-does-not-exist"]}]}}}' | _settings
|
|
131
|
+
run _run_hook "$(_input)"
|
|
132
|
+
[ "$status" -eq 0 ]
|
|
133
|
+
[ -z "$output" ]
|
|
134
|
+
[ "$(_event_count inspector.check.skipped)" = "1" ]
|
|
135
|
+
[ "$(jq -r 'select(.event_type=="inspector.check.skipped").payload.reason' "$ONLOOKER_EVENTS_LOG")" = "tool_missing" ]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@test "run.completed aggregates pass/fail/skip counts" {
|
|
139
|
+
cat <<'EOF' | _settings
|
|
140
|
+
{"inspector":{"enabled":true,"checks":{".ts":[
|
|
141
|
+
{"name":"clean", "kind":"lint", "argv":["true"]},
|
|
142
|
+
{"name":"broken","kind":"typecheck", "argv":["sh","-c","echo 'oops'; exit 1"]},
|
|
143
|
+
{"name":"ghost", "kind":"lint", "argv":["this-tool-does-not-exist"]}
|
|
144
|
+
]}}}
|
|
145
|
+
EOF
|
|
146
|
+
run _run_hook "$(_input)"
|
|
147
|
+
[ "$status" -eq 0 ]
|
|
148
|
+
local completed
|
|
149
|
+
completed=$(jq -c 'select(.event_type=="inspector.run.completed").payload' "$ONLOOKER_EVENTS_LOG")
|
|
150
|
+
[ "$(echo "$completed" | jq -r '.checks_run')" = "2" ]
|
|
151
|
+
[ "$(echo "$completed" | jq -r '.checks_passed')" = "1" ]
|
|
152
|
+
[ "$(echo "$completed" | jq -r '.checks_failed')" = "1" ]
|
|
153
|
+
[ "$(echo "$completed" | jq -r '.checks_skipped')" = "1" ]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@test "argv is expanded with the touched file path" {
|
|
157
|
+
echo '{"inspector":{"enabled":true,"checks":{".ts":[{"name":"echo-file","kind":"lint","argv":["sh","-c","echo $1; test -n \"$1\"","--","${file}"]}]}}}' | _settings
|
|
158
|
+
run _run_hook "$(_input)"
|
|
159
|
+
[ "$status" -eq 0 ]
|
|
160
|
+
local argv
|
|
161
|
+
argv=$(jq -c 'select(.event_type=="inspector.check.passed").payload.argv' "$ONLOOKER_EVENTS_LOG")
|
|
162
|
+
# The last argv slot should now hold the resolved touched file path.
|
|
163
|
+
[[ "$(echo "$argv" | jq -r '.[-1]')" == *"src/sample.ts" ]]
|
|
164
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises Inspector's project-key derivation.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/inspector"
|
|
9
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/inspector-project-key.sh"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@test "produces a 12-char hex key from origin remote" {
|
|
15
|
+
REPO="${BATS_TEST_TMPDIR}/repo"
|
|
16
|
+
mkdir -p "$REPO"
|
|
17
|
+
git -C "$REPO" init -q
|
|
18
|
+
git -C "$REPO" remote add origin "https://example.com/widgets.git"
|
|
19
|
+
|
|
20
|
+
local key
|
|
21
|
+
key=$(inspector_project_key "$REPO")
|
|
22
|
+
[ "${#key}" = "12" ]
|
|
23
|
+
[[ "$key" =~ ^[0-9a-f]{12}$ ]]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@test "remote-derived key is stable across temp clones" {
|
|
27
|
+
REPO_A="${BATS_TEST_TMPDIR}/a"; mkdir -p "$REPO_A"
|
|
28
|
+
REPO_B="${BATS_TEST_TMPDIR}/b"; mkdir -p "$REPO_B"
|
|
29
|
+
git -C "$REPO_A" init -q
|
|
30
|
+
git -C "$REPO_B" init -q
|
|
31
|
+
git -C "$REPO_A" remote add origin "https://example.com/x.git"
|
|
32
|
+
git -C "$REPO_B" remote add origin "https://example.com/x.git"
|
|
33
|
+
|
|
34
|
+
local key_a key_b
|
|
35
|
+
key_a=$(inspector_project_key "$REPO_A")
|
|
36
|
+
key_b=$(inspector_project_key "$REPO_B")
|
|
37
|
+
[ "$key_a" = "$key_b" ]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@test "falls back to repo root when origin is missing" {
|
|
41
|
+
REPO="${BATS_TEST_TMPDIR}/no-remote"
|
|
42
|
+
mkdir -p "$REPO"
|
|
43
|
+
git -C "$REPO" init -q
|
|
44
|
+
local key
|
|
45
|
+
key=$(inspector_project_key "$REPO")
|
|
46
|
+
[ "${#key}" = "12" ]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@test "falls back to cwd when not a git repo" {
|
|
50
|
+
NOT_REPO="${BATS_TEST_TMPDIR}/not-git"
|
|
51
|
+
mkdir -p "$NOT_REPO"
|
|
52
|
+
local key
|
|
53
|
+
key=$(inspector_project_key "$NOT_REPO")
|
|
54
|
+
[ "${#key}" = "12" ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "project_repo_root returns repo top-level for a git checkout" {
|
|
58
|
+
REPO="${BATS_TEST_TMPDIR}/repo2"
|
|
59
|
+
mkdir -p "$REPO/sub"
|
|
60
|
+
git -C "$REPO" init -q
|
|
61
|
+
local root expected
|
|
62
|
+
root=$(inspector_project_repo_root "$REPO/sub")
|
|
63
|
+
# Canonicalize both sides: macOS git resolves tmp paths through /private/,
|
|
64
|
+
# while $REPO retains the symlinked /var/folders/ prefix.
|
|
65
|
+
expected=$(cd "$REPO" && /bin/pwd -P 2>/dev/null || pwd)
|
|
66
|
+
root=$(cd "$root" && /bin/pwd -P 2>/dev/null || pwd)
|
|
67
|
+
[ "$root" = "$expected" ]
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises Inspector's ULID generator.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/inspector"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/inspector-ulid.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "produces a 26-char Crockford Base32 ULID" {
|
|
14
|
+
local id
|
|
15
|
+
id=$(inspector_ulid)
|
|
16
|
+
[ "${#id}" = "26" ]
|
|
17
|
+
[[ "$id" =~ ^[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}$ ]]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@test "produces unique values on consecutive calls" {
|
|
21
|
+
local a b
|
|
22
|
+
a=$(inspector_ulid)
|
|
23
|
+
b=$(inspector_ulid)
|
|
24
|
+
[ "$a" != "$b" ]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "is lexicographically sortable across a short delay" {
|
|
28
|
+
local a b
|
|
29
|
+
a=$(inspector_ulid)
|
|
30
|
+
# Sleep is intentionally <1ms to avoid lengthening the suite, but
|
|
31
|
+
# even back-to-back invocations should not regress the timestamp prefix.
|
|
32
|
+
b=$(inspector_ulid)
|
|
33
|
+
[[ "$a" < "$b" || "$a" == "$b" ]] || [[ "${a:0:10}" == "${b:0:10}" ]]
|
|
34
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises scripts/lib/onlooker-schema.sh — the bash wrappers around the
|
|
4
|
+
# canonical event emitter (scripts/lib/onlooker-event.mjs). Events are
|
|
5
|
+
# validated against the real installed @onlooker-community/schema.
|
|
6
|
+
|
|
7
|
+
setup() {
|
|
8
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
9
|
+
load_validate_path
|
|
10
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
11
|
+
export ONLOOKER_PLUGIN_NAME="onlooker"
|
|
12
|
+
# shellcheck disable=SC1091
|
|
13
|
+
source "${REPO_ROOT}/scripts/lib/onlooker-schema.sh"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_fixture() {
|
|
17
|
+
cat "${REPO_ROOT}/test/fixtures/hook-inputs/${1}"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# --- onlooker_append_event -------------------------------------------------
|
|
21
|
+
|
|
22
|
+
@test "onlooker_append_event appends a parseable line carrying the event_type" {
|
|
23
|
+
local line
|
|
24
|
+
line='{"event_type":"tool.file.read","payload":{"path":"/x"}}'
|
|
25
|
+
run onlooker_append_event "$line"
|
|
26
|
+
[ "$status" -eq 0 ]
|
|
27
|
+
|
|
28
|
+
local last
|
|
29
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
30
|
+
# Round-trips through jq (valid JSON) and preserves the event_type.
|
|
31
|
+
[ "$(printf '%s' "$last" | jq -r '.event_type')" = "tool.file.read" ]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test "onlooker_append_event with empty input is a no-op returning 0" {
|
|
35
|
+
run onlooker_append_event ""
|
|
36
|
+
[ "$status" -eq 0 ]
|
|
37
|
+
# No log line written.
|
|
38
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@test "onlooker_append_event creates parent dirs when missing" {
|
|
42
|
+
# Point the log at a path whose parent does not yet exist.
|
|
43
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/nested/deeper/onlooker-events.jsonl"
|
|
44
|
+
[ ! -d "$(dirname "$ONLOOKER_EVENTS_LOG")" ]
|
|
45
|
+
|
|
46
|
+
run onlooker_append_event '{"event_type":"tool.file.read"}'
|
|
47
|
+
[ "$status" -eq 0 ]
|
|
48
|
+
[ -d "$(dirname "$ONLOOKER_EVENTS_LOG")" ]
|
|
49
|
+
[ "$(tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -r '.event_type')" = "tool.file.read" ]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "onlooker_append_event appends rather than truncating" {
|
|
53
|
+
onlooker_append_event '{"event_type":"tool.file.read"}'
|
|
54
|
+
onlooker_append_event '{"event_type":"tool.shell.exec"}'
|
|
55
|
+
[ "$(wc -l <"$ONLOOKER_EVENTS_LOG" | tr -d ' ')" -eq 2 ]
|
|
56
|
+
[ "$(tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -r '.event_type')" = "tool.shell.exec" ]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# --- onlooker_event_from_hook ----------------------------------------------
|
|
60
|
+
|
|
61
|
+
@test "onlooker_event_from_hook maps a mappable fixture to tool.file.read" {
|
|
62
|
+
local event
|
|
63
|
+
event=$(onlooker_event_from_hook "$(_fixture post-tool-use-read.json)")
|
|
64
|
+
[ -n "$event" ]
|
|
65
|
+
[ "$(printf '%s' "$event" | jq -r '.event_type')" = "tool.file.read" ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "onlooker_event_from_hook output validates against the schema" {
|
|
69
|
+
local event
|
|
70
|
+
event=$(onlooker_event_from_hook "$(_fixture post-tool-use-read.json)")
|
|
71
|
+
[ -n "$event" ]
|
|
72
|
+
printf '%s' "$event" | node "$_ONLOOKER_EVENT_JS" validate
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "onlooker_event_from_hook prints empty for an unmappable fixture" {
|
|
76
|
+
local event
|
|
77
|
+
event=$(onlooker_event_from_hook "$(_fixture session-start-startup.json)")
|
|
78
|
+
[ -z "$event" ]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "onlooker_event_from_hook with empty input returns 0 and prints nothing" {
|
|
82
|
+
run onlooker_event_from_hook ""
|
|
83
|
+
[ "$status" -eq 0 ]
|
|
84
|
+
[ -z "$output" ]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# --- onlooker_emit_from_hook -----------------------------------------------
|
|
88
|
+
|
|
89
|
+
@test "onlooker_emit_from_hook writes exactly one tool.file.read line" {
|
|
90
|
+
run onlooker_emit_from_hook "$(_fixture post-tool-use-read.json)"
|
|
91
|
+
[ "$status" -eq 0 ]
|
|
92
|
+
|
|
93
|
+
[ "$(wc -l <"$ONLOOKER_EVENTS_LOG" | tr -d ' ')" -eq 1 ]
|
|
94
|
+
[ "$(tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -r '.event_type')" = "tool.file.read" ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "onlooker_emit_from_hook leaves the log empty for an unmappable fixture" {
|
|
98
|
+
run onlooker_emit_from_hook "$(_fixture session-start-startup.json)"
|
|
99
|
+
[ "$status" -eq 0 ]
|
|
100
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "onlooker_emit_from_hook records blocked=true for a failed Bash tool" {
|
|
104
|
+
run onlooker_emit_from_hook "$(_fixture post-tool-use-failure-bash.json)"
|
|
105
|
+
[ "$status" -eq 0 ]
|
|
106
|
+
|
|
107
|
+
local last
|
|
108
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
109
|
+
[ "$(printf '%s' "$last" | jq -r '.event_type')" = "tool.shell.exec" ]
|
|
110
|
+
[ "$(printf '%s' "$last" | jq -r '.payload.blocked')" = "true" ]
|
|
111
|
+
}
|
|
@@ -267,3 +267,101 @@ write_project_rules() {
|
|
|
267
267
|
echo "$out" | grep -A2 "id: r1" | grep -q "fired: yes"
|
|
268
268
|
echo "$out" | grep -A2 "id: r2" | grep -q "fired: no"
|
|
269
269
|
}
|
|
270
|
+
|
|
271
|
+
@test "project_path: appends .claude/prompt-rules.json to the given cwd" {
|
|
272
|
+
local path
|
|
273
|
+
path=$(prompt_rules_project_path "/some/where")
|
|
274
|
+
[ "$path" = "/some/where/.claude/prompt-rules.json" ]
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@test "project_path: defaults to \$PWD when no cwd is given" {
|
|
278
|
+
local path expected
|
|
279
|
+
path=$(prompt_rules_project_path)
|
|
280
|
+
expected="$PWD/.claude/prompt-rules.json"
|
|
281
|
+
[ "$path" = "$expected" ]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@test "fired_path: builds path under the sessions dir from the session id" {
|
|
285
|
+
local path expected
|
|
286
|
+
path=$(prompt_rules_fired_path "sess-XYZ")
|
|
287
|
+
expected="$ONLOOKER_PROMPT_RULES_SESSIONS_DIR/sess-XYZ.json"
|
|
288
|
+
[ "$path" = "$expected" ]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@test "fired_path: defaults session id to 'unknown' when none is given" {
|
|
292
|
+
local path expected
|
|
293
|
+
path=$(prompt_rules_fired_path)
|
|
294
|
+
expected="$ONLOOKER_PROMPT_RULES_SESSIONS_DIR/unknown.json"
|
|
295
|
+
[ "$path" = "$expected" ]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@test "emit: appends a JSON event line with type, session, payload, and plugin" {
|
|
299
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
300
|
+
run prompt_rules_emit "sess-emit" "prompt_rule.matched" '{"rule_id":"rule-1"}'
|
|
301
|
+
[ "$status" -eq 0 ]
|
|
302
|
+
|
|
303
|
+
local line
|
|
304
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
305
|
+
[ "$(echo "$line" | jq -r '.event_type')" = "prompt_rule.matched" ]
|
|
306
|
+
[ "$(echo "$line" | jq -r '.session_id')" = "sess-emit" ]
|
|
307
|
+
[ "$(echo "$line" | jq -r '.payload.rule_id')" = "rule-1" ]
|
|
308
|
+
# No ONLOOKER_PLUGIN_NAME exported in this test → defaults to "onlooker".
|
|
309
|
+
[ "$(echo "$line" | jq -r '.plugin')" = "onlooker" ]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@test "emit: honors ONLOOKER_PLUGIN_NAME for the plugin field" {
|
|
313
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
314
|
+
ONLOOKER_PLUGIN_NAME="prompt-rules" prompt_rules_emit "sess-plugin" "prompt_rule.applied" '{"rule_id":"r9"}'
|
|
315
|
+
|
|
316
|
+
local line
|
|
317
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
318
|
+
[ "$(echo "$line" | jq -r '.plugin')" = "prompt-rules" ]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@test "emit: defaults payload to an empty object when none is given" {
|
|
322
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
323
|
+
run prompt_rules_emit "sess-nopayload" "prompt_rule.matched"
|
|
324
|
+
[ "$status" -eq 0 ]
|
|
325
|
+
|
|
326
|
+
local line
|
|
327
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
328
|
+
[ "$(echo "$line" | jq -c '.payload')" = "{}" ]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@test "emit: defaults session id to 'unknown' when none is given" {
|
|
332
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
333
|
+
prompt_rules_emit "" "prompt_rule.matched" '{}'
|
|
334
|
+
|
|
335
|
+
local line
|
|
336
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
337
|
+
[ "$(echo "$line" | jq -r '.session_id')" = "unknown" ]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@test "emit: includes a numeric turn field when ONLOOKER_TURN_NUMBER is exported" {
|
|
341
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
342
|
+
ONLOOKER_TURN_NUMBER=7 prompt_rules_emit "sess-turn" "prompt_rule.matched" '{}'
|
|
343
|
+
|
|
344
|
+
local line
|
|
345
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
346
|
+
[ "$(echo "$line" | jq -r '.turn')" = "7" ]
|
|
347
|
+
[ "$(echo "$line" | jq -r '.turn | type')" = "number" ]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@test "emit: omits the turn field when ONLOOKER_TURN_NUMBER is unset" {
|
|
351
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
352
|
+
# Guard against any ambient value leaking in from the environment.
|
|
353
|
+
unset ONLOOKER_TURN_NUMBER
|
|
354
|
+
prompt_rules_emit "sess-noturn" "prompt_rule.matched" '{}'
|
|
355
|
+
|
|
356
|
+
local line
|
|
357
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
358
|
+
[ "$(echo "$line" | jq -e 'has("turn")')" = "false" ] || \
|
|
359
|
+
[ "$(echo "$line" | jq 'has("turn")')" = "false" ]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@test "emit: returns 1 and writes nothing when event_type is empty" {
|
|
363
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
364
|
+
run prompt_rules_emit "sess-empty" ""
|
|
365
|
+
[ "$status" -eq 1 ]
|
|
366
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
367
|
+
}
|