Files
dsh_shenxian/scripts/verify-platform-admin-section.mjs
T
admin f25723b743 feat(R2)+refactor: 平台管理分区 + 删 3 桩页(档案80 #7)+ 无浏览器验收脚本
- R2:business-plugins 0.2.9 新增原生「平台管理」只读分区(已铺发到 admin/guest profile 并生效)
- 档案80 #7:删 web/{desktop,plugins,skills}.html(9→6 页)+ 5 处引用改直达 portal(nginx 已加 301)
- 新增 scripts/verify-platform-admin-section.mjs(打桩 react/jsx-runtime 真执行 client.js 做渲染断言)并接入 npm run verify
2026-09-13 19:27:30 +08:00

116 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* verify-platform-admin-section.mjs —— R2「平台管理」分区的**无浏览器**渲染验收(档案 82)
*
* 为什么需要:本机 `agent-browser` 的 daemon 起不来(2026-09-13),浏览器截图验收受阻;
* 而 `06-工作台UI规范 §7.3` 三段式在 bundle **按需动态加载** 时也拿不到带 rev 的完整 URL。
* 做法:打桩 `react` / `react/jsx-runtime` + 最小 hooks 循环 + **真执行** client.js,
* 断言分区**被注册**、**admin 渲染出 6 张卡**、**非 admin 被门禁挡住**。
* 用法:node scripts/verify-platform-admin-section.mjs 退出码 0=全绿 / 1=有失败
*/
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import vm from "node:vm";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const src = readFileSync(join(ROOT, "poc/business-plugins/lib/client.js"), "utf8");
let failed = 0;
const ok = (name, cond, extra = "") => { console.log((cond ? " ✓ " : " ✗ ") + name + (extra ? " " + extra : "")); if (!cond) failed++; };
let slots = [], cursor = 0, dirty = false;
const React = {
useState(init) { const i = cursor++; if (!(i in slots)) slots[i] = init; return [slots[i], (v) => { slots[i] = typeof v === "function" ? v(slots[i]) : v; dirty = true; }]; },
useEffect(fn) { const i = cursor++; if (!slots[i]) { slots[i] = 1; fn(); } },
};
const jsx = (type, props) => ({ type, props: props || {} });
const jsxRuntime = { jsx, jsxs: jsx, Fragment: "Fragment" };
let def = null;
const win = {
__ModuleLoader__: { load: (d) => { def = d; } },
location: { hostname: "admin.alotbuy.com", protocol: "https:", origin: "https://admin.alotbuy.com" },
open: (u) => { globalThis.__OPENED = u; },
document: { createElement: () => ({ setAttribute() {}, remove() {}, textContent: "" }), head: { appendChild() {} } },
};
let ROLE = "admin";
const DATA = {
"/api/dsh/status": { running: true, instance: { port: 43095, restarts: 2 }, breaker: null },
"/api/admin/users": { users: [{ role: "admin" }, { role: "active" }, { role: "pending" }, { role: "disabled" }] },
"/api/admin/storage": { generatedAt: 1, users: [{}, {}] },
"/api/plugins/business": { plugins: [{}, {}, {}] },
"/api/admin/runtime": { items: [{}, {}, {}, {}, {}] },
};
const sandbox = {
console, setTimeout, clearTimeout,
JSON, Object, Promise, Buffer, URL, globalThis: undefined,
window: win, document: win.document,
fetch: async (url) => {
const p = String(url).replace("https://alotbuy.com", "");
const body = p === "/api/auth/me" ? { user: { id: "u1", username: ROLE, role: ROLE } } : DATA[p];
return { ok: body !== undefined, status: body === undefined ? 404 : 200, json: async () => body };
},
require: (m) => (m === "react" ? React : m === "react/jsx-runtime" ? jsxRuntime : (() => { throw new Error("require " + m); })()),
};
sandbox.globalThis = sandbox;
vm.createContext(sandbox);
vm.runInContext(src, sandbox);
const mod = def.factory(sandbox.require);
let DICT = {}, regs = [];
const ctx = {
effect: (fn, name) => { try { fn(); } catch (e) { console.log(" effect 抛错:", name, e.message); } },
locale: { register: (ns, d) => { DICT = Object.assign({}, dICT_zh(d)); }, bind: () => (k) => DICT[k] ?? k },
slots: { inject: (n, fn) => fn(), register: (meta, Comp) => { regs.push({ meta, Comp }); return () => {}; } },
};
function dICT_zh(d) { return d.zh || {}; }
const inj = Array.isArray(mod.inject) ? mod.inject : [];
console.log(" inject 声明:", JSON.stringify(inj));
mod.apply(ctx);
console.log(" 已注册 section:", regs.map(r => r.meta.id + " / order=" + r.meta.order + " / label=" + r.meta.label()).join(" | "));
function text(n, out = [], depth = 0) {
if (n == null || depth > 12) return out;
if (typeof n === "string" || typeof n === "number") { out.push(String(n)); return out; }
if (Array.isArray(n)) { n.forEach(x => text(x, out, depth + 1)); return out; }
if (typeof n === "object") {
if (typeof n.type === "function") { text(n.type(n.props || {}), out, depth + 1); return out; }
if (n.props) text(n.props.children, out, depth + 1);
}
return out;
}
async function render(Comp) {
cursor = 0; slots = [];
let tree = Comp();
for (let i = 0; i < 6; i++) { await new Promise(r => setTimeout(r, 30)); if (!dirty) break; dirty = false; cursor = 0; tree = Comp(); }
return tree;
}
for (const { meta, Comp } of regs.filter(r => r.meta.id === "platform-admin")) {
for (const role of ["admin", "active"]) {
ROLE = role;
delete globalThis.__OPENED;
const tree = await render(Comp);
const t = text(tree).join(" | ");
console.log("\n ── role=" + role + " ──");
console.log(" 渲染文本:", t.slice(0, 300));
console.log(" 含「平台管理」:", t.includes("平台管理"), "| 含 6 张卡:", ["当前实例","熔断","用户","存储","候选池","运行时"].every(k => t.includes(k)));
if (role === "active") console.log(" 含仅管理员说明:", t.includes("仅对管理员显示"));
}
}
ok("注册了两个 settings.section", regs.length === 2, "-> " + regs.map(r => r.meta.id).join(", "));
const pa = regs.find(r => r.meta.id === "platform-admin");
ok("存在 platform-admin 分区", !!pa);
if (pa) ok("其 order = 102", pa.meta.order === 102);
ROLE = "admin"; delete globalThis.__OPENED;
const tAdmin = text(await render(pa.Comp)).join(" | ");
ok("admin:含分区标题「平台管理」", tAdmin.includes("平台管理"));
ok("admin:6 张卡齐全", ["当前实例","熔断","用户","存储","候选池","运行时"].every(k => tAdmin.includes(k)));
ok("admin:含只读声明", tAdmin.includes("只读"));
ROLE = "active";
const tUser = text(await render(pa.Comp)).join(" | ");
ok("非 admin:被门禁挡住", tUser.includes("仅对管理员显示"));
ok("非 admin:不出现任何指标卡", !["候选池","运行时"].some(k => tUser.includes(k)));
console.log(failed === 0 ? "\n结论:全绿 ✅" : "\n结论:有 " + failed + " 项失败 ❌");
process.exit(failed === 0 ? 0 : 1);