import { describe, expect, it } from 'vitest'; import { AnimationState, type StoryOption } from '../../types'; import { resolveStoryResponseOptions } from './storyResponseOptions'; function createOption( functionId: string, actionText: string, priority = 0, interaction?: StoryOption['interaction'], ): StoryOption { return { functionId, actionText, text: actionText, priority, interaction, visuals: { playerAnimation: AnimationState.IDLE, playerMoveMeters: 0, playerOffsetY: 0, playerFacing: 'right', scrollWorld: false, monsterChanges: [], }, }; } describe('storyResponseOptions', () => { it('keeps rewritten actionText when camp companion follow-up uses available options', () => { const availableOptions = [ createOption('npc_chat', '先聊聊营地安排', 3), createOption('npc_gift', '把旧礼物递给你', 2), createOption('camp_travel_home_scene', '前往旧地点', 1), ]; const responseOptions = [ createOption('npc_chat', '顺着你刚才的话继续问下去', 3), createOption('npc_gift', '把刚挑好的礼物正式交给你', 2), createOption('camp_travel_home_scene', '前往云河渡', 1), ]; const resolved = resolveStoryResponseOptions({ responseOptions, availableOptions, getSanitizedOptions: () => { throw new Error('available options branch should not sanitize'); }, }); expect(resolved.map((option) => option.actionText)).toEqual([ '顺着你刚才的话继续问下去', '把刚挑好的礼物正式交给你', '前往云河渡', ]); }); it('preserves interaction metadata when AI rewrites provided npc options', () => { const availableOptions = [ createOption('npc_chat', '继续交谈', 3, { kind: 'npc', npcId: 'npc-camp', action: 'chat', }), createOption('camp_travel_home_scene', '前往旧地点', 1), ]; const responseOptions = [ createOption('npc_chat', '顺着你刚才那句提醒继续追问', 3), createOption('camp_travel_home_scene', '先回云河渡', 1), ]; const resolved = resolveStoryResponseOptions({ responseOptions, availableOptions, getSanitizedOptions: () => { throw new Error('available options branch should not sanitize'); }, }); expect(resolved[0]).toEqual( expect.objectContaining({ functionId: 'npc_chat', actionText: '顺着你刚才那句提醒继续追问', interaction: { kind: 'npc', npcId: 'npc-camp', action: 'chat', }, }), ); }); it('falls back to available options when the response omits them entirely', () => { const availableOptions = [ createOption('npc_chat', '继续交谈', 2), createOption('camp_travel_home_scene', '前往山门', 1), ]; const resolved = resolveStoryResponseOptions({ responseOptions: [], availableOptions, getSanitizedOptions: () => { throw new Error('available options fallback should not sanitize'); }, }); expect(resolved.map((option) => option.actionText)).toEqual([ '继续交谈', '前往山门', ]); }); it('keeps only AI-selected options when optionCatalog is used for reasoned follow-ups', () => { const optionCatalog = [ createOption('npc_chat', '继续交谈', 3, { kind: 'npc', npcId: 'npc-camp', action: 'chat', }), createOption('npc_help', '请求援手', 2, { kind: 'npc', npcId: 'npc-camp', action: 'help', }), createOption('npc_trade', '看看能交换什么', 1, { kind: 'npc', npcId: 'npc-camp', action: 'trade', }), ]; const responseOptions = [ createOption('npc_help', '顺着刚才的话请他搭把手', 3), createOption('npc_chat', '追问他刚才为什么突然沉默', 2), ]; const resolved = resolveStoryResponseOptions({ responseOptions, optionCatalog, getSanitizedOptions: () => { throw new Error('option catalog branch should not sanitize'); }, }); expect(resolved).toEqual([ expect.objectContaining({ functionId: 'npc_help', actionText: '顺着刚才的话请他搭把手', interaction: { kind: 'npc', npcId: 'npc-camp', action: 'help', }, }), expect.objectContaining({ functionId: 'npc_chat', actionText: '追问他刚才为什么突然沉默', interaction: { kind: 'npc', npcId: 'npc-camp', action: 'chat', }, }), ]); }); it('falls back to the raw catalog only when the AI omits optionCatalog results entirely', () => { const optionCatalog = [ createOption('npc_chat', '继续交谈', 2), createOption('npc_trade', '看看能交换什么', 1), ]; const resolved = resolveStoryResponseOptions({ responseOptions: [], optionCatalog, getSanitizedOptions: () => { throw new Error('option catalog fallback should not sanitize'); }, }); expect(resolved.map((option) => option.actionText)).toEqual([ '继续交谈', '看看能交换什么', ]); }); });