diff --git a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs index caab020..c302f57 100644 --- a/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +++ b/shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs @@ -2485,3 +2485,61 @@ test('测试批次管理与旧数据纳入遵守 root、权限和预览提交契 assert.doesNotMatch(source, /TestBatchAdoptionDialog/, `${reportPath} 不得提供纳入入口`) } }) + +test('测试批次授权、清理和对话框异步状态拒绝错误目标与过期会话', async () => { + const managementPath = 'src/views/system/testBatch/index.vue' + const management = await readFile(path.join(frontendRoot, managementPath), 'utf8') + const parseAssignedUserIds = Function(`return (${extractNamedFunction(management, 'parseAssignedUserIds')})`)() + const shouldLeaveTestMode = Function(`return (${extractNamedFunction(management, 'shouldLeaveTestMode')})`)() + + assert.deepEqual(parseAssignedUserIds(''), { valid: true, userIds: [] }, '空输入是显式移除全部授权') + assert.deepEqual(parseAssignedUserIds('1, 2,1'), { valid: true, userIds: [1, 2] }) + for (const invalidInput of ['1,abc,2', '1,,2', '1,', '0', '-1', '1.5']) { + assert.deepEqual(parseAssignedUserIds(invalidInput), { valid: false, userIds: [] }, `${invalidInput} 必须整体拒绝`) + } + assert.equal(shouldLeaveTestMode('12', 12), true, '仅相同批次清理后退出测试模式') + assert.equal(shouldLeaveTestMode('13', 12), false, '清理其他批次不得退出当前测试模式') + assert.equal(shouldLeaveTestMode('', 12), false) + + assert.doesNotMatch(management, /row\.userCount/, '列表 VO 未声明 userCount,禁止直接消费') + assert.match(management, /let listRequestVersion = 0/) + assert.match(management, /function hydrateParticipantCounts\(/) + assert.match(management, /participantState:\s*['"]loading['"]/, '详情 hydration 前必须显式 loading') + assert.match(management, /participantState:\s*['"]error['"]/, '单个详情失败必须展示明确失败状态') + assert.match(management, /userIds\.length/, '参与人数必须来自详情已声明的 userIds') + assert.match(management, /function isCurrentListRequest\(listContext\)[\s\S]*?listContext\.version === listRequestVersion/) + + assert.match(management, /const detailLoaded = ref\(false\)/) + assert.match(management, /let assignSession = 0/) + assert.match(management, /function isCurrentAssignContext\(context\)[\s\S]*?context\.session === assignSession[\s\S]*?context\.batchId === assignRequestBatchId\.value/) + const openAssignSource = extractNamedFunction(management, 'openAssign') + assert.match(openAssignSource, /detailLoaded\.value = false/) + assert.match(openAssignSource, /if \(!isCurrentAssignContext\(context\)\) return/) + const submitAssignSource = extractNamedFunction(management, 'submitAssign') + assert.match(submitAssignSource, /if \(assignSubmitting\.value \|\| !detailLoaded\.value \|\| !assignBatch\.value\) return/) + assert.match(submitAssignSource, /const parsed = parseAssignedUserIds\(assignedUserIds\.value\)/) + assert.match(submitAssignSource, /if \(!parsed\.valid\)/) + assert.match(submitAssignSource, /if \(!isCurrentAssignContext\(context\)\) return/) + assert.match(management, /function resetAssignDialog\(\)[\s\S]*?assignSession \+= 1[\s\S]*?assignBatch\.value = null[\s\S]*?detailLoaded\.value = false/) + assert.match(management, /:disabled="assignLoading \|\| !detailLoaded \|\| assignSubmitting"/) + assert.match(management, /if \(shouldLeaveTestMode\(testBatchStore\.currentBatchId, purgeBatch\.value\.batchId\)\) \{\s*testBatchStore\.leaveTestMode\(\)/) + assert.match(management, /class="test-batch-dialog"/) + assert.match(management, /:column="descriptionColumns"/) + assert.match(management, /max-height:\s*90vh/) + assert.match(management, /overflow-y:\s*auto/) + + const adoptionPath = 'src/components/TestBatchAdoptionDialog/index.vue' + const adoption = await readFile(path.join(frontendRoot, adoptionPath), 'utf8') + assert.match(adoption, /let dialogSession = 0/) + assert.match(adoption, /let optionsRequestVersion = 0/) + assert.match(adoption, /function isCurrentOptionsRequest\(context\)[\s\S]*?context\.session === dialogSession[\s\S]*?context\.version === optionsRequestVersion[\s\S]*?visible\.value/) + const loadBatchesSource = extractNamedFunction(adoption, 'loadBatches') + for (const callback of ['then', 'catch', 'finally']) { + assert.match(loadBatchesSource, new RegExp(`\\.${callback}\\([\\s\\S]*?if \\(!isCurrentOptionsRequest\\(context\\)\\) return`), `options ${callback} 必须拒绝过期会话`) + } + assert.match(adoption, /function resetState\(\)[\s\S]*?dialogSession \+= 1[\s\S]*?optionsRequestVersion \+= 1/) + assert.match(adoption, /class="test-batch-adoption-dialog"/) + assert.match(adoption, /:column="descriptionColumns"/) + assert.match(adoption, /max-height:\s*90vh/) + assert.match(adoption, /overflow-y:\s*auto/) +}) diff --git a/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue b/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue index aa18312..bf7622e 100644 --- a/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue +++ b/shierfeng-fastapi-frontend/src/components/TestBatchAdoptionDialog/index.vue @@ -1,6 +1,7 @@