@onlooker-community/ecosystem 0.26.1 → 0.28.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 +26 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +4 -2
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +4 -0
- package/docs/architecture.md +8 -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/lineage/.claude-plugin/plugin.json +14 -0
- package/plugins/lineage/CHANGELOG.md +9 -0
- package/plugins/lineage/README.md +133 -0
- package/plugins/lineage/config.json +11 -0
- package/plugins/lineage/hooks/hooks.json +33 -0
- package/plugins/lineage/scripts/hooks/lineage-post-tool-use.sh +132 -0
- package/plugins/lineage/scripts/lib/lineage-config.sh +100 -0
- package/plugins/lineage/scripts/lib/lineage-events.sh +81 -0
- package/plugins/lineage/scripts/lib/lineage-project-key.sh +85 -0
- package/plugins/lineage/scripts/lib/lineage-query.sh +88 -0
- package/plugins/lineage/scripts/lib/lineage-record.sh +132 -0
- package/plugins/lineage/scripts/lib/lineage-redact.sh +51 -0
- package/plugins/lineage/scripts/lib/lineage-ulid.sh +53 -0
- package/plugins/lineage/scripts/lib/portable-lock.sh +59 -0
- package/plugins/lineage/skills/lineage/SKILL.md +165 -0
- package/release-please-config.json +32 -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/lineage-config.bats +73 -0
- package/test/bats/lineage-events.bats +81 -0
- package/test/bats/lineage-post-tool-use.bats +115 -0
- package/test/bats/lineage-project-key.bats +51 -0
- package/test/bats/lineage-query.bats +85 -0
- package/test/bats/lineage-record.bats +79 -0
- package/test/bats/lineage-redact.bats +63 -0
- package/test/bats/lineage-ulid.bats +28 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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/lineage"
|
|
8
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
9
|
+
# shellcheck disable=SC1091
|
|
10
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-config.sh"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@test "lineage is disabled by default" {
|
|
14
|
+
lineage_config_load ""
|
|
15
|
+
run lineage_config_enabled
|
|
16
|
+
[ "$status" -ne 0 ]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@test "user-level settings.json can enable lineage" {
|
|
20
|
+
mkdir -p "${HOME}/.claude"
|
|
21
|
+
printf '%s\n' '{"lineage":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
22
|
+
lineage_config_load ""
|
|
23
|
+
run lineage_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' '{"lineage":{"enabled":true}}' > "${HOME}/.claude/settings.json"
|
|
30
|
+
local repo="${BATS_TEST_TMPDIR}/repo"
|
|
31
|
+
mkdir -p "${repo}/.claude"
|
|
32
|
+
printf '%s\n' '{"lineage":{"enabled":false}}' > "${repo}/.claude/settings.json"
|
|
33
|
+
lineage_config_load "$repo"
|
|
34
|
+
run lineage_config_enabled
|
|
35
|
+
[ "$status" -ne 0 ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "default max_snippet_chars is 4000" {
|
|
39
|
+
lineage_config_load ""
|
|
40
|
+
[ "$(lineage_config_max_snippet_chars)" = "4000" ]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "max_snippet_chars is configurable" {
|
|
44
|
+
mkdir -p "${HOME}/.claude"
|
|
45
|
+
printf '%s\n' '{"lineage":{"max_snippet_chars":256}}' > "${HOME}/.claude/settings.json"
|
|
46
|
+
lineage_config_load ""
|
|
47
|
+
[ "$(lineage_config_max_snippet_chars)" = "256" ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@test "redaction is on by default and can be disabled with an explicit false" {
|
|
51
|
+
lineage_config_load ""
|
|
52
|
+
run lineage_config_redact_enabled
|
|
53
|
+
[ "$status" -eq 0 ]
|
|
54
|
+
|
|
55
|
+
mkdir -p "${HOME}/.claude"
|
|
56
|
+
printf '%s\n' '{"lineage":{"redact_secrets":false}}' > "${HOME}/.claude/settings.json"
|
|
57
|
+
lineage_config_load ""
|
|
58
|
+
run lineage_config_redact_enabled
|
|
59
|
+
[ "$status" -ne 0 ]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test "default prompt_source is historian_then_transcript" {
|
|
63
|
+
lineage_config_load ""
|
|
64
|
+
[ "$(lineage_config_prompt_source)" = "historian_then_transcript" ]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test "ignore_globs are exposed one per line" {
|
|
68
|
+
lineage_config_load ""
|
|
69
|
+
run lineage_config_ignore_globs
|
|
70
|
+
[ "$status" -eq 0 ]
|
|
71
|
+
[[ "$output" == *"node_modules"* ]]
|
|
72
|
+
[[ "$output" == *".lock"* ]]
|
|
73
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Validates that lineage.* 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/lineage"
|
|
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
|
+
export _ONLOOKER_EVENT_JS="${REPO_ROOT}/scripts/lib/onlooker-event.mjs"
|
|
14
|
+
|
|
15
|
+
# shellcheck disable=SC1091
|
|
16
|
+
source "${PLUGIN_ROOT}/scripts/lib/lineage-events.sh"
|
|
17
|
+
|
|
18
|
+
export CLAUDE_SESSION_ID="bats-lineage-session-$$"
|
|
19
|
+
PK="proj0123abcd"
|
|
20
|
+
SID="bats-lineage-sid-000"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_validate_latest_event() {
|
|
24
|
+
local last
|
|
25
|
+
last=$(tail -n 1 "$ONLOOKER_EVENTS_LOG")
|
|
26
|
+
[ -n "$last" ] || return 1
|
|
27
|
+
printf '%s' "$last" | ONLOOKER_DIR="$ONLOOKER_DIR" \
|
|
28
|
+
node "${REPO_ROOT}/scripts/lib/onlooker-event.mjs" validate >/dev/null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "lineage.change.recorded validates (full payload)" {
|
|
32
|
+
local p
|
|
33
|
+
p=$(jq -n --arg pk "$PK" --arg sid "$SID" \
|
|
34
|
+
'{project_key:$pk, session_id:$sid, file_path:"src/main.ts", tool:"Edit",
|
|
35
|
+
operation:"edit", change_id:"01JLNG0000000000000000CHG1", turn:4,
|
|
36
|
+
tool_use_id:"toolu_1", lines_added:3, lines_removed:1, bytes:142,
|
|
37
|
+
edit_count:1, content_sha256:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}')
|
|
38
|
+
lineage_emit_event "lineage.change.recorded" "$p" "$SID"
|
|
39
|
+
run _validate_latest_event
|
|
40
|
+
[ "$status" -eq 0 ]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "lineage.change.recorded validates (minimal Write)" {
|
|
44
|
+
local p
|
|
45
|
+
p=$(jq -n --arg pk "$PK" --arg sid "$SID" \
|
|
46
|
+
'{project_key:$pk, session_id:$sid, file_path:"README.md", tool:"Write", operation:"create"}')
|
|
47
|
+
lineage_emit_event "lineage.change.recorded" "$p" "$SID"
|
|
48
|
+
run _validate_latest_event
|
|
49
|
+
[ "$status" -eq 0 ]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@test "lineage.query.answered validates" {
|
|
53
|
+
local p
|
|
54
|
+
p=$(jq -n --arg pk "$PK" \
|
|
55
|
+
'{project_key:$pk, file_path:"src/main.ts", matches:2, query:"src/main.ts:42", line:42, resolved_via:"historian"}')
|
|
56
|
+
lineage_emit_event "lineage.query.answered" "$p" "$SID"
|
|
57
|
+
run _validate_latest_event
|
|
58
|
+
[ "$status" -eq 0 ]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "lineage.query.answered validates with no matches" {
|
|
62
|
+
local p
|
|
63
|
+
p=$(jq -n --arg pk "$PK" \
|
|
64
|
+
'{project_key:$pk, file_path:"src/gone.ts", matches:0, resolved_via:"none"}')
|
|
65
|
+
lineage_emit_event "lineage.query.answered" "$p" "$SID"
|
|
66
|
+
run _validate_latest_event
|
|
67
|
+
[ "$status" -eq 0 ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@test "an invalid tool enum is rejected by the schema" {
|
|
71
|
+
local p
|
|
72
|
+
p=$(jq -n --arg pk "$PK" --arg sid "$SID" \
|
|
73
|
+
'{project_key:$pk, session_id:$sid, file_path:"x", tool:"NotebookEdit", operation:"edit"}')
|
|
74
|
+
run lineage_emit_event "lineage.change.recorded" "$p" "$SID"
|
|
75
|
+
[ "$status" -ne 0 ]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@test "lineage_emit_event returns nonzero for an unknown event type" {
|
|
79
|
+
run lineage_emit_event "lineage.no_such_event" '{"project_key":"x"}' "$SID"
|
|
80
|
+
[ "$status" -ne 0 ]
|
|
81
|
+
}
|