import assert from 'node:assert/strict'; import { after, test } from 'node:test'; import { fileURLToPath } from 'node:url'; import { createServer } from 'vite'; const storage = new Map([ ['aihr_mobile_access_token', 'expired-token'], ['aihr_mobile_client_id', 'app-client'], ['aihr_mobile_phone', '13800000000'], ['aihr_mobile_tenant_id', '000000'] ]); const redirects = []; let uploadShouldFail = false; globalThis.plus = {}; globalThis.getCurrentPages = () => [{ route: 'pages/user/sop/index', $page: { fullPath: '/pages/user/sop/index?source=rc006' } }]; globalThis.uni = { getStorageSync: (key) => storage.get(key) || '', setStorageSync: (key, value) => storage.set(key, value), removeStorageSync: (key) => storage.delete(key), reLaunch: ({ url }) => redirects.push(url), request: ({ fail }) => { queueMicrotask(() => fail({ errMsg: 'request:fail network' })); return { abort: () => {} }; }, uploadFile: ({ fail, success }) => { queueMicrotask(() => uploadShouldFail ? fail({ errMsg: 'uploadFile:fail network' }) : success({ statusCode: 200, data: '{"code":200,"data":{"text":"ok"}}' })); return { abort: () => {} }; } }; const vite = await createServer({ root: fileURLToPath(new URL('..', import.meta.url)), configFile: false, optimizeDeps: { noDiscovery: true }, server: { middlewareMode: true, hmr: false }, appType: 'custom' }); after(async () => { await vite.close(); delete globalThis.plus; delete globalThis.uni; delete globalThis.window; delete globalThis.getCurrentPages; }); test('ASR upload failure has a stable retryable result', async () => { uploadShouldFail = true; const { transcribeSpeechFile } = await vite.ssrLoadModule('/src/services/speech.ts'); await assert.rejects( transcribeSpeechFile({ path: '/tmp/voice.mp3', name: 'voice.mp3', size: 3 }), /语音上传失败/ ); }); test('TTS network failure falls back to device speech when supported', async () => { const spoken = []; globalThis.window = { SpeechSynthesisUtterance: class { constructor(text) { this.text = text; } }, speechSynthesis: { speak: (utterance) => spoken.push(utterance.text), cancel: () => {} } }; const states = []; const errors = []; const { createSpeechPlaybackController } = await vite.ssrLoadModule('/src/services/speech.ts'); const controller = createSpeechPlaybackController( (state) => states.push(state.status), (message) => errors.push(message) ); await controller.toggle('answer-1', '请先确认问题,再说明处理时限'); assert.deepEqual(spoken, ['请先确认问题,再说明处理时限']); assert.ok(states.includes('loading')); assert.ok(states.includes('playing')); assert.deepEqual(errors, []); controller.destroy(); }); test('401 clears the expired session and relaunches login while 403 does not', async () => { const { ApiError, readPayload } = await vite.ssrLoadModule('/src/services/api.ts'); assert.throws( () => readPayload(401, { code: 401, msg: 'login expired' }), (error) => error instanceof ApiError && error.statusCode === 401 ); assert.equal(storage.has('aihr_mobile_access_token'), false); assert.equal(storage.get('aihr_mobile_login_redirect'), '/pages/user/sop/index?source=rc006'); assert.deepEqual(redirects, ['/pages/auth/login/index']); storage.set('aihr_mobile_access_token', 'new-token'); assert.throws( () => readPayload(403, { code: 403, msg: 'forbidden' }), (error) => error instanceof ApiError && error.statusCode === 403 ); assert.equal(storage.get('aihr_mobile_access_token'), 'new-token'); assert.deepEqual(redirects, ['/pages/auth/login/index']); });