@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.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +26 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.release-please-manifest.json +4 -2
  4. package/CHANGELOG.md +14 -0
  5. package/CLAUDE.md +4 -0
  6. package/docs/architecture.md +8 -0
  7. package/package.json +3 -3
  8. package/plugins/bursar/.claude-plugin/plugin.json +14 -0
  9. package/plugins/bursar/CHANGELOG.md +10 -0
  10. package/plugins/bursar/README.md +100 -0
  11. package/plugins/bursar/config.json +11 -0
  12. package/plugins/bursar/hooks/hooks.json +26 -0
  13. package/plugins/bursar/scripts/hooks/bursar-session-end.sh +142 -0
  14. package/plugins/bursar/scripts/hooks/bursar-session-start.sh +130 -0
  15. package/plugins/bursar/scripts/lib/bursar-config.sh +108 -0
  16. package/plugins/bursar/scripts/lib/bursar-events.sh +82 -0
  17. package/plugins/bursar/scripts/lib/bursar-ledger.sh +152 -0
  18. package/plugins/bursar/scripts/lib/bursar-project-key.sh +85 -0
  19. package/plugins/bursar/scripts/lib/bursar-ulid.sh +53 -0
  20. package/plugins/bursar/scripts/lib/portable-lock.sh +59 -0
  21. package/plugins/lineage/.claude-plugin/plugin.json +14 -0
  22. package/plugins/lineage/CHANGELOG.md +9 -0
  23. package/plugins/lineage/README.md +133 -0
  24. package/plugins/lineage/config.json +11 -0
  25. package/plugins/lineage/hooks/hooks.json +33 -0
  26. package/plugins/lineage/scripts/hooks/lineage-post-tool-use.sh +132 -0
  27. package/plugins/lineage/scripts/lib/lineage-config.sh +100 -0
  28. package/plugins/lineage/scripts/lib/lineage-events.sh +81 -0
  29. package/plugins/lineage/scripts/lib/lineage-project-key.sh +85 -0
  30. package/plugins/lineage/scripts/lib/lineage-query.sh +88 -0
  31. package/plugins/lineage/scripts/lib/lineage-record.sh +132 -0
  32. package/plugins/lineage/scripts/lib/lineage-redact.sh +51 -0
  33. package/plugins/lineage/scripts/lib/lineage-ulid.sh +53 -0
  34. package/plugins/lineage/scripts/lib/portable-lock.sh +59 -0
  35. package/plugins/lineage/skills/lineage/SKILL.md +165 -0
  36. package/release-please-config.json +32 -0
  37. package/test/bats/bursar-config.bats +79 -0
  38. package/test/bats/bursar-events.bats +73 -0
  39. package/test/bats/bursar-ledger.bats +116 -0
  40. package/test/bats/bursar-project-key.bats +51 -0
  41. package/test/bats/bursar-session-end.bats +131 -0
  42. package/test/bats/bursar-session-start.bats +126 -0
  43. package/test/bats/bursar-ulid.bats +28 -0
  44. package/test/bats/lineage-config.bats +73 -0
  45. package/test/bats/lineage-events.bats +81 -0
  46. package/test/bats/lineage-post-tool-use.bats +115 -0
  47. package/test/bats/lineage-project-key.bats +51 -0
  48. package/test/bats/lineage-query.bats +85 -0
  49. package/test/bats/lineage-record.bats +79 -0
  50. package/test/bats/lineage-redact.bats +63 -0
  51. 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
+ }