import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { dirname, extname, join, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(fileURLToPath(new URL("../../..", import.meta.url))); const webRoot = join(root, "kdl-wasm", "web"); const deployRoot = join(webRoot, "test-results", "deploy"); const zipPath = resolve(process.argv[2] ?? latestZipPath()); const extractRoot = join(deployRoot, "_verify", zipBasename(zipPath)); assertSafeVerifyPath(extractRoot); rmSync(extractRoot, { recursive: true, force: true }); mkdirSync(extractRoot, { recursive: true }); expandZip(zipPath, extractRoot); const manifest = readJson(join(extractRoot, "demo-manifest.json")); const report = readJson(join(extractRoot, manifest.report_json)); const programManifest = readJson(join(extractRoot, manifest.program_manifest)); const grlFiles = walkFiles(join(extractRoot, "spec-programs")).filter((file) => extname(file).toLowerCase() === ".grl"); const requiredFiles = [ "index.html", "demo-manifest.json", "README.md", "app/virtual-controller.html", "app/virtual-controller.css", "app/virtual-controller.js", "spec-programs/manifest.json", manifest.job_json, manifest.report_json, manifest.report_html, manifest.screenshots.desktop, manifest.screenshots.mobile, manifest.screenshots.evidence, "docs/GRL功能语法逻辑与程序创建使用手册.docx", "docs/通用机器人离线编程系统测试文档.docx", "docs/working2/README.md", "docs/working2/05-验收证据.md" ]; const checks = []; for (const file of requiredFiles) { checks.push(checkFile(file)); } checks.push(check("program_count", manifest.program_count === 19, `program_count=${manifest.program_count}`)); checks.push(check("manifest_programs", programManifest.programs.length === 19, `manifest programs=${programManifest.programs.length}`)); checks.push(check("grl_files", grlFiles.length === 19, `grl files=${grlFiles.length}`)); checks.push(check("summary_fail", report.summary.fail === 0, `summary.fail=${report.summary.fail}`)); checks.push(check("report_programs", report.summary.programs === 19, `report programs=${report.summary.programs}`)); checks.push(check("runtime_programs", manifest.runtime_programs === 12, `runtime=${manifest.runtime_programs}`)); checks.push(check("static_programs", manifest.static_programs === 4, `static=${manifest.static_programs}`)); checks.push(check("post_programs", manifest.post_programs === 3, `post=${manifest.post_programs}`)); checks.push(check("missing_sections", Array.isArray(report.coverage.missingSections) && report.coverage.missingSections.length === 0, `missing=${report.coverage.missingSections?.join(",") ?? "n/a"}`)); checks.push(check("relative_urls", allManifestUrlsAreRelative(manifest), "manifest URLs are relative")); for (const program of manifest.programs) { checks.push(checkFile(program.source)); checks.push(checkFile(program.artifacts.compile)); } for (const doc of manifest.docs ?? []) { checks.push(checkFile(doc.href)); } const runtimeFramePrograms = manifest.programs.filter((program) => program.coverage_level === "Runtime"); let frameProgramCount = 0; for (const program of runtimeFramePrograms) { const trajectory = readJson(join(extractRoot, program.artifacts.trajectory)); const frames = Array.isArray(trajectory.frames) ? trajectory.frames : []; const hasFrameState = frames.some((frame) => Array.isArray(frame.joints) && frame.joints.length === 6 && Array.isArray(frame.tcp?.position) && frame.tcp.position.length === 3 && Array.isArray(frame.tcp?.quaternion) && frame.tcp.quaternion.length === 4 ); if (hasFrameState) frameProgramCount += 1; } checks.push(check( "runtime_trajectory_frames", frameProgramCount > 0, `${frameProgramCount}/${runtimeFramePrograms.length} runtime programs include frame joints and TCP` )); const failed = checks.filter((item) => !item.ok); const summary = { zip: zipPath, extract_root: extractRoot, release_id: manifest.release_id, job_id: manifest.job_id, program_count: manifest.program_count, grl_files: grlFiles.length, summary_fail: report.summary.fail, checks, ok: failed.length === 0 }; writeFileSync(join(deployRoot, `${zipBasename(zipPath)}.verify.json`), `${JSON.stringify(summary, null, 2)}\n`, "utf8"); console.log(JSON.stringify(summary, null, 2)); if (failed.length > 0) { throw new Error(`Demo package verification failed: ${failed.map((item) => item.name).join(", ")}`); } function checkFile(relativePath) { const file = join(extractRoot, relativePath); const exists = existsSync(file); return check(`file:${relativePath}`, exists && statSync(file).isFile(), exists ? `${statSync(file).size} bytes` : "missing"); } function check(name, ok, details) { return { name, ok: Boolean(ok), details }; } function allManifestUrlsAreRelative(value) { const urls = []; collectUrls(value, urls); return urls.every((url) => !/^[a-z]+:\/\//i.test(url) && !/^[a-z]:[\\/]/i.test(url) && !url.startsWith("/") && !url.includes("\\")); } function collectUrls(value, urls) { if (Array.isArray(value)) { value.forEach((item) => collectUrls(item, urls)); return; } if (!value || typeof value !== "object") return; for (const [key, item] of Object.entries(value)) { if (typeof item === "string" && (key === "href" || key.includes("json") || key.includes("html") || key.includes("entry") || key.includes("manifest") || key.includes("source") || key.includes("screenshot") || key.includes("report") || key.includes("compile") || key.includes("controller") || key.includes("trace") || key.includes("trajectory") || key.includes("roundtrip") || key.includes("io") || key.includes("queue"))) { urls.push(item); } else { collectUrls(item, urls); } } } function expandZip(source, destination) { const script = [ "Add-Type -AssemblyName System.IO.Compression.FileSystem", `$src = ${psSingleQuoted(source)}`, `$dst = ${psSingleQuoted(destination)}`, "if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Recurse -Force }", "New-Item -ItemType Directory -Force -Path $dst | Out-Null", "[System.IO.Compression.ZipFile]::ExtractToDirectory($src, $dst)" ].join("\n"); execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], { stdio: "inherit" }); } function latestZipPath() { if (!existsSync(deployRoot)) { throw new Error(`No deploy output directory found: ${deployRoot}`); } const zips = readdirSync(deployRoot, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.startsWith("kdl-olp-demo-") && entry.name.endsWith(".zip")) .map((entry) => ({ path: join(deployRoot, entry.name), mtimeMs: statSync(join(deployRoot, entry.name)).mtimeMs })) .sort((left, right) => right.mtimeMs - left.mtimeMs); if (zips.length === 0) { throw new Error(`No kdl-olp-demo-*.zip files found in ${deployRoot}`); } return zips[0].path; } function walkFiles(dir) { const files = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { files.push(...walkFiles(fullPath)); } else if (entry.isFile()) { files.push(fullPath); } } return files; } function readJson(file) { return JSON.parse(readFileSync(file, "utf8")); } function zipBasename(file) { return file.split(/[\\/]/).at(-1).replace(/\.zip$/i, ""); } function psSingleQuoted(value) { return `'${String(value).replaceAll("'", "''")}'`; } function assertSafeVerifyPath(path) { const allowedRoot = resolve(join(deployRoot, "_verify")) + sep; const resolved = resolve(path); if (!resolved.startsWith(allowedRoot)) { throw new Error(`Refusing to modify path outside verify root: ${resolved}`); } }