@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
|
@@ -267,3 +267,101 @@ write_project_rules() {
|
|
|
267
267
|
echo "$out" | grep -A2 "id: r1" | grep -q "fired: yes"
|
|
268
268
|
echo "$out" | grep -A2 "id: r2" | grep -q "fired: no"
|
|
269
269
|
}
|
|
270
|
+
|
|
271
|
+
@test "project_path: appends .claude/prompt-rules.json to the given cwd" {
|
|
272
|
+
local path
|
|
273
|
+
path=$(prompt_rules_project_path "/some/where")
|
|
274
|
+
[ "$path" = "/some/where/.claude/prompt-rules.json" ]
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@test "project_path: defaults to \$PWD when no cwd is given" {
|
|
278
|
+
local path expected
|
|
279
|
+
path=$(prompt_rules_project_path)
|
|
280
|
+
expected="$PWD/.claude/prompt-rules.json"
|
|
281
|
+
[ "$path" = "$expected" ]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@test "fired_path: builds path under the sessions dir from the session id" {
|
|
285
|
+
local path expected
|
|
286
|
+
path=$(prompt_rules_fired_path "sess-XYZ")
|
|
287
|
+
expected="$ONLOOKER_PROMPT_RULES_SESSIONS_DIR/sess-XYZ.json"
|
|
288
|
+
[ "$path" = "$expected" ]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@test "fired_path: defaults session id to 'unknown' when none is given" {
|
|
292
|
+
local path expected
|
|
293
|
+
path=$(prompt_rules_fired_path)
|
|
294
|
+
expected="$ONLOOKER_PROMPT_RULES_SESSIONS_DIR/unknown.json"
|
|
295
|
+
[ "$path" = "$expected" ]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@test "emit: appends a JSON event line with type, session, payload, and plugin" {
|
|
299
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
300
|
+
run prompt_rules_emit "sess-emit" "prompt_rule.matched" '{"rule_id":"rule-1"}'
|
|
301
|
+
[ "$status" -eq 0 ]
|
|
302
|
+
|
|
303
|
+
local line
|
|
304
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
305
|
+
[ "$(echo "$line" | jq -r '.event_type')" = "prompt_rule.matched" ]
|
|
306
|
+
[ "$(echo "$line" | jq -r '.session_id')" = "sess-emit" ]
|
|
307
|
+
[ "$(echo "$line" | jq -r '.payload.rule_id')" = "rule-1" ]
|
|
308
|
+
# No ONLOOKER_PLUGIN_NAME exported in this test → defaults to "onlooker".
|
|
309
|
+
[ "$(echo "$line" | jq -r '.plugin')" = "onlooker" ]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@test "emit: honors ONLOOKER_PLUGIN_NAME for the plugin field" {
|
|
313
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
314
|
+
ONLOOKER_PLUGIN_NAME="prompt-rules" prompt_rules_emit "sess-plugin" "prompt_rule.applied" '{"rule_id":"r9"}'
|
|
315
|
+
|
|
316
|
+
local line
|
|
317
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
318
|
+
[ "$(echo "$line" | jq -r '.plugin')" = "prompt-rules" ]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@test "emit: defaults payload to an empty object when none is given" {
|
|
322
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
323
|
+
run prompt_rules_emit "sess-nopayload" "prompt_rule.matched"
|
|
324
|
+
[ "$status" -eq 0 ]
|
|
325
|
+
|
|
326
|
+
local line
|
|
327
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
328
|
+
[ "$(echo "$line" | jq -c '.payload')" = "{}" ]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@test "emit: defaults session id to 'unknown' when none is given" {
|
|
332
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
333
|
+
prompt_rules_emit "" "prompt_rule.matched" '{}'
|
|
334
|
+
|
|
335
|
+
local line
|
|
336
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
337
|
+
[ "$(echo "$line" | jq -r '.session_id')" = "unknown" ]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@test "emit: includes a numeric turn field when ONLOOKER_TURN_NUMBER is exported" {
|
|
341
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
342
|
+
ONLOOKER_TURN_NUMBER=7 prompt_rules_emit "sess-turn" "prompt_rule.matched" '{}'
|
|
343
|
+
|
|
344
|
+
local line
|
|
345
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
346
|
+
[ "$(echo "$line" | jq -r '.turn')" = "7" ]
|
|
347
|
+
[ "$(echo "$line" | jq -r '.turn | type')" = "number" ]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@test "emit: omits the turn field when ONLOOKER_TURN_NUMBER is unset" {
|
|
351
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
352
|
+
# Guard against any ambient value leaking in from the environment.
|
|
353
|
+
unset ONLOOKER_TURN_NUMBER
|
|
354
|
+
prompt_rules_emit "sess-noturn" "prompt_rule.matched" '{}'
|
|
355
|
+
|
|
356
|
+
local line
|
|
357
|
+
line=$(tail -1 "$ONLOOKER_EVENTS_LOG")
|
|
358
|
+
[ "$(echo "$line" | jq -e 'has("turn")')" = "false" ] || \
|
|
359
|
+
[ "$(echo "$line" | jq 'has("turn")')" = "false" ]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
@test "emit: returns 1 and writes nothing when event_type is empty" {
|
|
363
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
364
|
+
run prompt_rules_emit "sess-empty" ""
|
|
365
|
+
[ "$status" -eq 1 ]
|
|
366
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
367
|
+
}
|
|
@@ -73,3 +73,263 @@ setup() {
|
|
|
73
73
|
@test "session_tracker_map_end_reason maps logout to user_exit" {
|
|
74
74
|
[ "$(session_tracker_map_end_reason logout)" = "user_exit" ]
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
# Seed a tracker file's start_time_ms to a deterministic point in the past.
|
|
78
|
+
# Usage: seed_start_ms_ago "$session_id" 5000
|
|
79
|
+
seed_start_ms_ago() {
|
|
80
|
+
local session_id="$1"
|
|
81
|
+
local ago_ms="$2"
|
|
82
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
83
|
+
turn_state_ensure_session "$session_id"
|
|
84
|
+
local past
|
|
85
|
+
past=$(( $(session_tracker_now_ms) - ago_ms ))
|
|
86
|
+
jq --argjson start_ms "$past" '.start_time_ms = $start_ms' "$tracker" >"${tracker}.tmp"
|
|
87
|
+
mv "${tracker}.tmp" "$tracker"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@test "session_tracker_now_ms returns 13-digit epoch milliseconds" {
|
|
91
|
+
local ms
|
|
92
|
+
ms=$(session_tracker_now_ms)
|
|
93
|
+
[[ "$ms" =~ ^[0-9]+$ ]]
|
|
94
|
+
[ "${#ms}" -eq 13 ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "session_tracker_now_ms is monotonic-ish across two calls" {
|
|
98
|
+
local first second
|
|
99
|
+
first=$(session_tracker_now_ms)
|
|
100
|
+
second=$(session_tracker_now_ms)
|
|
101
|
+
[ "$second" -ge "$first" ]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@test "session_tracker_git_context returns nothing for empty cwd" {
|
|
105
|
+
run session_tracker_git_context ""
|
|
106
|
+
[ "$status" -eq 0 ]
|
|
107
|
+
[ -z "$output" ]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@test "session_tracker_git_context returns two empty lines for non-git dir" {
|
|
111
|
+
local non_git="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
112
|
+
mkdir -p "$non_git"
|
|
113
|
+
run session_tracker_git_context "$non_git"
|
|
114
|
+
[ "$status" -eq 0 ]
|
|
115
|
+
# Two empty fields: branch line + commit line, both empty.
|
|
116
|
+
[ -z "$output" ]
|
|
117
|
+
local branch commit
|
|
118
|
+
branch=$(session_tracker_git_context "$non_git" | sed -n '1p')
|
|
119
|
+
commit=$(session_tracker_git_context "$non_git" | sed -n '2p')
|
|
120
|
+
[ -z "$branch" ]
|
|
121
|
+
[ -z "$commit" ]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@test "session_tracker_git_context returns branch and short commit for a repo" {
|
|
125
|
+
local repo="${BATS_TEST_TMPDIR}/gitrepo"
|
|
126
|
+
mkdir -p "$repo"
|
|
127
|
+
git -C "$repo" init -q -b trunk
|
|
128
|
+
git -C "$repo" config user.email "test@example.com"
|
|
129
|
+
git -C "$repo" config user.name "Test"
|
|
130
|
+
touch "$repo/file.txt"
|
|
131
|
+
git -C "$repo" add file.txt
|
|
132
|
+
git -C "$repo" commit -q -m "initial"
|
|
133
|
+
|
|
134
|
+
local out branch commit
|
|
135
|
+
out=$(session_tracker_git_context "$repo")
|
|
136
|
+
branch=$(echo "$out" | sed -n '1p')
|
|
137
|
+
commit=$(echo "$out" | sed -n '2p')
|
|
138
|
+
[ "$branch" = "trunk" ]
|
|
139
|
+
[[ "$commit" =~ ^[0-9a-f]{7}$ ]]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@test "session_tracker_record_start writes start metadata to tracker" {
|
|
143
|
+
local sid="rec-start-001"
|
|
144
|
+
local input='{"cwd":"/p","source":"startup","model":"m","transcript_path":"/t","agent_type":"a"}'
|
|
145
|
+
session_tracker_record_start "$sid" "$input"
|
|
146
|
+
|
|
147
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
148
|
+
[ -f "$tracker" ]
|
|
149
|
+
jq -e '.cwd == "/p"
|
|
150
|
+
and .start_source == "startup"
|
|
151
|
+
and .model == "m"
|
|
152
|
+
and .transcript_path == "/t"
|
|
153
|
+
and .agent_type == "a"
|
|
154
|
+
and (.start_time_ms | type) == "number"
|
|
155
|
+
and .start_time_ms > 0' "$tracker" >/dev/null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@test "session_tracker_record_start no-ops on empty input" {
|
|
159
|
+
local sid="rec-start-empty"
|
|
160
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
161
|
+
rm -f "$tracker"
|
|
162
|
+
run session_tracker_record_start "$sid" ""
|
|
163
|
+
[ "$status" -eq 0 ]
|
|
164
|
+
[ ! -f "$tracker" ]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@test "session_tracker_record_start no-ops on null session_id" {
|
|
168
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/null"
|
|
169
|
+
rm -f "$tracker"
|
|
170
|
+
run session_tracker_record_start "null" '{"cwd":"/p"}'
|
|
171
|
+
[ "$status" -eq 0 ]
|
|
172
|
+
[ ! -f "$tracker" ]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@test "session_tracker_build_start_payload sets working_directory and git fields in repo" {
|
|
176
|
+
local repo="${BATS_TEST_TMPDIR}/payload-repo"
|
|
177
|
+
mkdir -p "$repo"
|
|
178
|
+
git -C "$repo" init -q -b main
|
|
179
|
+
git -C "$repo" config user.email "test@example.com"
|
|
180
|
+
git -C "$repo" config user.name "Test"
|
|
181
|
+
touch "$repo/f"
|
|
182
|
+
git -C "$repo" add f
|
|
183
|
+
git -C "$repo" commit -q -m "init"
|
|
184
|
+
|
|
185
|
+
local payload
|
|
186
|
+
payload=$(session_tracker_build_start_payload "{\"cwd\":\"$repo\"}")
|
|
187
|
+
echo "$payload" | jq -e --arg wd "$repo" '.working_directory == $wd
|
|
188
|
+
and .git_branch == "main"
|
|
189
|
+
and (.git_commit | test("^[0-9a-f]{7}$"))' >/dev/null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@test "session_tracker_build_start_payload falls back to pwd and omits git fields outside repo" {
|
|
193
|
+
local non_git="${BATS_TEST_TMPDIR}/payload-nogit"
|
|
194
|
+
mkdir -p "$non_git"
|
|
195
|
+
local payload
|
|
196
|
+
payload=$(cd "$non_git" && session_tracker_build_start_payload '{}')
|
|
197
|
+
echo "$payload" | jq -e --arg wd "$non_git" '.working_directory == $wd
|
|
198
|
+
and (has("git_branch") | not)
|
|
199
|
+
and (has("git_commit") | not)' >/dev/null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@test "session_tracker_build_end_payload computes duration, turn_count, and end_reason" {
|
|
203
|
+
local sid="end-payload-001"
|
|
204
|
+
seed_start_ms_ago "$sid" 5000
|
|
205
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
206
|
+
jq '.turn_number = 2' "$tracker" >"${tracker}.tmp"
|
|
207
|
+
mv "${tracker}.tmp" "$tracker"
|
|
208
|
+
|
|
209
|
+
local payload
|
|
210
|
+
payload=$(session_tracker_build_end_payload "$sid" '{"reason":"logout"}')
|
|
211
|
+
echo "$payload" | jq -e '.duration_ms >= 5000
|
|
212
|
+
and .turn_count == 2
|
|
213
|
+
and .end_reason == "user_exit"' >/dev/null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@test "session_tracker_build_end_payload defaults when no tracker exists" {
|
|
217
|
+
local sid="end-payload-missing"
|
|
218
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
219
|
+
local payload
|
|
220
|
+
payload=$(session_tracker_build_end_payload "$sid" '{"reason":"logout"}')
|
|
221
|
+
echo "$payload" | jq -e '.duration_ms == 0 and .turn_count == 1' >/dev/null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@test "session_tracker_build_end_payload returns 1 for null session_id" {
|
|
225
|
+
run session_tracker_build_end_payload "null" '{"reason":"logout"}'
|
|
226
|
+
[ "$status" -eq 1 ]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@test "session_tracker_duration_ms returns elapsed ms for seeded start" {
|
|
230
|
+
local sid="dur-001"
|
|
231
|
+
seed_start_ms_ago "$sid" 3000
|
|
232
|
+
local dur
|
|
233
|
+
dur=$(session_tracker_duration_ms "$sid")
|
|
234
|
+
[[ "$dur" =~ ^[0-9]+$ ]]
|
|
235
|
+
[ "$dur" -ge 3000 ]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@test "session_tracker_duration_ms returns 0 for null session_id" {
|
|
239
|
+
[ "$(session_tracker_duration_ms null)" = "0" ]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@test "session_tracker_duration_ms returns 0 for missing tracker" {
|
|
243
|
+
local sid="dur-missing"
|
|
244
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
245
|
+
[ "$(session_tracker_duration_ms "$sid")" = "0" ]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@test "session_tracker_duration_ms returns 0 for invalid start_time_ms" {
|
|
249
|
+
local sid="dur-invalid"
|
|
250
|
+
turn_state_ensure_session "$sid"
|
|
251
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
252
|
+
jq '.start_time_ms = "not-a-number"' "$tracker" >"${tracker}.tmp"
|
|
253
|
+
mv "${tracker}.tmp" "$tracker"
|
|
254
|
+
[ "$(session_tracker_duration_ms "$sid")" = "0" ]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@test "session_tracker_update_duration persists session_duration_ms" {
|
|
258
|
+
local sid="upd-001"
|
|
259
|
+
seed_start_ms_ago "$sid" 2000
|
|
260
|
+
session_tracker_update_duration "$sid"
|
|
261
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
262
|
+
jq -e '.session_duration_ms >= 2000' "$tracker" >/dev/null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@test "session_tracker_update_duration returns 0 for null session_id" {
|
|
266
|
+
run session_tracker_update_duration "null"
|
|
267
|
+
[ "$status" -eq 0 ]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@test "session_tracker_update_duration creates tracker with zero duration for new sid" {
|
|
271
|
+
local sid="upd-new"
|
|
272
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
273
|
+
rm -f "$tracker"
|
|
274
|
+
session_tracker_update_duration "$sid"
|
|
275
|
+
[ -f "$tracker" ]
|
|
276
|
+
jq -e '.session_duration_ms == 0' "$tracker" >/dev/null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@test "session_tracker_build_duration_context renders turn and humanized elapsed" {
|
|
280
|
+
local sid="ctx-001"
|
|
281
|
+
seed_start_ms_ago "$sid" 154000
|
|
282
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
283
|
+
jq '.turn_number = 3' "$tracker" >"${tracker}.tmp"
|
|
284
|
+
mv "${tracker}.tmp" "$tracker"
|
|
285
|
+
|
|
286
|
+
local out
|
|
287
|
+
out=$(session_tracker_build_duration_context "$sid")
|
|
288
|
+
[[ "$out" == *"turn 3"* ]]
|
|
289
|
+
[[ "$out" == *"2m 34s"* ]]
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@test "session_tracker_build_duration_context defaults for missing tracker" {
|
|
293
|
+
local sid="ctx-missing"
|
|
294
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
295
|
+
local out
|
|
296
|
+
out=$(session_tracker_build_duration_context "$sid")
|
|
297
|
+
[[ "$out" == *"turn 1"* ]]
|
|
298
|
+
[[ "$out" == *"0s"* ]]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@test "session_tracker_emit appends to both session history and events log" {
|
|
302
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
303
|
+
local sid="emit-001"
|
|
304
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${sid}.jsonl"
|
|
305
|
+
rm -f "$history_file"
|
|
306
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
307
|
+
|
|
308
|
+
run session_tracker_emit "$sid" "session.start" '{"working_directory":"/tmp"}'
|
|
309
|
+
[ "$status" -eq 0 ]
|
|
310
|
+
|
|
311
|
+
[ -f "$history_file" ]
|
|
312
|
+
jq -e '.event_type == "session.start" and .session_id == "emit-001"' "$history_file" >/dev/null
|
|
313
|
+
grep -q '"event_type":"session.start"' "$ONLOOKER_EVENTS_LOG" \
|
|
314
|
+
|| jq -e 'select(.event_type == "session.start" and .session_id == "emit-001")' "$ONLOOKER_EVENTS_LOG" >/dev/null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@test "session_tracker_emit no-ops on empty session_id" {
|
|
318
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
319
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
320
|
+
run session_tracker_emit "" "session.start" '{"working_directory":"/tmp"}'
|
|
321
|
+
[ "$status" -eq 0 ]
|
|
322
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@test "session_tracker_emit no-ops on empty event_type" {
|
|
326
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
327
|
+
local sid="emit-noevent"
|
|
328
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${sid}.jsonl"
|
|
329
|
+
rm -f "$history_file"
|
|
330
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
331
|
+
run session_tracker_emit "$sid" "" '{"working_directory":"/tmp"}'
|
|
332
|
+
[ "$status" -eq 0 ]
|
|
333
|
+
[ ! -f "$history_file" ]
|
|
334
|
+
[ ! -s "$ONLOOKER_EVENTS_LOG" ]
|
|
335
|
+
}
|
|
@@ -74,3 +74,66 @@ setup() {
|
|
|
74
74
|
tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -e '.event_type == "skill.invoked"' >/dev/null
|
|
75
75
|
tail -n 1 "$ONLOOKER_EVENTS_LOG" | onlooker_validate_event
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
@test "skill_usage_append writes a built skill.invoked record to the session history file" {
|
|
79
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-expansion-skill.json"
|
|
80
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/skill-session-001.jsonl"
|
|
81
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
82
|
+
|
|
83
|
+
local record
|
|
84
|
+
record=$(skill_usage_build_record "$(cat "$fixture")")
|
|
85
|
+
|
|
86
|
+
run skill_usage_append "skill-session-001" "$record"
|
|
87
|
+
[ "$status" -eq 0 ]
|
|
88
|
+
|
|
89
|
+
[ -f "$history_file" ]
|
|
90
|
+
tail -n 1 "$history_file" | jq -e \
|
|
91
|
+
'.event_type == "skill.invoked"
|
|
92
|
+
and .payload.skill_name == "code-review"
|
|
93
|
+
and .payload.invocation_source == "slash_command"
|
|
94
|
+
and .session_id == "skill-session-001"' \
|
|
95
|
+
>/dev/null
|
|
96
|
+
tail -n 1 "$history_file" | onlooker_validate_event
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "skill_usage_append routes the record to the per-session file named after the session id" {
|
|
100
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/pre-tool-use-skill.json"
|
|
101
|
+
local session_id="skill-session-custom"
|
|
102
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${session_id}.jsonl"
|
|
103
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
104
|
+
|
|
105
|
+
# Build a canonical record, then retarget its session_id; skill_usage_append
|
|
106
|
+
# must file it under the session_id argument, not the embedded one.
|
|
107
|
+
local record
|
|
108
|
+
record=$(skill_usage_build_record "$(cat "$fixture")" \
|
|
109
|
+
| jq -c --arg sid "$session_id" '.session_id = $sid')
|
|
110
|
+
|
|
111
|
+
run skill_usage_append "$session_id" "$record"
|
|
112
|
+
[ "$status" -eq 0 ]
|
|
113
|
+
|
|
114
|
+
[ -f "$history_file" ]
|
|
115
|
+
[ "$(wc -l <"$history_file")" -eq 1 ]
|
|
116
|
+
tail -n 1 "$history_file" | jq -e \
|
|
117
|
+
'.session_id == "skill-session-custom"
|
|
118
|
+
and .payload.skill_name == "code-review"
|
|
119
|
+
and .payload.invocation_source == "tool"' \
|
|
120
|
+
>/dev/null
|
|
121
|
+
tail -n 1 "$history_file" | onlooker_validate_event
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@test "skill_usage_append is a no-op when the session id is empty" {
|
|
125
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/pre-tool-use-skill.json"
|
|
126
|
+
local record
|
|
127
|
+
record=$(skill_usage_build_record "$(cat "$fixture")")
|
|
128
|
+
|
|
129
|
+
# Snapshot the history dir, then assert the empty-session call adds nothing.
|
|
130
|
+
local before
|
|
131
|
+
before=$(find "$ONLOOKER_SESSION_HISTORY_DIR" -type f | sort)
|
|
132
|
+
|
|
133
|
+
run skill_usage_append "" "$record"
|
|
134
|
+
[ "$status" -eq 0 ]
|
|
135
|
+
|
|
136
|
+
local after
|
|
137
|
+
after=$(find "$ONLOOKER_SESSION_HISTORY_DIR" -type f | sort)
|
|
138
|
+
[ "$before" = "$after" ]
|
|
139
|
+
}
|
|
@@ -97,3 +97,105 @@ setup() {
|
|
|
97
97
|
tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -e '.event_type == "task.start"' >/dev/null
|
|
98
98
|
tail -n 1 "$ONLOOKER_EVENTS_LOG" | onlooker_validate_event
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
@test "task_tracker_record_created writes start_time_ms as a number" {
|
|
102
|
+
local sid="rec-session-001"
|
|
103
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
104
|
+
|
|
105
|
+
run task_tracker_record_created "$sid" "task-rec-001"
|
|
106
|
+
[ "$status" -eq 0 ]
|
|
107
|
+
[ -f "$tracker" ]
|
|
108
|
+
jq -e '.tasks["task-rec-001"].start_time_ms | type == "number"' "$tracker" >/dev/null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@test "task_tracker_record_created no-ops on empty session_id" {
|
|
112
|
+
run task_tracker_record_created "" "task-rec-002"
|
|
113
|
+
[ "$status" -eq 0 ]
|
|
114
|
+
# load_validate_path always creates the trackers dir; assert no tracker file landed.
|
|
115
|
+
[ -z "$(find "${ONLOOKER_SESSION_TRACKERS_DIR}" -type f 2>/dev/null)" ]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@test "task_tracker_record_created no-ops on null session_id" {
|
|
119
|
+
run task_tracker_record_created "null" "task-rec-003"
|
|
120
|
+
[ "$status" -eq 0 ]
|
|
121
|
+
[ ! -f "${ONLOOKER_SESSION_TRACKERS_DIR}/null" ]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@test "task_tracker_record_created no-ops on empty task_id" {
|
|
125
|
+
local sid="rec-session-empty-task"
|
|
126
|
+
run task_tracker_record_created "$sid" ""
|
|
127
|
+
[ "$status" -eq 0 ]
|
|
128
|
+
[ ! -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}" ]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@test "task_tracker_duration_ms returns elapsed ms for a seeded task" {
|
|
132
|
+
local sid="dur-session-001"
|
|
133
|
+
turn_state_ensure_session "$sid"
|
|
134
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
135
|
+
|
|
136
|
+
local past
|
|
137
|
+
past=$(( $(session_tracker_now_ms) - 2000 ))
|
|
138
|
+
jq --argjson ms "$past" '.tasks["task-dur-001"] = {start_time_ms: $ms}' \
|
|
139
|
+
"$tracker" >"${tracker}.tmp"
|
|
140
|
+
mv "${tracker}.tmp" "$tracker"
|
|
141
|
+
|
|
142
|
+
local elapsed
|
|
143
|
+
elapsed=$(task_tracker_duration_ms "$sid" "task-dur-001")
|
|
144
|
+
[ -n "$elapsed" ]
|
|
145
|
+
echo "$elapsed" | grep -Eq '^[0-9]+$'
|
|
146
|
+
[ "$elapsed" -ge 1900 ]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@test "task_tracker_duration_ms is empty for an unknown task_id" {
|
|
150
|
+
local sid="dur-session-unknown"
|
|
151
|
+
turn_state_ensure_session "$sid"
|
|
152
|
+
|
|
153
|
+
local out
|
|
154
|
+
run task_tracker_duration_ms "$sid" "task-does-not-exist"
|
|
155
|
+
[ "$status" -eq 0 ]
|
|
156
|
+
[ -z "$output" ]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@test "task_tracker_clear removes the task timing entry" {
|
|
160
|
+
local sid="clear-session-001"
|
|
161
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
162
|
+
|
|
163
|
+
task_tracker_record_created "$sid" "task-clear-001"
|
|
164
|
+
jq -e '.tasks["task-clear-001"]' "$tracker" >/dev/null
|
|
165
|
+
|
|
166
|
+
run task_tracker_clear "$sid" "task-clear-001"
|
|
167
|
+
[ "$status" -eq 0 ]
|
|
168
|
+
|
|
169
|
+
run jq -e '.tasks["task-clear-001"]' "$tracker"
|
|
170
|
+
[ "$status" -ne 0 ]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@test "task_tracker_clear no-ops when tracker file is missing" {
|
|
174
|
+
local sid="clear-session-missing"
|
|
175
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
176
|
+
rm -f "$tracker"
|
|
177
|
+
|
|
178
|
+
run task_tracker_clear "$sid" "task-nope"
|
|
179
|
+
[ "$status" -eq 0 ]
|
|
180
|
+
[ ! -f "$tracker" ]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@test "task_tracker_append delegates to tool_history_append" {
|
|
184
|
+
local sid="append-session-001"
|
|
185
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${sid}.jsonl"
|
|
186
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
187
|
+
|
|
188
|
+
local event
|
|
189
|
+
event=$(jq -c -n --arg sid "$sid" \
|
|
190
|
+
'{schema_version: "1.0",
|
|
191
|
+
plugin: "ecosystem",
|
|
192
|
+
session_id: $sid,
|
|
193
|
+
event_type: "task.start",
|
|
194
|
+
payload: {task_summary: "append test"}}')
|
|
195
|
+
|
|
196
|
+
run task_tracker_append "$sid" "$event"
|
|
197
|
+
[ "$status" -eq 0 ]
|
|
198
|
+
[ -f "$history_file" ]
|
|
199
|
+
tail -n 1 "$history_file" | jq -e '.event_type == "task.start"' >/dev/null
|
|
200
|
+
tail -n 1 "$history_file" | jq -e '.session_id == "append-session-001"' >/dev/null
|
|
201
|
+
}
|