生产 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]>
132 lines
5.6 KiB
JavaScript
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,
|
|
),
|
|
);
|