Files
claude-hud/tests/render.test.js
melon eb18f786d3 feat: config system + usage API + bug fixes (supersedes #32) (#35)
* feat: display last 3 path segments first in session line

Shows the last 3 segments of the working directory path at the
beginning of the session line for quick project identification.

Before: [Opus 4.5] ████░░░░░░ 19% | my-project git:(main) | ...
After:  dev/apps/my-project git:(main) | [Opus 4.5] ████░░░░░░ 19% | ...

This helps distinguish between projects with similar names in
different locations and puts the most relevant info first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: configurable path levels (1-3) and git status toggle

- Add config system at ~/.claude/plugins/claude-hud/config.json
- Default path display to 1 level (was hardcoded at 3)
- Add pathLevels option: 1, 2, or 3 directory segments
- Add gitStatus.enabled toggle to show/hide git branch
- Add interactive CLI: npx claude-hud-configure
- Add comprehensive tests for config and path levels
- Update README with configuration documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: handle cross-platform path separators

- Split paths by both / and \ for Windows compatibility
- Always output forward slashes for consistent display
- Add tests for Windows paths, UNC paths, and mixed separators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: show existing config when reconfiguring

- Display current values when config file exists
- Prompt user that Enter keeps current values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: interactive CLI with arrow-key selection

- Add @inquirer/prompts for better UX
- Arrow keys to select path levels
- Visual feedback with checkmarks
- Cleaner, more compact output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add dirty indicator and ahead/behind git status

- Add gitStatus.showDirty option (default: true)
- Add gitStatus.showAheadBehind option (default: false)
- Update getGitStatus to return isDirty, ahead, behind
- Update CLI to configure new options with preview
- Add tests for dirty and ahead/behind display
- Update README with new options

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add configurable display options for all HUD elements

- Add display configuration object with 8 boolean options:
  - showModel: Toggle model name display [Opus]
  - showContextBar: Toggle visual context bar ████░░░░░░
  - showConfigCounts: Toggle CLAUDE.md, rules, MCPs, hooks counts
  - showDuration: Toggle session duration display
  - showTokenBreakdown: Toggle token details at high context (85%+)
  - showTools: Toggle tools activity line
  - showAgents: Toggle agents activity line
  - showTodos: Toggle todos progress line

- All options default to true for backward compatibility
- Enhanced CLI preview with colors matching actual HUD output
- Added 5 new tests for display configuration (87 total)
- Updated README with complete configuration reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add configurable layout options (default, condensed, separators)

- Add layout config option with three styles:
  - default: All info on first line (original behavior)
  - condensed: Model/context top, project bottom
  - separators: Condensed with visual separator lines

- Create project-line.ts for rendering project path in split layouts
- Add renderSessionLineMinimal for condensed/separators layouts
- Interactive CLI preview shows selected layout style
- All 87 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add interactive HUD preview and folder icon

- Add live preview that updates as config options are selected
- Show initial preview on startup based on existing/default config
- Add folder icon (📁) in front of project path
- Extract preview generation to separate module for reuse

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update README with folder icon and live preview

- Add folder icon (📁) to all path examples
- Document live preview feature in interactive CLI section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add menu-based navigation to configure CLI

- Replace linear flow with main menu loop
- Show current values in menu (layout, path levels, git status, etc.)
- Users can edit any section and return to menu
- Preview updates after each section change
- Save & Exit or Exit without saving options

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: update tests for folder icon and config structure

- Update integration test expected output with folder icon
- Make config test environment-independent (validates structure, not specific values)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add comprehensive config loading tests

Add 23 new tests for config system validation:
- DEFAULT_CONFIG structure (layout, gitStatus, 8 display options)
- Layout validation (default, condensed, separators)
- PathLevels validation (1, 2, or 3)
- Git status configuration defaults
- Display configuration (booleans, defaults to true, count)
- loadConfig behavior (complete fields, valid values)
- getConfigPath structure tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add usage API, config enhancements, and bug fixes

This PR combines and enhances the config system from PR #32 with new features
and bug fixes addressing the owner's review feedback.

## New Features
- Usage API integration showing 5h/7d limits for Pro/Max/Team users
- Interactive `/claude-hud:configure` skill for in-Claude-Code configuration
- Hybrid showUsage toggle (env var + config for privacy control)

## Bug Fixes (addressing #32 review feedback)
- Fix git status spacing: `main*↑2↓1` → `main* ↑2 ↓1`
- Fix root path rendering: show `/` instead of empty folder icon
- Fix Windows path normalization in truncatePath
- Fix duplicate dependencies key in package.json
- Fix multiSelect for mutually exclusive options in configure skill

## Credits
- Config system, layouts, path levels, git toggle, CLI by @Tsopic (PR #32)
- Usage API, configure skill, bug fixes by @melon-hub

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Tsopic <Tsopic@users.noreply.github.com>

* fix: move configure command to correct location

- Move configure.md from .claude-plugin/skills/ to commands/
- Remove skills array from plugin.json (commands are auto-discovered)

Commands must be in the commands/ directory at plugin root,
not inside .claude-plugin/. This matches the existing setup.md pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: layout order, remove condensed, redesign configure flow

Render fixes (per PR feedback):
- Fix element order: model first, project second
- Remove condensed layout (only default/separators remain)
- Separators: single line below header only when activity exists
- Delete project-line.ts (no longer needed)
- Remove renderSessionLineMinimal() function

Configure skill redesign:
- Context-aware tab order (returning users start on Turn Off)
- Explicit Turn Off/Turn On questions (no toggle ambiguity)
- Git Style question for dirty/ahead-behind options
- Combined Layout/Reset tab for returning users
- Duration now toggleable
- Guards against empty submissions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: rename 'Returning User' to 'Update Config' in configure flow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: fix tests for new element order and removed function

- Update render-basic.txt expected output (model first, project second)
- Remove renderSessionLineMinimal import and test (function was deleted)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update documentation for usage API, config flow, and troubleshooting

- Remove stale CLAUDE_HUD_SHOW_USAGE env var references (now config-based)
- Add usage API data sources to CLAUDE.md
- Add new source files to file structure (config.ts, git.ts, usage-api.ts)
- Update README with usage limits section and requirements
- Add troubleshooting sections for config, git, and usage issues
- Standardize output examples across all documentation files
- Remove condensed layout references (only default/separators now)
- Update configure skill reference (npx CLI → /claude-hud:configure)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove dist/ from PR and update outdated comment

- Reset dist/ to main branch (dist will be built by CI after merge)
- Update comment: "requires env var opt-in AND config" → "shown when enabled in config"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove internal design doc from release

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove CLI and runtime dependency

- Remove src/bin/configure.ts and src/bin/preview.ts
- Remove @inquirer/prompts dependency (plugins don't run npm install)
- Remove bin field from package.json
- Users configure via /claude-hud:configure skill instead

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: bump version to 0.0.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use file-based cache for usage API

HUD runs as a new process every ~300ms, so in-memory cache was useless.
Now caches to ~/.claude/plugins/claude-hud/.usage-cache.json.

This reduces API calls from ~10,800/hour to max 60/hour.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use consistent homeDir/now in getUsage

Avoids potential divergence if deps functions are non-deterministic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add real tests for usage API file cache

- Test credential parsing with mock HOME directory
- Test cache TTL behavior (success and failure)
- Test apiUnavailable flag handling
- Replace placeholder assert.ok(true) with actual assertions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Martin Kask <martin@industrial.ninja>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: melon-hub <melon-hub@users.noreply.github.com>
Co-authored-by: Tsopic <Tsopic@users.noreply.github.com>
Co-authored-by: Jarrod Watts <jarrod@cubelabs.xyz>
2026-01-07 16:00:32 +11:00

501 lines
15 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { renderSessionLine } from '../dist/render/session-line.js';
import { renderToolsLine } from '../dist/render/tools-line.js';
import { renderAgentsLine } from '../dist/render/agents-line.js';
import { renderTodosLine } from '../dist/render/todos-line.js';
import { getContextColor } from '../dist/render/colors.js';
function baseContext() {
return {
stdin: {
model: { display_name: 'Opus' },
context_window: {
context_window_size: 200000,
current_usage: {
input_tokens: 10000,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
},
},
transcript: { tools: [], agents: [], todos: [] },
claudeMdCount: 0,
rulesCount: 0,
mcpCount: 0,
hooksCount: 0,
sessionDuration: '',
gitStatus: null,
usageData: null,
config: {
layout: 'default',
pathLevels: 1,
gitStatus: { enabled: true, showDirty: true, showAheadBehind: false },
display: { showModel: true, showContextBar: true, showConfigCounts: true, showDuration: true, showTokenBreakdown: true, showUsage: true, showTools: true, showAgents: true, showTodos: true },
},
};
}
test('renderSessionLine adds token breakdown when context is high', () => {
const ctx = baseContext();
// For 90%: (tokens + 45000) / 200000 = 0.9 → tokens = 135000
ctx.stdin.context_window.current_usage.input_tokens = 135000;
const line = renderSessionLine(ctx);
assert.ok(line.includes('in:'), 'expected token breakdown');
assert.ok(line.includes('cache:'), 'expected cache breakdown');
});
test('renderSessionLine includes duration and formats large tokens', () => {
const ctx = baseContext();
ctx.sessionDuration = '1m';
// Use 1M context, need 85%+ to show breakdown
// For 85%: (tokens + 45000) / 1000000 = 0.85 → tokens = 805000
ctx.stdin.context_window.context_window_size = 1000000;
ctx.stdin.context_window.current_usage.input_tokens = 805000;
ctx.stdin.context_window.current_usage.cache_read_input_tokens = 1500;
const line = renderSessionLine(ctx);
assert.ok(line.includes('⏱️'));
assert.ok(line.includes('805k') || line.includes('805.0k'), 'expected large input token display');
assert.ok(line.includes('2k'), 'expected cache token display');
});
test('renderSessionLine handles missing input tokens and cache creation usage', () => {
const ctx = baseContext();
// For 90%: (tokens + 45000) / 200000 = 0.9 → tokens = 135000 (all from cache)
ctx.stdin.context_window.context_window_size = 200000;
ctx.stdin.context_window.current_usage = {
cache_creation_input_tokens: 135000,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('90%'));
assert.ok(line.includes('in: 0'));
});
test('renderSessionLine handles missing cache token fields', () => {
const ctx = baseContext();
// For 90%: (tokens + 45000) / 200000 = 0.9 → tokens = 135000
ctx.stdin.context_window.context_window_size = 200000;
ctx.stdin.context_window.current_usage = {
input_tokens: 135000,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('cache: 0'));
});
test('getContextColor returns yellow for warning threshold', () => {
assert.equal(getContextColor(70), '\x1b[33m');
});
test('renderSessionLine includes config counts when present', () => {
const ctx = baseContext();
ctx.stdin.cwd = '/tmp/my-project';
ctx.claudeMdCount = 1;
ctx.rulesCount = 2;
ctx.mcpCount = 3;
ctx.hooksCount = 4;
const line = renderSessionLine(ctx);
assert.ok(line.includes('CLAUDE.md'));
assert.ok(line.includes('rules'));
assert.ok(line.includes('MCPs'));
assert.ok(line.includes('hooks'));
});
test('renderSessionLine displays project name from POSIX cwd', () => {
const ctx = baseContext();
ctx.stdin.cwd = '/Users/jarrod/my-project';
const line = renderSessionLine(ctx);
assert.ok(line.includes('my-project'));
assert.ok(!line.includes('/Users/jarrod'));
});
test('renderSessionLine displays project name from Windows cwd', { skip: process.platform !== 'win32' }, () => {
const ctx = baseContext();
ctx.stdin.cwd = 'C:\\Users\\jarrod\\my-project';
const line = renderSessionLine(ctx);
assert.ok(line.includes('my-project'));
assert.ok(!line.includes('C:\\'));
});
test('renderSessionLine handles root path gracefully', () => {
const ctx = baseContext();
ctx.stdin.cwd = '/';
const line = renderSessionLine(ctx);
assert.ok(line.includes('[Opus]'));
});
test('renderSessionLine omits project name when cwd is undefined', () => {
const ctx = baseContext();
ctx.stdin.cwd = undefined;
const line = renderSessionLine(ctx);
assert.ok(line.includes('[Opus]'));
});
test('renderSessionLine displays git branch when present', () => {
const ctx = baseContext();
ctx.stdin.cwd = '/tmp/my-project';
ctx.gitStatus = { branch: 'main', isDirty: false, ahead: 0, behind: 0 };
const line = renderSessionLine(ctx);
assert.ok(line.includes('git:('));
assert.ok(line.includes('main'));
});
test('renderSessionLine omits git branch when null', () => {
const ctx = baseContext();
ctx.stdin.cwd = '/tmp/my-project';
ctx.gitStatus = null;
const line = renderSessionLine(ctx);
assert.ok(!line.includes('git:('));
});
test('renderSessionLine displays branch with slashes', () => {
const ctx = baseContext();
ctx.stdin.cwd = '/tmp/my-project';
ctx.gitStatus = { branch: 'feature/add-auth', isDirty: false, ahead: 0, behind: 0 };
const line = renderSessionLine(ctx);
assert.ok(line.includes('git:('));
assert.ok(line.includes('feature/add-auth'));
});
test('renderToolsLine renders running and completed tools', () => {
const ctx = baseContext();
ctx.transcript.tools = [
{
id: 'tool-1',
name: 'Read',
status: 'completed',
startTime: new Date(0),
endTime: new Date(0),
duration: 0,
},
{
id: 'tool-2',
name: 'Edit',
target: '/tmp/very/long/path/to/authentication.ts',
status: 'running',
startTime: new Date(0),
},
];
const line = renderToolsLine(ctx);
assert.ok(line?.includes('Read'));
assert.ok(line?.includes('Edit'));
assert.ok(line?.includes('.../authentication.ts'));
});
test('renderToolsLine truncates long filenames', () => {
const ctx = baseContext();
ctx.transcript.tools = [
{
id: 'tool-1',
name: 'Edit',
target: '/tmp/this-is-a-very-very-long-filename.ts',
status: 'running',
startTime: new Date(0),
},
];
const line = renderToolsLine(ctx);
assert.ok(line?.includes('...'));
assert.ok(!line?.includes('/tmp/'));
});
test('renderToolsLine handles trailing slash paths', () => {
const ctx = baseContext();
ctx.transcript.tools = [
{
id: 'tool-1',
name: 'Read',
target: '/tmp/very/long/path/with/trailing/',
status: 'running',
startTime: new Date(0),
},
];
const line = renderToolsLine(ctx);
assert.ok(line?.includes('...'));
});
test('renderToolsLine preserves short targets and handles missing targets', () => {
const ctx = baseContext();
ctx.transcript.tools = [
{
id: 'tool-1',
name: 'Read',
target: 'short.txt',
status: 'running',
startTime: new Date(0),
},
{
id: 'tool-2',
name: 'Write',
status: 'running',
startTime: new Date(0),
},
];
const line = renderToolsLine(ctx);
assert.ok(line?.includes('short.txt'));
assert.ok(line?.includes('Write'));
});
test('renderToolsLine returns null when tools are unrecognized', () => {
const ctx = baseContext();
ctx.transcript.tools = [
{
id: 'tool-1',
name: 'WeirdTool',
status: 'unknown',
startTime: new Date(0),
},
];
assert.equal(renderToolsLine(ctx), null);
});
test('renderAgentsLine returns null when no agents exist', () => {
const ctx = baseContext();
assert.equal(renderAgentsLine(ctx), null);
});
test('renderAgentsLine renders completed agents', () => {
const ctx = baseContext();
ctx.transcript.agents = [
{
id: 'agent-1',
type: 'explore',
model: 'haiku',
description: 'Finding auth code',
status: 'completed',
startTime: new Date(0),
endTime: new Date(0),
elapsed: 0,
},
];
const line = renderAgentsLine(ctx);
assert.ok(line?.includes('explore'));
assert.ok(line?.includes('haiku'));
});
test('renderAgentsLine truncates long descriptions and formats elapsed time', () => {
const ctx = baseContext();
ctx.transcript.agents = [
{
id: 'agent-1',
type: 'explore',
model: 'haiku',
description: 'A very long description that should be truncated in the HUD output',
status: 'completed',
startTime: new Date(0),
endTime: new Date(1500),
},
{
id: 'agent-2',
type: 'analyze',
status: 'completed',
startTime: new Date(0),
endTime: new Date(65000),
},
];
const line = renderAgentsLine(ctx);
assert.ok(line?.includes('...'));
assert.ok(line?.includes('2s'));
assert.ok(line?.includes('1m'));
});
test('renderAgentsLine renders running agents with live elapsed time', () => {
const ctx = baseContext();
const originalNow = Date.now;
Date.now = () => 2000;
try {
ctx.transcript.agents = [
{
id: 'agent-1',
type: 'plan',
status: 'running',
startTime: new Date(0),
},
];
const line = renderAgentsLine(ctx);
assert.ok(line?.includes('◐'));
assert.ok(line?.includes('2s'));
} finally {
Date.now = originalNow;
}
});
test('renderTodosLine handles in-progress and completed-only cases', () => {
const ctx = baseContext();
ctx.transcript.todos = [
{ content: 'First task', status: 'completed' },
{ content: 'Second task', status: 'in_progress' },
];
assert.ok(renderTodosLine(ctx)?.includes('Second task'));
ctx.transcript.todos = [{ content: 'First task', status: 'completed' }];
assert.ok(renderTodosLine(ctx)?.includes('All todos complete'));
});
test('renderTodosLine returns null when no todos are in progress', () => {
const ctx = baseContext();
ctx.transcript.todos = [
{ content: 'First task', status: 'completed' },
{ content: 'Second task', status: 'pending' },
];
assert.equal(renderTodosLine(ctx), null);
});
test('renderTodosLine truncates long todo content', () => {
const ctx = baseContext();
ctx.transcript.todos = [
{
content: 'This is a very long todo content that should be truncated for display',
status: 'in_progress',
},
];
const line = renderTodosLine(ctx);
assert.ok(line?.includes('...'));
});
test('renderTodosLine returns null when no todos exist', () => {
const ctx = baseContext();
assert.equal(renderTodosLine(ctx), null);
});
test('renderToolsLine returns null when no tools exist', () => {
const ctx = baseContext();
assert.equal(renderToolsLine(ctx), null);
});
// Usage display tests
test('renderSessionLine displays plan name in model bracket', () => {
const ctx = baseContext();
ctx.usageData = {
planName: 'Max',
fiveHour: 23,
sevenDay: 45,
fiveHourResetAt: null,
sevenDayResetAt: null,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('Opus'), 'should include model name');
assert.ok(line.includes('Max'), 'should include plan name');
});
test('renderSessionLine displays usage percentages (7d hidden when low)', () => {
const ctx = baseContext();
ctx.usageData = {
planName: 'Pro',
fiveHour: 6,
sevenDay: 13,
fiveHourResetAt: null,
sevenDayResetAt: null,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('5h:'), 'should include 5h label');
assert.ok(!line.includes('7d:'), 'should NOT include 7d when below 80%');
assert.ok(line.includes('6%'), 'should include 5h percentage');
});
test('renderSessionLine shows 7d when approaching limit (>=80%)', () => {
const ctx = baseContext();
ctx.usageData = {
planName: 'Pro',
fiveHour: 45,
sevenDay: 85,
fiveHourResetAt: null,
sevenDayResetAt: null,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('5h:'), 'should include 5h label');
assert.ok(line.includes('7d:'), 'should include 7d when >= 80%');
assert.ok(line.includes('85%'), 'should include 7d percentage');
});
test('renderSessionLine shows 5hr reset countdown', () => {
const ctx = baseContext();
const resetTime = new Date(Date.now() + 7200000); // 2 hours from now
ctx.usageData = {
planName: 'Pro',
fiveHour: 45,
sevenDay: 20,
fiveHourResetAt: resetTime,
sevenDayResetAt: null,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('5h:'), 'should include 5h label');
assert.ok(line.includes('2h'), 'should include reset countdown');
});
test('renderSessionLine displays limit reached warning', () => {
const ctx = baseContext();
const resetTime = new Date(Date.now() + 3600000); // 1 hour from now
ctx.usageData = {
planName: 'Pro',
fiveHour: 100,
sevenDay: 45,
fiveHourResetAt: resetTime,
sevenDayResetAt: null,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('Limit reached'), 'should show limit reached');
assert.ok(line.includes('resets'), 'should show reset time');
});
test('renderSessionLine displays -- for null usage values', () => {
const ctx = baseContext();
ctx.usageData = {
planName: 'Max',
fiveHour: null,
sevenDay: null,
fiveHourResetAt: null,
sevenDayResetAt: null,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('5h:'), 'should include 5h label');
assert.ok(line.includes('--'), 'should show -- for null values');
});
test('renderSessionLine omits usage when usageData is null', () => {
const ctx = baseContext();
ctx.usageData = null;
const line = renderSessionLine(ctx);
assert.ok(!line.includes('5h:'), 'should not include 5h label');
assert.ok(!line.includes('7d:'), 'should not include 7d label');
});
test('renderSessionLine displays warning when API is unavailable', () => {
const ctx = baseContext();
ctx.usageData = {
planName: 'Max',
fiveHour: null,
sevenDay: null,
fiveHourResetAt: null,
sevenDayResetAt: null,
apiUnavailable: true,
};
const line = renderSessionLine(ctx);
assert.ok(line.includes('usage:'), 'should show usage label');
assert.ok(line.includes('⚠'), 'should show warning indicator');
assert.ok(!line.includes('5h:'), 'should not show 5h when API unavailable');
});
test('renderSessionLine hides usage when showUsage config is false (hybrid toggle)', () => {
const ctx = baseContext();
ctx.usageData = {
planName: 'Pro',
fiveHour: 25,
sevenDay: 10,
fiveHourResetAt: null,
sevenDayResetAt: null,
};
// Even with usageData present, setting showUsage to false should hide it
ctx.config.display.showUsage = false;
const line = renderSessionLine(ctx);
assert.ok(!line.includes('5h:'), 'should not show usage when showUsage is false');
assert.ok(!line.includes('Pro'), 'should not show plan name when showUsage is false');
});