import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; const source = (path) => readFile(new URL(path, import.meta.url), 'utf8'); test('工作上报首屏是语音优先会话,不再暴露传统表单主路径', async () => { const page = await source('../src/pages/user/report/index.vue'); assert.match(page, /成果投稿/); assert.match(page, /把好经验沉淀给公司复用/); assert.match(page, /我的投稿/); assert.match(page, /哪段处置经验值得公司复用?按住说,也可以发照片或视频,我会整理成案例、SOP 或知识投稿。/); assert.match(page, / { const page = await source('../src/pages/user/report/index.vue'); assert.match(page, /:panel-items="composerPanelItems"/); assert.match(page, /label: '照片\/视频'/); assert.match(page, /label: '选择音频'/); assert.match(page, /@panel="handlePanelAction"/); assert.doesNotMatch(page, /仅用于本次现场分析/); }); test('语音转写、文字和附件补充都进入无状态整理', async () => { const page = await source('../src/pages/user/report/index.vue'); assert.match(page, /语音转文字/); assert.match(page, /transcribeSpeechCapture/); assert.match(page, /transcribeSpeechFile/); assert.match(page, /organizeWorkReport\(/); assert.match(page, /const previousDraft = draftPayload\(\)/); assert.match(page, /currentDraft:\s*previousDraft/); assert.match(page, /attachmentOssId:\s*attachment\.value\?\.ossId/); assert.match(page, /void organizeInput\(text, \{ voice: false \}\)/); assert.match(page, /void addMediaAttachment\(\)/); }); test('短追问最多两条,文字直接可见并只提供播放', async () => { const page = await source('../src/pages/user/report/index.vue'); assert.match(page, /missingQuestions\.slice\(0, 2\)/); assert.match(page, /class="question-text"/); assert.match(page, /toggleQuestionSpeech/); assert.match(page, /aria-label="播放追问"/); assert.doesNotMatch(page, />转文字<\/button>/); assert.doesNotMatch(page, /收起文字/); }); test('确认卡允许修改最终类型标题正文且不承诺自动脱敏', async () => { const page = await source('../src/pages/user/report/index.vue'); assert.match(page, /建议类型/); assert.match(page, /changeDraftType/); assert.match(page, /v-model="currentDraft\.title"/); assert.match(page, /v-model="currentDraft\.content"/); assert.match(page, /检测到具体房号,请修改后再提交/); assert.match(page, /const hasRoomPrivacyWarning = computed/); assert.match(page, /请处理上述敏感信息后再提交/); assert.match(page, /const finishDraftEditing = async/); assert.match(page, /manuallyConfirmedType\.value\s*\|\|/); assert.match(page, /manuallyConfirmedType\.value = selected\.value/); assert.match(page, /requestId:\s*submitRequestId\.value[\s\S]*type:\s*draft\.suggestedType/); assert.match(page, /transcript:\s*''/); assert.match(page, /if \(privacyReviewRequired\.value\)/); assert.doesNotMatch(page, /privacyReviewed/); assert.match(page, /editingDraft \? '完成修改' : '修改'/); assert.match(page, /@click="confirmSubmit"/); assert.match(page, /'确认投稿'/); assert.doesNotMatch(page, /自动隐藏/); assert.doesNotMatch(page, />继续补充<\/button>/); }); test('正式提交只发生在确认动作并复用稳定幂等键', async () => { const page = await source('../src/pages/user/report/index.vue'); const submitCalls = page.match(/submitWorkReport\(/g) || []; assert.equal(submitCalls.length, 1); assert.match(page, /const confirmSubmit = async \(\) =>/); assert.match(page, /if \(submitActionDisabled\.value \|\| editingDraft\.value\)/); assert.match(page, /stableWorkReportRequestId\(submitRequestId\.value, createRequestId\)/); assert.match(page, /requestId:\s*submitRequestId\.value/); assert.match(page, /const draftLocked = computed/); assert.match(page, /\|\| Boolean\(submitRequestId\.value\)/); assert.match(page, /isDefinitiveWorkReportRejection/); assert.match(page, /if \(rejected\) submitRequestId\.value = ''/); assert.match(page, /const record = await submitWorkReport/); assert.match(page, /submittedReport\.value = record/); assert.match(page, /await loadMine\(true\)/); assert.match(page, /PENDING/); assert.match(page, /待审核/); assert.match(page, /submittedReport\.status\.toLowerCase\(\)/); assert.match(page, /submittedReport\.statusLabel/); assert.match(page, /@click="startNewReport">再报一条/); }); test('异步请求具备 generation、上传取消和离页 teardown', async () => { const page = await source('../src/pages/user/report/index.vue'); assert.match(page, /let organizeGeneration = 0/); assert.match(page, /isCurrentWorkReportRequest/); assert.match(page, /currentAccountKey\(\)/); assert.match(page, /visibleAccountKey/); assert.match(page, /mediaSelecting\.value/); assert.match(page, /abortVoiceTranscription\?\.\(\)/); assert.match(page, /activeUploadTask\?\.abort\(\)/); assert.match(page, /!previousDraft \|\| submitActionDisabled\.value \|\| draftLocked\.value/); assert.match(page, /:disabled="draftLocked \|\| submitActionDisabled"/); assert.match(page, /composerRef\.value\?\.teardown\(\)/); assert.match(page, /speechPlayback\.stop\(\)/); assert.match(page, /onHide\(\(\) => teardown\(isPlatformFileSelectionActive\(\)\)\)/); assert.match(page, /onUnload/); }); test('AI 长草稿不被前端静默截断,改由确认时明确校验', async () => { const page = await source('../src/pages/user/report/index.vue'); assert.doesNotMatch(page, /String\(result\.content[\s\S]*?\.slice\(0, 2000\)/); assert.match(page, /content\.length > 2000/); assert.match(page, /投稿内容不能超过2000字/); }); test('工作上报服务声明整理契约并在正式提交携带 requestId', async () => { const service = await source('../src/services/work-report.ts'); assert.match(service, /export interface WorkReportDraft/); assert.match(service, /suggestedType:\s*WorkReportType/); assert.match(service, /export interface OrganizeResponse extends WorkReportDraft/); assert.match(service, /missingQuestions:\s*string\[\]/); assert.match(service, /privacyWarnings:\s*string\[\]/); assert.match(service, /export const organizeWorkReport/); assert.match(service, /\/api\/aihr\/work-report\/organize/); assert.match(service, /requestId:\s*string/); assert.match(service, /data:\s*params/); }); test('幂等键与异步账号守卫是可执行行为,不只是源码字符串', async (context) => { const { createServer } = await import('vite'); const root = fileURLToPath(new URL('../', import.meta.url)); const server = await createServer({ root, configFile: false, appType: 'custom', logLevel: 'silent', resolve: { alias: { '@': fileURLToPath(new URL('../src', import.meta.url)) } }, server: { middlewareMode: true } }); context.after(() => server.close()); const state = await server.ssrLoadModule('/src/services/work-report.ts'); let created = 0; const create = () => `request-${++created}`; const first = state.stableWorkReportRequestId('', create); const retry = state.stableWorkReportRequestId(first, create); assert.equal(first, retry); assert.equal(created, 1); assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 400 }), true); assert.equal(state.isDefinitiveWorkReportRejection({ bizCode: 409 }), false); assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 408 }), false); assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 429 }), false); assert.equal(state.isDefinitiveWorkReportRejection({ statusCode: 500 }), false); assert.equal(state.isDefinitiveWorkReportRejection({}), false); assert.equal(state.isCurrentWorkReportRequest(3, 3, 'account-a', 'account-a'), true); assert.equal(state.isCurrentWorkReportRequest(2, 3, 'account-a', 'account-a'), false); assert.equal(state.isCurrentWorkReportRequest(3, 3, 'account-a', 'account-b'), false); });