visual-game-agent.test.js 6.19 KB
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()
  })
})