Files
aiturk-hermes-ide/apps/mobile/tests/voice-usage.test.mjs
T

101 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { VOICE_PRICING, VoiceUsage, estimateUsage, voiceLimits, usageLabel } from '../src/voice-usage.mjs'
import { VoiceSession } from '../src/voice.mjs'
const model = VOICE_PRICING.model
const report = { promptTokenCount: 2000, responseTokenCount: 1000, totalTokenCount: 3000,
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 500 }, { modality: 'AUDIO', tokenCount: 1500 }],
responseTokensDetails: [{ modality: 'TEXT', tokenCount: 100 }, { modality: 'AUDIO', tokenCount: 900 }] }
const expected = (500 * VOICE_PRICING.input.TEXT + 1500 * VOICE_PRICING.input.AUDIO +
100 * VOICE_PRICING.output.TEXT + 900 * VOICE_PRICING.output.AUDIO) / 1e6
test('reported input history is billed again on each request, including identical reports', () => {
const usage = new VoiceUsage(model, { minutes: 10, usd: expected * 1.5 })
assert.equal(usage.add(report).reached, false)
const next = usage.add(report)
assert.equal(next.lowUsd, expected * 2); assert.equal(next.highUsd, expected * 2)
assert.equal(next.reports, 2); assert.equal(next.reached, true)
})
test('missing or unknown modality yields a finite range, not zero/free usage', () => {
for (const details of [undefined, [{ modality: 'toString', tokenCount: 3000 }]]) {
const cost = estimateUsage({ promptTokenCount: 3000, promptTokensDetails: details }, model)
assert.ok(cost.lowUsd > 0); assert.ok(cost.highUsd > cost.lowUsd); assert.equal(cost.uncertain, true)
}
const extra = estimateUsage({ ...report, thoughtsTokenCount: 100, toolUsePromptTokenCount: 100 }, model)
assert.ok(extra.highUsd > expected); assert.equal(extra.uncertain, true)
const noBreakdown = estimateUsage({ totalTokenCount: 1000 }, model)
assert.ok(noBreakdown.highUsd > 0); assert.equal(noBreakdown.uncertain, true)
})
test('invalid metadata cannot poison the running total or silently bypass the limit', () => {
const usage = new VoiceUsage(model)
usage.add(report); const before = usage.snapshot()
for (const metadata of [{}, { promptTokenCount: -1 }, { responseTokenCount: NaN },
{ promptTokenCount: '100' }, { ...report, totalTokenCount: 1 },
{ promptTokenCount: 1, promptTokensDetails: [{ modality: 'AUDIO', tokenCount: 2 }] }]) {
assert.throws(() => usage.add(metadata)); assert.deepEqual(usage.snapshot(), before)
}
assert.throws(() => estimateUsage(report, 'unknown-model'))
})
test('limits validate actual numeric bounds and usage label does not claim quota or invoice', () => {
assert.deepEqual(voiceLimits({ minutes: '3', usd: '0.05' }), { minutes: 3, usd: 0.05 })
for (const value of [{ minutes: 0, usd: 1 }, { minutes: 11, usd: 1 }, { minutes: 1, usd: 0 },
{ minutes: 1, usd: Infinity }, { minutes: 1.5, usd: 0.25 }, { minutes: 1, usd: 11 }]) assert.throws(() => voiceLimits(value))
assert.match(usageLabel(new VoiceUsage(model).snapshot(), 0), /henüz bilinmiyor/)
const usage = new VoiceUsage(model); usage.add(report)
assert.match(usageLabel(usage.snapshot(), 30), /IDE modeli dahil değildir/)
})
test('usage threshold closes voice before a co-delivered tool can submit IDE work', async () => {
let dispatched = 0, closed = 0, tracks = 0
const notices = []
const voice = new VoiceSession({ model, limits: { minutes: 10, usd: 0.01 }, onText: (_, text) => notices.push(text),
onState: text => notices.push(text), onSubmit: () => { dispatched++ } })
voice.startedAt = voice.now(); voice.lastUsageAt = voice.startedAt
voice.stream = { getTracks: () => [{ stop() { tracks++ } }] }
voice.socket = { readyState: 1, close() { closed++ }, send() { throw new Error('must not send after cap') } }
await voice.receive({ data: JSON.stringify({ usageMetadata: report,
toolCall: { functionCalls: [{ id: '1', name: 'submit_task', args: {} }] } }) })
voice.text = VoiceSession.prototype.text.bind(voice)
assert.throws(() => voice.text('Bunu yap'))
voice.result({ status: 'complete', text: 'Done' }); voice.stop()
assert.equal(voice.stopped, true); assert.deepEqual([dispatched, closed, tracks], [0, 1, 1])
assert.ok(notices.some(text => text.includes('maliyet sınırına')))
assert.ok(notices.some(text => text.includes('IDE işi devam edebilir')))
})
test('malformed usage closes an established session with a visible error', async () => {
const errors = []
const voice = new VoiceSession({ model, onError: text => errors.push(text) })
voice.ready = true
await voice.receive({ data: JSON.stringify({ usageMetadata: {} }) })
assert.equal(voice.stopped, true); assert.match(errors[0], /hesaplanamadı/)
})
for (const scenario of [
{ minutes: 1, elapsed: 60000, reason: /süresi doldu/ },
{ minutes: 10, elapsed: 120000, reason: /bildirimi alınamadı/ },
]) test(`real voice lifecycle timer stops without resuming: ${scenario.minutes} minute setting`, async t => {
t.mock.timers.enable({ apis: ['setInterval', 'setTimeout'] })
let now = 0, connections = 0, closed = 0
const messages = [], usage = []
const originalContext = globalThis.AudioContext, originalSocket = globalThis.WebSocket
globalThis.AudioContext = class { async resume() {} async close() {} }
globalThis.WebSocket = class { constructor() { connections++ } close() { closed++ } }
t.after(() => { globalThis.AudioContext = originalContext; globalThis.WebSocket = originalSocket })
const voice = new VoiceSession({ token: 'test-only', model, now: () => now,
limits: { minutes: scenario.minutes, usd: 0.25 }, onText() {}, onError: text => messages.push(text),
onState: text => messages.push(text), onUsage: value => usage.push(value) })
t.after(() => voice.stop())
await voice.start(false)
await voice.receive({ data: JSON.stringify({ setupComplete: {} }) })
now = scenario.elapsed; t.mock.timers.tick(scenario.elapsed)
assert.equal(voice.stopped, true); assert.deepEqual([connections, closed], [1, 1])
assert.ok(messages.some(text => scenario.reason.test(text)))
assert.equal(usage.at(-1).seconds, scenario.elapsed / 1000)
t.mock.timers.tick(600000); assert.equal(connections, 1)
})