@onlooker-community/ecosystem 0.26.1 → 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.
@@ -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
+ }