mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-15 11:35:40 +08:00
158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
import { describe, it, beforeEach } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
|
|
import type { RuntimeDecisionState, RuntimeDecisionSystem } from '~~/shared/types/decision'
|
|
import type { LLMDecisionInput } from '~~/shared/types/llm'
|
|
import { __setRuntimeDecisionSystemForTests, routeDecision } from './openai'
|
|
|
|
const createState = (overrides: Partial<RuntimeDecisionState>): RuntimeDecisionState => ({
|
|
role: 'pilot',
|
|
phase: 'ground',
|
|
name: 'State',
|
|
summary: 'Generic state',
|
|
say_tpl: undefined,
|
|
utterance_tpl: undefined,
|
|
else_say_tpl: undefined,
|
|
next: [],
|
|
ok_next: [],
|
|
bad_next: [],
|
|
timer_next: [],
|
|
auto: null,
|
|
readback_required: undefined,
|
|
actions: undefined,
|
|
handoff: undefined,
|
|
guard: undefined,
|
|
trigger: undefined,
|
|
frequency: undefined,
|
|
frequencyName: undefined,
|
|
auto_transitions: [],
|
|
triggers: [],
|
|
conditions: [],
|
|
metadata: undefined,
|
|
...overrides,
|
|
})
|
|
|
|
const START = createState({
|
|
name: 'Start',
|
|
summary: 'Start of flow',
|
|
role: 'atc',
|
|
})
|
|
|
|
const ACK = createState({
|
|
name: 'Acknowledge',
|
|
summary: 'Acknowledge pilot readback',
|
|
triggers: [
|
|
{ id: 'ack-regex', type: 'regex', pattern: 'roger', patternFlags: 'i' },
|
|
],
|
|
})
|
|
|
|
const TAXI = createState({
|
|
name: 'Taxi clearance',
|
|
summary: 'Pilot requesting taxi clearance',
|
|
triggers: [
|
|
{ id: 'taxi-regex', type: 'regex', pattern: 'request', patternFlags: 'i' },
|
|
],
|
|
})
|
|
|
|
const HOLD = createState({
|
|
name: 'Hold position',
|
|
summary: 'Pilot requesting hold position',
|
|
triggers: [
|
|
{ id: 'hold-regex', type: 'regex', pattern: 'request', patternFlags: 'i' },
|
|
],
|
|
})
|
|
|
|
START.next = [
|
|
{ to: 'ACK' },
|
|
{ to: 'TAXI' },
|
|
{ to: 'HOLD' },
|
|
]
|
|
|
|
const runtimeSystem: RuntimeDecisionSystem = {
|
|
main: 'main',
|
|
order: ['main'],
|
|
flows: {
|
|
main: {
|
|
slug: 'main',
|
|
schema_version: '1.0',
|
|
name: 'Main Flow',
|
|
start_state: 'START',
|
|
end_states: [],
|
|
variables: {},
|
|
flags: {},
|
|
policies: {},
|
|
hooks: {},
|
|
roles: ['pilot', 'atc', 'system'],
|
|
phases: ['ground'],
|
|
entry_mode: 'main',
|
|
states: {
|
|
START,
|
|
ACK,
|
|
TAXI,
|
|
HOLD,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const baseInput: Omit<LLMDecisionInput, 'candidates'> = {
|
|
state_id: 'START',
|
|
state: START,
|
|
variables: { callsign: 'TEST123' },
|
|
flags: { current_unit: 'TWR', in_air: false },
|
|
pilot_utterance: '',
|
|
}
|
|
|
|
describe('routeDecision', () => {
|
|
beforeEach(() => {
|
|
__setRuntimeDecisionSystemForTests(runtimeSystem)
|
|
})
|
|
|
|
it('returns heuristic decision when exactly one candidate matches', async () => {
|
|
const input: LLMDecisionInput = {
|
|
...baseInput,
|
|
pilot_utterance: 'Roger that',
|
|
candidates: [
|
|
{ id: 'ACK', state: ACK },
|
|
],
|
|
}
|
|
|
|
const result = await routeDecision(input)
|
|
|
|
assert.equal(result.decision.next_state, 'ACK')
|
|
assert.equal(result.trace?.calls.length ?? 0, 0)
|
|
assert.equal(result.trace?.autoSelection?.id, 'ACK')
|
|
assert.equal(result.pilot_intent ?? null, null)
|
|
})
|
|
|
|
it('falls back to heuristic selection when OpenAI call fails', async () => {
|
|
const input: LLMDecisionInput = {
|
|
...baseInput,
|
|
pilot_utterance: 'Request taxi instructions',
|
|
candidates: [
|
|
{ id: 'TAXI', state: TAXI },
|
|
{ id: 'HOLD', state: HOLD },
|
|
],
|
|
}
|
|
|
|
const previousApiKey = process.env.OPENAI_API_KEY
|
|
process.env.OPENAI_API_KEY = ''
|
|
let result: Awaited<ReturnType<typeof routeDecision>>
|
|
try {
|
|
result = await routeDecision(input)
|
|
} finally {
|
|
if (previousApiKey === undefined) {
|
|
delete process.env.OPENAI_API_KEY
|
|
} else {
|
|
process.env.OPENAI_API_KEY = previousApiKey
|
|
}
|
|
}
|
|
|
|
assert.equal(result.trace?.calls.length, 1)
|
|
assert.ok(result.trace?.calls[0]?.error)
|
|
assert.equal(result.trace?.fallback?.used, true)
|
|
assert.equal(result.decision.next_state, 'TAXI')
|
|
assert.equal(result.pilot_intent ?? null, null)
|
|
})
|
|
})
|