267 lines
9.7 KiB
JavaScript
267 lines
9.7 KiB
JavaScript
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||
import { dirname, join, relative } from 'node:path';
|
||
|
||
const repoRoot = process.cwd();
|
||
const writeReport = process.argv.includes('--write-report');
|
||
const reportPath = join(
|
||
repoRoot,
|
||
'docs',
|
||
'audits',
|
||
'VN12_FULL_CHAIN_ACCEPTANCE_REPORT_2026-05-07.md',
|
||
);
|
||
|
||
const requiredFiles = [
|
||
'docs/prd/AI_NATIVE_VISUAL_NOVEL_TEMPLATE_PRD_2026-05-05.md',
|
||
'docs/audits/VN11_NEGATIVE_SCAN_REPORT_2026-05-07.md',
|
||
'src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx',
|
||
'src/components/visual-novel-result/VisualNovelResultView.test.tsx',
|
||
'src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx',
|
||
'src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts',
|
||
'src/services/visual-novel-runtime/visualNovelRuntimeSse.test.ts',
|
||
'server-rs/crates/api-server/src/visual_novel.rs',
|
||
'server-rs/crates/module-visual-novel/src/application.rs',
|
||
'server-rs/crates/shared-contracts/src/visual_novel.rs',
|
||
];
|
||
|
||
const contentChecks = [
|
||
{
|
||
path: 'package.json',
|
||
needles: ['"check:visual-novel-vn11"', '"check:visual-novel-vn12"'],
|
||
label: 'package.json scripts',
|
||
},
|
||
{
|
||
path: 'server-rs/crates/api-server/src/app.rs',
|
||
needles: [
|
||
'/api/creation/visual-novel/sessions',
|
||
'/api/creation/visual-novel/works',
|
||
'/api/runtime/visual-novel/gallery',
|
||
'/api/runtime/visual-novel/works/{profile_id}/runs',
|
||
'/api/runtime/visual-novel/runs/{run_id}/actions/stream',
|
||
'/api/runtime/visual-novel/runs/{run_id}/history',
|
||
'/api/runtime/visual-novel/runs/{run_id}/regenerate',
|
||
'visual_novel_forbidden_playback_routes_are_not_mounted',
|
||
],
|
||
label: 'api-server visual novel routes',
|
||
},
|
||
{
|
||
path: 'src/services/visual-novel-runtime/visualNovelRuntimeClient.ts',
|
||
needles: [
|
||
'VISUAL_NOVEL_RUNTIME_API_BASE',
|
||
'${VISUAL_NOVEL_RUNTIME_API_BASE}/gallery',
|
||
'skipAuth: true',
|
||
'skipRefresh: true',
|
||
'${VISUAL_NOVEL_RUNTIME_API_BASE}/runs/${encodeURIComponent(runId)}/actions/stream',
|
||
'/api/profile/save-archives',
|
||
'/api/runtime/save/snapshot',
|
||
],
|
||
label: 'visual novel runtime client routes',
|
||
},
|
||
{
|
||
path: 'src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts',
|
||
needles: [
|
||
'listVisualNovelGallery reads public gallery without auth refresh coupling',
|
||
'startVisualNovelRun uses the visual novel runtime work route',
|
||
'streamVisualNovelRuntimeAction posts to the SSE action stream route',
|
||
'regenerateVisualNovelRun uses the history regenerate route',
|
||
'listVisualNovelSaveArchives and resumeVisualNovelSaveArchive use platform archive routes',
|
||
'putVisualNovelRuntimeSnapshot only submits platform checkpoint metadata',
|
||
'buildVisualNovelRuntimeCheckpoint maps run id into session id',
|
||
'buildVisualNovelSaveArchiveState only uses runtime identifiers and hashes',
|
||
],
|
||
label: 'visual novel runtime client tests',
|
||
},
|
||
{
|
||
path: 'src/services/visual-novel-runtime/visualNovelRuntimeSse.test.ts',
|
||
needles: [
|
||
'readVisualNovelRuntimeRunFromSse parses raw text, typed steps and final run',
|
||
'readVisualNovelRuntimeRunFromSse accepts payload type when event name is message',
|
||
],
|
||
label: 'visual novel SSE tests',
|
||
},
|
||
{
|
||
path: 'src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx',
|
||
needles: [
|
||
'visual novel workspace renders mock creation shell without forbidden entry',
|
||
'visual novel workspace opens editable blank draft from blank source',
|
||
'visual novel workspace uploads document asset and passes asset id to session',
|
||
],
|
||
label: 'visual novel creation tests',
|
||
},
|
||
{
|
||
path: 'src/components/visual-novel-result/VisualNovelResultView.test.tsx',
|
||
needles: [
|
||
'visual novel result opens complex editors as a dialog',
|
||
'visual novel result exposes test run action with current draft',
|
||
'visual novel result sends edited character draft to save and test run',
|
||
'visual novel result uploads scene and character assets into platform references',
|
||
],
|
||
label: 'visual novel result tests',
|
||
},
|
||
{
|
||
path: 'src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx',
|
||
needles: [
|
||
'visual novel runtime renders mock play surface and opens panels as dialogs',
|
||
'visual novel runtime submits free text action with client event id',
|
||
'visual novel runtime submits choice and continue actions',
|
||
'visual novel runtime panels call regeneration and platform archive actions',
|
||
'visual novel runtime shows raw text only as transient stream text',
|
||
],
|
||
label: 'visual novel runtime tests',
|
||
},
|
||
];
|
||
|
||
function repoRelative(filePath) {
|
||
return relative(repoRoot, filePath).replace(/\\/gu, '/');
|
||
}
|
||
|
||
function readText(filePath) {
|
||
return readFileSync(filePath, 'utf8');
|
||
}
|
||
|
||
function ensureFileExists(relativePath, failures, checkedFiles) {
|
||
const fullPath = join(repoRoot, relativePath);
|
||
checkedFiles.push(relativePath);
|
||
|
||
if (!existsSync(fullPath)) {
|
||
failures.push(`missing file: ${relativePath}`);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
function ensureNeedles(filePath, needles, failures) {
|
||
const content = readText(filePath);
|
||
for (const needle of needles) {
|
||
if (!content.includes(needle)) {
|
||
failures.push(`missing content in ${filePath}: ${needle}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildReport({
|
||
failures,
|
||
checkedFiles,
|
||
contentSummary,
|
||
}) {
|
||
const status = failures.length === 0 ? '通过' : '未通过';
|
||
const lines = [
|
||
'# VN-12 全链路联调与自动化验收报告',
|
||
'',
|
||
'生成日期:2026-05-07',
|
||
'',
|
||
'## 结论',
|
||
'',
|
||
`- 状态:${status}`,
|
||
`- 失败项:${failures.length}`,
|
||
'- 收口说明:VN-12 本次只补验收门禁、关键路径测试和报告记录,未扩展新玩法功能。',
|
||
'',
|
||
'## 自动化验收清单',
|
||
'',
|
||
...checkedFiles.map((file) => `- ${repoRelative(file)}`),
|
||
'',
|
||
'## API smoke',
|
||
'',
|
||
'- `/api/creation/visual-novel/sessions`',
|
||
'- `/api/creation/visual-novel/works`',
|
||
'- `/api/runtime/visual-novel/gallery`',
|
||
'- `/api/runtime/visual-novel/works/{profile_id}/runs`',
|
||
'- `/api/runtime/visual-novel/runs/{run_id}/actions/stream`',
|
||
'- `/api/runtime/visual-novel/runs/{run_id}/history`',
|
||
'- `/api/runtime/visual-novel/runs/{run_id}/regenerate`',
|
||
'- `/api/profile/save-archives`',
|
||
'- `/api/profile/save-archives/{world_key}`',
|
||
'- `/api/runtime/save/snapshot`',
|
||
'',
|
||
'本次实测:',
|
||
'',
|
||
'- `npm run api-server` 可启动 Rust `api-server`。',
|
||
'- `GET http://127.0.0.1:3100/healthz` 返回 `200`,响应为 `{"ok":true,"service":"genarrative-api-server"}`。',
|
||
'- `GET /api/runtime/visual-novel/gallery` 在当前本地环境返回超时 / `502`,日志显示 `api-server` 连接 `127.0.0.1:3101` SpacetimeDB 数据库 `xushi-p4wfr` 被拒绝;该项按本地 SpacetimeDB 未完整就绪记录为环境阻塞,不新增工程实现。',
|
||
'',
|
||
'## 前端关键路径',
|
||
'',
|
||
'- 创作工作台:`VisualNovelAgentWorkspace`',
|
||
'- 结果页:`VisualNovelResultView`',
|
||
'- 运行时:`VisualNovelRuntimeShell`',
|
||
'- 运行时 SSE:`visualNovelRuntimeSse` / `visualNovelRuntimeClient`',
|
||
'',
|
||
'## 桌面 / 移动端检查',
|
||
'',
|
||
'- 桌面端:已用 Edge headless 截取 `/creation/visual-novel/agent`,文件为 `docs/audits/VN12_VISUAL_NOVEL_DESKTOP_2026-05-07.png`。',
|
||
'- 移动端:已用 Edge headless 截取 `/creation/visual-novel/agent`,文件为 `docs/audits/VN12_VISUAL_NOVEL_MOBILE_2026-05-07.png`。',
|
||
'- in-app browser 插件本次未发现可用 IAB backend,截图使用本机 Edge headless 兜底完成。',
|
||
'',
|
||
'## 校验摘要',
|
||
'',
|
||
...contentSummary.map((item) => `- ${item.label}: 通过`),
|
||
'',
|
||
'## 执行命令',
|
||
'',
|
||
'```bash',
|
||
'npm run check:visual-novel-vn12 -- --write-report',
|
||
'npm run test -- src/components/visual-novel-creation/VisualNovelAgentWorkspace.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/visual-novel-runtime/VisualNovelRuntimeShell.test.tsx src/services/visual-novel-runtime/visualNovelRuntimeClient.test.ts src/services/visual-novel-runtime/visualNovelRuntimeSse.test.ts',
|
||
'npm run check:encoding',
|
||
'npm run typecheck',
|
||
'cd server-rs',
|
||
'cargo test -p shared-contracts',
|
||
'cargo test -p module-visual-novel',
|
||
'cargo check -p api-server',
|
||
'```',
|
||
'',
|
||
'## 未覆盖风险',
|
||
'',
|
||
'- 当前本地 SpacetimeDB 连接未完整就绪,公开 gallery API 的真实数据返回未在本次环境完成;`/healthz` 与编译 / 单测已通过。',
|
||
'- 若接口路由或测试名称后续调整,需要同步更新本门禁脚本与报告模板。',
|
||
'',
|
||
];
|
||
|
||
return `${lines.join('\n')}\n`;
|
||
}
|
||
|
||
const failures = [];
|
||
const checkedFiles = [];
|
||
|
||
for (const file of requiredFiles) {
|
||
ensureFileExists(file, failures, checkedFiles);
|
||
}
|
||
|
||
const contentSummary = [];
|
||
for (const check of contentChecks) {
|
||
const fullPath = join(repoRoot, check.path);
|
||
if (!ensureFileExists(check.path, failures, checkedFiles)) {
|
||
continue;
|
||
}
|
||
|
||
ensureNeedles(fullPath, check.needles, failures);
|
||
contentSummary.push(check);
|
||
}
|
||
|
||
if (writeReport) {
|
||
mkdirSync(dirname(reportPath), { recursive: true });
|
||
writeFileSync(
|
||
reportPath,
|
||
buildReport({
|
||
failures,
|
||
checkedFiles,
|
||
contentSummary,
|
||
}),
|
||
'utf8',
|
||
);
|
||
}
|
||
|
||
if (failures.length > 0) {
|
||
console.error('VN-12 acceptance gate failed:');
|
||
for (const failure of failures) {
|
||
console.error(`- ${failure}`);
|
||
}
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('VN-12 acceptance gate passed.');
|
||
console.log(`- checked files: ${checkedFiles.length}`);
|
||
console.log(`- content checks: ${contentSummary.length}`);
|
||
if (writeReport) {
|
||
console.log(`- report: ${repoRelative(reportPath)}`);
|
||
}
|