mirror of
https://github.com/jarrodwatts/claude-hud.git
synced 2026-05-03 11:02:39 +00:00
392 lines
14 KiB
JavaScript
392 lines
14 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
loadConfig,
|
|
getConfigPath,
|
|
mergeConfig,
|
|
DEFAULT_CONFIG,
|
|
DEFAULT_ELEMENT_ORDER,
|
|
} from '../dist/config.js';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
function restoreEnvVar(name, value) {
|
|
if (value === undefined) {
|
|
delete process.env[name];
|
|
return;
|
|
}
|
|
process.env[name] = value;
|
|
}
|
|
|
|
test('loadConfig returns valid config structure', async () => {
|
|
const config = await loadConfig();
|
|
|
|
// pathLevels must be 1, 2, or 3
|
|
assert.ok([1, 2, 3].includes(config.pathLevels), 'pathLevels should be 1, 2, or 3');
|
|
|
|
// lineLayout must be valid
|
|
const validLineLayouts = ['compact', 'expanded'];
|
|
assert.ok(validLineLayouts.includes(config.lineLayout), 'lineLayout should be valid');
|
|
|
|
// showSeparators must be boolean
|
|
assert.equal(typeof config.showSeparators, 'boolean', 'showSeparators should be boolean');
|
|
assert.ok(Array.isArray(config.elementOrder), 'elementOrder should be an array');
|
|
assert.ok(config.elementOrder.length > 0, 'elementOrder should not be empty');
|
|
assert.deepEqual(config.elementOrder, DEFAULT_ELEMENT_ORDER, 'elementOrder should default to the full expanded layout');
|
|
|
|
// gitStatus object with expected properties
|
|
assert.equal(typeof config.gitStatus, 'object');
|
|
assert.equal(typeof config.gitStatus.enabled, 'boolean');
|
|
assert.equal(typeof config.gitStatus.showDirty, 'boolean');
|
|
assert.equal(typeof config.gitStatus.showAheadBehind, 'boolean');
|
|
|
|
// display object with expected properties
|
|
assert.equal(typeof config.display, 'object');
|
|
assert.equal(typeof config.display.showModel, 'boolean');
|
|
assert.equal(typeof config.display.showContextBar, 'boolean');
|
|
assert.ok(['percent', 'tokens', 'remaining', 'both'].includes(config.display.contextValue), 'contextValue should be valid');
|
|
assert.equal(typeof config.display.showConfigCounts, 'boolean');
|
|
assert.equal(typeof config.display.showDuration, 'boolean');
|
|
assert.equal(typeof config.display.showSpeed, 'boolean');
|
|
assert.equal(typeof config.display.showTokenBreakdown, 'boolean');
|
|
assert.equal(typeof config.display.showUsage, 'boolean');
|
|
assert.equal(typeof config.display.showTools, 'boolean');
|
|
assert.equal(typeof config.display.showAgents, 'boolean');
|
|
assert.equal(typeof config.display.showTodos, 'boolean');
|
|
assert.equal(typeof config.display.showSessionName, 'boolean');
|
|
assert.equal(typeof config.display.showClaudeCodeVersion, 'boolean');
|
|
assert.equal(typeof config.display.showMemoryUsage, 'boolean');
|
|
assert.equal(typeof config.colors, 'object');
|
|
for (const key of ['context', 'usage', 'warning', 'usageWarning', 'critical']) {
|
|
const t = typeof config.colors[key];
|
|
assert.ok(t === 'string' || t === 'number', `colors.${key} should be string or number, got ${t}`);
|
|
}
|
|
});
|
|
|
|
test('getConfigPath returns correct path', () => {
|
|
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
delete process.env.CLAUDE_CONFIG_DIR;
|
|
|
|
try {
|
|
const configPath = getConfigPath();
|
|
const homeDir = os.homedir();
|
|
assert.equal(configPath, path.join(homeDir, '.claude', 'plugins', 'claude-hud', 'config.json'));
|
|
} finally {
|
|
restoreEnvVar('CLAUDE_CONFIG_DIR', originalConfigDir);
|
|
}
|
|
});
|
|
|
|
test('mergeConfig defaults showSessionName to false', () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.display.showSessionName, false);
|
|
assert.equal(DEFAULT_CONFIG.display.showSessionName, false);
|
|
});
|
|
|
|
test('mergeConfig preserves explicit showSessionName=true', () => {
|
|
const config = mergeConfig({ display: { showSessionName: true } });
|
|
assert.equal(config.display.showSessionName, true);
|
|
});
|
|
|
|
test('mergeConfig defaults showClaudeCodeVersion to false', () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.display.showClaudeCodeVersion, false);
|
|
assert.equal(DEFAULT_CONFIG.display.showClaudeCodeVersion, false);
|
|
});
|
|
|
|
test('mergeConfig preserves explicit showClaudeCodeVersion=true', () => {
|
|
const config = mergeConfig({ display: { showClaudeCodeVersion: true } });
|
|
assert.equal(config.display.showClaudeCodeVersion, true);
|
|
});
|
|
|
|
test('mergeConfig defaults showMemoryUsage to false', () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.display.showMemoryUsage, false);
|
|
assert.equal(DEFAULT_CONFIG.display.showMemoryUsage, false);
|
|
});
|
|
|
|
test('mergeConfig preserves explicit showMemoryUsage=true', () => {
|
|
const config = mergeConfig({ display: { showMemoryUsage: true } });
|
|
assert.equal(config.display.showMemoryUsage, true);
|
|
});
|
|
|
|
test('mergeConfig preserves customLine and truncates long values', () => {
|
|
const customLine = 'x'.repeat(120);
|
|
const config = mergeConfig({ display: { customLine } });
|
|
assert.equal(config.display.customLine.length, 80);
|
|
assert.equal(config.display.customLine, customLine.slice(0, 80));
|
|
});
|
|
|
|
test('getConfigPath respects CLAUDE_CONFIG_DIR', async () => {
|
|
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
const customConfigDir = await mkdtemp(path.join(tmpdir(), 'claude-hud-config-dir-'));
|
|
|
|
try {
|
|
process.env.CLAUDE_CONFIG_DIR = customConfigDir;
|
|
const configPath = getConfigPath();
|
|
assert.equal(configPath, path.join(customConfigDir, 'plugins', 'claude-hud', 'config.json'));
|
|
} finally {
|
|
restoreEnvVar('CLAUDE_CONFIG_DIR', originalConfigDir);
|
|
await rm(customConfigDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('loadConfig reads user config from CLAUDE_CONFIG_DIR', async () => {
|
|
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
const customConfigDir = await mkdtemp(path.join(tmpdir(), 'claude-hud-config-load-'));
|
|
|
|
try {
|
|
process.env.CLAUDE_CONFIG_DIR = customConfigDir;
|
|
const pluginDir = path.join(customConfigDir, 'plugins', 'claude-hud');
|
|
await mkdir(pluginDir, { recursive: true });
|
|
await writeFile(
|
|
path.join(pluginDir, 'config.json'),
|
|
JSON.stringify({
|
|
lineLayout: 'compact',
|
|
pathLevels: 2,
|
|
display: { showSpeed: true },
|
|
}),
|
|
'utf8'
|
|
);
|
|
|
|
const config = await loadConfig();
|
|
assert.equal(config.lineLayout, 'compact');
|
|
assert.equal(config.pathLevels, 2);
|
|
assert.equal(config.display.showSpeed, true);
|
|
} finally {
|
|
restoreEnvVar('CLAUDE_CONFIG_DIR', originalConfigDir);
|
|
await rm(customConfigDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// --- migrateConfig tests (via mergeConfig) ---
|
|
|
|
test('migrate legacy layout: "default" -> compact, no separators', () => {
|
|
const config = mergeConfig({ layout: 'default' });
|
|
assert.equal(config.lineLayout, 'compact');
|
|
assert.equal(config.showSeparators, false);
|
|
});
|
|
|
|
test('migrate legacy layout: "separators" -> compact, with separators', () => {
|
|
const config = mergeConfig({ layout: 'separators' });
|
|
assert.equal(config.lineLayout, 'compact');
|
|
assert.equal(config.showSeparators, true);
|
|
});
|
|
|
|
test('migrate object layout: extracts nested fields to top level', () => {
|
|
const config = mergeConfig({
|
|
layout: { lineLayout: 'expanded', showSeparators: true, pathLevels: 2 },
|
|
});
|
|
assert.equal(config.lineLayout, 'expanded');
|
|
assert.equal(config.showSeparators, true);
|
|
assert.equal(config.pathLevels, 2);
|
|
});
|
|
|
|
test('migrate object layout: empty object does not crash', () => {
|
|
const config = mergeConfig({ layout: {} });
|
|
// Should fall back to defaults since no fields were extracted
|
|
assert.equal(config.lineLayout, DEFAULT_CONFIG.lineLayout);
|
|
assert.equal(config.showSeparators, DEFAULT_CONFIG.showSeparators);
|
|
assert.equal(config.pathLevels, DEFAULT_CONFIG.pathLevels);
|
|
});
|
|
|
|
test('no layout key -> no migration, uses defaults', () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.lineLayout, DEFAULT_CONFIG.lineLayout);
|
|
assert.equal(config.showSeparators, DEFAULT_CONFIG.showSeparators);
|
|
});
|
|
|
|
test('both layout and lineLayout present -> layout ignored', () => {
|
|
const config = mergeConfig({ layout: 'separators', lineLayout: 'expanded' });
|
|
// When lineLayout is already present, migration should not run
|
|
assert.equal(config.lineLayout, 'expanded');
|
|
assert.equal(config.showSeparators, DEFAULT_CONFIG.showSeparators);
|
|
});
|
|
|
|
test('mergeConfig accepts contextValue=remaining', () => {
|
|
const config = mergeConfig({
|
|
display: {
|
|
contextValue: 'remaining',
|
|
},
|
|
});
|
|
assert.equal(config.display.contextValue, 'remaining');
|
|
});
|
|
|
|
test('mergeConfig accepts contextValue=both', () => {
|
|
const config = mergeConfig({
|
|
display: {
|
|
contextValue: 'both',
|
|
},
|
|
});
|
|
assert.equal(config.display.contextValue, 'both');
|
|
});
|
|
|
|
test('mergeConfig falls back to default for invalid contextValue', () => {
|
|
const config = mergeConfig({
|
|
display: {
|
|
contextValue: 'invalid-mode',
|
|
},
|
|
});
|
|
assert.equal(config.display.contextValue, DEFAULT_CONFIG.display.contextValue);
|
|
});
|
|
|
|
test('mergeConfig defaults elementOrder to the full expanded layout', () => {
|
|
const config = mergeConfig({});
|
|
assert.deepEqual(config.elementOrder, DEFAULT_ELEMENT_ORDER);
|
|
});
|
|
|
|
test('mergeConfig preserves valid custom elementOrder including activity elements', () => {
|
|
const config = mergeConfig({
|
|
elementOrder: ['tools', 'project', 'usage', 'memory', 'context', 'agents', 'todos', 'environment'],
|
|
});
|
|
assert.deepEqual(
|
|
config.elementOrder,
|
|
['tools', 'project', 'usage', 'memory', 'context', 'agents', 'todos', 'environment']
|
|
);
|
|
});
|
|
|
|
test('mergeConfig filters unknown entries and de-duplicates elementOrder', () => {
|
|
const config = mergeConfig({
|
|
elementOrder: ['project', 'agents', 'project', 'banana', 'usage', 'memory', 'agents', 'context'],
|
|
});
|
|
assert.deepEqual(config.elementOrder, ['project', 'agents', 'usage', 'memory', 'context']);
|
|
});
|
|
|
|
test('mergeConfig treats elementOrder as an explicit expanded-mode filter', () => {
|
|
const config = mergeConfig({
|
|
elementOrder: ['usage', 'project'],
|
|
});
|
|
assert.deepEqual(config.elementOrder, ['usage', 'project']);
|
|
});
|
|
|
|
test('mergeConfig falls back to default when elementOrder is empty or invalid', () => {
|
|
assert.deepEqual(mergeConfig({ elementOrder: [] }).elementOrder, DEFAULT_ELEMENT_ORDER);
|
|
assert.deepEqual(mergeConfig({ elementOrder: ['unknown'] }).elementOrder, DEFAULT_ELEMENT_ORDER);
|
|
assert.deepEqual(mergeConfig({ elementOrder: 'project' }).elementOrder, DEFAULT_ELEMENT_ORDER);
|
|
});
|
|
|
|
test('mergeConfig defaults usage to expected values', () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.usage.cacheTtlSeconds, 60);
|
|
assert.equal(config.usage.failureCacheTtlSeconds, 15);
|
|
});
|
|
|
|
test('mergeConfig defaults colors to expected semantic palette', () => {
|
|
const config = mergeConfig({});
|
|
assert.equal(config.colors.context, 'green');
|
|
assert.equal(config.colors.usage, 'brightBlue');
|
|
assert.equal(config.colors.warning, 'yellow');
|
|
assert.equal(config.colors.usageWarning, 'brightMagenta');
|
|
assert.equal(config.colors.critical, 'red');
|
|
});
|
|
|
|
test('mergeConfig accepts valid color overrides and filters invalid values', () => {
|
|
const config = mergeConfig({
|
|
colors: {
|
|
context: 'cyan',
|
|
usage: 'magenta',
|
|
warning: 'brightBlue',
|
|
usageWarning: 'yellow',
|
|
critical: 'not-a-color',
|
|
},
|
|
});
|
|
|
|
assert.equal(config.colors.context, 'cyan');
|
|
assert.equal(config.colors.usage, 'magenta');
|
|
assert.equal(config.colors.warning, 'brightBlue');
|
|
assert.equal(config.colors.usageWarning, 'yellow');
|
|
assert.equal(config.colors.critical, DEFAULT_CONFIG.colors.critical);
|
|
});
|
|
|
|
test('mergeConfig accepts custom usage TTL values', () => {
|
|
const config = mergeConfig({
|
|
usage: { cacheTtlSeconds: 120, failureCacheTtlSeconds: 30 },
|
|
});
|
|
assert.equal(config.usage.cacheTtlSeconds, 120);
|
|
assert.equal(config.usage.failureCacheTtlSeconds, 30);
|
|
});
|
|
|
|
test('mergeConfig falls back to defaults for invalid usage values', () => {
|
|
const config = mergeConfig({
|
|
usage: { cacheTtlSeconds: -1, failureCacheTtlSeconds: 0 },
|
|
});
|
|
assert.equal(config.usage.cacheTtlSeconds, DEFAULT_CONFIG.usage.cacheTtlSeconds);
|
|
assert.equal(config.usage.failureCacheTtlSeconds, DEFAULT_CONFIG.usage.failureCacheTtlSeconds);
|
|
});
|
|
|
|
// --- Custom color value tests (256-color and hex) ---
|
|
|
|
test('mergeConfig accepts 256-color index values', () => {
|
|
const config = mergeConfig({
|
|
colors: {
|
|
context: 82,
|
|
usage: 214,
|
|
warning: 220,
|
|
usageWarning: 97,
|
|
critical: 196,
|
|
},
|
|
});
|
|
assert.equal(config.colors.context, 82);
|
|
assert.equal(config.colors.usage, 214);
|
|
assert.equal(config.colors.warning, 220);
|
|
assert.equal(config.colors.usageWarning, 97);
|
|
assert.equal(config.colors.critical, 196);
|
|
});
|
|
|
|
test('mergeConfig accepts hex color strings', () => {
|
|
const config = mergeConfig({
|
|
colors: {
|
|
context: '#33ff00',
|
|
usage: '#FFB000',
|
|
warning: '#ff87d7',
|
|
},
|
|
});
|
|
assert.equal(config.colors.context, '#33ff00');
|
|
assert.equal(config.colors.usage, '#FFB000');
|
|
assert.equal(config.colors.warning, '#ff87d7');
|
|
});
|
|
|
|
test('mergeConfig accepts mixed named, 256-color, and hex values', () => {
|
|
const config = mergeConfig({
|
|
colors: {
|
|
context: '#33ff00',
|
|
usage: 214,
|
|
warning: 'yellow',
|
|
usageWarning: '#af87ff',
|
|
critical: 'red',
|
|
},
|
|
});
|
|
assert.equal(config.colors.context, '#33ff00');
|
|
assert.equal(config.colors.usage, 214);
|
|
assert.equal(config.colors.warning, 'yellow');
|
|
assert.equal(config.colors.usageWarning, '#af87ff');
|
|
assert.equal(config.colors.critical, 'red');
|
|
});
|
|
|
|
test('mergeConfig rejects invalid 256-color indices', () => {
|
|
const config = mergeConfig({
|
|
colors: {
|
|
context: 256,
|
|
usage: -1,
|
|
warning: 1.5,
|
|
},
|
|
});
|
|
assert.equal(config.colors.context, DEFAULT_CONFIG.colors.context);
|
|
assert.equal(config.colors.usage, DEFAULT_CONFIG.colors.usage);
|
|
assert.equal(config.colors.warning, DEFAULT_CONFIG.colors.warning);
|
|
});
|
|
|
|
test('mergeConfig rejects invalid hex strings', () => {
|
|
const config = mergeConfig({
|
|
colors: {
|
|
context: '#fff',
|
|
usage: '#gggggg',
|
|
warning: 'ff0000',
|
|
},
|
|
});
|
|
assert.equal(config.colors.context, DEFAULT_CONFIG.colors.context);
|
|
assert.equal(config.colors.usage, DEFAULT_CONFIG.colors.usage);
|
|
assert.equal(config.colors.warning, DEFAULT_CONFIG.colors.warning);
|
|
});
|