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