cc-context-stats 1.7.0 → 1.8.1
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/package.json +9 -1
- package/scripts/context-stats.sh +1 -1
- package/scripts/statusline.js +128 -18
- package/.editorconfig +0 -60
- package/.eslintrc.json +0 -35
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
- package/.github/dependabot.yml +0 -44
- package/.github/workflows/ci.yml +0 -294
- package/.github/workflows/release.yml +0 -151
- package/.pre-commit-config.yaml +0 -74
- package/.prettierrc +0 -33
- package/.shellcheckrc +0 -10
- package/CHANGELOG.md +0 -163
- package/CLAUDE.md +0 -66
- package/CODE_OF_CONDUCT.md +0 -59
- package/CONTRIBUTING.md +0 -240
- package/RELEASE_NOTES.md +0 -19
- package/SECURITY.md +0 -44
- package/TODOS.md +0 -72
- package/assets/logo/favicon.svg +0 -19
- package/assets/logo/logo-black.svg +0 -24
- package/assets/logo/logo-full.svg +0 -40
- package/assets/logo/logo-icon.svg +0 -27
- package/assets/logo/logo-mark.svg +0 -28
- package/assets/logo/logo-white.svg +0 -24
- package/assets/logo/logo-wordmark.svg +0 -6
- package/config/settings-example.json +0 -7
- package/config/settings-node.json +0 -7
- package/config/settings-python.json +0 -7
- package/docs/ARCHITECTURE.md +0 -128
- package/docs/CSV_FORMAT.md +0 -42
- package/docs/DEPLOYMENT.md +0 -71
- package/docs/DEVELOPMENT.md +0 -161
- package/docs/configuration.md +0 -118
- package/docs/context-stats.md +0 -143
- package/docs/installation.md +0 -255
- package/docs/scripts.md +0 -140
- package/docs/troubleshooting.md +0 -278
- package/images/claude-statusline-token-graph.gif +0 -0
- package/images/claude-statusline.png +0 -0
- package/images/context-status-dumbzone.png +0 -0
- package/images/context-status.png +0 -0
- package/images/statusline-detail.png +0 -0
- package/images/token-graph.jpeg +0 -0
- package/images/token-graph.png +0 -0
- package/images/v1.6.1.png +0 -0
- package/install +0 -351
- package/install.sh +0 -298
- package/jest.config.js +0 -11
- package/pyproject.toml +0 -115
- package/requirements-dev.txt +0 -12
- package/scripts/statusline-full.sh +0 -304
- package/scripts/statusline-git.sh +0 -88
- package/scripts/statusline-minimal.sh +0 -67
- package/scripts/statusline.py +0 -485
- package/src/claude_statusline/__init__.py +0 -11
- package/src/claude_statusline/__main__.py +0 -6
- package/src/claude_statusline/cli/__init__.py +0 -1
- package/src/claude_statusline/cli/context_stats.py +0 -512
- package/src/claude_statusline/cli/explain.py +0 -228
- package/src/claude_statusline/cli/statusline.py +0 -169
- package/src/claude_statusline/core/__init__.py +0 -1
- package/src/claude_statusline/core/colors.py +0 -124
- package/src/claude_statusline/core/config.py +0 -148
- package/src/claude_statusline/core/git.py +0 -78
- package/src/claude_statusline/core/state.py +0 -323
- package/src/claude_statusline/formatters/__init__.py +0 -1
- package/src/claude_statusline/formatters/layout.py +0 -67
- package/src/claude_statusline/formatters/time.py +0 -50
- package/src/claude_statusline/formatters/tokens.py +0 -70
- package/src/claude_statusline/graphs/__init__.py +0 -1
- package/src/claude_statusline/graphs/renderer.py +0 -366
- package/src/claude_statusline/graphs/statistics.py +0 -92
- package/src/claude_statusline/ui/__init__.py +0 -1
- package/src/claude_statusline/ui/icons.py +0 -93
- package/src/claude_statusline/ui/waiting.py +0 -62
- package/tests/bash/test_delta_parity.bats +0 -199
- package/tests/bash/test_install.bats +0 -29
- package/tests/bash/test_parity.bats +0 -315
- package/tests/bash/test_statusline_full.bats +0 -139
- package/tests/bash/test_statusline_git.bats +0 -42
- package/tests/bash/test_statusline_minimal.bats +0 -37
- package/tests/fixtures/json/comma_in_path.json +0 -31
- package/tests/fixtures/json/high_usage.json +0 -17
- package/tests/fixtures/json/low_usage.json +0 -17
- package/tests/fixtures/json/medium_usage.json +0 -17
- package/tests/fixtures/json/valid_full.json +0 -30
- package/tests/fixtures/json/valid_minimal.json +0 -9
- package/tests/node/rotation.test.js +0 -89
- package/tests/node/statusline.test.js +0 -240
- package/tests/python/conftest.py +0 -84
- package/tests/python/test_colors.py +0 -105
- package/tests/python/test_config_colors.py +0 -78
- package/tests/python/test_data_pipeline.py +0 -446
- package/tests/python/test_explain.py +0 -177
- package/tests/python/test_icons.py +0 -152
- package/tests/python/test_layout.py +0 -127
- package/tests/python/test_state_rotation_validation.py +0 -232
- package/tests/python/test_statusline.py +0 -215
- package/tests/python/test_waiting.py +0 -127
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Delta calculation parity tests: Python vs Node.js statusline scripts
|
|
4
|
-
# Verifies both implementations compute identical deltas from identical state.
|
|
5
|
-
|
|
6
|
-
strip_ansi() {
|
|
7
|
-
printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g'
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
setup() {
|
|
11
|
-
PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
|
12
|
-
PYTHON_SCRIPT="$PROJECT_ROOT/scripts/statusline.py"
|
|
13
|
-
NODE_SCRIPT="$PROJECT_ROOT/scripts/statusline.js"
|
|
14
|
-
|
|
15
|
-
# Create isolated temp HOME so state files don't pollute real ~/.claude/
|
|
16
|
-
TEST_HOME=$(mktemp -d)
|
|
17
|
-
export HOME="$TEST_HOME"
|
|
18
|
-
|
|
19
|
-
# Normalize terminal width for deterministic output
|
|
20
|
-
export COLUMNS=200
|
|
21
|
-
|
|
22
|
-
# Enable delta display
|
|
23
|
-
mkdir -p "$TEST_HOME/.claude"
|
|
24
|
-
echo "show_delta=true" > "$TEST_HOME/.claude/statusline.conf"
|
|
25
|
-
|
|
26
|
-
# Create a non-git temp working directory so both scripts skip git info
|
|
27
|
-
TEST_WORKDIR=$(mktemp -d)
|
|
28
|
-
cd "$TEST_WORKDIR"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
teardown() {
|
|
32
|
-
rm -rf "$TEST_HOME"
|
|
33
|
-
rm -rf "$TEST_WORKDIR"
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
# Helper: create a JSON payload with specific token values and session_id
|
|
37
|
-
make_payload() {
|
|
38
|
-
local session="$1" input_tokens="$2" cache_creation="$3" cache_read="$4"
|
|
39
|
-
cat <<EOF
|
|
40
|
-
{
|
|
41
|
-
"model": {"display_name": "Opus 4.5"},
|
|
42
|
-
"workspace": {"current_dir": "$TEST_WORKDIR", "project_dir": "$TEST_WORKDIR"},
|
|
43
|
-
"session_id": "$session",
|
|
44
|
-
"context_window": {
|
|
45
|
-
"context_window_size": 200000,
|
|
46
|
-
"current_usage": {
|
|
47
|
-
"input_tokens": $input_tokens,
|
|
48
|
-
"cache_creation_input_tokens": $cache_creation,
|
|
49
|
-
"cache_read_input_tokens": $cache_read
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
EOF
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# ============================================
|
|
57
|
-
# Delta Calculation Parity Tests
|
|
58
|
-
# ============================================
|
|
59
|
-
|
|
60
|
-
@test "delta parity: both scripts show identical delta after two sequential payloads" {
|
|
61
|
-
# First payload: 30k context usage (10k input + 10k cache_create + 10k cache_read)
|
|
62
|
-
local py_session="delta-parity-py"
|
|
63
|
-
local node_session="delta-parity-node"
|
|
64
|
-
|
|
65
|
-
local payload1_py=$(make_payload "$py_session" 10000 10000 10000)
|
|
66
|
-
local payload1_node=$(make_payload "$node_session" 10000 10000 10000)
|
|
67
|
-
|
|
68
|
-
# Run first payload (seeds state file, no delta shown)
|
|
69
|
-
echo "$payload1_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
70
|
-
echo "$payload1_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
71
|
-
|
|
72
|
-
# Second payload: 80k context usage (40k input + 20k cache_create + 20k cache_read)
|
|
73
|
-
# Expected delta = 80k - 30k = 50k
|
|
74
|
-
local payload2_py=$(make_payload "$py_session" 40000 20000 20000)
|
|
75
|
-
local payload2_node=$(make_payload "$node_session" 40000 20000 20000)
|
|
76
|
-
|
|
77
|
-
local py_output=$(echo "$payload2_py" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
|
|
78
|
-
local node_output=$(echo "$payload2_node" | node "$NODE_SCRIPT" 2>/dev/null)
|
|
79
|
-
|
|
80
|
-
local py_clean=$(strip_ansi "$py_output")
|
|
81
|
-
local node_clean=$(strip_ansi "$node_output")
|
|
82
|
-
|
|
83
|
-
# Both should contain [+50,000] delta
|
|
84
|
-
if [[ "$py_clean" != *"[+50,000]"* ]]; then
|
|
85
|
-
echo "Python output missing expected delta [+50,000]"
|
|
86
|
-
echo "Python output: $py_clean"
|
|
87
|
-
return 1
|
|
88
|
-
fi
|
|
89
|
-
if [[ "$node_clean" != *"[+50,000]"* ]]; then
|
|
90
|
-
echo "Node.js output missing expected delta [+50,000]"
|
|
91
|
-
echo "Node.js output: $node_clean"
|
|
92
|
-
return 1
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
# Compare outputs ignoring session_id suffix (which intentionally differs)
|
|
96
|
-
# Strip the trailing session ID from both outputs for comparison
|
|
97
|
-
local py_no_session=$(echo "$py_clean" | sed 's/ delta-parity-py$//')
|
|
98
|
-
local node_no_session=$(echo "$node_clean" | sed 's/ delta-parity-node$//')
|
|
99
|
-
|
|
100
|
-
if [ "$py_no_session" != "$node_no_session" ]; then
|
|
101
|
-
echo "DELTA PARITY MISMATCH (ignoring session_id)"
|
|
102
|
-
echo "Python: $py_no_session"
|
|
103
|
-
echo "Node.js: $node_no_session"
|
|
104
|
-
return 1
|
|
105
|
-
fi
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@test "delta parity: no delta shown on first run (no previous state)" {
|
|
109
|
-
local py_session="delta-first-py"
|
|
110
|
-
local node_session="delta-first-node"
|
|
111
|
-
|
|
112
|
-
local payload_py=$(make_payload "$py_session" 50000 10000 5000)
|
|
113
|
-
local payload_node=$(make_payload "$node_session" 50000 10000 5000)
|
|
114
|
-
|
|
115
|
-
local py_output=$(echo "$payload_py" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
|
|
116
|
-
local node_output=$(echo "$payload_node" | node "$NODE_SCRIPT" 2>/dev/null)
|
|
117
|
-
|
|
118
|
-
local py_clean=$(strip_ansi "$py_output")
|
|
119
|
-
local node_clean=$(strip_ansi "$node_output")
|
|
120
|
-
|
|
121
|
-
# Neither should show a delta on first run
|
|
122
|
-
if [[ "$py_clean" == *"[+"* ]]; then
|
|
123
|
-
echo "Python should not show delta on first run"
|
|
124
|
-
echo "Python output: $py_clean"
|
|
125
|
-
return 1
|
|
126
|
-
fi
|
|
127
|
-
if [[ "$node_clean" == *"[+"* ]]; then
|
|
128
|
-
echo "Node.js should not show delta on first run"
|
|
129
|
-
echo "Node.js output: $node_clean"
|
|
130
|
-
return 1
|
|
131
|
-
fi
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
@test "delta parity: no delta shown when tokens decrease (context reset)" {
|
|
135
|
-
local py_session="delta-decrease-py"
|
|
136
|
-
local node_session="delta-decrease-node"
|
|
137
|
-
|
|
138
|
-
# First payload: high usage
|
|
139
|
-
local payload1_py=$(make_payload "$py_session" 80000 20000 10000)
|
|
140
|
-
local payload1_node=$(make_payload "$node_session" 80000 20000 10000)
|
|
141
|
-
|
|
142
|
-
echo "$payload1_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
143
|
-
echo "$payload1_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
144
|
-
|
|
145
|
-
# Second payload: lower usage (context was reset/compacted)
|
|
146
|
-
local payload2_py=$(make_payload "$py_session" 20000 5000 5000)
|
|
147
|
-
local payload2_node=$(make_payload "$node_session" 20000 5000 5000)
|
|
148
|
-
|
|
149
|
-
local py_output=$(echo "$payload2_py" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
|
|
150
|
-
local node_output=$(echo "$payload2_node" | node "$NODE_SCRIPT" 2>/dev/null)
|
|
151
|
-
|
|
152
|
-
local py_clean=$(strip_ansi "$py_output")
|
|
153
|
-
local node_clean=$(strip_ansi "$node_output")
|
|
154
|
-
|
|
155
|
-
# Neither should show delta when tokens decrease
|
|
156
|
-
if [[ "$py_clean" == *"[+"* ]]; then
|
|
157
|
-
echo "Python should not show delta when tokens decrease"
|
|
158
|
-
echo "Python output: $py_clean"
|
|
159
|
-
return 1
|
|
160
|
-
fi
|
|
161
|
-
if [[ "$node_clean" == *"[+"* ]]; then
|
|
162
|
-
echo "Node.js should not show delta when tokens decrease"
|
|
163
|
-
echo "Node.js output: $node_clean"
|
|
164
|
-
return 1
|
|
165
|
-
fi
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
@test "delta parity: duplicate guard prevents writing when tokens unchanged" {
|
|
169
|
-
local py_session="delta-dedup-py"
|
|
170
|
-
local node_session="delta-dedup-node"
|
|
171
|
-
|
|
172
|
-
local payload_py=$(make_payload "$py_session" 50000 10000 5000)
|
|
173
|
-
local payload_node=$(make_payload "$node_session" 50000 10000 5000)
|
|
174
|
-
|
|
175
|
-
# Run same payload three times
|
|
176
|
-
echo "$payload_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
177
|
-
echo "$payload_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
178
|
-
echo "$payload_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
179
|
-
|
|
180
|
-
echo "$payload_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
181
|
-
echo "$payload_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
182
|
-
echo "$payload_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
183
|
-
|
|
184
|
-
local py_state="$TEST_HOME/.claude/statusline/statusline.${py_session}.state"
|
|
185
|
-
local node_state="$TEST_HOME/.claude/statusline/statusline.${node_session}.state"
|
|
186
|
-
|
|
187
|
-
# Both should have written only 1 line (duplicate guard)
|
|
188
|
-
local py_lines=$(wc -l < "$py_state" | tr -d ' ')
|
|
189
|
-
local node_lines=$(wc -l < "$node_state" | tr -d ' ')
|
|
190
|
-
|
|
191
|
-
if [ "$py_lines" -ne 1 ]; then
|
|
192
|
-
echo "Python wrote $py_lines lines (expected 1 — duplicate guard failed)"
|
|
193
|
-
return 1
|
|
194
|
-
fi
|
|
195
|
-
if [ "$node_lines" -ne 1 ]; then
|
|
196
|
-
echo "Node.js wrote $node_lines lines (expected 1 — duplicate guard failed)"
|
|
197
|
-
return 1
|
|
198
|
-
fi
|
|
199
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Test suite for install.sh
|
|
4
|
-
|
|
5
|
-
setup() {
|
|
6
|
-
PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
|
7
|
-
SCRIPT="$PROJECT_ROOT/install.sh"
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
@test "install.sh exists and is executable" {
|
|
11
|
-
[ -f "$SCRIPT" ]
|
|
12
|
-
[ -x "$SCRIPT" ]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@test "install.sh contains expected functions" {
|
|
16
|
-
grep -q "check_jq" "$SCRIPT"
|
|
17
|
-
grep -q "select_script" "$SCRIPT"
|
|
18
|
-
grep -q "ensure_claude_dir" "$SCRIPT"
|
|
19
|
-
grep -q "install_script" "$SCRIPT"
|
|
20
|
-
grep -q "update_settings" "$SCRIPT"
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@test "install.sh has correct shebang" {
|
|
24
|
-
head -1 "$SCRIPT" | grep -q "#!/bin/bash"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
@test "install.sh uses set -e for error handling" {
|
|
28
|
-
grep -q "set -e" "$SCRIPT"
|
|
29
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Cross-implementation parity tests: Python vs Node.js statusline scripts
|
|
4
|
-
# Ensures both implementations produce equivalent output for identical input.
|
|
5
|
-
|
|
6
|
-
strip_ansi() {
|
|
7
|
-
printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g'
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
setup() {
|
|
11
|
-
PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
|
12
|
-
PYTHON_SCRIPT="$PROJECT_ROOT/scripts/statusline.py"
|
|
13
|
-
NODE_SCRIPT="$PROJECT_ROOT/scripts/statusline.js"
|
|
14
|
-
FIXTURES="$PROJECT_ROOT/tests/fixtures/json"
|
|
15
|
-
|
|
16
|
-
# Create isolated temp HOME so state files don't pollute real ~/.claude/
|
|
17
|
-
TEST_HOME=$(mktemp -d)
|
|
18
|
-
export HOME="$TEST_HOME"
|
|
19
|
-
|
|
20
|
-
# Normalize terminal width for deterministic output
|
|
21
|
-
export COLUMNS=120
|
|
22
|
-
|
|
23
|
-
# Disable delta display to avoid cross-fixture state file interference
|
|
24
|
-
mkdir -p "$TEST_HOME/.claude"
|
|
25
|
-
echo "show_delta=false" > "$TEST_HOME/.claude/statusline.conf"
|
|
26
|
-
|
|
27
|
-
# Create a non-git temp working directory so both scripts skip git info
|
|
28
|
-
TEST_WORKDIR=$(mktemp -d)
|
|
29
|
-
cd "$TEST_WORKDIR"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
teardown() {
|
|
33
|
-
rm -rf "$TEST_HOME"
|
|
34
|
-
rm -rf "$TEST_WORKDIR"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# Helper: inject a session_id into a JSON fixture via Python
|
|
38
|
-
inject_session_py() {
|
|
39
|
-
local fixture="$1" session="$2"
|
|
40
|
-
python3 -c "
|
|
41
|
-
import json, sys
|
|
42
|
-
data = json.load(open('$fixture'))
|
|
43
|
-
data['session_id'] = '$session'
|
|
44
|
-
json.dump(data, sys.stdout)
|
|
45
|
-
"
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Helper: inject a session_id into a JSON fixture via Node
|
|
49
|
-
inject_session_node() {
|
|
50
|
-
local fixture="$1" session="$2"
|
|
51
|
-
node -e "
|
|
52
|
-
const fs = require('fs');
|
|
53
|
-
const data = JSON.parse(fs.readFileSync('$fixture', 'utf8'));
|
|
54
|
-
data.session_id = '$session';
|
|
55
|
-
process.stdout.write(JSON.stringify(data));
|
|
56
|
-
"
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
# ============================================
|
|
60
|
-
# Stdout Parity Tests
|
|
61
|
-
# ============================================
|
|
62
|
-
|
|
63
|
-
@test "stdout parity: Python and Node.js produce identical ANSI-stripped output for all fixtures" {
|
|
64
|
-
for fixture in "$FIXTURES"/*.json; do
|
|
65
|
-
fixture_name=$(basename "$fixture")
|
|
66
|
-
|
|
67
|
-
py_output=$(cat "$fixture" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
|
|
68
|
-
node_output=$(cat "$fixture" | node "$NODE_SCRIPT" 2>/dev/null)
|
|
69
|
-
|
|
70
|
-
py_clean=$(strip_ansi "$py_output")
|
|
71
|
-
node_clean=$(strip_ansi "$node_output")
|
|
72
|
-
|
|
73
|
-
if [ "$py_clean" != "$node_clean" ]; then
|
|
74
|
-
echo "STDOUT MISMATCH for fixture: $fixture_name"
|
|
75
|
-
echo "---"
|
|
76
|
-
echo "Python output: $py_clean"
|
|
77
|
-
echo "Node.js output: $node_clean"
|
|
78
|
-
echo "---"
|
|
79
|
-
return 1
|
|
80
|
-
fi
|
|
81
|
-
done
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# ============================================
|
|
85
|
-
# CSV State File Parity Tests
|
|
86
|
-
# ============================================
|
|
87
|
-
|
|
88
|
-
@test "CSV parity: both scripts write exactly 14 fields for all fixtures" {
|
|
89
|
-
for fixture in "$FIXTURES"/*.json; do
|
|
90
|
-
fixture_name=$(basename "$fixture" .json)
|
|
91
|
-
|
|
92
|
-
py_session="parity-py-${fixture_name}"
|
|
93
|
-
node_session="parity-node-${fixture_name}"
|
|
94
|
-
|
|
95
|
-
py_input=$(inject_session_py "$fixture" "$py_session")
|
|
96
|
-
node_input=$(inject_session_node "$fixture" "$node_session")
|
|
97
|
-
|
|
98
|
-
echo "$py_input" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
99
|
-
echo "$node_input" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
100
|
-
|
|
101
|
-
py_state_file="$TEST_HOME/.claude/statusline/statusline.${py_session}.state"
|
|
102
|
-
node_state_file="$TEST_HOME/.claude/statusline/statusline.${node_session}.state"
|
|
103
|
-
|
|
104
|
-
# Skip fixtures that don't produce state files (e.g., no context_window data)
|
|
105
|
-
if [ ! -f "$py_state_file" ] && [ ! -f "$node_state_file" ]; then
|
|
106
|
-
continue
|
|
107
|
-
fi
|
|
108
|
-
|
|
109
|
-
# If only one script wrote a state file, that's a parity failure
|
|
110
|
-
if [ ! -f "$py_state_file" ]; then
|
|
111
|
-
echo "PARITY ERROR for fixture: $fixture_name"
|
|
112
|
-
echo "Node.js wrote a state file but Python did not"
|
|
113
|
-
return 1
|
|
114
|
-
fi
|
|
115
|
-
if [ ! -f "$node_state_file" ]; then
|
|
116
|
-
echo "PARITY ERROR for fixture: $fixture_name"
|
|
117
|
-
echo "Python wrote a state file but Node.js did not"
|
|
118
|
-
return 1
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
|
-
# Read last line of each state file
|
|
122
|
-
py_line=$(tail -1 "$py_state_file")
|
|
123
|
-
node_line=$(tail -1 "$node_state_file")
|
|
124
|
-
|
|
125
|
-
# Count fields (comma-separated)
|
|
126
|
-
py_field_count=$(echo "$py_line" | awk -F',' '{print NF}')
|
|
127
|
-
node_field_count=$(echo "$node_line" | awk -F',' '{print NF}')
|
|
128
|
-
|
|
129
|
-
if [ "$py_field_count" -ne 14 ]; then
|
|
130
|
-
echo "FIELD COUNT ERROR for fixture: $fixture_name"
|
|
131
|
-
echo "Python state has $py_field_count fields (expected 14)"
|
|
132
|
-
echo "Python line: $py_line"
|
|
133
|
-
return 1
|
|
134
|
-
fi
|
|
135
|
-
if [ "$node_field_count" -ne 14 ]; then
|
|
136
|
-
echo "FIELD COUNT ERROR for fixture: $fixture_name"
|
|
137
|
-
echo "Node.js state has $node_field_count fields (expected 14)"
|
|
138
|
-
echo "Node.js line: $node_line"
|
|
139
|
-
return 1
|
|
140
|
-
fi
|
|
141
|
-
done
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@test "CSV parity: fields 1-13 match between Python and Node.js for all fixtures" {
|
|
145
|
-
# Field names for diagnostic output (index 0 = timestamp, 1-13 = data fields)
|
|
146
|
-
local field_names=(
|
|
147
|
-
"timestamp"
|
|
148
|
-
"total_input_tokens"
|
|
149
|
-
"total_output_tokens"
|
|
150
|
-
"current_usage_input_tokens"
|
|
151
|
-
"current_usage_output_tokens"
|
|
152
|
-
"current_usage_cache_creation"
|
|
153
|
-
"current_usage_cache_read"
|
|
154
|
-
"total_cost_usd"
|
|
155
|
-
"total_lines_added"
|
|
156
|
-
"total_lines_removed"
|
|
157
|
-
"session_id"
|
|
158
|
-
"model_id"
|
|
159
|
-
"workspace_project_dir"
|
|
160
|
-
"context_window_size"
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
for fixture in "$FIXTURES"/*.json; do
|
|
164
|
-
fixture_name=$(basename "$fixture" .json)
|
|
165
|
-
|
|
166
|
-
py_session="parity-fields-py-${fixture_name}"
|
|
167
|
-
node_session="parity-fields-node-${fixture_name}"
|
|
168
|
-
|
|
169
|
-
py_input=$(inject_session_py "$fixture" "$py_session")
|
|
170
|
-
node_input=$(inject_session_node "$fixture" "$node_session")
|
|
171
|
-
|
|
172
|
-
echo "$py_input" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
173
|
-
echo "$node_input" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
174
|
-
|
|
175
|
-
py_state_file="$TEST_HOME/.claude/statusline/statusline.${py_session}.state"
|
|
176
|
-
node_state_file="$TEST_HOME/.claude/statusline/statusline.${node_session}.state"
|
|
177
|
-
|
|
178
|
-
# Skip fixtures that don't produce state files
|
|
179
|
-
if [ ! -f "$py_state_file" ] && [ ! -f "$node_state_file" ]; then
|
|
180
|
-
continue
|
|
181
|
-
fi
|
|
182
|
-
|
|
183
|
-
[ -f "$py_state_file" ] || { echo "Python wrote no state file but Node did for $fixture_name"; return 1; }
|
|
184
|
-
[ -f "$node_state_file" ] || { echo "Node wrote no state file but Python did for $fixture_name"; return 1; }
|
|
185
|
-
|
|
186
|
-
py_line=$(tail -1 "$py_state_file")
|
|
187
|
-
node_line=$(tail -1 "$node_state_file")
|
|
188
|
-
|
|
189
|
-
# Compare fields 1-13 (skip timestamp at index 0, and skip session_id at index 10 since we set different ones)
|
|
190
|
-
local has_mismatch=0
|
|
191
|
-
for i in $(seq 1 13); do
|
|
192
|
-
# Skip field 10 (session_id) since we intentionally set different session IDs
|
|
193
|
-
if [ "$i" -eq 10 ]; then
|
|
194
|
-
continue
|
|
195
|
-
fi
|
|
196
|
-
|
|
197
|
-
py_field=$(echo "$py_line" | cut -d',' -f$((i + 1)))
|
|
198
|
-
node_field=$(echo "$node_line" | cut -d',' -f$((i + 1)))
|
|
199
|
-
|
|
200
|
-
if [ "$py_field" != "$node_field" ]; then
|
|
201
|
-
echo "FIELD MISMATCH for fixture: $fixture_name"
|
|
202
|
-
echo " Field $i (${field_names[$i]}): Python='$py_field' Node='$node_field'"
|
|
203
|
-
has_mismatch=1
|
|
204
|
-
fi
|
|
205
|
-
done
|
|
206
|
-
|
|
207
|
-
if [ "$has_mismatch" -eq 1 ]; then
|
|
208
|
-
echo "Full Python line: $py_line"
|
|
209
|
-
echo "Full Node.js line: $node_line"
|
|
210
|
-
return 1
|
|
211
|
-
fi
|
|
212
|
-
done
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
@test "CSV parity: comma in workspace_project_dir is sanitized to underscore" {
|
|
216
|
-
fixture="$FIXTURES/comma_in_path.json"
|
|
217
|
-
[ -f "$fixture" ] || skip "comma_in_path.json fixture missing"
|
|
218
|
-
|
|
219
|
-
# Enable show_delta so state files are written
|
|
220
|
-
echo "show_delta=true" > "$TEST_HOME/.claude/statusline.conf"
|
|
221
|
-
|
|
222
|
-
py_session="parity-comma-py"
|
|
223
|
-
node_session="parity-comma-node"
|
|
224
|
-
|
|
225
|
-
py_input=$(inject_session_py "$fixture" "$py_session")
|
|
226
|
-
node_input=$(inject_session_node "$fixture" "$node_session")
|
|
227
|
-
|
|
228
|
-
echo "$py_input" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
229
|
-
echo "$node_input" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
230
|
-
|
|
231
|
-
# Restore show_delta=false for subsequent tests
|
|
232
|
-
echo "show_delta=false" > "$TEST_HOME/.claude/statusline.conf"
|
|
233
|
-
|
|
234
|
-
py_state_file="$TEST_HOME/.claude/statusline/statusline.${py_session}.state"
|
|
235
|
-
node_state_file="$TEST_HOME/.claude/statusline/statusline.${node_session}.state"
|
|
236
|
-
|
|
237
|
-
[ -f "$py_state_file" ] || { echo "Python wrote no state file"; return 1; }
|
|
238
|
-
[ -f "$node_state_file" ] || { echo "Node wrote no state file"; return 1; }
|
|
239
|
-
|
|
240
|
-
# Extract workspace_project_dir (field 13, 1-indexed in cut)
|
|
241
|
-
py_dir=$(tail -1 "$py_state_file" | cut -d',' -f13)
|
|
242
|
-
node_dir=$(tail -1 "$node_state_file" | cut -d',' -f13)
|
|
243
|
-
|
|
244
|
-
# Both must have commas replaced with underscores
|
|
245
|
-
expected="/home/user/my_project_dir"
|
|
246
|
-
|
|
247
|
-
if [ "$py_dir" != "$expected" ]; then
|
|
248
|
-
echo "Python did not sanitize commas: got '$py_dir', expected '$expected'"
|
|
249
|
-
return 1
|
|
250
|
-
fi
|
|
251
|
-
if [ "$node_dir" != "$expected" ]; then
|
|
252
|
-
echo "Node.js did not sanitize commas: got '$node_dir', expected '$expected'"
|
|
253
|
-
return 1
|
|
254
|
-
fi
|
|
255
|
-
|
|
256
|
-
# Both must produce exactly 14 fields (commas didn't corrupt the CSV)
|
|
257
|
-
py_fields=$(tail -1 "$py_state_file" | awk -F',' '{print NF}')
|
|
258
|
-
node_fields=$(tail -1 "$node_state_file" | awk -F',' '{print NF}')
|
|
259
|
-
|
|
260
|
-
if [ "$py_fields" -ne 14 ]; then
|
|
261
|
-
echo "Python state has $py_fields fields (expected 14)"
|
|
262
|
-
return 1
|
|
263
|
-
fi
|
|
264
|
-
if [ "$node_fields" -ne 14 ]; then
|
|
265
|
-
echo "Node.js state has $node_fields fields (expected 14)"
|
|
266
|
-
return 1
|
|
267
|
-
fi
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
@test "CSV parity: timestamp differs by at most 2 seconds between Python and Node.js" {
|
|
271
|
-
for fixture in "$FIXTURES"/*.json; do
|
|
272
|
-
fixture_name=$(basename "$fixture" .json)
|
|
273
|
-
|
|
274
|
-
py_session="parity-ts-py-${fixture_name}"
|
|
275
|
-
node_session="parity-ts-node-${fixture_name}"
|
|
276
|
-
|
|
277
|
-
py_input=$(inject_session_py "$fixture" "$py_session")
|
|
278
|
-
node_input=$(inject_session_node "$fixture" "$node_session")
|
|
279
|
-
|
|
280
|
-
echo "$py_input" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
|
|
281
|
-
echo "$node_input" | node "$NODE_SCRIPT" > /dev/null 2>&1
|
|
282
|
-
|
|
283
|
-
py_state_file="$TEST_HOME/.claude/statusline/statusline.${py_session}.state"
|
|
284
|
-
node_state_file="$TEST_HOME/.claude/statusline/statusline.${node_session}.state"
|
|
285
|
-
|
|
286
|
-
# Skip fixtures that don't produce state files
|
|
287
|
-
if [ ! -f "$py_state_file" ] && [ ! -f "$node_state_file" ]; then
|
|
288
|
-
continue
|
|
289
|
-
fi
|
|
290
|
-
|
|
291
|
-
[ -f "$py_state_file" ] || { echo "Python wrote no state file but Node did for $fixture_name"; return 1; }
|
|
292
|
-
[ -f "$node_state_file" ] || { echo "Node wrote no state file but Python did for $fixture_name"; return 1; }
|
|
293
|
-
|
|
294
|
-
py_line=$(tail -1 "$py_state_file")
|
|
295
|
-
node_line=$(tail -1 "$node_state_file")
|
|
296
|
-
|
|
297
|
-
# Extract timestamp (field 0, which is field 1 in cut 1-indexed)
|
|
298
|
-
py_ts=$(echo "$py_line" | cut -d',' -f1)
|
|
299
|
-
node_ts=$(echo "$node_line" | cut -d',' -f1)
|
|
300
|
-
|
|
301
|
-
# Calculate absolute difference
|
|
302
|
-
if [ -n "$py_ts" ] && [ -n "$node_ts" ]; then
|
|
303
|
-
diff=$((py_ts - node_ts))
|
|
304
|
-
abs_diff=${diff#-}
|
|
305
|
-
|
|
306
|
-
if [ "$abs_diff" -gt 2 ]; then
|
|
307
|
-
echo "TIMESTAMP DRIFT for fixture: $fixture_name"
|
|
308
|
-
echo " Python timestamp: $py_ts"
|
|
309
|
-
echo " Node.js timestamp: $node_ts"
|
|
310
|
-
echo " Difference: ${abs_diff}s (max allowed: 2s)"
|
|
311
|
-
return 1
|
|
312
|
-
fi
|
|
313
|
-
fi
|
|
314
|
-
done
|
|
315
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bats
|
|
2
|
-
|
|
3
|
-
# Test suite for statusline-full.sh
|
|
4
|
-
|
|
5
|
-
setup() {
|
|
6
|
-
PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
|
7
|
-
SCRIPT="$PROJECT_ROOT/scripts/statusline-full.sh"
|
|
8
|
-
FIXTURES="$PROJECT_ROOT/tests/fixtures/json"
|
|
9
|
-
|
|
10
|
-
# Create a temp directory for config tests
|
|
11
|
-
TEST_HOME=$(mktemp -d)
|
|
12
|
-
export HOME="$TEST_HOME"
|
|
13
|
-
mkdir -p "$TEST_HOME/.claude"
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
teardown() {
|
|
17
|
-
rm -rf "$TEST_HOME"
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
@test "statusline-full.sh exists and is executable" {
|
|
21
|
-
[ -f "$SCRIPT" ]
|
|
22
|
-
[ -x "$SCRIPT" ]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@test "outputs model name from JSON input" {
|
|
26
|
-
input='{"model":{"display_name":"Opus 4.5"},"workspace":{"current_dir":"/tmp/test","project_dir":"/tmp/test"}}'
|
|
27
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
28
|
-
[[ "$result" == *"Opus 4.5"* ]]
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
@test "outputs directory name from path" {
|
|
32
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/home/user/myproject","project_dir":"/home/user/myproject"}}'
|
|
33
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
34
|
-
[[ "$result" == *"myproject"* ]]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@test "handles full valid input with context window" {
|
|
38
|
-
result=$(cat "$FIXTURES/valid_full.json" | "$SCRIPT")
|
|
39
|
-
[[ "$result" == *"Opus 4.5"* ]]
|
|
40
|
-
[[ "$result" == *"my-project"* ]]
|
|
41
|
-
[[ "$result" == *"free"* ]]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@test "shows AC indicator when autocompact enabled" {
|
|
45
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}'
|
|
46
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
47
|
-
[[ "$result" == *"[AC:"* ]]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
@test "shows AC:off when autocompact disabled in config" {
|
|
51
|
-
echo "autocompact=false" > "$TEST_HOME/.claude/statusline.conf"
|
|
52
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}'
|
|
53
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
54
|
-
[[ "$result" == *"[AC:off]"* ]]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
@test "shows exact tokens by default (token_detail=true)" {
|
|
58
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}'
|
|
59
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
60
|
-
# Should NOT show 'k' suffix by default, should show comma-formatted number
|
|
61
|
-
[[ "$result" != *"k free"* ]]
|
|
62
|
-
[[ "$result" == *"free"* ]]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@test "shows abbreviated tokens when token_detail=false" {
|
|
66
|
-
echo "token_detail=false" > "$TEST_HOME/.claude/statusline.conf"
|
|
67
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}'
|
|
68
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
69
|
-
# Should show 'k' suffix for abbreviated format
|
|
70
|
-
[[ "$result" == *"k free"* ]]
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@test "handles missing context window gracefully" {
|
|
74
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp/test","project_dir":"/tmp/test"}}'
|
|
75
|
-
run bash "$SCRIPT" <<< "$input"
|
|
76
|
-
[ "$status" -eq 0 ]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
@test "calculates free tokens percentage correctly" {
|
|
80
|
-
# Low usage fixture: 30k tokens used out of 200k = 85% free
|
|
81
|
-
result=$(cat "$FIXTURES/low_usage.json" | "$SCRIPT")
|
|
82
|
-
[[ "$result" == *"free"* ]]
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@test "uses fixture files correctly" {
|
|
86
|
-
for fixture in valid_full valid_minimal low_usage medium_usage high_usage; do
|
|
87
|
-
run bash "$SCRIPT" < "$FIXTURES/${fixture}.json"
|
|
88
|
-
[ "$status" -eq 0 ]
|
|
89
|
-
done
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
@test "shows session_id by default (show_session=true)" {
|
|
93
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"},"session_id":"test-session-123"}'
|
|
94
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
95
|
-
[[ "$result" == *"test-session-123"* ]]
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@test "hides session_id when show_session=false" {
|
|
99
|
-
echo "show_session=false" > "$TEST_HOME/.claude/statusline.conf"
|
|
100
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"},"session_id":"test-session-123"}'
|
|
101
|
-
result=$(echo "$input" | "$SCRIPT")
|
|
102
|
-
[[ "$result" != *"test-session-123"* ]]
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
@test "handles missing session_id gracefully" {
|
|
106
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp","project_dir":"/tmp"}}'
|
|
107
|
-
run bash "$SCRIPT" <<< "$input"
|
|
108
|
-
[ "$status" -eq 0 ]
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
# Width truncation tests
|
|
112
|
-
|
|
113
|
-
strip_ansi() {
|
|
114
|
-
printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g'
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
@test "output fits within 80 columns" {
|
|
118
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp/proj","project_dir":"/tmp/proj"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":10000,"cache_creation_input_tokens":500,"cache_read_input_tokens":200}},"session_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}'
|
|
119
|
-
result=$(COLUMNS=80 bash "$SCRIPT" <<< "$input")
|
|
120
|
-
visible=$(strip_ansi "$result")
|
|
121
|
-
len=$(printf '%s' "$visible" | wc -m | tr -d ' ')
|
|
122
|
-
[ "$len" -le 80 ]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
@test "narrow terminal still shows model and directory" {
|
|
126
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp/proj","project_dir":"/tmp/proj"},"context_window":{"context_window_size":200000,"current_usage":{"input_tokens":10000,"cache_creation_input_tokens":500,"cache_read_input_tokens":200}}}'
|
|
127
|
-
result=$(COLUMNS=40 bash "$SCRIPT" <<< "$input")
|
|
128
|
-
visible=$(strip_ansi "$result")
|
|
129
|
-
len=$(printf '%s' "$visible" | wc -m | tr -d ' ')
|
|
130
|
-
[ "$len" -le 40 ]
|
|
131
|
-
[[ "$visible" == *"Claude"* ]]
|
|
132
|
-
[[ "$visible" == *"proj"* ]]
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
@test "wide terminal shows session_id" {
|
|
136
|
-
input='{"model":{"display_name":"Claude"},"workspace":{"current_dir":"/tmp/proj","project_dir":"/tmp/proj"},"session_id":"test-wide-session-uuid"}'
|
|
137
|
-
result=$(COLUMNS=200 bash "$SCRIPT" <<< "$input")
|
|
138
|
-
[[ "$result" == *"test-wide-session-uuid"* ]]
|
|
139
|
-
}
|