102 lines
3.7 KiB
TypeScript
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')
|
|
})
|
|
})
|