@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.
Files changed (52) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.release-please-manifest.json +2 -2
  4. package/CHANGELOG.md +14 -0
  5. package/CLAUDE.md +2 -0
  6. package/docs/plugin-catalog.md +125 -0
  7. package/package.json +3 -3
  8. package/plugins/compass/.claude-plugin/plugin.json +1 -1
  9. package/plugins/compass/CHANGELOG.md +14 -0
  10. package/plugins/compass/README.md +1 -3
  11. package/plugins/compass/config.json +1 -2
  12. package/plugins/compass/docs/design.md +1 -2
  13. package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
  14. package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +17 -5
  15. package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
  16. package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
  17. package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
  18. package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
  19. package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
  20. package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
  21. package/plugins/inspector/.claude-plugin/plugin.json +14 -0
  22. package/plugins/inspector/README.md +155 -0
  23. package/plugins/inspector/config.json +25 -0
  24. package/plugins/inspector/docs/design.md +286 -0
  25. package/plugins/inspector/hooks/hooks.json +33 -0
  26. package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
  27. package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
  28. package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
  29. package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
  30. package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
  31. package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
  32. package/test/bats/archivist-project-key.bats +79 -0
  33. package/test/bats/archivist-storage.bats +79 -0
  34. package/test/bats/compact-tracker.bats +125 -0
  35. package/test/bats/compass-config.bats +65 -0
  36. package/test/bats/compass-gate.bats +129 -0
  37. package/test/bats/compass-sanitizer.bats +69 -0
  38. package/test/bats/compass-symbolic-skip.bats +88 -0
  39. package/test/bats/compass-transcript.bats +80 -0
  40. package/test/bats/inspector-config.bats +118 -0
  41. package/test/bats/inspector-events.bats +156 -0
  42. package/test/bats/inspector-post-write-hook.bats +164 -0
  43. package/test/bats/inspector-project-key.bats +68 -0
  44. package/test/bats/inspector-ulid.bats +34 -0
  45. package/test/bats/onlooker-schema.bats +111 -0
  46. package/test/bats/prompt-rules.bats +98 -0
  47. package/test/bats/session-tracker.bats +260 -0
  48. package/test/bats/skill-usage-tracker.bats +63 -0
  49. package/test/bats/task-tracker.bats +102 -0
  50. package/test/bats/turn-tracker.bats +180 -0
  51. package/test/bats/validate-path.bats +125 -0
  52. 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
+ }