@onlooker-community/ecosystem 0.34.0 → 0.43.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/skills/writing-tests/SKILL.md +27 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +14 -14
- package/.release-please-manifest.json +7 -7
- package/AGENTS.md +116 -0
- package/CHANGELOG.md +175 -0
- package/CLAUDE.md +5 -0
- package/docs/lesson-promotion-pipeline.md +6 -6
- package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
- package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
- package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
- package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
- package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
- package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
- package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
- package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
- package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
- package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
- package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
- package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
- package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
- package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
- package/package.json +2 -2
- package/plugins/assayer/.claude-plugin/plugin.json +1 -1
- package/plugins/assayer/CHANGELOG.md +7 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
- package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
- package/plugins/cartographer/CHANGELOG.md +54 -0
- package/plugins/cartographer/README.md +56 -1
- package/plugins/cartographer/config.json +7 -1
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
- package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
- package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
- package/plugins/cartographer/scripts/run-audit.sh +136 -53
- package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
- package/plugins/curator/.claude-plugin/plugin.json +1 -1
- package/plugins/curator/CHANGELOG.md +7 -0
- package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
- package/plugins/historian/.claude-plugin/plugin.json +1 -1
- package/plugins/historian/CHANGELOG.md +7 -0
- package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +71 -0
- package/plugins/librarian/config.json +30 -1
- package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
- package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
- package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
- package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
- package/plugins/librarian/skills/librarian/SKILL.md +98 -1
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +63 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
- package/plugins/tribunal/config.json +1 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
- package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
- package/scripts/lib/prompt-rules.sh +6 -1
- package/test/bats/archivist-inject.bats +1 -1
- package/test/bats/assayer-extract.bats +2 -2
- package/test/bats/bursar-session-start.bats +3 -3
- package/test/bats/cartographer-config.bats +43 -0
- package/test/bats/cartographer-events.bats +267 -0
- package/test/bats/cartographer-filter.bats +169 -0
- package/test/bats/cartographer-lock.bats +3 -3
- package/test/bats/cartographer-omission.bats +202 -0
- package/test/bats/cartographer-resolve.bats +287 -0
- package/test/bats/cartographer-run-audit.bats +325 -0
- package/test/bats/compass-sanitizer.bats +11 -11
- package/test/bats/compass-transcript.bats +2 -2
- package/test/bats/config.bats +15 -15
- package/test/bats/curator-session-start.bats +10 -3
- package/test/bats/emit-payload-default.bats +52 -0
- package/test/bats/governor-ledger.bats +1 -1
- package/test/bats/historian-prompt-submit.bats +1 -1
- package/test/bats/inspector-post-write-hook.bats +4 -4
- package/test/bats/librarian-author-key.bats +477 -0
- package/test/bats/librarian-cli.bats +16 -16
- package/test/bats/librarian-lesson-judge.bats +884 -0
- package/test/bats/librarian-lesson-promote.bats +552 -0
- package/test/bats/librarian-lesson-review.bats +900 -0
- package/test/bats/librarian-lesson-transform.bats +17 -3
- package/test/bats/librarian-session-end.bats +161 -0
- package/test/bats/librarian-session-start.bats +36 -2
- package/test/bats/lineage-config.bats +1 -1
- package/test/bats/lineage-redact.bats +5 -5
- package/test/bats/session-tracker.bats +4 -4
- package/test/bats/tribunal-aggregate.bats +354 -1
- package/test/bats/tribunal-gate.bats +631 -0
- package/test/bats/tribunal-judge-agents.bats +119 -0
- package/test/bats/tribunal-jury.bats +1 -1
- package/test/bats/turn-tracker.bats +1 -1
- package/test/bats/warden-sanitizer.bats +3 -3
- package/test/bats/worktree-tracker.bats +2 -2
- package/test/node/lesson-validate-agreement.test.mjs +35 -6
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates every emitted cartographer.* event against @onlooker-community/schema.
|
|
4
|
+
#
|
|
5
|
+
# Both cartographer payloads were off-contract from the day the plugin shipped:
|
|
6
|
+
# the published schema described a design that was never built, so every event
|
|
7
|
+
# failed validation and emit_safe swallowed the failure with `|| true`. Findings
|
|
8
|
+
# still landed on disk, so /cartographer looked healthy and nothing surfaced the
|
|
9
|
+
# loss. Nothing here drove a real payload through validation, which is why the
|
|
10
|
+
# two sides could drift that far apart (ecosystem-q4d).
|
|
11
|
+
#
|
|
12
|
+
# So these tests build their payloads with the same
|
|
13
|
+
# cartographer_issue_found_payload / cartographer_audit_complete_payload the
|
|
14
|
+
# audit calls — not a copy of them. A copy would be free to drift in exactly the
|
|
15
|
+
# way the schema did.
|
|
16
|
+
|
|
17
|
+
setup() {
|
|
18
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
19
|
+
setup_test_env
|
|
20
|
+
|
|
21
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/cartographer"
|
|
22
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
23
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
24
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
25
|
+
|
|
26
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
27
|
+
export CLAUDE_SESSION_ID="bats-session-$$"
|
|
28
|
+
|
|
29
|
+
# shellcheck disable=SC1091
|
|
30
|
+
source "${PLUGIN_ROOT}/scripts/lib/cartographer-events.sh"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Skip when the installed schema predates the corrected cartographer payloads.
|
|
34
|
+
# The old definitions required issue_type/file_path; asserting against them
|
|
35
|
+
# would be asserting the bug.
|
|
36
|
+
_require_cartographer_schema() {
|
|
37
|
+
if ! grep -q "finding_hash" \
|
|
38
|
+
"${REPO_ROOT}/node_modules/@onlooker-community/schema/schemas/payload/plugins-memory.json" 2>/dev/null; then
|
|
39
|
+
skip "installed @onlooker-community/schema predates the corrected cartographer payloads"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Skip when the installed schema predates cartographer.issue.resolved (2.15.0).
|
|
44
|
+
_require_resolution_schema() {
|
|
45
|
+
if ! grep -q "cartographer.issue.resolved" \
|
|
46
|
+
"${REPO_ROOT}/node_modules/@onlooker-community/schema/schemas/payload/plugins-memory.json" 2>/dev/null; then
|
|
47
|
+
skip "installed @onlooker-community/schema predates cartographer.issue.resolved"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_validate_latest_event() {
|
|
52
|
+
local last
|
|
53
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
54
|
+
[ -n "$last" ] || return 1
|
|
55
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
56
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Valid 26-char Crockford Base32 ULID (no I, L, O, or U).
|
|
60
|
+
AUDIT_ID="01J0000000000000000000AB34"
|
|
61
|
+
|
|
62
|
+
# A finding record in the shape the analysis phases produce, which is what
|
|
63
|
+
# run_emit reads out of ALL_FINDINGS.
|
|
64
|
+
_finding() {
|
|
65
|
+
local type="${1:-undocumented_entity}" severity="${2:-warning}"
|
|
66
|
+
local file_a="${3:-CLAUDE.md}" file_b="${4:-null}"
|
|
67
|
+
jq -cn --arg t "$type" --arg s "$severity" --arg a "$file_a" --argjson b "$file_b" \
|
|
68
|
+
'{type: $t, severity: $s, file_a: $a, file_b: $b,
|
|
69
|
+
description: "fixture finding", suggested_fix: "document it"}'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "cartographer.issue.found validates for a single-file finding" {
|
|
73
|
+
_require_cartographer_schema
|
|
74
|
+
cartographer_emit_event "cartographer.issue.found" \
|
|
75
|
+
"$(cartographer_issue_found_payload "$AUDIT_ID" "abc123" "$(_finding)")"
|
|
76
|
+
run _validate_latest_event
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "cartographer.issue.found validates for a two-file finding" {
|
|
81
|
+
_require_cartographer_schema
|
|
82
|
+
cartographer_emit_event "cartographer.issue.found" \
|
|
83
|
+
"$(cartographer_issue_found_payload "$AUDIT_ID" "abc123" \
|
|
84
|
+
"$(_finding contradiction error CLAUDE.md '"AGENTS.md"')")"
|
|
85
|
+
run _validate_latest_event
|
|
86
|
+
[ "$status" -eq 0 ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "every finding type the analysis phases produce validates" {
|
|
90
|
+
_require_cartographer_schema
|
|
91
|
+
local t
|
|
92
|
+
for t in contradiction stale_ref dead_rule scope_collision undocumented_entity; do
|
|
93
|
+
cartographer_emit_event "cartographer.issue.found" \
|
|
94
|
+
"$(cartographer_issue_found_payload "$AUDIT_ID" "hash-${t}" "$(_finding "$t")")" \
|
|
95
|
+
|| return 1
|
|
96
|
+
_validate_latest_event || return 1
|
|
97
|
+
done
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@test "cartographer.audit.complete validates" {
|
|
101
|
+
_require_cartographer_schema
|
|
102
|
+
cartographer_emit_event "cartographer.audit.complete" \
|
|
103
|
+
"$(cartographer_audit_complete_payload "$AUDIT_ID" "session_start_first_run" 1 3 8420)"
|
|
104
|
+
run _validate_latest_event
|
|
105
|
+
[ "$status" -eq 0 ]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@test "every trigger the hooks set validates" {
|
|
109
|
+
_require_cartographer_schema
|
|
110
|
+
local t
|
|
111
|
+
for t in session_start_first_run session_start_interval post_tool_use manual; do
|
|
112
|
+
cartographer_emit_event "cartographer.audit.complete" \
|
|
113
|
+
"$(cartographer_audit_complete_payload "$AUDIT_ID" "$t" 0 0 5)" || return 1
|
|
114
|
+
_validate_latest_event || return 1
|
|
115
|
+
done
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# The emitted payload is the contract downstream reads, so assert on its
|
|
119
|
+
# contents and not merely that it validated. finding_hash in particular is what
|
|
120
|
+
# makes at-least-once delivery deduplicable (the plugin's ADR-003).
|
|
121
|
+
@test "the emitted payload carries the fields consumers read" {
|
|
122
|
+
_require_cartographer_schema
|
|
123
|
+
cartographer_emit_event "cartographer.issue.found" \
|
|
124
|
+
"$(cartographer_issue_found_payload "$AUDIT_ID" "abc123" \
|
|
125
|
+
"$(_finding contradiction error CLAUDE.md '"AGENTS.md"')")"
|
|
126
|
+
|
|
127
|
+
grep '"event_type":"cartographer.issue.found"' "$ONLOOKER_EVENTS_LOG" \
|
|
128
|
+
| jq -e --arg a "$AUDIT_ID" '
|
|
129
|
+
.payload.audit_id == $a
|
|
130
|
+
and .payload.finding_hash == "abc123"
|
|
131
|
+
and .payload.finding_type == "contradiction"
|
|
132
|
+
and .payload.severity == "error"
|
|
133
|
+
and .payload.affected_files == ["CLAUDE.md", "AGENTS.md"]
|
|
134
|
+
' >/dev/null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@test "a single-file finding carries exactly one affected file" {
|
|
138
|
+
_require_cartographer_schema
|
|
139
|
+
cartographer_emit_event "cartographer.issue.found" \
|
|
140
|
+
"$(cartographer_issue_found_payload "$AUDIT_ID" "abc123" "$(_finding)")"
|
|
141
|
+
|
|
142
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" \
|
|
143
|
+
| jq -e '.payload.affected_files == ["CLAUDE.md"]' >/dev/null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@test "the retired pre-implementation vocabulary no longer validates" {
|
|
147
|
+
_require_cartographer_schema
|
|
148
|
+
# Guards the direction of the fix: if someone "restores" the old schema,
|
|
149
|
+
# this is the test that objects.
|
|
150
|
+
run cartographer_emit_event "cartographer.issue.found" \
|
|
151
|
+
'{"issue_type":"orphaned_plugin","file_path":"CLAUDE.md","severity":"warning"}'
|
|
152
|
+
[ "$status" -ne 0 ]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# A finding that reaches the emit phase without a usable .type is a bug in
|
|
156
|
+
# whichever analysis phase produced it. The builder used to paper over that with
|
|
157
|
+
# finding_type "unknown" — a value no schema admits — so the payload was
|
|
158
|
+
# rejected, emit_safe swallowed the rejection with `|| true`, and the finding
|
|
159
|
+
# landed on disk with nothing on the bus. Failing here instead puts the problem
|
|
160
|
+
# in audit.log where an operator can read it (ecosystem-ci0).
|
|
161
|
+
#
|
|
162
|
+
# Empty string is covered alongside null and absent because jq's `//` treats ""
|
|
163
|
+
# as present, so it slipped past the old fallback and produced a payload just as
|
|
164
|
+
# unvalidatable as "unknown", by a different route.
|
|
165
|
+
@test "a finding with no usable type is rejected rather than defaulted" {
|
|
166
|
+
local shape
|
|
167
|
+
for shape in '{}' '{"type":null}' '{"type":""}'; do
|
|
168
|
+
run cartographer_issue_found_payload "$AUDIT_ID" "abc123" \
|
|
169
|
+
"$(jq -cn --argjson s "$shape" \
|
|
170
|
+
'$s + {severity:"warning", file_a:"CLAUDE.md", description:"d"}')"
|
|
171
|
+
[ "$status" -ne 0 ] || return 1
|
|
172
|
+
done
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# End-to-end statement of the symptom. Note this one does NOT discriminate the
|
|
176
|
+
# builder fix — before it, the schema rejected "unknown" downstream and the bus
|
|
177
|
+
# stayed empty for that reason instead. What it guards is the schema side: it
|
|
178
|
+
# fails if anyone ever admits "unknown" into the finding_type enum, which was
|
|
179
|
+
# the tempting cheap fix ci0 rejected.
|
|
180
|
+
@test "a typeless finding puts nothing on the bus" {
|
|
181
|
+
_require_cartographer_schema
|
|
182
|
+
run cartographer_emit_event "cartographer.issue.found" \
|
|
183
|
+
"$(cartographer_issue_found_payload "$AUDIT_ID" "abc123" \
|
|
184
|
+
'{"severity":"warning","file_a":"CLAUDE.md","description":"d"}')"
|
|
185
|
+
[ "$status" -ne 0 ] || return 1
|
|
186
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@test "the builder names the rejected finding on stderr" {
|
|
190
|
+
# Returning non-zero is not enough on its own — emit_safe appends this
|
|
191
|
+
# stream to audit.log, so the message is what an operator actually reads.
|
|
192
|
+
run cartographer_issue_found_payload "$AUDIT_ID" "abc123" \
|
|
193
|
+
'{"severity":"warning","file_a":"CLAUDE.md","description":"d"}'
|
|
194
|
+
[ "$status" -ne 0 ] || return 1
|
|
195
|
+
[[ "$output" == *"abc123"* ]] || return 1
|
|
196
|
+
[[ "$output" == *"carries no type"* ]]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# A full audit retires findings it stopped observing, and that never reached the
|
|
200
|
+
# bus: a consumer reading only the log saw every finding ever opened and none
|
|
201
|
+
# ever closed (ecosystem-w2i).
|
|
202
|
+
@test "cartographer.issue.resolved validates" {
|
|
203
|
+
_require_resolution_schema
|
|
204
|
+
cartographer_emit_event "cartographer.issue.resolved" \
|
|
205
|
+
"$(cartographer_issue_resolved_payload "$AUDIT_ID" "abc123")"
|
|
206
|
+
run _validate_latest_event
|
|
207
|
+
[ "$status" -eq 0 ]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Symmetry with issue.found is the whole point: the same hash opens and closes a
|
|
211
|
+
# finding, so a consumer can hold open/closed state from the log alone.
|
|
212
|
+
@test "the resolved payload carries the hash a consumer closes on" {
|
|
213
|
+
_require_resolution_schema
|
|
214
|
+
cartographer_emit_event "cartographer.issue.resolved" \
|
|
215
|
+
"$(cartographer_issue_resolved_payload "$AUDIT_ID" "abc123")"
|
|
216
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" \
|
|
217
|
+
| jq -e --arg a "$AUDIT_ID" \
|
|
218
|
+
'.payload.audit_id == $a and .payload.finding_hash == "abc123"' >/dev/null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@test "the resolved payload builder rejects missing arguments" {
|
|
222
|
+
run cartographer_issue_resolved_payload "" "abc123"
|
|
223
|
+
[ "$status" -ne 0 ] || return 1
|
|
224
|
+
run cartographer_issue_resolved_payload "$AUDIT_ID" ""
|
|
225
|
+
[ "$status" -ne 0 ]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@test "audit.complete carries resolved_finding_count when the sweep ran" {
|
|
229
|
+
_require_resolution_schema
|
|
230
|
+
cartographer_emit_event "cartographer.audit.complete" \
|
|
231
|
+
"$(cartographer_audit_complete_payload "$AUDIT_ID" "manual" 0 2 500 3)"
|
|
232
|
+
run _validate_latest_event
|
|
233
|
+
[ "$status" -eq 0 ] || return 1
|
|
234
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" \
|
|
235
|
+
| jq -e '.payload.resolved_finding_count == 3' >/dev/null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# A targeted or partial run skips the sweep entirely, so it must report no count
|
|
239
|
+
# rather than a zero that reads as "swept, found nothing to retire".
|
|
240
|
+
@test "audit.complete omits resolved_finding_count when the sweep was skipped" {
|
|
241
|
+
_require_resolution_schema
|
|
242
|
+
cartographer_emit_event "cartographer.audit.complete" \
|
|
243
|
+
"$(cartographer_audit_complete_payload "$AUDIT_ID" "post_tool_use" 1 1 20)"
|
|
244
|
+
run _validate_latest_event
|
|
245
|
+
[ "$status" -eq 0 ] || return 1
|
|
246
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" \
|
|
247
|
+
| jq -e '.payload | has("resolved_finding_count") | not' >/dev/null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@test "payload builders reject missing arguments" {
|
|
251
|
+
run cartographer_issue_found_payload "" "abc123" "$(_finding)"
|
|
252
|
+
[ "$status" -ne 0 ] || return 1
|
|
253
|
+
run cartographer_issue_found_payload "$AUDIT_ID" "" "$(_finding)"
|
|
254
|
+
[ "$status" -ne 0 ] || return 1
|
|
255
|
+
run cartographer_audit_complete_payload ""
|
|
256
|
+
[ "$status" -ne 0 ]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@test "emission fails on unknown event type" {
|
|
260
|
+
run cartographer_emit_event "cartographer.no.such.event" '{"audit_id":"x"}'
|
|
261
|
+
[ "$status" -ne 0 ]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@test "cartographer_emit_event returns 1 when payload is empty" {
|
|
265
|
+
run cartographer_emit_event "cartographer.issue.found" ""
|
|
266
|
+
[ "$status" -ne 0 ]
|
|
267
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Unit coverage for the --type and --scope narrowing.
|
|
4
|
+
#
|
|
5
|
+
# Both flags were documented in SKILL.md while run-audit.sh read neither, so a
|
|
6
|
+
# user who passed them got a silent full audit (ecosystem-9og). The end-to-end
|
|
7
|
+
# proof that they now change what runs lives in cartographer-run-audit.bats;
|
|
8
|
+
# this file pins the decision logic those tests depend on.
|
|
9
|
+
|
|
10
|
+
setup() {
|
|
11
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
12
|
+
setup_test_env
|
|
13
|
+
|
|
14
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/cartographer"
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
source "${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# ── Type validity ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
@test "every documented finding type is accepted" {
|
|
22
|
+
local t
|
|
23
|
+
for t in contradiction dead_rule stale_ref scope_collision undocumented_entity; do
|
|
24
|
+
cartographer_filter_valid_type "$t" || return 1
|
|
25
|
+
done
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# The published schema's finding_type enum and this list are the same five
|
|
29
|
+
# values; a type valid here that the schema rejects would emit nothing.
|
|
30
|
+
@test "the accepted types match the schema enum exactly" {
|
|
31
|
+
local schema="${REPO_ROOT}/node_modules/@onlooker-community/schema/schemas/payload/plugins-memory.json"
|
|
32
|
+
[ -f "$schema" ] || skip "schema package not installed"
|
|
33
|
+
local from_schema from_lib
|
|
34
|
+
from_schema=$(jq -r '.["$defs"]["cartographer.issue.found"].properties.finding_type.enum //
|
|
35
|
+
.definitions["cartographer.issue.found"].properties.finding_type.enum
|
|
36
|
+
| sort | join(" ")' "$schema")
|
|
37
|
+
from_lib=$(printf '%s\n' $CARTOGRAPHER_FINDING_TYPES | sort | tr '\n' ' ' | sed 's/ $//')
|
|
38
|
+
[ "$from_schema" = "$from_lib" ]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@test "an unknown type is rejected" {
|
|
42
|
+
run cartographer_filter_valid_type "not_a_type"
|
|
43
|
+
[ "$status" -ne 0 ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "an empty type is rejected, so callers must check before filtering" {
|
|
47
|
+
run cartographer_filter_valid_type ""
|
|
48
|
+
[ "$status" -ne 0 ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# ── Which analyzers run ──────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
@test "no filter runs every analyzer" {
|
|
54
|
+
local a
|
|
55
|
+
for a in contradiction stale_ref scope_collision undocumented_entity; do
|
|
56
|
+
cartographer_filter_wants "$a" "" || return 1
|
|
57
|
+
done
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@test "a type filter runs only its own analyzer" {
|
|
61
|
+
cartographer_filter_wants "stale_ref" "stale_ref" || return 1
|
|
62
|
+
run cartographer_filter_wants "scope_collision" "stale_ref"
|
|
63
|
+
[ "$status" -ne 0 ] || return 1
|
|
64
|
+
run cartographer_filter_wants "undocumented_entity" "stale_ref"
|
|
65
|
+
[ "$status" -ne 0 ] || return 1
|
|
66
|
+
run cartographer_filter_wants "contradiction" "stale_ref"
|
|
67
|
+
[ "$status" -ne 0 ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# contradiction and dead_rule come out of one LLM pass, so either request must
|
|
71
|
+
# run that analyzer. Skipping it for dead_rule would silently produce nothing.
|
|
72
|
+
@test "dead_rule runs the contradiction analyzer" {
|
|
73
|
+
cartographer_filter_wants "contradiction" "dead_rule"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@test "asking for dead_rule still skips the unrelated analyzers" {
|
|
77
|
+
run cartographer_filter_wants "stale_ref" "dead_rule"
|
|
78
|
+
[ "$status" -ne 0 ] || return 1
|
|
79
|
+
run cartographer_filter_wants "undocumented_entity" "dead_rule"
|
|
80
|
+
[ "$status" -ne 0 ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# ── Filtering the results ────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
FINDINGS='[{"type":"contradiction","file_a":"a"},{"type":"dead_rule","file_a":"b"}]'
|
|
86
|
+
|
|
87
|
+
@test "no filter passes findings through untouched" {
|
|
88
|
+
run cartographer_filter_findings "$FINDINGS" ""
|
|
89
|
+
[ "$output" = "$FINDINGS" ]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# The shared LLM pass returns both types, so the unrequested one is dropped here.
|
|
93
|
+
@test "the co-emitted type is dropped from the results" {
|
|
94
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
95
|
+
cartographer_filter_findings '$FINDINGS' 'dead_rule' | jq -c '[.[].type]'"
|
|
96
|
+
[ "$output" = '["dead_rule"]' ]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "filtering to the other co-emitted type keeps only it" {
|
|
100
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
101
|
+
cartographer_filter_findings '$FINDINGS' 'contradiction' | jq -c '[.[].type]'"
|
|
102
|
+
[ "$output" = '["contradiction"]' ]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@test "a type present in no finding yields an empty array, not an error" {
|
|
106
|
+
run cartographer_filter_findings "$FINDINGS" "stale_ref"
|
|
107
|
+
[ "$status" -eq 0 ] || return 1
|
|
108
|
+
[ "$output" = "[]" ]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@test "malformed findings json degrades to empty rather than aborting" {
|
|
112
|
+
run cartographer_filter_findings "not json" "stale_ref"
|
|
113
|
+
[ "$status" -eq 0 ] || return 1
|
|
114
|
+
[ "$output" = "[]" ]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# ── Scope ────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
FILES='["/repo/CLAUDE.md","/repo/plugins/tribunal/CLAUDE.md","/repo/plugins/echo/CLAUDE.md"]'
|
|
120
|
+
|
|
121
|
+
@test "no scope passes the file list through untouched" {
|
|
122
|
+
run cartographer_filter_scope "$FILES" "/repo" ""
|
|
123
|
+
[ "$output" = "$FILES" ]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@test "a repo-relative scope keeps only files beneath it" {
|
|
127
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
128
|
+
cartographer_filter_scope '$FILES' '/repo' 'plugins/tribunal' | jq -c ."
|
|
129
|
+
[ "$output" = '["/repo/plugins/tribunal/CLAUDE.md"]' ]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@test "an absolute scope works too" {
|
|
133
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
134
|
+
cartographer_filter_scope '$FILES' '/repo' '/repo/plugins/echo' | jq -c ."
|
|
135
|
+
[ "$output" = '["/repo/plugins/echo/CLAUDE.md"]' ]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@test "a trailing slash on the scope does not change the match" {
|
|
139
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
140
|
+
cartographer_filter_scope '$FILES' '/repo' 'plugins/tribunal/' | jq -c 'length'"
|
|
141
|
+
[ "$output" = "1" ]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test "a leading ./ on the scope does not change the match" {
|
|
145
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
146
|
+
cartographer_filter_scope '$FILES' '/repo' './plugins/tribunal' | jq -c 'length'"
|
|
147
|
+
[ "$output" = "1" ]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# A sibling whose name merely starts with the scope string is not inside it.
|
|
151
|
+
# Prefix matching without the separator would pull plugins/echo-legacy into a
|
|
152
|
+
# scope of plugins/echo.
|
|
153
|
+
@test "a sibling sharing a name prefix is not swept in" {
|
|
154
|
+
local files='["/repo/plugins/echo/CLAUDE.md","/repo/plugins/echo-legacy/CLAUDE.md"]'
|
|
155
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
156
|
+
cartographer_filter_scope '$files' '/repo' 'plugins/echo' | jq -c ."
|
|
157
|
+
[ "$output" = '["/repo/plugins/echo/CLAUDE.md"]' ]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@test "scoping to an exact file path keeps that file" {
|
|
161
|
+
run bash -c "source '${PLUGIN_ROOT}/scripts/lib/cartographer-filter.sh'
|
|
162
|
+
cartographer_filter_scope '$FILES' '/repo' 'CLAUDE.md' | jq -c ."
|
|
163
|
+
[ "$output" = '["/repo/CLAUDE.md"]' ]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@test "a scope matching nothing yields an empty list" {
|
|
167
|
+
run cartographer_filter_scope "$FILES" "/repo" "does/not/exist"
|
|
168
|
+
[ "$output" = "[]" ]
|
|
169
|
+
}
|
|
@@ -89,8 +89,8 @@ teardown() {
|
|
|
89
89
|
cartographer_lock_release '${BATS_TEST_TMPDIR}/x.lock' && echo RELEASE_OK
|
|
90
90
|
"
|
|
91
91
|
[ "$status" -eq 0 ]
|
|
92
|
-
[[ "$output" == *"SOURCED_OK"* ]]
|
|
93
|
-
[[ "$output" == *"ACQUIRE_FAILED"* ]]
|
|
94
|
-
[[ "$output" == *"RELEASE_OK"* ]]
|
|
92
|
+
[[ "$output" == *"SOURCED_OK"* ]] || return 1
|
|
93
|
+
[[ "$output" == *"ACQUIRE_FAILED"* ]] || return 1
|
|
94
|
+
[[ "$output" == *"RELEASE_OK"* ]] || return 1
|
|
95
95
|
[[ "$output" == *"locking disabled"* ]]
|
|
96
96
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
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/cartographer"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/cartographer-omission.sh"
|
|
11
|
+
|
|
12
|
+
FIXTURE_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
13
|
+
mkdir -p "${FIXTURE_REPO}/plugins/alpha" \
|
|
14
|
+
"${FIXTURE_REPO}/plugins/beta" \
|
|
15
|
+
"${FIXTURE_REPO}/skills/solo"
|
|
16
|
+
DOC="${FIXTURE_REPO}/CLAUDE.md"
|
|
17
|
+
printf '# Doc\nThe alpha plugin does things.\n' > "$DOC"
|
|
18
|
+
CORPUS=$(jq -n --arg f "$DOC" '[$f]')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@test "flags an entity whose name appears nowhere in the corpus" {
|
|
22
|
+
local out
|
|
23
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
24
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
25
|
+
[[ "$(printf '%s' "$out" | jq -r 'length')" == "1" ]] || return 1
|
|
26
|
+
[[ "$(printf '%s' "$out" | jq -r '.[0].excerpt_a')" == "beta" ]] || return 1
|
|
27
|
+
[ "$(printf '%s' "$out" | jq -r '.[0].type')" = "undocumented_entity" ]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@test "does not flag an entity the corpus mentions" {
|
|
31
|
+
local out
|
|
32
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
33
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
34
|
+
[ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "alpha")] | length')" = "0" ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "finding carries the entity as file_a and a null file_b" {
|
|
38
|
+
local out
|
|
39
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
40
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
41
|
+
[[ "$(printf '%s' "$out" | jq -r '.[0].file_a')" == "${FIXTURE_REPO}/plugins/beta" ]] || return 1
|
|
42
|
+
[[ "$(printf '%s' "$out" | jq -r '.[0].file_b')" == "null" ]] || return 1
|
|
43
|
+
[ "$(printf '%s' "$out" | jq -r '.[0].severity')" = "warning" ]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "word boundary: a longer word containing the name does not count as a mention" {
|
|
47
|
+
printf '# Doc\nWe do a lot of counseling here.\n' > "$DOC"
|
|
48
|
+
mkdir -p "${FIXTURE_REPO}/plugins/counsel"
|
|
49
|
+
local out
|
|
50
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
51
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
52
|
+
[ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "counsel")] | length')" = "1" ]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test "word boundary: a hyphenated name is not matched inside a longer hyphenated token" {
|
|
56
|
+
printf '# Doc\nSee my-list-prompt-rules-thing for details.\n' > "$DOC"
|
|
57
|
+
mkdir -p "${FIXTURE_REPO}/skills/list-prompt-rules"
|
|
58
|
+
local out
|
|
59
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
60
|
+
"$CORPUS" "$FIXTURE_REPO" '["skills/*/"]' '[]' 20)
|
|
61
|
+
[ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "list-prompt-rules")] | length')" = "1" ]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@test "word boundary: a name bounded by slashes counts as a mention" {
|
|
65
|
+
printf '# Doc\nSee plugins/beta/ for details.\n' > "$DOC"
|
|
66
|
+
local out
|
|
67
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
68
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
69
|
+
[ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "beta")] | length')" = "0" ]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "exclude filters a matched path by substring" {
|
|
73
|
+
local out
|
|
74
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
75
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/","skills/*/"]' '["skills/"]' 20)
|
|
76
|
+
[[ "$(printf '%s' "$out" | jq -r 'length')" == "1" ]] || return 1
|
|
77
|
+
[ "$(printf '%s' "$out" | jq -r '.[0].excerpt_a')" = "beta" ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "max_findings caps the result and reports the drop count on stderr" {
|
|
81
|
+
local out err
|
|
82
|
+
err="${BATS_TEST_TMPDIR}/err.txt"
|
|
83
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
84
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/","skills/*/"]' '[]' 1 2>"$err")
|
|
85
|
+
[[ "$(printf '%s' "$out" | jq -r 'length')" == "1" ]] || return 1
|
|
86
|
+
grep -q "1 candidate" "$err"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "a glob matching nothing yields an empty array" {
|
|
90
|
+
local out
|
|
91
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
92
|
+
"$CORPUS" "$FIXTURE_REPO" '["nonexistent/*/"]' '[]' 20)
|
|
93
|
+
[ "$out" = "[]" ]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@test "an empty corpus yields an empty array rather than flagging everything" {
|
|
97
|
+
local out
|
|
98
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
99
|
+
'[]' "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
100
|
+
[ "$out" = "[]" ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "the same entity produces an identical finding hash across two runs" {
|
|
104
|
+
# shellcheck disable=SC1091
|
|
105
|
+
source "${PLUGIN_ROOT}/scripts/lib/cartographer-analyze.sh"
|
|
106
|
+
local a b h1 h2
|
|
107
|
+
a=$(cartographer_analyze_undocumented_entity \
|
|
108
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
109
|
+
b=$(cartographer_analyze_undocumented_entity \
|
|
110
|
+
"$CORPUS" "$FIXTURE_REPO" '["plugins/*/"]' '[]' 20)
|
|
111
|
+
h1=$(cartographer_finding_hash "undocumented_entity" \
|
|
112
|
+
"$(printf '%s' "$a" | jq -r '.[0].file_a')" \
|
|
113
|
+
"$(printf '%s' "$a" | jq -r '.[0].excerpt_a')" "" "")
|
|
114
|
+
h2=$(cartographer_finding_hash "undocumented_entity" \
|
|
115
|
+
"$(printf '%s' "$b" | jq -r '.[0].file_a')" \
|
|
116
|
+
"$(printf '%s' "$b" | jq -r '.[0].excerpt_a')" "" "")
|
|
117
|
+
[[ -n "$h1" ]] || return 1
|
|
118
|
+
[ "$h1" = "$h2" ]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@test "flags an entity under a repo root whose path contains a space" {
|
|
122
|
+
# BATS_TEST_TMPDIR itself never contains a space, which is exactly why this
|
|
123
|
+
# was missed — build a fixture one level under it that does.
|
|
124
|
+
local spaced_repo="${BATS_TEST_TMPDIR}/my repo"
|
|
125
|
+
mkdir -p "${spaced_repo}/plugins/alpha" "${spaced_repo}/plugins/beta"
|
|
126
|
+
local doc="${spaced_repo}/CLAUDE.md"
|
|
127
|
+
printf '# Doc\nThe alpha plugin does things.\n' > "$doc"
|
|
128
|
+
local corpus
|
|
129
|
+
corpus=$(jq -n --arg f "$doc" '[$f]')
|
|
130
|
+
|
|
131
|
+
local out
|
|
132
|
+
out=$(cartographer_analyze_undocumented_entity \
|
|
133
|
+
"$corpus" "$spaced_repo" '["plugins/*/"]' '[]' 20)
|
|
134
|
+
[ "$(printf '%s' "$out" | jq -r '[.[] | select(.excerpt_a == "beta")] | length')" = "1" ]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Stubs `claude` so the three LLM phases make no real call, then runs a full
|
|
138
|
+
# audit. Any extra env the caller needs is exported before calling this.
|
|
139
|
+
_run_audit() {
|
|
140
|
+
local stub="${BATS_TEST_TMPDIR}/bin"
|
|
141
|
+
mkdir -p "$stub"
|
|
142
|
+
printf '#!/usr/bin/env bash\ncat >/dev/null\nprintf "[]"\n' > "${stub}/claude"
|
|
143
|
+
chmod +x "${stub}/claude"
|
|
144
|
+
|
|
145
|
+
PATH="${stub}:${PATH}" \
|
|
146
|
+
CARTOGRAPHER_DIR="${BATS_TEST_TMPDIR}/state" \
|
|
147
|
+
CARTOGRAPHER_REPO_ROOT="$FIXTURE_REPO" \
|
|
148
|
+
CARTOGRAPHER_TRIGGER="manual" \
|
|
149
|
+
CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT" \
|
|
150
|
+
bash "${PLUGIN_ROOT}/scripts/run-audit.sh"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# run_emit writes findings with a bare `jq`, which pretty-prints — the file
|
|
154
|
+
# contains `"type": "undocumented_entity"` with a space. Parse rather than grep.
|
|
155
|
+
_findings_of_type() {
|
|
156
|
+
local dir="${BATS_TEST_TMPDIR}/state/findings"
|
|
157
|
+
local count=0 f
|
|
158
|
+
[[ -d "$dir" ]] || { printf '0'; return 0; }
|
|
159
|
+
for f in "$dir"/*.json; do
|
|
160
|
+
[[ -e "$f" ]] || continue
|
|
161
|
+
if [[ "$(jq -r '.type // ""' "$f" 2>/dev/null)" == "undocumented_entity" ]]; then
|
|
162
|
+
count=$(( count + 1 ))
|
|
163
|
+
fi
|
|
164
|
+
done
|
|
165
|
+
printf '%s' "$count"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Names of every undocumented_entity finding on disk, sorted, space-separated.
|
|
169
|
+
_findings_named() {
|
|
170
|
+
local dir="${BATS_TEST_TMPDIR}/state/findings"
|
|
171
|
+
local names=() f
|
|
172
|
+
[[ -d "$dir" ]] || return 0
|
|
173
|
+
for f in "$dir"/*.json; do
|
|
174
|
+
[[ -e "$f" ]] || continue
|
|
175
|
+
if [[ "$(jq -r '.type // ""' "$f" 2>/dev/null)" == "undocumented_entity" ]]; then
|
|
176
|
+
names+=("$(jq -r '.excerpt_a' "$f")")
|
|
177
|
+
fi
|
|
178
|
+
done
|
|
179
|
+
printf '%s' "$(printf '%s\n' "${names[@]+"${names[@]}"}" | sort | tr '\n' ' ' | sed 's/ $//')"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@test "integration: a full audit records the undocumented entities on disk" {
|
|
183
|
+
# alpha is documented; beta and solo are not, and the default globs cover
|
|
184
|
+
# both plugins/ and skills/. Assert identity, not just count — a bare count
|
|
185
|
+
# passes just as happily on two findings for the same entity.
|
|
186
|
+
_run_audit
|
|
187
|
+
[ "$(_findings_named)" = "beta solo" ]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@test "integration: a targeted post-write audit records no undocumented entity" {
|
|
191
|
+
export CARTOGRAPHER_TARGET_FILE="$DOC"
|
|
192
|
+
_run_audit
|
|
193
|
+
[ "$(_findings_of_type)" = "0" ]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@test "integration: enabled=false suppresses the phase" {
|
|
197
|
+
mkdir -p "${FIXTURE_REPO}/.claude"
|
|
198
|
+
printf '%s\n' '{"cartographer":{"undocumented_entity":{"enabled":false}}}' \
|
|
199
|
+
> "${FIXTURE_REPO}/.claude/settings.json"
|
|
200
|
+
_run_audit
|
|
201
|
+
[ "$(_findings_of_type)" = "0" ]
|
|
202
|
+
}
|