cc-context-stats 1.3.0
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/.claude/commands/context-stats.md +17 -0
- package/.claude/settings.local.json +85 -0
- package/.editorconfig +60 -0
- package/.eslintrc.json +35 -0
- package/.github/dependabot.yml +44 -0
- package/.github/workflows/ci.yml +255 -0
- package/.github/workflows/release.yml +149 -0
- package/.pre-commit-config.yaml +74 -0
- package/.prettierrc +33 -0
- package/.shellcheckrc +10 -0
- package/CHANGELOG.md +100 -0
- package/CONTRIBUTING.md +240 -0
- package/PUBLISHING_GUIDE.md +69 -0
- package/README.md +179 -0
- package/config/settings-example.json +7 -0
- package/config/settings-node.json +7 -0
- package/config/settings-python.json +7 -0
- package/docs/configuration.md +83 -0
- package/docs/context-stats.md +132 -0
- package/docs/installation.md +195 -0
- package/docs/scripts.md +116 -0
- package/docs/troubleshooting.md +189 -0
- 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/install +344 -0
- package/install.sh +272 -0
- package/jest.config.js +11 -0
- package/npm-publish.sh +33 -0
- package/package.json +36 -0
- package/publish.sh +24 -0
- package/pyproject.toml +113 -0
- package/requirements-dev.txt +12 -0
- package/scripts/context-stats.sh +970 -0
- package/scripts/statusline-full.sh +241 -0
- package/scripts/statusline-git.sh +32 -0
- package/scripts/statusline-minimal.sh +11 -0
- package/scripts/statusline.js +350 -0
- package/scripts/statusline.py +312 -0
- package/show_raw_claude_code_api.js +11 -0
- package/src/claude_statusline/__init__.py +11 -0
- package/src/claude_statusline/__main__.py +6 -0
- package/src/claude_statusline/cli/__init__.py +1 -0
- package/src/claude_statusline/cli/context_stats.py +379 -0
- package/src/claude_statusline/cli/statusline.py +172 -0
- package/src/claude_statusline/core/__init__.py +1 -0
- package/src/claude_statusline/core/colors.py +55 -0
- package/src/claude_statusline/core/config.py +98 -0
- package/src/claude_statusline/core/git.py +67 -0
- package/src/claude_statusline/core/state.py +266 -0
- package/src/claude_statusline/formatters/__init__.py +1 -0
- package/src/claude_statusline/formatters/time.py +50 -0
- package/src/claude_statusline/formatters/tokens.py +70 -0
- package/src/claude_statusline/graphs/__init__.py +1 -0
- package/src/claude_statusline/graphs/renderer.py +346 -0
- package/src/claude_statusline/graphs/statistics.py +58 -0
- package/tests/bash/test_install.bats +29 -0
- package/tests/bash/test_statusline_full.bats +109 -0
- package/tests/bash/test_statusline_git.bats +42 -0
- package/tests/bash/test_statusline_minimal.bats +37 -0
- package/tests/fixtures/json/high_usage.json +17 -0
- package/tests/fixtures/json/low_usage.json +17 -0
- package/tests/fixtures/json/medium_usage.json +17 -0
- package/tests/fixtures/json/valid_full.json +30 -0
- package/tests/fixtures/json/valid_minimal.json +9 -0
- package/tests/node/statusline.test.js +199 -0
- package/tests/python/conftest.py +84 -0
- package/tests/python/test_statusline.py +154 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model": {
|
|
3
|
+
"display_name": "Opus 4.5",
|
|
4
|
+
"api_name": "claude-opus-4-5"
|
|
5
|
+
},
|
|
6
|
+
"workspace": {
|
|
7
|
+
"current_dir": "/home/user/my-project",
|
|
8
|
+
"project_dir": "/home/user/my-project"
|
|
9
|
+
},
|
|
10
|
+
"context_window": {
|
|
11
|
+
"context_window_size": 200000,
|
|
12
|
+
"total_input_tokens": 75000,
|
|
13
|
+
"total_output_tokens": 8500,
|
|
14
|
+
"current_usage": {
|
|
15
|
+
"input_tokens": 50000,
|
|
16
|
+
"output_tokens": 5000,
|
|
17
|
+
"cache_creation_input_tokens": 10000,
|
|
18
|
+
"cache_read_input_tokens": 20000
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"cost": {
|
|
22
|
+
"total_cost_usd": 0.05234,
|
|
23
|
+
"total_duration_ms": 120000,
|
|
24
|
+
"total_api_duration_ms": 5000,
|
|
25
|
+
"total_lines_added": 250,
|
|
26
|
+
"total_lines_removed": 45
|
|
27
|
+
},
|
|
28
|
+
"session_id": "test-session-123",
|
|
29
|
+
"version": "1.0.80"
|
|
30
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const SCRIPT_PATH = path.join(__dirname, '..', '..', 'scripts', 'statusline.js');
|
|
6
|
+
const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures', 'json');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run the statusline.js script with the given input data
|
|
10
|
+
* @param {Object|string} inputData - JSON input or string
|
|
11
|
+
* @returns {Promise<{stdout: string, stderr: string, code: number}>}
|
|
12
|
+
*/
|
|
13
|
+
function runScript(inputData) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn('node', [SCRIPT_PATH]);
|
|
16
|
+
let stdout = '';
|
|
17
|
+
let stderr = '';
|
|
18
|
+
|
|
19
|
+
child.stdout.on('data', data => {
|
|
20
|
+
stdout += data.toString();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
child.stderr.on('data', data => {
|
|
24
|
+
stderr += data.toString();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
child.on('close', code => {
|
|
28
|
+
resolve({ stdout: stdout.trim(), stderr, code });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on('error', reject);
|
|
32
|
+
|
|
33
|
+
const input = typeof inputData === 'string' ? inputData : JSON.stringify(inputData);
|
|
34
|
+
child.stdin.write(input);
|
|
35
|
+
child.stdin.end();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load a JSON fixture file
|
|
41
|
+
* @param {string} name - Fixture name without .json extension
|
|
42
|
+
* @returns {Object}
|
|
43
|
+
*/
|
|
44
|
+
function loadFixture(name) {
|
|
45
|
+
const filePath = path.join(FIXTURES_DIR, `${name}.json`);
|
|
46
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('statusline.js', () => {
|
|
50
|
+
const sampleInput = {
|
|
51
|
+
model: { display_name: 'Claude 3.5 Sonnet' },
|
|
52
|
+
workspace: {
|
|
53
|
+
current_dir: '/home/user/myproject',
|
|
54
|
+
project_dir: '/home/user/myproject',
|
|
55
|
+
},
|
|
56
|
+
context_window: {
|
|
57
|
+
context_window_size: 200000,
|
|
58
|
+
current_usage: {
|
|
59
|
+
input_tokens: 10000,
|
|
60
|
+
cache_creation_input_tokens: 500,
|
|
61
|
+
cache_read_input_tokens: 200,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe('Script basics', () => {
|
|
67
|
+
test('script file exists', () => {
|
|
68
|
+
expect(fs.existsSync(SCRIPT_PATH)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('script has node shebang', () => {
|
|
72
|
+
const content = fs.readFileSync(SCRIPT_PATH, 'utf8');
|
|
73
|
+
expect(content.startsWith('#!/usr/bin/env node')).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Output content', () => {
|
|
78
|
+
test('outputs model name', async () => {
|
|
79
|
+
const result = await runScript(sampleInput);
|
|
80
|
+
expect(result.stdout).toContain('Claude 3.5 Sonnet');
|
|
81
|
+
expect(result.code).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('outputs directory name', async () => {
|
|
85
|
+
const result = await runScript(sampleInput);
|
|
86
|
+
expect(result.stdout).toContain('myproject');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('shows free tokens indicator', async () => {
|
|
90
|
+
const result = await runScript(sampleInput);
|
|
91
|
+
expect(result.stdout).toContain('free');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('shows AC indicator', async () => {
|
|
95
|
+
const result = await runScript(sampleInput);
|
|
96
|
+
expect(result.stdout).toContain('[AC:');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('shows percentage', async () => {
|
|
100
|
+
const result = await runScript(sampleInput);
|
|
101
|
+
expect(result.stdout).toMatch(/\d+\.\d+%/);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Error handling', () => {
|
|
106
|
+
test('handles missing model gracefully', async () => {
|
|
107
|
+
const input = {
|
|
108
|
+
workspace: { current_dir: '/tmp/test', project_dir: '/tmp/test' },
|
|
109
|
+
};
|
|
110
|
+
const result = await runScript(input);
|
|
111
|
+
expect(result.stdout).toContain('Claude'); // Default fallback
|
|
112
|
+
expect(result.code).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('handles missing context window gracefully', async () => {
|
|
116
|
+
const input = {
|
|
117
|
+
model: { display_name: 'Claude' },
|
|
118
|
+
workspace: { current_dir: '/tmp/test', project_dir: '/tmp/test' },
|
|
119
|
+
};
|
|
120
|
+
const result = await runScript(input);
|
|
121
|
+
expect(result.code).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('handles invalid JSON gracefully', async () => {
|
|
125
|
+
const result = await runScript('invalid json');
|
|
126
|
+
expect(result.code).toBe(0);
|
|
127
|
+
expect(result.stdout).toContain('Claude');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('handles empty input gracefully', async () => {
|
|
131
|
+
const result = await runScript('');
|
|
132
|
+
expect(result.code).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Fixtures', () => {
|
|
137
|
+
test('handles valid_full fixture', async () => {
|
|
138
|
+
const input = loadFixture('valid_full');
|
|
139
|
+
const result = await runScript(input);
|
|
140
|
+
expect(result.code).toBe(0);
|
|
141
|
+
expect(result.stdout).toContain('Opus 4.5');
|
|
142
|
+
expect(result.stdout).toContain('my-project');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('handles valid_minimal fixture', async () => {
|
|
146
|
+
const input = loadFixture('valid_minimal');
|
|
147
|
+
const result = await runScript(input);
|
|
148
|
+
expect(result.code).toBe(0);
|
|
149
|
+
expect(result.stdout).toContain('Claude');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('handles low_usage fixture', async () => {
|
|
153
|
+
const input = loadFixture('low_usage');
|
|
154
|
+
const result = await runScript(input);
|
|
155
|
+
expect(result.code).toBe(0);
|
|
156
|
+
expect(result.stdout).toContain('free');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('handles medium_usage fixture', async () => {
|
|
160
|
+
const input = loadFixture('medium_usage');
|
|
161
|
+
const result = await runScript(input);
|
|
162
|
+
expect(result.code).toBe(0);
|
|
163
|
+
expect(result.stdout).toContain('free');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('handles high_usage fixture', async () => {
|
|
167
|
+
const input = loadFixture('high_usage');
|
|
168
|
+
const result = await runScript(input);
|
|
169
|
+
expect(result.code).toBe(0);
|
|
170
|
+
expect(result.stdout).toContain('free');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('all JSON fixtures succeed', async () => {
|
|
174
|
+
const fixtures = fs.readdirSync(FIXTURES_DIR).filter(f => f.endsWith('.json'));
|
|
175
|
+
for (const fixture of fixtures) {
|
|
176
|
+
const input = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, fixture), 'utf8'));
|
|
177
|
+
const result = await runScript(input);
|
|
178
|
+
expect(result.code).toBe(0);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('Session ID display', () => {
|
|
184
|
+
test('shows session_id by default', async () => {
|
|
185
|
+
const inputWithSession = {
|
|
186
|
+
...sampleInput,
|
|
187
|
+
session_id: 'test-session-abc123',
|
|
188
|
+
};
|
|
189
|
+
const result = await runScript(inputWithSession);
|
|
190
|
+
expect(result.code).toBe(0);
|
|
191
|
+
expect(result.stdout).toContain('test-session-abc123');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('handles missing session_id gracefully', async () => {
|
|
195
|
+
const result = await runScript(sampleInput);
|
|
196
|
+
expect(result.code).toBe(0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Pytest configuration and fixtures for statusline tests."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
# Get the project root directory
|
|
9
|
+
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|
10
|
+
FIXTURES_DIR = PROJECT_ROOT / "tests" / "fixtures" / "json"
|
|
11
|
+
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def project_root():
|
|
16
|
+
"""Return the project root directory."""
|
|
17
|
+
return PROJECT_ROOT
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def scripts_dir():
|
|
22
|
+
"""Return the scripts directory."""
|
|
23
|
+
return SCRIPTS_DIR
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def fixtures_dir():
|
|
28
|
+
"""Return the fixtures directory."""
|
|
29
|
+
return FIXTURES_DIR
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def valid_full_input():
|
|
34
|
+
"""Load valid_full.json fixture."""
|
|
35
|
+
with open(FIXTURES_DIR / "valid_full.json") as f:
|
|
36
|
+
return json.load(f)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def valid_minimal_input():
|
|
41
|
+
"""Load valid_minimal.json fixture."""
|
|
42
|
+
with open(FIXTURES_DIR / "valid_minimal.json") as f:
|
|
43
|
+
return json.load(f)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def low_usage_input():
|
|
48
|
+
"""Load low_usage.json fixture."""
|
|
49
|
+
with open(FIXTURES_DIR / "low_usage.json") as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def medium_usage_input():
|
|
55
|
+
"""Load medium_usage.json fixture."""
|
|
56
|
+
with open(FIXTURES_DIR / "medium_usage.json") as f:
|
|
57
|
+
return json.load(f)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def high_usage_input():
|
|
62
|
+
"""Load high_usage.json fixture."""
|
|
63
|
+
with open(FIXTURES_DIR / "high_usage.json") as f:
|
|
64
|
+
return json.load(f)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture
|
|
68
|
+
def sample_input():
|
|
69
|
+
"""Return a sample input dictionary for testing."""
|
|
70
|
+
return {
|
|
71
|
+
"model": {"display_name": "Claude 3.5 Sonnet"},
|
|
72
|
+
"workspace": {
|
|
73
|
+
"current_dir": "/home/user/myproject",
|
|
74
|
+
"project_dir": "/home/user/myproject",
|
|
75
|
+
},
|
|
76
|
+
"context_window": {
|
|
77
|
+
"context_window_size": 200000,
|
|
78
|
+
"current_usage": {
|
|
79
|
+
"input_tokens": 10000,
|
|
80
|
+
"cache_creation_input_tokens": 500,
|
|
81
|
+
"cache_read_input_tokens": 200,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Tests for statusline.py script."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SCRIPT_PATH = Path(__file__).parent.parent.parent / "scripts" / "statusline.py"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_script(input_data: dict) -> tuple[str, int]:
|
|
12
|
+
"""Run the statusline.py script with the given input.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Tuple of (stdout, return_code)
|
|
16
|
+
"""
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
19
|
+
input=json.dumps(input_data),
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
return result.stdout.strip(), result.returncode
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestStatuslineScript:
|
|
27
|
+
"""Tests for the statusline.py script execution."""
|
|
28
|
+
|
|
29
|
+
def test_script_exists(self):
|
|
30
|
+
"""Script file should exist."""
|
|
31
|
+
assert SCRIPT_PATH.exists()
|
|
32
|
+
|
|
33
|
+
def test_script_is_python(self):
|
|
34
|
+
"""Script should have Python shebang."""
|
|
35
|
+
content = SCRIPT_PATH.read_text()
|
|
36
|
+
assert content.startswith("#!/usr/bin/env python3")
|
|
37
|
+
|
|
38
|
+
def test_outputs_model_name(self, sample_input):
|
|
39
|
+
"""Should output the model name."""
|
|
40
|
+
output, code = run_script(sample_input)
|
|
41
|
+
assert code == 0
|
|
42
|
+
assert "Claude 3.5 Sonnet" in output
|
|
43
|
+
|
|
44
|
+
def test_outputs_directory_name(self, sample_input):
|
|
45
|
+
"""Should output the directory name."""
|
|
46
|
+
output, code = run_script(sample_input)
|
|
47
|
+
assert code == 0
|
|
48
|
+
assert "myproject" in output
|
|
49
|
+
|
|
50
|
+
def test_shows_free_tokens(self, sample_input):
|
|
51
|
+
"""Should show free tokens indicator."""
|
|
52
|
+
output, code = run_script(sample_input)
|
|
53
|
+
assert code == 0
|
|
54
|
+
assert "free" in output
|
|
55
|
+
|
|
56
|
+
def test_shows_ac_indicator(self, sample_input):
|
|
57
|
+
"""Should show autocompact indicator."""
|
|
58
|
+
output, code = run_script(sample_input)
|
|
59
|
+
assert code == 0
|
|
60
|
+
assert "[AC:" in output
|
|
61
|
+
|
|
62
|
+
def test_handles_missing_model(self):
|
|
63
|
+
"""Should handle missing model gracefully."""
|
|
64
|
+
input_data = {"workspace": {"current_dir": "/tmp/test", "project_dir": "/tmp/test"}}
|
|
65
|
+
output, code = run_script(input_data)
|
|
66
|
+
assert code == 0
|
|
67
|
+
assert "Claude" in output # Default fallback
|
|
68
|
+
|
|
69
|
+
def test_handles_invalid_json(self):
|
|
70
|
+
"""Should handle invalid JSON gracefully."""
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
73
|
+
input="invalid json",
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
)
|
|
77
|
+
assert result.returncode == 0
|
|
78
|
+
assert "Claude" in result.stdout
|
|
79
|
+
|
|
80
|
+
def test_handles_empty_input(self):
|
|
81
|
+
"""Should handle empty input gracefully."""
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
84
|
+
input="",
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
)
|
|
88
|
+
assert result.returncode == 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestContextWindowColors:
|
|
92
|
+
"""Tests for context window color coding."""
|
|
93
|
+
|
|
94
|
+
def test_low_usage_has_output(self, low_usage_input):
|
|
95
|
+
"""Low usage (>50% free) should produce output with 'free'."""
|
|
96
|
+
output, code = run_script(low_usage_input)
|
|
97
|
+
assert code == 0
|
|
98
|
+
assert "free" in output
|
|
99
|
+
|
|
100
|
+
def test_medium_usage_has_output(self, medium_usage_input):
|
|
101
|
+
"""Medium usage (25-50% free) should produce output with 'free'."""
|
|
102
|
+
output, code = run_script(medium_usage_input)
|
|
103
|
+
assert code == 0
|
|
104
|
+
assert "free" in output
|
|
105
|
+
|
|
106
|
+
def test_high_usage_has_output(self, high_usage_input):
|
|
107
|
+
"""High usage (<25% free) should produce output with 'free'."""
|
|
108
|
+
output, code = run_script(high_usage_input)
|
|
109
|
+
assert code == 0
|
|
110
|
+
assert "free" in output
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestFixtures:
|
|
114
|
+
"""Tests using fixture files."""
|
|
115
|
+
|
|
116
|
+
def test_valid_full_fixture(self, valid_full_input):
|
|
117
|
+
"""Should handle valid_full.json fixture."""
|
|
118
|
+
output, code = run_script(valid_full_input)
|
|
119
|
+
assert code == 0
|
|
120
|
+
assert "Opus 4.5" in output
|
|
121
|
+
assert "my-project" in output
|
|
122
|
+
|
|
123
|
+
def test_valid_minimal_fixture(self, valid_minimal_input):
|
|
124
|
+
"""Should handle valid_minimal.json fixture."""
|
|
125
|
+
output, code = run_script(valid_minimal_input)
|
|
126
|
+
assert code == 0
|
|
127
|
+
assert "Claude" in output
|
|
128
|
+
|
|
129
|
+
def test_all_fixtures_succeed(self, fixtures_dir):
|
|
130
|
+
"""All JSON fixtures should be processed without errors."""
|
|
131
|
+
for fixture_file in fixtures_dir.glob("*.json"):
|
|
132
|
+
with open(fixture_file) as f:
|
|
133
|
+
input_data = json.load(f)
|
|
134
|
+
output, code = run_script(input_data)
|
|
135
|
+
assert code == 0, f"Failed on fixture: {fixture_file.name}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestSessionDisplay:
|
|
139
|
+
"""Tests for session_id display feature."""
|
|
140
|
+
|
|
141
|
+
def test_shows_session_id_by_default(self, sample_input):
|
|
142
|
+
"""Should show session_id by default (show_session=true)."""
|
|
143
|
+
sample_input["session_id"] = "test-session-12345"
|
|
144
|
+
output, code = run_script(sample_input)
|
|
145
|
+
assert code == 0
|
|
146
|
+
assert "test-session-12345" in output
|
|
147
|
+
|
|
148
|
+
def test_handles_missing_session_id(self, sample_input):
|
|
149
|
+
"""Should handle missing session_id gracefully."""
|
|
150
|
+
# Ensure no session_id in input
|
|
151
|
+
if "session_id" in sample_input:
|
|
152
|
+
del sample_input["session_id"]
|
|
153
|
+
output, code = run_script(sample_input)
|
|
154
|
+
assert code == 0
|