import { describe, expect, it } from 'vitest' import { parseVisualGameKpAgentOutput, parseVisualGameNpcActionKpOutput, parseVisualGameNpcActorOutput, } from '../src/index.js' import { VISUAL_GAME_NPC_STATE_PATCH_FIELDS, VISUAL_GAME_OBJECT_STATE_PATCH_FIELDS, } from '../src/visual-game.js' function kpOutput() { return { expression_state: null, action: { intent: 'investigate', targets: ['bronze_mirror'], stance: 'cautious', risk: 'normal', claims: [], }, judgementSuggestion: { valid: true }, clueJudgements: [], sceneResult: { narration: '', visibleFindings: [], discoveredObjectIds: [] }, responsePolicy: { npcReaction: 'none', reason: '玩家只检查环境。' }, npcDirective: { npcId: 'painted_woman', tactic: 'observe', mood: 'gentle', mustReactTo: [], mustHide: [], }, } } describe('visual-game Agent output contracts', () => { it('exports frozen CandidateDelta patch field definitions', () => { expect(Object.isFrozen(VISUAL_GAME_OBJECT_STATE_PATCH_FIELDS)).toBe(true) expect(Object.isFrozen(VISUAL_GAME_NPC_STATE_PATCH_FIELDS)).toBe(true) expect(VISUAL_GAME_NPC_STATE_PATCH_FIELDS.mood).toEqual({ schemaKind: 'string', registryScope: 'npc_state', registryPathKind: 'field', registryValueTypes: ['string'], }) }) it('accepts the complete KP boundary shape', () => { expect(parseVisualGameKpAgentOutput(kpOutput())).toEqual(kpOutput()) }) it('accepts a branch resolution decision', () => { const kp = kpOutput() kp.branch_resolution = { branch_id: 'branch_capture', matched: true, decision: 'resolve', resolution_summary: '精灵球完成三次摇晃并锁定,女鬼已经被收服。', } expect(parseVisualGameKpAgentOutput(kp)).toEqual(kp) }) it('requires an explicit dynamic-story tracker continuation choice', () => { const kp = kpOutput() kp.story_signal = { should_track: true, continuation_tracker_id: null, topic_cluster: 'capture_supernatural_entity', display_name: '异类收服', topic_summary: '玩家尝试收服屋内的超自然存在。', topic_kind: 'countermeasure', player_intended_action: '收服屋内的超自然存在。', decisive_action: false, distinct_progress: true, progress_marker: 'prepare_capture_device', reason: '玩家正在推进新的收服方向。', } expect(parseVisualGameKpAgentOutput(kp)).toEqual(kp) delete kp.story_signal.continuation_tracker_id expect(() => parseVisualGameKpAgentOutput(kp)).toThrow() }) it('rejects missing valid and misplaced judgement fields', () => { const missingValid = kpOutput() missingValid.judgementSuggestion = {} expect(() => parseVisualGameKpAgentOutput(missingValid)).toThrow() const misplaced = kpOutput() misplaced.judgementSuggestion.sceneResult = misplaced.sceneResult expect(() => parseVisualGameKpAgentOutput(misplaced)).toThrow() }) it('rejects unknown NPC Actor patch fields', () => { expect(() => parseVisualGameNpcActorOutput({ expression_state: 'cold_threatening', narration: '她伸手去拿镜子。', npcLine: '给我。', proposedNpcActions: [{ type: 'take_object', targetId: 'bronze_mirror', intent: '夺走镜子', description: '她试图夺走镜子。', candidate_delta: { objectStateChanges: { bronze_mirror: { obtained: true }, }, }, }], tacticUsed: 'seize_mirror', tone: 'threatening', playerPromptType: 'warning', playerOptions: { say: '你为什么要拿?', act: '把镜子收回身后。' }, })).toThrow() }) it('accepts the built-in NPC mood patch field', () => { const output = { expression_state: 'cold_threatening', narration: '守门人收起笑意。', npcLine: '到此为止。', proposedNpcActions: [{ type: 'change_mood', targetId: 'keeper', intent: '转为警戒', description: '守门人进入警戒状态。', candidate_delta: { npcStateChanges: { keeper: { mood: 'alert' }, }, }, }], tacticUsed: 'warn_player', tone: 'threatening', playerPromptType: 'warning', playerOptions: { say: '发生什么了?', act: '后退一步。' }, } expect(parseVisualGameNpcActorOutput(output)).toEqual(output) }) it('rejects an NPC-action relationshipFlags array', () => { expect(() => parseVisualGameNpcActionKpOutput({ npcId: 'painted_woman', approvedNpcActions: [{ type: 'move', targetId: 'door', result: '她走到门边。' }], judgementSuggestion: { valid: true, npcStateChanges: { painted_woman: { relationshipFlags: ['blocking_exit'] }, }, }, sceneResult: { narration: '她挡在门前。', visibleFindings: [], discoveredObjectIds: [] }, })).toThrow() }) it('rejects possibleEnding from every model delta', () => { const kp = kpOutput() kp.judgementSuggestion.possibleEnding = 'ending_escape' expect(() => parseVisualGameKpAgentOutput(kp)).toThrow() expect(() => parseVisualGameNpcActorOutput({ expression_state: 'cold_threatening', narration: '她伸手挡住门扇。', npcLine: '你不能走。', proposedNpcActions: [{ type: 'block', targetId: 'door', intent: '阻止玩家离开', description: '她试图挡住门。', candidate_delta: { possibleEnding: 'ending_trapped' }, }], tacticUsed: 'block_exit', tone: 'threatening', playerPromptType: 'warning', playerOptions: { say: '让开。', act: '试着绕过她。' }, })).toThrow() expect(() => parseVisualGameNpcActionKpOutput({ npcId: 'painted_woman', approvedNpcActions: [{ type: 'block', targetId: 'door', result: '她挡住门。' }], judgementSuggestion: { valid: true, reason: '她就在门边。', possibleEnding: 'ending_trapped', }, sceneResult: { narration: '她挡在门前。', visibleFindings: [], discoveredObjectIds: [] }, })).toThrow() }) })