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.
Files changed (106) hide show
  1. package/package.json +8 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/.editorconfig +0 -60
  4. package/.eslintrc.json +0 -35
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  7. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  8. package/.github/dependabot.yml +0 -44
  9. package/.github/workflows/ci.yml +0 -294
  10. package/.github/workflows/release.yml +0 -151
  11. package/.pre-commit-config.yaml +0 -74
  12. package/.prettierrc +0 -33
  13. package/.shellcheckrc +0 -10
  14. package/CHANGELOG.md +0 -187
  15. package/CLAUDE.md +0 -66
  16. package/CODE_OF_CONDUCT.md +0 -59
  17. package/CONTRIBUTING.md +0 -240
  18. package/RELEASE_NOTES.md +0 -19
  19. package/SECURITY.md +0 -44
  20. package/TODOS.md +0 -72
  21. package/assets/logo/favicon.svg +0 -19
  22. package/assets/logo/logo-black.svg +0 -24
  23. package/assets/logo/logo-full.svg +0 -40
  24. package/assets/logo/logo-icon.svg +0 -27
  25. package/assets/logo/logo-mark.svg +0 -28
  26. package/assets/logo/logo-white.svg +0 -24
  27. package/assets/logo/logo-wordmark.svg +0 -6
  28. package/config/settings-example.json +0 -7
  29. package/config/settings-node.json +0 -7
  30. package/config/settings-python.json +0 -7
  31. package/docs/ARCHITECTURE.md +0 -128
  32. package/docs/CSV_FORMAT.md +0 -42
  33. package/docs/DEPLOYMENT.md +0 -71
  34. package/docs/DEVELOPMENT.md +0 -161
  35. package/docs/MODEL_INTELLIGENCE.md +0 -396
  36. package/docs/configuration.md +0 -118
  37. package/docs/context-stats.md +0 -143
  38. package/docs/installation.md +0 -255
  39. package/docs/scripts.md +0 -140
  40. package/docs/troubleshooting.md +0 -278
  41. package/images/claude-statusline-token-graph.gif +0 -0
  42. package/images/claude-statusline.png +0 -0
  43. package/images/context-status-dumbzone.png +0 -0
  44. package/images/context-status.png +0 -0
  45. package/images/statusline-detail.png +0 -0
  46. package/images/token-graph.jpeg +0 -0
  47. package/images/token-graph.png +0 -0
  48. package/images/v1.6.1.png +0 -0
  49. package/install +0 -351
  50. package/install.sh +0 -298
  51. package/jest.config.js +0 -11
  52. package/pyproject.toml +0 -115
  53. package/requirements-dev.txt +0 -12
  54. package/scripts/statusline-full.sh +0 -438
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -569
  58. package/src/claude_statusline/__init__.py +0 -11
  59. package/src/claude_statusline/__main__.py +0 -6
  60. package/src/claude_statusline/cli/__init__.py +0 -1
  61. package/src/claude_statusline/cli/context_stats.py +0 -542
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -184
  64. package/src/claude_statusline/core/__init__.py +0 -1
  65. package/src/claude_statusline/core/colors.py +0 -124
  66. package/src/claude_statusline/core/config.py +0 -165
  67. package/src/claude_statusline/core/git.py +0 -78
  68. package/src/claude_statusline/core/state.py +0 -323
  69. package/src/claude_statusline/formatters/__init__.py +0 -1
  70. package/src/claude_statusline/formatters/layout.py +0 -67
  71. package/src/claude_statusline/formatters/time.py +0 -50
  72. package/src/claude_statusline/formatters/tokens.py +0 -70
  73. package/src/claude_statusline/graphs/__init__.py +0 -1
  74. package/src/claude_statusline/graphs/intelligence.py +0 -162
  75. package/src/claude_statusline/graphs/renderer.py +0 -401
  76. package/src/claude_statusline/graphs/statistics.py +0 -92
  77. package/src/claude_statusline/ui/__init__.py +0 -1
  78. package/src/claude_statusline/ui/icons.py +0 -93
  79. package/src/claude_statusline/ui/waiting.py +0 -62
  80. package/tests/bash/test_delta_parity.bats +0 -199
  81. package/tests/bash/test_install.bats +0 -29
  82. package/tests/bash/test_parity.bats +0 -315
  83. package/tests/bash/test_statusline_full.bats +0 -139
  84. package/tests/bash/test_statusline_git.bats +0 -42
  85. package/tests/bash/test_statusline_minimal.bats +0 -37
  86. package/tests/fixtures/json/comma_in_path.json +0 -31
  87. package/tests/fixtures/json/high_usage.json +0 -17
  88. package/tests/fixtures/json/low_usage.json +0 -17
  89. package/tests/fixtures/json/medium_usage.json +0 -17
  90. package/tests/fixtures/json/valid_full.json +0 -30
  91. package/tests/fixtures/json/valid_minimal.json +0 -9
  92. package/tests/fixtures/mi_test_vectors.json +0 -140
  93. package/tests/node/intelligence.test.js +0 -98
  94. package/tests/node/rotation.test.js +0 -89
  95. package/tests/node/statusline.test.js +0 -240
  96. package/tests/python/conftest.py +0 -84
  97. package/tests/python/test_colors.py +0 -105
  98. package/tests/python/test_config_colors.py +0 -78
  99. package/tests/python/test_data_pipeline.py +0 -446
  100. package/tests/python/test_explain.py +0 -177
  101. package/tests/python/test_icons.py +0 -152
  102. package/tests/python/test_intelligence.py +0 -314
  103. package/tests/python/test_layout.py +0 -127
  104. package/tests/python/test_state_rotation_validation.py +0 -232
  105. package/tests/python/test_statusline.py +0 -215
  106. 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
- });
@@ -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
- }