@onlooker-community/ecosystem 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +2 -2
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +2 -0
- package/docs/plugin-catalog.md +125 -0
- package/package.json +3 -3
- package/plugins/compass/.claude-plugin/plugin.json +1 -1
- package/plugins/compass/CHANGELOG.md +7 -0
- package/plugins/compass/README.md +1 -3
- package/plugins/compass/config.json +1 -2
- package/plugins/compass/docs/design.md +1 -2
- package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
- package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
- package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
- package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
- package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
- package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
- package/plugins/inspector/.claude-plugin/plugin.json +14 -0
- package/plugins/inspector/README.md +155 -0
- package/plugins/inspector/config.json +25 -0
- package/plugins/inspector/docs/design.md +286 -0
- package/plugins/inspector/hooks/hooks.json +33 -0
- package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
- package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
- package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
- package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
- package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
- package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
- package/test/bats/archivist-project-key.bats +79 -0
- package/test/bats/archivist-storage.bats +79 -0
- package/test/bats/compact-tracker.bats +125 -0
- package/test/bats/compass-config.bats +65 -0
- package/test/bats/compass-gate.bats +129 -0
- package/test/bats/compass-sanitizer.bats +69 -0
- package/test/bats/compass-symbolic-skip.bats +88 -0
- package/test/bats/compass-transcript.bats +80 -0
- package/test/bats/inspector-config.bats +118 -0
- package/test/bats/inspector-events.bats +156 -0
- package/test/bats/inspector-post-write-hook.bats +164 -0
- package/test/bats/inspector-project-key.bats +68 -0
- package/test/bats/inspector-ulid.bats +34 -0
- package/test/bats/onlooker-schema.bats +111 -0
- package/test/bats/prompt-rules.bats +98 -0
- package/test/bats/session-tracker.bats +260 -0
- package/test/bats/skill-usage-tracker.bats +63 -0
- package/test/bats/task-tracker.bats +102 -0
- package/test/bats/turn-tracker.bats +180 -0
- package/test/bats/validate-path.bats +125 -0
- package/test/bats/worktree-tracker.bats +167 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
# shellcheck source=../helpers/setup.bash
|
|
5
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
6
|
+
load_validate_path
|
|
7
|
+
export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
|
|
8
|
+
# shellcheck source=../../scripts/lib/onlooker-schema.sh
|
|
9
|
+
source "${REPO_ROOT}/scripts/lib/onlooker-schema.sh"
|
|
10
|
+
# shellcheck source=../../scripts/lib/tool-history.sh
|
|
11
|
+
source "${REPO_ROOT}/scripts/lib/tool-history.sh"
|
|
12
|
+
# shellcheck source=../../scripts/lib/session-tracker.sh
|
|
13
|
+
source "${REPO_ROOT}/scripts/lib/session-tracker.sh"
|
|
14
|
+
# shellcheck source=../../scripts/lib/turn-tracker.sh
|
|
15
|
+
source "${REPO_ROOT}/scripts/lib/turn-tracker.sh"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# ── turn_tracker_summarize_prompt ──────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
@test "turn_tracker_summarize_prompt collapses newlines and runs of spaces" {
|
|
21
|
+
local out
|
|
22
|
+
out=$(turn_tracker_summarize_prompt $'hello\n\nworld foo')
|
|
23
|
+
[ "$out" = "hello world foo" ]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@test "turn_tracker_summarize_prompt trims leading and trailing spaces" {
|
|
27
|
+
local out
|
|
28
|
+
out=$(turn_tracker_summarize_prompt " padded text ")
|
|
29
|
+
[ "$out" = "padded text" ]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "turn_tracker_summarize_prompt leaves a short prompt unchanged" {
|
|
33
|
+
local out
|
|
34
|
+
out=$(turn_tracker_summarize_prompt "fix the bug")
|
|
35
|
+
[ "$out" = "fix the bug" ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "turn_tracker_summarize_prompt does not truncate input of exactly 200 chars" {
|
|
39
|
+
local exact
|
|
40
|
+
exact=$(printf 'b%.0s' {1..200})
|
|
41
|
+
local out
|
|
42
|
+
out=$(turn_tracker_summarize_prompt "$exact")
|
|
43
|
+
[ "${#out}" -eq 200 ]
|
|
44
|
+
[[ "$out" != *…* ]]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@test "turn_tracker_summarize_prompt truncates to 200 chars plus an ellipsis when over 200" {
|
|
48
|
+
local long
|
|
49
|
+
long=$(printf 'a%.0s' {1..300})
|
|
50
|
+
local out
|
|
51
|
+
out=$(turn_tracker_summarize_prompt "$long")
|
|
52
|
+
# 200 retained characters + the single-character ellipsis = length 201.
|
|
53
|
+
[ "${#out}" -eq 201 ]
|
|
54
|
+
[[ "$out" == *… ]]
|
|
55
|
+
[ "${out%…}" = "$(printf 'a%.0s' {1..200})" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "turn_tracker_summarize_prompt returns empty and succeeds on empty input" {
|
|
59
|
+
run turn_tracker_summarize_prompt ""
|
|
60
|
+
[ "$status" -eq 0 ]
|
|
61
|
+
[ -z "$output" ]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# ── turn_tracker_on_user_prompt ────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
@test "turn_tracker_on_user_prompt keeps turn 1 and marks prompts seen on first prompt" {
|
|
67
|
+
local sid="turn-first-001"
|
|
68
|
+
turn_tracker_on_user_prompt "$sid"
|
|
69
|
+
|
|
70
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
71
|
+
[ -f "$tracker" ]
|
|
72
|
+
jq -e '.turn_number == 1
|
|
73
|
+
and .user_prompts_seen == true
|
|
74
|
+
and .turn_tool_seq == 0' "$tracker" >/dev/null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@test "turn_tracker_on_user_prompt advances the turn on the second prompt" {
|
|
78
|
+
local sid="turn-second-001"
|
|
79
|
+
turn_tracker_on_user_prompt "$sid"
|
|
80
|
+
turn_tracker_on_user_prompt "$sid"
|
|
81
|
+
|
|
82
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
83
|
+
jq -e '.turn_number == 2
|
|
84
|
+
and .user_prompts_seen == true
|
|
85
|
+
and .turn_tool_seq == 0' "$tracker" >/dev/null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@test "turn_tracker_on_user_prompt increments once per subsequent prompt" {
|
|
89
|
+
local sid="turn-many-001"
|
|
90
|
+
turn_tracker_on_user_prompt "$sid" # turn 1, marks seen
|
|
91
|
+
turn_tracker_on_user_prompt "$sid" # turn 2
|
|
92
|
+
turn_tracker_on_user_prompt "$sid" # turn 3
|
|
93
|
+
turn_tracker_on_user_prompt "$sid" # turn 4
|
|
94
|
+
|
|
95
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
96
|
+
jq -e '.turn_number == 4' "$tracker" >/dev/null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "turn_tracker_on_user_prompt no-ops on empty session_id" {
|
|
100
|
+
run turn_tracker_on_user_prompt ""
|
|
101
|
+
[ "$status" -eq 0 ]
|
|
102
|
+
# No tracker file should have been created for an empty id.
|
|
103
|
+
[ ! -f "${ONLOOKER_SESSION_TRACKERS_DIR}/" ]
|
|
104
|
+
[ -z "$(ls -A "${ONLOOKER_SESSION_TRACKERS_DIR}")" ]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@test "turn_tracker_on_user_prompt no-ops on null session_id" {
|
|
108
|
+
run turn_tracker_on_user_prompt "null"
|
|
109
|
+
[ "$status" -eq 0 ]
|
|
110
|
+
[ ! -f "${ONLOOKER_SESSION_TRACKERS_DIR}/null" ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ── turn_tracker_build_prompt_payload ──────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
@test "turn_tracker_build_prompt_payload reads turn_number from the tracker and includes input_summary" {
|
|
116
|
+
local sid="payload-001"
|
|
117
|
+
turn_state_ensure_session "$sid"
|
|
118
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
119
|
+
jq '.turn_number = 5' "$tracker" >"${tracker}.tmp"
|
|
120
|
+
mv "${tracker}.tmp" "$tracker"
|
|
121
|
+
|
|
122
|
+
local payload
|
|
123
|
+
payload=$(turn_tracker_build_prompt_payload "$sid" $'review the\n\ndiff')
|
|
124
|
+
echo "$payload" | jq -e '.turn_number == 5
|
|
125
|
+
and .input_summary == "review the diff"' >/dev/null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@test "turn_tracker_build_prompt_payload defaults turn_number to 1 when tracker is missing" {
|
|
129
|
+
local sid="payload-missing"
|
|
130
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${sid}"
|
|
131
|
+
|
|
132
|
+
local payload
|
|
133
|
+
payload=$(turn_tracker_build_prompt_payload "$sid" "hello")
|
|
134
|
+
echo "$payload" | jq -e '.turn_number == 1 and .input_summary == "hello"' >/dev/null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@test "turn_tracker_build_prompt_payload omits input_summary for an empty prompt" {
|
|
138
|
+
local sid="payload-empty"
|
|
139
|
+
turn_state_ensure_session "$sid"
|
|
140
|
+
|
|
141
|
+
local payload
|
|
142
|
+
payload=$(turn_tracker_build_prompt_payload "$sid" "")
|
|
143
|
+
echo "$payload" | jq -e '.turn_number == 1 and (has("input_summary") | not)' >/dev/null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@test "turn_tracker_build_prompt_payload truncates a long prompt in input_summary" {
|
|
147
|
+
local sid="payload-long"
|
|
148
|
+
turn_state_ensure_session "$sid"
|
|
149
|
+
local long
|
|
150
|
+
long=$(printf 'x%.0s' {1..300})
|
|
151
|
+
|
|
152
|
+
local payload summary
|
|
153
|
+
payload=$(turn_tracker_build_prompt_payload "$sid" "$long")
|
|
154
|
+
summary=$(echo "$payload" | jq -r '.input_summary')
|
|
155
|
+
[ "${#summary}" -eq 201 ]
|
|
156
|
+
[[ "$summary" == *… ]]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@test "turn_tracker_build_prompt_payload returns 1 for null session_id" {
|
|
160
|
+
run turn_tracker_build_prompt_payload "null" "hi"
|
|
161
|
+
[ "$status" -eq 1 ]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@test "turn_tracker_build_prompt_payload returns 1 for empty session_id" {
|
|
165
|
+
run turn_tracker_build_prompt_payload "" "hi"
|
|
166
|
+
[ "$status" -eq 1 ]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ── integration: orchestrator + payload reflect the same turn ──────────────
|
|
170
|
+
|
|
171
|
+
@test "turn_tracker payload reflects turn advanced by turn_tracker_on_user_prompt" {
|
|
172
|
+
local sid="turn-integration-001"
|
|
173
|
+
# Two prompts -> turn 2; payload built afterward should report turn 2.
|
|
174
|
+
turn_tracker_on_user_prompt "$sid"
|
|
175
|
+
turn_tracker_on_user_prompt "$sid"
|
|
176
|
+
|
|
177
|
+
local payload
|
|
178
|
+
payload=$(turn_tracker_build_prompt_payload "$sid" "second prompt")
|
|
179
|
+
echo "$payload" | jq -e '.turn_number == 2 and .input_summary == "second prompt"' >/dev/null
|
|
180
|
+
}
|