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) })