#!/bin/bash source "$(dirname "$0")/base-test.sh" require_command jq require_command python3 # Every fixture home lives under one scratch directory, cleaned up at exit. SCRATCH=$(mktemp -d) trap 'rm -rf "$SCRATCH"' EXIT TEST_HOME=$(mktemp -d "$SCRATCH/home.XXXXXX") projects="$TEST_HOME/.claude/projects/example" mkdir -p "$projects" timestamp="$(date +%Y-%m-%d)T12:00:00Z" cat >"$projects/session.jsonl" <>"$projects/session.jsonl" </dev/null | head -1) [[ -n $index && $(jq -r '.files | to_entries[0].value.records | length' "$index") == "3" ]] || fail "Claude collector keeps one record per API message in the scan index" "$(cat "$index" 2>/dev/null)" pass "Claude collector keeps one record per API message in the scan index" # An unchanged file is not opened again: its records come from the index. chmod 000 "$projects/session.jsonl" result=$(HOME="$TEST_HOME" XDG_CACHE_HOME="$TEST_HOME/.cache" XDG_DATA_HOME="$TEST_HOME/.local/share" \ "$ROOT/bin/omarchy-agent-usage-claude" --cache-seconds 0 2>/dev/null) chmod 644 "$projects/session.jsonl" [[ $(jq -r '.todayTotalTokens' <<<"$result") == "59000" ]] || fail "Claude collector serves unchanged transcripts from the scan index" "$result" pass "Claude collector serves unchanged transcripts from the scan index" # A file that shrank was rewritten, not appended to; it is read from the start. head -n 1 "$projects/session.jsonl" >"$projects/session.jsonl.new" mv "$projects/session.jsonl.new" "$projects/session.jsonl" result=$(HOME="$TEST_HOME" XDG_CACHE_HOME="$TEST_HOME/.cache" XDG_DATA_HOME="$TEST_HOME/.local/share" \ "$ROOT/bin/omarchy-agent-usage-claude" --cache-seconds 0) [[ $(jq -r '.todayTotalTokens' <<<"$result") == "29090" ]] || fail "Claude collector rescans a transcript that was rewritten" "$result" pass "Claude collector rescans a transcript that was rewritten" # A transcript replaced by a larger one, or rewritten in place with more than # it had, is a new file rather than an append: nothing of the old one stays, # and the new one is read from its start. INDEX_HOME=$(mktemp -d "$SCRATCH/home.XXXXXX") index_projects="$INDEX_HOME/.claude/projects/example" mkdir -p "$index_projects" index_line() { printf '{"timestamp":"%s","type":"assistant","sessionId":"s","message":{"id":"%s","role":"assistant","model":"claude-test","usage":{"input_tokens":%s,"output_tokens":0}}}\n' "$timestamp" "$1" "$2" } index_scan() { HOME="$INDEX_HOME" XDG_CACHE_HOME="$INDEX_HOME/.cache" XDG_DATA_HOME="$INDEX_HOME/.local/share" \ "$ROOT/bin/omarchy-agent-usage-claude" --cache-seconds 0 "$@" } index_line old-1 1000 >"$index_projects/session.jsonl" index_scan >/dev/null { index_line new-1 20; index_line new-2 30; } >"$index_projects/session.jsonl.new" mv "$index_projects/session.jsonl.new" "$index_projects/session.jsonl" [[ $(index_scan | jq -c '[.todayTotalTokens, .totalPrompts]') == '[50,2]' ]] || fail "Claude collector reads a transcript replaced by a larger one from its start" "$(index_scan)" { index_line other-1 7; index_line other-2 8; index_line other-3 9; } >"$index_projects/session.jsonl" [[ $(index_scan | jq -c '[.todayTotalTokens, .totalPrompts]') == '[24,3]' ]] || fail "Claude collector reads a transcript rewritten in place from its start" "$(index_scan)" pass "Claude collector rereads a transcript that was replaced or rewritten" # A rewrite in place that grows the file and changes only its end keeps the # first kilobytes, so the end of what was read is checked too. for i in $(seq 1 60); do index_line "long-$i" 100; done >"$index_projects/session.jsonl" index_scan >/dev/null { for i in $(seq 1 59); do index_line "long-$i" 100; done; index_line long-60 5000; index_line long-61 1; } >"$index_projects/session.jsonl" [[ $(index_scan | jq -r '.todayTotalTokens') == "10901" ]] || fail "Claude collector rereads a transcript whose end was rewritten in place" "$(index_scan)" pass "Claude collector rereads a transcript whose end was rewritten in place" # A larger replacement that matches both ends of what was read but differs in # between is still a different file, so it is read from its start. for i in $(seq 1 200); do index_line "mid-$i" 100; done >"$index_projects/session.jsonl" index_scan >/dev/null { for i in $(seq 1 200); do if (( i == 100 )); then index_line "mid-$i" 900; else index_line "mid-$i" 100; fi; done; index_line mid-201 1; } >"$index_projects/session.jsonl.new" mv "$index_projects/session.jsonl.new" "$index_projects/session.jsonl" [[ $(index_scan | jq -r '.todayTotalTokens') == "20801" ]] || fail "Claude collector rereads a replacement whose ends match" "$(index_scan)" pass "Claude collector rereads a replacement whose ends match" # The index holds local days, so a new timezone reads every transcript again # rather than keep the days another timezone gave them. zone_timestamp="$(date -u +%Y-%m-%d)T01:00:00Z" printf '{"timestamp":"%s","type":"assistant","sessionId":"s","message":{"id":"zone-1","role":"assistant","model":"claude-test","usage":{"input_tokens":5,"output_tokens":0}}}\n' "$zone_timestamp" >"$index_projects/session.jsonl" TZ=UTC index_scan >/dev/null zone_dates=$(TZ=America/Los_Angeles index_scan | jq -r '.activeDates | join(",")') [[ $zone_dates == "$(TZ=America/Los_Angeles date -d "$zone_timestamp" +%Y-%m-%d)" ]] || fail "Claude collector recomputes indexed days after a timezone change" "$zone_dates" pass "Claude collector recomputes indexed days after a timezone change" # A streamed response is several lines sharing one message id. The first # line's output_tokens is a placeholder and the last line has the real count, # so the message is counted from the line with the highest output. STREAM_HOME=$(mktemp -d "$SCRATCH/home.XXXXXX") stream_projects="$STREAM_HOME/.claude/projects/example" mkdir -p "$stream_projects" cat >"$stream_projects/session.jsonl" <>"$stream_projects/session.jsonl" <>"$projects/session.jsonl" <"$HISTORY_HOME/.claude/history.jsonl" <"$PI_HOME/.pi/agent/sessions/project/pi.jsonl" <"$PI_HOME/.omp/agent/sessions/project/omp.jsonl" <` moves the whole agent tree under profiles//, so a # subscription spent entirely through a profile leaves the default root empty. cat >"$PI_HOME/.omp/profiles/work/agent/sessions/project/omp-profile.jsonl" <