Files
UHD_Media_Tool/docker/camofox/verify-upgrade.mjs
T
rootandClaude Opus 5.5 ceb66f8c77 feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)
生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。

- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
  PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
  (metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)

同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
  由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距

依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 15:47:17 +08:00

132 lines
5.6 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile, writeFile } from "node:fs/promises";
// Use only an isolated candidate instance: this creates a synthetic browser profile.
const base = process.env.BROWSER_VERIFY_URL ?? "http://127.0.0.1:19377";
const key = process.env.BROWSER_VERIFY_KEY ?? "umt-isolated-upgrade-key";
const statePath = process.env.BROWSER_VERIFY_STATE ?? "/tmp/umt-browser-upgrade-state.json";
const mode = process.argv[2] ?? "before";
const userId = "umt-upgrade-isolated";
async function call(path, body, auth = true, method = body === undefined ? "GET" : "POST") {
const response = await fetch(base + path, {
method,
headers: {
...(auth ? { authorization: `Bearer ${key}` } : {}),
...(body === undefined ? {} : { "content-type": "application/json" }),
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
signal: AbortSignal.timeout(60_000),
});
const data = await response.json();
return { status: response.status, data };
}
assert.equal((await call("/health", undefined, false)).status, 200);
assert.equal((await call("/umt/instance", undefined, false)).status, 401);
assert.equal((await call("/tabs", { userId, sessionKey: "upgrade" }, false)).status, 401);
const instance = await call("/umt/instance");
assert.equal(instance.status, 200);
const identity = await call("/umt/identity");
assert.equal(identity.status, 200);
const cookiesPath = `/umt/sessions/${userId}/cookies`;
if (mode === "after") {
const before = JSON.parse(await readFile(statePath, "utf8"));
assert.notEqual(instance.data.instanceId, before.instanceId);
assert.deepEqual(identity.data, before.identity);
const persisted = await call(`${cookiesPath}?urls=https%3A%2F%2Fexample.com%2F`);
assert.equal(persisted.status, 200);
assert.equal(persisted.data.source, "persisted");
assert.ok(persisted.data.cookies.some((c) => c.name === "umt_upgrade" && c.value === "candidate-only"));
} else {
const imported = await call(cookiesPath, {
cookies: [
{
name: "umt_upgrade",
value: "candidate-only",
domain: "example.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "Lax",
expires: Math.floor(Date.now() / 1000) + 3600,
},
],
});
assert.equal(imported.status, 200);
assert.equal(imported.data.count, 1);
await writeFile(statePath, JSON.stringify({ instanceId: instance.data.instanceId, identity: identity.data }), {
mode: 0o600,
});
}
const tab = await call("/tabs", { userId, sessionKey: "upgrade" });
assert.equal(tab.status, 200, JSON.stringify(tab.data));
assert.ok(tab.data.tabId);
const tabPath = `/tabs/${tab.data.tabId}`;
const evaluated = await call(`${tabPath}/evaluate`, {
userId,
expression: `(() => { document.body.innerHTML = '<button id="probe">升级验证</button>'; window.probeClicked = false; document.querySelector('#probe').addEventListener('click', () => { window.probeClicked = true; }); return { title: document.title, userAgent: navigator.userAgent }; })()`,
});
assert.equal(evaluated.status, 200, JSON.stringify(evaluated.data));
assert.match(evaluated.data.result.userAgent, /Firefox\/152/);
const snapshot = await call(`${tabPath}/snapshot?userId=${userId}`);
assert.equal(snapshot.status, 200, JSON.stringify(snapshot.data));
assert.match(JSON.stringify(snapshot.data), /升级验证/);
const screenshot = await fetch(`${base}${tabPath}/screenshot?userId=${userId}`, {
headers: { authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(60_000),
});
assert.equal(screenshot.status, 200);
assert.match(screenshot.headers.get("content-type"), /image\/png/);
assert.deepEqual(
new Uint8Array(await screenshot.arrayBuffer()).slice(0, 8),
new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
);
const clicked = await call(`${tabPath}/click`, { userId, selector: "#probe" });
assert.equal(clicked.status, 200, JSON.stringify(clicked.data));
const outcome = await call(`${tabPath}/evaluate`, { userId, expression: "window.probeClicked" });
assert.equal(outcome.data.result, true);
const mouseProbe = await call(`${tabPath}/evaluate`, {
userId,
expression:
"(() => { window.probeTrusted = false; const b = document.querySelector('#probe'); b.addEventListener('click', e => { window.probeTrusted = e.isTrusted; }); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()",
});
assert.equal(mouseProbe.status, 200);
const mouse = await call(`/umt/tabs/${tab.data.tabId}/mouse-click`, { userId, ...mouseProbe.data.result });
assert.equal(mouse.status, 200, JSON.stringify(mouse.data));
const trusted = await call(`${tabPath}/evaluate`, { userId, expression: "window.probeTrusted" });
assert.equal(trusted.data.result, true);
const wrongProfile = await call(`${tabPath}/evaluate`, {
userId: "umt-upgrade-other-isolated",
expression: "document.title",
});
assert.notEqual(wrongProfile.status, 200);
const cookies = await call(`${cookiesPath}?urls=https%3A%2F%2Fexample.com%2F`);
assert.ok(cookies.data.cookies.some((c) => c.name === "umt_upgrade" && c.value === "candidate-only"));
await call(`${tabPath}?userId=${userId}`, undefined, true, "DELETE");
console.log(
JSON.stringify(
{
mode,
ok: true,
instanceId: instance.data.instanceId,
identity: identity.data,
checks: [
"health",
"authorization",
"fixed-identity",
"cookies",
"new-context",
"new-page",
"evaluate",
"snapshot",
"screenshot",
"click",
"trusted-mouse-click",
"profile-isolation",
...(mode === "after" ? ["restart-persistence"] : []),
],
},
null,
2,
),
);