@onlooker-community/ecosystem 0.28.0 → 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 +14 -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 +14 -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 +17 -5
- 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,118 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises Inspector config loading: defaults, overrides, and the
|
|
4
|
+
# per-extension checks lookup helper.
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/inspector"
|
|
10
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
11
|
+
# shellcheck disable=SC1091
|
|
12
|
+
source "${PLUGIN_ROOT}/scripts/lib/inspector-config.sh"
|
|
13
|
+
|
|
14
|
+
REPO="${BATS_TEST_TMPDIR}/repo"
|
|
15
|
+
mkdir -p "${REPO}/.claude"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@test "disabled by default" {
|
|
19
|
+
inspector_config_load "$REPO"
|
|
20
|
+
run inspector_config_enabled
|
|
21
|
+
[ "$status" -ne 0 ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@test "enabled when settings opt in" {
|
|
25
|
+
printf '%s\n' '{"inspector":{"enabled":true}}' >"${REPO}/.claude/settings.json"
|
|
26
|
+
inspector_config_load "$REPO"
|
|
27
|
+
run inspector_config_enabled
|
|
28
|
+
[ "$status" -eq 0 ]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "default timeout_seconds_per_check is 10" {
|
|
32
|
+
inspector_config_load "$REPO"
|
|
33
|
+
[ "$(inspector_config_timeout_per_check)" = "10" ]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@test "timeout_seconds_per_check override is honored" {
|
|
37
|
+
printf '%s\n' '{"inspector":{"timeout_seconds_per_check":3}}' >"${REPO}/.claude/settings.json"
|
|
38
|
+
inspector_config_load "$REPO"
|
|
39
|
+
[ "$(inspector_config_timeout_per_check)" = "3" ]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@test "default total_timeout_seconds is 30" {
|
|
43
|
+
inspector_config_load "$REPO"
|
|
44
|
+
[ "$(inspector_config_total_timeout)" = "30" ]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@test "default output_excerpt_max_bytes is 4096" {
|
|
48
|
+
inspector_config_load "$REPO"
|
|
49
|
+
[ "$(inspector_config_output_excerpt_max_bytes)" = "4096" ]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "show_clean_runs is false by default" {
|
|
53
|
+
inspector_config_load "$REPO"
|
|
54
|
+
run inspector_config_show_clean_runs
|
|
55
|
+
[ "$status" -ne 0 ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "show_clean_runs flips on when set" {
|
|
59
|
+
printf '%s\n' '{"inspector":{"show_clean_runs":true}}' >"${REPO}/.claude/settings.json"
|
|
60
|
+
inspector_config_load "$REPO"
|
|
61
|
+
run inspector_config_show_clean_runs
|
|
62
|
+
[ "$status" -eq 0 ]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test "default exclude_paths includes node_modules and dist" {
|
|
66
|
+
inspector_config_load "$REPO"
|
|
67
|
+
local excludes
|
|
68
|
+
excludes=$(inspector_config_exclude_paths)
|
|
69
|
+
echo "$excludes" | jq -e 'index("node_modules")' >/dev/null
|
|
70
|
+
echo "$excludes" | jq -e 'index("dist")' >/dev/null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "exclude_paths is fully replaced by repo settings (not merged)" {
|
|
74
|
+
printf '%s\n' '{"inspector":{"exclude_paths":["only-this"]}}' >"${REPO}/.claude/settings.json"
|
|
75
|
+
inspector_config_load "$REPO"
|
|
76
|
+
local excludes
|
|
77
|
+
excludes=$(inspector_config_exclude_paths)
|
|
78
|
+
[ "$(echo "$excludes" | jq 'length')" = "1" ]
|
|
79
|
+
echo "$excludes" | jq -e 'index("only-this")' >/dev/null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "checks_for_extension returns [] when extension is unconfigured" {
|
|
83
|
+
inspector_config_load "$REPO"
|
|
84
|
+
[ "$(inspector_config_checks_for_extension '.ts')" = "[]" ]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@test "checks_for_extension returns the configured object form" {
|
|
88
|
+
cat >"${REPO}/.claude/settings.json" <<'EOF'
|
|
89
|
+
{ "inspector": { "checks": { ".ts": [
|
|
90
|
+
{ "name": "biome", "kind": "lint", "argv": ["biome", "check", "${file}"] }
|
|
91
|
+
] } } }
|
|
92
|
+
EOF
|
|
93
|
+
inspector_config_load "$REPO"
|
|
94
|
+
local checks
|
|
95
|
+
checks=$(inspector_config_checks_for_extension '.ts')
|
|
96
|
+
[ "$(echo "$checks" | jq 'length')" = "1" ]
|
|
97
|
+
[ "$(echo "$checks" | jq -r '.[0].name')" = "biome" ]
|
|
98
|
+
[ "$(echo "$checks" | jq -r '.[0].kind')" = "lint" ]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@test "checks_for_extension normalizes bare argv arrays into objects" {
|
|
102
|
+
cat >"${REPO}/.claude/settings.json" <<'EOF'
|
|
103
|
+
{ "inspector": { "checks": { ".sh": [
|
|
104
|
+
["shellcheck", "${file}"]
|
|
105
|
+
] } } }
|
|
106
|
+
EOF
|
|
107
|
+
inspector_config_load "$REPO"
|
|
108
|
+
local checks
|
|
109
|
+
checks=$(inspector_config_checks_for_extension '.sh')
|
|
110
|
+
[ "$(echo "$checks" | jq -r '.[0].name')" = "shellcheck" ]
|
|
111
|
+
[ "$(echo "$checks" | jq -r '.[0].kind')" = "lint" ]
|
|
112
|
+
[ "$(echo "$checks" | jq -r '.[0].argv | length')" = "2" ]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@test "checks_for_extension returns [] for an empty extension" {
|
|
116
|
+
inspector_config_load "$REPO"
|
|
117
|
+
[ "$(inspector_config_checks_for_extension '')" = "[]" ]
|
|
118
|
+
}
|
|
@@ -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
|
+
}
|