cc-context-stats 1.6.0 → 1.6.2
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/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/docs/context-stats.md +1 -1
- package/images/v1.6.1.png +0 -0
- package/install +9 -2
- package/install.sh +8 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/context-stats.sh +2 -2
- package/scripts/statusline.js +1 -1
- package/scripts/statusline.py +88 -34
- package/src/claude_statusline/__init__.py +1 -1
- package/src/claude_statusline/graphs/renderer.py +2 -2
- package/tests/bash/test_delta_parity.bats +199 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.2] - 2026-03-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Delta calculation parity** - Python statusline now reads correct CSV indices (3+5+6) for context usage delta, matching Node.js behavior
|
|
15
|
+
- **Missing duplicate-entry guard** - Python statusline now skips state file writes when token count is unchanged, preventing file bloat
|
|
16
|
+
- **Missing state file rotation** - Python statusline now calls rotation after writes (10k/5k threshold), matching Node.js
|
|
17
|
+
- **Missing git timeout** - Added 5-second timeout to git subprocess calls in standalone Python statusline script
|
|
18
|
+
- **Broad exception handling** - Narrowed `except Exception` to `(OSError, ValueError)` for state reads and `OSError` for writes
|
|
19
|
+
- **Stale CSV format comments** - Added missing `context_window_size` field to header comments in both Python and Node.js scripts
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Delta parity tests** - 4 new bats tests verifying Python/Node.js produce identical deltas, handle first-run/decrease/dedup correctly
|
|
24
|
+
|
|
25
|
+
## [1.6.1] - 2026-03-13
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Footer version drift** - Corrected stale version `1.2.3` in bash script and `1.0.0` default in Python renderer to match actual release version
|
|
30
|
+
- **Footer project name** - Renamed `claude-statusline` to `cc-context-stats` in the footer display across bash and Python implementations
|
|
31
|
+
- **Install version embedding** - Install scripts now read version from `package.json` and embed it into the installed script, preventing future version drift
|
|
32
|
+
|
|
10
33
|
## [1.6.0] - 2026-03-13
|
|
11
34
|
|
|
12
35
|
### Added
|
package/README.md
CHANGED
package/docs/context-stats.md
CHANGED
|
@@ -68,7 +68,7 @@ Session Summary
|
|
|
68
68
|
Output Tokens: 43,429
|
|
69
69
|
Session Duration: 2h 29m
|
|
70
70
|
|
|
71
|
-
Powered by
|
|
71
|
+
Powered by cc-context-stats v1.6.1 - https://github.com/luongnv89/cc-context-stats
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
## Features
|
|
Binary file
|
package/install
CHANGED
|
@@ -237,10 +237,17 @@ install_token_graph() {
|
|
|
237
237
|
download_file "scripts/token-graph.sh" "$DEST"
|
|
238
238
|
fi
|
|
239
239
|
|
|
240
|
-
# Embed commit hash
|
|
240
|
+
# Embed version and commit hash
|
|
241
|
+
local pkg_version
|
|
242
|
+
if [ "$INSTALL_MODE" = "local" ]; then
|
|
243
|
+
pkg_version=$(grep -o '"version": *"[^"]*"' "$SCRIPT_DIR/package.json" | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
244
|
+
else
|
|
245
|
+
pkg_version=$(curl -fsSL "${GITHUB_RAW_URL}/package.json" 2>/dev/null | grep -o '"version": *"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
246
|
+
fi
|
|
247
|
+
[ -n "$pkg_version" ] && sed -i.bak "s/VERSION=\"[^\"]*\"/VERSION=\"$pkg_version\"/" "$DEST" && rm -f "$DEST.bak"
|
|
241
248
|
sed -i.bak "s/COMMIT_HASH=\"dev\"/COMMIT_HASH=\"$commit_hash\"/" "$DEST" && rm -f "$DEST.bak"
|
|
242
249
|
chmod +x "$DEST"
|
|
243
|
-
echo -e "${GREEN}✓${RESET} Installed: $DEST (
|
|
250
|
+
echo -e "${GREEN}✓${RESET} Installed: $DEST (v${pkg_version:-1.6.0}-$commit_hash)"
|
|
244
251
|
|
|
245
252
|
# Check if ~/.local/bin is in PATH
|
|
246
253
|
if [[ ":$PATH:" != *":$LOCAL_BIN:"* ]]; then
|
package/install.sh
CHANGED
|
@@ -181,7 +181,14 @@ install_context_stats() {
|
|
|
181
181
|
download_file "scripts/context-stats.sh" "$DEST"
|
|
182
182
|
fi
|
|
183
183
|
|
|
184
|
-
# Embed commit hash
|
|
184
|
+
# Embed version and commit hash
|
|
185
|
+
local pkg_version
|
|
186
|
+
if [ "$INSTALL_MODE" = "local" ]; then
|
|
187
|
+
pkg_version=$(grep -o '"version": *"[^"]*"' "$SCRIPT_DIR/package.json" | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
188
|
+
else
|
|
189
|
+
pkg_version=$(curl -fsSL "${GITHUB_RAW_URL}/package.json" 2>/dev/null | grep -o '"version": *"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
190
|
+
fi
|
|
191
|
+
[ -n "$pkg_version" ] && sed -i.bak "s/VERSION=\"[^\"]*\"/VERSION=\"$pkg_version\"/" "$DEST" && rm -f "$DEST.bak"
|
|
185
192
|
sed -i.bak "s/COMMIT_HASH=\"dev\"/COMMIT_HASH=\"$commit_hash\"/" "$DEST" && rm -f "$DEST.bak"
|
|
186
193
|
chmod +x "$DEST"
|
|
187
194
|
echo -e "${GREEN}✓${RESET} Installed: $DEST"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-context-stats"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.2"
|
|
8
8
|
description = "Monitor your Claude Code session context in real-time - track token usage and never run out of context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
package/scripts/context-stats.sh
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
# === CONFIGURATION ===
|
|
24
24
|
# shellcheck disable=SC2034
|
|
25
|
-
VERSION="1.
|
|
25
|
+
VERSION="1.6.1"
|
|
26
26
|
COMMIT_HASH="dev" # Will be replaced during installation
|
|
27
27
|
STATE_DIR=~/.claude/statusline
|
|
28
28
|
CONFIG_FILE=~/.claude/statusline.conf
|
|
@@ -873,7 +873,7 @@ render_summary() {
|
|
|
873
873
|
}
|
|
874
874
|
|
|
875
875
|
render_footer() {
|
|
876
|
-
echo -e "${DIM}Powered by ${CYAN}
|
|
876
|
+
echo -e "${DIM}Powered by ${CYAN}cc-context-stats${DIM} v${VERSION}-${COMMIT_HASH} - https://github.com/luongnv89/cc-context-stats${RESET}"
|
|
877
877
|
echo ""
|
|
878
878
|
}
|
|
879
879
|
|
package/scripts/statusline.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,
|
|
25
25
|
* current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,
|
|
26
26
|
* total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,
|
|
27
|
-
* workspace_project_dir
|
|
27
|
+
* workspace_project_dir,context_window_size
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
const { execSync } = require('child_process');
|
package/scripts/statusline.py
CHANGED
|
@@ -21,7 +21,10 @@ Create/edit ~/.claude/statusline.conf and set:
|
|
|
21
21
|
When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
|
|
22
22
|
|
|
23
23
|
State file format (CSV):
|
|
24
|
-
timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,
|
|
24
|
+
timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,
|
|
25
|
+
current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,
|
|
26
|
+
total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,
|
|
27
|
+
workspace_project_dir,context_window_size
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
import json
|
|
@@ -30,6 +33,42 @@ import re
|
|
|
30
33
|
import shutil
|
|
31
34
|
import subprocess
|
|
32
35
|
import sys
|
|
36
|
+
import tempfile
|
|
37
|
+
|
|
38
|
+
ROTATION_THRESHOLD = 10_000
|
|
39
|
+
ROTATION_KEEP = 5_000
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def maybe_rotate_state_file(state_file):
|
|
43
|
+
"""Rotate a state file if it exceeds ROTATION_THRESHOLD lines.
|
|
44
|
+
|
|
45
|
+
Keeps the most recent ROTATION_KEEP lines via atomic temp-file + rename.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
if not os.path.exists(state_file):
|
|
49
|
+
return
|
|
50
|
+
with open(state_file) as f:
|
|
51
|
+
lines = f.readlines()
|
|
52
|
+
if len(lines) <= ROTATION_THRESHOLD:
|
|
53
|
+
return
|
|
54
|
+
keep = lines[-ROTATION_KEEP:]
|
|
55
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
56
|
+
dir=os.path.dirname(state_file), suffix=".tmp"
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
with os.fdopen(fd, "w") as tmp_f:
|
|
60
|
+
tmp_f.writelines(keep)
|
|
61
|
+
os.replace(tmp_path, state_file)
|
|
62
|
+
except BaseException:
|
|
63
|
+
try:
|
|
64
|
+
os.unlink(tmp_path)
|
|
65
|
+
except OSError:
|
|
66
|
+
pass
|
|
67
|
+
raise
|
|
68
|
+
except OSError as e:
|
|
69
|
+
sys.stderr.write(
|
|
70
|
+
f"[statusline] warning: failed to rotate state file: {e}\n"
|
|
71
|
+
)
|
|
33
72
|
|
|
34
73
|
# ANSI Colors
|
|
35
74
|
BLUE = "\033[0;34m"
|
|
@@ -106,6 +145,7 @@ def get_git_info(project_dir):
|
|
|
106
145
|
cwd=project_dir,
|
|
107
146
|
capture_output=True,
|
|
108
147
|
text=True,
|
|
148
|
+
timeout=5,
|
|
109
149
|
)
|
|
110
150
|
branch = result.stdout.strip()
|
|
111
151
|
|
|
@@ -118,13 +158,14 @@ def get_git_info(project_dir):
|
|
|
118
158
|
cwd=project_dir,
|
|
119
159
|
capture_output=True,
|
|
120
160
|
text=True,
|
|
161
|
+
timeout=5,
|
|
121
162
|
)
|
|
122
163
|
changes = len([line for line in result.stdout.split("\n") if line.strip()])
|
|
123
164
|
|
|
124
165
|
if changes > 0:
|
|
125
166
|
return f" | {MAGENTA}{branch}{RESET} {CYAN}[{changes}]{RESET}"
|
|
126
167
|
return f" | {MAGENTA}{branch}{RESET}"
|
|
127
|
-
except
|
|
168
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
128
169
|
return ""
|
|
129
170
|
|
|
130
171
|
|
|
@@ -302,19 +343,27 @@ def main():
|
|
|
302
343
|
try:
|
|
303
344
|
if os.path.exists(state_file):
|
|
304
345
|
has_prev = True
|
|
305
|
-
# Read last line to get previous
|
|
346
|
+
# Read last line to get previous context usage
|
|
306
347
|
with open(state_file) as f:
|
|
307
348
|
lines = f.readlines()
|
|
308
349
|
if lines:
|
|
309
350
|
last_line = lines[-1].strip()
|
|
310
351
|
if "," in last_line:
|
|
311
|
-
|
|
352
|
+
parts = last_line.split(",")
|
|
353
|
+
# Calculate previous context usage:
|
|
354
|
+
# cur_input + cache_creation + cache_read
|
|
355
|
+
# CSV indices: cur_in[3], cache_create[5], cache_read[6]
|
|
356
|
+
prev_cur_input = int(parts[3]) if len(parts) > 3 else 0
|
|
357
|
+
prev_cache_creation = int(parts[5]) if len(parts) > 5 else 0
|
|
358
|
+
prev_cache_read = int(parts[6]) if len(parts) > 6 else 0
|
|
359
|
+
prev_tokens = prev_cur_input + prev_cache_creation + prev_cache_read
|
|
312
360
|
else:
|
|
361
|
+
# Old format - single value
|
|
313
362
|
prev_tokens = int(last_line or 0)
|
|
314
|
-
except
|
|
363
|
+
except (OSError, ValueError) as e:
|
|
315
364
|
sys.stderr.write(f"[statusline] warning: failed to read state file: {e}\n")
|
|
316
365
|
prev_tokens = 0
|
|
317
|
-
# Calculate delta
|
|
366
|
+
# Calculate delta (difference in context window usage)
|
|
318
367
|
delta = used_tokens - prev_tokens
|
|
319
368
|
# Only show positive delta (and skip first run when no previous state)
|
|
320
369
|
if has_prev and delta > 0:
|
|
@@ -323,34 +372,39 @@ def main():
|
|
|
323
372
|
else:
|
|
324
373
|
delta_display = f"{delta / 1000:.1f}k"
|
|
325
374
|
delta_info = f" {DIM}[+{delta_display}]{RESET}"
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
375
|
+
# Only append if context usage changed (avoid duplicates from multiple refreshes)
|
|
376
|
+
if not has_prev or used_tokens != prev_tokens:
|
|
377
|
+
# Append current usage with comprehensive format
|
|
378
|
+
# Format: ts,total_in,total_out,cur_in,cur_out,cache_create,cache_read,
|
|
379
|
+
# cost_usd,lines_added,lines_removed,session_id,model_id,project_dir,
|
|
380
|
+
# context_window_size
|
|
381
|
+
try:
|
|
382
|
+
cur_input_tokens = current_usage.get("input_tokens", 0)
|
|
383
|
+
cur_output_tokens = current_usage.get("output_tokens", 0)
|
|
384
|
+
state_data = ",".join(
|
|
385
|
+
str(x)
|
|
386
|
+
for x in [
|
|
387
|
+
int(time.time()),
|
|
388
|
+
total_input_tokens,
|
|
389
|
+
total_output_tokens,
|
|
390
|
+
cur_input_tokens,
|
|
391
|
+
cur_output_tokens,
|
|
392
|
+
cache_creation,
|
|
393
|
+
cache_read,
|
|
394
|
+
cost_usd,
|
|
395
|
+
lines_added,
|
|
396
|
+
lines_removed,
|
|
397
|
+
session_id or "",
|
|
398
|
+
model_id,
|
|
399
|
+
workspace_project_dir.replace(",", "_"),
|
|
400
|
+
total_size,
|
|
401
|
+
]
|
|
402
|
+
)
|
|
403
|
+
with open(state_file, "a") as f:
|
|
404
|
+
f.write(f"{state_data}\n")
|
|
405
|
+
maybe_rotate_state_file(state_file)
|
|
406
|
+
except OSError as e:
|
|
407
|
+
sys.stderr.write(f"[statusline] warning: failed to write state file: {e}\n")
|
|
354
408
|
|
|
355
409
|
# Display session_id if enabled
|
|
356
410
|
if show_session and session_id:
|
|
@@ -351,7 +351,7 @@ class GraphRenderer:
|
|
|
351
351
|
)
|
|
352
352
|
self._emit()
|
|
353
353
|
|
|
354
|
-
def render_footer(self, version: str = "1.
|
|
354
|
+
def render_footer(self, version: str = "1.6.1", commit_hash: str = "dev") -> None:
|
|
355
355
|
"""Render the footer with version info.
|
|
356
356
|
|
|
357
357
|
Args:
|
|
@@ -359,7 +359,7 @@ class GraphRenderer:
|
|
|
359
359
|
commit_hash: Git commit hash
|
|
360
360
|
"""
|
|
361
361
|
self._emit(
|
|
362
|
-
f"{self.colors.dim}Powered by {self.colors.cyan}
|
|
362
|
+
f"{self.colors.dim}Powered by {self.colors.cyan}cc-context-stats"
|
|
363
363
|
f"{self.colors.dim} v{version}-{commit_hash} - "
|
|
364
364
|
f"https://github.com/luongnv89/cc-context-stats{self.colors.reset}"
|
|
365
365
|
)
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
}
|