@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
|
@@ -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
|
+
}
|
|
@@ -107,3 +107,128 @@ setup() {
|
|
|
107
107
|
and .schema_version == "1.0"' \
|
|
108
108
|
>/dev/null
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------------------------------------
|
|
112
|
+
# Hook health instrumentation: hook_register / hook_success / hook_failure
|
|
113
|
+
# ----------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
@test "hook_register seeds hook name and start time" {
|
|
116
|
+
hook_register "my-hook" "My Hook" "A description"
|
|
117
|
+
trap - EXIT # disarm the trap hook_register installed so it can't fire later
|
|
118
|
+
[ "${_HOOK_NAME}" = "my-hook" ]
|
|
119
|
+
[ -n "${_HOOK_START_TIME}" ]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "hook_success writes a success record to the hook-health log" {
|
|
123
|
+
export _HOOK_SESSION_ID="health-success-session"
|
|
124
|
+
hook_register "success-hook"
|
|
125
|
+
hook_success
|
|
126
|
+
[ -f "$ONLOOKER_HOOK_HEALTH_LOG" ]
|
|
127
|
+
tail -n 1 "$ONLOOKER_HOOK_HEALTH_LOG" | jq -e \
|
|
128
|
+
'.hook == "success-hook"
|
|
129
|
+
and .status == "success"
|
|
130
|
+
and .error == null
|
|
131
|
+
and .session_id == "health-success-session"' \
|
|
132
|
+
>/dev/null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@test "hook_failure writes a failure record with the error message" {
|
|
136
|
+
hook_register "failure-hook"
|
|
137
|
+
hook_failure "boom: it broke"
|
|
138
|
+
[ -f "$ONLOOKER_HOOK_HEALTH_LOG" ]
|
|
139
|
+
tail -n 1 "$ONLOOKER_HOOK_HEALTH_LOG" | jq -e \
|
|
140
|
+
'.hook == "failure-hook"
|
|
141
|
+
and .status == "failure"
|
|
142
|
+
and .error == "boom: it broke"' \
|
|
143
|
+
>/dev/null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@test "hook_health_summary reflects seeded success and failure records" {
|
|
147
|
+
# Two records for the same hook: one success, one failure.
|
|
148
|
+
hook_register "summary-hook"
|
|
149
|
+
hook_success
|
|
150
|
+
hook_register "summary-hook"
|
|
151
|
+
hook_failure "an error"
|
|
152
|
+
|
|
153
|
+
local summary
|
|
154
|
+
summary=$(hook_health_summary 24)
|
|
155
|
+
echo "$summary" | jq -e \
|
|
156
|
+
'map(select(.hook == "summary-hook"))
|
|
157
|
+
| .[0]
|
|
158
|
+
| .total == 2
|
|
159
|
+
and .success == 1
|
|
160
|
+
and .failure == 1
|
|
161
|
+
and .last_error == "an error"' \
|
|
162
|
+
>/dev/null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ----------------------------------------------------------------------------
|
|
166
|
+
# Hook composition bus: hook_bus_list / hook_bus_cleanup
|
|
167
|
+
# ----------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
@test "hook_bus_list lists put findings without the .json extension" {
|
|
170
|
+
export _HOOK_SESSION_ID="bus-list-session"
|
|
171
|
+
export _HOOK_TOOL_NAME="Agent"
|
|
172
|
+
hook_bus_init '{"tool_input":{"agent_id":"list"}}'
|
|
173
|
+
hook_bus_put "alpha" '{"a":1}'
|
|
174
|
+
hook_bus_put "beta" '{"b":2}'
|
|
175
|
+
local listing
|
|
176
|
+
listing=$(hook_bus_list | sort | tr '\n' ' ')
|
|
177
|
+
[ "$listing" = "alpha beta " ]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@test "hook_bus_cleanup removes aged bus dirs but keeps fresh ones" {
|
|
181
|
+
local tmp_dir
|
|
182
|
+
tmp_dir="$(cd /tmp && pwd -P)"
|
|
183
|
+
local fresh="${tmp_dir}/.onlooker-hook-bus-cleanup-fresh-$$"
|
|
184
|
+
local aged="${tmp_dir}/.onlooker-hook-bus-cleanup-aged-$$"
|
|
185
|
+
mkdir -p "$fresh" "$aged"
|
|
186
|
+
# Backdate the aged dir well past the 5-minute (-mmin +5) cutoff.
|
|
187
|
+
touch -t "$(date -v-10M +%Y%m%d%H%M.%S 2>/dev/null || date -d '10 minutes ago' +%Y%m%d%H%M.%S)" "$aged"
|
|
188
|
+
|
|
189
|
+
hook_bus_cleanup
|
|
190
|
+
|
|
191
|
+
[ ! -d "$aged" ]
|
|
192
|
+
[ -d "$fresh" ]
|
|
193
|
+
rm -rf "$fresh"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# ----------------------------------------------------------------------------
|
|
197
|
+
# Readability / writability validators
|
|
198
|
+
# ----------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
@test "validate_file_readable succeeds for existing readable file" {
|
|
201
|
+
local f="${BATS_TEST_TMPDIR}/readable.txt"
|
|
202
|
+
touch "$f"
|
|
203
|
+
validate_file_readable "$f"
|
|
204
|
+
[ "$?" -eq 0 ]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@test "validate_file_readable fails for missing file" {
|
|
208
|
+
! validate_file_readable "${BATS_TEST_TMPDIR}/no-such-file.txt"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@test "validate_file_writable succeeds when parent directory is writable" {
|
|
212
|
+
validate_file_writable "${BATS_TEST_TMPDIR}/new-file.txt"
|
|
213
|
+
[ "$?" -eq 0 ]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@test "validate_file_writable fails when parent directory does not exist" {
|
|
217
|
+
! validate_file_writable "${BATS_TEST_TMPDIR}/missing-dir/new-file.txt"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# ----------------------------------------------------------------------------
|
|
221
|
+
# Turn state tracking: turn_state_next_turn
|
|
222
|
+
# ----------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
@test "turn_state_next_turn increments turn_number from 1 to 2" {
|
|
225
|
+
local session_id="next-turn-session"
|
|
226
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
227
|
+
turn_state_ensure_session "$session_id"
|
|
228
|
+
[ -f "$tracker" ]
|
|
229
|
+
# Fresh session starts at turn_number 1.
|
|
230
|
+
jq -e '.turn_number == 1' "$tracker" >/dev/null
|
|
231
|
+
|
|
232
|
+
turn_state_next_turn "$session_id"
|
|
233
|
+
jq -e '.turn_number == 2 and .turn_tool_seq == 0' "$tracker" >/dev/null
|
|
234
|
+
}
|
|
@@ -127,3 +127,170 @@ setup() {
|
|
|
127
127
|
and (.payload.command | test("worktree:create"))' \
|
|
128
128
|
>/dev/null
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
@test "worktree_tracker_repo_root prints the git toplevel for a cwd inside the repo" {
|
|
132
|
+
local expected
|
|
133
|
+
expected=$(git -C "$GIT_REPO" rev-parse --show-toplevel)
|
|
134
|
+
|
|
135
|
+
run worktree_tracker_repo_root "$GIT_REPO"
|
|
136
|
+
[ "$status" -eq 0 ]
|
|
137
|
+
[ "$output" = "$expected" ]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@test "worktree_tracker_repo_root returns empty for a non-repo directory" {
|
|
141
|
+
local non_repo="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
142
|
+
mkdir -p "$non_repo"
|
|
143
|
+
|
|
144
|
+
run worktree_tracker_repo_root "$non_repo"
|
|
145
|
+
[ "$status" -ne 0 ]
|
|
146
|
+
[ -z "$output" ]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@test "worktree_tracker_repo_root returns non-zero for empty cwd" {
|
|
150
|
+
run worktree_tracker_repo_root ""
|
|
151
|
+
[ "$status" -ne 0 ]
|
|
152
|
+
[ -z "$output" ]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@test "worktree_tracker_git_create creates a worktree at the expected path" {
|
|
156
|
+
local name="feature-create"
|
|
157
|
+
local expected="${GIT_REPO}/.claude/worktrees/${name}"
|
|
158
|
+
|
|
159
|
+
local worktree_path
|
|
160
|
+
worktree_path=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
161
|
+
|
|
162
|
+
[ "$worktree_path" = "$(cd "$expected" && pwd -P)" ]
|
|
163
|
+
[ -d "$worktree_path" ]
|
|
164
|
+
git -C "$GIT_REPO" worktree list --porcelain | grep -Fq "worktree $(cd "$expected" && pwd -P)"
|
|
165
|
+
git -C "$GIT_REPO" show-ref --verify --quiet "refs/heads/worktree-${name}"
|
|
166
|
+
|
|
167
|
+
worktree_tracker_git_remove "$GIT_REPO" "$worktree_path" 2>/dev/null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@test "worktree_tracker_git_create is idempotent for an existing worktree dir" {
|
|
171
|
+
local name="feature-idempotent"
|
|
172
|
+
|
|
173
|
+
local first second
|
|
174
|
+
first=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
175
|
+
second=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
176
|
+
|
|
177
|
+
[ "$first" = "$second" ]
|
|
178
|
+
[ -d "$second" ]
|
|
179
|
+
|
|
180
|
+
worktree_tracker_git_remove "$GIT_REPO" "$first" 2>/dev/null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@test "worktree_tracker_git_create returns non-zero with missing args" {
|
|
184
|
+
run worktree_tracker_git_create "$GIT_REPO" ""
|
|
185
|
+
[ "$status" -ne 0 ]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "worktree_tracker_git_remove removes a registered worktree" {
|
|
189
|
+
local name="feature-remove"
|
|
190
|
+
local worktree_path
|
|
191
|
+
worktree_path=$(worktree_tracker_git_create "$GIT_REPO" "$name" 2>/dev/null)
|
|
192
|
+
[ -d "$worktree_path" ]
|
|
193
|
+
|
|
194
|
+
worktree_tracker_git_remove "$GIT_REPO" "$worktree_path" 2>/dev/null
|
|
195
|
+
|
|
196
|
+
[ ! -d "$worktree_path" ]
|
|
197
|
+
! git -C "$GIT_REPO" worktree list --porcelain | grep -Fq "worktree ${worktree_path}"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "worktree_tracker_record_created writes timing into the session tracker" {
|
|
201
|
+
local session_id="worktree-record-001"
|
|
202
|
+
local name="feature-record"
|
|
203
|
+
local worktree_path="${GIT_REPO}/.claude/worktrees/${name}"
|
|
204
|
+
local branch="worktree-${name}"
|
|
205
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
206
|
+
|
|
207
|
+
worktree_tracker_record_created "$session_id" "$name" "$worktree_path" "$branch"
|
|
208
|
+
|
|
209
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
210
|
+
[ -f "$tracker_file" ]
|
|
211
|
+
jq -e \
|
|
212
|
+
--arg name "$name" \
|
|
213
|
+
--arg path "$worktree_path" \
|
|
214
|
+
--arg branch "$branch" \
|
|
215
|
+
'.worktrees[$name].path == $path
|
|
216
|
+
and .worktrees[$name].branch == $branch
|
|
217
|
+
and (.worktrees[$name].start_time_ms | type == "number")' \
|
|
218
|
+
"$tracker_file" >/dev/null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@test "worktree_tracker_record_created is a no-op with missing args" {
|
|
222
|
+
local session_id="worktree-record-noop"
|
|
223
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
224
|
+
|
|
225
|
+
run worktree_tracker_record_created "$session_id" "" "/some/path" "branch"
|
|
226
|
+
[ "$status" -eq 0 ]
|
|
227
|
+
[ ! -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}" ]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@test "worktree_tracker_duration_ms returns elapsed ms from a seeded start" {
|
|
231
|
+
local session_id="worktree-duration-001"
|
|
232
|
+
local name="feature-duration"
|
|
233
|
+
local worktree_path="${GIT_REPO}/.claude/worktrees/${name}"
|
|
234
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
235
|
+
|
|
236
|
+
worktree_tracker_record_created "$session_id" "$name" "$worktree_path" "worktree-${name}"
|
|
237
|
+
|
|
238
|
+
# Rewind the recorded start_time_ms by ~2s so elapsed is a stable positive value.
|
|
239
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
240
|
+
local now_ms past temp
|
|
241
|
+
now_ms=$(session_tracker_now_ms)
|
|
242
|
+
past=$(( now_ms - 2000 ))
|
|
243
|
+
temp=$(mktemp)
|
|
244
|
+
jq --arg name "$name" --argjson ms "$past" \
|
|
245
|
+
'.worktrees[$name].start_time_ms = $ms' "$tracker_file" >"$temp"
|
|
246
|
+
mv "$temp" "$tracker_file"
|
|
247
|
+
|
|
248
|
+
local duration
|
|
249
|
+
duration=$(worktree_tracker_duration_ms "$session_id" "$worktree_path")
|
|
250
|
+
[[ "$duration" =~ ^[0-9]+$ ]]
|
|
251
|
+
[ "$duration" -ge 1900 ]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@test "worktree_tracker_duration_ms returns empty for an unknown worktree path" {
|
|
255
|
+
local session_id="worktree-duration-unknown"
|
|
256
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
257
|
+
turn_state_ensure_session "$session_id"
|
|
258
|
+
|
|
259
|
+
run worktree_tracker_duration_ms "$session_id" "/never/recorded"
|
|
260
|
+
[ "$status" -eq 0 ]
|
|
261
|
+
[ -z "$output" ]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@test "worktree_tracker_clear_by_path removes the recorded entry" {
|
|
265
|
+
local session_id="worktree-clear-001"
|
|
266
|
+
local name="feature-clear"
|
|
267
|
+
local worktree_path="${GIT_REPO}/.claude/worktrees/${name}"
|
|
268
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
269
|
+
|
|
270
|
+
worktree_tracker_record_created "$session_id" "$name" "$worktree_path" "worktree-${name}"
|
|
271
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
272
|
+
jq -e --arg name "$name" '.worktrees | has($name)' "$tracker_file" >/dev/null
|
|
273
|
+
|
|
274
|
+
worktree_tracker_clear_by_path "$session_id" "$worktree_path"
|
|
275
|
+
|
|
276
|
+
jq -e --arg name "$name" '(.worktrees | has($name)) | not' "$tracker_file" >/dev/null
|
|
277
|
+
run worktree_tracker_duration_ms "$session_id" "$worktree_path"
|
|
278
|
+
[ -z "$output" ]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@test "worktree_tracker_append lands a JSON line in the session history" {
|
|
282
|
+
local session_id="worktree-append-001"
|
|
283
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${session_id}.jsonl"
|
|
284
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
285
|
+
|
|
286
|
+
local event
|
|
287
|
+
event=$(jq -c -n '{event_type: "tool.shell.exec", payload: {command: "git worktree:create"}}')
|
|
288
|
+
|
|
289
|
+
worktree_tracker_append "$session_id" "$event"
|
|
290
|
+
|
|
291
|
+
[ -f "$history_file" ]
|
|
292
|
+
tail -n 1 "$history_file" | jq -e \
|
|
293
|
+
'.event_type == "tool.shell.exec"
|
|
294
|
+
and (.payload.command | test("worktree:create"))' \
|
|
295
|
+
>/dev/null
|
|
296
|
+
}
|