test(live-atc): cover the wait auto-tune actually depends on

The decision to tune was tested; the three seconds between announcing it and
doing it were not, and that gap is where the feature can go wrong — it is the
only window in which the radio moves on its own.

Moves the scheduling into a testable unit and wires the composable to it, so
the tested code is the code that runs. Covered: nothing is announced when no
change is due, the change is announced immediately but made only after the
delay, and it is dropped when the pilot tuned the radio themselves, when the
session ended, and when a second handoff superseded it — the last of which would
otherwise have tuned to a stale frequency.

Leaving the flight or unmounting now cancels a pending change explicitly rather
than relying on the fire-time guards to notice.
This commit is contained in:
itsrubberduck
2026-07-27 10:04:07 +02:00
parent cd4c640040
commit db1c9948dc
3 changed files with 204 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { AUTO_TUNE_DELAY_MS, planAutoTune } from '~~/shared/utils/autoTune'
import { AUTO_TUNE_DELAY_MS, createAutoTuneScheduler, planAutoTune } from '~~/shared/utils/autoTune'
const plan = (over: Partial<Parameters<typeof planAutoTune>[0]> = {}) =>
planAutoTune({ enabled: true, active: '118.700', expected: '121.800', ...over })
@@ -65,3 +65,90 @@ describe('planAutoTune', () => {
assert.equal(plan({ active: '118.700', expected: '118.700' }), null)
})
})
describe('createAutoTuneScheduler — the wait between announcing and tuning', () => {
function harness(over: Partial<Parameters<typeof planAutoTune>[0]> = {}) {
const calls = { announced: [] as string[], tuned: [] as string[], cancelled: [] as string[] }
let sessionId: string | null = 'session-1'
let active = '118.700'
let fire: (() => void) | null = null
let delay = 0
const scheduler = createAutoTuneScheduler({
announce: t => calls.announced.push(t),
tune: f => { calls.tuned.push(f); active = f },
currentSessionId: () => sessionId,
currentActive: () => active,
onCancelled: r => calls.cancelled.push(r),
setTimeoutFn: (fn, ms) => { fire = fn; delay = ms; return 1 },
clearTimeoutFn: () => { fire = null },
})
return {
calls,
scheduler,
get delay() { return delay },
run: () => { const f = fire; fire = null; f?.() },
endSession: () => { sessionId = null },
tuneManually: (f: string) => { active = f },
schedule: (extra: Record<string, unknown> = {}) => scheduler.schedule({
enabled: true, active, expected: '121.800', ...over, ...extra,
} as any),
}
}
it('announces immediately and tunes only after the delay', () => {
const h = harness()
h.schedule()
assert.deepEqual(h.calls.announced, ['OpenSquawk changing frequency to 121.800'])
assert.deepEqual(h.calls.tuned, [], 'tuned before the announcement was heard')
assert.equal(h.delay, AUTO_TUNE_DELAY_MS)
h.run()
assert.deepEqual(h.calls.tuned, ['121.800'])
})
it('says nothing at all when no change is due', () => {
const h = harness()
assert.equal(h.schedule({ active: '121.800' }), null)
assert.deepEqual(h.calls.announced, [])
assert.equal(h.scheduler.pending, false)
})
it('drops the change when the pilot tunes the radio first', () => {
const h = harness()
h.schedule()
h.tuneManually('119.500')
h.run()
assert.deepEqual(h.calls.tuned, [], 'overrode the pilot')
assert.deepEqual(h.calls.cancelled, ['tuned_manually'])
})
it('drops the change when the session ended while waiting', () => {
const h = harness()
h.schedule()
h.endSession()
h.run()
assert.deepEqual(h.calls.tuned, [])
assert.deepEqual(h.calls.cancelled, ['session_changed'])
})
it('a second handoff supersedes the first rather than tuning twice', () => {
const h = harness()
h.schedule()
h.schedule({ expected: '131.150' })
assert.deepEqual(h.calls.cancelled, ['superseded'])
h.run()
assert.deepEqual(h.calls.tuned, ['131.150'], 'tuned to the stale frequency')
})
it('cancel() stops a pending change', () => {
const h = harness()
h.schedule()
assert.equal(h.scheduler.pending, true)
h.scheduler.cancel()
assert.equal(h.scheduler.pending, false)
h.run()
assert.deepEqual(h.calls.tuned, [])
})
})