import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import ts from 'typescript' import { sceneGroups } from './scene-editor-feature-catalog.mjs' import { sceneEvidenceBindings, sceneUnitPlan } from './scene-editor-evidence-bindings.mjs' // Static test-plan extraction, never executes a business test or accesses API. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const report = path.join(root, 'reports/scene-editor-closure-20260906') const catalog = sceneGroups.flatMap(([group, rows]) => rows.map(([id, name]) => ({ id, name, group }))) const browser = [] const literal = node => node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : null function property(node, key) { return node && ts.isObjectLiteralExpression(node) ? node.properties.find(p => ts.isPropertyAssignment(p) && p.name.getText().replace(/['"]/g, '') === key)?.initializer : null } for (const basename of fs.readdirSync(path.join(root, 'tests')).filter(f => /^scene-editor-.*closure\.spec\.ts$/.test(f))) { const file = `tests/${basename}`, text = fs.readFileSync(path.join(root, file), 'utf8') const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true) function visit(node, title = '') { if (ts.isCallExpression(node) && node.expression.getText(source) === 'test') title = literal(node.arguments[0]) || node.arguments[0].getText(source) if (ts.isCallExpression(node) && node.expression.getText(source) === 'evidence') { const name = literal(node.arguments[0]), data = node.arguments[1] const idsNode = property(data, 'featureIds') const explicitIds = idsNode && ts.isArrayLiteralExpression(idsNode) ? idsNode.elements.map(literal).filter(Boolean) : [] const binding = name && sceneEvidenceBindings[name] const ids = explicitIds.length ? explicitIds : binding?.featureIds || [] const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1 if (name) { const featureIds = name === 'basic-assets' ? [...ids, ...['diagnostic','equipment','workbench','shelf','safety-zone','camera','worker','light'].map(v => `asset.${v}`)] : ids browser.push({ file, line, evidenceName: name, testTitle: title, kind: 'browser', featureIds: [...new Set(featureIds)], scope: literal(property(data, 'scope')) || binding?.scope || '以源测试实际断言为准;计划关联不代表最终通过。' }) } else if (basename === 'scene-editor-templates-closure.spec.ts') { for (const item of catalog.filter(item => item.id.startsWith('template.') && item.id !== 'template.switch-cancel')) { browser.push({ file, line, evidenceName: item.id.replace('template.', 'template-'), testTitle: `场景模板实际加载、保存重开:${item.name}`, kind: 'browser', featureIds: [item.id], scope: '循环参数化模板用例,每种独立副本、真实点击、保存、刷新与截图。' }) } } else if (basename === 'scene-editor-downstream-closure.spec.ts') { for (const key of ['fox','truck']) browser.push({ file, line, evidenceName: `downstream-${key}`, testTitle: `${key}:场景发布后在真实训练编排选择、保存并重开静态模型`, kind: 'browser', featureIds: ids, scope: '真实训练编排静态还原;不计来源模型动画/蓝图自动执行。' }) } } ts.forEachChild(node, child => visit(child, title)) } visit(source) } const unit = sceneUnitPlan.map(([file, testTitle, featureIds]) => ({ file, testTitle, featureIds, kind: 'unit', scope: '聚焦合同/真实DOM/Three对象计算;不称为浏览器鼠标交互。' })) const rows = catalog.map(item => ({ ...item, verification: 'PENDING', plannedEvidence: [...browser, ...unit].filter(proof => proof.featureIds.includes(item.id)) })) const unknownIds = [...new Set([...browser, ...unit].flatMap(p => p.featureIds).filter(id => !catalog.some(row => row.id === id)))] const data = { generatedAt: new Date().toISOString(), status: 'PENDING', scope: '只读解析当前spec与明确unit计划;不执行测试,不推断最终PASS。', count: { total: rows.length, withBrowserPlan: rows.filter(r => r.plannedEvidence.some(p => p.kind === 'browser')).length, onlyUnitPlan: rows.filter(r => r.plannedEvidence.length && r.plannedEvidence.every(p => p.kind === 'unit')).length, unmapped: rows.filter(r => !r.plannedEvidence.length).length }, unknownIds, gaps: rows.filter(r => !r.plannedEvidence.length).map(({ id, name }) => ({ id, name })), features: rows } fs.mkdirSync(path.join(report, 'data'), { recursive: true }) fs.writeFileSync(path.join(report, 'data/coverage-test-plan.json'), JSON.stringify(data, null, 2)) console.log(JSON.stringify({ count: data.count, gaps: data.gaps, unknownIds }, null, 2))