hermes-agent/ui-tui/src/__tests__/creditsCommand.test.ts
Brooklyn Nicholson 62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00

142 lines
4.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getOverlayState, resetOverlayState } from '../app/overlayStore.js'
import { creditsCommands } from '../app/slash/commands/credits.js'
import type { CreditsViewResponse } from '../gatewayTypes.js'
// The command opens the top-up URL through this helper on confirm. Mock it so
// the test never shells out to a real browser/`xdg-open` and we can assert the
// success/failure messaging deterministically.
vi.mock('../lib/openExternalUrl.js', () => ({
openExternalUrl: vi.fn(() => true)
}))
import { openExternalUrl } from '../lib/openExternalUrl.js'
const openExternalUrlMock = vi.mocked(openExternalUrl)
const creditsCommand = creditsCommands.find(cmd => cmd.name === 'credits')!
const buildView = (overrides: Partial<CreditsViewResponse> = {}): CreditsViewResponse => ({
balance_lines: ['Grant: $9.50 left', 'Top-up: $25.00'],
depleted: false,
identity_line: 'Signed in as ada@example.com',
logged_in: true,
topup_url: 'https://portal.nousresearch.com/billing/topup',
...overrides
})
// Mirror createSlashHandler's real `guarded` wrapper: skip the handler when the
// command is stale OR the response is falsy. Tests stay non-stale, so this is a
// straightforward "run the handler when we got a response" shim.
const guarded =
<T>(fn: (r: T) => void) =>
(r: null | T) => {
if (r) {
fn(r)
}
}
const buildCtx = (rpcResult: CreditsViewResponse) => {
const sys = vi.fn()
const rpc = vi.fn(() => Promise.resolve(rpcResult))
const guardedErr = vi.fn()
const ctx = {
gateway: { rpc },
guarded,
guardedErr,
sid: 'sid-abc',
stale: () => false,
transcript: { page: vi.fn(), panel: vi.fn(), sys }
}
// Run the command, then await the rpc promise so the .then() handler has
// flushed before assertions — deterministic, no polling/timeouts.
const run = async () => {
creditsCommand.run('', ctx as any, 'credits')
await rpc.mock.results[0]?.value
// Allow the chained .then() microtask to settle.
await Promise.resolve()
}
return { ctx, rpc, run, sys }
}
describe('/credits slash command', () => {
beforeEach(() => {
resetOverlayState()
openExternalUrlMock.mockClear()
openExternalUrlMock.mockReturnValue(true)
})
it('renders the balance (including top-up URL) and arms the confirm overlay', async () => {
const view = buildView()
const { rpc, run, sys } = buildCtx(view)
await run()
expect(rpc).toHaveBeenCalledWith('credits.view', { session_id: 'sid-abc' })
// (a) sys received the balance text including the topup_url
const printed = sys.mock.calls.map(call => call[0]).join('\n')
expect(printed).toContain('💳 Nous credits')
expect(printed).toContain('Grant: $9.50 left')
expect(printed).toContain('Signed in as ada@example.com')
expect(printed).toContain(view.topup_url)
// (b) confirm overlay set with the expected label + detail
const confirm = getOverlayState().confirm
expect(confirm).toBeTruthy()
expect(confirm?.confirmLabel).toBe('Open top-up in browser')
expect(confirm?.cancelLabel).toBe('Cancel')
expect(confirm?.title).toBe('Add credits?')
expect(confirm?.detail).toBe(view.topup_url)
// onConfirm opens the URL and reports success back to the transcript
confirm?.onConfirm()
expect(openExternalUrlMock).toHaveBeenCalledWith(view.topup_url)
expect(sys).toHaveBeenCalledWith('Complete your top-up in the browser — credits will appear in /credits shortly.')
})
it('falls back to printing the URL when the browser open is rejected', async () => {
openExternalUrlMock.mockReturnValue(false)
const view = buildView()
const { run, sys } = buildCtx(view)
await run()
const confirm = getOverlayState().confirm
expect(confirm).toBeTruthy()
confirm?.onConfirm()
expect(sys).toHaveBeenCalledWith(`Open this URL to top up: ${view.topup_url}`)
})
it('does not arm the confirm overlay when there is no top-up URL', async () => {
const view = buildView({ topup_url: null })
const { run, sys } = buildCtx(view)
await run()
const printed = sys.mock.calls.map(call => call[0]).join('\n')
expect(printed).toContain('💳 Nous credits')
expect(getOverlayState().confirm).toBeNull()
})
it('shows the not-logged-in message and does NOT arm the confirm overlay', async () => {
const view = buildView({
balance_lines: [],
identity_line: null,
logged_in: false,
topup_url: null
})
const { run, sys } = buildCtx(view)
await run()
expect(sys).toHaveBeenCalledWith('💳 Not logged into Nous Portal — run /portal to log in.')
expect(getOverlayState().confirm).toBeNull()
expect(openExternalUrlMock).not.toHaveBeenCalled()
})
})