// @vitest-environment jsdom /** * ui-openrouter-usage browser half on a real cordis Context with fake slots/ * locale/remote faces: the plugin registers the CostDock entry at * conversation.composer.dock and the BalanceBadge entry at * sidebar.footer.action, the balance snapshot reads roll through the injected * face at call time, and registration disposal rides the plugin fiber (HMR * safety). The node half and the invariant companion are exercised over the * same Context. */ import { Context, Service } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { cleanup } from '@testing-library/react' import { afterEach } from 'vitest' import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client' import type { OpenRouterBalance } from '@deepseek-ai/dsh-openrouter-usage/client' import type { BalanceBadgeActions } from '../src/client/BalanceBadge.tsx' import { BalanceBadge } from '../src/client/BalanceBadge.tsx' import { CostDock } from '../src/client/CostDock.tsx' import { apply, inject, NS } from '../src/client/index.ts' import { apply as nodeApply } from '../src/index.ts' afterEach(cleanup) async function bench() { const ctx = new Context() await ctx.plugin(SlotRegistry).await() const locale = new LocaleRuntime(ctx) ctx.provide('locale', locale) class RemoteService extends Service { constructor(serviceCtx: Context) { super(serviceCtx, 'remote') } } new RemoteService(ctx) const snapshot = vi.fn<() => Promise>>() .mockResolvedValue({ ok: true, value: balance() }) const refresh = vi.fn<() => Promise>>() .mockResolvedValue({ ok: true, value: balance({ balanceUsd: 9 }) }) ctx.provide('remote.openRouterUsage', { snapshot, refresh }) return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, snapshot, refresh } } function declare(slots: SlotRegistry): () => void { return slots.register({ name: 'root', children: { 'conversation.composer.dock': { kind: 'list', scope: 'session' }, 'sidebar.footer.action': { kind: 'list', scope: 'root' }, }, } as never, () => null) } function balance(over: Partial = {}): OpenRouterBalance { return { balanceUsd: 5.5, label: 'k', usageTokens: 0, limitTokens: 0, isFreeTier: false, updatedAt: 1_700_000_000_000, currency: 'USD', ...over, } } describe('ui-openrouter-usage browser plugin', () => { it('declares only the services its two entries read', () => { expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.openRouterUsage']) }) it('registers localized cost and balance entries without reading the Remote eagerly', async () => { const b = await bench() declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() const cost = b.slots.entries('conversation.composer.dock')[0]! expect(cost.component).toBe(CostDock) expect(cost.options).toMatchObject({ id: 'openrouter-cost', order: 5 }) expect(cost.locale).toBe(NS) const badge = b.slots.entries('sidebar.footer.action')[0]! expect(badge.component).toBe(BalanceBadge) expect(badge.options).toMatchObject({ id: 'openrouter-balance', order: 10 }) expect(badge.locale).toBe(NS) // Registration reads nothing live; both reads roll at call time. expect(b.snapshot).not.toHaveBeenCalled() expect(b.refresh).not.toHaveBeenCalled() const injected = (badge.inject as unknown as () => BalanceBadgeActions)() await expect(injected.snapshot()).resolves.toEqual(balance()) expect(b.snapshot).toHaveBeenCalledOnce() await expect(injected.refresh()).resolves.toEqual(balance({ balanceUsd: 9 })) expect(b.refresh).toHaveBeenCalledOnce() await b.ctx.fiber.dispose() }) it('forwards a Remote failure out of the injected snapshot verbatim', async () => { const b = await bench() declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() b.snapshot.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'boom', details: {} } }) const injected = (b.slots.entries('sidebar.footer.action')[0]!.inject as unknown as () => BalanceBadgeActions)() await expect(injected.snapshot()).rejects.toThrow('openRouterUsage.snapshot failed: REMOTE_ERROR: boom') await b.ctx.fiber.dispose() }) it('forwards a Remote failure out of the injected refresh verbatim', async () => { const b = await bench() declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() b.refresh.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'nope', details: {} } }) const injected = (b.slots.entries('sidebar.footer.action')[0]!.inject as unknown as () => BalanceBadgeActions)() await expect(injected.refresh()).rejects.toThrow('openRouterUsage.refresh failed: REMOTE_ERROR: nope') await b.ctx.fiber.dispose() }) it('follows locale and recovers across late declaration and declarer reload', async () => { const b = await bench() const fiber = b.ctx.plugin({ inject: [...inject], apply }) await fiber.await() expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0) expect(b.slots.entries('sidebar.footer.action')).toHaveLength(0) const stop = declare(b.slots) await vi.waitFor(() => { expect(b.slots.entries('conversation.composer.dock')).toHaveLength(1) }) expect(b.slots.entries('sidebar.footer.action')).toHaveLength(1) stop() expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0) declare(b.slots) await vi.waitFor(() => { expect(b.slots.entries('sidebar.footer.action')[0]?.component).toBe(BalanceBadge) }) await fiber.dispose() expect(b.slots.entries('conversation.composer.dock')).toHaveLength(0) expect(b.slots.entries('sidebar.footer.action')).toHaveLength(0) await b.ctx.fiber.dispose() }) }) describe('ui-openrouter-usage node half', () => { it('the node apply is an inert loader seat', () => { expect(() => { nodeApply() }).not.toThrow() }) })