d3ro-voice/apps/mobile-rn/__tests__/command-service.test.ts
2026-08-29 18:33:45 +09:00

102 lines
3.7 KiB
TypeScript

import {
BUILTIN_COMMANDS,
buildBuiltinCommandPrompt,
COMMAND_INPUT_MAX_CHARS,
CommandExecutionError,
executeBuiltinCommand,
getBuiltinCommand,
buildSyncedCommandPrompt,
normalizeSyncedCommand,
} from '../src/features/commands/command-service'
const originalFetch = global.fetch
afterEach(() => {
global.fetch = originalFetch
})
describe('mobile built-in command service', () => {
it('exposes an immutable, read-only catalog instead of fake CRUD data', () => {
expect(Object.isFrozen(BUILTIN_COMMANDS)).toBe(true)
expect(BUILTIN_COMMANDS).toHaveLength(4)
expect(BUILTIN_COMMANDS.every((command) => Object.isFrozen(command))).toBe(true)
expect(getBuiltinCommand('missing')).toBeNull()
})
it('builds a bounded prompt from the selected verified command', () => {
expect(buildBuiltinCommandPrompt('builtin-summarize', ' 실제 입력 '))
.toContain('실제 입력')
expect(() => buildBuiltinCommandPrompt('missing', 'text'))
.toThrow(CommandExecutionError)
expect(() => buildBuiltinCommandPrompt('builtin-formal', ' '))
.toThrow(CommandExecutionError)
expect(() => buildBuiltinCommandPrompt(
'builtin-formal',
'x'.repeat(COMMAND_INPUT_MAX_CHARS + 1),
)).toThrow(CommandExecutionError)
})
it('executes through the authenticated llm-proxy and returns only provider output', async () => {
const generationId = '11111111-1111-4111-8111-111111111111'
global.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: (name: string) => name.toLowerCase() === 'x-d3ro-generation-id' ? generationId : null },
json: async () => ({ content: [{ type: 'text', text: '서버 결과' }] }),
})
await expect(executeBuiltinCommand('builtin-formal', '원문', {
accessToken: 'signed-access-token',
model: 'claude-haiku-4-5-20251001',
})).resolves.toEqual({ text: '서버 결과', generationId })
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/functions/v1/llm-proxy'),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer signed-access-token',
'X-D3RO-Generation-Purpose': 'command_response',
}),
}),
)
})
it('fails closed on provider errors without returning the original input', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 503,
json: async () => ({ error: 'provider_unavailable' }),
})
await expect(executeBuiltinCommand('builtin-translate-en', '민감한 원문', {
accessToken: 'signed-access-token',
})).rejects.toMatchObject({
code: 'PROVIDER_UNAVAILABLE',
retryable: true,
status: 503,
})
})
it('normalizes the server command SSOT and builds bounded custom prompts', () => {
const command = normalizeSyncedCommand({
id: '00000000-0000-4000-8000-000000000011',
user_id: '00000000-0000-4000-8000-000000000012',
builtin_key: null,
name: 'Release notes',
description: 'Summarize a release',
prompt: 'Summarize without inventing facts: {{text}}',
icon: 'sparkles',
sort_order: 100,
revision: 2,
created_at: '2026-08-21T00:00:00Z',
updated_at: '2026-08-21T00:01:00Z',
}, '00000000-0000-4000-8000-000000000012')
expect(command).toMatchObject({ builtinKey: null, revision: 2, sortOrder: 100 })
expect(buildSyncedCommandPrompt(command, ' 실제 변경 내용 ')).toContain('실제 변경 내용')
expect(() => normalizeSyncedCommand({
...command,
user_id: '00000000-0000-4000-8000-000000000099',
}, '00000000-0000-4000-8000-000000000012')).toThrow('server')
})
})