cc-context-stats 1.8.0 → 1.8.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/package.json +8 -1
- package/scripts/context-stats.sh +1 -1
- 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 -187
- 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/MODEL_INTELLIGENCE.md +0 -396
- 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 -438
- package/scripts/statusline-git.sh +0 -88
- package/scripts/statusline-minimal.sh +0 -67
- package/scripts/statusline.py +0 -569
- 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 -542
- package/src/claude_statusline/cli/explain.py +0 -228
- package/src/claude_statusline/cli/statusline.py +0 -184
- 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 -165
- 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/intelligence.py +0 -162
- package/src/claude_statusline/graphs/renderer.py +0 -401
- 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/fixtures/mi_test_vectors.json +0 -140
- package/tests/node/intelligence.test.js +0 -98
- 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_intelligence.py +0 -314
- 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,92 +0,0 @@
|
|
|
1
|
-
"""Statistical calculations for token data."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass
|
|
9
|
-
class Stats:
|
|
10
|
-
"""Statistical summary of a data series."""
|
|
11
|
-
|
|
12
|
-
min_val: int
|
|
13
|
-
max_val: int
|
|
14
|
-
avg_val: int
|
|
15
|
-
total: int
|
|
16
|
-
count: int
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def calculate_stats(data: list[int]) -> Stats:
|
|
20
|
-
"""Calculate basic statistics for a list of integers.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
data: List of integer values
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
Stats object with min, max, avg, total, and count
|
|
27
|
-
"""
|
|
28
|
-
if not data:
|
|
29
|
-
return Stats(min_val=0, max_val=0, avg_val=0, total=0, count=0)
|
|
30
|
-
|
|
31
|
-
min_val = min(data)
|
|
32
|
-
max_val = max(data)
|
|
33
|
-
total = sum(data)
|
|
34
|
-
count = len(data)
|
|
35
|
-
avg_val = total // count if count > 0 else 0
|
|
36
|
-
|
|
37
|
-
return Stats(min_val=min_val, max_val=max_val, avg_val=avg_val, total=total, count=count)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def detect_spike(deltas: list[int], context_window_size: int, window: int = 5) -> bool:
|
|
41
|
-
"""Check if the latest delta is a spike.
|
|
42
|
-
|
|
43
|
-
A spike is defined as:
|
|
44
|
-
- Latest delta > 15% of context window size, OR
|
|
45
|
-
- Latest delta > 3x the rolling average of the last `window` deltas
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
deltas: List of token deltas
|
|
49
|
-
context_window_size: Total context window size in tokens
|
|
50
|
-
window: Number of recent deltas for rolling average (default: 5)
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
True if the latest delta qualifies as a spike
|
|
54
|
-
"""
|
|
55
|
-
if not deltas:
|
|
56
|
-
return False
|
|
57
|
-
|
|
58
|
-
latest = deltas[-1]
|
|
59
|
-
|
|
60
|
-
# Check absolute threshold: > 15% of context window
|
|
61
|
-
if context_window_size > 0 and latest > context_window_size * 0.15:
|
|
62
|
-
return True
|
|
63
|
-
|
|
64
|
-
# Check relative threshold: > 3x rolling average of previous deltas
|
|
65
|
-
previous = deltas[-(window + 1):-1] if len(deltas) > window else deltas[:-1]
|
|
66
|
-
if previous:
|
|
67
|
-
avg = sum(previous) / len(previous)
|
|
68
|
-
if avg > 0 and latest > avg * 3:
|
|
69
|
-
return True
|
|
70
|
-
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def calculate_deltas(values: list[int]) -> list[int]:
|
|
75
|
-
"""Calculate deltas between consecutive values.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
values: List of values (e.g., cumulative token counts)
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
List of deltas (length = len(values) - 1)
|
|
82
|
-
"""
|
|
83
|
-
if len(values) < 2:
|
|
84
|
-
return []
|
|
85
|
-
|
|
86
|
-
deltas = []
|
|
87
|
-
for i in range(1, len(values)):
|
|
88
|
-
delta = values[i] - values[i - 1]
|
|
89
|
-
# Handle negative deltas (session reset) by showing 0
|
|
90
|
-
deltas.append(max(0, delta))
|
|
91
|
-
|
|
92
|
-
return deltas
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""UI components for context-stats display."""
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"""Activity tier detection for token usage visualization."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from enum import Enum
|
|
6
|
-
|
|
7
|
-
from claude_statusline.core.state import StateEntry
|
|
8
|
-
from claude_statusline.graphs.statistics import calculate_deltas, detect_spike
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ActivityTier(Enum):
|
|
12
|
-
"""Token activity intensity tiers."""
|
|
13
|
-
|
|
14
|
-
IDLE = "idle"
|
|
15
|
-
LOW = "low"
|
|
16
|
-
MEDIUM = "medium"
|
|
17
|
-
HIGH = "high"
|
|
18
|
-
SPIKE = "spike"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Tier labels for accessibility (understandable without color)
|
|
22
|
-
TIER_LABELS: dict[ActivityTier, str] = {
|
|
23
|
-
ActivityTier.IDLE: "Idle",
|
|
24
|
-
ActivityTier.LOW: "Low activity",
|
|
25
|
-
ActivityTier.MEDIUM: "Active",
|
|
26
|
-
ActivityTier.HIGH: "High activity",
|
|
27
|
-
ActivityTier.SPIKE: "Spike!",
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_activity_tier(
|
|
32
|
-
entries: list[StateEntry],
|
|
33
|
-
context_window_size: int,
|
|
34
|
-
) -> ActivityTier:
|
|
35
|
-
"""Determine the current activity tier based on recent token deltas.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
entries: List of StateEntry objects (chronological order)
|
|
39
|
-
context_window_size: Total context window size in tokens
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
The current ActivityTier
|
|
43
|
-
"""
|
|
44
|
-
if len(entries) < 2:
|
|
45
|
-
return ActivityTier.IDLE
|
|
46
|
-
|
|
47
|
-
# Check if session is idle (>30s since last entry)
|
|
48
|
-
import time
|
|
49
|
-
|
|
50
|
-
now = int(time.time())
|
|
51
|
-
last_timestamp = entries[-1].timestamp
|
|
52
|
-
if now - last_timestamp > 30:
|
|
53
|
-
return ActivityTier.IDLE
|
|
54
|
-
|
|
55
|
-
# Calculate deltas from context usage
|
|
56
|
-
context_used = [e.current_used_tokens for e in entries]
|
|
57
|
-
deltas = calculate_deltas(context_used)
|
|
58
|
-
|
|
59
|
-
if not deltas:
|
|
60
|
-
return ActivityTier.IDLE
|
|
61
|
-
|
|
62
|
-
latest_delta = deltas[-1]
|
|
63
|
-
|
|
64
|
-
if context_window_size <= 0:
|
|
65
|
-
return ActivityTier.LOW if latest_delta > 0 else ActivityTier.IDLE
|
|
66
|
-
|
|
67
|
-
# Check for spike first (highest priority)
|
|
68
|
-
if detect_spike(deltas, context_window_size):
|
|
69
|
-
return ActivityTier.SPIKE
|
|
70
|
-
|
|
71
|
-
# Calculate delta as percentage of context window
|
|
72
|
-
delta_pct = (latest_delta / context_window_size) * 100
|
|
73
|
-
|
|
74
|
-
if delta_pct > 5:
|
|
75
|
-
return ActivityTier.HIGH
|
|
76
|
-
elif delta_pct > 2:
|
|
77
|
-
return ActivityTier.MEDIUM
|
|
78
|
-
elif latest_delta > 0:
|
|
79
|
-
return ActivityTier.LOW
|
|
80
|
-
else:
|
|
81
|
-
return ActivityTier.IDLE
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def get_tier_label(tier: ActivityTier) -> str:
|
|
85
|
-
"""Get an accessible text label for a tier.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
tier: The activity tier
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Human-readable label string
|
|
92
|
-
"""
|
|
93
|
-
return TIER_LABELS.get(tier, "")
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"""Rotating waiting text for active sessions."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
|
|
7
|
-
from claude_statusline.core.state import StateEntry
|
|
8
|
-
|
|
9
|
-
WAITING_MESSAGES = [
|
|
10
|
-
"Thinking...",
|
|
11
|
-
"Cooking...",
|
|
12
|
-
"Crunching tokens...",
|
|
13
|
-
"Compiling plan...",
|
|
14
|
-
"Running steps...",
|
|
15
|
-
"Processing...",
|
|
16
|
-
"Working on it...",
|
|
17
|
-
"Analyzing...",
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
# Static message for reduced-motion mode
|
|
21
|
-
STATIC_MESSAGE = "Working..."
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_waiting_text(cycle_index: int, reduced_motion: bool = False) -> str:
|
|
25
|
-
"""Get the current waiting text based on the refresh cycle.
|
|
26
|
-
|
|
27
|
-
Messages rotate every 2 cycles (approximately every 4 seconds at 2s refresh).
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
cycle_index: The current watch-mode refresh counter
|
|
31
|
-
reduced_motion: If True, return a static message instead of rotating
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
A waiting message string
|
|
35
|
-
"""
|
|
36
|
-
if reduced_motion:
|
|
37
|
-
return STATIC_MESSAGE
|
|
38
|
-
|
|
39
|
-
# Rotate every 2 cycles to keep it readable
|
|
40
|
-
message_index = (cycle_index // 2) % len(WAITING_MESSAGES)
|
|
41
|
-
return WAITING_MESSAGES[message_index]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def is_active(entries: list[StateEntry], timeout: int = 30) -> bool:
|
|
45
|
-
"""Determine if the session is currently active.
|
|
46
|
-
|
|
47
|
-
A session is considered active if the most recent state entry
|
|
48
|
-
was recorded within `timeout` seconds of the current time.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
entries: List of StateEntry objects (chronological order)
|
|
52
|
-
timeout: Seconds since last entry to consider session active (default: 30)
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
True if the session appears to be actively running
|
|
56
|
-
"""
|
|
57
|
-
if not entries:
|
|
58
|
-
return False
|
|
59
|
-
|
|
60
|
-
now = int(time.time())
|
|
61
|
-
last_timestamp = entries[-1].timestamp
|
|
62
|
-
return (now - last_timestamp) <= timeout
|
|
@@ -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
|
-
}
|