import { beforeEach, describe, expect, it, vi } from 'vitest' import { createSlashHandler } from '../app/createSlashHandler.js' import { getOverlayState, resetOverlayState } from '../app/overlayStore.js' import { getUiState, resetUiState } from '../app/uiStore.js' describe('createSlashHandler', () => { beforeEach(() => { resetOverlayState() resetUiState() }) it('opens the resume picker locally', () => { const ctx = buildCtx() expect(createSlashHandler(ctx)('/resume')).toBe(true) expect(getOverlayState().picker).toBe(true) }) it('cycles details mode and persists it', async () => { const ctx = buildCtx() expect(getUiState().detailsMode).toBe('collapsed') expect(createSlashHandler(ctx)('/details toggle')).toBe(true) expect(getUiState().detailsMode).toBe('expanded') expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'details_mode', value: 'expanded' }) expect(ctx.transcript.sys).toHaveBeenCalledWith('details: expanded') }) it('shows tool enable usage when names are missing', () => { const ctx = buildCtx() expect(createSlashHandler(ctx)('/tools enable')).toBe(true) expect(ctx.transcript.sys).toHaveBeenNthCalledWith(1, 'usage: /tools enable [name ...]') expect(ctx.transcript.sys).toHaveBeenNthCalledWith(2, 'built-in toolset: /tools enable web') expect(ctx.transcript.sys).toHaveBeenNthCalledWith(3, 'MCP tool: /tools enable github:create_issue') }) it('resolves unique local aliases through the catalog', () => { const ctx = buildCtx({ local: { catalog: { canon: { '/h': '/help', '/help': '/help' } } } }) expect(createSlashHandler(ctx)('/h')).toBe(true) expect(ctx.transcript.panel).toHaveBeenCalledWith('Commands', expect.any(Array)) }) }) const buildCtx = (overrides: Partial = {}): Ctx => ({ ...overrides, composer: { ...buildComposer(), ...overrides.composer }, gateway: { ...buildGateway(), ...overrides.gateway }, local: { ...buildLocal(), ...overrides.local }, session: { ...buildSession(), ...overrides.session }, transcript: { ...buildTranscript(), ...overrides.transcript }, voice: { ...buildVoice(), ...overrides.voice } }) const buildComposer = () => ({ enqueue: vi.fn(), hasSelection: false, paste: vi.fn(), queueRef: { current: [] as string[] }, selection: { copySelection: vi.fn(() => '') }, setInput: vi.fn() }) const buildGateway = () => ({ gw: { getLogTail: vi.fn(() => ''), request: vi.fn(() => Promise.resolve({})) }, rpc: vi.fn(() => Promise.resolve({})) }) const buildLocal = () => ({ catalog: null, getHistoryItems: vi.fn(() => []), getLastUserMsg: vi.fn(() => ''), maybeWarn: vi.fn() }) const buildSession = () => ({ closeSession: vi.fn(() => Promise.resolve(null)), die: vi.fn(), guardBusySessionSwitch: vi.fn(() => false), newSession: vi.fn(), resetVisibleHistory: vi.fn(), resumeById: vi.fn(), setSessionStartedAt: vi.fn() }) const buildTranscript = () => ({ page: vi.fn(), panel: vi.fn(), send: vi.fn(), setHistoryItems: vi.fn(), sys: vi.fn(), trimLastExchange: vi.fn(items => items) }) const buildVoice = () => ({ setVoiceEnabled: vi.fn() }) interface Ctx { composer: ReturnType gateway: ReturnType local: ReturnType session: ReturnType transcript: ReturnType voice: ReturnType }