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,140 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"description": "Fresh session - low utilization, good cache, productive",
|
|
4
|
-
"input": {
|
|
5
|
-
"current_used": 20000,
|
|
6
|
-
"context_window": 200000,
|
|
7
|
-
"cache_read": 12000,
|
|
8
|
-
"current_input": 5000,
|
|
9
|
-
"cache_creation": 3000,
|
|
10
|
-
"prev_lines_added": 0,
|
|
11
|
-
"prev_lines_removed": 0,
|
|
12
|
-
"cur_lines_added": 150,
|
|
13
|
-
"cur_lines_removed": 10,
|
|
14
|
-
"prev_output": 0,
|
|
15
|
-
"cur_output": 1000,
|
|
16
|
-
"beta": 1.5
|
|
17
|
-
},
|
|
18
|
-
"expected": {
|
|
19
|
-
"cps": 0.968,
|
|
20
|
-
"es": 0.72,
|
|
21
|
-
"ps": 0.84,
|
|
22
|
-
"mi": 0.887
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"description": "Mid-session - moderate utilization, some cache, moderate productivity",
|
|
27
|
-
"input": {
|
|
28
|
-
"current_used": 100000,
|
|
29
|
-
"context_window": 200000,
|
|
30
|
-
"cache_read": 40000,
|
|
31
|
-
"current_input": 35000,
|
|
32
|
-
"cache_creation": 25000,
|
|
33
|
-
"prev_lines_added": 50,
|
|
34
|
-
"prev_lines_removed": 10,
|
|
35
|
-
"cur_lines_added": 150,
|
|
36
|
-
"cur_lines_removed": 20,
|
|
37
|
-
"prev_output": 500,
|
|
38
|
-
"cur_output": 1500,
|
|
39
|
-
"beta": 1.5
|
|
40
|
-
},
|
|
41
|
-
"expected": {
|
|
42
|
-
"cps": 0.646,
|
|
43
|
-
"es": 0.58,
|
|
44
|
-
"ps": 0.64,
|
|
45
|
-
"mi": 0.629
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"description": "Late session - high utilization, low cache, low productivity",
|
|
50
|
-
"input": {
|
|
51
|
-
"current_used": 170000,
|
|
52
|
-
"context_window": 200000,
|
|
53
|
-
"cache_read": 34000,
|
|
54
|
-
"current_input": 85000,
|
|
55
|
-
"cache_creation": 51000,
|
|
56
|
-
"prev_lines_added": 200,
|
|
57
|
-
"prev_lines_removed": 50,
|
|
58
|
-
"cur_lines_added": 250,
|
|
59
|
-
"cur_lines_removed": 55,
|
|
60
|
-
"prev_output": 2000,
|
|
61
|
-
"cur_output": 3000,
|
|
62
|
-
"beta": 1.5
|
|
63
|
-
},
|
|
64
|
-
"expected": {
|
|
65
|
-
"cps": 0.217,
|
|
66
|
-
"es": 0.44,
|
|
67
|
-
"ps": 0.42,
|
|
68
|
-
"mi": 0.303
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"description": "No previous entry - PS defaults to 0.5",
|
|
73
|
-
"input": {
|
|
74
|
-
"current_used": 50000,
|
|
75
|
-
"context_window": 200000,
|
|
76
|
-
"cache_read": 30000,
|
|
77
|
-
"current_input": 10000,
|
|
78
|
-
"cache_creation": 10000,
|
|
79
|
-
"prev_lines_added": null,
|
|
80
|
-
"prev_lines_removed": null,
|
|
81
|
-
"cur_lines_added": 100,
|
|
82
|
-
"cur_lines_removed": 5,
|
|
83
|
-
"prev_output": null,
|
|
84
|
-
"cur_output": 800,
|
|
85
|
-
"beta": 1.5
|
|
86
|
-
},
|
|
87
|
-
"expected": {
|
|
88
|
-
"cps": 0.875,
|
|
89
|
-
"es": 0.72,
|
|
90
|
-
"ps": 0.5,
|
|
91
|
-
"mi": 0.780
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
"description": "Context window is zero - guard clause returns defaults",
|
|
96
|
-
"input": {
|
|
97
|
-
"current_used": 50000,
|
|
98
|
-
"context_window": 0,
|
|
99
|
-
"cache_read": 30000,
|
|
100
|
-
"current_input": 10000,
|
|
101
|
-
"cache_creation": 10000,
|
|
102
|
-
"prev_lines_added": 0,
|
|
103
|
-
"prev_lines_removed": 0,
|
|
104
|
-
"cur_lines_added": 100,
|
|
105
|
-
"cur_lines_removed": 5,
|
|
106
|
-
"prev_output": 0,
|
|
107
|
-
"cur_output": 800,
|
|
108
|
-
"beta": 1.5
|
|
109
|
-
},
|
|
110
|
-
"expected": {
|
|
111
|
-
"cps": 1.0,
|
|
112
|
-
"es": 1.0,
|
|
113
|
-
"ps": 0.5,
|
|
114
|
-
"mi": 1.0
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"description": "No cache at all - ES at minimum floor",
|
|
119
|
-
"input": {
|
|
120
|
-
"current_used": 80000,
|
|
121
|
-
"context_window": 200000,
|
|
122
|
-
"cache_read": 0,
|
|
123
|
-
"current_input": 80000,
|
|
124
|
-
"cache_creation": 0,
|
|
125
|
-
"prev_lines_added": 0,
|
|
126
|
-
"prev_lines_removed": 0,
|
|
127
|
-
"cur_lines_added": 50,
|
|
128
|
-
"cur_lines_removed": 10,
|
|
129
|
-
"prev_output": 0,
|
|
130
|
-
"cur_output": 500,
|
|
131
|
-
"beta": 1.5
|
|
132
|
-
},
|
|
133
|
-
"expected": {
|
|
134
|
-
"cps": 0.747,
|
|
135
|
-
"es": 0.3,
|
|
136
|
-
"ps": 0.68,
|
|
137
|
-
"mi": 0.625
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
]
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Model Intelligence (MI) score computation.
|
|
3
|
-
* Uses shared test vectors for cross-implementation parity.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const { computeMI } = require('../../scripts/statusline');
|
|
9
|
-
|
|
10
|
-
const VECTORS_PATH = path.join(__dirname, '..', 'fixtures', 'mi_test_vectors.json');
|
|
11
|
-
const vectors = JSON.parse(fs.readFileSync(VECTORS_PATH, 'utf8'));
|
|
12
|
-
|
|
13
|
-
describe('computeMI', () => {
|
|
14
|
-
test('guard clause: context_window=0 returns defaults', () => {
|
|
15
|
-
const result = computeMI(50000, 0, 30000, 50000, 0, null, 1.5);
|
|
16
|
-
expect(result.mi).toBe(1.0);
|
|
17
|
-
expect(result.cps).toBe(1.0);
|
|
18
|
-
expect(result.es).toBe(1.0);
|
|
19
|
-
expect(result.ps).toBe(0.5);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test('empty context returns CPS=1', () => {
|
|
23
|
-
const result = computeMI(0, 200000, 0, 0, 0, null, 1.5);
|
|
24
|
-
expect(result.cps).toBe(1.0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('full context returns CPS=0', () => {
|
|
28
|
-
const result = computeMI(200000, 200000, 0, 200000, 0, 100, 1.5);
|
|
29
|
-
expect(result.cps).toBe(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('no cache returns ES=0.3', () => {
|
|
33
|
-
const result = computeMI(100000, 200000, 0, 100000, 0, null, 1.5);
|
|
34
|
-
expect(result.es).toBeCloseTo(0.3, 1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('all cache returns ES=1.0', () => {
|
|
38
|
-
const result = computeMI(100000, 200000, 100000, 100000, 0, null, 1.5);
|
|
39
|
-
expect(result.es).toBeCloseTo(1.0, 1);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('no previous returns PS=0.5', () => {
|
|
43
|
-
const result = computeMI(100000, 200000, 50000, 100000, 0, null, 1.5);
|
|
44
|
-
expect(result.ps).toBe(0.5);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('no output returns PS=0.5', () => {
|
|
48
|
-
const result = computeMI(100000, 200000, 50000, 100000, 100, 0, 1.5);
|
|
49
|
-
expect(result.ps).toBe(0.5);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('MI is always between 0 and 1', () => {
|
|
53
|
-
const utilizations = [0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0];
|
|
54
|
-
for (const u of utilizations) {
|
|
55
|
-
const used = Math.floor(u * 200000);
|
|
56
|
-
const result = computeMI(used, 200000, used / 2, used, 50, 500, 1.5);
|
|
57
|
-
expect(result.mi).toBeGreaterThanOrEqual(0);
|
|
58
|
-
expect(result.mi).toBeLessThanOrEqual(1);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('shared test vectors', () => {
|
|
64
|
-
vectors.forEach((vec) => {
|
|
65
|
-
test(vec.description, () => {
|
|
66
|
-
const inp = vec.input;
|
|
67
|
-
const exp = vec.expected;
|
|
68
|
-
|
|
69
|
-
const hasPrev = inp.prev_output !== null;
|
|
70
|
-
let deltaLines, deltaOutput;
|
|
71
|
-
|
|
72
|
-
if (hasPrev) {
|
|
73
|
-
const deltaLA = inp.cur_lines_added - inp.prev_lines_added;
|
|
74
|
-
const deltaLR = inp.cur_lines_removed - inp.prev_lines_removed;
|
|
75
|
-
deltaLines = deltaLA + deltaLR;
|
|
76
|
-
deltaOutput = inp.cur_output - inp.prev_output;
|
|
77
|
-
} else {
|
|
78
|
-
deltaLines = 0;
|
|
79
|
-
deltaOutput = null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const result = computeMI(
|
|
83
|
-
inp.current_used,
|
|
84
|
-
inp.context_window,
|
|
85
|
-
inp.cache_read,
|
|
86
|
-
inp.current_used,
|
|
87
|
-
deltaLines,
|
|
88
|
-
deltaOutput,
|
|
89
|
-
inp.beta
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
expect(result.cps).toBeCloseTo(exp.cps, 1);
|
|
93
|
-
expect(result.es).toBeCloseTo(exp.es, 1);
|
|
94
|
-
expect(result.ps).toBeCloseTo(exp.ps, 1);
|
|
95
|
-
expect(result.mi).toBeCloseTo(exp.mi, 1);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
|
-
// Import rotation function from statusline.js
|
|
6
|
-
// The script reads stdin on require, so we mock stdin to prevent hanging
|
|
7
|
-
const originalStdin = process.stdin;
|
|
8
|
-
|
|
9
|
-
// Prevent the script's stdin listener from blocking
|
|
10
|
-
jest.spyOn(process.stdin, 'setEncoding').mockImplementation(() => {});
|
|
11
|
-
jest.spyOn(process.stdin, 'on').mockImplementation(() => {});
|
|
12
|
-
|
|
13
|
-
const { maybeRotateStateFile, ROTATION_THRESHOLD, ROTATION_KEEP } = require('../../scripts/statusline.js');
|
|
14
|
-
|
|
15
|
-
function makeCsvLine(index) {
|
|
16
|
-
return `${1710288000 + index},100,200,300,400,500,600,0.01,10,5,sess-${index},model,/tmp/proj,200000`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('maybeRotateStateFile', () => {
|
|
20
|
-
let tmpDir;
|
|
21
|
-
let stateFile;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rotation-test-'));
|
|
25
|
-
stateFile = path.join(tmpDir, 'test.state');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('file below threshold is not rotated', () => {
|
|
33
|
-
const lines = Array.from({ length: 9999 }, (_, i) => makeCsvLine(i));
|
|
34
|
-
fs.writeFileSync(stateFile, lines.join('\n') + '\n');
|
|
35
|
-
|
|
36
|
-
maybeRotateStateFile(stateFile);
|
|
37
|
-
|
|
38
|
-
const result = fs.readFileSync(stateFile, 'utf8').trim().split('\n');
|
|
39
|
-
expect(result.length).toBe(9999);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('file at exactly threshold is not rotated', () => {
|
|
43
|
-
const lines = Array.from({ length: ROTATION_THRESHOLD }, (_, i) => makeCsvLine(i));
|
|
44
|
-
fs.writeFileSync(stateFile, lines.join('\n') + '\n');
|
|
45
|
-
|
|
46
|
-
maybeRotateStateFile(stateFile);
|
|
47
|
-
|
|
48
|
-
const result = fs.readFileSync(stateFile, 'utf8').trim().split('\n');
|
|
49
|
-
expect(result.length).toBe(ROTATION_THRESHOLD);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('file exceeding threshold is truncated to ROTATION_KEEP lines', () => {
|
|
53
|
-
const lines = Array.from({ length: 10001 }, (_, i) => makeCsvLine(i));
|
|
54
|
-
fs.writeFileSync(stateFile, lines.join('\n') + '\n');
|
|
55
|
-
|
|
56
|
-
maybeRotateStateFile(stateFile);
|
|
57
|
-
|
|
58
|
-
const result = fs.readFileSync(stateFile, 'utf8').trim().split('\n');
|
|
59
|
-
expect(result.length).toBe(ROTATION_KEEP);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('retained lines are the most recent', () => {
|
|
63
|
-
const total = 10001;
|
|
64
|
-
const lines = Array.from({ length: total }, (_, i) => makeCsvLine(i));
|
|
65
|
-
fs.writeFileSync(stateFile, lines.join('\n') + '\n');
|
|
66
|
-
|
|
67
|
-
maybeRotateStateFile(stateFile);
|
|
68
|
-
|
|
69
|
-
const result = fs.readFileSync(stateFile, 'utf8').trim().split('\n');
|
|
70
|
-
// First retained line should be index (total - ROTATION_KEEP)
|
|
71
|
-
expect(result[0]).toContain(`sess-${total - ROTATION_KEEP}`);
|
|
72
|
-
// Last retained line should be the last original line
|
|
73
|
-
expect(result[result.length - 1]).toContain(`sess-${total - 1}`);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('non-existent file does not throw', () => {
|
|
77
|
-
expect(() => maybeRotateStateFile('/tmp/nonexistent-rotation-test.state')).not.toThrow();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('no temp files remain after rotation', () => {
|
|
81
|
-
const lines = Array.from({ length: 10001 }, (_, i) => makeCsvLine(i));
|
|
82
|
-
fs.writeFileSync(stateFile, lines.join('\n') + '\n');
|
|
83
|
-
|
|
84
|
-
maybeRotateStateFile(stateFile);
|
|
85
|
-
|
|
86
|
-
const tmpFiles = fs.readdirSync(tmpDir).filter(f => f.endsWith('.tmp'));
|
|
87
|
-
expect(tmpFiles.length).toBe(0);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,240 +0,0 @@
|
|
|
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
|
-
* Strip ANSI escape sequences from a string
|
|
10
|
-
*/
|
|
11
|
-
function stripAnsi(s) {
|
|
12
|
-
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Run the statusline.js script with the given input data
|
|
17
|
-
* @param {Object|string} inputData - JSON input or string
|
|
18
|
-
* @param {Object} [envOverrides] - Optional environment variable overrides
|
|
19
|
-
* @returns {Promise<{stdout: string, stderr: string, code: number}>}
|
|
20
|
-
*/
|
|
21
|
-
function runScript(inputData, envOverrides) {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
const env = { ...process.env, ...envOverrides };
|
|
24
|
-
const child = spawn('node', [SCRIPT_PATH], { env });
|
|
25
|
-
let stdout = '';
|
|
26
|
-
let stderr = '';
|
|
27
|
-
|
|
28
|
-
child.stdout.on('data', data => {
|
|
29
|
-
stdout += data.toString();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
child.stderr.on('data', data => {
|
|
33
|
-
stderr += data.toString();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
child.on('close', code => {
|
|
37
|
-
resolve({ stdout: stdout.trim(), stderr, code });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
child.on('error', reject);
|
|
41
|
-
|
|
42
|
-
const input = typeof inputData === 'string' ? inputData : JSON.stringify(inputData);
|
|
43
|
-
child.stdin.write(input);
|
|
44
|
-
child.stdin.end();
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Load a JSON fixture file
|
|
50
|
-
* @param {string} name - Fixture name without .json extension
|
|
51
|
-
* @returns {Object}
|
|
52
|
-
*/
|
|
53
|
-
function loadFixture(name) {
|
|
54
|
-
const filePath = path.join(FIXTURES_DIR, `${name}.json`);
|
|
55
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
describe('statusline.js', () => {
|
|
59
|
-
const sampleInput = {
|
|
60
|
-
model: { display_name: 'Claude 3.5 Sonnet' },
|
|
61
|
-
workspace: {
|
|
62
|
-
current_dir: '/home/user/myproject',
|
|
63
|
-
project_dir: '/home/user/myproject',
|
|
64
|
-
},
|
|
65
|
-
context_window: {
|
|
66
|
-
context_window_size: 200000,
|
|
67
|
-
current_usage: {
|
|
68
|
-
input_tokens: 10000,
|
|
69
|
-
cache_creation_input_tokens: 500,
|
|
70
|
-
cache_read_input_tokens: 200,
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
describe('Script basics', () => {
|
|
76
|
-
test('script file exists', () => {
|
|
77
|
-
expect(fs.existsSync(SCRIPT_PATH)).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('script has node shebang', () => {
|
|
81
|
-
const content = fs.readFileSync(SCRIPT_PATH, 'utf8');
|
|
82
|
-
expect(content.startsWith('#!/usr/bin/env node')).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('Output content', () => {
|
|
87
|
-
test('outputs model name', async () => {
|
|
88
|
-
const result = await runScript(sampleInput);
|
|
89
|
-
expect(result.stdout).toContain('Claude 3.5 Sonnet');
|
|
90
|
-
expect(result.code).toBe(0);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('outputs directory name', async () => {
|
|
94
|
-
const result = await runScript(sampleInput);
|
|
95
|
-
expect(result.stdout).toContain('myproject');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('shows free tokens indicator', async () => {
|
|
99
|
-
const result = await runScript(sampleInput);
|
|
100
|
-
expect(result.stdout).toContain('%');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('shows AC indicator', async () => {
|
|
104
|
-
const result = await runScript(sampleInput);
|
|
105
|
-
expect(result.stdout).toContain('[AC:');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('shows percentage', async () => {
|
|
109
|
-
const result = await runScript(sampleInput);
|
|
110
|
-
expect(result.stdout).toMatch(/\d+\.\d+%/);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('Error handling', () => {
|
|
115
|
-
test('handles missing model gracefully', async () => {
|
|
116
|
-
const input = {
|
|
117
|
-
workspace: { current_dir: '/tmp/test', project_dir: '/tmp/test' },
|
|
118
|
-
};
|
|
119
|
-
const result = await runScript(input);
|
|
120
|
-
expect(result.stdout).toContain('Claude'); // Default fallback
|
|
121
|
-
expect(result.code).toBe(0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('handles missing context window gracefully', async () => {
|
|
125
|
-
const input = {
|
|
126
|
-
model: { display_name: 'Claude' },
|
|
127
|
-
workspace: { current_dir: '/tmp/test', project_dir: '/tmp/test' },
|
|
128
|
-
};
|
|
129
|
-
const result = await runScript(input);
|
|
130
|
-
expect(result.code).toBe(0);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('handles invalid JSON gracefully', async () => {
|
|
134
|
-
const result = await runScript('invalid json');
|
|
135
|
-
expect(result.code).toBe(0);
|
|
136
|
-
expect(result.stdout).toContain('Claude');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('handles empty input gracefully', async () => {
|
|
140
|
-
const result = await runScript('');
|
|
141
|
-
expect(result.code).toBe(0);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('Fixtures', () => {
|
|
146
|
-
test('handles valid_full fixture', async () => {
|
|
147
|
-
const input = loadFixture('valid_full');
|
|
148
|
-
const result = await runScript(input);
|
|
149
|
-
expect(result.code).toBe(0);
|
|
150
|
-
expect(result.stdout).toContain('Opus 4.5');
|
|
151
|
-
expect(result.stdout).toContain('my-project');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('handles valid_minimal fixture', async () => {
|
|
155
|
-
const input = loadFixture('valid_minimal');
|
|
156
|
-
const result = await runScript(input);
|
|
157
|
-
expect(result.code).toBe(0);
|
|
158
|
-
expect(result.stdout).toContain('Claude');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('handles low_usage fixture', async () => {
|
|
162
|
-
const input = loadFixture('low_usage');
|
|
163
|
-
const result = await runScript(input);
|
|
164
|
-
expect(result.code).toBe(0);
|
|
165
|
-
expect(result.stdout).toContain('%');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('handles medium_usage fixture', async () => {
|
|
169
|
-
const input = loadFixture('medium_usage');
|
|
170
|
-
const result = await runScript(input);
|
|
171
|
-
expect(result.code).toBe(0);
|
|
172
|
-
expect(result.stdout).toContain('%');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test('handles high_usage fixture', async () => {
|
|
176
|
-
const input = loadFixture('high_usage');
|
|
177
|
-
const result = await runScript(input);
|
|
178
|
-
expect(result.code).toBe(0);
|
|
179
|
-
expect(result.stdout).toContain('%');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('all JSON fixtures succeed', async () => {
|
|
183
|
-
const fixtures = fs.readdirSync(FIXTURES_DIR).filter(f => f.endsWith('.json'));
|
|
184
|
-
for (const fixture of fixtures) {
|
|
185
|
-
const input = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, fixture), 'utf8'));
|
|
186
|
-
const result = await runScript(input);
|
|
187
|
-
expect(result.code).toBe(0);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe('Session ID display', () => {
|
|
193
|
-
test('shows session_id by default', async () => {
|
|
194
|
-
const inputWithSession = {
|
|
195
|
-
...sampleInput,
|
|
196
|
-
session_id: 'test-session-abc123',
|
|
197
|
-
};
|
|
198
|
-
const result = await runScript(inputWithSession, { COLUMNS: '200' });
|
|
199
|
-
expect(result.code).toBe(0);
|
|
200
|
-
expect(result.stdout).toContain('test-session-abc123');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('handles missing session_id gracefully', async () => {
|
|
204
|
-
const result = await runScript(sampleInput);
|
|
205
|
-
expect(result.code).toBe(0);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe('Width truncation', () => {
|
|
210
|
-
test('output fits 80 columns', async () => {
|
|
211
|
-
const inputWithSession = {
|
|
212
|
-
...sampleInput,
|
|
213
|
-
session_id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
|
214
|
-
};
|
|
215
|
-
const result = await runScript(inputWithSession, { COLUMNS: '80' });
|
|
216
|
-
expect(result.code).toBe(0);
|
|
217
|
-
const visible = stripAnsi(result.stdout);
|
|
218
|
-
expect(visible.length).toBeLessThanOrEqual(80);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('narrow terminal drops parts', async () => {
|
|
222
|
-
const result = await runScript(sampleInput, { COLUMNS: '40' });
|
|
223
|
-
expect(result.code).toBe(0);
|
|
224
|
-
const visible = stripAnsi(result.stdout);
|
|
225
|
-
expect(visible.length).toBeLessThanOrEqual(40);
|
|
226
|
-
expect(visible).toContain('Claude 3.5 Sonnet');
|
|
227
|
-
expect(visible).toContain('myproject');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('wide terminal shows all', async () => {
|
|
231
|
-
const inputWithSession = {
|
|
232
|
-
...sampleInput,
|
|
233
|
-
session_id: 'test-wide-session-uuid',
|
|
234
|
-
};
|
|
235
|
-
const result = await runScript(inputWithSession, { COLUMNS: '200' });
|
|
236
|
-
expect(result.code).toBe(0);
|
|
237
|
-
expect(result.stdout).toContain('test-wide-session-uuid');
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
});
|
package/tests/python/conftest.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
}
|