mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
/**
|
|
* Tests for scripts/lib/statusline-render.js
|
|
*
|
|
* Run with: node tests/lib/statusline-render.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
C,
|
|
G,
|
|
pctColor,
|
|
buildBar,
|
|
formatCountdown,
|
|
formatMs,
|
|
computeCacheStats,
|
|
buildUsageLine,
|
|
buildSessionLine,
|
|
buildEccLine,
|
|
readInstalledPlugins,
|
|
getHooksSummary,
|
|
} = require('../../scripts/lib/statusline-render');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` \u2713 ${name}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(` \u2717 ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function runTests() {
|
|
console.log('\n=== Testing statusline-render.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const t = (name, fn) => (test(name, fn) ? passed++ : failed++);
|
|
|
|
console.log('buildBar:');
|
|
t('0% renders all empty blocks', () => {
|
|
assert.strictEqual(buildBar(0, 8), G.empty.repeat(8));
|
|
});
|
|
t('100% renders all full blocks', () => {
|
|
assert.strictEqual(buildBar(100, 8), G.full.repeat(8));
|
|
});
|
|
t('50% renders half full', () => {
|
|
assert.strictEqual(buildBar(50, 8), G.full.repeat(4) + G.empty.repeat(4));
|
|
});
|
|
t('clamps values beyond 100', () => {
|
|
assert.strictEqual(buildBar(250, 4), G.full.repeat(4));
|
|
});
|
|
t('clamps negative values', () => {
|
|
assert.strictEqual(buildBar(-10, 4), G.empty.repeat(4));
|
|
});
|
|
|
|
console.log('\nglyphSet:');
|
|
t('non-UTF-8 locale falls back to ASCII glyphs', () => {
|
|
const { glyphSet, ASCII_GLYPHS } = require('../../scripts/lib/statusline-render');
|
|
assert.strictEqual(glyphSet({ LANG: 'C' }), ASCII_GLYPHS);
|
|
assert.strictEqual(glyphSet({}), ASCII_GLYPHS);
|
|
});
|
|
t('UTF-8 locale uses unicode glyphs', () => {
|
|
const { glyphSet, UNICODE_GLYPHS } = require('../../scripts/lib/statusline-render');
|
|
assert.strictEqual(glyphSet({ LANG: 'en_US.UTF-8' }), UNICODE_GLYPHS);
|
|
assert.strictEqual(glyphSet({ LC_ALL: 'C.utf8' }), UNICODE_GLYPHS);
|
|
});
|
|
t('ECC_BAR_GLYPHS forces either set', () => {
|
|
const { glyphSet, ASCII_GLYPHS, UNICODE_GLYPHS } = require('../../scripts/lib/statusline-render');
|
|
assert.strictEqual(glyphSet({ LANG: 'en_US.UTF-8', ECC_BAR_GLYPHS: 'ascii' }), ASCII_GLYPHS);
|
|
assert.strictEqual(glyphSet({ LANG: 'C', ECC_BAR_GLYPHS: 'unicode' }), UNICODE_GLYPHS);
|
|
});
|
|
|
|
console.log('\npctColor:');
|
|
t('below 60 uses brand color', () => {
|
|
assert.strictEqual(pctColor(59, C.amber), C.amber);
|
|
});
|
|
t('60-79 is yellow', () => {
|
|
assert.strictEqual(pctColor(60), C.yellow);
|
|
});
|
|
t('80-89 is orange', () => {
|
|
assert.strictEqual(pctColor(80), C.orange);
|
|
});
|
|
t('90+ is red', () => {
|
|
assert.strictEqual(pctColor(95), C.red);
|
|
});
|
|
|
|
console.log('\nformatCountdown:');
|
|
t('past timestamp returns empty', () => {
|
|
assert.strictEqual(formatCountdown(Date.now() / 1000 - 60), '');
|
|
});
|
|
t('invalid input returns empty', () => {
|
|
assert.strictEqual(formatCountdown(null), '');
|
|
assert.strictEqual(formatCountdown('soon'), '');
|
|
});
|
|
t('minutes-scale countdown ends with m', () => {
|
|
const result = formatCountdown(Date.now() / 1000 + 42 * 60);
|
|
assert.ok(/^\d+m$/.test(result), `got: ${result}`);
|
|
});
|
|
t('hours-scale countdown contains h', () => {
|
|
const result = formatCountdown(Date.now() / 1000 + 90 * 60);
|
|
assert.ok(result.includes('h'), `got: ${result}`);
|
|
});
|
|
t('days-scale countdown ends with d', () => {
|
|
const result = formatCountdown(Date.now() / 1000 + 3 * 24 * 3600 + 60);
|
|
assert.ok(/^\d+d$/.test(result), `got: ${result}`);
|
|
});
|
|
|
|
console.log('\nformatMs:');
|
|
t('zero returns empty', () => {
|
|
assert.strictEqual(formatMs(0), '');
|
|
});
|
|
t('seconds scale', () => {
|
|
assert.strictEqual(formatMs(30 * 1000), '30s');
|
|
});
|
|
t('minutes scale', () => {
|
|
assert.strictEqual(formatMs(5 * 60 * 1000), '5m');
|
|
});
|
|
t('hours scale with remainder', () => {
|
|
assert.strictEqual(formatMs((3 * 60 + 11) * 60 * 1000), '3h11m');
|
|
});
|
|
|
|
console.log('\ncomputeCacheStats:');
|
|
t('null usage returns null', () => {
|
|
assert.strictEqual(computeCacheStats(null), null);
|
|
});
|
|
t('zero tokens returns null', () => {
|
|
assert.strictEqual(computeCacheStats({ input_tokens: 0 }), null);
|
|
});
|
|
t('computes hit percentage from cache reads', () => {
|
|
const stats = computeCacheStats({
|
|
input_tokens: 10,
|
|
cache_creation_input_tokens: 10,
|
|
cache_read_input_tokens: 80,
|
|
});
|
|
assert.strictEqual(stats.hitPct, 80);
|
|
});
|
|
|
|
console.log('\nbuildUsageLine:');
|
|
t('no rate limits returns empty string', () => {
|
|
assert.strictEqual(buildUsageLine(undefined, { hitPct: 90 }), '');
|
|
assert.strictEqual(buildUsageLine({}, null), '');
|
|
});
|
|
t('renders both windows with percentages', () => {
|
|
const line = buildUsageLine({
|
|
five_hour: { used_percentage: 71, resets_at: Date.now() / 1000 + 3600 },
|
|
seven_day: { used_percentage: 23, resets_at: Date.now() / 1000 + 86400 * 3 },
|
|
}, null);
|
|
assert.ok(line.includes('5h'), 'expected 5h label');
|
|
assert.ok(line.includes('7d'), 'expected 7d label');
|
|
assert.ok(line.includes('71%'), 'expected 5h percentage');
|
|
assert.ok(line.includes('23%'), 'expected 7d percentage');
|
|
assert.ok(line.includes(G.reset), 'expected reset countdown');
|
|
});
|
|
t('renders a single window when only one exists', () => {
|
|
const line = buildUsageLine({ five_hour: { used_percentage: 5 } }, null);
|
|
assert.ok(line.includes('5h'), 'expected 5h label');
|
|
assert.ok(!line.includes('7d'), 'no 7d label expected');
|
|
});
|
|
t('appends cache hit rate when bars exist', () => {
|
|
const line = buildUsageLine({ five_hour: { used_percentage: 5 } }, { hitPct: 98 });
|
|
assert.ok(line.includes('cache'), 'expected cache segment');
|
|
assert.ok(line.includes('98%'), 'expected cache percentage');
|
|
});
|
|
|
|
console.log('\nbuildSessionLine:');
|
|
t('shows model with effort badge', () => {
|
|
const line = buildSessionLine({ model: 'Opus 5', effort: 'xhigh' });
|
|
assert.ok(line.includes('Opus 5'), 'expected model name');
|
|
assert.ok(line.includes('xhigh'), 'expected effort badge');
|
|
});
|
|
t('default effort high shows no badge', () => {
|
|
const line = buildSessionLine({ model: 'Opus 5', effort: 'high' });
|
|
assert.ok(!line.includes('high'), 'no badge expected for default effort');
|
|
});
|
|
t('shows cost, diff, and task', () => {
|
|
const line = buildSessionLine({
|
|
model: 'Opus 5',
|
|
costUsd: 1.234,
|
|
linesAdded: 11,
|
|
linesRemoved: 4,
|
|
durationMs: 60 * 1000,
|
|
task: 'Fixing auth bug',
|
|
});
|
|
assert.ok(line.includes('$1.23'), 'expected cost');
|
|
assert.ok(line.includes('+11/-4'), 'expected diff stats');
|
|
assert.ok(line.includes('1m'), 'expected duration');
|
|
assert.ok(line.includes('Fixing auth bug'), 'expected task');
|
|
});
|
|
t('shows 1M context window size', () => {
|
|
const line = buildSessionLine({ model: 'Opus 5', ctxBar: ' bar 10%', ctxWindowSize: 1000000 });
|
|
assert.ok(line.includes('1M'), 'expected 1M badge');
|
|
});
|
|
|
|
console.log('\nbuildEccLine:');
|
|
t('shows version, hooks profile, plugins, dirname', () => {
|
|
const line = buildEccLine({
|
|
eccVersion: '2.2.0',
|
|
hooks: { enabled: true, profile: 'standard', disabledCount: 0 },
|
|
plugins: [{ name: 'ecc', version: '2.2.0' }, { name: 'swift-lsp', version: '1.0.0' }],
|
|
dirname: 'myproject',
|
|
});
|
|
assert.ok(line.includes('ECC 2.2.0'), 'expected ECC version');
|
|
assert.ok(line.includes('hooks standard'), 'expected hooks profile');
|
|
assert.ok(line.includes('swift-lsp'), 'expected plugin name');
|
|
assert.ok(line.includes('1.0.0'), 'expected plugin version');
|
|
assert.ok(line.includes('myproject'), 'expected dirname');
|
|
});
|
|
t('disabled hooks render hooks off', () => {
|
|
const line = buildEccLine({ hooks: { enabled: false, profile: 'standard', disabledCount: 0 } });
|
|
assert.ok(line.includes('hooks off'), 'expected hooks off');
|
|
});
|
|
t('disabled hook count is shown', () => {
|
|
const line = buildEccLine({ hooks: { enabled: true, profile: 'minimal', disabledCount: 2 } });
|
|
assert.ok(line.includes('(2 off)'), 'expected disabled count');
|
|
});
|
|
t('caps plugin list at 4 with overflow count', () => {
|
|
const plugins = ['a', 'b', 'c', 'd', 'e', 'f'].map(name => ({ name, version: '1.0.0' }));
|
|
const line = buildEccLine({ plugins });
|
|
assert.ok(line.includes('+2'), 'expected +2 overflow');
|
|
});
|
|
|
|
console.log('\nreadInstalledPlugins:');
|
|
t('reads v2 plugin file and filters disabled plugins, ecc first', () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-render-test-'));
|
|
try {
|
|
fs.mkdirSync(path.join(tmpDir, 'plugins'), { recursive: true });
|
|
fs.writeFileSync(path.join(tmpDir, 'plugins', 'installed_plugins.json'), JSON.stringify({
|
|
version: 2,
|
|
plugins: {
|
|
'zeta@market': [{ version: '0.5.0', scope: 'user' }],
|
|
'ecc@ecc': [{ version: '2.2.0', scope: 'user' }],
|
|
'off-plugin@market': [{ version: '9.9.9', scope: 'user' }],
|
|
},
|
|
}));
|
|
fs.writeFileSync(path.join(tmpDir, 'settings.json'), JSON.stringify({
|
|
enabledPlugins: { 'zeta@market': true, 'ecc@ecc': true, 'off-plugin@market': false },
|
|
}));
|
|
|
|
const plugins = readInstalledPlugins(tmpDir);
|
|
assert.strictEqual(plugins.length, 2);
|
|
assert.strictEqual(plugins[0].name, 'ecc');
|
|
assert.strictEqual(plugins[0].version, '2.2.0');
|
|
assert.strictEqual(plugins[1].name, 'zeta');
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
t('missing plugin file returns empty array', () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-render-test-'));
|
|
try {
|
|
assert.deepStrictEqual(readInstalledPlugins(tmpDir), []);
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
console.log('\ngetHooksSummary:');
|
|
t('defaults to enabled standard profile', () => {
|
|
const summary = getHooksSummary({});
|
|
assert.strictEqual(summary.enabled, true);
|
|
assert.strictEqual(summary.profile, 'standard');
|
|
assert.strictEqual(summary.disabledCount, 0);
|
|
});
|
|
t('respects ECC_HOOKS_ENABLED=false', () => {
|
|
assert.strictEqual(getHooksSummary({ ECC_HOOKS_ENABLED: 'false' }).enabled, false);
|
|
});
|
|
t('reports profile and disabled hook count', () => {
|
|
const summary = getHooksSummary({
|
|
ECC_HOOK_PROFILE: 'minimal',
|
|
ECC_DISABLED_HOOKS: 'a,b',
|
|
});
|
|
assert.strictEqual(summary.profile, 'minimal');
|
|
assert.strictEqual(summary.disabledCount, 2);
|
|
});
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}\n`);
|
|
return { passed, failed };
|
|
}
|
|
|
|
const { failed } = runTests();
|
|
process.exit(failed > 0 ? 1 : 0);
|