import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { tmpdir } from "node:os"; import { spawn } from "node:child_process"; import { createServer } from "node:net"; const packageRoot = resolve(process.env.DADA_POSTV1_PACKAGE_ROOT ?? ".build/portable-release/Dada-P0A-0.0.0-win-x64"); const port = 43121; async function waitForHealth(child) { const deadline = Date.now() + 15_000; while (Date.now() < deadline) { if (child.exitCode !== null) throw new Error(`packaged api exited: ${child.exitCode}: ${child.errorOutput ?? ""}`); try { const response = await fetch(`http://127.0.0.1:${port}/healthz`, { headers: { host: `127.0.0.1:${port}` } }); if (response.ok) return; } catch {} await new Promise((resolveDelay) => setTimeout(resolveDelay, 100)); } throw new Error("packaged api health timeout"); } function startApi(configPath, dataRoot) { const child = spawn(join(packageRoot, "runtime", "node.exe"), [join(packageRoot, "server", "api.mjs"), "--dada-credential-stdin"], { cwd: packageRoot, env: { ...process.env, DADA_INSTANCE_CONFIG_PATH: configPath, DADA_SUPPORT_GATE_ROOT: join(packageRoot, "web", "support-gate"), DADA_WEB_ROOT: join(packageRoot, "web") }, stdio: ["pipe", "ignore", "pipe"], windowsHide: true, }); child.errorOutput = ""; child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk) => { child.errorOutput += chunk; }); child.stdin.end(JSON.stringify({ "Dada/P0A/api/amap": "", "Dada/P0A/api/resend": "", "Dada/P0A/admin/pepper": "portable-test-pepper-00000000000000000000000000000000" })); return child; } async function stop(child) { if (child.exitCode === null) { child.kill(); await new Promise((resolveExit) => child.once("exit", resolveExit)); } } async function verifyWorkerStartup(configPath) { const pipeName = `Dada.P0A.PostV1.${process.pid}.${Date.now()}`; const pipePath = `\\\\.\\pipe\\${pipeName}`; const server = createServer(); await new Promise((resolveListen, rejectListen) => { server.once("error", rejectListen); server.listen(pipePath, resolveListen); }); const child = spawn(join(packageRoot, "runtime", "node.exe"), [ join(packageRoot, "server", "worker.mjs"), "--dada-control-pipe", pipeName, "--dada-credential-stdin", ], { cwd: packageRoot, env: { ...process.env, DADA_INSTANCE_CONFIG_PATH: configPath, DADA_SQLITE_NATIVE_BINDING: join(packageRoot, "server", "native", "better_sqlite3.node"), }, stdio: ["pipe", "ignore", "ignore"], windowsHide: true, }); child.stdin.end(JSON.stringify({ "Dada/P0A/worker/ai-gateway": "synthetic-runtime-token" })); try { await new Promise((resolveReady, rejectReady) => { let settled = false; const finish = (error) => { if (settled) return; settled = true; clearTimeout(deadline); child.off("exit", onExit); if (error) rejectReady(error); else resolveReady(); }; const deadline = setTimeout(() => finish(new Error("packaged worker ready timeout")), 15_000); const onExit = (code) => finish(new Error(`packaged worker exited before ready: ${code}`)); child.once("exit", onExit); server.once("connection", (connection) => { connection.setEncoding("utf8"); let pending = ""; connection.on("data", (chunk) => { pending += chunk; while (pending.includes("\n")) { const newline = pending.indexOf("\n"); const status = pending.slice(0, newline).trim(); pending = pending.slice(newline + 1); if (status === "storage_unavailable") finish(new Error("packaged worker reported storage_unavailable")); if (status === "ready") { setTimeout(() => { if (settled) return; connection.write("shutdown\n"); finish(); }, 500); } } }); }); }); const exitCode = await new Promise((resolveExit) => child.once("exit", resolveExit)); assert.equal(exitCode, 0); } finally { if (child.exitCode === null) child.kill(); await new Promise((resolveClose) => server.close(resolveClose)); } } test("portable package serves the product and keeps SQLite data across API restart", async () => { assert.ok(existsSync(join(packageRoot, "Dada.exe"))); assert.ok(existsSync(join(packageRoot, "web", "index.html"))); const runtimeAssetManifest = JSON.parse(await readFile(join(packageRoot, "asset-metadata", "manifest.json"), "utf8")); assert.deepEqual(runtimeAssetManifest.counts, { dynamic_fonts: 7, dynamic_images: 8, font_panel_items: 11, static_stickers: 1407, }); assert.equal(runtimeAssetManifest.entries.length, 1433); const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8"); const packagedOneApiAdapter = await readFile(join(packageRoot, "server", "worker", "dist", "oneapi-generation-adapter.js"), "utf8"); assert.match(packagedWorker, /GenerationProcessor/); assert.match(packagedWorker, /OneApiGenerationAdapter/); assert.match(packagedOneApiAdapter, /oneapi\.intelligrow\.cn/); assert.doesNotMatch(packagedWorker, /portable-mock-worker/); const root = await mkdtemp(join(tmpdir(), "dada-postv1-")); const dataRoot = join(root, "data"); const configPath = join(root, "instance.json"); await writeFile(configPath, JSON.stringify({ data_root: dataRoot, initialized: true, instance_id: "portable-test", schema_version: 1, secure_config_revision: 1, admin_allowlist_hashes: [], admin_recovery_hashes: [] })); let api = startApi(configPath, dataRoot); try { await waitForHealth(api); const initialPage = await fetch(`http://127.0.0.1:${port}/`, { headers: { host: `127.0.0.1:${port}` } }); assert.equal(initialPage.status, 200); assert.match(await initialPage.text(), /当前浏览器无法使用 Dada/); const support = await fetch(`http://127.0.0.1:${port}/api/v1/support/check`, { method: "POST", headers: { host: `127.0.0.1:${port}`, origin: `http://127.0.0.1:${port}`, "content-type": "application/json", "sec-ch-ua": '"Google Chrome";v="150"', "sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"', "sec-ch-ua-platform": '"Windows"' }, body: JSON.stringify({ brands: [{ brand: "Google Chrome", version: "150" }], full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }], platform: "Windows" }), }); assert.equal(support.status, 200); const cookie = support.headers.get("set-cookie")?.split(";", 1)[0]; assert.ok(cookie); const page = await fetch(`http://127.0.0.1:${port}/app`, { headers: { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' } }); assert.equal(page.status, 200); const pageHtml = await page.text(); assert.match(pageHtml, /