import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; const sourceApi = readFileSync(resolve(process.cwd(), 'src/api/source.ts'), 'utf8'); const projectApi = readFileSync(resolve(process.cwd(), 'src/api/project.ts'), 'utf8'); const formCenter = readFileSync(resolve(process.cwd(), 'src/pages/forms/FormCenterPage.vue'), 'utf8'); const formEditor = readFileSync(resolve(process.cwd(), 'src/pages/forms/FormEditorPage.vue'), 'utf8'); const taskCenter = readFileSync(resolve(process.cwd(), 'src/pages/tasks/TaskCenterPage.vue'), 'utf8'); const projectDetail = readFileSync(resolve(process.cwd(), 'src/pages/projects/ProjectDetailPage.vue'), 'utf8'); const accountingPage = readFileSync(resolve(process.cwd(), 'src/pages/finance/AccountingPage.vue'), 'utf8'); const projectArchives = readFileSync(resolve(process.cwd(), 'src/pages/archives/ProjectArchivesPage.vue'), 'utf8'); const archiveFiles = readFileSync(resolve(process.cwd(), 'src/pages/archives/ArchiveFilesPage.vue'), 'utf8'); const projectSubresourcePanel = readFileSync( resolve(process.cwd(), 'src/components/business/project/ProjectSubresourcePanel.vue'), 'utf8', ); describe('pAGE-07 through PAGE-11 command controls', () => { it('adds command idempotency headers to source and project writes', () => { expect(sourceApi).toContain("commandHeaders('source-form-create', idempotencyKey)"); expect(sourceApi).toContain("commandHeaders('source-form-submit', idempotencyKey)"); expect(sourceApi).toContain('commandHeaders(`workflow-task-' + '$' + '{action}`, idempotencyKey)'); expect(sourceApi).toContain("commandHeaders('workflow-instance-withdraw', idempotencyKey)"); expect(sourceApi).toContain("commandHeaders('workflow-instance-void', idempotencyKey)"); expect(sourceApi).toContain("commandHeaders('source-import-create', idempotencyKey)"); expect(sourceApi).toContain("commandHeaders('source-import-confirm', idempotencyKey)"); expect(projectApi).toContain("commandHeaders('project-risk-create', idempotencyKey)"); expect(projectApi).toContain("commandHeaders('project-risk-resolve', idempotencyKey)"); }); it('gates mutations with server allowedActions and re-reads server state', () => { expect(formCenter).toContain("!row.allowedActions.includes('VALIDATE')"); expect(formCenter).toContain('await loadForms();'); expect(formEditor).toContain("detail.value.summary.allowedActions.includes('SUBMIT')"); expect(formEditor).toContain('await loadDetail();'); expect(taskCenter).toContain('if (!canRunAction(task, nextAction)) return;'); expect(taskCenter).toContain('await getWorkflowTask(task.publicId);'); expect(taskCenter).toContain('task.form.allowedActions.includes(code)'); expect(taskCenter).toContain('await withdrawSourceForm('); expect(taskCenter).toContain('await voidSourceForm('); expect(taskCenter).toContain('const detail = await getWorkflowTask(task.publicId);'); expect(taskCenter).not.toContain("opinion: '审批通过'"); expect(projectDetail).toContain("!project.value.allowedActions.includes('CREATE_RISK_FLAG')"); expect(projectDetail).toContain('await loadProject();'); }); it('uses active source template metadata and TDesign controls for projection fields', () => { expect(sourceApi).toContain('suppliers: Array.isArray(references?.suppliers)'); expect(formEditor).toContain('const active = (await getSourceTemplates()).find'); expect(formEditor).not.toContain('getSourceTemplate(formType.value, 1)'); expect(formEditor).toContain("field.code.toLowerCase().includes('supplier')"); expect(formEditor).toContain("column.dataType === 'ENUM'"); expect(formEditor).toContain("column.dataType === 'DATE'"); expect(formEditor).toContain('detail.value?.fields || template.value?.fields'); expect(formEditor).toContain(' { expect(projectSubresourcePanel).toContain('#businessNo="{ row }"'); expect(projectSubresourcePanel).toContain("$emit('open-row', row)"); expect(projectDetail).toContain('@open-row="openDrillRow(tab.value, $event)"'); expect(projectDetail).toContain("name: 'FormEditor'"); expect(projectDetail).toContain('query: { from: route.fullPath }'); expect(projectDetail).toContain("openLinkedPage('/finance/payments'"); expect(projectDetail).toContain("openLinkedPage('/finance/accounting'"); expect(projectDetail).toContain("openLinkedPage('/archives/files'"); expect(formEditor).toContain("from.startsWith('/projects/')"); }); it('keeps project accounting and archive drill-down bidirectional across lifecycle states', () => { expect(projectDetail).toContain("voucherStatus: textValue(row.voucherStatus, '')"); expect(projectDetail).toContain("sortAt: textValue(isVoucher ? row.updatedAt : row.createdAt, '')"); expect(projectDetail).toContain('return rightTime - leftTime;'); expect(projectDetail).toContain('from: route.fullPath'); expect(accountingPage).toContain("name: 'ProjectDetail'"); expect(accountingPage).toContain('function openEventSource(event: AccountingEvent)'); expect(accountingPage).toContain("route.name !== 'Accounting'"); expect(formCenter).toContain("name: 'ProjectDetail'"); expect(projectArchives).toContain('function openSnapshot(snapshot: ArchivePackageObjectSnapshot)'); expect(projectArchives).toContain("ACCOUNTING_VOUCHER: { path: '/finance/accounting'"); expect(archiveFiles).toContain("name: 'ProjectDetail'"); }); });