@onlooker-community/ecosystem 0.26.0 → 0.27.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 +4 -3
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +2 -0
- package/docs/architecture.md +4 -0
- package/package.json +3 -3
- package/plugins/bursar/.claude-plugin/plugin.json +14 -0
- package/plugins/bursar/CHANGELOG.md +10 -0
- package/plugins/bursar/README.md +100 -0
- package/plugins/bursar/config.json +11 -0
- package/plugins/bursar/hooks/hooks.json +26 -0
- package/plugins/bursar/scripts/hooks/bursar-session-end.sh +142 -0
- package/plugins/bursar/scripts/hooks/bursar-session-start.sh +130 -0
- package/plugins/bursar/scripts/lib/bursar-config.sh +108 -0
- package/plugins/bursar/scripts/lib/bursar-events.sh +82 -0
- package/plugins/bursar/scripts/lib/bursar-ledger.sh +152 -0
- package/plugins/bursar/scripts/lib/bursar-project-key.sh +85 -0
- package/plugins/bursar/scripts/lib/bursar-ulid.sh +53 -0
- package/plugins/bursar/scripts/lib/portable-lock.sh +59 -0
- package/plugins/counsel/.claude-plugin/plugin.json +1 -1
- package/plugins/counsel/CHANGELOG.md +8 -0
- package/plugins/counsel/scripts/lib/counsel-reader.sh +7 -1
- package/plugins/counsel/scripts/lib/counsel-synthesize.sh +8 -1
- package/release-please-config.json +16 -0
- package/test/bats/bursar-config.bats +79 -0
- package/test/bats/bursar-events.bats +73 -0
- package/test/bats/bursar-ledger.bats +116 -0
- package/test/bats/bursar-project-key.bats +51 -0
- package/test/bats/bursar-session-end.bats +131 -0
- package/test/bats/bursar-session-start.bats +126 -0
- package/test/bats/bursar-ulid.bats +28 -0
- package/test/bats/counsel-reader.bats +28 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/bursar-config.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "bursar is disabled by default" {
|
|
14
|
+
bursar_config_load ""
|
|
15
|
+
run bursar_config_enabled
|
|
16
|
+
[ "$status" -ne 0 ]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "user-level settings.json can enable bursar" {
|
|
20
|
+
mkdir -p "${HOME}/.claude"
|
|
21
|
+
printf '%s\n' '{"bursar":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
22
|
+
bursar_config_load ""
|
|
23
|
+
run bursar_config_enabled
|
|
24
|
+
[ "$status" -eq 0 ]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "repo-level settings.json overrides user-level" {
|
|
28
|
+
mkdir -p "${HOME}/.claude"
|
|
29
|
+
printf '%s\n' '{"bursar":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
30
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
31
|
+
mkdir -p "${repo}/.claude"
|
|
32
|
+
printf '%s\n' '{"bursar":{"enabled":false}}' > "${repo}/.claude/settings.json"
|
|
33
|
+
bursar_config_load "$repo"
|
|
34
|
+
run bursar_config_enabled
|
|
35
|
+
[ "$status" -ne 0 ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "default window is rolling_7d" {
|
|
39
|
+
bursar_config_load ""
|
|
40
|
+
[ "$(bursar_config_window)" = "rolling_7d" ]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "window can be set to calendar_week" {
|
|
44
|
+
mkdir -p "${HOME}/.claude"
|
|
45
|
+
printf '%s\n' '{"bursar":{"window":"calendar_week"}}' > "${HOME}/.claude/settings.json"
|
|
46
|
+
bursar_config_load ""
|
|
47
|
+
[ "$(bursar_config_window)" = "calendar_week" ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@test "an invalid window falls back to rolling_7d" {
|
|
51
|
+
mkdir -p "${HOME}/.claude"
|
|
52
|
+
printf '%s\n' '{"bursar":{"window":"yearly"}}' > "${HOME}/.claude/settings.json"
|
|
53
|
+
bursar_config_load ""
|
|
54
|
+
[ "$(bursar_config_window)" = "rolling_7d" ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "default week_start is monday" {
|
|
58
|
+
bursar_config_load ""
|
|
59
|
+
[ "$(bursar_config_week_start)" = "monday" ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "week_start can be set to sunday" {
|
|
63
|
+
mkdir -p "${HOME}/.claude"
|
|
64
|
+
printf '%s\n' '{"bursar":{"week_start":"sunday"}}' > "${HOME}/.claude/settings.json"
|
|
65
|
+
bursar_config_load ""
|
|
66
|
+
[ "$(bursar_config_week_start)" = "sunday" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@test "surfacing is on by default and can be disabled" {
|
|
70
|
+
bursar_config_load ""
|
|
71
|
+
run bursar_config_surface_enabled
|
|
72
|
+
[ "$status" -eq 0 ]
|
|
73
|
+
|
|
74
|
+
mkdir -p "${HOME}/.claude"
|
|
75
|
+
printf '%s\n' '{"bursar":{"surface_at_session_start":false}}' > "${HOME}/.claude/settings.json"
|
|
76
|
+
bursar_config_load ""
|
|
77
|
+
run bursar_config_surface_enabled
|
|
78
|
+
[ "$status" -ne 0 ]
|
|
79
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates that bursar.* events pass @onlooker-community/schema validation.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
10
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
11
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
12
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
13
|
+
|
|
14
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
15
|
+
|
|
16
|
+
# shellcheck disable=SC1091
|
|
17
|
+
source "${PLUGIN_ROOT}/scripts/lib/bursar-events.sh"
|
|
18
|
+
|
|
19
|
+
export CLAUDE_SESSION_ID="bats-bursar-session-$$"
|
|
20
|
+
PK="proj0123abcd"
|
|
21
|
+
SID="bats-bursar-sid-000"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_validate_latest_event() {
|
|
25
|
+
local last
|
|
26
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
27
|
+
[ -n "$last" ] || return 1
|
|
28
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
29
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@test "bursar.session.recorded with governor present validates" {
|
|
33
|
+
local p
|
|
34
|
+
p=$(jq -n --arg pk "$PK" --arg sid "$SID" \
|
|
35
|
+
'{project_key:$pk, session_id:$sid, governor_present:true,
|
|
36
|
+
cost_usd:0.42, tokens:42000, api_calls:12, model:"claude-opus-4-8"}')
|
|
37
|
+
bursar_emit_event "bursar.session.recorded" "$p" "$SID"
|
|
38
|
+
run _validate_latest_event
|
|
39
|
+
[ "$status" -eq 0 ]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@test "bursar.session.recorded with governor absent validates" {
|
|
43
|
+
local p
|
|
44
|
+
p=$(jq -n --arg pk "$PK" --arg sid "$SID" \
|
|
45
|
+
'{project_key:$pk, session_id:$sid, governor_present:false}')
|
|
46
|
+
bursar_emit_event "bursar.session.recorded" "$p" "$SID"
|
|
47
|
+
run _validate_latest_event
|
|
48
|
+
[ "$status" -eq 0 ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "bursar.rollup.surfaced validates" {
|
|
52
|
+
local p
|
|
53
|
+
p=$(jq -n --arg pk "$PK" \
|
|
54
|
+
'{project_key:$pk, window:"rolling_7d", total_cost_usd:3.17,
|
|
55
|
+
session_count:8, total_tokens:310000, sessions_with_cost:7,
|
|
56
|
+
window_start:"2026-06-05T00:00:00Z"}')
|
|
57
|
+
bursar_emit_event "bursar.rollup.surfaced" "$p" "$SID"
|
|
58
|
+
run _validate_latest_event
|
|
59
|
+
[ "$status" -eq 0 ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "bursar.rollup.skipped validates" {
|
|
63
|
+
local p
|
|
64
|
+
p=$(jq -n --arg pk "$PK" '{reason:"no_data", project_key:$pk}')
|
|
65
|
+
bursar_emit_event "bursar.rollup.skipped" "$p" "$SID"
|
|
66
|
+
run _validate_latest_event
|
|
67
|
+
[ "$status" -eq 0 ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@test "bursar_emit_event returns nonzero for an unknown event type" {
|
|
71
|
+
run bursar_emit_event "bursar.no_such_event" '{"project_key":"x"}' "$SID"
|
|
72
|
+
[ "$status" -ne 0 ]
|
|
73
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/portable-lock.sh"
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
source "${PLUGIN_ROOT}/scripts/lib/bursar-ledger.sh"
|
|
12
|
+
|
|
13
|
+
KEY="proj0123abcd"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_record() {
|
|
17
|
+
# _record <session_id> <cost-or-empty> <tokens-or-empty> <ts_epoch>
|
|
18
|
+
local sid="$1" cost="$2" tokens="$3" ts="$4"
|
|
19
|
+
local rec
|
|
20
|
+
rec=$(jq -n --arg sid "$sid" --arg pk "$KEY" --argjson te "$ts" \
|
|
21
|
+
'{ts:"x", ts_epoch:$te, session_id:$sid, project_key:$pk, governor_present:true}')
|
|
22
|
+
[ -n "$cost" ] && rec=$(printf '%s' "$rec" | jq --argjson v "$cost" '. + {cost_usd:$v}')
|
|
23
|
+
[ -n "$tokens" ] && rec=$(printf '%s' "$rec" | jq --argjson v "$tokens" '. + {tokens:$v}')
|
|
24
|
+
printf '%s' "$rec"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@test "recording a session creates a single ledger line" {
|
|
28
|
+
local now
|
|
29
|
+
now=$(date +%s)
|
|
30
|
+
bursar_ledger_record "$KEY" "$(_record s1 1.0 100 "$now")"
|
|
31
|
+
local path
|
|
32
|
+
path=$(bursar_ledger_path "$KEY")
|
|
33
|
+
[ -f "$path" ]
|
|
34
|
+
[ "$(wc -l < "$path")" -eq 1 ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "re-recording the same session upserts in place (idempotent)" {
|
|
38
|
+
local now
|
|
39
|
+
now=$(date +%s)
|
|
40
|
+
bursar_ledger_record "$KEY" "$(_record s1 1.0 100 "$now")"
|
|
41
|
+
bursar_ledger_record "$KEY" "$(_record s1 2.5 200 "$now")"
|
|
42
|
+
local path
|
|
43
|
+
path=$(bursar_ledger_path "$KEY")
|
|
44
|
+
[ "$(wc -l < "$path")" -eq 1 ]
|
|
45
|
+
[ "$(jq -r '.cost_usd' "$path")" = "2.5" ]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@test "different sessions append distinct lines" {
|
|
49
|
+
local now
|
|
50
|
+
now=$(date +%s)
|
|
51
|
+
bursar_ledger_record "$KEY" "$(_record s1 1.0 100 "$now")"
|
|
52
|
+
bursar_ledger_record "$KEY" "$(_record s2 2.0 200 "$now")"
|
|
53
|
+
[ "$(wc -l < "$(bursar_ledger_path "$KEY")")" -eq 2 ]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@test "rolling_7d cutoff is roughly now minus seven days" {
|
|
57
|
+
local now cutoff diff
|
|
58
|
+
now=$(date +%s)
|
|
59
|
+
cutoff=$(bursar_window_cutoff_epoch "rolling_7d" "monday")
|
|
60
|
+
diff=$(( now - cutoff ))
|
|
61
|
+
# 7 days = 604800s; allow a couple of seconds of clock drift across calls.
|
|
62
|
+
[ "$diff" -ge 604798 ]
|
|
63
|
+
[ "$diff" -le 604803 ]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@test "calendar_week cutoff is at or before now and not in the future" {
|
|
67
|
+
local now cutoff
|
|
68
|
+
now=$(date +%s)
|
|
69
|
+
cutoff=$(bursar_window_cutoff_epoch "calendar_week" "monday")
|
|
70
|
+
[ "$cutoff" -le "$now" ]
|
|
71
|
+
# Never more than a full week back.
|
|
72
|
+
[ "$(( now - cutoff ))" -le 604800 ]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@test "window totals sum cost and tokens, count sessions, and track cost coverage" {
|
|
76
|
+
local now in1 in2 out
|
|
77
|
+
now=$(date +%s)
|
|
78
|
+
in1=$(( now - 100 ))
|
|
79
|
+
in2=$(( now - 200 ))
|
|
80
|
+
out=$(( now - 700000 )) # older than 7 days
|
|
81
|
+
|
|
82
|
+
local dir
|
|
83
|
+
dir=$(bursar_ledger_dir "$KEY")
|
|
84
|
+
mkdir -p "$dir"
|
|
85
|
+
{
|
|
86
|
+
_record withcost 1.0 100 "$in1"
|
|
87
|
+
printf '\n'
|
|
88
|
+
# governor absent: no cost_usd, no tokens
|
|
89
|
+
jq -nc --arg pk "$KEY" --argjson te "$in2" \
|
|
90
|
+
'{ts:"x", ts_epoch:$te, session_id:"nocost", project_key:$pk, governor_present:false}'
|
|
91
|
+
_record stale 50.0 9999 "$out"
|
|
92
|
+
printf '\n'
|
|
93
|
+
} > "${dir}/sessions.jsonl"
|
|
94
|
+
|
|
95
|
+
local cutoff totals
|
|
96
|
+
cutoff=$(bursar_window_cutoff_epoch "rolling_7d" "monday")
|
|
97
|
+
totals=$(bursar_window_totals "$KEY" "$cutoff")
|
|
98
|
+
|
|
99
|
+
[ "$(printf '%s' "$totals" | jq -r '.total_cost_usd')" = "1" ]
|
|
100
|
+
[ "$(printf '%s' "$totals" | jq -r '.total_tokens')" = "100" ]
|
|
101
|
+
[ "$(printf '%s' "$totals" | jq -r '.session_count')" = "2" ]
|
|
102
|
+
[ "$(printf '%s' "$totals" | jq -r '.sessions_with_cost')" = "1" ]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@test "window totals are zero when no ledger exists" {
|
|
106
|
+
local totals
|
|
107
|
+
totals=$(bursar_window_totals "nonexistent000" "0")
|
|
108
|
+
[ "$(printf '%s' "$totals" | jq -r '.session_count')" = "0" ]
|
|
109
|
+
[ "$(printf '%s' "$totals" | jq -r '.total_cost_usd')" = "0" ]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@test "token formatting is human-friendly" {
|
|
113
|
+
[ "$(bursar_fmt_tokens 800)" = "800" ]
|
|
114
|
+
[ "$(bursar_fmt_tokens 42000)" = "42k" ]
|
|
115
|
+
[ "$(bursar_fmt_tokens 3100000)" = "3.1M" ]
|
|
116
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/bursar-project-key.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_mk_repo_with_remote() {
|
|
13
|
+
local dir="$1" url="$2"
|
|
14
|
+
mkdir -p "$dir"
|
|
15
|
+
git init -q "$dir" 2>/dev/null
|
|
16
|
+
git -C "$dir" remote add origin "$url" 2>/dev/null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "key is a 12-char hex string for a repo with a remote" {
|
|
20
|
+
local repo="${BATS_TEST_TMPDIR}/repo-a"
|
|
21
|
+
_mk_repo_with_remote "$repo" "https://example.com/onlooker/a.git"
|
|
22
|
+
run bursar_project_key "$repo"
|
|
23
|
+
[ "$status" -eq 0 ]
|
|
24
|
+
[ "${#output}" -eq 12 ]
|
|
25
|
+
[[ "$output" =~ ^[0-9a-f]{12}$ ]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "same cwd yields a stable key" {
|
|
29
|
+
local repo="${BATS_TEST_TMPDIR}/repo-b"
|
|
30
|
+
_mk_repo_with_remote "$repo" "https://example.com/onlooker/b.git"
|
|
31
|
+
local a b
|
|
32
|
+
a=$(bursar_project_key "$repo")
|
|
33
|
+
b=$(bursar_project_key "$repo")
|
|
34
|
+
[ -n "$a" ]
|
|
35
|
+
[ "$a" = "$b" ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "different remotes yield different keys" {
|
|
39
|
+
local r1="${BATS_TEST_TMPDIR}/repo-c" r2="${BATS_TEST_TMPDIR}/repo-d"
|
|
40
|
+
_mk_repo_with_remote "$r1" "https://example.com/onlooker/c.git"
|
|
41
|
+
_mk_repo_with_remote "$r2" "https://example.com/onlooker/d.git"
|
|
42
|
+
[ "$(bursar_project_key "$r1")" != "$(bursar_project_key "$r2")" ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "empty key for a non-git directory" {
|
|
46
|
+
local plain="${BATS_TEST_TMPDIR}/not-a-repo"
|
|
47
|
+
mkdir -p "$plain"
|
|
48
|
+
run bursar_project_key "$plain"
|
|
49
|
+
[ "$status" -eq 0 ]
|
|
50
|
+
[ -z "$output" ]
|
|
51
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the SessionEnd hook end-to-end against an isolated $ONLOOKER_DIR.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
10
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/bursar-session-end.sh"
|
|
11
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
12
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
13
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
14
|
+
|
|
15
|
+
SID="bats-end-001"
|
|
16
|
+
PK="projendabcd12"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_enable() {
|
|
20
|
+
mkdir -p "${HOME}/.claude"
|
|
21
|
+
printf '%s\n' '{"bursar":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_breadcrumb() {
|
|
25
|
+
local dir="${ONLOOKER_DIR}/bursar/sessions"
|
|
26
|
+
mkdir -p "$dir"
|
|
27
|
+
jq -n --arg pk "$PK" '{project_key:$pk, cwd:"/tmp", started_at:"x"}' > "${dir}/${SID}.json"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_seed_governor_event() {
|
|
31
|
+
# A minimal but well-formed governor.session.complete envelope line.
|
|
32
|
+
jq -nc --arg sid "$SID" \
|
|
33
|
+
'{event_type:"governor.session.complete", plugin:"governor", session_id:"outer",
|
|
34
|
+
payload:{session_id:$sid, total_cost_usd:0.42, total_tokens:42000, total_api_calls:12,
|
|
35
|
+
budget_usd:1.0, under_budget:true, duration_ms:0, calls_blocked:0,
|
|
36
|
+
calls_warned:0, ledger_poisoned:false}}' >> "$ONLOOKER_EVENTS_LOG"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_ledger_path() { printf '%s/bursar/projects/%s/sessions.jsonl' "$ONLOOKER_DIR" "$PK"; }
|
|
40
|
+
|
|
41
|
+
_run_hook() {
|
|
42
|
+
printf '%s' "{\"session_id\":\"$SID\"}" > "${BATS_TEST_TMPDIR}/in.json"
|
|
43
|
+
run bash "$HOOK" < "${BATS_TEST_TMPDIR}/in.json"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test "records a session's spend from governor.session.complete" {
|
|
47
|
+
_enable
|
|
48
|
+
_breadcrumb
|
|
49
|
+
_seed_governor_event
|
|
50
|
+
_run_hook
|
|
51
|
+
[ "$status" -eq 0 ]
|
|
52
|
+
|
|
53
|
+
local path
|
|
54
|
+
path=$(_ledger_path)
|
|
55
|
+
[ -f "$path" ]
|
|
56
|
+
[ "$(wc -l < "$path")" -eq 1 ]
|
|
57
|
+
[ "$(jq -r '.cost_usd' "$path")" = "0.42" ]
|
|
58
|
+
[ "$(jq -r '.tokens' "$path")" = "42000" ]
|
|
59
|
+
[ "$(jq -r '.api_calls' "$path")" = "12" ]
|
|
60
|
+
[ "$(jq -r '.governor_present' "$path")" = "true" ]
|
|
61
|
+
[ "$(jq -r '.session_id' "$path")" = "$SID" ]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@test "removes the breadcrumb after recording" {
|
|
65
|
+
_enable
|
|
66
|
+
_breadcrumb
|
|
67
|
+
_seed_governor_event
|
|
68
|
+
_run_hook
|
|
69
|
+
[ ! -f "${ONLOOKER_DIR}/bursar/sessions/${SID}.json" ]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@test "degrades to governor_present:false when no governor event exists" {
|
|
73
|
+
_enable
|
|
74
|
+
_breadcrumb
|
|
75
|
+
# no governor.session.complete seeded
|
|
76
|
+
_run_hook
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
|
|
79
|
+
local path
|
|
80
|
+
path=$(_ledger_path)
|
|
81
|
+
[ -f "$path" ]
|
|
82
|
+
[ "$(jq -r '.governor_present' "$path")" = "false" ]
|
|
83
|
+
[ "$(jq -r 'has("cost_usd")' "$path")" = "false" ]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@test "is idempotent across a repeated SessionEnd" {
|
|
87
|
+
_enable
|
|
88
|
+
_breadcrumb
|
|
89
|
+
_seed_governor_event
|
|
90
|
+
_run_hook
|
|
91
|
+
# Breadcrumb is gone now; re-create it to simulate a second SessionEnd.
|
|
92
|
+
_breadcrumb
|
|
93
|
+
_run_hook
|
|
94
|
+
[ "$(wc -l < "$(_ledger_path)")" -eq 1 ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "writes nothing when bursar is disabled" {
|
|
98
|
+
# bursar disabled (no settings written)
|
|
99
|
+
_breadcrumb
|
|
100
|
+
_seed_governor_event
|
|
101
|
+
_run_hook
|
|
102
|
+
[ "$status" -eq 0 ]
|
|
103
|
+
[ ! -d "${ONLOOKER_DIR}/bursar/projects" ]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@test "keeps the breadcrumb and emits nothing when the ledger write fails" {
|
|
107
|
+
_enable
|
|
108
|
+
_breadcrumb
|
|
109
|
+
_seed_governor_event
|
|
110
|
+
# Force bursar_ledger_record to fail: a file where the projects dir must go,
|
|
111
|
+
# so its `mkdir -p` cannot create the project directory.
|
|
112
|
+
mkdir -p "${ONLOOKER_DIR}/bursar"
|
|
113
|
+
printf 'x' > "${ONLOOKER_DIR}/bursar/projects"
|
|
114
|
+
_run_hook
|
|
115
|
+
[ "$status" -eq 0 ]
|
|
116
|
+
# Breadcrumb retained so the attribution survives for a later attempt.
|
|
117
|
+
[ -f "${ONLOOKER_DIR}/bursar/sessions/${SID}.json" ]
|
|
118
|
+
# No false "recorded" event.
|
|
119
|
+
run grep -c '"event_type":"bursar.session.recorded"' "$ONLOOKER_EVENTS_LOG"
|
|
120
|
+
[ "$output" -eq 0 ]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@test "emits bursar.session.recorded" {
|
|
124
|
+
_enable
|
|
125
|
+
_breadcrumb
|
|
126
|
+
_seed_governor_event
|
|
127
|
+
_run_hook
|
|
128
|
+
run grep -c '"event_type":"bursar.session.recorded"' "$ONLOOKER_EVENTS_LOG"
|
|
129
|
+
[ "$status" -eq 0 ]
|
|
130
|
+
[ "$output" -ge 1 ]
|
|
131
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Exercises the SessionStart hook end-to-end against an isolated $ONLOOKER_DIR.
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
7
|
+
setup_test_env
|
|
8
|
+
|
|
9
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
10
|
+
HOOK="${PLUGIN_ROOT}/scripts/hooks/bursar-session-start.sh"
|
|
11
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
12
|
+
export ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
13
|
+
mkdir -p "$(dirname "$ONLOOKER_EVENTS_LOG")"
|
|
14
|
+
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
source "${PLUGIN_ROOT}/scripts/lib/bursar-project-key.sh"
|
|
17
|
+
|
|
18
|
+
# A real git repo so a project key resolves.
|
|
19
|
+
REPO="${BATS_TEST_TMPDIR}/proj"
|
|
20
|
+
mkdir -p "$REPO"
|
|
21
|
+
git init -q "$REPO" 2>/dev/null
|
|
22
|
+
git -C "$REPO" remote add origin https://example.com/onlooker/bursar-test.git 2>/dev/null
|
|
23
|
+
KEY=$(bursar_project_key "$REPO")
|
|
24
|
+
|
|
25
|
+
SID="bats-start-001"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_enable() {
|
|
29
|
+
mkdir -p "${HOME}/.claude"
|
|
30
|
+
printf '%s\n' '{"bursar":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_seed_ledger() {
|
|
34
|
+
# One recent recorded session with cost.
|
|
35
|
+
local dir="${ONLOOKER_DIR}/bursar/projects/${KEY}"
|
|
36
|
+
mkdir -p "$dir"
|
|
37
|
+
local now
|
|
38
|
+
now=$(date +%s)
|
|
39
|
+
jq -nc --arg pk "$KEY" --argjson te "$now" \
|
|
40
|
+
'{ts:"x", ts_epoch:$te, session_id:"older", project_key:$pk,
|
|
41
|
+
governor_present:true, cost_usd:0.42, tokens:42000, api_calls:12}' \
|
|
42
|
+
> "${dir}/sessions.jsonl"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_run_hook() {
|
|
46
|
+
printf '%s' "{\"session_id\":\"$SID\",\"cwd\":\"$REPO\",\"source\":\"startup\"}" \
|
|
47
|
+
> "${BATS_TEST_TMPDIR}/in.json"
|
|
48
|
+
run bash "$HOOK" < "${BATS_TEST_TMPDIR}/in.json"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@test "produces no output when bursar is disabled" {
|
|
52
|
+
_run_hook
|
|
53
|
+
[ "$status" -eq 0 ]
|
|
54
|
+
[ -z "$output" ]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@test "writes a breadcrumb carrying the project key" {
|
|
58
|
+
_enable
|
|
59
|
+
_run_hook
|
|
60
|
+
[ "$status" -eq 0 ]
|
|
61
|
+
local bc="${ONLOOKER_DIR}/bursar/sessions/${SID}.json"
|
|
62
|
+
[ -f "$bc" ]
|
|
63
|
+
[ "$(jq -r '.project_key' "$bc")" = "$KEY" ]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@test "surfaces no additionalContext when the window is empty" {
|
|
67
|
+
_enable
|
|
68
|
+
_run_hook
|
|
69
|
+
[ "$status" -eq 0 ]
|
|
70
|
+
[[ "$output" != *"hookSpecificOutput"* ]]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "emits bursar.rollup.skipped when there is no data" {
|
|
74
|
+
_enable
|
|
75
|
+
_run_hook
|
|
76
|
+
run grep -c '"event_type":"bursar.rollup.skipped"' "$ONLOOKER_EVENTS_LOG"
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
[ "$output" -ge 1 ]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "surfaces the burned total as SessionStart additionalContext" {
|
|
82
|
+
_enable
|
|
83
|
+
_seed_ledger
|
|
84
|
+
_run_hook
|
|
85
|
+
[ "$status" -eq 0 ]
|
|
86
|
+
[[ "$output" == *"hookSpecificOutput"* ]]
|
|
87
|
+
[[ "$output" == *"SessionStart"* ]]
|
|
88
|
+
[[ "$output" == *"burned \$0.42"* ]]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test "reports a tracked \$0.00 total without nudging to enable governor" {
|
|
92
|
+
_enable
|
|
93
|
+
# governor present, but the window's cost is legitimately zero
|
|
94
|
+
local dir="${ONLOOKER_DIR}/bursar/projects/${KEY}"
|
|
95
|
+
mkdir -p "$dir"
|
|
96
|
+
local now
|
|
97
|
+
now=$(date +%s)
|
|
98
|
+
jq -nc --arg pk "$KEY" --argjson te "$now" \
|
|
99
|
+
'{ts:"x", ts_epoch:$te, session_id:"zero", project_key:$pk,
|
|
100
|
+
governor_present:true, cost_usd:0, tokens:0, api_calls:0}' \
|
|
101
|
+
> "${dir}/sessions.jsonl"
|
|
102
|
+
_run_hook
|
|
103
|
+
[ "$status" -eq 0 ]
|
|
104
|
+
[[ "$output" == *"burned \$0.00"* ]]
|
|
105
|
+
[[ "$output" != *"Enable governor"* ]]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@test "emits bursar.rollup.surfaced when data exists" {
|
|
109
|
+
_enable
|
|
110
|
+
_seed_ledger
|
|
111
|
+
_run_hook
|
|
112
|
+
run grep -c '"event_type":"bursar.rollup.surfaced"' "$ONLOOKER_EVENTS_LOG"
|
|
113
|
+
[ "$status" -eq 0 ]
|
|
114
|
+
[ "$output" -ge 1 ]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@test "stays silent at the surface step but still records the breadcrumb when surfacing is disabled" {
|
|
118
|
+
mkdir -p "${HOME}/.claude"
|
|
119
|
+
printf '%s\n' '{"bursar":{"enabled":true,"surface_at_session_start":false}}' \
|
|
120
|
+
> "${HOME}/.claude/settings.json"
|
|
121
|
+
_seed_ledger
|
|
122
|
+
_run_hook
|
|
123
|
+
[ "$status" -eq 0 ]
|
|
124
|
+
[ -z "$output" ]
|
|
125
|
+
[ -f "${ONLOOKER_DIR}/bursar/sessions/${SID}.json" ]
|
|
126
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
setup() {
|
|
4
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
5
|
+
setup_test_env
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/bursar"
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
source "${PLUGIN_ROOT}/scripts/lib/bursar-ulid.sh"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@test "bursar_ulid is 26 characters" {
|
|
13
|
+
run bursar_ulid
|
|
14
|
+
[ "$status" -eq 0 ]
|
|
15
|
+
[ "${#output}" -eq 26 ]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@test "bursar_ulid uses only Crockford base32 (no I, L, O, U)" {
|
|
19
|
+
run bursar_ulid
|
|
20
|
+
[[ "$output" =~ ^[0-9A-HJKMNP-TV-Z]+$ ]]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@test "two ulids differ" {
|
|
24
|
+
local a b
|
|
25
|
+
a=$(bursar_ulid)
|
|
26
|
+
b=$(bursar_ulid)
|
|
27
|
+
[ "$a" != "$b" ]
|
|
28
|
+
}
|
|
@@ -114,6 +114,34 @@ setup() {
|
|
|
114
114
|
[ "$output" = "3" ]
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
@test "read_events survives caller pipefail when output exceeds chars_max" {
|
|
118
|
+
# Regression: head -c closes the pipe once chars_max bytes arrive, sending jq
|
|
119
|
+
# SIGPIPE. Under the caller's `set -o pipefail` (as the hook and skill run),
|
|
120
|
+
# the reader must still return the truncated output, not discard everything.
|
|
121
|
+
local log="${BATS_TEST_TMPDIR}/big-events.jsonl"
|
|
122
|
+
local ts
|
|
123
|
+
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null) || ts="2099-01-01T00:00:00Z"
|
|
124
|
+
: > "$log"
|
|
125
|
+
local i
|
|
126
|
+
for ((i = 0; i < 60; i++)); do
|
|
127
|
+
printf '%s\n' \
|
|
128
|
+
"{\"event_type\":\"tribunal.gate.blocked\",\"timestamp\":\"${ts}\",\"session_id\":\"s${i}\",\"payload\":{\"k\":\"vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\"}}" \
|
|
129
|
+
>> "$log"
|
|
130
|
+
done
|
|
131
|
+
export ONLOOKER_EVENTS_LOG="$log"
|
|
132
|
+
|
|
133
|
+
# Tiny cap so head closes the pipe after the first event or two.
|
|
134
|
+
local out
|
|
135
|
+
set -o pipefail
|
|
136
|
+
out=$(counsel_read_events "30" "200")
|
|
137
|
+
set +o pipefail
|
|
138
|
+
|
|
139
|
+
[ -n "$out" ]
|
|
140
|
+
run counsel_count_events "$out"
|
|
141
|
+
[ "$status" -eq 0 ]
|
|
142
|
+
[ "$output" -ge 1 ]
|
|
143
|
+
}
|
|
144
|
+
|
|
117
145
|
@test "read_events output preserves source types for sources_from_events" {
|
|
118
146
|
local log="${BATS_TEST_TMPDIR}/source-events.jsonl"
|
|
119
147
|
local ts
|