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 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
@@ -15,7 +15,7 @@
15
15
 
16
16
  **Never run out of context unexpectedly** - monitor your session context in real-time.
17
17
 
18
- ![Context Stats](images/context-status-dumbzone.png)
18
+ ![Context Stats](images/v1.6.1.png)
19
19
 
20
20
  ## Why Context Stats?
21
21
 
@@ -68,7 +68,7 @@ Session Summary
68
68
  Output Tokens: 43,429
69
69
  Session Duration: 2h 29m
70
70
 
71
- Powered by claude-statusline v1.2.0 - https://github.com/luongnv89/cc-context-stats
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 (v1.0.0-$commit_hash)"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-context-stats",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Monitor your Claude Code session context in real-time - track token usage and never run out of context",
5
5
  "main": "scripts/statusline.js",
6
6
  "scripts": {
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.0"
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" }
@@ -22,7 +22,7 @@
22
22
 
23
23
  # === CONFIGURATION ===
24
24
  # shellcheck disable=SC2034
25
- VERSION="1.2.3"
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}claude-statusline${DIM} v${VERSION}-${COMMIT_HASH} - https://github.com/luongnv89/cc-context-stats${RESET}"
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
 
@@ -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');
@@ -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,current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,workspace_project_dir
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 Exception:
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 token count
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
- prev_tokens = int(last_line.split(",")[1])
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 Exception as e:
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
- # Append current usage with comprehensive format
327
- # Format: timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,workspace_project_dir
328
- try:
329
- cur_input_tokens = current_usage.get("input_tokens", 0)
330
- cur_output_tokens = current_usage.get("output_tokens", 0)
331
- state_data = ",".join(
332
- str(x)
333
- for x in [
334
- int(time.time()),
335
- total_input_tokens,
336
- total_output_tokens,
337
- cur_input_tokens,
338
- cur_output_tokens,
339
- cache_creation,
340
- cache_read,
341
- cost_usd,
342
- lines_added,
343
- lines_removed,
344
- session_id or "",
345
- model_id,
346
- workspace_project_dir.replace(",", "_"),
347
- total_size,
348
- ]
349
- )
350
- with open(state_file, "a") as f:
351
- f.write(f"{state_data}\n")
352
- except Exception as e:
353
- sys.stderr.write(f"[statusline] warning: failed to write state file: {e}\n")
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:
@@ -3,7 +3,7 @@
3
3
  Never run out of context unexpectedly - monitor your session context in real-time.
4
4
  """
5
5
 
6
- __version__ = "1.6.0"
6
+ __version__ = "1.6.2"
7
7
 
8
8
  from claude_statusline.core.config import Config
9
9
  from claude_statusline.core.state import StateFile
@@ -351,7 +351,7 @@ class GraphRenderer:
351
351
  )
352
352
  self._emit()
353
353
 
354
- def render_footer(self, version: str = "1.0.0", commit_hash: str = "dev") -> None:
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}claude-statusline"
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
+ }