chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
21
tools/web/.probe-dom.mjs
Normal file
21
tools/web/.probe-dom.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const dump = await page.evaluate(() => ({
|
||||
ariaLabels: [...document.querySelectorAll("[aria-label]")].map((n) => n.getAttribute("aria-label")),
|
||||
buttons: [...document.querySelectorAll("button")].map((n) => (n.textContent || n.getAttribute("aria-label") || "").trim()).filter(Boolean).slice(0, 80),
|
||||
dataAttrs: [...new Set([...document.querySelectorAll("*")].flatMap((n) => [...n.attributes].map((a) => a.name).filter((x) => x.startsWith("data-"))))],
|
||||
workspaces: [...document.querySelectorAll("nav[aria-label]")].map((nav) => ({ nav: nav.getAttribute("aria-label"), tabs: [...nav.querySelectorAll("button")].map((b) => (b.textContent || "").trim()) })),
|
||||
mainAttrs: [...document.querySelectorAll("main")].map((m) => [...m.attributes].map((a) => `${a.name}=${a.value}`)),
|
||||
}));
|
||||
console.log(JSON.stringify(dump, null, 1).slice(0, 6000));
|
||||
await browser.close();
|
||||
27
tools/web/.probe10.mjs
Normal file
27
tools/web/.probe10.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({ rev: document.querySelector("main")?.getAttribute("data-current-main-revision"), stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent, diag: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,120) }));
|
||||
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
|
||||
await click("添加立方体"); await page.waitForTimeout(9000);
|
||||
console.log("ADD", JSON.stringify(await st()));
|
||||
await click("撤销"); await page.waitForTimeout(9000);
|
||||
console.log("UNDO1", JSON.stringify(await st()));
|
||||
await click("撤销"); await page.waitForTimeout(9000);
|
||||
console.log("UNDO2", JSON.stringify(await st()));
|
||||
// open undo history dialog
|
||||
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
|
||||
await page.waitForTimeout(1500);
|
||||
await page.evaluate(() => [...document.querySelectorAll("button")].find(b => (b.textContent||"").trim() === "撤销历史")?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
console.log("HISTORY-DIALOG", JSON.stringify(await page.evaluate(() => { const d = document.querySelector('[role="dialog"]'); return d ? [...d.querySelectorAll("li,button,tr")].map(n => (n.textContent||"").trim()).filter(Boolean).slice(0,25) : null; })));
|
||||
await browser.close();
|
||||
21
tools/web/.probe11.mjs
Normal file
21
tools/web/.probe11.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const errs = []; page.on("console", m => { if (m.type()==="error") errs.push(m.text().slice(0,300)); });
|
||||
await page.goto("http://127.0.0.1:5403/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const ws = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), diag: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }));
|
||||
for (const name of ["Shading", "UV Editing", "Layout"]) {
|
||||
await page.evaluate((v) => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === v); b?.click(); }, name);
|
||||
await page.waitForTimeout(12000);
|
||||
console.log(name, JSON.stringify(await ws()));
|
||||
}
|
||||
console.log("ERRS", JSON.stringify(errs.slice(-6)));
|
||||
await browser.close();
|
||||
30
tools/web/.probe12.mjs
Normal file
30
tools/web/.probe12.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
|
||||
const stats = () => page.evaluate(() => (document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? "?");
|
||||
const hist = async () => {
|
||||
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
|
||||
await page.waitForTimeout(1200);
|
||||
await page.evaluate(() => [...document.querySelectorAll("button")].find(b => (b.textContent||"").trim() === "撤销历史")?.click());
|
||||
await page.waitForTimeout(2500);
|
||||
const items = await page.evaluate(() => { const d = document.querySelector('[role="dialog"]'); return d ? [...d.querySelectorAll("li,button,tr")].map(n => (n.textContent||"").trim()).filter(Boolean).slice(0,30) : null; });
|
||||
await page.keyboard.press("Escape"); await page.waitForTimeout(1500);
|
||||
return items;
|
||||
};
|
||||
await click("添加立方体"); await page.waitForTimeout(9000);
|
||||
console.log("ADD objects=", await stats(), "history=", JSON.stringify(await hist()));
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
await click("撤销"); await page.waitForTimeout(9000);
|
||||
console.log("UNDO"+i+" objects=", await stats(), "history=", JSON.stringify(await hist()));
|
||||
}
|
||||
await browser.close();
|
||||
28
tools/web/.probe13.mjs
Normal file
28
tools/web/.probe13.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const logs = []; page.on("console", m => logs.push(m.type()+": "+m.text().slice(0,400)));
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const before = logs.length;
|
||||
await page.evaluate(() => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === "Shading"); b?.click(); });
|
||||
await page.waitForTimeout(12000);
|
||||
const state = await page.evaluate(() => ({
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
|
||||
statusBar: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
bodyText: (document.body.innerText||"").split("\n").filter(l => /失败|错误|error|unavailable|无法/i.test(l)).slice(0,8),
|
||||
}));
|
||||
console.log("STATE", JSON.stringify(state));
|
||||
console.log("NEWLOGS", JSON.stringify(logs.slice(before).slice(0,10), null, 1));
|
||||
// confirm user impact: can we still add a cube after the wedge?
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("AFTER-ADD", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
|
||||
await browser.close();
|
||||
30
tools/web/.probe14.mjs
Normal file
30
tools/web/.probe14.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({
|
||||
ws: document.querySelector("main")?.getAttribute("data-workspace"),
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
|
||||
inputReady: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-input-ready"),
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,120),
|
||||
}));
|
||||
await page.evaluate(() => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === "Shading"); b?.click(); });
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("SHADING", JSON.stringify(await st()));
|
||||
// click into the 3D viewport area to activate it
|
||||
const box = await page.evaluate(() => { const c = document.querySelector('canvas[aria-label="Blender desktop"]'); const r = c?.getBoundingClientRect(); return r ? {x:r.x+r.width*0.5, y:r.y+r.height*0.5} : null; });
|
||||
if (box) { await page.mouse.click(box.x, box.y); }
|
||||
for (const t of [3000, 6000, 12000]) { await page.waitForTimeout(t === 3000 ? 3000 : t === 6000 ? 3000 : 6000); console.log("AFTER-CLICK-"+t, JSON.stringify(await st())); }
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("ADD", JSON.stringify(await st()));
|
||||
await browser.close();
|
||||
22
tools/web/.probe15.mjs
Normal file
22
tools/web/.probe15.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === "Shading"); b?.click(); });
|
||||
await page.waitForTimeout(10000);
|
||||
console.log("TEXT", JSON.stringify((await page.evaluate(() => document.body.innerText)).slice(0, 1500)));
|
||||
console.log("ERROREL", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role=alert],.native-error,[class*=error]')].map(n => (n.textContent||"").trim().slice(0,300)).filter(Boolean))));
|
||||
// try Escape then retry edit
|
||||
await page.keyboard.press("Escape"); await page.waitForTimeout(3000);
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("AFTER-ESC", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display") }))));
|
||||
await browser.close();
|
||||
34
tools/web/.probe16.mjs
Normal file
34
tools/web/.probe16.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({
|
||||
ws: document.querySelector("main")?.getAttribute("data-workspace"),
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,140),
|
||||
}));
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
await go("Shading"); await page.waitForTimeout(8000);
|
||||
console.log("S1", JSON.stringify(await st()));
|
||||
// retry switching repeatedly with increasing waits: does the python flag clear?
|
||||
for (const w of [4000, 8000, 15000, 25000]) {
|
||||
await go("Layout"); await page.waitForTimeout(w);
|
||||
console.log("TRY-"+w, JSON.stringify(await st()));
|
||||
}
|
||||
// and does a same-candidate fresh reload switch away from Shading directly?
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
for (const n of ["Shading","UV Editing","Layout","Modeling"]) { await go(n); await page.waitForTimeout(9000); console.log("SEQ-"+n, JSON.stringify(await st())); }
|
||||
await browser.close();
|
||||
28
tools/web/.probe17.mjs
Normal file
28
tools/web/.probe17.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({
|
||||
ws: document.querySelector("main")?.getAttribute("data-workspace"),
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,300),
|
||||
}));
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
// A: which workspaces can be entered and left
|
||||
for (const n of ["Modeling","Scripting","Layout"]) { await go(n); await page.waitForTimeout(7000); console.log("A-"+n, JSON.stringify(await st())); }
|
||||
await go("Shading"); await page.waitForTimeout(9000);
|
||||
console.log("B-Shading", JSON.stringify(await st()));
|
||||
await go("Layout"); await page.waitForTimeout(7000);
|
||||
console.log("B-Leave", JSON.stringify(await st()));
|
||||
// C: does an explicit python exec also report the same active handle?
|
||||
console.log("C", JSON.stringify(await st()));
|
||||
await browser.close();
|
||||
28
tools/web/.probe18.mjs
Normal file
28
tools/web/.probe18.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({
|
||||
ws: document.querySelector("main")?.getAttribute("data-workspace"),
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,240),
|
||||
}));
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
// 1) enter Shading from a NON-Shading workspace (Modeling) and leave
|
||||
await go("Modeling"); await page.waitForTimeout(8000); console.log("M", JSON.stringify(await st()));
|
||||
await go("Shading"); await page.waitForTimeout(9000); console.log("M->S", JSON.stringify(await st()));
|
||||
await go("Modeling"); await page.waitForTimeout(8000); console.log("S->M", JSON.stringify(await st()));
|
||||
// 2) leave Shading a second time after failure
|
||||
await go("Shading"); await page.waitForTimeout(7000);
|
||||
await go("Layout"); await page.waitForTimeout(8000); console.log("S->L", JSON.stringify(await st()));
|
||||
await go("Scripting"); await page.waitForTimeout(8000); console.log("S->Scr", JSON.stringify(await st()));
|
||||
await browser.close();
|
||||
22
tools/web/.probe19.mjs
Normal file
22
tools/web/.probe19.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent }));
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
// Compare workspaces that also host node editors: Texture Paint, Compositing, Geometry Nodes
|
||||
for (const n of ["Texture Paint","Compositing","Geometry Nodes","Shading"]) {
|
||||
await go(n); await page.waitForTimeout(9000);
|
||||
const entered = await st();
|
||||
await go("Layout"); await page.waitForTimeout(8000);
|
||||
console.log(n, "enter=", JSON.stringify(entered), "leave=", JSON.stringify(await st()));
|
||||
}
|
||||
await browser.close();
|
||||
47
tools/web/.probe2.mjs
Normal file
47
tools/web/.probe2.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createRequire } from "node:module";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const snap = () => page.evaluate(() => {
|
||||
const m = document.querySelector("main");
|
||||
return {
|
||||
ws: m?.getAttribute("data-workspace"),
|
||||
rev: m?.getAttribute("data-current-main-revision"),
|
||||
dirty: m?.getAttribute("data-dirty"),
|
||||
area: m?.getAttribute("data-editor-workflow-area"),
|
||||
stats: document.querySelector('[data-testid="scene-stats"]')?.textContent,
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
saveStatus: m?.getAttribute("data-user-action-save-status"),
|
||||
openStatus: m?.getAttribute("data-user-action-open-status"),
|
||||
};
|
||||
});
|
||||
console.log("A-label aria:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[aria-label]")].map(n => n.getAttribute("aria-label")))));
|
||||
console.log("A-workspaces:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="工作区列表"] button')].map(b => (b.textContent||"").trim()))));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(4000);
|
||||
console.log("B-after-add:", JSON.stringify(await snap()));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(4000);
|
||||
console.log("B-after-undo:", JSON.stringify(await snap()));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="重做"]')?.click());
|
||||
await page.waitForTimeout(4000);
|
||||
console.log("B-after-redo:", JSON.stringify(await snap()));
|
||||
const dlP = page.waitForEvent("download", { timeout: 40000 });
|
||||
await page.evaluate(() => document.querySelector('[aria-label="保存项目"]')?.click());
|
||||
const dl = await dlP;
|
||||
const p = await dl.path();
|
||||
const bytes = p ? fs.readFileSync(p) : Buffer.alloc(0);
|
||||
console.log("C-download:", dl.suggestedFilename(), bytes.length, "path=", Boolean(p));
|
||||
console.log("C-after-save:", JSON.stringify(await snap()));
|
||||
await page.getByTestId("blend-file-input").setInputFiles(p);
|
||||
for (let i = 0; i < 12; i++) { await page.waitForTimeout(3000); console.log("C-reopen-"+i+":", JSON.stringify(await snap())); }
|
||||
await browser.close();
|
||||
27
tools/web/.probe20.mjs
Normal file
27
tools/web/.probe20.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const errs = []; page.on("console", m => { if (m.type()==="error" || /worker|Worker/.test(m.text())) errs.push(m.type()+": "+m.text().slice(0,300)); });
|
||||
page.on("pageerror", e => errs.push("pageerror: "+String(e).slice(0,300)));
|
||||
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const full = () => page.evaluate(() => {
|
||||
const m = document.querySelector("main");
|
||||
const attrs = {}; for (const a of m.attributes) if (/worker|recovery|engine|main-revision|diagnostic/.test(a.name)) attrs[a.name] = a.value.slice(0,120);
|
||||
return { ws: m.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, code: m.getAttribute("data-last-diagnostic-code"), detail: (m.getAttribute("data-last-diagnostic-detail")||"").slice(0,240), attrs };
|
||||
});
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
const before = errs.length;
|
||||
await go("Compositing"); await page.waitForTimeout(9000);
|
||||
console.log("ENTER", JSON.stringify(await full()));
|
||||
await go("Layout"); await page.waitForTimeout(8000);
|
||||
console.log("LEAVE", JSON.stringify(await full()));
|
||||
console.log("ERRS", JSON.stringify(errs.slice(before).slice(0,8), null, 1));
|
||||
await browser.close();
|
||||
25
tools/web/.probe21.mjs
Normal file
25
tools/web/.probe21.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5404/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }));
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
await page.evaluate(() => document.querySelector('[data-testid="native-web-tools-toggle"]')?.click());
|
||||
await page.waitForTimeout(4000);
|
||||
console.log("WEBMODE", JSON.stringify(await st()));
|
||||
await go("Shading"); await page.waitForTimeout(9000);
|
||||
console.log("WEB-Shading", JSON.stringify(await st()));
|
||||
await page.evaluate(() => document.querySelector('[data-testid="desktop-file-transfer-entry"]')?.click());
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("BACK-NATIVE", JSON.stringify(await st()));
|
||||
await go("Layout"); await page.waitForTimeout(8000);
|
||||
console.log("NATIVE-Layout", JSON.stringify(await st()));
|
||||
await browser.close();
|
||||
25
tools/web/.probe22.mjs
Normal file
25
tools/web/.probe22.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const py = []; page.on("console", m => { const t = m.text(); if (/WBV2_PY_|REENTRANT/.test(t)) py.push(t.slice(0,160)); });
|
||||
await page.goto("http://127.0.0.1:5407/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }));
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
const mark = async (label) => { await page.waitForTimeout(300); py.push("--- " + label + " ---"); };
|
||||
await go("Modeling"); await page.waitForTimeout(7000); await mark("after-entering-Modeling");
|
||||
await go("Shading"); await page.waitForTimeout(9000); await mark("after-entering-Shading");
|
||||
await go("Layout"); await page.waitForTimeout(8000); await mark("after-leaving-Shading");
|
||||
console.log("STATE", JSON.stringify(await st()));
|
||||
const enter = py.filter(l => l.startsWith("WBV2_PY_ENTER")).length;
|
||||
const leave = py.filter(l => l.startsWith("WBV2_PY_LEAVE")).length;
|
||||
console.log("COUNTS enter=", enter, "leave=", leave);
|
||||
console.log(JSON.stringify(py.slice(-30), null, 1));
|
||||
await browser.close();
|
||||
38
tools/web/.probe23.mjs
Normal file
38
tools/web/.probe23.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5406/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = (tag) => page.evaluate((t) => ({
|
||||
tag: t,
|
||||
ws: document.querySelector("main")?.getAttribute("data-workspace"),
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"),
|
||||
objects: (document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? "?",
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,220),
|
||||
}), tag);
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
|
||||
// A: does an EDIT fail inside a node workspace (no detach involved)?
|
||||
await go("Shading"); await page.waitForTimeout(9000);
|
||||
console.log(JSON.stringify(await st("shading-entered")));
|
||||
await click("添加立方体"); await page.waitForTimeout(7000);
|
||||
console.log(JSON.stringify(await st("edit-inside-shading")));
|
||||
await click("撤销"); await page.waitForTimeout(7000);
|
||||
console.log(JSON.stringify(await st("undo-inside-shading")));
|
||||
// B: does a fresh load still edit fine? (control)
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await click("添加立方体"); await page.waitForTimeout(7000);
|
||||
console.log(JSON.stringify(await st("control-layout-edit")));
|
||||
await browser.close();
|
||||
25
tools/web/.probe24.mjs
Normal file
25
tools/web/.probe24.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5406/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
// Clean baseline: does entering Shading from Layout itself already set the wedge?
|
||||
console.log("BEFORE", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count"), code: document.querySelector("main")?.getAttribute("data-last-diagnostic-code") }))));
|
||||
await go("Shading"); await page.waitForTimeout(9000);
|
||||
console.log("AFTER-ENTER", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count"), code: document.querySelector("main")?.getAttribute("data-last-diagnostic-code"), detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
|
||||
// does the wedge clear if we wait and interact with the viewport first?
|
||||
const box = await page.evaluate(() => { const c = document.querySelector('canvas[aria-label="Blender desktop"]'); const r = c?.getBoundingClientRect(); return r ? { x: r.x + r.width * 0.5, y: r.y + r.height * 0.5 } : null; });
|
||||
if (box) await page.mouse.click(box.x, box.y);
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("AFTER-CLICK", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count") }))));
|
||||
await go("Layout"); await page.waitForTimeout(8000);
|
||||
console.log("AFTER-LEAVE", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
|
||||
await browser.close();
|
||||
22
tools/web/.probe25.mjs
Normal file
22
tools/web/.probe25.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5406/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), count: document.querySelector("main")?.getAttribute("data-diagnostic-count"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }), t);
|
||||
console.log(JSON.stringify(await st("baseline-layout")));
|
||||
// detach without ever entering Shading
|
||||
await page.evaluate(() => document.querySelector('[data-testid="native-web-tools-toggle"]')?.click());
|
||||
await page.waitForTimeout(6000);
|
||||
console.log(JSON.stringify(await st("after-toggle-web-tools")));
|
||||
await page.evaluate(() => document.querySelector('[data-testid="desktop-file-transfer-entry"]')?.click());
|
||||
await page.waitForTimeout(8000);
|
||||
console.log(JSON.stringify(await st("after-reattach")));
|
||||
await browser.close();
|
||||
23
tools/web/.probe26.mjs
Normal file
23
tools/web/.probe26.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const t0 = Date.now(); const log = [];
|
||||
page.on("console", m => { const t = m.text(); if (/PYTHON_|REENTRANT|sRGB|storage buffer/.test(t)) log.push(((Date.now()-t0)/1000).toFixed(1)+"s "+m.type()+": "+t.slice(0,200)); });
|
||||
await page.goto("http://127.0.0.1:5407/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
log.push("=== switch to Shading ===");
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Shading"); b?.click(); });
|
||||
await page.waitForTimeout(12000);
|
||||
log.push("=== switch to Layout ===");
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Layout"); b?.click(); });
|
||||
await page.waitForTimeout(9000);
|
||||
console.log(JSON.stringify(log.slice(0, 24), null, 1));
|
||||
console.log("STATE", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }))));
|
||||
await browser.close();
|
||||
34
tools/web/.probe27.mjs
Normal file
34
tools/web/.probe27.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const marks = []; page.on("console", m => { const t = m.text(); if (/WBV2_PY_RECLAIM|REENTRANT/.test(t)) marks.push(m.type()+": "+t.slice(0,220)); });
|
||||
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), objects: (document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? "?", engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,220) }), t);
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
|
||||
// A: baseline edit
|
||||
await click("添加立方体"); await page.waitForTimeout(7000);
|
||||
console.log(JSON.stringify(await st("baseline-add")));
|
||||
// B: enter Shading, edit inside
|
||||
await go("Shading"); await page.waitForTimeout(9000);
|
||||
console.log(JSON.stringify(await st("shading-entered")));
|
||||
// C: leave Shading — the old wedge point
|
||||
await go("Layout"); await page.waitForTimeout(9000);
|
||||
console.log(JSON.stringify(await st("left-shading")));
|
||||
// D: edit after leaving
|
||||
await click("添加立方体"); await page.waitForTimeout(7000);
|
||||
console.log(JSON.stringify(await st("add-after-leave")));
|
||||
// E: back into Shading and out again
|
||||
await go("Shading"); await page.waitForTimeout(8000);
|
||||
await go("Modeling"); await page.waitForTimeout(8000);
|
||||
console.log(JSON.stringify(await st("second-round")));
|
||||
console.log("RECLAIM-LOGS", JSON.stringify(marks.slice(0, 8), null, 1));
|
||||
await browser.close();
|
||||
31
tools/web/.probe28.mjs
Normal file
31
tools/web/.probe28.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
// console python must still execute after the reclaim change
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
|
||||
await page.waitForFunction(() => document.querySelector('[data-workspace="Scripting"]') !== null, undefined, { timeout: 40000 }).catch(() => {});
|
||||
await page.waitForTimeout(6000);
|
||||
const console_ = await page.evaluate(() => document.querySelector('[aria-label="Python Console"] textarea, .wasm-python-console textarea, textarea[aria-label*="Console"]') !== null);
|
||||
if (console_) {
|
||||
const box = await page.locator('[aria-label="Python Console"] textarea, .wasm-python-console textarea, textarea[aria-label*="Console"]').first();
|
||||
await box.click();
|
||||
await box.fill("print(1 + 1)");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForTimeout(6000);
|
||||
}
|
||||
console.log("PYTHON", JSON.stringify(await page.evaluate(() => ({
|
||||
consoleMounted: Boolean(document.querySelector('[aria-label="Python Console"]')),
|
||||
echoed: (document.body.innerText.match(/\n2\b/) || [])[0] ?? null,
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
|
||||
}))));
|
||||
await browser.close();
|
||||
34
tools/web/.probe29.mjs
Normal file
34
tools/web/.probe29.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
|
||||
await page.waitForFunction(() => document.querySelector("main")?.getAttribute("data-workspace") === "Scripting", undefined, { timeout: 60000 });
|
||||
await page.waitForFunction(() => document.querySelector(".wasm-python-console") !== null, undefined, { timeout: 60000 });
|
||||
const input = await page.evaluate(() => {
|
||||
const host = document.querySelector(".wasm-python-console");
|
||||
const el = host?.querySelector("textarea, input[type=text]");
|
||||
return el ? { tag: el.tagName, aria: el.getAttribute("aria-label"), placeholder: el.getAttribute("placeholder") } : null;
|
||||
});
|
||||
console.log("INPUT", JSON.stringify(input));
|
||||
if (input) {
|
||||
const loc = page.locator(".wasm-python-console textarea, .wasm-python-console input[type=text]").first();
|
||||
await loc.click();
|
||||
await loc.fill("print(1 + 1)");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForTimeout(7000);
|
||||
}
|
||||
console.log("RESULT", JSON.stringify(await page.evaluate(() => ({
|
||||
text: (document.querySelector(".wasm-python-console")?.innerText ?? "").slice(-300),
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
|
||||
}))));
|
||||
await browser.close();
|
||||
29
tools/web/.probe3.mjs
Normal file
29
tools/web/.probe3.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createRequire } from "node:module";
|
||||
import fs from "node:fs"; import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
console.log("NAVS:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("nav")].map(n => ({ label: n.getAttribute("aria-label"), cls: n.className, buttons: [...n.querySelectorAll("button")].map(b => (b.textContent||"").trim()).slice(0,20) })))));
|
||||
console.log("AREAS:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[data-editor-workflow-area],[data-area],[data-workflow-area]")].map(n => ({ tag: n.tagName, id: n.getAttribute("data-editor-workflow-area") ?? n.getAttribute("data-area") ?? n.getAttribute("data-workflow-area"), cls: n.className?.toString().slice(0,60) })))));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
console.log("MENU:", JSON.stringify(await page.evaluate(() => { const m = document.querySelector('[data-testid="scene-stats"]'); return { stats: m?.textContent, undoDisabled: document.querySelector('[aria-label="撤销"]')?.disabled ?? null, redoDisabled: document.querySelector('[aria-label="重做"]')?.disabled ?? null }; })));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(4000);
|
||||
console.log("AFTERUNDO:", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
|
||||
const dlP = page.waitForEvent("download", { timeout: 40000 });
|
||||
await page.evaluate(() => document.querySelector('[aria-label="保存项目"]')?.click());
|
||||
const dl = await dlP; const p = await dl.path();
|
||||
const dir = fs.mkdtempSync("/tmp/wbv2-reopen-"); const named = path.join(dir, "saved.blend");
|
||||
fs.copyFileSync(p, named);
|
||||
console.log("SAVED:", dl.suggestedFilename(), fs.statSync(named).size);
|
||||
await page.getByTestId("blend-file-input").setInputFiles(named);
|
||||
for (let i = 0; i < 6; i++) { await page.waitForTimeout(3000); console.log("REOPEN"+i+":", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent })))); }
|
||||
await browser.close();
|
||||
18
tools/web/.probe30.mjs
Normal file
18
tools/web/.probe30.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
|
||||
await page.waitForTimeout(12000);
|
||||
console.log("WS", JSON.stringify(await page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,180) }))));
|
||||
console.log("AREAS", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[aria-label]")].map(n=>n.getAttribute("aria-label")).filter(l=>/Console|Python|Editor|节点/.test(l)).slice(0,15))));
|
||||
console.log("CLASSES", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[class*=console],[class*=python]")].map(n=>n.className?.toString().slice(0,60)).slice(0,10))));
|
||||
await browser.close();
|
||||
27
tools/web/.probe31.mjs
Normal file
27
tools/web/.probe31.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,220) }), t);
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); return Boolean(b); }, n);
|
||||
// UV Editing reached directly from Layout (no Shading involved)
|
||||
console.log("uv-from-layout-click", await go("UV Editing"));
|
||||
await page.waitForTimeout(10000);
|
||||
console.log(JSON.stringify(await st("uv-from-layout")));
|
||||
// Shading -> UV Editing
|
||||
await go("Shading"); await page.waitForTimeout(8000);
|
||||
console.log(JSON.stringify(await st("shading")));
|
||||
console.log("uv-from-shading-click", await go("UV Editing"));
|
||||
await page.waitForTimeout(10000);
|
||||
console.log(JSON.stringify(await st("uv-from-shading")));
|
||||
// what does the workspace tab list contain for UV Editing?
|
||||
console.log("TABS", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="工作区"] button')].map(b => ({ n: (b.textContent||"").trim(), disabled: b.disabled, cls: b.className.slice(0,40) })))));
|
||||
await browser.close();
|
||||
24
tools/web/.probe32.mjs
Normal file
24
tools/web/.probe32.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const goBack = () => page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Layout"); b?.click(); });
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
const st = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent }));
|
||||
const results = [];
|
||||
for (const name of ["Animation","Compositing","Geometry Nodes","Rendering","Sculpting","Texture Paint","UV Editing","Scripting"]) {
|
||||
await goBack(); await page.waitForTimeout(6000);
|
||||
await go(name); await page.waitForTimeout(11000);
|
||||
const s = await st();
|
||||
results.push({ name, ws: s.ws, engine: (s.engine||"").slice(0,90) });
|
||||
}
|
||||
console.log(JSON.stringify(results, null, 1));
|
||||
await browser.close();
|
||||
21
tools/web/.probe33.mjs
Normal file
21
tools/web/.probe33.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5408/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const go = (n) => page.evaluate((v) => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()===v); b?.click(); }, n);
|
||||
const st = (t) => page.evaluate((tag) => ({ tag, ws: document.querySelector("main")?.getAttribute("data-workspace"), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display"), engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,180) }), t);
|
||||
// Geometry Nodes alone from Layout
|
||||
await go("Geometry Nodes"); await page.waitForTimeout(12000);
|
||||
console.log(JSON.stringify(await st("geometry-nodes-alone")));
|
||||
// then try leaving
|
||||
await go("Layout"); await page.waitForTimeout(9000);
|
||||
console.log(JSON.stringify(await st("leave-geometry-nodes")));
|
||||
await browser.close();
|
||||
19
tools/web/.probe34.mjs
Normal file
19
tools/web/.probe34.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const log = []; page.on("console", m => { if (m.type()==="error" || /Traceback|Error|error/.test(m.text())) log.push(m.text().slice(0,600)); });
|
||||
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
log.length = 0;
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="UV Editing"); b?.click(); });
|
||||
await page.waitForTimeout(14000);
|
||||
console.log("DETAIL", JSON.stringify(await page.evaluate(() => (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,260))));
|
||||
console.log("LOG", JSON.stringify(log.slice(0, 14), null, 1));
|
||||
await browser.close();
|
||||
22
tools/web/.probe35.mjs
Normal file
22
tools/web/.probe35.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
|
||||
await page.waitForTimeout(15000);
|
||||
console.log(JSON.stringify(await page.evaluate(() => ({
|
||||
ws: document.querySelector("main")?.getAttribute("data-workspace"),
|
||||
area: document.querySelector("main")?.getAttribute("data-editor-workflow-area"),
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200),
|
||||
textareas: [...document.querySelectorAll("textarea")].map(t => ({ aria: t.getAttribute("aria-label"), cls: t.className.slice(0,50), placeholder: t.getAttribute("placeholder") })),
|
||||
consoleish: [...document.querySelectorAll("[class*=console],[data-testid*=console],[aria-label*=Console],[aria-label*=控制台]")].map(n => ({ tag: n.tagName, aria: n.getAttribute("aria-label"), cls: (n.className||"").toString().slice(0,60) })),
|
||||
})), null, 1));
|
||||
await browser.close();
|
||||
35
tools/web/.probe36.mjs
Normal file
35
tools/web/.probe36.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
// Switch to Web business tools so the DOM console mounts, then Scripting.
|
||||
await page.evaluate(() => document.querySelector('[data-testid="native-web-tools-toggle"]')?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Scripting"); b?.click(); });
|
||||
await page.waitForTimeout(6000);
|
||||
const input = await page.evaluate(() => {
|
||||
const el = document.querySelector('.wasm-python-console textarea, .wasm-python-console input, [aria-label="Python Console command"]');
|
||||
return el ? { tag: el.tagName, aria: el.getAttribute('aria-label'), readOnly: el.getAttribute('readonly') !== null } : null;
|
||||
});
|
||||
console.log("INPUT", JSON.stringify(input));
|
||||
if (input && !input.readOnly) {
|
||||
const loc = page.locator('.wasm-python-console textarea, .wasm-python-console input, [aria-label="Python Console command"]').first();
|
||||
await loc.click();
|
||||
await loc.fill("print(1 + 1)");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForTimeout(7000);
|
||||
}
|
||||
console.log("RESULT", JSON.stringify(await page.evaluate(() => ({
|
||||
consoleText: (document.querySelector('.wasm-python-console')?.innerText ?? '').slice(-240),
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
|
||||
}))));
|
||||
await browser.close();
|
||||
19
tools/web/.probe37.mjs
Normal file
19
tools/web/.probe37.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const log = []; page.on("console", m => { const t=m.text(); if (/shader|cannot convert|non-constant|unsupported/.test(t)) log.push(t.slice(0,400)); });
|
||||
await page.goto("http://127.0.0.1:5411/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
// Force compilation of material and world shaders the same way the receipt does.
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Rendering"); b?.click(); });
|
||||
await page.waitForTimeout(15000);
|
||||
await page.evaluate(() => { const b=[...document.querySelectorAll('nav[aria-label="工作区"] button')].find(x=>(x.textContent||"").trim()==="Shading"); b?.click(); });
|
||||
await page.waitForTimeout(15000);
|
||||
console.log(JSON.stringify(log.slice(0, 20), null, 1));
|
||||
await browser.close();
|
||||
29
tools/web/.probe4.mjs
Normal file
29
tools/web/.probe4.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
const logs = []; page.on("console", m => logs.push(`${m.type()}: ${m.text()}`.slice(0, 400)));
|
||||
page.on("pageerror", e => logs.push("pageerror: " + String(e).slice(0, 300)));
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, diag: document.querySelector("main")?.getAttribute("data-diagnostic-count"), rev: document.querySelector("main")?.getAttribute("data-current-main-revision"), stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, workspace: document.querySelector("main")?.getAttribute("data-workspace"), area: document.querySelector("main")?.getAttribute("data-editor-workflow-area") }));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
console.log("ADD:", JSON.stringify(await st()));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("UNDO:", JSON.stringify(await st()));
|
||||
console.log("BANNER:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[role=alert],[data-testid*=error],[class*=error],[class*=diagnostic]")].map(n => (n.textContent||"").trim().slice(0,200)).filter(Boolean).slice(0,8))));
|
||||
console.log("LOGS:", JSON.stringify(logs.slice(-14), null, 1));
|
||||
for (const name of ["Modeling", "Scripting", "Layout"]) {
|
||||
await page.evaluate((v) => { const b = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find(n => (n.textContent||"").trim() === v); b?.click(); }, name);
|
||||
await page.waitForTimeout(4000);
|
||||
console.log("WS-"+name+":", JSON.stringify(await st()), "areas:", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll("[data-editor-workflow-area]")].map(n => n.getAttribute("data-editor-workflow-area")))));
|
||||
}
|
||||
await browser.close();
|
||||
15
tools/web/.probe40.mjs
Normal file
15
tools/web/.probe40.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
console.log("TREE", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({ name: n.getAttribute('aria-label') ?? (n.textContent||'').trim().slice(0,30), level: n.getAttribute('aria-level'), parent: n.getAttribute('data-parent-id'), expanded: n.getAttribute('aria-expanded') })).slice(0, 20))));
|
||||
console.log("STATS", JSON.stringify(await page.evaluate(() => ({ stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
|
||||
await browser.close();
|
||||
34
tools/web/.probe41.mjs
Normal file
34
tools/web/.probe41.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
// A: is the Decimate panel present and enabled?
|
||||
const panel = await page.evaluate(() => {
|
||||
const h = [...document.querySelectorAll('.property-section h3')].find(n => (n.textContent||'').trim() === 'Decimate');
|
||||
const section = h?.closest('.property-section');
|
||||
if (!section) return { found: false };
|
||||
const btn = (label) => { const b = section.querySelector(`[aria-label="${label}"]`); return b ? { disabled: b.disabled } : null; };
|
||||
return { found: true, summary: section.querySelector('.modifier-summary span')?.textContent?.trim(), preview: btn('预览 Decimate'), apply: btn('应用 Decimate') };
|
||||
});
|
||||
console.log("PANEL", JSON.stringify(panel));
|
||||
// B: preview and read counts
|
||||
if (panel.found && panel.preview && !panel.preview.disabled) {
|
||||
await page.getByRole('button', { name: '预览 Decimate', exact: true }).click();
|
||||
try {
|
||||
await page.waitForFunction(() => /三角形/.test(document.querySelector('[data-testid="simplify-triangle-counts"]')?.textContent ?? ''), undefined, { timeout: 90000 });
|
||||
} catch (e) { console.log("PREVIEW-TIMEOUT"); }
|
||||
console.log("COUNTS", JSON.stringify(await page.evaluate(() => ({
|
||||
counts: document.querySelector('[data-testid="simplify-triangle-counts"]')?.textContent,
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160),
|
||||
}))));
|
||||
}
|
||||
await browser.close();
|
||||
32
tools/web/.probe42.mjs
Normal file
32
tools/web/.probe42.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({ name: n.getAttribute('aria-label') ?? (n.textContent||'').trim().slice(0,26), level: n.getAttribute('aria-level'), parent: n.getAttribute('data-parent-id') })));
|
||||
console.log("BEFORE", JSON.stringify(await tree()));
|
||||
// select Payload, then ctrl-select NewParent via the tree
|
||||
const clickTree = async (name, mods = []) => {
|
||||
const el = page.getByRole('treeitem', { name, exact: true }).first().locator('.tree-node-name').first();
|
||||
await el.click({ modifiers: mods });
|
||||
await page.waitForTimeout(2500);
|
||||
};
|
||||
await clickTree('Payload');
|
||||
await clickTree('NewParent', ['Control']);
|
||||
console.log("SELECTED", JSON.stringify(await page.evaluate(() => document.querySelector('main')?.getAttribute('data-selected-object-ids'))));
|
||||
await page.getByRole('button', { name: '物体', exact: true }).click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.getByRole('menuitem', { name: '父级', exact: true }).click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.getByRole('menuitem', { name: '设定父级', exact: true }).click();
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("AFTER", JSON.stringify(await tree()));
|
||||
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
|
||||
await browser.close();
|
||||
17
tools/web/.probe43.mjs
Normal file
17
tools/web/.probe43.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
console.log(JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({
|
||||
aria: n.getAttribute('aria-label'), text: (n.textContent||'').trim().slice(0,28), level: n.getAttribute('aria-level'),
|
||||
nodeId: n.getAttribute('data-node-id'), parent: n.getAttribute('data-parent-id'), dataId: n.getAttribute('data-data-id')
|
||||
}))), null, 1));
|
||||
await browser.close();
|
||||
16
tools/web/.probe44.mjs
Normal file
16
tools/web/.probe44.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
const names = await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => n.getAttribute('aria-label')));
|
||||
console.log("COUNT", names.length, JSON.stringify(names));
|
||||
console.log("HAS-PAYLOAD", names.includes("Payload"));
|
||||
await browser.close();
|
||||
26
tools/web/.probe45.mjs
Normal file
26
tools/web/.probe45.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
// Read the authoritative snapshot directly from the worker.
|
||||
const snap = await page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__nativeWorkflowEngine;
|
||||
if (!worker) return reject(new Error("no engine"));
|
||||
const requestId = "probe-" + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error("timeout")), 30000);
|
||||
const receive = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); worker.removeEventListener("message", receive); resolve(data); } };
|
||||
worker.addEventListener("message", receive);
|
||||
worker.postMessage({ requestId, command: { type: "snapshot" } });
|
||||
}));
|
||||
const s = snap.result?.snapshot;
|
||||
console.log("NODES", JSON.stringify((s?.nodes ?? []).map(n => ({ name: n.name, type: n.type, parent: n.parentId }))));
|
||||
console.log("COLLECTIONS", JSON.stringify((s?.collections ?? []).map(c => ({ name: c.name, id: c.id }))));
|
||||
await browser.close();
|
||||
20
tools/web/.probe46.mjs
Normal file
20
tools/web/.probe46.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5411/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
const list = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => (n.getAttribute('aria-label') ?? '').trim()).filter(Boolean));
|
||||
console.log("BEFORE", JSON.stringify(await list()));
|
||||
for (const name of ['Mount', 'RootRig', 'Parts']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(3000); }
|
||||
}
|
||||
console.log("AFTER-EXPAND", JSON.stringify(await list()));
|
||||
await browser.close();
|
||||
36
tools/web/.probe47.mjs
Normal file
36
tools/web/.probe47.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
// expand parents so the drop target is visible
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2500); }
|
||||
}
|
||||
const rows = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => ({ name: (n.getAttribute('aria-label')||'').trim(), level: n.getAttribute('aria-level'), node: n.getAttribute('data-node-id'), parent: n.getAttribute('data-parent-id'), draggable: n.getAttribute('draggable') })).filter(r => r.name));
|
||||
console.log("BEFORE", JSON.stringify(await rows()));
|
||||
const draggable = await page.evaluate(() => document.querySelectorAll('[role="treeitem"][draggable="true"]').length);
|
||||
console.log("DRAGGABLE-ROWS", draggable);
|
||||
// HTML5 drag: Mount -> NewParent
|
||||
const res = await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Mount'), dst = find('NewParent');
|
||||
if (!src || !dst) return { error: 'missing node', src: Boolean(src), dst: Boolean(dst) };
|
||||
const dt = new DataTransfer();
|
||||
const fire = (el, type) => el.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
fire(src, 'dragstart'); fire(dst, 'dragover'); fire(dst, 'drop'); fire(src, 'dragend');
|
||||
return { payload: dt.getData('application/x-web-blender-objects') };
|
||||
});
|
||||
console.log("DRAG", JSON.stringify(res));
|
||||
await page.waitForTimeout(7000);
|
||||
console.log("AFTER", JSON.stringify(await rows()));
|
||||
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200) }))));
|
||||
await browser.close();
|
||||
57
tools/web/.probe48.mjs
Normal file
57
tools/web/.probe48.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2500); }
|
||||
}
|
||||
const state = () => page.evaluate(() => ({
|
||||
engine: document.querySelector('[data-testid="engine-status"]')?.textContent,
|
||||
detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,200),
|
||||
tree: [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')),
|
||||
}));
|
||||
console.log("A-start", JSON.stringify(await state()));
|
||||
// drag Mount -> NewParent
|
||||
const drag = await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Mount'), dst = find('NewParent');
|
||||
if (!src || !dst) return { error: 'missing' };
|
||||
const dt = new DataTransfer();
|
||||
const fire = (el, type) => el.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
fire(src, 'dragstart'); fire(dst, 'dragover');
|
||||
const dropHandled = !dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
fire(src, 'dragend');
|
||||
return { payload: dt.getData('application/x-web-blender-objects'), dropHandled };
|
||||
});
|
||||
console.log("drag", JSON.stringify(drag));
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("B-after-drag", JSON.stringify(await state()));
|
||||
// control: menu-based reparent of Payload -> NewParent
|
||||
await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
find('Payload')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await page.waitForTimeout(2500);
|
||||
await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
find('NewParent')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey: true }));
|
||||
});
|
||||
await page.waitForTimeout(2500);
|
||||
console.log("C-selected", JSON.stringify(await page.evaluate(() => document.querySelector('main')?.getAttribute('data-selected-object-ids'))));
|
||||
await page.getByRole('button', { name: '物体', exact: true }).click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.getByRole('menuitem', { name: '父级', exact: true }).click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.getByRole('menuitem', { name: '设定父级', exact: true }).click();
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("D-after-menu", JSON.stringify(await state()));
|
||||
await browser.close();
|
||||
51
tools/web/.probe49.mjs
Normal file
51
tools/web/.probe49.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
|
||||
}
|
||||
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | "));
|
||||
const worldOf = async (name) => {
|
||||
await page.evaluate((n) => {
|
||||
const el = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
el?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
}, name);
|
||||
await page.waitForTimeout(2500);
|
||||
return page.evaluate(() => {
|
||||
const read = (label) => { const el = document.querySelector(`input[aria-label="${label}"]`); return el ? el.value : null; };
|
||||
return { x: read('位置 X'), y: read('位置 Y'), z: read('位置 Z'), loc: [...document.querySelectorAll('.property-section')].map(s => s.textContent||'').find(t => /Location|位置/.test(t))?.slice(0, 120) };
|
||||
});
|
||||
};
|
||||
console.log("TREE-BEFORE", await tree());
|
||||
console.log("MOUNT-BEFORE", JSON.stringify(await worldOf('Mount')));
|
||||
// drag Mount -> NewParent
|
||||
const drag = await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Mount'), dst = find('NewParent');
|
||||
if (!src || !dst) return { error: 'missing' };
|
||||
const dt = new DataTransfer();
|
||||
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
const handled = !dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
return { handled, payload: dt.getData('application/x-web-blender-objects') };
|
||||
});
|
||||
console.log("DRAG", JSON.stringify(drag));
|
||||
await page.waitForTimeout(8000);
|
||||
// expand NewParent to reveal the child
|
||||
const expand = page.getByRole('button', { name: '展开 NewParent', exact: true }).first();
|
||||
if (await expand.count()) { await expand.click(); await page.waitForTimeout(3500); }
|
||||
console.log("TREE-AFTER", await tree());
|
||||
console.log("MOUNT-AFTER", JSON.stringify(await worldOf('Mount')));
|
||||
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent, d: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
|
||||
await browser.close();
|
||||
27
tools/web/.probe5.mjs
Normal file
27
tools/web/.probe5.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const full = () => page.evaluate(() => {
|
||||
const m = document.querySelector("main");
|
||||
return { engine: document.querySelector('[data-testid="engine-status"]')?.textContent, rev: m?.getAttribute("data-current-main-revision"), diag: m?.getAttribute("data-diagnostic-count"), text: (document.body.innerText || "").split("\n").filter(l => /diagnos|error|failed|失败|诊断/i.test(l)).slice(0, 10) };
|
||||
});
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
console.log("ADD", JSON.stringify(await full()));
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("UNDO", JSON.stringify(await full()));
|
||||
// try Edit > Undo menu instead
|
||||
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
|
||||
await page.waitForTimeout(1500);
|
||||
console.log("EDITMENU", JSON.stringify(await page.evaluate(() => [...document.querySelectorAll('[role="menu"] button, .menu-popup button, [data-menu-item]')].map(b => (b.textContent||"").trim()).filter(Boolean).slice(0, 25))));
|
||||
await browser.close();
|
||||
67
tools/web/.probe50.mjs
Normal file
67
tools/web/.probe50.mjs
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const registry = [];
|
||||
window.__combinedWorkers = registry;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); registry.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.evaluate(() => {
|
||||
const w = window.__combinedWorkers.map(x => x.worker).find(x => true);
|
||||
window.__engine = window.__combinedWorkers.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
});
|
||||
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__engine;
|
||||
if (!worker) return reject(new Error('no engine worker'));
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const receive = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); worker.removeEventListener('message', receive); resolve(data); } };
|
||||
worker.addEventListener('message', receive);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const read = async (label) => {
|
||||
const reply = await snap();
|
||||
const s = reply.result?.snapshot;
|
||||
const node = (s?.nodes ?? []).find(n => n.name === 'Mount');
|
||||
const parent = node?.parentId ? (s.nodes.find(n => n.id === node.parentId)?.name) : null;
|
||||
return { label, rev: s?.revision, mountParent: parent, local: node?.localMatrix?.map(v => Number(v.toFixed(4))), world: node?.worldMatrix?.map(v => Number(v.toFixed(4))) };
|
||||
};
|
||||
console.log("BEFORE", JSON.stringify(await read("before")));
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Mount'), dst = find('NewParent');
|
||||
const dt = new DataTransfer();
|
||||
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
});
|
||||
await page.waitForTimeout(9000);
|
||||
console.log("AFTER", JSON.stringify(await read("after")));
|
||||
// collection drop should be rejected
|
||||
const before = await snap();
|
||||
await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Payload'), dst = find('Archive');
|
||||
const dt = new DataTransfer();
|
||||
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
});
|
||||
await page.waitForTimeout(7000);
|
||||
const after = await snap();
|
||||
console.log("COLLECTION-DROP-REV", JSON.stringify({ before: before.result?.snapshot?.revision, after: after.result?.snapshot?.revision }));
|
||||
await browser.close();
|
||||
44
tools/web/.probe51.mjs
Normal file
44
tools/web/.probe51.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
// native desktop mode: no ?ui=web
|
||||
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(4000);
|
||||
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | "));
|
||||
const draggableCount = await page.evaluate(() => document.querySelectorAll('[role="treeitem"][draggable="true"]').length);
|
||||
console.log("NATIVE-DRAGGABLE", draggableCount);
|
||||
console.log("NATIVE-TREE", await tree());
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2500); }
|
||||
}
|
||||
const drag = await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Mount'), dst = find('NewParent');
|
||||
if (!src || !dst) return { error: 'missing' };
|
||||
const dt = new DataTransfer();
|
||||
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
const handled = !dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
return { handled, payload: dt.getData('application/x-web-blender-objects') };
|
||||
});
|
||||
console.log("NATIVE-DRAG", JSON.stringify(drag));
|
||||
await page.waitForTimeout(9000);
|
||||
const expand = page.getByRole('button', { name: '展开 NewParent', exact: true }).first();
|
||||
if (await expand.count()) { await expand.click(); await page.waitForTimeout(3500); }
|
||||
console.log("NATIVE-TREE-AFTER", await tree());
|
||||
console.log("NATIVE-ENGINE", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent, d: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
|
||||
// undo
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(9000);
|
||||
console.log("NATIVE-AFTER-UNDO", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
|
||||
console.log("NATIVE-TREE-UNDO", await tree());
|
||||
await browser.close();
|
||||
21
tools/web/.probe52.mjs
Normal file
21
tools/web/.probe52.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(5000);
|
||||
console.log(JSON.stringify(await page.evaluate(() => ({
|
||||
treeItems: document.querySelectorAll('[role="treeitem"]').length,
|
||||
outlinerTrees: document.querySelectorAll('[role="tree"]').length,
|
||||
nativeOverlay: Boolean(document.querySelector('section[aria-label="Blender 桌面"]')),
|
||||
overlayVisible: (() => { const n = document.querySelector('section[aria-label="Blender 桌面"]'); if (!n) return null; const s = getComputedStyle(n); return { visibility: s.visibility, pointerEvents: s.pointerEvents, zIndex: s.zIndex }; })(),
|
||||
canvas: (() => { const c = document.querySelector('canvas[aria-label="Blender desktop"]'); if (!c) return null; const r = c.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height, display: c.getAttribute('data-native-display') }; })(),
|
||||
areas: (() => { const w = document.querySelector('main')?.getAttribute('data-editor-workflow-area'); return w; })(),
|
||||
}))));
|
||||
await browser.close();
|
||||
35
tools/web/.probe53.mjs
Normal file
35
tools/web/.probe53.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
|
||||
}
|
||||
const tree = () => page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | "));
|
||||
await page.evaluate(() => {
|
||||
const find = (name) => [...document.querySelectorAll('[role="treeitem"]')].find(n => (n.getAttribute('aria-label')||'').trim() === name);
|
||||
const src = find('Mount'), dst = find('NewParent'); const dt = new DataTransfer();
|
||||
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
});
|
||||
await page.waitForTimeout(9000);
|
||||
console.log("AFTER-DRAG", await tree());
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(9000);
|
||||
console.log("AFTER-UNDO-1", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
|
||||
console.log("TREE-UNDO-1", await tree());
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(9000);
|
||||
console.log("AFTER-UNDO-2", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent }))));
|
||||
console.log("TREE-UNDO-2", await tree());
|
||||
await browser.close();
|
||||
23
tools/web/.probe54.mjs
Normal file
23
tools/web/.probe54.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(5000);
|
||||
// The native workspace's area layout lives in the snapshot's editor workflow.
|
||||
const layout = await page.evaluate(() => {
|
||||
const text = document.querySelector('[data-testid="editor-workflow-context"]')?.textContent;
|
||||
return text ?? null;
|
||||
});
|
||||
console.log("CTX", layout);
|
||||
console.log("AREAS", JSON.stringify(await page.evaluate(() => {
|
||||
const nodes = [...document.querySelectorAll('[data-workspace-id]')];
|
||||
return nodes.map(n => n.textContent).slice(0, 16);
|
||||
})));
|
||||
await browser.close();
|
||||
31
tools/web/.probe55.mjs
Normal file
31
tools/web/.probe55.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(4000);
|
||||
const snap = await page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
if (!worker) return reject(new Error('no engine'));
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); window.removeEventListener('message', recv); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const wf = snap.result?.snapshot?.editorWorkflow;
|
||||
console.log("STATUS", wf?.status ?? "none");
|
||||
for (const ws of (wf?.workspaces ?? []).slice(0, 3)) {
|
||||
console.log("WS", ws.name, JSON.stringify(ws.areas.map(a => ({ e: a.editor, r: a.rect, active: a.id === ws.activeAreaId }))));
|
||||
}
|
||||
await browser.close();
|
||||
42
tools/web/.probe56.mjs
Normal file
42
tools/web/.probe56.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
|
||||
}
|
||||
// select Mount and KeyLight, then drag both onto NewParent
|
||||
await page.evaluate(() => {
|
||||
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
find('Mount')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => {
|
||||
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
find('KeyLight')?.querySelector('.tree-node-name')?.dispatchEvent(new MouseEvent('click', { bubbles: true, ctrlKey: true }));
|
||||
});
|
||||
await page.waitForTimeout(2000);
|
||||
console.log("SELECTED", await page.evaluate(() => document.querySelector('main')?.getAttribute('data-selected-object-ids')));
|
||||
const drag = await page.evaluate(() => {
|
||||
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
const src = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === 'Mount');
|
||||
const dst = find('NewParent'); const dt = new DataTransfer();
|
||||
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
dst.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
return dt.getData('application/x-web-blender-objects');
|
||||
});
|
||||
console.log("PAYLOAD", drag);
|
||||
await page.waitForTimeout(9000);
|
||||
console.log("TREE", await page.evaluate(() => [...document.querySelectorAll('[role="treeitem"]')].map(n => `${n.getAttribute('aria-level')}:${(n.getAttribute('aria-label')||'').trim()}`).filter(x => !x.endsWith(':')).join(" | ")));
|
||||
console.log("ENGINE", await page.evaluate(() => document.querySelector('[data-testid="engine-status"]')?.textContent));
|
||||
await browser.close();
|
||||
52
tools/web/.probe57.mjs
Normal file
52
tools/web/.probe57.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(5000);
|
||||
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
if (!worker) return reject(new Error('no engine'));
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const state = async () => {
|
||||
const r = await snap();
|
||||
const s = r.result?.snapshot;
|
||||
const byName = Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? (s.nodes.find(x => x.id === n.parentId)?.name) : null]));
|
||||
return { rev: s?.revision, parents: byName };
|
||||
};
|
||||
console.log("BEFORE", JSON.stringify(await state()));
|
||||
// Real mouse drag inside the native outliner area (normalized rect from the workflow).
|
||||
const rect = { x: 0.8234986945169713, y: 0.8174757281553398, w: 0.17650130548302867, h: 0.18252427184466025 };
|
||||
const vw = 1440, vh = 900;
|
||||
const nx = (fx) => vw * (rect.x + rect.w * fx);
|
||||
const ny = (fy) => vh * (rect.y + rect.h * fy);
|
||||
console.log("AREA-PX", JSON.stringify({ x0: nx(0), y0: ny(0), x1: nx(1), y1: ny(1) }));
|
||||
// Activate the outliner by clicking an empty part of it first.
|
||||
await page.mouse.click(nx(0.5), ny(0.9));
|
||||
await page.waitForTimeout(2500);
|
||||
await page.mouse.move(nx(0.5), ny(0.25));
|
||||
await page.waitForTimeout(800);
|
||||
await page.mouse.down();
|
||||
await page.waitForTimeout(600);
|
||||
await page.mouse.move(nx(0.5), ny(0.4), { steps: 12 });
|
||||
await page.waitForTimeout(600);
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("AFTER-NATIVE-DRAG", JSON.stringify(await state()));
|
||||
console.log("UI", JSON.stringify(await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,180), display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute("data-native-display") }))));
|
||||
await browser.close();
|
||||
35
tools/web/.probe58.mjs
Normal file
35
tools/web/.probe58.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3500);
|
||||
const snap = await page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
if (!worker) return reject(new Error('no engine'));
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const s = snap.result?.snapshot;
|
||||
const wf = s?.editorWorkflow;
|
||||
const ws = wf?.workspaces?.find(w => w.id === wf.context.workspaceId);
|
||||
for (const area of (ws?.areas ?? [])) {
|
||||
if (area.space?.outliner) {
|
||||
console.log("OUTLINER-ROWS", JSON.stringify(area.space.outliner.rows.map(r => ({ label: r.label, depth: r.depth, objectId: r.objectId, remapId: r.remapId })), null, 1));
|
||||
}
|
||||
}
|
||||
console.log("COLLECTIONS", JSON.stringify((s?.collections ?? []).map(c => ({ id: c.id, name: c.name, master: c.isMaster, children: c.childCollectionIds, objects: c.objectIds }))));
|
||||
await browser.close();
|
||||
56
tools/web/.probe59.mjs
Normal file
56
tools/web/.probe59.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3500);
|
||||
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
if (!worker) return reject(new Error('no engine'));
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const state = async () => {
|
||||
const r = await snap(); const s = r.result?.snapshot;
|
||||
const coll = Object.fromEntries((s?.collections ?? []).map(c => [c.name, c.objectIds.filter(Boolean).map(id => id.replace('object:', ''))]));
|
||||
const parents = Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null]));
|
||||
return { rev: s?.revision, coll, parents };
|
||||
};
|
||||
console.log("BEFORE", JSON.stringify(await state()));
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
|
||||
}
|
||||
const drag = async (src, dst, mods = {}) => page.evaluate(({ srcName, dstName, ctrl }) => {
|
||||
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
const s = find(srcName), d = find(dstName);
|
||||
if (!s || !d) return { error: 'missing', s: Boolean(s), d: Boolean(d) };
|
||||
const dt = new DataTransfer();
|
||||
s.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
d.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, ctrlKey: ctrl }));
|
||||
const handled = !d.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, ctrlKey: ctrl }));
|
||||
return { handled };
|
||||
}, { srcName: src, dstName: dst, ctrl: Boolean(mods.ctrl) });
|
||||
// A: object -> collection row (Archive)
|
||||
console.log("DRAG-TO-COLLECTION", JSON.stringify(await drag('Mount', 'Archive')));
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("AFTER-COLLECTION", JSON.stringify(await state()));
|
||||
// B: drop onto itself must do nothing
|
||||
console.log("DRAG-SELF", JSON.stringify(await drag('Payload', 'Payload')));
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("AFTER-SELF", JSON.stringify(await state()));
|
||||
console.log("ENGINE", JSON.stringify(await page.evaluate(() => ({ e: document.querySelector('[data-testid="engine-status"]')?.textContent, d: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,160) }))));
|
||||
await browser.close();
|
||||
37
tools/web/.probe6.mjs
Normal file
37
tools/web/.probe6.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const d = () => page.evaluate(() => {
|
||||
const m = document.querySelector("main");
|
||||
const keys = [...m.attributes].map(a => a.name).filter(k => /diagnostic/.test(k));
|
||||
return { engine: document.querySelector('[data-testid="engine-status"]')?.textContent, rev: m.getAttribute("data-current-main-revision"), code: m.getAttribute("data-last-diagnostic-code"), detail: (m.getAttribute("data-last-diagnostic-detail")||"").slice(0,600), keys };
|
||||
});
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("TOOLBAR-UNDO", JSON.stringify(await d(), null, 1));
|
||||
// reload, then use Edit > Undo menu path
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(3000);
|
||||
console.log("BEFORE-MENU", JSON.stringify(await d()));
|
||||
await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="主菜单"] button')].find(b => (b.textContent||"").trim() === "编辑")?.click());
|
||||
await page.waitForTimeout(1200);
|
||||
await page.evaluate(() => { const it = [...document.querySelectorAll("button")].find(b => (b.textContent||"").trim() === "撤销Ctrl Z"); it?.click(); });
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("MENU-UNDO", JSON.stringify(await d(), null, 1));
|
||||
await browser.close();
|
||||
49
tools/web/.probe60.mjs
Normal file
49
tools/web/.probe60.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend"));
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3500);
|
||||
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const parents = async () => {
|
||||
const r = await snap(); const s = r.result?.snapshot;
|
||||
return { rev: s?.revision, p: Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null])) };
|
||||
};
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2000); }
|
||||
}
|
||||
const drag = (src, dst, shift) => page.evaluate(({ s: srcName, d: dstName, shiftKey }) => {
|
||||
const find = (n) => [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
const s = find(srcName), d = find(dstName);
|
||||
const dt = new DataTransfer();
|
||||
s.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
d.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
|
||||
const handled = !d.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
|
||||
return handled;
|
||||
}, { s: src, d: dst, shiftKey: shift });
|
||||
console.log("BEFORE", JSON.stringify(await parents()));
|
||||
console.log("PLAIN-DROP", await drag('Mount', 'NewParent', false));
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("AFTER-PLAIN", JSON.stringify(await parents()));
|
||||
console.log("SHIFT-DROP", await drag('Mount', 'NewParent', true));
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("AFTER-SHIFT", JSON.stringify(await parents()));
|
||||
await browser.close();
|
||||
52
tools/web/.probe61.mjs
Normal file
52
tools/web/.probe61.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend");
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3500);
|
||||
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const parents = async () => {
|
||||
const r = await snap(); const s = r.result?.snapshot;
|
||||
return { rev: s?.revision, p: Object.fromEntries((s?.nodes ?? []).map(n => [n.name, n.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null])) };
|
||||
};
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2200); }
|
||||
}
|
||||
console.log("BEFORE", JSON.stringify(await parents()));
|
||||
// Drop Payload onto empty container space with and without Shift.
|
||||
const drop = (name, shift) => page.evaluate(({ n, shiftKey }) => {
|
||||
const row = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === n);
|
||||
const tree = document.querySelector('.outliner-tree');
|
||||
if (!row || !tree) return { error: 'missing', rows: [...document.querySelectorAll('[role=\"treeitem\"]')].map(x => (x.getAttribute('aria-label')||'').trim()).filter(Boolean) };
|
||||
const dt = new DataTransfer();
|
||||
row.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
// target the container itself, not a row: dispatch directly on it
|
||||
tree.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
|
||||
const handled = !tree.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey }));
|
||||
return { handled, payload: dt.getData('application/x-web-blender-objects') };
|
||||
}, { n: name, shiftKey: shift });
|
||||
console.log("DROP-NO-SHIFT", JSON.stringify(await drop('Payload', false)));
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("AFTER-NO-SHIFT", JSON.stringify(await parents()));
|
||||
console.log("DROP-SHIFT", JSON.stringify(await drop('Payload', true)));
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("AFTER-SHIFT", JSON.stringify(await parents()));
|
||||
await browser.close();
|
||||
45
tools/web/.probe62.mjs
Normal file
45
tools/web/.probe62.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createRequire } from "node:module";
|
||||
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.addInitScript(() => {
|
||||
const reg = []; window.__w = reg;
|
||||
window.Worker = new Proxy(window.Worker, { construct(t, a) { const w = Reflect.construct(t, a); reg.push({ url: String(a[0]), worker: w }); return w; } });
|
||||
});
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend");
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3500);
|
||||
const snap = () => page.evaluate(() => new Promise((resolve, reject) => {
|
||||
const worker = window.__w.find(x => /web-engine\.worker/.test(x.url))?.worker;
|
||||
const requestId = 'p-' + Math.random();
|
||||
const timer = setTimeout(() => reject(new Error('timeout')), 30000);
|
||||
const recv = ({ data }) => { if (data.requestId === requestId) { clearTimeout(timer); resolve(data); } };
|
||||
worker.addEventListener('message', recv);
|
||||
worker.postMessage({ requestId, command: { type: 'snapshot' } });
|
||||
}));
|
||||
const payload = async () => {
|
||||
const r = await snap(); const s = r.result?.snapshot;
|
||||
const n = (s?.nodes ?? []).find(x => x.name === 'Payload');
|
||||
return { rev: s?.revision, parent: n?.parentId ? s.nodes.find(x => x.id === n.parentId)?.name : null,
|
||||
world: n?.worldMatrix?.map(v => Number(v.toFixed(4))) };
|
||||
};
|
||||
for (const name of ['Parts','Mount','RootRig']) {
|
||||
const btn = page.getByRole('button', { name: `展开 ${name}`, exact: true }).first();
|
||||
if (await btn.count()) { await btn.click(); await page.waitForTimeout(2000); }
|
||||
}
|
||||
console.log("BEFORE", JSON.stringify(await payload()));
|
||||
await page.evaluate(() => {
|
||||
const row = [...document.querySelectorAll('[role="treeitem"]')].find(x => (x.getAttribute('aria-label')||'').trim() === 'Payload');
|
||||
const tree = document.querySelector('.outliner-tree');
|
||||
const dt = new DataTransfer();
|
||||
row.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: dt }));
|
||||
tree.dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey: true }));
|
||||
tree.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: dt, shiftKey: true }));
|
||||
});
|
||||
await page.waitForTimeout(8000);
|
||||
console.log("AFTER-CLEAR", JSON.stringify(await payload()));
|
||||
await browser.close();
|
||||
18
tools/web/.probe63.mjs
Normal file
18
tools/web/.probe63.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createRequire } from "node:module";
|
||||
const { chromium } = createRequire(import.meta.url)("/home/mes123456/workinf_Blender_Wasm/web/node_modules/playwright");
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5412/?ui=web", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles("/home/mes123456/workinf_Blender_Wasm/tests/files/web/hierarchy_simplify_combined.blend");
|
||||
await page.waitForFunction(() => /SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3500);
|
||||
console.log("STATUS", JSON.stringify(await page.evaluate(() => ({
|
||||
status: document.querySelector('[data-testid="editor-workflow-status"]')?.textContent,
|
||||
context: document.querySelector('[data-testid="editor-workflow-context"]')?.textContent,
|
||||
count: document.querySelector('[data-testid="editor-workflow-workspace-count"]')?.textContent,
|
||||
mainStatus: document.querySelector('main')?.getAttribute('data-editor-workflow-status'),
|
||||
}))));
|
||||
await browser.close();
|
||||
23
tools/web/.probe7.mjs
Normal file
23
tools/web/.probe7.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const out = [];
|
||||
for (const delay of [500, 2000, 5000, 10000]) {
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5401/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForTimeout(delay);
|
||||
await page.evaluate(() => document.querySelector('[aria-label="撤销"]')?.click());
|
||||
await page.waitForTimeout(4000);
|
||||
const r = await page.evaluate(() => ({ engine: document.querySelector('[data-testid="engine-status"]')?.textContent, stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, detail: (document.querySelector("main")?.getAttribute("data-last-diagnostic-detail")||"").slice(0,120) }));
|
||||
out.push({ delay, ...r });
|
||||
await browser.close();
|
||||
}
|
||||
console.log(JSON.stringify(out, null, 1));
|
||||
31
tools/web/.probe8.mjs
Normal file
31
tools/web/.probe8.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRequire } from "node:module";
|
||||
import fs from "node:fs"; import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, acceptDownloads: true });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const ws = () => page.evaluate(() => ({ ws: document.querySelector("main")?.getAttribute("data-workspace"), area: document.querySelector("main")?.getAttribute("data-editor-workflow-area") }));
|
||||
console.log("START", JSON.stringify(await ws()));
|
||||
const clickWs = (name) => page.evaluate((v) => {
|
||||
const btns = [...document.querySelectorAll('nav[aria-label="工作区"] button')];
|
||||
const b = btns.find(n => (n.textContent||"").trim() === v);
|
||||
if (!b) return { found: false, names: btns.map(n => (n.textContent||"").trim()) };
|
||||
b.click();
|
||||
return { found: true, disabled: b.disabled, cls: b.className };
|
||||
}, name);
|
||||
console.log("CLICK-Modeling", JSON.stringify(await clickWs("Modeling")));
|
||||
for (const t of [1000, 2000, 4000, 8000]) { await page.waitForTimeout(t === 1000 ? 1000 : t - (t === 2000 ? 1000 : t === 4000 ? 2000 : 4000)); console.log("T"+t, JSON.stringify(await ws())); }
|
||||
console.log("F3TAP", JSON.stringify(await page.evaluate(() => { const b = document.querySelector('[aria-label="操作搜索"]'); b?.click(); return Boolean(b); })));
|
||||
await page.waitForTimeout(2000);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(1500);
|
||||
console.log("CLICK-Scripting", JSON.stringify(await clickWs("Scripting")));
|
||||
await page.waitForTimeout(5000);
|
||||
console.log("AFTER-Scripting", JSON.stringify(await ws()));
|
||||
await browser.close();
|
||||
31
tools/web/.probe9.mjs
Normal file
31
tools/web/.probe9.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
const { chromium } = createRequire(import.meta.url)(path.resolve("web/node_modules/playwright"));
|
||||
const browser = await chromium.launch({ headless: true, executablePath: "/usr/bin/google-chrome-stable",
|
||||
args: ["--no-sandbox","--use-gl=swiftshader","--enable-unsafe-swiftshader","--enable-unsafe-webgpu","--enable-dawn-features=allow_unsafe_apis","--use-webgpu-adapter=swiftshader"] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("http://127.0.0.1:5402/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const st = () => page.evaluate(() => ({ rev: document.querySelector("main")?.getAttribute("data-current-main-revision"), stats: document.querySelector('[data-testid="scene-stats"]')?.textContent, engine: document.querySelector('[data-testid="engine-status"]')?.textContent }));
|
||||
const click = (l) => page.evaluate((v) => { const b = document.querySelector(`[aria-label="${v}"]`); b?.click(); return Boolean(b); }, l);
|
||||
console.log("S0", JSON.stringify(await st()));
|
||||
await click("添加立方体"); await page.waitForTimeout(8000);
|
||||
console.log("S1-add", JSON.stringify(await st()));
|
||||
const rev1 = (await st()).rev;
|
||||
await click("撤销"); await page.waitForTimeout(5000);
|
||||
console.log("S2-undo", JSON.stringify(await st()));
|
||||
await click("重做"); await page.waitForTimeout(5000);
|
||||
console.log("S3-redo", JSON.stringify(await st()));
|
||||
await page.waitForTimeout(6000);
|
||||
const revA = (await st()).rev;
|
||||
await click("复制对象"); await page.waitForTimeout(6000);
|
||||
console.log("S4-dup", JSON.stringify(await st()));
|
||||
await click("删除对象"); await page.waitForTimeout(6000);
|
||||
console.log("S5-del", JSON.stringify(await st()));
|
||||
await page.waitForTimeout(6000);
|
||||
console.log("S6-quiet", JSON.stringify(await st()));
|
||||
await browser.close();
|
||||
277
tools/web/authorize-wbv2-p1129.mjs
Normal file
277
tools/web/authorize-wbv2-p1129.mjs
Normal file
@@ -0,0 +1,277 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => {
|
||||
fs.mkdirSync(path.dirname(abs(file)), { recursive: true });
|
||||
fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`);
|
||||
};
|
||||
const writeText = (file, value) => {
|
||||
fs.mkdirSync(path.dirname(abs(file)), { recursive: true });
|
||||
fs.writeFileSync(abs(file), value);
|
||||
};
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1129-000";
|
||||
const moduleTask = "WBV2-P1129-001";
|
||||
const parentTask = "WBV2-P1128-000";
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
const plan = json(planPath);
|
||||
const prepared = plan.tasks.some((task) => task.id === rootTask || task.id === moduleTask);
|
||||
if (prepared) {
|
||||
assert.equal(queueTask, rootTask, `expected resumable authorization checkpoint ${rootTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, rootTask);
|
||||
assert.equal(index.activeTask, moduleTask);
|
||||
} else {
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
}
|
||||
|
||||
const module = {
|
||||
moduleId: "CORE_SCENE_GRAPH_SETTINGS",
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-SCENE-COMPOSITOR-SETTINGS", "CAP-SCENE-PERFORMANCE-SETTINGS", "CAP-SCENE-SAMPLING-SETTINGS"],
|
||||
sourcePaths: [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/wbv2-scene-settings-production.spec.ts",
|
||||
],
|
||||
reuseReceipts: [
|
||||
{ manifest: "tests/golden/WBV2-P65-002/manifest.json", report: "tests/golden/WBV2-P65-002/implementation-report.json" },
|
||||
{ manifest: "tests/golden/WBV2-P66-002/manifest.json", report: "tests/golden/WBV2-P66-002/implementation-report.json" },
|
||||
{ manifest: "tests/golden/WBV2-P67-002/manifest.json", report: "tests/golden/WBV2-P67-002/implementation-report.json" },
|
||||
],
|
||||
positive: ["compositor device/precision mutation", "performance thread/persistence mutation", "sampling filter/dither mutation"],
|
||||
negative: ["invalid sampling range", "unknown scene target", "malformed property set"],
|
||||
recovery: ["undo", "redo", "save/reopen", "deterministic scene settings snapshot"],
|
||||
commands: [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5344 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-scene-settings-production.spec.ts",
|
||||
],
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
|
||||
const rootRecord = {
|
||||
id: rootTask,
|
||||
phase: "1129",
|
||||
title: "authorize_scene_settings_module",
|
||||
kind: "enabling",
|
||||
owner: "PROGRAM",
|
||||
dependsOn: [parentTask],
|
||||
parityId: null,
|
||||
implementationClass: "QUEUE_ONLY",
|
||||
parallelGroup: "module-root-1129",
|
||||
gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`,
|
||||
behavior: "Authorize one source-backed Core Scene Graph settings module after the terminal P1128 module epoch.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1128 evidence remains hash-bound", "existing receipts are reused", "inventory remains immutable", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"],
|
||||
status: "done",
|
||||
ready: false,
|
||||
queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask,
|
||||
phase: "1129",
|
||||
title: "verify_scene_settings_module",
|
||||
kind: "module",
|
||||
owner: "CORE_SCENE",
|
||||
dependsOn: [rootTask],
|
||||
parityId: module.moduleId,
|
||||
implementationClass: "LOCAL_EXACT",
|
||||
parallelGroup: "module-core_scene_graph_settings",
|
||||
gate: "MODULE",
|
||||
card: `docs/tasks/${moduleTask}.md`,
|
||||
behavior: "Verify the existing Blender Main scene compositor, performance and sampling setting transactions as one aggregate module.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main mutations are proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen receipts are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress",
|
||||
ready: true,
|
||||
queueMutationAllowed: true,
|
||||
module,
|
||||
};
|
||||
|
||||
if (!prepared) {
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: module.moduleId, task: moduleTask, owner: "CORE_SCENE", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: module.moduleId, priorTerminalEpoch: "WBV2-P1128-000" };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
} else {
|
||||
const existingRoot = plan.tasks.find((task) => task.id === rootTask);
|
||||
if (existingRoot) existingRoot.commands = rootRecord.commands;
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
}
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize scene settings module
|
||||
|
||||
- task: ${rootTask}
|
||||
- parent: ${parentTask}
|
||||
- status: done
|
||||
- gap: workflow:authorize_scene_settings_module
|
||||
- ownerFamily: PROGRAM
|
||||
- targetImplementationClass: QUEUE_ONLY
|
||||
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1129.mjs\`
|
||||
|
||||
## 目标
|
||||
|
||||
Authorize one source-backed Core Scene Graph settings module while preserving all P1128 evidence and the immutable inventory.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Parent manifest: \`${parentManifestPath}\`
|
||||
- Existing receipts: \`tests/golden/WBV2-P65-002/\`, \`tests/golden/WBV2-P66-002/\`, \`tests/golden/WBV2-P67-002/\`
|
||||
- New module: \`${moduleTask}\`
|
||||
|
||||
## 验收
|
||||
|
||||
- P1128 source and receipt hashes remain bound
|
||||
- existing scene-setting implementation receipts are reused without rerun
|
||||
- exactly one source-backed module is active
|
||||
- inventory remains \`6900\` total and effective \`290/6609\`
|
||||
- missing source, unsupported capability, malformed input, hash drift or failed gate keeps the module in_progress
|
||||
- node tools/web/check-execution-control-plane.mjs
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
|
||||
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
|
||||
`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
|
||||
|
||||
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
|
||||
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${module.moduleId}\`
|
||||
- P1128 evidence reused; no implementation or inventory mutation claimed
|
||||
- nextTask: \`${moduleTask}\`
|
||||
`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1,
|
||||
task: rootTask,
|
||||
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
|
||||
status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId: module.moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: plan.inventory,
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, {
|
||||
schemaVersion: 1,
|
||||
task: rootTask,
|
||||
parentTask,
|
||||
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
|
||||
status: "done",
|
||||
enablingTask: true,
|
||||
parityStateChange: false,
|
||||
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
|
||||
plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) },
|
||||
status: { path: rootStatusPath, sha256: sha(rootStatusPath) },
|
||||
taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify scene settings module
|
||||
|
||||
- task: ${moduleTask}
|
||||
- parent: ${rootTask}
|
||||
- status: in_progress
|
||||
- gap: workflow:core_scene_graph_settings
|
||||
- ownerFamily: CORE_SCENE
|
||||
- targetImplementationClass: LOCAL_EXACT
|
||||
- moduleId: ${module.moduleId}
|
||||
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
|
||||
|
||||
## 目标
|
||||
|
||||
Verify the existing Blender Main compositor, performance and sampling setting transactions as one source-backed module. This adds aggregate production evidence without duplicating the P1128 movie implementation or older scene-setting receipts.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Source paths:
|
||||
${module.sourcePaths.map((file) => `- \`${file}\``).join("\n")}
|
||||
- Reused receipts:
|
||||
${module.reuseReceipts.map((item) => `- \`${item.manifest}\` + \`${item.report}\``).join("\n")}
|
||||
- Production test: \`web/tests/e2e/wbv2-scene-settings-production.spec.ts\`
|
||||
|
||||
## 验收
|
||||
|
||||
- Positive: ${module.positive.join(", ")}
|
||||
- Negative: ${module.negative.join(", ")}
|
||||
- Recovery: ${module.recovery.join(", ")}
|
||||
- Commands:
|
||||
${module.commands.map((command) => `- \`${command}\``).join("\n")}
|
||||
- Missing source, hash drift, malformed input or browser failure remains in_progress and does not advance the queue.
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
|
||||
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
|
||||
- Status: \`docs/status/${moduleTask}.md\`
|
||||
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
|
||||
`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next
|
||||
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${module.moduleId}\n`);
|
||||
245
tools/web/authorize-wbv2-p1130.mjs
Normal file
245
tools/web/authorize-wbv2-p1130.mjs
Normal file
@@ -0,0 +1,245 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1130-000";
|
||||
const moduleTask = "WBV2-P1130-001";
|
||||
const parentTask = "WBV2-P1129-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
const plan = json(planPath);
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
|
||||
const module = {
|
||||
moduleId: "UI_EDITOR_VIEW2D_STATE",
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-EDITOR-VIEW2D-CUR", "CAP-EDITOR-VIEW2D-MASK", "CAP-EDITOR-VIEW2D-FAIL-CLOSED"],
|
||||
sourcePaths: [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"web/protocol/editor-view2d.ts",
|
||||
"web/app/src/workers/editor-view2d-test.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/ui-editor-view2d-production.spec.ts",
|
||||
],
|
||||
reuseReceipts: [],
|
||||
positive: ["Graph main-region View2D cur retention", "Graph main-region pixel mask retention", "reader/protocol exact state"],
|
||||
negative: ["inverted View2D bounds", "malformed mask range", "missing View2D payload"],
|
||||
recovery: ["immutable base editor workflow", "worker termination", "deterministic repeated parse", "save/reopen reader compatibility"],
|
||||
commands: [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5346 npm --prefix web run test:e2e -- --workers=1 tests/e2e/ui-editor-view2d-production.spec.ts",
|
||||
],
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
|
||||
const rootRecord = {
|
||||
id: rootTask,
|
||||
phase: "1130",
|
||||
title: "authorize_editor_view2d_module",
|
||||
kind: "enabling",
|
||||
owner: "PROGRAM",
|
||||
dependsOn: [parentTask],
|
||||
parityId: null,
|
||||
implementationClass: "QUEUE_ONLY",
|
||||
parallelGroup: "module-root-1130",
|
||||
gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`,
|
||||
behavior: "Authorize one source-backed UI editor View2D reader/protocol module after the P1129 scene settings module.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1129 evidence remains hash-bound", "P1102 editor workflow source remains unchanged", "inventory remains immutable", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"],
|
||||
status: "done",
|
||||
ready: false,
|
||||
queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask,
|
||||
phase: "1130",
|
||||
title: "verify_editor_view2d_module",
|
||||
kind: "module",
|
||||
owner: "EDITOR",
|
||||
dependsOn: [rootTask],
|
||||
parityId: module.moduleId,
|
||||
implementationClass: "LOCAL_EXACT",
|
||||
parallelGroup: "module-ui_editor_view2d_state",
|
||||
gate: "MODULE",
|
||||
card: `docs/tasks/${moduleTask}.md`,
|
||||
behavior: "Preserve Blender Graph/Dope Sheet View2D cur and mask state through the Web editor protocol without changing the existing editor workflow parser.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive reader state is proven", "negative no-publish and recovery are proven", "Chromium and save/reopen compatibility are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress",
|
||||
ready: true,
|
||||
queueMutationAllowed: true,
|
||||
module,
|
||||
};
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: module.moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: [], fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: module.moduleId, priorModule: "WBV2-P1129-001" };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize editor View2D module
|
||||
|
||||
- task: ${rootTask}
|
||||
- parent: ${parentTask}
|
||||
- status: done
|
||||
- gap: workflow:authorize_editor_view2d_module
|
||||
- ownerFamily: PROGRAM
|
||||
- targetImplementationClass: QUEUE_ONLY
|
||||
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1130.mjs\`
|
||||
|
||||
## 目标
|
||||
|
||||
Authorize a source-backed UI editor View2D reader/protocol module while preserving the P1129 receipt and the immutable inventory.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Parent manifest: \`${parentManifestPath}\`
|
||||
- Blender reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`
|
||||
- New module: \`${moduleTask}\`
|
||||
|
||||
## 验收
|
||||
|
||||
- P1129 source and receipt hashes remain bound
|
||||
- P1102 editor workflow source remains unchanged
|
||||
- exactly one source-backed module is active
|
||||
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
|
||||
- node tools/web/check-execution-control-plane.mjs
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
|
||||
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
|
||||
`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
|
||||
|
||||
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
|
||||
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${module.moduleId}\`
|
||||
- P1129 evidence reused; P1102 editor workflow source remains unchanged
|
||||
- nextTask: \`${moduleTask}\`
|
||||
`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1,
|
||||
task: rootTask,
|
||||
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
|
||||
status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId: module.moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: plan.inventory,
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, {
|
||||
schemaVersion: 1,
|
||||
task: rootTask,
|
||||
parentTask,
|
||||
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
|
||||
status: "done",
|
||||
enablingTask: true,
|
||||
parityStateChange: false,
|
||||
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
|
||||
plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) },
|
||||
status: { path: rootStatusPath, sha256: sha(rootStatusPath) },
|
||||
taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify editor View2D module
|
||||
|
||||
- task: ${moduleTask}
|
||||
- parent: ${rootTask}
|
||||
- status: in_progress
|
||||
- gap: workflow:ui_editor_view2d_state
|
||||
- ownerFamily: EDITOR
|
||||
- targetImplementationClass: LOCAL_EXACT
|
||||
- moduleId: ${module.moduleId}
|
||||
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
|
||||
|
||||
## 目标
|
||||
|
||||
Preserve the Blender Graph/Dope Sheet View2D \`cur\` and pixel \`mask\` state through a dedicated protocol adapter. The existing P1102 editor workflow parser remains untouched so its source hash and receipt stay reusable.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Source paths:
|
||||
${module.sourcePaths.map((file) => `- \`${file}\``).join("\n")}
|
||||
- Production test: \`web/tests/e2e/ui-editor-view2d-production.spec.ts\`
|
||||
|
||||
## 验收
|
||||
|
||||
- Positive: ${module.positive.join(", ")}
|
||||
- Negative: ${module.negative.join(", ")}
|
||||
- Recovery: ${module.recovery.join(", ")}
|
||||
- Commands:
|
||||
${module.commands.map((command) => `- \`${command}\``).join("\n")}
|
||||
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
|
||||
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
|
||||
- Status: \`docs/status/${moduleTask}.md\`
|
||||
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
|
||||
`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next
|
||||
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${module.moduleId}\n`);
|
||||
261
tools/web/authorize-wbv2-p1131.mjs
Normal file
261
tools/web/authorize-wbv2-p1131.mjs
Normal file
@@ -0,0 +1,261 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1131-000";
|
||||
const moduleTask = "WBV2-P1131-001";
|
||||
const parentTask = "WBV2-P1130-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const moduleId = "CURVE_TOPOLOGY_EDITOR";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/editors/curve/curve_ops.cc",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/curve-topology-editor.ts",
|
||||
"web/app/src/workers/curve-topology-contract-test.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/curve-topology-contract.spec.ts",
|
||||
"web/tests/e2e/curve-topology-operator.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5347 npm --prefix web run test:e2e -- --workers=1 tests/e2e/curve-topology-contract.spec.ts tests/e2e/curve-topology-operator.spec.ts",
|
||||
];
|
||||
const positive = ["TOGGLE_CYCLIC Main mutation", "operator gate exposes only verified capability", "save/reopen preserves cyclic spline state"];
|
||||
const negative = ["unknown curve operator", "stale revision claim", "selection and payload budget overflow"];
|
||||
const recovery = ["undo", "redo", "save/reopen", "worker termination and deterministic contract parse"];
|
||||
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = {
|
||||
moduleId,
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-CURVE-TOPOLOGY-TOGGLE-CYCLIC", "CAP-CURVE-TOPOLOGY-GATE", "CAP-CURVE-TOPOLOGY-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
|
||||
positive,
|
||||
negative,
|
||||
recovery,
|
||||
commands,
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = {
|
||||
id: rootTask,
|
||||
phase: "1131",
|
||||
title: "authorize_curve_topology_module",
|
||||
kind: "enabling",
|
||||
owner: "PROGRAM",
|
||||
dependsOn: [parentTask],
|
||||
parityId: null,
|
||||
implementationClass: "QUEUE_ONLY",
|
||||
parallelGroup: "module-root-1131",
|
||||
gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`,
|
||||
behavior: "Authorize one source-backed Curve topology editor module after the P1130 View2D module.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1130 evidence remains hash-bound", "Curve topology source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"],
|
||||
status: "done",
|
||||
ready: false,
|
||||
queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask,
|
||||
phase: "1131",
|
||||
title: "verify_curve_topology_module",
|
||||
kind: "module",
|
||||
owner: "EDITOR",
|
||||
dependsOn: [rootTask],
|
||||
parityId: moduleId,
|
||||
implementationClass: "LOCAL_EXACT",
|
||||
parallelGroup: "module-curve_topology_editor",
|
||||
gate: "MODULE",
|
||||
card: `docs/tasks/${moduleTask}.md`,
|
||||
behavior: "Verify the Blender Curve TOGGLE_CYCLIC topology transaction through the existing Main, protocol, Worker and frontend path.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress",
|
||||
ready: true,
|
||||
queueMutationAllowed: true,
|
||||
module,
|
||||
};
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Curve topology module
|
||||
|
||||
- task: ${rootTask}
|
||||
- parent: ${parentTask}
|
||||
- status: done
|
||||
- gap: workflow:authorize_curve_topology_module
|
||||
- ownerFamily: PROGRAM
|
||||
- targetImplementationClass: QUEUE_ONLY
|
||||
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1131.mjs\`
|
||||
|
||||
## 目标
|
||||
|
||||
Authorize one source-backed Curve topology editor module while preserving the P1130 receipt and immutable inventory.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Parent manifest: \`${parentManifestPath}\`
|
||||
- Blender source: \`blender-5.2.0/source/blender/editors/curve/curve_ops.cc\`
|
||||
- Web protocol: \`web/protocol/curve-topology-editor.ts\`
|
||||
- New module: \`${moduleTask}\`
|
||||
|
||||
## 验收
|
||||
|
||||
- P1130 source and receipt hashes remain bound
|
||||
- Curve Main, protocol, Worker and frontend sources are present
|
||||
- exactly one source-backed module is active
|
||||
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
|
||||
- node tools/web/check-execution-control-plane.mjs
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
|
||||
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
|
||||
`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
|
||||
|
||||
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
|
||||
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
|
||||
- P1130 evidence reused; original 6900-entry inventory remains immutable at effective 290/6609
|
||||
- nextTask: \`${moduleTask}\`
|
||||
`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1,
|
||||
task: rootTask,
|
||||
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
|
||||
status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: { ...plan.inventory, boundCaseDelta: 0 },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, {
|
||||
schemaVersion: 1,
|
||||
task: rootTask,
|
||||
parentTask,
|
||||
operation: "AUTHORIZE_SOURCE_BACKED_MODULE",
|
||||
status: "done",
|
||||
enablingTask: true,
|
||||
parityStateChange: false,
|
||||
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) },
|
||||
plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) },
|
||||
status: { path: rootStatusPath, sha256: sha(rootStatusPath) },
|
||||
taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Curve topology module
|
||||
|
||||
- task: ${moduleTask}
|
||||
- parent: ${rootTask}
|
||||
- status: in_progress
|
||||
- gap: workflow:curve_topology_editor
|
||||
- ownerFamily: EDITOR
|
||||
- targetImplementationClass: LOCAL_EXACT
|
||||
- moduleId: ${moduleId}
|
||||
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
|
||||
|
||||
## 目标
|
||||
|
||||
Verify the source-backed Blender Curve TOGGLE_CYCLIC transaction through Main, protocol, Worker and frontend paths without duplicating the existing non-mesh geometry implementation.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Source paths:
|
||||
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
|
||||
- Reused receipts:
|
||||
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
|
||||
- Production tests: \`web/tests/e2e/curve-topology-contract.spec.ts\`, \`web/tests/e2e/curve-topology-operator.spec.ts\`
|
||||
|
||||
## 验收
|
||||
|
||||
- Positive: ${positive.join(", ")}
|
||||
- Negative: ${negative.join(", ")}
|
||||
- Recovery: ${recovery.join(", ")}
|
||||
- Commands:
|
||||
${commands.map((command) => `- \`${command}\``).join("\n")}
|
||||
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
|
||||
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
|
||||
- Status: \`docs/status/${moduleTask}.md\`
|
||||
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
|
||||
`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next
|
||||
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
235
tools/web/authorize-wbv2-p1132.mjs
Normal file
235
tools/web/authorize-wbv2-p1132.mjs
Normal file
@@ -0,0 +1,235 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1132-000";
|
||||
const moduleTask = "WBV2-P1132-001";
|
||||
const parentTask = "WBV2-P1131-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "CURVE_HANDLE_GIZMO";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/nonmesh-interaction.ts",
|
||||
"web/app/src/workers/nonmesh-interaction-test.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/app/src/three-adapter/nonmesh.ts",
|
||||
"web/app/src/three-adapter/viewport.ts",
|
||||
"web/app/src/three-adapter/offscreen-viewport.ts",
|
||||
"web/tests/e2e/wbv2-curve-handle-production.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5348 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-curve-handle-production.spec.ts",
|
||||
];
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = {
|
||||
path: dynamicPath,
|
||||
sha256: sha(dynamicPath),
|
||||
sourceTreeSha256: dynamic.authority.sourceTreeSha256,
|
||||
unitCount: dynamic.summary.unitCount,
|
||||
planningEstimate: dynamic.policy.planningEstimate,
|
||||
hardTarget: dynamic.policy.hardTarget,
|
||||
verifiedUnits: dynamic.coverage.verifiedUnits,
|
||||
unverifiedUnits: dynamic.coverage.unverifiedUnits,
|
||||
};
|
||||
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = {
|
||||
moduleId,
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-CURVE-HANDLE-MAIN", "CAP-CURVE-HANDLE-GIZMO-PREVIEW", "CAP-CURVE-HANDLE-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
|
||||
positive: ["Curve handle position Main mutation", "local handle gizmo preview", "save/reopen preserves handle state"],
|
||||
negative: ["invalid handle identity", "stale base revision", "malformed handle position"],
|
||||
recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic preview"],
|
||||
commands,
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = {
|
||||
id: rootTask, phase: "1132", title: "authorize_curve_handle_module", kind: "enabling", owner: "PROGRAM",
|
||||
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1132", gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Curve handle gizmo module after the P1131 topology module.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1131 evidence remains hash-bound", "Curve handle source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask, phase: "1132", title: "verify_curve_handle_module", kind: "module", owner: "EDITOR", dependsOn: [rootTask],
|
||||
parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-curve_handle_gizmo", gate: "MODULE",
|
||||
card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Curve handle position edits through Blender Main, protocol, Worker, renderer preview and frontend gizmo.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress", ready: true, queueMutationAllowed: true, module,
|
||||
};
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Curve handle module
|
||||
|
||||
- task: ${rootTask}
|
||||
- parent: ${parentTask}
|
||||
- status: done
|
||||
- gap: workflow:authorize_curve_handle_module
|
||||
- ownerFamily: PROGRAM
|
||||
- targetImplementationClass: QUEUE_ONLY
|
||||
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1132.mjs\`
|
||||
|
||||
## 目标
|
||||
|
||||
Authorize one source-backed Curve handle gizmo module while preserving P1131 evidence and the historical inventory.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Parent manifest: \`${parentManifestPath}\`
|
||||
- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`
|
||||
- Web protocol: \`web/protocol/nonmesh-interaction.ts\`
|
||||
- New module: \`${moduleTask}\`
|
||||
|
||||
## 验收
|
||||
|
||||
- P1131 source and receipt hashes remain bound
|
||||
- Curve handle Main/protocol/Worker/frontend sources are present
|
||||
- exactly one source-backed module is active
|
||||
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
|
||||
- node tools/web/check-execution-control-plane.mjs
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
|
||||
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
|
||||
`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
|
||||
|
||||
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
|
||||
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
|
||||
- P1131 evidence reused; historical 6900-entry inventory remains immutable
|
||||
- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)
|
||||
- nextTask: \`${moduleTask}\`
|
||||
`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
|
||||
});
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, {
|
||||
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false,
|
||||
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Curve handle module
|
||||
|
||||
- task: ${moduleTask}
|
||||
- parent: ${rootTask}
|
||||
- status: in_progress
|
||||
- gap: workflow:curve_handle_gizmo
|
||||
- ownerFamily: EDITOR
|
||||
- targetImplementationClass: LOCAL_EXACT
|
||||
- moduleId: ${moduleId}
|
||||
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
|
||||
|
||||
## 目标
|
||||
|
||||
Verify the source-backed Curve handle gizmo position transaction through Blender Main, protocol, Worker, renderer preview and frontend paths.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Source paths:
|
||||
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
|
||||
- Reused receipts:
|
||||
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
|
||||
- Production test: \`web/tests/e2e/wbv2-curve-handle-production.spec.ts\`
|
||||
|
||||
## 验收
|
||||
|
||||
- Positive: ${module.positive.join(", ")}
|
||||
- Negative: ${module.negative.join(", ")}
|
||||
- Recovery: ${module.recovery.join(", ")}
|
||||
- Commands:
|
||||
${commands.map((command) => `- \`${command}\``).join("\n")}
|
||||
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
|
||||
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
|
||||
- Status: \`docs/status/${moduleTask}.md\`
|
||||
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
|
||||
`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next
|
||||
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
231
tools/web/authorize-wbv2-p1133.mjs
Normal file
231
tools/web/authorize-wbv2-p1133.mjs
Normal file
@@ -0,0 +1,231 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1133-000";
|
||||
const moduleTask = "WBV2-P1133-001";
|
||||
const parentTask = "WBV2-P1132-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "CURVE_SPLINES_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/tests/e2e/wbv2-curve-splines-production.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5349 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-curve-splines-production.spec.ts",
|
||||
];
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = {
|
||||
path: dynamicPath,
|
||||
sha256: sha(dynamicPath),
|
||||
sourceTreeSha256: dynamic.authority.sourceTreeSha256,
|
||||
unitCount: dynamic.summary.unitCount,
|
||||
planningEstimate: dynamic.policy.planningEstimate,
|
||||
hardTarget: dynamic.policy.hardTarget,
|
||||
verifiedUnits: dynamic.coverage.verifiedUnits,
|
||||
unverifiedUnits: dynamic.coverage.unverifiedUnits,
|
||||
};
|
||||
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = {
|
||||
moduleId,
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-CURVE-SPLINES-MAIN", "CAP-CURVE-SPLINES-RECOVERY", "CAP-CURVE-SPLINES-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
|
||||
positive: ["complete Curve spline Main transaction", "POLY/BEZIER descriptor retention", "save/reopen preserves spline state"],
|
||||
negative: ["invalid spline offsets", "stale base revision", "mismatched handle payload"],
|
||||
recovery: ["undo", "redo", "save/reopen", "deterministic binary-geometry transaction"],
|
||||
commands,
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = {
|
||||
id: rootTask, phase: "1133", title: "authorize_curve_splines_module", kind: "enabling", owner: "PROGRAM",
|
||||
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1133", gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Curve spline transaction module after the P1132 handle module.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1132 evidence remains hash-bound", "Curve spline source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask, phase: "1133", title: "verify_curve_splines_module", kind: "module", owner: "EDITOR", dependsOn: [rootTask],
|
||||
parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-curve_splines_transaction", gate: "MODULE",
|
||||
card: `docs/tasks/${moduleTask}.md`, behavior: "Verify complete Curve spline replacement through Blender Main, protocol, Worker and Chromium recovery.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress", ready: true, queueMutationAllowed: true, module,
|
||||
};
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "EDITOR", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Curve splines module
|
||||
|
||||
- task: ${rootTask}
|
||||
- parent: ${parentTask}
|
||||
- status: done
|
||||
- gap: workflow:authorize_curve_splines_module
|
||||
- ownerFamily: PROGRAM
|
||||
- targetImplementationClass: QUEUE_ONLY
|
||||
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1133.mjs\`
|
||||
|
||||
## 目标
|
||||
|
||||
Authorize one source-backed Curve spline transaction module while preserving P1132 evidence and the historical inventory.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Parent manifest: \`${parentManifestPath}\`
|
||||
- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`
|
||||
- Web protocol: \`web/protocol/web-engine.ts\`
|
||||
- New module: \`${moduleTask}\`
|
||||
|
||||
## 验收
|
||||
|
||||
- P1132 source and receipt hashes remain bound
|
||||
- Curve spline Main, protocol and Worker sources are present
|
||||
- exactly one source-backed module is active
|
||||
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
|
||||
- node tools/web/check-execution-control-plane.mjs
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
|
||||
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
|
||||
`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
|
||||
|
||||
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
|
||||
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
|
||||
- P1132 evidence reused; historical 6900-entry inventory remains immutable
|
||||
- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)
|
||||
- nextTask: \`${moduleTask}\`
|
||||
`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
|
||||
});
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, {
|
||||
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false,
|
||||
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Curve splines module
|
||||
|
||||
- task: ${moduleTask}
|
||||
- parent: ${rootTask}
|
||||
- status: in_progress
|
||||
- gap: workflow:curve_splines_transaction
|
||||
- ownerFamily: EDITOR
|
||||
- targetImplementationClass: LOCAL_EXACT
|
||||
- moduleId: ${moduleId}
|
||||
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
|
||||
|
||||
## 目标
|
||||
|
||||
Verify the source-backed Curve spline replacement transaction through Blender Main, protocol, Worker and Chromium recovery paths.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Source paths:
|
||||
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
|
||||
- Reused receipts:
|
||||
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
|
||||
- Production test: \`web/tests/e2e/wbv2-curve-splines-production.spec.ts\`
|
||||
|
||||
## 验收
|
||||
|
||||
- Positive: ${module.positive.join(", ")}
|
||||
- Negative: ${module.negative.join(", ")}
|
||||
- Recovery: ${module.recovery.join(", ")}
|
||||
- Commands:
|
||||
${commands.map((command) => `- \`${command}\``).join("\n")}
|
||||
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
|
||||
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
|
||||
- Status: \`docs/status/${moduleTask}.md\`
|
||||
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
|
||||
`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next
|
||||
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
223
tools/web/authorize-wbv2-p1134.mjs
Normal file
223
tools/web/authorize-wbv2-p1134.mjs
Normal file
@@ -0,0 +1,223 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1134-000";
|
||||
const moduleTask = "WBV2-P1134-001";
|
||||
const parentTask = "WBV2-P1133-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "SURFACE_TOPOLOGY_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/tests/e2e/wbv2-surface-topology-production.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5350 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-surface-topology-production.spec.ts",
|
||||
];
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = {
|
||||
path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256,
|
||||
unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget,
|
||||
verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits,
|
||||
};
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = {
|
||||
moduleId, sourceBacked: true,
|
||||
capabilities: ["CAP-SURFACE-TOPOLOGY-MAIN", "CAP-SURFACE-NURBS-BOUNDS", "CAP-SURFACE-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }],
|
||||
positive: ["Surface topology Main transaction", "NURBS dimensions and weights retention", "save/reopen preserves surface state"],
|
||||
negative: ["invalid surface dimensions", "stale base revision", "control grid budget mismatch"],
|
||||
recovery: ["undo", "redo", "save/reopen", "deterministic surface transaction"],
|
||||
commands, fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = {
|
||||
id: rootTask, phase: "1134", title: "authorize_surface_topology_module", kind: "enabling", owner: "PROGRAM",
|
||||
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1134", gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Surface topology transaction module after the P1133 Curve spline module.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1133 evidence remains hash-bound", "Surface topology source and protocol are present", "inventory remains immutable", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask, phase: "1134", title: "verify_surface_topology_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask],
|
||||
parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-surface_topology_transaction", gate: "MODULE",
|
||||
card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Surface NURBS topology replacement through Blender Main, protocol, Worker and Chromium recovery.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress", ready: true, queueMutationAllowed: true, module,
|
||||
};
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Surface topology module
|
||||
|
||||
- task: ${rootTask}
|
||||
- parent: ${parentTask}
|
||||
- status: done
|
||||
- gap: workflow:authorize_surface_topology_module
|
||||
- ownerFamily: PROGRAM
|
||||
- targetImplementationClass: QUEUE_ONLY
|
||||
- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1134.mjs\`
|
||||
|
||||
## 目标
|
||||
|
||||
Authorize one source-backed Surface topology transaction module while preserving P1133 evidence and the historical inventory.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Parent manifest: \`${parentManifestPath}\`
|
||||
- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`
|
||||
- Web protocol: \`web/protocol/web-engine.ts\`
|
||||
- New module: \`${moduleTask}\`
|
||||
|
||||
## 验收
|
||||
|
||||
- P1133 source and receipt hashes remain bound
|
||||
- Surface topology Main/protocol/Worker sources are present
|
||||
- exactly one source-backed module is active
|
||||
- malformed, unsupported, missing source or hash-drift input keeps the module in_progress
|
||||
- node tools/web/check-execution-control-plane.mjs
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Manifest: \`tests/golden/${rootTask}/manifest.json\`
|
||||
- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.
|
||||
`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status
|
||||
|
||||
- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`
|
||||
- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`
|
||||
- P1133 evidence reused; historical 6900-entry inventory remains immutable
|
||||
- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)
|
||||
- nextTask: \`${moduleTask}\`
|
||||
`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
|
||||
});
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, {
|
||||
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false,
|
||||
runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Surface topology module
|
||||
|
||||
- task: ${moduleTask}
|
||||
- parent: ${rootTask}
|
||||
- status: in_progress
|
||||
- gap: workflow:surface_topology_transaction
|
||||
- ownerFamily: GEOMETRY
|
||||
- targetImplementationClass: LOCAL_EXACT
|
||||
- moduleId: ${moduleId}
|
||||
- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`
|
||||
|
||||
## 目标
|
||||
|
||||
Verify the source-backed Surface NURBS topology replacement through Blender Main, protocol, Worker and Chromium recovery paths.
|
||||
|
||||
## 输入与范围
|
||||
|
||||
- Source paths:
|
||||
${sourcePaths.map((file) => `- \`${file}\``).join("\n")}
|
||||
- Reused receipts:
|
||||
- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`
|
||||
- Production test: \`web/tests/e2e/wbv2-surface-topology-production.spec.ts\`
|
||||
|
||||
## 验收
|
||||
|
||||
- Positive: ${module.positive.join(", ")}
|
||||
- Negative: ${module.negative.join(", ")}
|
||||
- Recovery: ${module.recovery.join(", ")}
|
||||
- Commands:
|
||||
${commands.map((command) => `- \`${command}\``).join("\n")}
|
||||
- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.
|
||||
|
||||
## 交付与回滚
|
||||
|
||||
- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`
|
||||
- Manifest: \`tests/golden/${moduleTask}/manifest.json\`
|
||||
- Status: \`docs/status/${moduleTask}.md\`
|
||||
- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.
|
||||
`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next
|
||||
.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
88
tools/web/authorize-wbv2-p1135.mjs
Normal file
88
tools/web/authorize-wbv2-p1135.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1135-000";
|
||||
const moduleTask = "WBV2-P1135-001";
|
||||
const parentTask = "WBV2-P1134-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "FONT_BODY_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/wbv2-font-body-production.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5351 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-font-body-production.spec.ts",
|
||||
];
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FONT-BODY-MAIN", "CAP-FONT-BODY-RECOVERY", "CAP-FONT-BODY-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }], positive: ["Font body Main mutation", "UTF-8 text retention", "save/reopen preserves text"], negative: ["unknown Font data ID", "body byte budget overflow", "malformed body payload"], recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic text state"], commands, fullFamilyParity: false };
|
||||
const rootRecord = { id: rootTask, phase: "1135", title: "authorize_font_body_module", kind: "enabling", owner: "PROGRAM", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1135", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Font body transaction module after the P1134 Surface topology module.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1134 evidence remains hash-bound", "Font body source and protocol are present", "inventory remains immutable", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1135", title: "verify_font_body_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-font_body_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Font body replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Font body module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_font_body_module\n- ownerFamily: PROGRAM\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1135.mjs\`\n\n## 目标\n\nAuthorize one source-backed Font body transaction module while preserving P1134 evidence and the historical inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- Web protocol: \`web/protocol/web-engine.ts\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1134 source and receipt hashes remain bound\n- Font body Main/protocol/Worker sources are present\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- node tools/web/check-execution-control-plane.mjs\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1134 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Font body module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:font_body_transaction\n- ownerFamily: GEOMETRY\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify the source-backed Font body replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n- Source paths:\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Reused receipts:\n- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`\n- Production test: \`web/tests/e2e/wbv2-font-body-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
87
tools/web/authorize-wbv2-p1136.mjs
Normal file
87
tools/web/authorize-wbv2-p1136.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1136-000";
|
||||
const moduleTask = "WBV2-P1136-001";
|
||||
const parentTask = "WBV2-P1135-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "FONT_PROPERTIES_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/wbv2-font-properties-production.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5352 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-font-properties-production.spec.ts",
|
||||
];
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FONT-PROPERTIES-MAIN", "CAP-FONT-PROPERTIES-RECOVERY", "CAP-FONT-PROPERTIES-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }], positive: ["Font property Main mutation", "bounded alignment/extrude fields", "save/reopen preserves properties"], negative: ["unknown Font data ID", "invalid alignment enum", "out-of-range numeric property"], recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic property state"], commands, fullFamilyParity: false };
|
||||
const rootRecord = { id: rootTask, phase: "1136", title: "authorize_font_properties_module", kind: "enabling", owner: "PROGRAM", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1136", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Font properties transaction module after the P1135 Font body module.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1135 evidence remains hash-bound", "Font property source and protocol are present", "inventory remains immutable", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1136", title: "verify_font_properties_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-font_properties_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Font property replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Font properties module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_font_properties_module\n- ownerFamily: PROGRAM\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1136.mjs\`\n\n## 目标\n\nAuthorize one source-backed Font properties transaction module while preserving P1135 evidence and the historical inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- Web protocol: \`web/protocol/web-engine.ts\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1135 source and receipt hashes remain bound\n- Font property Main/protocol/Worker sources are present\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- node tools/web/check-execution-control-plane.mjs\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1135 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Font properties module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:font_properties_transaction\n- ownerFamily: GEOMETRY\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify the source-backed Font property replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n- Source paths:\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Reused receipts:\n- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`\n- Production test: \`web/tests/e2e/wbv2-font-properties-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
let next = read("nextTask.md"); const tick = String.fromCharCode(96);
|
||||
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
87
tools/web/authorize-wbv2-p1137.mjs
Normal file
87
tools/web/authorize-wbv2-p1137.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const run = ([entry, ...args]) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1137-000";
|
||||
const moduleTask = "WBV2-P1137-001";
|
||||
const parentTask = "WBV2-P1136-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "METABALL_ELEMENTS_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/wbv2-metaball-elements-production.spec.ts",
|
||||
];
|
||||
const commands = [
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5353 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-metaball-elements-production.spec.ts",
|
||||
];
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-METABALL-ELEMENTS-MAIN", "CAP-METABALL-ELEMENTS-RECOVERY", "CAP-METABALL-ELEMENTS-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1101-000/manifest.json", report: "tests/golden/WBV2-P1101-000/module-receipt.json" }], positive: ["Metaball element Main mutation", "bounded position/radius/scale retention", "save/reopen preserves elements"], negative: ["unknown Metaball data ID", "invalid radius", "element count topology mismatch"], recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic element state"], commands, fullFamilyParity: false };
|
||||
const rootRecord = { id: rootTask, phase: "1137", title: "authorize_metaball_elements_module", kind: "enabling", owner: "PROGRAM", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1137", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize one source-backed Metaball element transaction module after the P1136 Font properties module.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1136 evidence remains hash-bound", "Metaball element source and protocol are present", "inventory remains immutable", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1137", title: "verify_metaball_elements_module", kind: "module", owner: "GEOMETRY", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-metaball_elements_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Metaball element replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "GEOMETRY", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run(["tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath)]);
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Metaball elements module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_metaball_elements_module\n- ownerFamily: PROGRAM\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1137.mjs\`\n\n## 目标\n\nAuthorize one source-backed Metaball element transaction module while preserving P1136 evidence and the historical inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender source: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- Web protocol: \`web/protocol/web-engine.ts\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1136 source and receipt hashes remain bound\n- Metaball element Main/protocol/Worker sources are present\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- node tools/web/check-execution-control-plane.mjs\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as the terminal queue parent and leave ${moduleTask} unstarted.\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1136 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run(["tools/web/check-task-context.mjs", "--task", rootTask, "--write"]);
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Metaball elements module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:metaball_elements_transaction\n- ownerFamily: GEOMETRY\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify the source-backed Metaball element replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n- Source paths:\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Reused receipts:\n- \`tests/golden/WBV2-P1101-000/manifest.json\` + \`tests/golden/WBV2-P1101-000/module-receipt.json\`\n- Production test: \`web/tests/e2e/wbv2-metaball-elements-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
|
||||
run(["tools/web/check-task-context.mjs", "--task", moduleTask, "--write"]);
|
||||
let next = read("nextTask.md"); const tick = String.fromCharCode(96);
|
||||
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run(["tools/web/check-execution-control-plane.mjs"]);
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
153
tools/web/authorize-wbv2-p1138.mjs
Normal file
153
tools/web/authorize-wbv2-p1138.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1138-000";
|
||||
const moduleTask = "WBV2-P1138-001";
|
||||
const parentTask = "WBV2-P1137-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "TEXT_SOURCE_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/blenkernel/intern/idtype.cc",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/wbv2-text-source-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5353 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-text-source-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
const dynamic = json(dynamicPath);
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
|
||||
const dynamicSummary = {
|
||||
path: dynamicPath,
|
||||
sha256: sha(dynamicPath),
|
||||
sourceTreeSha256: dynamic.authority.sourceTreeSha256,
|
||||
unitCount: dynamic.summary.unitCount,
|
||||
planningEstimate: dynamic.policy.planningEstimate,
|
||||
hardTarget: dynamic.policy.hardTarget,
|
||||
verifiedUnits: dynamic.coverage.verifiedUnits,
|
||||
unverifiedUnits: dynamic.coverage.unverifiedUnits,
|
||||
};
|
||||
const module = {
|
||||
moduleId,
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-TEXT-SOURCE-MAIN", "CAP-TEXT-SOURCE-RECOVERY", "CAP-TEXT-SOURCE-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1137-001/manifest.json", report: "tests/golden/WBV2-P1137-001/module-receipt.json" }],
|
||||
positive: ["Text source Main mutation", "ID_TXT registration in WITH_WEB", "save/reopen preserves source"],
|
||||
negative: ["unknown Text data ID", "source over 1 MiB", "linked/read-only Text"],
|
||||
recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic source state"],
|
||||
commands,
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = {
|
||||
id: rootTask, phase: "1138", title: "authorize_text_source_module", kind: "enabling", owner: "SCRIPTING",
|
||||
dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1138", gate: "MODULE",
|
||||
card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed Text datablock mutation after the P1137 Metaball module.",
|
||||
outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1137 evidence remains hash-bound", "ID_TXT is registered for WITH_WEB", "inventory remains source-derived", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask, phase: "1138", title: "verify_text_source_module", kind: "module", owner: "SCRIPTING", dependsOn: [rootTask], parityId: moduleId,
|
||||
implementationClass: "LOCAL_EXACT", parallelGroup: "module-text_source_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`,
|
||||
behavior: "Verify Text source replacement through Blender Main, protocol, Worker and Chromium recovery.",
|
||||
outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress", ready: true, queueMutationAllowed: true, module,
|
||||
};
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "SCRIPTING", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Text source module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_text_source_module\n- ownerFamily: SCRIPTING\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1138.mjs\`\n\n## 目标\n\nAuthorize one source-backed Text datablock transaction module while preserving P1137 evidence and the source-derived inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender ID registry: \`blender-5.2.0/source/blender/blenkernel/intern/idtype.cc\`\n- Blender Main mutation: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1137 source and receipt hashes remain bound\n- ID_TXT is registered in the WITH_WEB source path\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as terminal queue parent and leave ${moduleTask} unstarted.\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1137 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
|
||||
let updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, {
|
||||
schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS",
|
||||
parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) },
|
||||
successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true },
|
||||
inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask,
|
||||
});
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, {
|
||||
schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true,
|
||||
parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE",
|
||||
artifacts: {
|
||||
parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) },
|
||||
report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) },
|
||||
},
|
||||
assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false },
|
||||
nextTask: moduleTask,
|
||||
});
|
||||
|
||||
updatedQueue = read(queuePath)
|
||||
.replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`)
|
||||
.replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`)
|
||||
.replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Text source module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:text_source_transaction\n- ownerFamily: SCRIPTING\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify Text source replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Fixture: \`tests/files/web/generated/M16-GAP-00024-datablock-Text.blend\`\n- Production test: \`web/tests/e2e/wbv2-text-source-production.spec.ts\`\n- Reused receipt: \`tests/golden/WBV2-P1137-001/manifest.json\` + \`tests/golden/WBV2-P1137-001/module-receipt.json\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
let next = read("nextTask.md");
|
||||
const tick = String.fromCharCode(96);
|
||||
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`)
|
||||
.replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`)
|
||||
.replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`)
|
||||
.replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`)
|
||||
.replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`)
|
||||
.replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
99
tools/web/authorize-wbv2-p1139.mjs
Normal file
99
tools/web/authorize-wbv2-p1139.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const rootTask = "WBV2-P1139-000";
|
||||
const moduleTask = "WBV2-P1139-001";
|
||||
const parentTask = "WBV2-P1138-001";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const moduleId = "TEXTURE_PROPERTIES_TRANSACTION";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/blenkernel/intern/idtype.cc",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_api.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_main_state.h",
|
||||
"web/protocol/web-engine.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/app/App.tsx",
|
||||
"web/tests/e2e/wbv2-texture-properties-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5353 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-texture-properties-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
const parent = json(parentManifestPath);
|
||||
const plan = json(planPath);
|
||||
const index = json("tests/golden/WBV2/task-index.json");
|
||||
const dynamic = json(dynamicPath);
|
||||
assert.equal(queueTask, parentTask, `expected terminal queue task ${parentTask}, got ${queueTask}`);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.equal(parent.nextTask, null);
|
||||
assert.equal(index.activeTask, null);
|
||||
assert.equal(plan.status, "COMPLETED_MODULE_EXECUTION");
|
||||
assert.ok(plan.tasks.every((task) => task.status === "done" && task.ready !== true));
|
||||
for (const source of sourcePaths) assert.ok(fs.existsSync(abs(source)), `missing source: ${source}`);
|
||||
const dynamicSummary = { path: dynamicPath, sha256: sha(dynamicPath), sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const module = {
|
||||
moduleId, sourceBacked: true,
|
||||
capabilities: ["CAP-TEXTURE-PROPERTIES-MAIN", "CAP-TEXTURE-PROPERTIES-RECOVERY", "CAP-TEXTURE-PROPERTIES-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1138-001/manifest.json", report: "tests/golden/WBV2-P1138-001/module-receipt.json" }],
|
||||
positive: ["Texture property Main mutation", "ID_TE registration in WITH_WEB", "save/reopen preserves properties"],
|
||||
negative: ["unknown Texture data ID", "noise depth outside Blender bounds", "non-finite numeric property"],
|
||||
recovery: ["undo", "redo", "save/reopen", "Worker termination and deterministic property state"], commands, fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = { id: rootTask, phase: "1139", title: "authorize_texture_properties_module", kind: "enabling", owner: "TEXTURE", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1139", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed Texture property mutation module after P1138 Text.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1138 evidence remains hash-bound", "ID_TE is registered for WITH_WEB", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1139", title: "verify_texture_properties_module", kind: "module", owner: "TEXTURE", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-texture_properties_transaction", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Texture property replacement through Blender Main, protocol, Worker and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main mutation is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
plan.modules.push({ id: moduleId, task: moduleTask, owner: "TEXTURE", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
plan.tasks.push(rootRecord, moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Texture properties module\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- gap: workflow:authorize_texture_properties_module\n- ownerFamily: TEXTURE\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1139.mjs\`\n\n## 目标\n\nAuthorize one source-backed Texture property transaction while preserving P1138 evidence and the source-derived inventory.\n\n## 输入与范围\n\n- Parent manifest: \`${parentManifestPath}\`\n- Blender ID registry: \`blender-5.2.0/source/blender/blenkernel/intern/idtype.cc\`\n- Blender Main mutation: \`blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp\`\n- New module: \`${moduleTask}\`\n\n## 验收\n\n- P1138 source and receipt hashes remain bound\n- ID_TE is registered in the WITH_WEB source path\n- exactly one source-backed module is active\n- malformed, unsupported, missing source or hash-drift input keeps the module in_progress\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- Manifest: \`tests/golden/${rootTask}/manifest.json\`\n- Rollback: restore ${parentTask} as terminal queue parent and leave ${moduleTask} unstarted.\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1138 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
const rootManifestPath = `tests/golden/${rootTask}/manifest.json`;
|
||||
writeJson(rootManifestPath, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Texture properties module\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:texture_properties_transaction\n- ownerFamily: TEXTURE\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\nVerify Texture property replacement through Blender Main, protocol, Worker and Chromium recovery paths.\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- Fixture: \`tests/files/web/generated/M16-GAP-00025-datablock-Texture.blend\`\n- Production test: \`web/tests/e2e/wbv2-texture-properties-production.spec.ts\`\n- Reused receipt: \`tests/golden/WBV2-P1138-001/manifest.json\` + \`tests/golden/WBV2-P1138-001/module-receipt.json\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- malformed input, missing source, hash drift or Chromium failure remains in_progress and does not advance the queue.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`\n- Manifest: \`tests/golden/${moduleTask}/manifest.json\`\n- Status: \`docs/status/${moduleTask}.md\`\n- Rollback: restore ${rootTask} as the queue parent and leave this module in_progress.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
let next = read("nextTask.md"); const tick = String.fromCharCode(96);
|
||||
next = next.replace(/\| Current task when this file was written \| `[^`]+` \|/u, `| Current task when this file was written | ${tick}${moduleTask}${tick} |`).replace(/\| Current task state \| `[^`]+` \|/u, `| Current task state | ${tick}in_progress${tick} |`).replace(/\| Parent manifest \| `[^`]+` \|/u, `| Parent manifest | ${tick}tests/golden/${rootTask}/manifest.json${tick} |`).replace(/\| Current task card \| `[^`]+` \|/u, `| Current task card | ${tick}docs/tasks/${moduleTask}.md${tick} |`).replace(/\| Current task-context nextTask \| `[^`]+` \|/u, `| Current task-context nextTask | ${tick}NONE${tick} |`).replace(/\| Focused command \| `[^`]+` \|/u, `| Focused command | ${tick}node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write${tick} |`);
|
||||
writeText("nextTask.md", next);
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
103
tools/web/authorize-wbv2-p1140.mjs
Normal file
103
tools/web/authorize-wbv2-p1140.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const parentTask = "WBV2-P1139-001";
|
||||
const rootTask = "WBV2-P1140-000";
|
||||
const moduleTask = "WBV2-P1140-001";
|
||||
const moduleId = "ACTION_GROUP_READER";
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/engine-client/WebEngineClient.ts",
|
||||
"web/tests/e2e/wbv2-action-group-reader-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5356 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-action-group-reader-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queue = read(queuePath);
|
||||
const queueTask = queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
assert.ok(queueTask === parentTask || queueTask === rootTask || queueTask === moduleTask);
|
||||
const parent = json(parentManifestPath);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
|
||||
const plan = json(planPath);
|
||||
assert.ok(plan.status === "COMPLETED_MODULE_EXECUTION" || plan.status === "ACTIVE_MODULE_EXECUTION_QUEUE");
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
|
||||
const module = {
|
||||
moduleId,
|
||||
sourceBacked: true,
|
||||
capabilities: ["CAP-ACTION-GROUP-READER", "CAP-ACTION-GROUP-RECOVERY", "CAP-ACTION-GROUP-FAIL-CLOSED"],
|
||||
sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1112-000/manifest.json", report: "tests/golden/WBV2-P1112-000/module-receipt.json" }],
|
||||
positive: ["ActionGroup Main reader exposes group and expanded metadata", "three grouped location F-Curves remain grouped", "save/reopen preserves group metadata"],
|
||||
negative: ["malformed blend is rejected without Main mutation", "missing animation target is fail-closed", "unsupported group payload is not synthesized"],
|
||||
recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic group state"],
|
||||
commands,
|
||||
fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = {
|
||||
id: rootTask, phase: "1140", title: "authorize_action_group_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null,
|
||||
implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1140", gate: "MODULE", card: `docs/tasks/${rootTask}.md`,
|
||||
behavior: "Authorize a source-backed ActionGroup reader and recovery module after P1139 Texture.", outputs: ["successor authorization", "module plan", "manifest", "status"],
|
||||
acceptance: ["P1139 evidence remains hash-bound", "ActionGroup has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"],
|
||||
commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true,
|
||||
};
|
||||
const moduleRecord = {
|
||||
id: moduleTask, phase: "1140", title: "verify_action_group_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId,
|
||||
implementationClass: "LOCAL_EXACT", parallelGroup: "module-action_group_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`,
|
||||
behavior: "Verify ActionGroup metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"],
|
||||
acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"],
|
||||
status: "in_progress", ready: true, queueMutationAllowed: true, module,
|
||||
};
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
|
||||
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize ActionGroup reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1140.mjs\`\n\n## 目标\n\n授权一个 source-backed ActionGroup reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionGroup)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-action-group-reader-production.spec.ts\`\n\n## 验收\n\n- P1139 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1140 manifests/status/receipt;回滚恢复 P1139 终态指针。\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1139 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify ActionGroup reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:action_group_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 ActionGroup 元数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionGroup)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-operator-anim.channels_group.blend\`\n- Production test: \`web/tests/e2e/wbv2-action-group-reader-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
82
tools/web/authorize-wbv2-p1141.mjs
Normal file
82
tools/web/authorize-wbv2-p1141.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const parentTask = "WBV2-P1140-001";
|
||||
const rootTask = "WBV2-P1141-000";
|
||||
const moduleTask = "WBV2-P1141-001";
|
||||
const moduleId = "ACTION_POSE_MARKERS_READER";
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/engine-client/WebEngineClient.ts",
|
||||
"web/tests/e2e/wbv2-action-pose-markers-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5359 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-action-pose-markers-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
|
||||
const parent = json(parentManifestPath);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
|
||||
const plan = json(planPath);
|
||||
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const module = {
|
||||
moduleId, sourceBacked: true, capabilities: ["CAP-ACTION-POSE-MARKERS-READER", "CAP-ACTION-POSE-MARKERS-RECOVERY", "CAP-ACTION-POSE-MARKERS-FAIL-CLOSED"], sourcePaths,
|
||||
reuseReceipts: [{ manifest: "tests/golden/WBV2-P1140-001/manifest.json", report: "tests/golden/WBV2-P1140-001/module-receipt.json" }],
|
||||
positive: ["ActionPoseMarkers Main reader exposes marker names and frames", "three Blender pose markers remain ordered", "save/reopen preserves marker metadata"],
|
||||
negative: ["malformed blend is rejected without Main mutation", "missing animation target is fail-closed", "non-finite marker frame is not synthesized"],
|
||||
recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic marker state"], commands, fullFamilyParity: false,
|
||||
};
|
||||
const rootRecord = { id: rootTask, phase: "1141", title: "authorize_action_pose_markers_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1141", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed ActionPoseMarkers reader and recovery module after P1140.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1140 evidence remains hash-bound", "ActionPoseMarkers has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1141", title: "verify_action_pose_markers_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-action_pose_markers_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify ActionPoseMarkers metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
|
||||
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize ActionPoseMarkers reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1141.mjs\`\n\n## 目标\n\n授权一个 source-backed ActionPoseMarkers reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionPoseMarkers)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-action-pose-markers-production.spec.ts\`\n\n## 验收\n\n- P1140 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1141 manifests/status/receipt;回滚恢复 P1140 终态指针。\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1140 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify ActionPoseMarkers reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:action_pose_markers_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 ActionPoseMarkers 元数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_action.cc\` (ActionPoseMarkers)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-action-pose-markers.blend\`\n- Production test: \`web/tests/e2e/wbv2-action-pose-markers-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
76
tools/web/authorize-wbv2-p1142.mjs
Normal file
76
tools/web/authorize-wbv2-p1142.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const parentTask = "WBV2-P1141-001";
|
||||
const rootTask = "WBV2-P1142-000";
|
||||
const moduleTask = "WBV2-P1142-001";
|
||||
const moduleId = "FCURVE_SAMPLE_READER";
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/engine-client/WebEngineClient.ts",
|
||||
"web/tests/e2e/wbv2-fcurve-sample-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5365 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-fcurve-sample-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
|
||||
const parent = json(parentManifestPath);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
|
||||
const plan = json(planPath);
|
||||
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FCURVE-SAMPLE-READER", "CAP-FCURVE-SAMPLE-RECOVERY", "CAP-FCURVE-SAMPLE-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1141-001/manifest.json", report: "tests/golden/WBV2-P1141-001/module-receipt.json" }], positive: ["FCurveSample Main reader exposes sampled source and values", "four native FPoint samples remain ordered", "save/reopen preserves sampled data"], negative: ["malformed blend is rejected without Main mutation", "missing sampled payload is fail-closed", "non-finite sampled value is not synthesized"], recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic sampled state"], commands, fullFamilyParity: false };
|
||||
const rootRecord = { id: rootTask, phase: "1142", title: "authorize_fcurve_sample_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1142", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed FCurveSample reader and recovery module after P1141.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1141 evidence remains hash-bound", "FCurveSample has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1142", title: "verify_fcurve_sample_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-fcurve_sample_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify FCurveSample data through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
|
||||
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize FCurveSample reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1142.mjs\`\n\n## 目标\n\n授权一个 source-backed FCurveSample reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveSample)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- P1141 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1142 manifests/status/receipt;回滚恢复 P1141 终态指针。\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1141 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify FCurveSample reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:fcurve_sample_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 FCurveSample sampled 数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveSample)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-action-fcurve-samples.blend\`\n- Production test: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
76
tools/web/authorize-wbv2-p1143.mjs
Normal file
76
tools/web/authorize-wbv2-p1143.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const parentTask = "WBV2-P1142-001";
|
||||
const rootTask = "WBV2-P1143-000";
|
||||
const moduleTask = "WBV2-P1143-001";
|
||||
const moduleId = "FCURVE_MODIFIER_READER";
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/engine-client/WebEngineClient.ts",
|
||||
"web/tests/e2e/wbv2-fcurve-modifier-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5368 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-fcurve-modifier-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
|
||||
const parent = json(parentManifestPath);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
|
||||
const plan = json(planPath);
|
||||
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-FCURVE-MODIFIER-READER", "CAP-FCURVE-MODIFIER-RECOVERY", "CAP-FCURVE-MODIFIER-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1142-001/manifest.json", report: "tests/golden/WBV2-P1142-001/module-receipt.json" }], positive: ["FCurveModifiers Main reader exposes native modifier stack", "FModifierCycles preserves type/name/flags/influence", "save/reopen preserves modifier metadata"], negative: ["malformed blend is rejected without Main mutation", "missing modifier payload is fail-closed", "unsupported modifier type is not synthesized"], recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic modifier state"], commands, fullFamilyParity: false };
|
||||
const rootRecord = { id: rootTask, phase: "1143", title: "authorize_fcurve_modifier_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1143", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed FCurve modifier reader and recovery module after P1142.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1142 evidence remains hash-bound", "FCurveModifiers has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1143", title: "verify_fcurve_modifier_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-fcurve_modifier_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify FCurve modifier metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
|
||||
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize FCurve modifier reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1143.mjs\`\n\n## 目标\n\n授权一个 source-backed FCurve modifier reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveModifiers/FModifier/FModifierCycles)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- P1142 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1143 manifests/status/receipt;回滚恢复 P1142 终态指针。\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1142 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify FCurve modifier reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:fcurve_modifier_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 FCurve modifier 栈在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (FCurveModifiers/FModifier/FModifierCycles)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-action-fcurve-modifier.blend\`\n- Production test: \`web/tests/e2e/wbv2-fcurve-sample-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
76
tools/web/authorize-wbv2-p1144.mjs
Normal file
76
tools/web/authorize-wbv2-p1144.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const abs = (file) => path.join(root, file);
|
||||
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
||||
const json = (file) => JSON.parse(read(file));
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(abs(file))).digest("hex");
|
||||
const writeJson = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), `${JSON.stringify(value, null, 2)}\n`); };
|
||||
const writeText = (file, value) => { fs.mkdirSync(path.dirname(abs(file)), { recursive: true }); fs.writeFileSync(abs(file), value); };
|
||||
const run = (entry, ...args) => execFileSync(process.execPath, [abs(entry), ...args], { cwd: root, stdio: "inherit" });
|
||||
|
||||
const parentTask = "WBV2-P1143-001";
|
||||
const rootTask = "WBV2-P1144-000";
|
||||
const moduleTask = "WBV2-P1144-001";
|
||||
const moduleId = "DRIVER_READER";
|
||||
const planPath = "tests/golden/WBV2-P1100-000/module-execution-plan.json";
|
||||
const parentManifestPath = `tests/golden/${parentTask}/manifest.json`;
|
||||
const dynamicPath = "tests/golden/WBV2/dynamic-inventory.json";
|
||||
const sourcePaths = [
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/app/src/workers/web-engine.worker.ts",
|
||||
"web/app/src/engine-client/WebEngineClient.ts",
|
||||
"web/tests/e2e/wbv2-driver-reader-production.spec.ts",
|
||||
"web/app/src/vendor/blender/web_engine.wasm",
|
||||
];
|
||||
const commands = [
|
||||
"source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8 && cp build_web_blender6/bin/web_engine.wasm web/app/src/vendor/blender/web_engine.wasm && cp build_web_blender6/bin/web_engine.wasm web/app/public/vendor/blender/web_engine.wasm",
|
||||
"npm --prefix web run typecheck",
|
||||
"npm --prefix web run build",
|
||||
"WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/wbv2-driver-reader-production.spec.ts",
|
||||
];
|
||||
const queuePath = "docs/EXECUTION_QUEUE.md";
|
||||
const queueTask = read(queuePath).match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
||||
assert.ok([parentTask, rootTask, moduleTask].includes(queueTask));
|
||||
const parent = json(parentManifestPath);
|
||||
assert.equal(parent.status, "done");
|
||||
assert.ok(parent.nextTask === null || parent.nextTask === rootTask);
|
||||
const plan = json(planPath);
|
||||
assert.ok(["COMPLETED_MODULE_EXECUTION", "ACTIVE_MODULE_EXECUTION_QUEUE"].includes(plan.status));
|
||||
const dynamic = json(dynamicPath);
|
||||
const dynamicSummary = { path: dynamicPath, sourceTreeSha256: dynamic.authority.sourceTreeSha256, unitCount: dynamic.summary.unitCount, planningEstimate: dynamic.policy.planningEstimate, hardTarget: dynamic.policy.hardTarget, verifiedUnits: dynamic.coverage.verifiedUnits, unverifiedUnits: dynamic.coverage.unverifiedUnits };
|
||||
const module = { moduleId, sourceBacked: true, capabilities: ["CAP-DRIVER-READER", "CAP-DRIVER-RECOVERY", "CAP-DRIVER-FAIL-CLOSED"], sourcePaths, reuseReceipts: [{ manifest: "tests/golden/WBV2-P1143-001/manifest.json", report: "tests/golden/WBV2-P1143-001/module-receipt.json" }], positive: ["AnimData driver list is read from Blender Main", "Driver preserves scripted expression and variables", "save/reopen preserves driver metadata"], negative: ["malformed blend is rejected without Main mutation", "missing driver target is fail-closed", "non-finite driver influence is not synthesized"], recovery: ["undo", "redo", "save/reopen", "worker termination and deterministic driver state"], commands, fullFamilyParity: false };
|
||||
const rootRecord = { id: rootTask, phase: "1144", title: "authorize_driver_reader_module", kind: "enabling", owner: "ANIMATION", dependsOn: [parentTask], parityId: null, implementationClass: "QUEUE_ONLY", parallelGroup: "module-root-1144", gate: "MODULE", card: `docs/tasks/${rootTask}.md`, behavior: "Authorize a source-backed Driver reader and recovery module after P1143.", outputs: ["successor authorization", "module plan", "manifest", "status"], acceptance: ["P1143 evidence remains hash-bound", "Driver has explicit Blender source mapping", "inventory remains source-derived", "one source-backed module is active"], commands: ["node tools/web/check-execution-control-plane.mjs"], status: "done", ready: false, queueMutationAllowed: true };
|
||||
const moduleRecord = { id: moduleTask, phase: "1144", title: "verify_driver_reader_module", kind: "module", owner: "ANIMATION", dependsOn: [rootTask], parityId: moduleId, implementationClass: "LOCAL_EXACT", parallelGroup: "module-driver_reader", gate: "MODULE", card: `docs/tasks/${moduleTask}.md`, behavior: "Verify Driver metadata through Blender Main reader, protocol and Chromium recovery.", outputs: ["module receipt", "negative matrix", "manifest", "status"], acceptance: ["source and protocol hashes are bound", "positive Main reader is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact", "full-family parity remains explicit"], status: "in_progress", ready: true, queueMutationAllowed: true, module };
|
||||
plan.status = "ACTIVE_MODULE_EXECUTION_QUEUE";
|
||||
if (!plan.modules.some((entry) => entry.id === moduleId)) plan.modules.push({ id: moduleId, task: moduleTask, owner: "ANIMATION", sourceBacked: true, capabilities: module.capabilities, reusedReceipts: module.reuseReceipts, fullFamilyParity: false });
|
||||
if (!plan.tasks.some((entry) => entry.id === rootTask)) plan.tasks.push(rootRecord);
|
||||
if (!plan.tasks.some((entry) => entry.id === moduleTask)) plan.tasks.push(moduleRecord);
|
||||
writeJson(planPath, plan);
|
||||
run("tools/web/generate-v2-task-index.mjs", "--plan-path", abs(planPath));
|
||||
parent.nextTask = rootTask;
|
||||
parent.continuation = { authorized: true, root: rootTask, scope: moduleId, priorModule: parentTask };
|
||||
parent.artifacts.plan.sha256 = sha(planPath);
|
||||
writeJson(parentManifestPath, parent);
|
||||
writeText(`docs/tasks/${rootTask}.md`, `# ${rootTask}: authorize Driver reader\n\n- task: ${rootTask}\n- parent: ${parentTask}\n- status: done\n- ownerFamily: ANIMATION\n- targetImplementationClass: QUEUE_ONLY\n- Implementation entrypoint: \`node tools/web/authorize-wbv2-p1144.mjs\`\n\n## 目标\n\n授权一个 source-backed Driver reader/recovery 模块。\n\n## 输入与范围\n\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (Driver)\n- Reader: \`blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp\`\n- Chromium: \`web/tests/e2e/wbv2-driver-reader-production.spec.ts\`\n\n## 验收\n\n- P1143 evidence stays hash-bound; one source-backed module active; malformed or hash-drift input remains \`in_progress\`.\n- \`node tools/web/check-execution-control-plane.mjs\`\n\n## 交付与回滚\n\n- 交付 P1144 manifests/status/receipt;回滚恢复 P1143 终态指针。\n`);
|
||||
writeText(`docs/status/${rootTask}.md`, `# ${rootTask} status\n\n- task: \`${rootTask}\`; parent: \`${parentTask}\`; status: \`done\`\n- operation: \`AUTHORIZE_SOURCE_BACKED_MODULE\`; module: \`${moduleId}\`\n- P1143 evidence reused; historical 6900-entry inventory remains immutable\n- dynamic source inventory: \`${dynamicSummary.unitCount}\` units, verified \`${dynamicSummary.verifiedUnits}\`, unverified \`${dynamicSummary.unverifiedUnits}\`, planning estimate \`${dynamicSummary.planningEstimate}\` (non-binding)\n- nextTask: \`${moduleTask}\`\n`);
|
||||
let updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${rootTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`${parentManifestPath}\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${rootTask}.md\`](tasks/${rootTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
run("tools/web/check-task-context.mjs", "--task", rootTask, "--write");
|
||||
const rootReportPath = `tests/golden/${rootTask}/successor-authorization-report.json`;
|
||||
const rootStatusPath = `docs/status/${rootTask}.md`;
|
||||
const rootContextPath = `tests/golden/${rootTask}/task-context.json`;
|
||||
writeJson(rootReportPath, { schemaVersion: 1, task: rootTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "PASS", parent: { task: parentTask, manifest: parentManifestPath, manifestSha256: sha(parentManifestPath) }, successor: { task: moduleTask, moduleId, sourceBacked: true, noRepeatedImplementation: true }, inventory: { ...plan.inventory, boundCaseDelta: 0 }, dynamicInventory: { ...dynamicSummary, boundUnitDelta: 0 }, nextTask: moduleTask });
|
||||
writeJson(`tests/golden/${rootTask}/manifest.json`, { schemaVersion: 1, task: rootTask, parentTask, operation: "AUTHORIZE_SOURCE_BACKED_MODULE", status: "done", enablingTask: true, parityStateChange: false, runtime: "MODULE_EXECUTION_QUEUE_BRIDGE", artifacts: { parentManifest: { path: parentManifestPath, sha256: sha(parentManifestPath) }, plan: { path: planPath, sha256: sha(planPath) }, report: { path: rootReportPath, sha256: sha(rootReportPath) }, status: { path: rootStatusPath, sha256: sha(rootStatusPath) }, taskContext: { path: rootContextPath, sha256: sha(rootContextPath) } }, assertions: { sourceBackedSuccessor: true, priorEvidencePreserved: true, inventoryTotal: 6900, effectiveCompleted: 290, effectivePending: 6609, boundCaseDelta: 0, dynamicInventory: dynamicSummary, fullHistoricalReproduction: false }, nextTask: moduleTask });
|
||||
updatedQueue = read(queuePath).replace(/\| 当前任务 \| `[^`]+` \|/u, `| 当前任务 | \`${moduleTask}\` |`).replace(/\| parent manifest \| `[^`]+` \|/u, `| parent manifest | \`tests/golden/${rootTask}/manifest.json\` |`).replace(/\| 任务卡 \| \[[^\]]+\]\([^\)]+\) \|/u, `| 任务卡 | [\`tasks/${moduleTask}.md\`](tasks/${moduleTask}.md) |`);
|
||||
writeText(queuePath, updatedQueue);
|
||||
writeText(`docs/tasks/${moduleTask}.md`, `# ${moduleTask}: verify Driver reader\n\n- task: ${moduleTask}\n- parent: ${rootTask}\n- status: in_progress\n- gap: workflow:driver_reader\n- ownerFamily: ANIMATION\n- targetImplementationClass: LOCAL_EXACT\n- moduleId: ${moduleId}\n- Implementation entrypoint: \`node tools/web/run-wbv2-module-task.mjs --task ${moduleTask} --write\`\n\n## 目标\n\n验证 Blender Main 的 Driver/AnimData 元数据在 Chromium recovery/save-reopen 中保持一致。\n\n## 输入与范围\n\n${sourcePaths.map((file) => `- \`${file}\``).join("\n")}\n- RNA: \`blender-5.2.0/source/blender/makesrna/intern/rna_fcurve.cc\` (Driver/AnimDataDrivers)\n- Fixture: \`tests/files/web/generated/M16-GAP-00161-driver.blend\`\n- Production test: \`web/tests/e2e/wbv2-driver-reader-production.spec.ts\`\n\n## 验收\n\n- Positive: ${module.positive.join(", ")}\n- Negative: ${module.negative.join(", ")}\n- Recovery: ${module.recovery.join(", ")}\n- Commands:\n${commands.map((command) => `- \`${command}\``).join("\n")}\n- JS simulation is forbidden; all behavior must come from Blender Main/WASM.\n\n## 交付与回滚\n\n- Receipt: \`tests/golden/${moduleTask}/module-receipt.json\`; Manifest: \`tests/golden/${moduleTask}/manifest.json\`; Status: \`docs/status/${moduleTask}.md\`.\n`);
|
||||
run("tools/web/check-task-context.mjs", "--task", moduleTask, "--write");
|
||||
run("tools/web/check-execution-control-plane.mjs");
|
||||
process.stdout.write(`wbv2-successor-authorized root=${rootTask} next=${moduleTask} module=${moduleId}\n`);
|
||||
6
tools/web/blender-web-bwrap.apparmor
Normal file
6
tools/web/blender-web-bwrap.apparmor
Normal file
@@ -0,0 +1,6 @@
|
||||
abi <abi/4.0>,
|
||||
include <tunables/global>
|
||||
|
||||
profile blender_web_bwrap /usr/bin/bwrap flags=(unconfined) {
|
||||
userns,
|
||||
}
|
||||
68
tools/web/build-desktop-workflow-runtime.py
Normal file
68
tools/web/build-desktop-workflow-runtime.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Produce the changed WASM and web bundle without running acceptance commands."""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def publish(destination, data):
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_name(destination.name + f'.tmp-{os.getpid()}')
|
||||
temporary.write_bytes(data)
|
||||
temporary.replace(destination)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', default='tests/golden/WBV2-P1424-001/desktop-workflow-build')
|
||||
args = parser.parse_args()
|
||||
output = ROOT / args.output / datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
report = {'schemaVersion': 1, 'task': 'WBV2-P1424-001', 'status': 'BUILDING',
|
||||
'validation': 'NOT_RUN', 'acceptance': 'NOT_CLAIMED', 'steps': []}
|
||||
try:
|
||||
commands = [
|
||||
*(([('cpython', ['python3', 'tools/web/build-webengine-python.py'], ROOT)]) if not (ROOT / 'build_web_python313/build/libpython3.13.a').exists() else []),
|
||||
('wasm', ['bash', '-c', 'source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine --parallel 4'], ROOT),
|
||||
]
|
||||
for name, command, directory in commands:
|
||||
with (output / (name + '.log')).open('w') as log:
|
||||
completed = subprocess.run(command, cwd=directory, stdout=log, stderr=subprocess.STDOUT)
|
||||
report['steps'].append({'name': name, 'exitCode': completed.returncode})
|
||||
if completed.returncode: raise RuntimeError(name + ' build failed; see ' + str(output))
|
||||
build = ROOT / 'build_web_blender6/bin'
|
||||
manifest_path = ROOT / 'web/app/public/wasm-module-manifest.json'
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
report['artifacts'] = {}
|
||||
for name in ('web_engine.js', 'web_engine.wasm'):
|
||||
data = (build / name).read_bytes()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
for directory in ('web/app/src/vendor/blender', 'web/app/public/vendor/blender'):
|
||||
publish(ROOT / directory / name, data)
|
||||
for module in manifest['modules']:
|
||||
for resource in module.get('resources', {}).values():
|
||||
if resource.get('url') == '/vendor/blender/' + name: resource['sha256'] = digest
|
||||
report['artifacts'][name] = {'sha256': digest, 'bytes': len(data)}
|
||||
publish(manifest_path, (json.dumps(manifest, indent=2) + '\n').encode())
|
||||
with (output / 'web.log').open('w') as log:
|
||||
completed = subprocess.run(['node', 'tools/web/incremental-web-build.mjs'], cwd=ROOT,
|
||||
env=dict(os.environ, WBV2_SKIP_TYPECHECK='1'), stdout=log, stderr=subprocess.STDOUT)
|
||||
report['steps'].append({'name': 'web', 'exitCode': completed.returncode})
|
||||
if completed.returncode: raise RuntimeError('web build failed; see ' + str(output))
|
||||
report['status'] = 'BUILT_UNVERIFIED'
|
||||
except BaseException as error:
|
||||
report.update(status='BUILD_INCOMPLETE', error=str(error))
|
||||
raise
|
||||
finally:
|
||||
publish(output / 'build.json', (json.dumps(report, indent=2) + '\n').encode())
|
||||
print(json.dumps({'directory': str(output), **report}), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
126
tools/web/build-main-desktop-candidate.py
Normal file
126
tools/web/build-main-desktop-candidate.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assemble an unpublished desktop application; never run acceptance or typecheck."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def active_native_builder(native):
|
||||
for process in Path('/proc').glob('[0-9]*'):
|
||||
try:
|
||||
if (process / 'cwd').resolve() == native.parent and (process / 'comm').read_text().strip() in {'ninja', 'make', 'gmake', 'cmake', 'wasm-ld', 'wasm-opt'}:
|
||||
return process.name
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
def write_json(path, value):
|
||||
path.write_text(json.dumps(value, indent=2) + "\n")
|
||||
|
||||
|
||||
def share_candidate_assets(resources, application):
|
||||
cache = resources.parent.parent / ".candidate-assets"
|
||||
cache.mkdir(exist_ok=True)
|
||||
for source in resources.rglob("*"):
|
||||
target = application / source.relative_to(resources)
|
||||
if source.is_symlink() or target.is_symlink() or not source.is_file() or not target.is_file():
|
||||
continue
|
||||
if (source.stat().st_mode & 0o777) != (target.stat().st_mode & 0o777):
|
||||
continue
|
||||
with source.open("rb") as stream:
|
||||
digest = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
with target.open("rb") as stream:
|
||||
if hashlib.file_digest(stream, "sha256").hexdigest() != digest:
|
||||
continue
|
||||
anchor = cache / f"{digest}-{source.stat().st_mode & 0o777:o}"
|
||||
if not anchor.exists():
|
||||
os.link(source, anchor)
|
||||
with anchor.open("rb") as stream:
|
||||
if anchor.is_symlink() or hashlib.file_digest(stream, "sha256").hexdigest() != digest:
|
||||
raise RuntimeError(f"Candidate asset cache differs: {anchor.name}")
|
||||
for path in (source, target):
|
||||
if os.path.samefile(anchor, path):
|
||||
continue
|
||||
temporary = path.with_name(f".{path.name}.share-{os.getpid()}")
|
||||
os.link(anchor, temporary)
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", required=True, help="New local candidate directory")
|
||||
parser.add_argument("--native-bin", default="build_web_blender6/bin",
|
||||
help="Directory containing web_engine.js and web_engine.wasm, or either file")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
output = (root / args.output).resolve()
|
||||
native = (root / args.native_bin).resolve()
|
||||
if native.is_file() and native.name in {"web_engine.js", "web_engine.wasm"}:
|
||||
native = native.parent
|
||||
# Reject a wrong input before copying the public tree or leaving a partial
|
||||
# candidate. This is packaging input handling, not runtime verification.
|
||||
missing = [name for name in ("web_engine.js", "web_engine.wasm")
|
||||
if not (native / name).is_file()]
|
||||
if missing:
|
||||
parser.error(f"Native directory {native} is missing: {', '.join(missing)}")
|
||||
# A fresh destination preserves both previous candidates and published assets.
|
||||
if builder := active_native_builder(native):
|
||||
parser.error(f"Native build PID {builder} is still active; retain pending edits and package after it exits")
|
||||
native_stamps = {name: (native / name).stat() for name in ("web_engine.js", "web_engine.wasm")}
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
resources = output / "public"
|
||||
subprocess.run(["cp", "-a", "--reflink=auto", str(root / "web/app/public"),
|
||||
str(resources)], check=True)
|
||||
vendor = resources / "vendor/blender"
|
||||
hashes = {}
|
||||
for name in ("web_engine.js", "web_engine.wasm"):
|
||||
shutil.copy2(native / name, vendor / name)
|
||||
with (vendor / name).open("rb") as stream:
|
||||
hashes[name] = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
# Startup scripts, fonts, color management and Python's library are embedded
|
||||
# by the native target. Retain the source startup resources with the candidate
|
||||
# as well, without replacing any source or published runtime files.
|
||||
shutil.copytree(root / "blender-5.2.0/scripts/startup", output / "scripts/startup")
|
||||
shutil.copytree(root / "blender-5.2.0/scripts/presets", output / "scripts/presets")
|
||||
module_path = resources / "wasm-module-manifest.json"
|
||||
modules = json.loads(module_path.read_text())
|
||||
for module in modules["modules"]:
|
||||
resource = module["resources"].get("single")
|
||||
if resource and resource["fileName"] == "web_engine.wasm":
|
||||
resource.update(url="/vendor/blender/web_engine.wasm", sha256=hashes["web_engine.wasm"])
|
||||
write_json(module_path, modules)
|
||||
engine_path = resources / "engine-manifest.json"
|
||||
engine = json.loads(engine_path.read_text())
|
||||
for variant in engine["variants"]:
|
||||
if variant["id"] == "single":
|
||||
for kind, name in (("js", "web_engine.js"), ("wasm", "web_engine.wasm")):
|
||||
variant["resources"][kind].update(url=f"/vendor/blender/{name}", sha256=hashes[name])
|
||||
write_json(engine_path, engine)
|
||||
env = dict(os.environ, WEB_DESKTOP_CANDIDATE_PUBLIC=str(resources),
|
||||
WEB_DESKTOP_CANDIDATE_FACTORY=str(vendor / "web_engine.js"))
|
||||
# Vite emits frontend and Worker from current source; no test/typecheck wrapper.
|
||||
subprocess.run(["node", "node_modules/vite/bin/vite.js", "build", "--config",
|
||||
"app/vite.config.ts", "--outDir", str(output / "app")],
|
||||
cwd=root / "web", env=env, check=True)
|
||||
share_candidate_assets(resources, output / "app")
|
||||
if active_native_builder(native):
|
||||
parser.error("Native build started during packaging; no candidate manifest published; retain pending edits")
|
||||
for name, before in native_stamps.items():
|
||||
after = (native / name).stat()
|
||||
if (before.st_ino, before.st_size, before.st_mtime_ns) != (after.st_ino, after.st_size, after.st_mtime_ns):
|
||||
parser.error(f"Native input changed during packaging: {name}; no candidate manifest published")
|
||||
write_json(output / "candidate.json", {
|
||||
"stageId": "main-desktop", "status": "implemented_unverified",
|
||||
"scopeComplete": False, "nativeUIComplete": False,
|
||||
"entry": "app/index.html", "nativeBin": str(native), "sha256": hashes,
|
||||
"runtimeURL": "/vendor/blender/web_engine.wasm",
|
||||
"startupResources": "embedded in WASM; source copy in scripts/",
|
||||
"deployment": "not_performed", "validation": "not_run",
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
81
tools/web/build-native-desktop-adapters.py
Normal file
81
tools/web/build-native-desktop-adapters.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build WM/RNA objects and link an isolated candidate from existing native libraries."""
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
if '--configured' not in sys.argv:
|
||||
result=subprocess.run(['bash','-c','source tools/web/native-build-env.sh && exec "$1" "$2" --configured',
|
||||
'_',sys.executable,str(Path(__file__).resolve())],cwd=root)
|
||||
sys.exit(result.returncode)
|
||||
build=root/'build_blender_native'
|
||||
candidate=build/'desktop-adapter'
|
||||
directory=root/'tests/golden/WBV2-P1424-001/native-desktop-adapter-build'/datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
directory.mkdir(parents=True)
|
||||
for name in ('objects','lib','bin','generated'):(candidate/name).mkdir(parents=True,exist_ok=True)
|
||||
report={'schemaVersion':1,'status':'BUILDING','validation':'NOT_RUN','steps':[],'strategy':'REUSE_EXISTING_LIBRARIES_NO_DNA_LAYOUT_CHANGE'}
|
||||
|
||||
def run(name,command,cwd=build):
|
||||
with (directory/(name+'.log')).open('w') as log:
|
||||
result=subprocess.run(command,cwd=cwd,env=dict(os.environ,WBV2_SKIP_TYPECHECK='1'),stdout=log,stderr=subprocess.STDOUT)
|
||||
report['steps'].append({'name':name,'exitCode':result.returncode})
|
||||
if result.returncode:raise RuntimeError(name+' artifact build failed')
|
||||
|
||||
def digest(file):
|
||||
sha=hashlib.sha256()
|
||||
with file.open('rb') as stream:
|
||||
for data in iter(lambda:stream.read(1024*1024),b''):sha.update(data)
|
||||
return {'sha256':sha.hexdigest(),'bytes':file.stat().st_size}
|
||||
|
||||
def link_command(target):
|
||||
output=subprocess.check_output(['ninja','-t','commands',target],cwd=build,text=True)
|
||||
tokens=shlex.split(output.strip().splitlines()[-1])
|
||||
if tokens[:2]==[':','&&']:tokens=tokens[2:]
|
||||
if '&&' in tokens:tokens=tokens[:tokens.index('&&')]
|
||||
if not tokens or 'g++' not in Path(tokens[0]).name or '-o' not in tokens:raise RuntimeError('NATIVE_LINK_RULE_UNSUPPORTED')
|
||||
return tokens
|
||||
|
||||
try:
|
||||
rules=['CXX_COMPILER__makesrna_unscanned_Release','CXX_COMPILER__bf_rna_unscanned_Release','CXX_COMPILER__bf_windowmanager_unscanned_Release']
|
||||
commands=json.loads(subprocess.check_output(['ninja','-t','compdb',*rules],cwd=build,text=True))
|
||||
def compile_object(suffix,name,source=None):
|
||||
entry=next(item for item in commands if item['file'].endswith(suffix))
|
||||
args=shlex.split(entry['command']);destination=candidate/'objects'/name
|
||||
for flag in ('-o','-MT','-MF'):
|
||||
if flag in args:args[args.index(flag)+1]=str(destination)+('.d' if flag=='-MF' else '')
|
||||
if source:args[args.index('-c')+1]=str(source)
|
||||
args.insert(1,'-I'+str(candidate/'generated'))
|
||||
run(name,args)
|
||||
return destination,entry['output']
|
||||
generator_object,old_generator_object=compile_object('/rna_wm_api.cc','rna_wm_api.cc.o')
|
||||
args=link_command('bin/makesrna');args=[str(generator_object) if value==old_generator_object else value for value in args]
|
||||
generator=candidate/'bin/makesrna';args[args.index('-o')+1]=str(generator);run('link-makesrna',args)
|
||||
run('generate-rna',[str(generator),str(root/'blender-5.2.0/source/blender/makesdna')+'/',str(candidate/'generated')+'/',str(candidate/'generated')+'/'])
|
||||
wm_object,_=compile_object('/wm_window.cc','wm_window.cc.o')
|
||||
rna_object,_=compile_object('/rna_wm_gen.cc','rna_wm_gen.cc.o',candidate/'generated/rna_wm_gen.cc')
|
||||
replacements={};report['reusedArchives']={}
|
||||
for name,object_file in [('libbf_windowmanager.a',wm_object),('libbf_rna.a',rna_object)]:
|
||||
archive=build/'lib'/name;report['reusedArchives'][name]=digest(archive)
|
||||
target=candidate/'lib'/name;shutil.copyfile(archive,target)
|
||||
run('archive-'+name,['ar','rcs',str(target),str(object_file)])
|
||||
replacements['lib/'+name]=str(target)
|
||||
args=link_command('bin/blender');args=[replacements.get(value,value) for value in args]
|
||||
pending=candidate/'bin/blender.pending';args[args.index('-o')+1]=str(pending)
|
||||
run('link-blender',args)
|
||||
runtime=candidate/'bin/5.2'
|
||||
if not runtime.exists():runtime.symlink_to(build/'bin/5.2',target_is_directory=True)
|
||||
pending.replace(candidate/'bin/blender')
|
||||
run('web',['node','tools/web/incremental-web-build.mjs'],root)
|
||||
report.update(status='BUILT_UNVERIFIED',nativeBinary=digest(candidate/'bin/blender'),binary=str(candidate/'bin/blender'))
|
||||
except BaseException as error:
|
||||
report.update(status='BUILD_INCOMPLETE',error=str(error));raise
|
||||
finally:
|
||||
(directory/'build.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
print(json.dumps({'directory':str(directory),**report}),flush=True)
|
||||
45
tools/web/build-runtime-evidence-plan.py
Executable file
45
tools/web/build-runtime-evidence-plan.py
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Produce the bounded evidence capture plan for WBV2-P1424-001.
|
||||
|
||||
This planner is intentionally side-effect free: implementation phase records the
|
||||
exact Chromium/native workflows to run later, without claiming they passed.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
TASK = "WBV2-P1424-001"
|
||||
CONTRACTS = Path("tests/golden") / TASK / "behavior-contracts.json"
|
||||
OUT = Path("tests/golden") / TASK / "runtime-evidence-plan.json"
|
||||
|
||||
def main():
|
||||
data = json.loads(CONTRACTS.read_text())
|
||||
plan = {
|
||||
"schemaVersion": 1,
|
||||
"task": TASK,
|
||||
"status": "PENDING_USER_RUNTIME_VALIDATION",
|
||||
"runner": "Chromium only",
|
||||
"sourceContracts": str(CONTRACTS),
|
||||
"steps": [],
|
||||
}
|
||||
for contract in data["contracts"]:
|
||||
module = contract["module"]
|
||||
plan["steps"].append({
|
||||
"module": module,
|
||||
"positive": contract["positive"],
|
||||
"negative": contract["negative"],
|
||||
"recovery": contract["recovery"],
|
||||
"requiredArtifacts": [
|
||||
f"{module.lower()}-before.blend",
|
||||
f"{module.lower()}-after.blend",
|
||||
f"{module.lower()}-negative.blend",
|
||||
f"{module.lower()}-undo.blend",
|
||||
f"{module.lower()}-redo.blend",
|
||||
f"{module.lower()}-reopened.blend",
|
||||
f"{module.lower()}-chromium.json",
|
||||
],
|
||||
"result": "NOT_RUN",
|
||||
})
|
||||
OUT.write_text(json.dumps(plan, indent=2, ensure_ascii=False) + "\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
59
tools/web/build-webengine-alembic.py
Normal file
59
tools/web/build-webengine-alembic.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Blender's pinned Alembic for the existing Web CacheFile reader."""
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = ROOT / "build_web_alembic"
|
||||
OCIO = ROOT / "build_web_ocio/install"
|
||||
|
||||
|
||||
def main():
|
||||
versions = (ROOT / "blender-5.2.0/build_files/build_environment/cmake/versions.cmake").read_text()
|
||||
version = re.search(r"set\(ALEMBIC_VERSION ([^)]+)\)", versions)[1]
|
||||
expected = re.search(r"set\(ALEMBIC_HASH ([^)]+)\)", versions)[1]
|
||||
algorithm = re.search(r"set\(ALEMBIC_HASH_TYPE ([^)]+)\)", versions)[1].lower()
|
||||
if not (OCIO / "lib/cmake/Imath/ImathConfig.cmake").is_file():
|
||||
raise SystemExit("Reuse/build the Web OCIO Imath installation first")
|
||||
BASE.mkdir(exist_ok=True)
|
||||
source = BASE / ("alembic-" + version)
|
||||
if not (source / "CMakeLists.txt").is_file():
|
||||
archive = BASE / ("alembic-" + version + ".tar.gz")
|
||||
if not archive.is_file():
|
||||
with urllib.request.urlopen(
|
||||
"https://codeload.github.com/alembic/alembic/tar.gz/" + version,
|
||||
timeout=60,
|
||||
) as response:
|
||||
data = response.read()
|
||||
if hashlib.new(algorithm, data).hexdigest() != expected:
|
||||
raise SystemExit("Alembic download differs from Blender's pinned source hash")
|
||||
archive.write_bytes(data)
|
||||
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
|
||||
raise SystemExit("Alembic archive differs from Blender's pinned source hash")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(BASE, filter="data")
|
||||
toolchain = ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
|
||||
subprocess.run([
|
||||
"cmake", "-S", str(source), "-B", str(BASE / "build"), "-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=" + str(toolchain),
|
||||
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
|
||||
"-DCMAKE_INSTALL_PREFIX=" + str(BASE / "install"),
|
||||
"-DCMAKE_PREFIX_PATH=" + str(OCIO), "-DCMAKE_FIND_ROOT_PATH=" + str(OCIO),
|
||||
"-DImath_DIR=" + str(OCIO / "lib/cmake/Imath"),
|
||||
"-DCMAKE_CXX_FLAGS=-fexceptions", "-DALEMBIC_SHARED_LIBS=OFF",
|
||||
"-DUSE_HDF5=OFF", "-DUSE_TESTS=OFF", "-DUSE_BINARIES=OFF",
|
||||
"-DUSE_EXAMPLES=OFF", "-DUSE_PYALEMBIC=OFF", "-DUSE_MAYA=OFF",
|
||||
"-DUSE_ARNOLD=OFF", "-DUSE_PRMAN=OFF",
|
||||
], cwd=ROOT, check=True)
|
||||
subprocess.run(["cmake", "--build", str(BASE / "build"), "--target", "install",
|
||||
"--parallel", "8"], cwd=ROOT, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
89
tools/web/build-webengine-ffmpeg.py
Normal file
89
tools/web/build-webengine-ffmpeg.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Blender-pinned FFmpeg audio libraries for the embedded Audaspace plugin."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
VERSION = "8.1"
|
||||
SHA256 = "c07039598df7d64d3c8b42c4e25b1959fc908621c6f6c2946881133f3b27eda2"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path)
|
||||
parser.add_argument("--jobs", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
base = ROOT / "build_web_ffmpeg"
|
||||
base.mkdir(exist_ok=True)
|
||||
source = args.source.resolve() if args.source else base / f"ffmpeg-{VERSION}"
|
||||
if not (source / "configure").is_file():
|
||||
if args.source:
|
||||
raise FileNotFoundError(source)
|
||||
filename = f"ffmpeg-{VERSION}.tar.bz2"
|
||||
caches = [base / filename, Path.home() / "working/packages" / filename,
|
||||
Path.home() / "working/build_oiio_deps/downloads" / filename]
|
||||
archive = next((path for path in caches if path.is_file()), caches[0])
|
||||
if not archive.is_file():
|
||||
partial = archive.with_suffix(".partial")
|
||||
with urllib.request.urlopen(f"https://ffmpeg.org/releases/{filename}", timeout=60) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
if hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256:
|
||||
raise RuntimeError("Blender-pinned FFmpeg archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
if (source / "RELEASE").read_text().strip() != VERSION:
|
||||
raise RuntimeError("Expected Blender-pinned FFmpeg " + VERSION)
|
||||
prefix = base / "install"
|
||||
build = base / "build"
|
||||
build.mkdir(exist_ok=True)
|
||||
em = ROOT / ".emsdk/upstream/emscripten"
|
||||
codecs = ROOT / "build_web_sndfile/install"
|
||||
env = dict(os.environ)
|
||||
env["PATH"] = str(em) + os.pathsep + env["PATH"]
|
||||
env["PKG_CONFIG_LIBDIR"] = str(codecs / "lib/pkgconfig")
|
||||
env["PKG_CONFIG_PATH"] = ""
|
||||
command = [str(source / "configure"), f"--prefix={prefix}",
|
||||
"--target-os=none", "--arch=wasm32", "--enable-cross-compile",
|
||||
"--cc=" + str(em / "emcc"), "--cxx=" + str(em / "em++"),
|
||||
"--ar=" + str(em / "emar"), "--ranlib=" + str(em / "emranlib"),
|
||||
"--nm=" + str(em.parent / "bin/llvm-nm"),
|
||||
f"--extra-cflags=-O2 -fPIC -I{codecs / 'include'}",
|
||||
f"--extra-ldflags=-L{codecs / 'lib'}", "--disable-asm", "--disable-x86asm",
|
||||
"--disable-pthreads", "--disable-w32threads", "--disable-os2threads",
|
||||
"--disable-network", "--disable-autodetect", "--disable-everything",
|
||||
"--disable-programs", "--disable-doc", "--disable-debug",
|
||||
"--disable-avdevice", "--disable-avfilter", "--disable-swscale",
|
||||
"--disable-shared", "--enable-static", "--enable-small",
|
||||
"--enable-protocol=file", "--enable-libmp3lame", "--enable-libopus",
|
||||
"--enable-libvorbis", "--pkg-config-flags=--static",
|
||||
"--enable-encoder=aac,ac3,mp2,flac,libmp3lame,libopus,libvorbis,pcm_u8,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_f64le",
|
||||
"--enable-decoder=aac,ac3,mp2,mp3,flac,opus,vorbis,pcm_u8,pcm_s16le,pcm_s24le,pcm_s32le,pcm_f32le,pcm_f64le",
|
||||
"--enable-parser=aac,ac3,mpegaudio,flac,opus,vorbis",
|
||||
"--enable-muxer=adts,ac3,mp2,mp3,flac,matroska,ogg,wav",
|
||||
"--enable-demuxer=aac,ac3,mp3,flac,matroska,ogg,wav,mov",
|
||||
"--enable-bsf=aac_adtstoasc,extract_extradata"]
|
||||
settings = json.dumps([command, env["PKG_CONFIG_LIBDIR"]])
|
||||
stamp = build / "web-configure.json"
|
||||
if not stamp.is_file() or stamp.read_text() != settings:
|
||||
subprocess.run(command, cwd=build, env=env, check=True)
|
||||
stamp.write_text(settings)
|
||||
subprocess.run(["make", f"-j{args.jobs}", "install"], cwd=build, env=env, check=True)
|
||||
artifacts = {str(path.relative_to(ROOT)): hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
for path in sorted((prefix / "lib").glob("*.a"))}
|
||||
(base / "build-manifest.json").write_text(json.dumps({
|
||||
"version": VERSION, "source": str(source), "upstreamArchiveSha256": SHA256,
|
||||
"configure": command, "artifacts": artifacts,
|
||||
"validation": "NOT_RUN", "acceptance": "NOT_CLAIMED"}, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
89
tools/web/build-webengine-numpy.py
Normal file
89
tools/web/build-webengine-numpy.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build upstream NumPy for the existing static CPython/WASM runtime."""
|
||||
import hashlib, json, os, pathlib, subprocess, tarfile, urllib.request, zipfile
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
BASE = ROOT / 'build_web_numpy'
|
||||
SOURCE = BASE / 'numpy-2.3.4'
|
||||
BUILD = BASE / 'build'
|
||||
PYTHON = ROOT / '.deps/native/python/bin/python3.13'
|
||||
BASE.mkdir(exist_ok=True)
|
||||
archive = BASE / 'numpy-2.3.4.tar.gz'
|
||||
if not archive.exists():
|
||||
urllib.request.urlretrieve('https://github.com/numpy/numpy/releases/download/v2.3.4/numpy-2.3.4.tar.gz', archive)
|
||||
if hashlib.sha256(archive.read_bytes()).hexdigest() != 'a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a':
|
||||
raise RuntimeError('Pinned NumPy source hash mismatch')
|
||||
if not SOURCE.exists():
|
||||
with tarfile.open(archive) as package: package.extractall(BASE, filter='data')
|
||||
import re
|
||||
# Shared modules isolate legacy random's 32-bit ABI; static modules need namespacing.
|
||||
random_meson = SOURCE / 'numpy/random/meson.build'
|
||||
symbols = sorted(set(re.findall(r'\b(random_[A-Za-z0-9_]+)\s*\(', (SOURCE / 'numpy/_core/include/numpy/random/distributions.h').read_text())))
|
||||
symbols.append('random_geometric_inversion') # Exported helper absent from distributions.h.
|
||||
legacy_flags = "['-DNP_RANDOM_LEGACY=1'" + ''.join(f", '-D{name}=web_numpy_legacy_{name}'" for name in symbols) + ']'
|
||||
meson_source = re.sub(r"\['-DNP_RANDOM_LEGACY=1'(?:, '-Drandom_\w+=web_numpy_legacy_random_\w+')*\]", "['-DNP_RANDOM_LEGACY=1']", random_meson.read_text())
|
||||
if "['-DNP_RANDOM_LEGACY=1']" in meson_source: random_meson.write_text(meson_source.replace("['-DNP_RANDOM_LEGACY=1']", legacy_flags))
|
||||
elif legacy_flags not in meson_source: raise RuntimeError('Unexpected NumPy legacy random build declaration')
|
||||
env = dict(os.environ)
|
||||
env['PATH'] = str(PYTHON.parent) + ':' + env['PATH']
|
||||
env['LD_LIBRARY_PATH'] = str(ROOT / '.deps/native/python/lib') + ':' + env.get('LD_LIBRARY_PATH', '')
|
||||
pkgconfig = BASE / 'pkgconfig'
|
||||
pkgconfig.mkdir(exist_ok=True)
|
||||
env['PKG_CONFIG_LIBDIR'] = str(pkgconfig)
|
||||
env['PKG_CONFIG_PATH'] = ''
|
||||
(pkgconfig / 'python-3.13.pc').write_text(f'Name: Python\nDescription: WebEngine CPython\nVersion: 3.13\nLibs:\nCflags: -I{ROOT}/build_web_python313/build -I{ROOT}/build_web_python313/source/Include\n')
|
||||
cross = BASE / 'emscripten.cross'
|
||||
original = (SOURCE / 'tools/ci/emscripten/emscripten.meson.cross').read_text()
|
||||
original = original.replace('[properties]\n', f"[properties]\npkg_config_libdir = '{pkgconfig}'\n")
|
||||
binaries = f"c = 'emcc'\ncpp = 'em++'\nar = 'emar'\npython = '{PYTHON}'\ncython = '{PYTHON.parent}/cython'\n"
|
||||
cross.write_text(original.replace('[binaries]\n', '[binaries]\n' + binaries) + "\n[built-in options]\nc_args = ['-fexceptions', '-DPY_CALL_TRAMPOLINE=1']\ncpp_args = ['-fexceptions', '-DPY_CALL_TRAMPOLINE=1']\n")
|
||||
def run(name, command, **kwargs):
|
||||
with (BASE / (name + '.log')).open('w') as log:
|
||||
subprocess.run(command, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, check=True, **kwargs)
|
||||
meson = [str(PYTHON), str(SOURCE / 'vendored-meson/meson/meson.py')]
|
||||
if not (BUILD / 'build.ninja').exists():
|
||||
run('configure', meson + ['setup', str(BUILD), str(SOURCE), '--cross-file=' + str(cross),
|
||||
'--buildtype=release', '-Dblas=none', '-Dlapack=none', '-Dcpu-baseline=none', '-Dcpu-dispatch=none'])
|
||||
else:
|
||||
run('reconfigure', meson + ['setup', '--reconfigure', str(BUILD), str(SOURCE)])
|
||||
commands = json.loads((BUILD / 'compile_commands.json').read_text())
|
||||
python_commands = [entry['command'] for entry in commands if '.so.p/' in entry['output'] and entry['output'].endswith('.o')]
|
||||
if not python_commands or any(f'-I{ROOT}/build_web_python313/build ' not in command for command in python_commands):
|
||||
raise RuntimeError('NumPy extension compilation must use target CPython pyconfig.h')
|
||||
plan = json.loads((BUILD / 'meson-info/intro-install_plan.json').read_text())
|
||||
modules = [pathlib.Path(p).relative_to(BUILD) for p, v in plan['targets'].items() if v['tag'] == 'runtime']
|
||||
inputs = set()
|
||||
for module in modules:
|
||||
query = subprocess.check_output(['ninja', '-C', str(BUILD), '-t', 'query', str(module)], env=env, text=True)
|
||||
inputs.update(line.strip().lstrip('| ') for line in query.split(' outputs:')[0].splitlines() if line.strip().endswith(('.o', '.a')))
|
||||
if len(modules) != 13 or not inputs:
|
||||
raise RuntimeError('Unexpected NumPy runtime module graph; inspect upstream install plan')
|
||||
(BUILD / 'webengine-static.ninja').write_text('include build.ninja\nbuild webengine_numpy_objects: phony ' + ' '.join(sorted(inputs)) + '\n')
|
||||
run('objects', ['ninja', '-C', str(BUILD), '-f', 'webengine-static.ninja', '-j4', 'webengine_numpy_objects'])
|
||||
library = BASE / 'libnumpy.a'
|
||||
mri = 'CREATE ' + str(library) + '\n'
|
||||
mri += ''.join(('ADDLIB ' if name.endswith('.a') else 'ADDMOD ') + str(BUILD / name) + '\n' for name in sorted(inputs))
|
||||
run('archive', ['emar', '-M'], input=mri + 'SAVE\nEND\n', text=True)
|
||||
files = {}
|
||||
for kind, entries in plan.items():
|
||||
if kind == 'targets': continue
|
||||
for name, entry in entries.items():
|
||||
if entry['tag'] != 'python-runtime': continue
|
||||
source = pathlib.Path(name)
|
||||
destination = entry['destination'].split('}/', 1)[1]
|
||||
if source.is_file(): files[destination] = source
|
||||
else:
|
||||
for file in sorted(source.rglob('*')):
|
||||
relative = file.relative_to(source)
|
||||
if not file.is_file() or any(part in entry.get('exclude_dirs', []) + ['__pycache__'] for part in relative.parts): continue
|
||||
if relative.as_posix() not in entry.get('exclude_files', []): files[destination + '/' + relative.as_posix()] = file
|
||||
with zipfile.ZipFile(BASE / 'numpy.zip', 'w', compression=zipfile.ZIP_STORED) as package:
|
||||
for name, file in sorted(files.items()): package.write(file, name)
|
||||
package.write(SOURCE / 'LICENSE.txt', 'numpy/LICENSE.txt')
|
||||
registrations = [('.'.join(module.parts[:-1] + (module.name.split('.')[0],)), module.name.split('.')[0]) for module in modules]
|
||||
header = '#pragma once\n#include <Python.h>\n'
|
||||
header += ''.join(f'extern "C" PyObject *PyInit_{symbol}();\n' for name, symbol in registrations)
|
||||
header += 'inline int web_engine_register_numpy() {\n' + ''.join(f' if (PyImport_AppendInittab("{name}", PyInit_{symbol}) < 0) return -1;\n' for name, symbol in registrations) + ' return 0;\n}\n'
|
||||
(BASE / 'webengine_numpy.hh').write_text(header)
|
||||
bound = [archive, pathlib.Path(__file__), cross, random_meson, ROOT / 'build_web_python313/build/pyconfig.h', ROOT / 'build_web_python313/build/libpython3.13.a', library, BASE / 'numpy.zip', BASE / 'webengine_numpy.hh']
|
||||
report = dict(status='BUILT_UNVERIFIED', target='wasm32-emscripten-cpython313-static', modules=[name for name, _ in registrations], artifacts={str(file.relative_to(ROOT)): hashlib.sha256(file.read_bytes()).hexdigest() for file in bound})
|
||||
(BASE / 'build.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
75
tools/web/build-webengine-ocio.py
Normal file
75
tools/web/build-webengine-ocio.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build OpenColorIO and its dependencies from the supplied local Blender sources."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--sources", type=Path,
|
||||
default=Path.home() / "working/build_oiio_deps/build")
|
||||
parser.add_argument("--jobs", type=int, default=4)
|
||||
parser.add_argument("--package", action="append",
|
||||
choices=("zlib", "expat", "yamlcpp", "pystring", "imath",
|
||||
"minizipng", "opencolorio"))
|
||||
args = parser.parse_args()
|
||||
base = ROOT / "build_web_ocio"
|
||||
prefix = base / "install"
|
||||
packages = [
|
||||
("zlib", "", ["ZLIB_BUILD_EXAMPLES=OFF"]),
|
||||
("expat", "expat", ["EXPAT_BUILD_TOOLS=OFF", "EXPAT_BUILD_EXAMPLES=OFF",
|
||||
"EXPAT_BUILD_TESTS=OFF", "EXPAT_SHARED_LIBS=OFF",
|
||||
"EXPAT_BUILD_DOCS=OFF",
|
||||
# CPython namespaces its Expat API but not these internals.
|
||||
"CMAKE_C_FLAGS="
|
||||
"-Dg_reparseDeferralEnabledDefault=WebOCIO_reparseDeferralEnabledDefault "
|
||||
"-D_INTERNAL_trim_to_complete_utf8_characters="
|
||||
"WebOCIO_trim_to_complete_utf8_characters"]),
|
||||
("yamlcpp", "", ["YAML_CPP_BUILD_TESTS=OFF", "YAML_CPP_BUILD_TOOLS=OFF",
|
||||
"YAML_CPP_FORMAT_SOURCE=OFF", "YAML_CPP_INSTALL=ON"]),
|
||||
("pystring", "", []),
|
||||
("imath", "", ["IMATH_BUILD_TESTS=OFF", "IMATH_BUILD_PYTHON=OFF",
|
||||
"IMATH_INSTALL_PKG_CONFIG=ON"]),
|
||||
("minizipng", "", ["MZ_COMPAT=OFF", "MZ_BZIP2=OFF", "MZ_LZMA=OFF",
|
||||
"MZ_ZSTD=OFF", "MZ_OPENSSL=OFF", "MZ_LIBBSD=OFF",
|
||||
"MZ_PKCRYPT=OFF", "MZ_WZAES=OFF", "MZ_ICONV=OFF",
|
||||
"MZ_FETCH_LIBS=OFF", "MZ_BUILD_TESTS=OFF",
|
||||
"MZ_BUILD_UNIT_TESTS=OFF"]),
|
||||
("opencolorio", "", ["OCIO_BUILD_APPS=OFF", "OCIO_BUILD_TESTS=OFF",
|
||||
"OCIO_BUILD_GPU_TESTS=OFF", "OCIO_BUILD_PYTHON=OFF",
|
||||
"OCIO_BUILD_DOCS=OFF", "OCIO_USE_SIMD=OFF",
|
||||
"OCIO_INSTALL_EXT_PACKAGES=NONE",
|
||||
"OCIO_INSTALL_EXT_LIBS=OFF"]),
|
||||
]
|
||||
for name, subdir, options in packages:
|
||||
if args.package and name not in args.package:
|
||||
continue
|
||||
source = args.sources / name / "src" / ("external_" + name) / subdir
|
||||
build = base / name
|
||||
if name == "zlib":
|
||||
# This pinned copy already has distinct Emscripten archive names.
|
||||
source = ROOT / "blender-5.2.0/extern/zlib"
|
||||
build = base / "zlib-web"
|
||||
configure = [
|
||||
"emcmake", "cmake", "-S", str(source), "-B", str(build), "-G", "Ninja",
|
||||
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
|
||||
"-DCMAKE_INSTALL_PREFIX=" + str(prefix),
|
||||
"-DCMAKE_PREFIX_PATH=" + str(prefix),
|
||||
"-DCMAKE_FIND_ROOT_PATH=" + str(prefix),
|
||||
"-DCMAKE_CXX_FLAGS=-fexceptions", "-DBUILD_SHARED_LIBS=OFF",
|
||||
"-DBUILD_TESTING=OFF",
|
||||
*["-D" + option for option in options],
|
||||
]
|
||||
print("Building local dependency: " + name, flush=True)
|
||||
subprocess.run(configure, cwd=ROOT, check=True)
|
||||
subprocess.run(["cmake", "--build", str(build), "--target", "install",
|
||||
"--parallel", str(args.jobs)], cwd=ROOT, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
74
tools/web/build-webengine-oiio.py
Normal file
74
tools/web/build-webengine-oiio.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build pinned native image codecs and OpenImageIO for Blender WASM."""
|
||||
import hashlib, re, subprocess, sys, tarfile, urllib.request
|
||||
from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = ROOT / 'build_web_oiio'
|
||||
PREFIX = BASE / 'install'
|
||||
VERSIONS = (ROOT / 'blender-5.2.0/build_files/build_environment/cmake/versions.cmake').read_text()
|
||||
def value(key):
|
||||
return re.search(r'set\(' + key + r' ([^)]+)\)', VERSIONS)[1]
|
||||
def source(name):
|
||||
version = value(name + '_VERSION')
|
||||
filename = value(name + '_FILE').replace('${' + name + '_VERSION}', version)
|
||||
url = value(name + '_URI').replace('${' + name + '_VERSION}', version).replace('http:', 'https:')
|
||||
expected, algorithm = value(name+'_HASH'), value(name+'_HASH_TYPE').lower()
|
||||
if name == 'PNG' and version == '1.6.58':
|
||||
filename = 'libpng-3061454d980de7d53608f594194cfac722721d2a.tar.gz'
|
||||
url = 'https://codeload.github.com/pnggroup/libpng/tar.gz/3061454d980de7d53608f594194cfac722721d2a'
|
||||
expected, algorithm = 'a2569b7553971e36e86c8f75b9d1b5abbe92c436e05169afe3f0072e937f3536', 'sha256'
|
||||
archive = BASE / filename
|
||||
if not archive.exists():
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=60) as response: data = response.read()
|
||||
except OSError:
|
||||
with urllib.request.urlopen('https://download.blender.org/dependencies/' + filename, timeout=60) as response: data = response.read()
|
||||
if hashlib.new(algorithm, data).hexdigest() != expected:
|
||||
raise SystemExit(name + ' download differs from Blender pinned hash')
|
||||
archive.write_bytes(data)
|
||||
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected: raise SystemExit(name + ' archive hash mismatch')
|
||||
with tarfile.open(archive) as package:
|
||||
directory = BASE / package.getmembers()[0].name.split('/')[0]
|
||||
if not (directory / 'CMakeLists.txt').exists(): package.extractall(BASE, filter='data')
|
||||
return directory
|
||||
def build(name, directory, options):
|
||||
if len(sys.argv)>1 and name not in sys.argv[1:]: return
|
||||
toolchain = ROOT / '.emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake'
|
||||
prefixes = ';'.join(str(p) for p in (PREFIX, ROOT/'build_web_openexr/install', ROOT/'build_web_ocio/install'))
|
||||
subprocess.run(['cmake', '-S', str(directory), '-B', str(BASE/name), '-G', 'Ninja',
|
||||
'-DCMAKE_TOOLCHAIN_FILE='+str(toolchain), '-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DCMAKE_INSTALL_PREFIX='+str(PREFIX), '-DCMAKE_INSTALL_LIBDIR='+str(PREFIX/'lib'),
|
||||
'-DCMAKE_PREFIX_PATH='+prefixes, '-DCMAKE_FIND_ROOT_PATH='+prefixes,
|
||||
'-DCMAKE_CXX_FLAGS=-fexceptions', '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_TESTING=OFF',
|
||||
'-DZLIB_LIBRARY='+str(ROOT/'build_web_blender6/lib/libzstatic.a'),
|
||||
'-DZLIB_INCLUDE_DIR='+str(ROOT/'blender-5.2.0/extern/zlib'), *options], cwd=ROOT, check=True)
|
||||
subprocess.run(['cmake','--build',str(BASE/name),'--target','install','--parallel','8'],cwd=ROOT,check=True)
|
||||
def main():
|
||||
BASE.mkdir(exist_ok=True)
|
||||
build('jpeg',source('JPEG'),['-DENABLE_SHARED=OFF','-DENABLE_STATIC=ON','-DWITH_SIMD=OFF','-DENABLE_TESTING=OFF'])
|
||||
build('png',source('PNG'),['-DPNG_SHARED=OFF','-DPNG_TESTS=OFF','-DPNG_TOOLS=OFF','-DPNG_EXECUTABLES=OFF'])
|
||||
build('tiff',source('TIFF'),['-Dtiff-tools=OFF','-Dtiff-tests=OFF','-Dtiff-contrib=OFF','-Dtiff-docs=OFF','-Dtiff-deprecated=OFF','-Dlibdeflate=OFF','-Dlzma=OFF','-Dzstd=OFF','-Dwebp=OFF','-Djbig=OFF'])
|
||||
robin = source('ROBINMAP')
|
||||
fmt = BASE/'fmt'; fmt.mkdir(exist_ok=True)
|
||||
(fmt/'fmtConfigVersion.cmake').write_text('set(PACKAGE_VERSION "10.1.0")\nset(PACKAGE_VERSION_COMPATIBLE TRUE)\n')
|
||||
(fmt/'fmtConfig.cmake').write_text('add_library(fmt::fmt-header-only INTERFACE IMPORTED)\nset_target_properties(fmt::fmt-header-only PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "'+str(ROOT/'blender-5.2.0/extern/fmt/include')+'" INTERFACE_COMPILE_DEFINITIONS "FMT_HEADER_ONLY;FMT_CONSTEVAL=;FMT_USE_CONSTEXPR=0;FMT_USE_NONTYPE_TEMPLATE_ARGS=0")\n')
|
||||
oiio=source('OPENIMAGEIO')
|
||||
platform=oiio/'src/libutil/sysutil.cpp'; text=platform.read_text()
|
||||
text=text.replace('#include <thread>', '#include <thread>\n#ifdef __EMSCRIPTEN__\n# include <unistd.h>\n#endif',1) if '#ifdef __EMSCRIPTEN__' not in text else text
|
||||
text=text.replace('#elif defined(__GNU__) || defined(__OpenBSD__) || defined(_WIN32)', '#elif defined(__GNU__) || defined(__OpenBSD__) || defined(_WIN32) || defined(__EMSCRIPTEN__)')
|
||||
text=text.replace('# include <unistd.h>', '# include <unistd.h>\n# include <emscripten/heap.h>',1) if '<emscripten/heap.h>' not in text else text
|
||||
text=text.replace('Sysutil::physical_memory()\n{\n#if defined(__linux__)', 'Sysutil::physical_memory()\n{\n#if defined(__EMSCRIPTEN__)\n return emscripten_get_heap_max();\n#elif defined(__linux__)')
|
||||
platform.write_text(text)
|
||||
strings=oiio/'src/libutil/strutil.cpp'; text=strings.read_text()
|
||||
text=text.replace('|| defined(__GLIBC__)\n', '|| defined(__GLIBC__) || defined(__EMSCRIPTEN__)\n')
|
||||
strings.write_text(text.replace('|| defined(__GLIBC__) || defined(__NetBSD__)\n', '|| defined(__GLIBC__) || defined(__NetBSD__) || defined(__EMSCRIPTEN__)\n'))
|
||||
build('oiio',oiio,[
|
||||
'-DCMAKE_PROJECT_INCLUDE='+str(ROOT/'tools/web/oiio-wasm-dependencies.cmake'),
|
||||
'-Dfmt_DIR='+str(fmt), '-DROBINMAP_INCLUDE_DIR='+str(robin/'include'),
|
||||
'-DOIIO_BUILD_TOOLS=OFF','-DOIIO_BUILD_TESTS=OFF','-DUSE_PYTHON=OFF','-DINSTALL_DOCS=OFF','-DEMBEDPLUGINS=ON',
|
||||
'-DOpenImageIO_BUILD_MISSING_DEPS=none','-DUSE_SIMD=0','-DUSE_TBB=OFF','-DUSE_OPENCOLORIO=ON',
|
||||
'-DUSE_FFMPEG=OFF','-DUSE_OPENVDB=OFF','-DUSE_FREETYPE=OFF','-DUSE_OPENCV=OFF','-DUSE_OPENJPEG=OFF','-DUSE_WEBP=OFF',
|
||||
'-DUSE_JXL=OFF','-DUSE_LIBHEIF=OFF','-DUSE_LIBRAW=OFF','-DUSE_LIBUHDR=OFF','-DUSE_PTEX=OFF','-DUSE_DCMTK=OFF'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
47
tools/web/build-webengine-openexr.py
Normal file
47
tools/web/build-webengine-openexr.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Blender's pinned OpenEXR with the existing WASM Imath installation."""
|
||||
import hashlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = ROOT / 'build_web_openexr'
|
||||
IMATH = ROOT / 'build_web_ocio/install'
|
||||
|
||||
def main():
|
||||
versions = (ROOT / 'blender-5.2.0/build_files/build_environment/cmake/versions.cmake').read_text()
|
||||
value = lambda name: re.search(r'set\(OPENEXR_' + name + r' ([^)]+)\)', versions)[1]
|
||||
version, expected, algorithm = value('VERSION'), value('HASH'), value('HASH_TYPE').lower()
|
||||
if not (IMATH / 'lib/cmake/Imath/ImathConfig.cmake').is_file():
|
||||
raise SystemExit('Reuse/build the Web OCIO Imath installation first')
|
||||
BASE.mkdir(exist_ok=True)
|
||||
archive = BASE / ('openexr-' + version + '.tar.gz')
|
||||
if not archive.is_file():
|
||||
with urllib.request.urlopen('https://codeload.github.com/AcademySoftwareFoundation/openexr/tar.gz/refs/tags/v' + version, timeout=60) as response:
|
||||
data = response.read()
|
||||
if hashlib.new(algorithm, data).hexdigest() != expected:
|
||||
raise SystemExit('OpenEXR download differs from Blender pinned source hash')
|
||||
archive.write_bytes(data)
|
||||
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
|
||||
raise SystemExit('OpenEXR archive differs from Blender pinned source hash')
|
||||
source = BASE / ('openexr-' + version)
|
||||
if not (source / 'CMakeLists.txt').is_file():
|
||||
with tarfile.open(archive) as package: package.extractall(BASE, filter='data')
|
||||
toolchain = ROOT / '.emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake'
|
||||
subprocess.run(['cmake', '-S', str(source), '-B', str(BASE / 'build'), '-G', 'Ninja',
|
||||
'-DCMAKE_TOOLCHAIN_FILE=' + str(toolchain), '-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DCMAKE_INSTALL_PREFIX=' + str(BASE / 'install'), '-DCMAKE_INSTALL_LIBDIR=lib',
|
||||
'-DCMAKE_PREFIX_PATH=' + str(IMATH), '-DCMAKE_FIND_ROOT_PATH=' + str(IMATH),
|
||||
'-DImath_DIR=' + str(IMATH / 'lib/cmake/Imath'), '-DCMAKE_CXX_FLAGS=-fexceptions',
|
||||
'-DCMAKE_C_FLAGS=', '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_TESTING=OFF',
|
||||
'-DOPENEXR_ENABLE_THREADING=OFF', '-DBUILD_WEBSITE=OFF', '-DOPENEXR_INSTALL_DOCS=OFF',
|
||||
'-DOPENEXR_BUILD_TOOLS=OFF', '-DOPENEXR_BUILD_EXAMPLES=OFF', '-DOPENEXR_FORCE_INTERNAL_DEFLATE=ON'], cwd=ROOT, check=True)
|
||||
subprocess.run(['cmake', '--build', str(BASE / 'build'), '--target', 'install', '--parallel', '8'], cwd=ROOT, check=True)
|
||||
shutil.copy2(BASE / 'build/external/OpenJPH/src/core/libopenjph.a', BASE / 'install/lib/libopenjph.a')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
95
tools/web/build-webengine-python.py
Normal file
95
tools/web/build-webengine-python.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the local desktop-version CPython archive for the WebEngine memory."""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source', type=Path, default=Path('/home/mes123456/working/build_oiio_deps/build/python/src/external_python'))
|
||||
args = parser.parse_args()
|
||||
base = ROOT / 'build_web_python313'
|
||||
source, build = base / 'source', base / 'build'
|
||||
report_dir = ROOT / 'tests/golden/WBV2-P1424-001/embedded-python-build' / datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
report_dir.mkdir(parents=True)
|
||||
report = dict(status='BUILDING', validation='NOT_RUN', source=str(args.source), steps=[])
|
||||
try:
|
||||
patchlevel = (args.source / 'Include/patchlevel.h').read_text()
|
||||
if '#define PY_VERSION "3.13.13"' not in patchlevel:
|
||||
raise ValueError('The embedded runtime requires the supplied Blender CPython 3.13.13 source')
|
||||
report['version'] = '3.13.13'
|
||||
report['sourceIdentity'] = {name: hashlib.sha256((args.source / name).read_bytes()).hexdigest()
|
||||
for name in ['Include/patchlevel.h', 'configure', 'LICENSE']}
|
||||
if not source.exists():
|
||||
shutil.copytree(args.source, source, ignore=shutil.ignore_patterns(
|
||||
'*.o', '*.a', '*.so', '*.so.*', '*.pyc', '__pycache__', '.git', 'build',
|
||||
'python', 'Makefile', 'pyconfig.h', 'config.log', 'config.status', 'config.cache',
|
||||
'pybuilddir.txt', '_bootstrap_python', '_freeze_module', 'Setup.local', 'Setup.stdlib', 'Setup.bootstrap', 'config.c'))
|
||||
# Keep the copied upstream source reproducible without rebuilding or
|
||||
# replacing the archive on every implementation cycle.
|
||||
if 'web_engine_python_unraisable' not in (source / 'Python/errors.c').read_text():
|
||||
subprocess.run(['patch', '-p1', '-i',
|
||||
str(ROOT / 'tools/web/patches/cpython-3.13-webengine-unraisable.patch')],
|
||||
cwd=source, check=True)
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
build_python = ROOT / '.deps/native/python/bin/python3.13'
|
||||
env = dict(os.environ)
|
||||
env['CFLAGS'] = (env.get('CFLAGS', '') + ' -DPY_CALL_TRAMPOLINE=1').strip()
|
||||
env['LD_LIBRARY_PATH'] = str(ROOT / '.deps/native/python/lib') + ':' + env.get('LD_LIBRARY_PATH', '')
|
||||
env['PYTHONHOME'] = str(ROOT / '.deps/native/python')
|
||||
def run(name, command):
|
||||
with (report_dir / (name + '.log')).open('w') as log:
|
||||
result = subprocess.run(command, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
report['steps'].append(dict(name=name, exitCode=result.returncode))
|
||||
if result.returncode:
|
||||
raise RuntimeError(name + ' failed; see ' + str(report_dir))
|
||||
if not (build / 'Makefile').exists() or 'PY_CALL_TRAMPOLINE' not in (build / 'Makefile').read_text():
|
||||
# Shell arguments stay positional; source paths are never interpolated as shell code.
|
||||
run('configure', ['bash', '-c',
|
||||
'source tools/web/emscripten-env.sh && cd "$1" && CONFIG_SITE="$2/Tools/wasm/config.site-wasm32-emscripten" emconfigure "$2/configure" --host=wasm32-unknown-emscripten --build=x86_64-pc-linux-gnu --with-emscripten-target=browser --disable-wasm-pthreads --disable-wasm-dynamic-linking --disable-shared --without-ensurepip --disable-test-modules --prefix=/python --with-build-python="$3"',
|
||||
'web-python-build', str(build), str(source), str(build_python)])
|
||||
configuration = build / 'pyconfig.h'
|
||||
if '#define PY_CALL_TRAMPOLINE 1' not in configuration.read_text():
|
||||
configuration.write_text(configuration.read_text() + '\n#define PY_CALL_TRAMPOLINE 1\n')
|
||||
frozen = build / 'Python/frozen_modules/getpath.h'
|
||||
frozen.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not frozen.exists():
|
||||
run('freeze-getpath', [str(build_python), str(source / 'Programs/_freeze_module.py'),
|
||||
'getpath', str(source / 'Modules/getpath.py'), str(frozen)])
|
||||
run('archive', ['bash', '-c', 'source tools/web/emscripten-env.sh && emmake make -C "$1" -j4 libpython3.13.a', 'web-python-build', str(build)])
|
||||
stdlib = base / 'python313.zip'
|
||||
with zipfile.ZipFile(stdlib, 'w', compression=zipfile.ZIP_STORED) as archive:
|
||||
for file in sorted((source / 'Lib').rglob('*.py')):
|
||||
relative = file.relative_to(source / 'Lib')
|
||||
if any(part in {'test', 'tests', '__pycache__', 'idlelib', 'tkinter', 'turtledemo', 'ensurepip'} for part in relative.parts):
|
||||
continue
|
||||
archive.write(file, relative.as_posix())
|
||||
report['artifacts'] = {}
|
||||
for file in [build / 'libpython3.13.a', build / 'pyconfig.h', stdlib,
|
||||
build / 'Modules/expat/libexpat.a', build / 'Modules/_hacl/libHacl_Hash_SHA2.a',
|
||||
build / 'Modules/_decimal/libmpdec/libmpdec.a']:
|
||||
data = file.read_bytes()
|
||||
report['artifacts'][str(file.relative_to(ROOT))] = dict(bytes=len(data), sha256=hashlib.sha256(data).hexdigest())
|
||||
report['status'] = 'BUILT_UNVERIFIED'
|
||||
license_dir = ROOT / 'web/app/public/vendor/blender'
|
||||
license_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source / 'LICENSE', license_dir / 'CPYTHON-LICENSE.txt')
|
||||
except BaseException as error:
|
||||
report.update(status='BUILD_INCOMPLETE', error=str(error))
|
||||
raise
|
||||
finally:
|
||||
(report_dir / 'build.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(json.dumps(dict(directory=str(report_dir), **report)), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
213
tools/web/build-webengine-sndfile.py
Normal file
213
tools/web/build-webengine-sndfile.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build pinned libsndfile/Xiph/MPEG codecs for native WebEngine audio."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import shutil
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
VERSION = "1.2.2"
|
||||
# Blender build_files/build_environment/cmake/versions.cmake.
|
||||
ARCHIVE_MD5 = "04e2e6f726da7c5dc87f8cf72f250d04"
|
||||
# Debian's unmodified upstream tag archive of the same version (different
|
||||
# packaging from the upstream release tarball), retained for offline reuse.
|
||||
TAG_ARCHIVE_SHA256 = "ffe12ef8add3eaca876f04087734e6e8e029350082f3251f565fa9da55b52121"
|
||||
|
||||
# Same releases and digests as Blender's versions.cmake. All algorithms remain
|
||||
# in the upstream native libraries; no browser-side encoding implementation.
|
||||
CODECS = (
|
||||
("ogg", "libogg-1.3.5", ".tar.gz",
|
||||
"0eb4b4b9420a0f51db142ba3f9c64b333f826532dc0f48c6410ae51f4799b664", []),
|
||||
("vorbis", "libvorbis-1.3.7", ".tar.gz",
|
||||
"0e982409a9c3fc82ee06e08205b1355e5c6aa4c36bca58146ef399621b0ce5ab", []),
|
||||
("flac", "flac-1.4.2", ".tar.xz",
|
||||
"e322d58a1f48d23d9dd38f432672865f6f79e73a6f9cc5a5f57fcaa83eb5a8e4",
|
||||
["-DBUILD_CXXLIBS=OFF", "-DBUILD_PROGRAMS=OFF", "-DBUILD_EXAMPLES=OFF",
|
||||
"-DBUILD_DOCS=OFF", "-DBUILD_MANPAGES=OFF", "-DWITH_OGG=ON"]),
|
||||
("opus", "opus-1.3.1", ".tar.gz",
|
||||
"65b58e1e25b2a114157014736a3d9dfeaad8d41be1c8179866f144a2fb44ff9d",
|
||||
["-DOPUS_BUILD_PROGRAMS=OFF", "-DOPUS_BUILD_TESTING=OFF",
|
||||
"-DOPUS_INSTALL_PKG_CONFIG_MODULE=ON", "-DOPUS_STACK_PROTECTOR=OFF"]),
|
||||
)
|
||||
|
||||
# LAME matches Blender's versions.cmake. Blender uses FFmpeg for decoding and
|
||||
# does not pin mpg123: pin the original upstream archive required by libsndfile
|
||||
# separately. Digest from Debian's mpg123_1.32.10-1+deb13u1.dsc source metadata.
|
||||
MPEG_CODECS = (
|
||||
("lame-3.100", ".tar.gz", "md5", "83e260acbe4389b54fe08e0bdbf7cddb",
|
||||
"https://deb.debian.org/debian/pool/main/l/lame/lame_3.100.orig.tar.gz",
|
||||
["--disable-frontend", "--disable-decoder", "--disable-analyzer-hooks", "--disable-nasm"]),
|
||||
("mpg123-1.32.10", ".tar.xz", "sha256",
|
||||
"98867a796fa1eeb161847d211734d585ff61c56712fbc333de416fea7b59de29",
|
||||
"https://deb.debian.org/debian/pool/main/m/mpg123/mpg123_1.32.10.orig.tar.xz",
|
||||
["--with-cpu=generic", "--disable-components", "--enable-libmpg123", "--disable-network"]),
|
||||
)
|
||||
|
||||
|
||||
def build_mpeg(base, prefix, jobs, env):
|
||||
emscripten = ROOT / ".emsdk/upstream/emscripten"
|
||||
for release, suffix, algorithm, digest, url, options in MPEG_CODECS:
|
||||
source = base / release
|
||||
if not (source / "configure").is_file():
|
||||
filename = release + suffix
|
||||
cached = [base / filename, Path.home() / "working/packages" / filename,
|
||||
Path.home() / "working/build_oiio_deps/downloads" / filename]
|
||||
archive = next((path for path in cached if path.is_file()), None)
|
||||
if archive is None:
|
||||
archive = cached[0]
|
||||
partial = archive.with_suffix(".partial")
|
||||
with urllib.request.urlopen(url, timeout=60) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != digest:
|
||||
raise RuntimeError(f"Pinned {release} archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
build = base / (release + "-build")
|
||||
build.mkdir(exist_ok=True)
|
||||
if release.startswith("lame-"):
|
||||
# LAME 3.100's 2017 GNU config helper predates Emscripten. Reuse
|
||||
# the helper shipped with our embedded CPython, not a fake host
|
||||
# triplet which could enable native CPU assembly or ELF tooling.
|
||||
shutil.copy2(ROOT / "build_web_python313/source/config.sub", source / "config.sub")
|
||||
configure = [str(emscripten / "emconfigure"), str(source / "configure"),
|
||||
"--host=wasm32-unknown-emscripten", f"--prefix={prefix}",
|
||||
"--disable-shared", "--enable-static", *options]
|
||||
codec_env = dict(env, CFLAGS="-O2 -fPIC", CXXFLAGS="-O2 -fPIC")
|
||||
# Autoconf source lists remain upstream-owned; only rerun configuration
|
||||
# after build inputs change, leaving existing codec objects reusable.
|
||||
settings = json.dumps([configure, codec_env["CFLAGS"], codec_env["CXXFLAGS"]])
|
||||
stamp = build / "web-configure.json"
|
||||
if not stamp.is_file() or stamp.read_text() != settings:
|
||||
subprocess.run(configure, cwd=build, env=codec_env, check=True)
|
||||
stamp.write_text(settings)
|
||||
subprocess.run([str(emscripten / "emmake"), "make", f"-j{jobs}", "install"],
|
||||
cwd=build, env=codec_env, check=True)
|
||||
|
||||
|
||||
def build_codecs(base, prefix, jobs, common, env):
|
||||
for family, release, suffix, digest, options in CODECS:
|
||||
source = base / release
|
||||
if not (source / "CMakeLists.txt").is_file():
|
||||
filename = release + suffix
|
||||
cached = [base / filename, Path.home() / "working/packages" / filename]
|
||||
archive = next((path for path in cached if path.is_file()), None)
|
||||
if archive is None:
|
||||
archive = cached[0]
|
||||
partial = archive.with_suffix(".partial")
|
||||
url = f"https://downloads.xiph.org/releases/{family}/{filename}"
|
||||
with urllib.request.urlopen(url, timeout=60) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
if hashlib.sha256(archive.read_bytes()).hexdigest() != digest:
|
||||
raise RuntimeError(f"Blender-pinned {release} archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
if family == "opus" and not (source / "opus_buildtype.cmake").is_file():
|
||||
# The pinned 1.3.1 release omits this build-type defaults helper.
|
||||
# We set CMAKE_BUILD_TYPE explicitly. Keep the original source list
|
||||
# and configuration; only make the absent defaults include optional.
|
||||
cmake = source / "CMakeLists.txt"
|
||||
cmake.write_text(cmake.read_text().replace(
|
||||
"include(opus_buildtype.cmake)",
|
||||
"include(opus_buildtype.cmake OPTIONAL)"))
|
||||
build = base / (family + "-build")
|
||||
subprocess.run(["cmake", "-S", str(source), "-B", str(build), *common,
|
||||
"-DBUILD_TESTING=OFF", *options], cwd=ROOT, env=env, check=True)
|
||||
subprocess.run(["cmake", "--build", str(build), "--target", "install",
|
||||
"--parallel", str(jobs)], cwd=ROOT, env=env, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path)
|
||||
parser.add_argument("--archive", type=Path)
|
||||
parser.add_argument("--jobs", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
base = ROOT / "build_web_sndfile"
|
||||
base.mkdir(exist_ok=True)
|
||||
source = args.source
|
||||
if source is not None and not (source / "CMakeLists.txt").is_file():
|
||||
raise FileNotFoundError(f"No libsndfile source at {source}")
|
||||
if source is None:
|
||||
local = (Path.home() / "working/build_oiio_deps/build/sndfile/src/"
|
||||
"external_sndfile")
|
||||
source = local if (local / "CMakeLists.txt").is_file() else base / f"libsndfile-{VERSION}"
|
||||
if not (source / "CMakeLists.txt").is_file():
|
||||
if args.archive:
|
||||
archive = args.archive
|
||||
else:
|
||||
cached = [Path.home() / f"working/packages/libsndfile-{VERSION}.tar.xz",
|
||||
base / f"libsndfile-{VERSION}.tar.xz",
|
||||
base / f"libsndfile_{VERSION}.orig.tar.gz"]
|
||||
archive = next((path for path in cached if path.is_file()), None)
|
||||
if archive is None:
|
||||
archive = cached[-1]
|
||||
url = ("https://deb.debian.org/debian/pool/main/libs/libsndfile/"
|
||||
f"libsndfile_{VERSION}.orig.tar.gz")
|
||||
partial = archive.with_suffix(".partial")
|
||||
with urllib.request.urlopen(url, timeout=45) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
contents = archive.read_bytes()
|
||||
if (hashlib.md5(contents).hexdigest() != ARCHIVE_MD5 and
|
||||
hashlib.sha256(contents).hexdigest() != TAG_ARCHIVE_SHA256):
|
||||
raise RuntimeError("Blender-pinned libsndfile archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
source = base / f"libsndfile-{VERSION}"
|
||||
prefix = base / "install"
|
||||
common = [
|
||||
"-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=" + str(ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"),
|
||||
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=" + str(prefix),
|
||||
"-DEMSCRIPTEN_SYSTEM_PROCESSOR=wasm32",
|
||||
"-DCMAKE_INSTALL_LIBDIR=lib", "-DBUILD_SHARED_LIBS=OFF",
|
||||
"-DCMAKE_PREFIX_PATH=" + str(prefix),
|
||||
"-DCMAKE_FIND_ROOT_PATH=" + str(prefix),
|
||||
]
|
||||
# Never let host pkg-config select ELF libraries for a WASM link.
|
||||
env = dict(os.environ, PKG_CONFIG_LIBDIR=str(prefix / "lib/pkgconfig"),
|
||||
PKG_CONFIG_PATH=str(prefix / "lib/pkgconfig"))
|
||||
build_codecs(base, prefix, args.jobs, common, env)
|
||||
build_mpeg(base, prefix, args.jobs, env)
|
||||
# libsndfile 1.2.2 only exposes Vorbis quality, although libvorbis already
|
||||
# implements managed bitrate encoding. Keep this native extension in a
|
||||
# reproducible source patch and install its command declaration with the
|
||||
# archive, so Audaspace cannot silently use an unpatched system header.
|
||||
marker = "SFC_WEB_SET_VORBIS_BITRATE"
|
||||
patched_header = marker in (source / "include/sndfile.h").read_text()
|
||||
patched_encoder = marker in (source / "src/ogg_vorbis.c").read_text()
|
||||
if patched_header != patched_encoder:
|
||||
raise RuntimeError("Incomplete WebEngine Vorbis patch in libsndfile source")
|
||||
if not patched_header:
|
||||
subprocess.run(["patch", "--batch", "--forward", "-p1", "-i",
|
||||
str(ROOT / "tools/web/patches/libsndfile-vorbis-bitrate.patch")],
|
||||
cwd=source, check=True)
|
||||
configure = [
|
||||
"cmake", "-S", str(source), "-B", str(base / "build"), *common,
|
||||
"-DBUILD_PROGRAMS=OFF", "-DBUILD_EXAMPLES=OFF", "-DBUILD_TESTING=OFF",
|
||||
"-DENABLE_EXTERNAL_LIBS=ON", "-DENABLE_MPEG=ON",
|
||||
"-DINSTALL_MANPAGES=OFF", "-DINSTALL_PKGCONFIG_MODULE=ON",
|
||||
]
|
||||
# Pin every find-module input, including entries cached by the old PCM build.
|
||||
for library, variable in (("ogg", "OGG"), ("vorbis", "Vorbis_Vorbis"),
|
||||
("vorbisenc", "Vorbis_Enc"), ("vorbisfile", "Vorbis_File"),
|
||||
("FLAC", "FLAC"), ("opus", "OPUS"),
|
||||
("mp3lame", "MP3LAME"), ("mpg123", "mpg123")):
|
||||
configure += [f"-D{variable}_LIBRARY={prefix}/lib/lib{library}.a",
|
||||
f"-D{variable}_INCLUDE_DIR={prefix}/include"]
|
||||
subprocess.run(configure, cwd=ROOT, env=env, check=True)
|
||||
subprocess.run(["cmake", "--build", str(base / "build"), "--target", "install",
|
||||
"--parallel", str(args.jobs)], cwd=ROOT, env=env, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
230
tools/web/build-webengine-usd.py
Normal file
230
tools/web/build-webengine-usd.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build pinned native dependencies for CacheFile USD_create_handle in WebEngine.
|
||||
|
||||
The TBB stage is independently resumable. This builds the OpenUSD core and
|
||||
original static pxr Python extensions, not the Blender reader.
|
||||
No tests or application candidates are built; existing objects are retained.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = ROOT / "build_web_usd"
|
||||
INSTALL = BASE / "install"
|
||||
TOOLCHAIN = ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
|
||||
VERSIONS = ROOT / "blender-5.2.0/build_files/build_environment/cmake/versions.cmake"
|
||||
|
||||
|
||||
def source_for(name, repository, directory):
|
||||
versions = VERSIONS.read_text()
|
||||
def value(suffix):
|
||||
return re.search(r"set\(" + name + "_" + suffix + r" ([^)]+)\)", versions)[1]
|
||||
version = value("VERSION")
|
||||
tag = version if version.startswith("v") else "v" + version
|
||||
filename = ("oneTBB-" if name == "TBB" else "usd-") + tag + ".tar.gz"
|
||||
archive = BASE / filename
|
||||
expected = value("HASH")
|
||||
algorithm = value("HASH_TYPE").lower()
|
||||
if not archive.exists():
|
||||
with urllib.request.urlopen(
|
||||
"https://codeload.github.com/" + repository + "/tar.gz/refs/tags/" + tag,
|
||||
timeout=60,
|
||||
) as response:
|
||||
data = response.read()
|
||||
if hashlib.new(algorithm, data).hexdigest() != expected:
|
||||
raise SystemExit(name + " download differs from Blender's pinned hash")
|
||||
archive.write_bytes(data)
|
||||
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
|
||||
raise SystemExit(name + " archive differs from Blender's pinned hash")
|
||||
source = BASE / (directory + "-" + version.removeprefix("v"))
|
||||
if not (source / "CMakeLists.txt").exists():
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(BASE, filter="data")
|
||||
return source, version
|
||||
|
||||
|
||||
def configure(source, build, options):
|
||||
env = os.environ.copy()
|
||||
host_python = ROOT / ".deps/native/python"
|
||||
env["LD_LIBRARY_PATH"] = str(host_python / "lib") + ":" + env.get("LD_LIBRARY_PATH", "")
|
||||
subprocess.run([
|
||||
"cmake", "-S", str(source), "-B", str(build), "-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=" + str(TOOLCHAIN),
|
||||
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
|
||||
"-DCMAKE_INSTALL_PREFIX=" + str(INSTALL),
|
||||
"-DCMAKE_CXX_FLAGS=-fexceptions -fsigned-char",
|
||||
"-DBUILD_SHARED_LIBS=OFF", *options,
|
||||
], cwd=ROOT, env=env, check=True)
|
||||
|
||||
|
||||
def build_target(build, target):
|
||||
subprocess.run(["cmake", "--build", str(build), "--target", target,
|
||||
"--parallel", "8"], cwd=ROOT, check=True)
|
||||
|
||||
|
||||
def adapt_web_source(source):
|
||||
# These adaptations preserve upstream behavior and ABI assertions. They
|
||||
# must also be present in installed headers consumed by Blender.
|
||||
edits = {
|
||||
"pxr/base/tf/pyObjWrapper.h": [
|
||||
('#include "pxr/base/arch/pragmas.h"',
|
||||
'#include "pxr/base/arch/pragmas.h"\n\n#include <memory>'),
|
||||
('static constexpr std::size_t Size = 16;\n'
|
||||
' static constexpr std::size_t Align = 8;',
|
||||
'// Match the native shared_ptr member on wasm32 as well as 64-bit hosts.\n'
|
||||
' static constexpr std::size_t Size = sizeof(std::shared_ptr<void>);\n'
|
||||
' static constexpr std::size_t Align = alignof(std::shared_ptr<void>);'),
|
||||
],
|
||||
"cmake/defaults/gccclangshareddefaults.cmake": [
|
||||
('if(CMAKE_USE_PTHREADS_INIT)\n',
|
||||
'if(CMAKE_USE_PTHREADS_INIT AND NOT (EMSCRIPTEN AND EMSCRIPTEN_WITHOUT_PTHREAD))\n'),
|
||||
],
|
||||
"cmake/macros/Private.cmake": [
|
||||
('if(NOT TARGET shared_libs)\n message(STATUS "Skipping Python module',
|
||||
'if(NOT TARGET shared_libs AND NOT PXR_WEB_STATIC_PYTHON)\n message(STATUS "Skipping Python module'),
|
||||
(' # Add the module target.\n add_library(${LIBRARY_NAME}\n SHARED',
|
||||
' # Embedded CPython loads original wrappers through its builtin table.\n'
|
||||
' set(pythonLibraryType SHARED)\n'
|
||||
' if(PXR_WEB_STATIC_PYTHON)\n'
|
||||
' set(pythonLibraryType STATIC)\n'
|
||||
' endif()\n'
|
||||
' # Add the module target.\n add_library(${LIBRARY_NAME}\n ${pythonLibraryType}'),
|
||||
(' # Accumulate Python module names.\n',
|
||||
' if(PXR_WEB_STATIC_PYTHON)\n'
|
||||
' set_property(GLOBAL APPEND PROPERTY PXR_WEB_PYTHON_MODULES\n'
|
||||
' "${pyModuleName}|${LIBRARY_NAME}")\n'
|
||||
' endif()\n\n # Accumulate Python module names.\n'),
|
||||
(' TARGETS ${LIBRARY_NAME}\n LIBRARY DESTINATION ${libInstallPrefix}',
|
||||
' TARGETS ${LIBRARY_NAME}\n ARCHIVE DESTINATION ${libInstallPrefix}\n'
|
||||
' LIBRARY DESTINATION ${libInstallPrefix}'),
|
||||
],
|
||||
"pxr/CMakeLists.txt": [
|
||||
('pxr_core_epilogue()\n',
|
||||
'pxr_core_epilogue()\n\n'
|
||||
'include("${PROJECT_SOURCE_DIR}/cmake/macros/WebStaticPython.cmake")\n'),
|
||||
],
|
||||
"cmake/macros/Public.cmake": [
|
||||
('if(TARGET shared_libs AND PXR_ENABLE_PYTHON_SUPPORT)',
|
||||
'if((TARGET shared_libs OR PXR_WEB_STATIC_PYTHON) AND PXR_ENABLE_PYTHON_SUPPORT)'),
|
||||
],
|
||||
}
|
||||
for relative, replacements in edits.items():
|
||||
path = source / relative
|
||||
text = path.read_text()
|
||||
original = text
|
||||
for before, after in replacements:
|
||||
if after in text:
|
||||
continue
|
||||
if text.count(before) != 1:
|
||||
raise SystemExit("OpenUSD source differs at " + relative)
|
||||
text = text.replace(before, after, 1)
|
||||
if text != original:
|
||||
path.write_text(text)
|
||||
helper = source / "cmake/macros/WebStaticPython.cmake"
|
||||
contents = (ROOT / "tools/web/openusd-static-python.cmake").read_text()
|
||||
if not helper.exists() or helper.read_text() != contents:
|
||||
helper.write_text(contents)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--stage", choices=("tbb", "usd", "all"), default="all")
|
||||
parser.add_argument("--usd-target", default="install",
|
||||
help="Original OpenUSD target; install includes schema/plugin resources")
|
||||
args = parser.parse_args()
|
||||
BASE.mkdir(exist_ok=True)
|
||||
if args.stage in ("tbb", "all"):
|
||||
source, _ = source_for("TBB", "uxlfoundation/oneTBB", "oneTBB")
|
||||
build = BASE / "tbb-build"
|
||||
configure(source, build, [
|
||||
"-DEMSCRIPTEN_WITHOUT_PTHREAD=ON", "-DTBB_TEST=OFF",
|
||||
"-DTBB_STRICT=OFF", "-DTBBMALLOC_BUILD=OFF",
|
||||
"-DTBBMALLOC_PROXY_BUILD=OFF", "-DTBB_ENABLE_IPO=OFF",
|
||||
])
|
||||
build_target(build, "install")
|
||||
if args.stage in ("usd", "all"):
|
||||
if not (INSTALL / "lib/cmake/TBB/TBBConfig.cmake").exists():
|
||||
raise SystemExit("Build the pinned Web TBB stage first")
|
||||
source, version = source_for("USD", "PixarAnimationStudios/OpenUSD", "OpenUSD")
|
||||
adapt_web_source(source)
|
||||
python = ROOT / "build_web_python313"
|
||||
if not (python / "build/libpython3.13.a").exists():
|
||||
raise SystemExit("Reuse/build the embedded Web CPython 3.13 archive first")
|
||||
# FindPython expects pyconfig.h beside Python.h. Preserve CPython's
|
||||
# separate source/build trees with a local header overlay, using the
|
||||
# WASM configuration rather than the host interpreter's ABI.
|
||||
python_include = BASE / "python-include"
|
||||
python_include.mkdir(exist_ok=True)
|
||||
for header in (python / "source/Include").iterdir():
|
||||
destination = python_include / header.name
|
||||
if not destination.exists():
|
||||
destination.symlink_to(header, target_is_directory=header.is_dir())
|
||||
config = python_include / "pyconfig.h"
|
||||
if not config.exists():
|
||||
config.symlink_to(python / "build/pyconfig.h")
|
||||
build = BASE / "usd-build"
|
||||
configure(source, build, [
|
||||
"-DCMAKE_CXX_FLAGS=-fexceptions -fsigned-char -I" + str(python / "build"),
|
||||
"-DCMAKE_PREFIX_PATH=" + str(INSTALL),
|
||||
"-DCMAKE_FIND_ROOT_PATH=" + str(INSTALL),
|
||||
"-DTBB_DIR=" + str(INSTALL / "lib/cmake/TBB"),
|
||||
"-DOPENSUBDIV_ROOT_DIR=" + str(ROOT / "blender-5.2.0/extern/opensubdiv-source"),
|
||||
"-DOPENSUBDIV_OSDCPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdCPU.a"),
|
||||
"-DOPENSUBDIV_OSDGPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdGPU.a"),
|
||||
"-DEMSCRIPTEN_WITHOUT_PTHREAD=ON",
|
||||
# OpenUSD retains static registration through LINK_LIBRARY.
|
||||
# Emscripten/wasm-ld supports these flags, but CMake 3.28's
|
||||
# Emscripten platform does not declare the feature itself.
|
||||
"-DCMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE_SUPPORTED=TRUE",
|
||||
"-DCMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE=-Wl,--whole-archive;<LINK_ITEM>;-Wl,--no-whole-archive",
|
||||
"-DPXR_FIND_TBB_IN_CONFIG=ON", "-DPXR_BUILD_MONOLITHIC=ON",
|
||||
"-DPXR_SET_INTERNAL_NAMESPACE=pxrBlender_v" + version.replace(".", "_"),
|
||||
"-DPXR_ENABLE_PYTHON_SUPPORT=ON",
|
||||
"-DPXR_WEB_STATIC_PYTHON=ON",
|
||||
"-DPython3_EXECUTABLE=" + str(ROOT / ".deps/native/python/bin/python3.13"),
|
||||
"-DPython3_INCLUDE_DIR=" + str(python_include),
|
||||
# CMake 3.28 validates its previous internal include path before
|
||||
# applying an updated public path during incremental configure.
|
||||
"-D_Python3_INCLUDE_DIR=" + str(python_include),
|
||||
"-DPython3_LIBRARY=" + str(python / "build/libpython3.13.a"),
|
||||
"-DPXR_PY_UNDEFINED_DYNAMIC_LOOKUP=ON",
|
||||
"-DPXR_BUILD_TESTS=OFF", "-DPXR_BUILD_EXAMPLES=OFF",
|
||||
"-DPXR_BUILD_TUTORIALS=OFF", "-DPXR_BUILD_USD_TOOLS=OFF",
|
||||
"-DPXR_BUILD_USDVIEW=OFF",
|
||||
# Blender's Web USD reader consumes the native shape adapters.
|
||||
# Reuse the existing Emscripten OpenSubdiv package from the main
|
||||
# build tree; do not silently disable imaging and leave headers out
|
||||
# of the install set.
|
||||
"-DPXR_BUILD_IMAGING=ON",
|
||||
"-DPXR_BUILD_USD_IMAGING=ON",
|
||||
"-DOPENSUBDIV_INCLUDE_DIR=" + str(ROOT / "blender-5.2.0/extern/opensubdiv-source"),
|
||||
"-DOPENSUBDIV_CPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdCPU.a"),
|
||||
"-DOPENSUBDIV_GPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdGPU.a"),
|
||||
"-DPXR_BUILD_USD_VALIDATION=OFF",
|
||||
"-DPXR_BUILD_EXEC=OFF", "-DPXR_ENABLE_GL_SUPPORT=OFF",
|
||||
"-DPXR_ENABLE_VULKAN_SUPPORT=OFF", "-DPXR_ENABLE_OPENVDB_SUPPORT=OFF",
|
||||
"-DPXR_ENABLE_MATERIALX_SUPPORT=OFF", "-DPXR_ENABLE_OSL_SUPPORT=OFF",
|
||||
"-DPXR_BUILD_DOCUMENTATION=OFF", "-DPXR_VALIDATE_GENERATED_CODE=OFF",
|
||||
])
|
||||
build_target(build, args.usd_target)
|
||||
if args.usd_target == "install":
|
||||
# Only Python resources belong in the embedded package. Static
|
||||
# wrapper archives in lib/python are linked, never shipped as data.
|
||||
package = INSTALL / "lib/python"
|
||||
output = INSTALL / "lib/webengine-pxr.zip"
|
||||
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for path in sorted(package.rglob("*.py")):
|
||||
archive.write(path, path.relative_to(package))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
63
tools/web/capture-native-last-session.py
Normal file
63
tools/web/capture-native-last-session.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Capture one native GUI quit plus independent background readback; never claim release acceptance."""
|
||||
import argparse, datetime, hashlib, json, os, pathlib, shutil, signal, subprocess
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--blender", default=str(ROOT / "build_blender_native/bin/blender"))
|
||||
parser.add_argument("--input", required=True); parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--timeout", type=int, default=300)
|
||||
args = parser.parse_args(); native_binary = pathlib.Path(args.blender).resolve(); asset_input = pathlib.Path(args.input).resolve(); output = pathlib.Path(args.output).resolve()
|
||||
def sha(path):
|
||||
digest = hashlib.sha256()
|
||||
with pathlib.Path(path).open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1048576), b""): digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
producer = ROOT / "web/tests/fixtures/native-last-session-prepare.py"; reader = ROOT / "web/tests/fixtures/native-last-session-readback.py"
|
||||
companion = asset_input.with_name("ExternalImage.png")
|
||||
assert 0 < args.timeout <= 1800 and asset_input.suffix.lower() == ".blend" and all(path.is_file() for path in (native_binary, asset_input, companion, producer, reader)) and os.access(native_binary, os.X_OK), "Native executable, source fixtures, input blend and original image companion are required"
|
||||
environment = dict(os.environ); gui_prefix = []
|
||||
if not (environment.get("DISPLAY") or environment.get("WAYLAND_DISPLAY")):
|
||||
xvfb = shutil.which("xvfb-run"); assert xvfb, "GUI quit capture requires a live display or existing xvfb-run"
|
||||
gui_prefix = [xvfb, "-a"]; environment.setdefault("LIBGL_ALWAYS_SOFTWARE", "1")
|
||||
output.mkdir(parents=True, exist_ok=False); assets = output / "assets"; assets.mkdir()
|
||||
working_input = assets / asset_input.name; shutil.copy2(asset_input, working_input); shutil.copy2(companion, assets / companion.name)
|
||||
work = output / "native-work"; receipt_path = output / "desktop-baseline.json"
|
||||
state = {"schemaVersion": 1, "kind": "desktop-native-quit-baseline", "phase": "PREPARED", "acceptance": "NOT_CLAIMED", "workDir": str(work), "nativeBinary": {"path": str(native_binary), "sha256": sha(native_binary)}, "sources": {name: {"path": str(path), "sha256": sha(path)} for name, path in (("producer", producer), ("reader", reader), ("capture", pathlib.Path(__file__).resolve()))}, "input": {"source": str(asset_input), "path": str(working_input), "sha256": sha(working_input), "companion": {"source": str(companion), "path": str(assets / companion.name), "sha256": sha(assets / companion.name)}}, "steps": {}}
|
||||
def persist():
|
||||
temporary = receipt_path.with_suffix(".tmp"); temporary.write_text(json.dumps(state, indent=2) + "\n"); temporary.replace(receipt_path)
|
||||
def terminate_owned(process):
|
||||
for sig, grace in ((signal.SIGTERM, 5), (signal.SIGKILL, 5)):
|
||||
try: os.killpg(process.pid, sig)
|
||||
except ProcessLookupError: pass
|
||||
try: process.wait(timeout=grace); return
|
||||
except subprocess.TimeoutExpired: continue
|
||||
state["cleanupPendingPid"] = process.pid; persist(); raise RuntimeError(f"Owned process {process.pid} remains live after bounded cleanup")
|
||||
def run_step(label, command):
|
||||
log = output / f"{label}.log"; step = {"command": command, "log": str(log), "startedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "timeoutSeconds": args.timeout}; state["steps"][label] = step; state["phase"] = label.upper() + "_RUNNING"; persist()
|
||||
with log.open("w") as stream:
|
||||
process = subprocess.Popen(command, cwd=ROOT, env=environment, stdout=stream, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
step["pid"] = process.pid; persist()
|
||||
try: step["exitCode"] = process.wait(timeout=args.timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
step["timedOut"] = True; terminate_owned(process); step["exitCode"] = process.returncode
|
||||
except BaseException:
|
||||
terminate_owned(process); raise
|
||||
step["finishedAt"] = datetime.datetime.now(datetime.timezone.utc).isoformat(); step["logSha256"] = sha(log); persist()
|
||||
if step.get("timedOut") or step["exitCode"] != 0: raise RuntimeError(f"{label} failed or timed out; see {log}")
|
||||
def capture():
|
||||
persist()
|
||||
run_step("gui-producer", gui_prefix + [str(native_binary), "--factory-startup", "--disable-autoexec", "--python-exit-code", "1", "--python", str(producer), "--", "--input", str(working_input), "--work-dir", str(work)])
|
||||
quit_file = work / "native-temp/quit.blend"; assert quit_file.is_file() and quit_file.stat().st_size > 0, "GUI exit did not produce quit.blend"; state["quitFile"] = {"path": str(quit_file), "bytes": quit_file.stat().st_size, "sha256": sha(quit_file)}; persist()
|
||||
run_step("background-reader", [str(native_binary), "--background", "--factory-startup", "--disable-autoexec", "--python-exit-code", "1", "--python", str(reader), "--", "--work-dir", str(work)])
|
||||
readback_path = work / "native-last-session-readback.json"; readback = json.loads(readback_path.read_text())
|
||||
assert readback["kind"] == "desktop-native-quit-readback" and readback["phase"] == "NATIVE_QUIT_READBACK_CAPTURED" and readback["nativeBinary"] == str(native_binary) and readback["quitFile"] == state["quitFile"] and sha(quit_file) == state["quitFile"]["sha256"]
|
||||
state["readback"] = {"path": str(readback_path), "sha256": sha(readback_path)}; state["version"] = readback["version"]; state["platform"] = readback["platform"]
|
||||
assert readback["inputSha256"] == state["input"]["sha256"] and readback["producerSha256"] == state["sources"]["producer"]["sha256"] and readback["readerSha256"] == state["sources"]["reader"]["sha256"] and sha(work / "native-last-session-expected.json") == readback["expectedReceiptSha256"]
|
||||
assert sha(native_binary) == state["nativeBinary"]["sha256"] and all(sha(item["path"]) == item["sha256"] for item in state["sources"].values()), "Native/source inputs changed during capture"
|
||||
assert sha(asset_input) == sha(working_input) == state["input"]["sha256"] and sha(companion) == sha(assets / companion.name) == state["input"]["companion"]["sha256"], "Original or working assets changed"
|
||||
state["phase"] = "DESKTOP_BASELINE_CAPTURED"; state["finishedAt"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
persist(); print(str(receipt_path), flush=True)
|
||||
def capture_interrupted(signum, frame): raise KeyboardInterrupt(f"Capture interrupted by signal {signum}")
|
||||
signal.signal(signal.SIGTERM, capture_interrupted)
|
||||
try: capture()
|
||||
except BaseException as error:
|
||||
state["phase"] = "FAILED_WITH_OWNED_PROCESS_PENDING" if state.get("cleanupPendingPid") else "FAILED"; state["error"] = str(error); persist(); raise
|
||||
8
tools/web/check-autosave-edit-priority.mjs
Normal file
8
tools/web/check-autosave-edit-priority.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import assert from 'node:assert/strict'; import { AutosaveScheduler } from '../../web/app/src/storage/autosave.ts';
|
||||
const timers = new Map(); let serial = 0; globalThis.window = { setTimeout(callback) { timers.set(++serial, callback); return serial; }, clearTimeout(id) { timers.delete(id); } };
|
||||
const fire = async () => { for (const [id, callback] of [...timers]) { timers.delete(id); callback(); } await Promise.resolve(); };
|
||||
const deferred = () => { let release; const promise = new Promise(resolve => { release = resolve; }); return { promise, release }; };
|
||||
const scheduler = new AutosaveScheduler(), edit = deferred(); let entered = 0, commits = 0; scheduler.schedule(async isCurrent => { entered++; await edit.promise; if (isCurrent()) commits++; }, 1);
|
||||
await fire(); assert.equal(entered, 1); scheduler.cancel(); edit.release(); await scheduler.whenIdle(); assert.equal(commits, 0, 'new edit cancels waiting checkpoint');
|
||||
scheduler.schedule(() => { commits++; }, 1); await fire(); await scheduler.whenIdle(); assert.equal(commits, 1, 'recovery reschedules'); scheduler.schedule(() => { commits++; }, 1); scheduler.cancel(); await fire(); assert.equal(commits, 1, 'timer cancellation prevents dispatch');
|
||||
const write = deferred(); let writing = false, finished = false; scheduler.schedule(async () => { writing = true; await write.promise; finished = true; }, 1); await fire(); assert.equal(writing, true); scheduler.cancel(); assert.equal(finished, false); write.release(); await scheduler.whenIdle(); assert.equal(finished, true, 'active write retains ownership until finished'); scheduler.dispose(); console.log(JSON.stringify({status:'PASS',checks:['waiting callback cancellation','recovery reschedule','timer cancellation','active write completion']}));
|
||||
@@ -11,6 +11,9 @@ assert.deepEqual(new Set(catalog.entries.map((entry) => entry.id)), new Set(inve
|
||||
assert.ok(catalog.entries.every((entry) => ["WEB_MAIN", "SERVER_NATIVE", "BROWSER_EQUIVALENT", "UNAVAILABLE"].includes(entry.route)));
|
||||
const browserGroups = new Set(Object.keys(catalog.browserActionGroups ?? {}));
|
||||
assert.ok(catalog.entries.filter((entry) => entry.route === "BROWSER_EQUIVALENT").every((entry) => browserGroups.has(entry.browserActionGroup)), "BROWSER_EQUIVALENT entries must declare an actionable browser group");
|
||||
assert.equal(catalog.entries.find((entry) => entry.identifier === "WM_OT_save_auto_save")?.browserActionGroup, "AUTOSAVE"); assert.equal(catalog.browserActionGroups.AUTOSAVE.command, "saveNativeAutoSave");
|
||||
assert.equal(catalog.entries.find((entry) => entry.identifier === "WM_OT_recover_auto_save")?.browserActionGroup, "RECOVERY"); assert.equal(catalog.browserActionGroups.RECOVERY.command, "recoverNativeAutoSave");
|
||||
assert.equal(catalog.entries.find((entry) => entry.identifier === "WM_OT_recover_last_session")?.browserActionGroup, "LAST_SESSION"); assert.equal(catalog.browserActionGroups.LAST_SESSION.command, "recoverLastSession");
|
||||
assert.ok(catalog.entries.filter((entry) => entry.route === "SERVER_NATIVE").every((entry) => entry.httpRoute === "/api/server-operator-job"), "SERVER_NATIVE entries must declare the HTTP provider route");
|
||||
assert.equal(catalog.behavioralEvidence.verifiedUnits + catalog.behavioralEvidence.unverifiedUnits, catalog.unitCount);
|
||||
assert.equal(catalog.behavioralEvidence.verifiedUnits, 1025);
|
||||
|
||||
@@ -25,8 +25,9 @@ if (index) {
|
||||
} finally {
|
||||
fs.closeSync(catalogHandle);
|
||||
}
|
||||
if (active.length !== 1) violations.push({ code: "INDEX_ACTIVE_TASK_COUNT", detail: `count=${active.length}` });
|
||||
if (current && active[0]?.[0] !== bundle.context.task) violations.push({ code: "INDEX_ACTIVE_TASK_MISMATCH", detail: `${active[0]?.[0] ?? "NONE"}!=${bundle.context.task}` });
|
||||
const terminal = index.activeTask === null && bundle.context.nextTask === null;
|
||||
if (active.length !== (terminal ? 0 : 1)) violations.push({ code: "INDEX_ACTIVE_TASK_COUNT", detail: `count=${active.length}` });
|
||||
if (current && !terminal && active[0]?.[0] !== bundle.context.task) violations.push({ code: "INDEX_ACTIVE_TASK_MISMATCH", detail: `${active[0]?.[0] ?? "NONE"}!=${bundle.context.task}` });
|
||||
}
|
||||
|
||||
assert.equal(violations.length, 0, `context governance violations: ${JSON.stringify(violations)}`);
|
||||
|
||||
@@ -33,8 +33,16 @@ const favicon = await fetch(`${origin}/favicon.svg`);
|
||||
assert.equal(favicon.status, 200);
|
||||
assert.equal(favicon.headers.get("content-type"), "image/svg+xml");
|
||||
|
||||
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome-stable" });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: false });
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome-stable",
|
||||
/* Match web/playwright.config.ts: without a WebGPU/Dawn adapter the native
|
||||
* GPU surface logs three init errors that are environment, not product. */
|
||||
args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader",
|
||||
"--enable-unsafe-webgpu", "--enable-dawn-features=allow_unsafe_apis",
|
||||
"--use-webgpu-adapter=swiftshader"],
|
||||
});
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, ignoreHTTPSErrors: false, acceptDownloads: true });
|
||||
const page = await context.newPage();
|
||||
const consoleErrors = [];
|
||||
const requestFailures = [];
|
||||
@@ -45,17 +53,236 @@ page.on("response", (item) => { if (item.status() >= 400) badResponses.push(`${i
|
||||
await page.goto(`${origin}/`, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
||||
await page.waitForFunction(() => document.body.innerText.includes("WASM ABI: ready"), undefined, { timeout: 120_000 });
|
||||
await page.getByTestId("blend-file-input").setInputFiles(path.resolve("tests/files/web/basic_scene.blend"));
|
||||
await page.waitForFunction(() => Number(document.body.innerText.match(/Objects (\d+)/)?.[1] ?? 0) > 0, undefined, { timeout: 120_000 });
|
||||
const beforeObjects = Number(await page.locator("body").evaluate((node) => node.innerText.match(/Objects (\d+)/)?.[1] ?? 0));
|
||||
await page.getByRole("button", { name: "添加立方体", exact: true }).click();
|
||||
await page.waitForFunction((before) => Number(document.body.innerText.match(/Objects (\d+)/)?.[1] ?? 0) === before + 1, beforeObjects, { timeout: 60_000 });
|
||||
/* Wait for the fixture open itself. The startup General template also leaves a
|
||||
* non-empty object count, so counting objects alone lets a still-running
|
||||
* template open overwrite the fixture and the later mutation never lands. */
|
||||
/* The startup General template also reaches "Engine: SceneIR" with three
|
||||
* objects (Cube, Camera, Light), so neither the status text nor the object
|
||||
* count identifies the fixture open: waiting on those let the template
|
||||
* replace the fixture and the later mutation was discarded with the retired
|
||||
* Main. basic_scene.blend names its objects BasicArea/BasicCamera/BasicCube,
|
||||
* so wait until one of them is the active object. */
|
||||
await page.waitForFunction(() => /Active Basic/.test(document.querySelector('[data-testid="scene-stats"]')?.textContent ?? ""), undefined, { timeout: 120_000 });
|
||||
await page.waitForFunction(() => /Engine: SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ""), undefined, { timeout: 120_000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const sceneStats = async () => page.locator('[data-testid="scene-stats"]').evaluate((node) => node.textContent ?? "");
|
||||
const objectCountFrom = (text) => Number(text.match(/Objects (\d+)/)?.[1] ?? 0);
|
||||
const beforeObjects = objectCountFrom(await sceneStats());
|
||||
/* The product toolbar sits under the native desktop overlay
|
||||
* (section[aria-label="Blender 桌面"], position fixed, z-index 20), so a
|
||||
* normal actionability click is intercepted. Dispatch on the element itself:
|
||||
* this is the same button a user reaches after switching to Web 业务工具. */
|
||||
const steps = [];
|
||||
const started = Date.now();
|
||||
const record = (name, status, detail) => steps.push({ name, status, durationMs: Date.now() - started, detail });
|
||||
record("startup", "PASS", { title: await page.title(), secureContext: await page.evaluate(() => window.isSecureContext) });
|
||||
record("open_fixture", "PASS", { stats: await sceneStats(), objects: beforeObjects });
|
||||
/* A synthetic pointer click lands on the native overlay that covers the Web
|
||||
* toolbar. Dispatch on the button element so the same handler a user reaches
|
||||
* after switching to Web 业务工具 runs. */
|
||||
await page.evaluate(() => document.querySelector('[aria-label="添加立方体"]')?.click());
|
||||
await page.waitForFunction((before) => Number((document.querySelector('[data-testid="scene-stats"]')?.textContent ?? "").match(/Objects (\d+)/)?.[1] ?? 0) === before + 1, beforeObjects, { timeout: 60_000 });
|
||||
const state = await page.evaluate(() => ({ secureContext: window.isSecureContext, crossOriginIsolated: window.crossOriginIsolated, body: document.body.innerText, wasm: performance.getEntriesByType("resource").filter((item) => item.name.includes(".wasm")).map((item) => item.name) }));
|
||||
assert.equal(state.secureContext, true);
|
||||
assert.equal(state.crossOriginIsolated, true);
|
||||
assert.equal(Number(state.body.match(/Objects (\d+)/)?.[1]), beforeObjects + 1);
|
||||
assert.equal(objectCountFrom(await sceneStats()), beforeObjects + 1);
|
||||
if (consoleErrors.length) process.stderr.write(`deployed-console-errors ${JSON.stringify(consoleErrors.slice(0, 6))}\n`);
|
||||
/* Kept as the hard gate for the boot path only. The remaining steps each
|
||||
* record the console errors they observe in their own detail, so a later
|
||||
* failure cannot hide behind this early assertion. */
|
||||
assert.equal(consoleErrors.length, 0);
|
||||
assert.equal(requestFailures.length, 0);
|
||||
assert.equal(badResponses.length, 0);
|
||||
record("add_cube", "PASS", { objects: beforeObjects + 1, consoleErrors: consoleErrors.length });
|
||||
|
||||
/* The remaining report steps keep the same names and count as the historical
|
||||
* 11-step receipt so the coverage denominator does not shrink. Each step
|
||||
* records what actually happened; a step that cannot run is SKIP with the
|
||||
* reason, never PASS. */
|
||||
const sceneObjects = async () => Number((await sceneStats()).match(/Objects (\d+)/)?.[1] ?? -1);
|
||||
const clickLabel = (label) => page.evaluate((value) => {
|
||||
const element = document.querySelector(`[aria-label="${value}"]`);
|
||||
if (!element) return false;
|
||||
element.click();
|
||||
return true;
|
||||
}, label);
|
||||
/* Workspace switches land asynchronously: an observed probe reached Modeling
|
||||
* only between 2s and 4s after the click, so poll the attribute instead of
|
||||
* sleeping a fixed interval that can sit right on the boundary. */
|
||||
const switchWorkspace = async (name, timeout = 30000) => {
|
||||
await page.evaluate((value) => {
|
||||
const tab = [...document.querySelectorAll('nav[aria-label="工作区"] button')].find((node) => (node.textContent ?? '').trim() === value);
|
||||
tab?.click();
|
||||
}, name);
|
||||
try {
|
||||
await page.waitForFunction((value) => document.querySelector('main')?.getAttribute('data-workspace') === value, name, { timeout });
|
||||
await page.waitForTimeout(1200);
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
};
|
||||
/* Native gestures keep engine.native_gesture_history set until the owned WM
|
||||
* handler retires, and capture_history_entry rejects edits meanwhile. Retry
|
||||
* the whole command rather than sleeping blindly; the attempt count is the
|
||||
* measured release window and is reported, not hidden. */
|
||||
const runEdit = async (label, attempts = 8, settle = 2000) => {
|
||||
for (let attempt = 1; attempt <= attempts; attempt++) {
|
||||
await clickLabel(label);
|
||||
await page.waitForTimeout(settle);
|
||||
const status = await page.evaluate(() => document.querySelector('[data-testid="engine-status"]')?.textContent ?? '');
|
||||
if (!status.includes('command failed')) return { ok: true, attempts: attempt, status };
|
||||
}
|
||||
return { ok: false, attempts, status: 'command failed' };
|
||||
};
|
||||
const surface = () => page.evaluate(() => ({
|
||||
workspace: document.querySelector('main')?.getAttribute('data-workspace') ?? null,
|
||||
display: document.querySelector('canvas[aria-label="Blender desktop"]')?.getAttribute('data-native-display') ?? null,
|
||||
area: document.querySelector('main')?.getAttribute('data-editor-workflow-area') ?? null,
|
||||
editors: [...new Set([...document.querySelectorAll('[data-editor-workflow-area]')].map((node) => node.getAttribute('data-editor-workflow-area')))],
|
||||
}));
|
||||
|
||||
try {
|
||||
/* One toolbar add currently lands two entries in the undo stack
|
||||
* (history reads "createPrimitive" then "Native UI"), so a single Undo only
|
||||
* retires the empty native entry. Press until the object count drops and
|
||||
* report how many presses that took: that count is the defect. */
|
||||
const undoBase = await sceneObjects();
|
||||
let undoPresses = 0, undone = undoBase;
|
||||
for (; undoPresses < 3; undoPresses++) {
|
||||
await clickLabel('撤销');
|
||||
await page.waitForTimeout(6000);
|
||||
undone = await sceneObjects();
|
||||
if (undone !== undoBase) break;
|
||||
}
|
||||
undoPresses += 1;
|
||||
let redoPresses = 0, redone = undone;
|
||||
for (; redoPresses < 3; redoPresses++) {
|
||||
await clickLabel('重做');
|
||||
await page.waitForTimeout(6000);
|
||||
redone = await sceneObjects();
|
||||
if (redone === undoBase) break;
|
||||
}
|
||||
redoPresses += 1;
|
||||
record('undo_redo', undone === undoBase - 1 && redone === undoBase ? 'PASS' : 'FAIL', { undoBase, undone, redone, undoPresses, redoPresses, defect: undoPresses > 1 ? 'one add operator pushes two history entries; the extra press is the empty native entry, not the operator' : null });
|
||||
}
|
||||
catch (error) { record('undo_redo', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
const duplicateBase = await sceneObjects();
|
||||
const duplicate = await runEdit('复制对象');
|
||||
const duplicated = duplicate.ok ? await sceneObjects() : duplicateBase;
|
||||
const remove = await runEdit('删除对象');
|
||||
const deleted = remove.ok ? await sceneObjects() : duplicated;
|
||||
record('duplicate_delete', duplicated === duplicateBase + 1 && deleted === duplicateBase ? 'PASS' : 'FAIL', { duplicateBase, duplicated, deleted, duplicate, remove });
|
||||
}
|
||||
catch (error) { record('duplicate_delete', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
const beforeSave = await sceneObjects();
|
||||
const downloading = page.waitForEvent('download', { timeout: 30000 });
|
||||
await clickLabel('保存项目');
|
||||
const download = await downloading;
|
||||
const savedPath = await download.path();
|
||||
const savedBytes = savedPath ? fs.readFileSync(savedPath) : Buffer.alloc(0);
|
||||
/* Playwright stages downloads under a random extension-less name. The blend
|
||||
* format route resolves on the file name, so reopening the staged path fails
|
||||
* with "IO: format route unavailable". Reopen a .blend-named copy instead. */
|
||||
const reopenDir = fs.mkdtempSync('/tmp/wbv2-reopen-');
|
||||
const reopenPath = path.join(reopenDir, download.suggestedFilename());
|
||||
fs.writeFileSync(reopenPath, savedBytes);
|
||||
await page.getByTestId('blend-file-input').setInputFiles(reopenPath);
|
||||
await page.waitForFunction(() => /Engine: SceneIR r\d+/.test(document.querySelector('[data-testid="engine-status"]')?.textContent ?? ''), undefined, { timeout: 120000 });
|
||||
await page.waitForTimeout(3000);
|
||||
const reopenedObjects = await sceneObjects();
|
||||
record('save_reopen', savedBytes.length > 0 && reopenedObjects === beforeSave ? 'PASS' : 'FAIL', {
|
||||
suggestedFilename: download.suggestedFilename(),
|
||||
byteLength: savedBytes.length,
|
||||
sha256: crypto.createHash('sha256').update(savedBytes).digest('hex'),
|
||||
reopenedObjects,
|
||||
});
|
||||
}
|
||||
catch (error) { record('save_reopen', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
await clickLabel('操作搜索');
|
||||
await page.waitForSelector('.operator-search-dialog', { timeout: 20000 });
|
||||
const form = await page.evaluate(() => {
|
||||
const dialog = document.querySelector('.operator-search-dialog');
|
||||
return { present: Boolean(dialog), controls: dialog ? dialog.querySelectorAll('.operator-property-form input, .operator-property-form select').length : 0, results: dialog ? dialog.querySelectorAll('[role="listbox"] button').length : 0 };
|
||||
});
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(800);
|
||||
record('f3_local_operator_properties', form.present ? 'PASS' : 'FAIL', form);
|
||||
}
|
||||
catch (error) { record('f3_local_operator_properties', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
const response = await fetch(`${origin}/api/server-operator-job`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-server-operator-action': 'describe' },
|
||||
body: JSON.stringify({ operator: 'mesh.primitive_cube_add' }),
|
||||
});
|
||||
const text = await response.text();
|
||||
let properties = null;
|
||||
try { properties = JSON.parse(text)?.result?.operator?.properties?.length ?? null; } catch { properties = null; }
|
||||
record('f3_server_operator_properties', response.status === 200 && properties ? 'PASS' : 'FAIL', { status: response.status, properties, body: text.slice(0, 200) });
|
||||
}
|
||||
catch (error) { record('f3_server_operator_properties', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
const beforeSplit = await surface();
|
||||
const switched = await switchWorkspace('Modeling');
|
||||
const afterSplit = await surface();
|
||||
record('browser_equivalent_area_split', switched && afterSplit.workspace === 'Modeling' && afterSplit.area !== beforeSplit.area ? 'PASS' : 'FAIL', { before: beforeSplit, after: afterSplit });
|
||||
}
|
||||
catch (error) { record('browser_equivalent_area_split', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
const tabs = await page.evaluate(() => [...document.querySelectorAll('nav[aria-label="工作区"] button')].map((node) => (node.textContent ?? '').trim()).filter(Boolean));
|
||||
const visited = [];
|
||||
for (const name of ['Layout', 'Modeling', 'Scripting', 'Shading', 'UV Editing']) {
|
||||
const switched = await switchWorkspace(name);
|
||||
visited.push({ name, switched, ...(await surface()) });
|
||||
}
|
||||
await switchWorkspace('Layout');
|
||||
record('workspace_surfaces', visited.every((entry) => entry.switched && entry.workspace === entry.name && entry.area?.includes(entry.name)) ? 'PASS' : 'FAIL', { scope: 'workspace identity and editor area identity; not full functional verification', tabs, visited });
|
||||
}
|
||||
catch (error) { record('workspace_surfaces', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
try {
|
||||
const render = await page.evaluate(() => ({
|
||||
canvases: [...document.querySelectorAll('canvas')].map((canvas) => ({ aria: canvas.getAttribute('aria-label'), width: canvas.width, height: canvas.height })),
|
||||
bodyWidth: document.body.clientWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
wasmResources: performance.getEntriesByType('resource').filter((entry) => entry.name.includes('.wasm')).map((entry) => entry.name),
|
||||
}));
|
||||
record('desktop_render', render.canvases.some((canvas) => canvas.width > 0 && canvas.height > 0) ? 'PASS' : 'FAIL', render);
|
||||
}
|
||||
catch (error) { record('desktop_render', 'FAIL', { error: String(error).slice(0, 200) }); }
|
||||
|
||||
await context.close();
|
||||
await browser.close();
|
||||
process.stdout.write(`deployed-web-blender-ok origin=${origin} wasm=${expectedWasmSha256} range=206 mainMutation=${beforeObjects}->${beforeObjects + 1} chromium=ready\n`);
|
||||
/* Bind the receipt to the exact source tree and candidate so a completion
|
||||
* evaluation can tell this run apart from an older deployment snapshot. */
|
||||
const reportPath = process.env.CHROMIUM_REPORT_PATH ?? null;
|
||||
if (reportPath) {
|
||||
const candidate = process.env.WBV2_PRODUCTION_CANDIDATE ?? null;
|
||||
const candidateSha256 = candidate && fs.existsSync(candidate) ? crypto.createHash("sha256").update(fs.readFileSync(candidate)).digest("hex") : null;
|
||||
const sha = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
const inventory = JSON.parse(fs.readFileSync(path.resolve("tests/golden/WBV2/dynamic-inventory.json"), "utf8"));
|
||||
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify({
|
||||
observedAt: new Date().toISOString(),
|
||||
origin,
|
||||
browser: "Chromium",
|
||||
sourceTreeSha256: inventory.authority.sourceTreeSha256,
|
||||
candidate,
|
||||
candidateSha256,
|
||||
engineWasmSha256: expectedWasmSha256,
|
||||
steps,
|
||||
errors: consoleErrors,
|
||||
failedRequests: requestFailures,
|
||||
badResponses,
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
process.stdout.write(`deployed-web-blender-ok origin=${origin} wasm=${expectedWasmSha256} range=206 mainMutation=${beforeObjects}->${beforeObjects + 1} chromium=ready steps=${steps.length}\n`);
|
||||
|
||||
@@ -23,7 +23,7 @@ assert.deepEqual(contract.responseHeaders.allResponses, {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
"Cross-Origin-Resource-Policy": "same-origin",
|
||||
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
||||
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self' blob:; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
||||
});
|
||||
const routes = Object.fromEntries(contract.responseHeaders.routes.map((route) => [route.id, route]));
|
||||
assert.deepEqual(routes["entry-document"], { id: "entry-document", patterns: ["/", "/index.html"], cacheControl: "no-cache" });
|
||||
|
||||
@@ -178,6 +178,17 @@ try {
|
||||
assert.notEqual(changed.headers.get("etag"), etagFirst);
|
||||
assert.deepEqual(await body(changed), replacement);
|
||||
|
||||
const privatePassword = crypto.randomBytes(24).toString("hex");
|
||||
const privateServer = createDeploymentHttpServer({ root, contract, accessUsername: "test-owner", accessPassword: privatePassword });
|
||||
const privateOrigin = await listenDeploymentHttpServer(privateServer), privateHeaders = { Authorization: `Basic ${Buffer.from(`test-owner:${privatePassword}`).toString("base64")}` };
|
||||
try {
|
||||
assert.equal((await fetch(privateOrigin)).status, 401);
|
||||
assert.equal((await fetch(privateOrigin, { headers: { Authorization: "Basic invalid" } })).status, 401);
|
||||
for (const route of ["server-operator-job", "blender-workflow", "blender-session", "blender-resources"]) assert.equal((await fetch(`${privateOrigin}/api/${route}`, { method: "POST" })).status, 401);
|
||||
const authorized = await fetch(privateOrigin, { headers: privateHeaders }); assert.equal(authorized.status, 200); assert.equal(authorized.headers.get("cache-control"), "private, no-store"); assert.equal(authorized.headers.get("vary"), "Authorization"); assert.deepEqual(await body(authorized), fixtures.get("/index.html"));
|
||||
assert.equal((await fetch(`${privateOrigin}/api/server-operator-job`, { method: "POST", headers: { ...privateHeaders, Origin: "https://untrusted.invalid" } })).status, 403);
|
||||
await assert.rejects(listenDeploymentHttpServer(createDeploymentHttpServer({ root, contract, accessPassword: "" }), { host: "0.0.0.0" }), /WEB_BLENDER_PASSWORD/);
|
||||
} finally { await new Promise((resolve) => privateServer.close(resolve)); }
|
||||
process.stdout.write("deployment-http-ok mime=10 head=4 ranges=5 invalidRanges=6 resumeSha256=verified\n");
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -34,6 +34,8 @@ let catalog = [];
|
||||
let plan = null;
|
||||
let matrix = null;
|
||||
let completed = [];
|
||||
let dynamicInventory = null;
|
||||
let dynamicInventoryManifest = null;
|
||||
|
||||
try {
|
||||
queue = parseQueue();
|
||||
@@ -46,23 +48,39 @@ try {
|
||||
|
||||
const capabilityIndex = queue.currentTask.startsWith("WBV2-");
|
||||
index = json(capabilityIndex ? "tests/golden/WBV2/task-index.json" : "tests/golden/M15-03A/task-index.json");
|
||||
if (index.activeTask !== queue.currentTask) addIssue(issues, "INDEX_ACTIVE_TASK_MISMATCH", `${index.activeTask}!=${queue.currentTask}`);
|
||||
const currentManifestPath = path.join(root, "tests/golden", queue.currentTask, "manifest.json");
|
||||
const currentManifest = fs.existsSync(currentManifestPath) ? JSON.parse(fs.readFileSync(currentManifestPath, "utf8")) : null;
|
||||
const terminal = index.activeTask === null && currentManifest?.status === "done" && currentManifest?.nextTask === null;
|
||||
if (!terminal && index.activeTask !== queue.currentTask) addIssue(issues, "INDEX_ACTIVE_TASK_MISMATCH", `${index.activeTask ?? "NONE"}!=${queue.currentTask}`);
|
||||
const catalogPath = path.join(root, index.catalog.path);
|
||||
if (!fs.existsSync(catalogPath)) addIssue(issues, "CATALOG_MISSING", index.catalog.path);
|
||||
else {
|
||||
catalog = fs.readFileSync(catalogPath, "utf8").trimEnd().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
||||
const active = catalog.filter((entry) => entry.state === "active");
|
||||
if (active.length !== 1) addIssue(issues, "ACTIVE_TASK_COUNT", `count=${active.length}`);
|
||||
else if (active[0].id !== queue.currentTask) addIssue(issues, "CATALOG_ACTIVE_TASK_MISMATCH", `${active[0].id}!=${queue.currentTask}`);
|
||||
if (terminal ? active.length !== 0 : active.length !== 1) addIssue(issues, "ACTIVE_TASK_COUNT", `count=${active.length}`);
|
||||
else if (!terminal && active[0].id !== queue.currentTask) addIssue(issues, "CATALOG_ACTIVE_TASK_MISMATCH", `${active[0].id}!=${queue.currentTask}`);
|
||||
}
|
||||
|
||||
plan = json("tests/golden/M15-03A/next-task-plan.json");
|
||||
matrix = json("docs/BLENDER_WASM_CAPABILITY_MATRIX_TEMPLATE.json");
|
||||
completed = json("tests/golden/M15-03A/completed-gap-tasks.json");
|
||||
dynamicInventory = json("tests/golden/WBV2/dynamic-inventory.json");
|
||||
dynamicInventoryManifest = json("tests/golden/WBV2/dynamic-inventory-manifest.json");
|
||||
} catch (error) {
|
||||
addIssue(issues, "CONTROL_PLANE_READ_ERROR", error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
if (!dynamicInventory || !dynamicInventoryManifest) addIssue(issues, "DYNAMIC_INVENTORY_MISSING", "tests/golden/WBV2/dynamic-inventory.json and manifest");
|
||||
else {
|
||||
if (dynamicInventory.operation !== "BLENDER_52_SOURCE_DYNAMIC_INVENTORY" || !["SOURCE_DERIVED_UNVERIFIED", "SOURCE_DERIVED_WITH_EXPLICIT_COVERAGE"].includes(dynamicInventory.status)) addIssue(issues, "DYNAMIC_INVENTORY_INVALID", "operation/status");
|
||||
if (dynamicInventory.policy?.hardTarget !== false || dynamicInventory.policy?.planningEstimate !== 5200) addIssue(issues, "DYNAMIC_INVENTORY_POLICY_INVALID", "planning estimate must remain non-binding");
|
||||
if ((dynamicInventory.coverage?.verifiedUnits ?? 0) + (dynamicInventory.coverage?.unverifiedUnits ?? 0) !== dynamicInventory.summary?.unitCount) addIssue(issues, "DYNAMIC_INVENTORY_COVERAGE_INVALID", "verified plus unverified must equal source unit count");
|
||||
if (dynamicInventoryManifest.operation !== "BLENDER_52_SOURCE_DYNAMIC_INVENTORY" || dynamicInventoryManifest.status !== "done") addIssue(issues, "DYNAMIC_INVENTORY_MANIFEST_INVALID", "operation/status");
|
||||
for (const artifact of Object.values(dynamicInventoryManifest.artifacts ?? {})) {
|
||||
if (!fs.existsSync(path.join(root, artifact.path)) || sha256(fs.readFileSync(path.join(root, artifact.path))) !== artifact.sha256) addIssue(issues, "DYNAMIC_INVENTORY_MANIFEST_HASH_DRIFT", artifact.path);
|
||||
}
|
||||
}
|
||||
|
||||
const active = catalog.find((entry) => entry.state === "active") ?? null;
|
||||
let legacyCoverage = null;
|
||||
try {
|
||||
@@ -124,6 +142,17 @@ const report = {
|
||||
productiveLane: capabilities.filter((entry) => entry.lifecycleStatus === "done" && ["L2", "L3", "L4", "L5"].includes(entry.parityLevel)).length,
|
||||
rule: "one capability task may close multiple legacy cases; legacy cases remain regression coverage",
|
||||
},
|
||||
dynamicInventory: dynamicInventory ? {
|
||||
path: "tests/golden/WBV2/dynamic-inventory.json",
|
||||
manifestPath: "tests/golden/WBV2/dynamic-inventory-manifest.json",
|
||||
sourceTreeSha256: dynamicInventory.authority.sourceTreeSha256,
|
||||
sourceFiles: dynamicInventory.summary.sourceFiles,
|
||||
unitCount: dynamicInventory.summary.unitCount,
|
||||
planningEstimate: dynamicInventory.policy.planningEstimate,
|
||||
hardTarget: dynamicInventory.policy.hardTarget,
|
||||
verifiedUnits: dynamicInventory.coverage.verifiedUnits,
|
||||
unverifiedUnits: dynamicInventory.coverage.unverifiedUnits,
|
||||
} : null,
|
||||
capabilityClusters,
|
||||
issues,
|
||||
remediation: issues.map(({ code }) => ({
|
||||
|
||||
@@ -26,13 +26,20 @@ function geometry(meshId, positions) {
|
||||
}
|
||||
|
||||
try {
|
||||
const exporterSource = fs.readFileSync(path.join(root, "web/protocol/glb-export.ts"), "utf8");
|
||||
const transpiled = ts.transpileModule(exporterSource, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: "glb-export.ts",
|
||||
});
|
||||
const transpile = (sourceName, outputName) => {
|
||||
const source = fs.readFileSync(path.join(root, "web/protocol", sourceName), "utf8");
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: sourceName,
|
||||
});
|
||||
fs.writeFileSync(path.join(temporary, outputName), transpiled.outputText);
|
||||
};
|
||||
// glb-export imports the non-mesh mapper at runtime; materialize that small
|
||||
// dependency chain beside the transpiled exporter for the standalone checker.
|
||||
transpile("nonmesh-binary.ts", "nonmesh-binary.js");
|
||||
transpile("nonmesh-export.ts", "nonmesh-export.js");
|
||||
transpile("glb-export.ts", "glb-export.cjs");
|
||||
const exporterPath = path.join(temporary, "glb-export.cjs");
|
||||
fs.writeFileSync(exporterPath, transpiled.outputText);
|
||||
const { exportGLB } = await import(pathToFileURL(exporterPath).href);
|
||||
|
||||
const roundTripPositions = [0, 0, 0, 1, 0, 0, 0, 1, 0];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user