diff --git a/my-tools/authorize_next_wbv2_task.py b/my-tools/authorize_next_wbv2_task.py index d3e65a59b..ebe52cdf1 100644 --- a/my-tools/authorize_next_wbv2_task.py +++ b/my-tools/authorize_next_wbv2_task.py @@ -440,6 +440,7 @@ PREFERRED_BATCHES = [ # command gate and fixture; no recipe is allowed to infer coverage from a # source filename alone. AUTO_OPERATOR_RECIPES = [ + {"moduleId": "OBJECT_MATERIAL_SLOT_REMOVE_OPERATOR_BATCH", "title": "verify material slot remove operator through native Main", "owner": "OBJECT", "identifier": "OBJECT_OT_material_slot_remove", "fixture": "tests/files/web/basic_scene.blend", "test": "web/tests/e2e/wbv2-material-slot-production.spec.ts", "production": "tests/golden/{task}/material-slot-production.json", "operation": "MATERIAL_SLOT_REMOVE", "slotOperation": "REMOVE"}, {"moduleId": "MESH_DECIMATE_OPERATOR_BATCH", "title": "verify mesh decimate operator through native Main", "owner": "MESH", "identifier": "MESH_OT_decimate", "fixture": "tests/files/web/generated/WBV2-P4-001-MODIFIER.DECLARED_SLICES.blend", "test": "web/tests/e2e/wbv2-modifier-parameter-production.spec.ts", "production": "tests/golden/{task}/modifier-parameter-production.json", "operation": "DECIMATE"}, {"moduleId": "OBJECT_MATERIAL_SLOT_ADD_OPERATOR_BATCH", "title": "verify material slot add operator through native Main", "owner": "OBJECT", "identifier": "OBJECT_OT_material_slot_add", "fixture": "tests/files/web/basic_scene.blend", "test": "web/tests/e2e/wbv2-material-slot-production.spec.ts", "production": "tests/golden/{task}/material-slot-production.json", "operation": "MATERIAL_SLOT_ADD"}, {"moduleId": "OBJECT_JOIN_OPERATOR_BATCH", "title": "verify object join operator through native Main", "owner": "OBJECT", "identifier": "OBJECT_OT_join", "fixture": "tests/files/web/basic_scene.blend", "test": "web/tests/e2e/wbv2-mesh-object-operations-production.spec.ts", "production": "tests/golden/{task}/mesh-object-operations-production.json", "operation": "JOIN"}, @@ -1178,6 +1179,7 @@ def main() -> int: production_env = f"WBV2_BATCH_PRODUCTION={batch['production'].format(task=module_task)}" operation_env = f"WBV2_BATCH_OPERATION={batch['batchOperation']}" if batch.get("batchOperation") else "" clear_mode_env = f"WBV2_BATCH_CLEAR_MODE={batch['clearMode']}" if batch.get("clearMode") else "" + slot_operation_env = f"WBV2_BATCH_SLOT_OPERATION={batch['slotOperation']}" if batch.get("slotOperation") else "" if batch.get("dependencyReuse"): commands = [f"node tools/web/reuse-wbv2-source-dependencies.mjs --task {module_task} --write"] else: @@ -1185,7 +1187,7 @@ def main() -> int: "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 && node tools/web/split-wbv2-wasm.mjs --input web/app/public/vendor/blender/web_engine.wasm --output-dir web/app/public/vendor/blender/modules/single --split-funcs web_engine_get_live_handles && node tools/web/split-wbv2-wasm.mjs --input web/app/public/vendor/blender/pthread/web_engine.wasm --output-dir web/app/public/vendor/blender/modules/pthread --split-funcs web_engine_get_live_handles", "npm --prefix web run typecheck", "npm --prefix web run build", - f"WBV2_BATCH_TASK={module_task} {production_env} {fixture_env} {operation_env} {clear_mode_env} WBV2_BATCH_TYPES='{batch_types}' WBV2_BATCH_UNITS='{batch_units}' WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/{Path(batch['test']).name}", + f"WBV2_BATCH_TASK={module_task} {production_env} {fixture_env} {operation_env} {clear_mode_env} {slot_operation_env} WBV2_BATCH_TYPES='{batch_types}' WBV2_BATCH_UNITS='{batch_units}' WEB_TEST_PORT=5371 npm --prefix web run test:e2e -- --workers=1 tests/e2e/{Path(batch['test']).name}", ] root_record = {"id": root_task, "phase": str(phase), "title": "authorize_modifier_reader_batch", "kind": "enabling", "owner": "PROGRAM", "dependsOn": [current], "parityId": None, "implementationClass": "QUEUE_ONLY", "parallelGroup": f"module-root-{phase}", "gate": "MODULE", "card": f"docs/tasks/{root_task}.md", "behavior": "Authorize one shared source-backed modifier reader batch.", "outputs": ["successor authorization", "module plan", "manifest", "status"], "acceptance": ["prior evidence remains hash-bound", "batch source mapping is explicit", "inventory remains source-derived"], "commands": ["node tools/web/check-execution-control-plane.mjs"], "status": "done", "ready": False, "queueMutationAllowed": True} module_record = {"id": module_task, "phase": str(phase), "title": batch["title"], "kind": "module", "owner": batch["owner"], "dependsOn": [root_task], "parityId": batch["moduleId"], "implementationClass": "LOCAL_EXACT", "parallelGroup": f"module-{batch['moduleId'].lower()}", "gate": "MODULE", "card": f"docs/tasks/{module_task}.md", "behavior": f"Verify the {batch['moduleId']} batch through Blender Main, WASM and Chromium recovery.", "outputs": ["aggregate module receipt", "coverage bindings", "manifest", "status"], "acceptance": ["source and protocol hashes are bound", "every selected Main reader type is proven", "negative no-mutation and recovery are proven", "Chromium and save/reopen evidence are exact"], "status": "in_progress", "ready": True, "queueMutationAllowed": True, "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "module": {"moduleId": batch["moduleId"], "batchId": contract["batchId"], "selectedRecordIds": contract["selectedRecordIds"], "sourceBacked": True, "capabilities": [f"CAP-{batch['moduleId']}-READER", f"CAP-{batch['moduleId']}-RECOVERY", f"CAP-{batch['moduleId']}-FAIL-CLOSED"], "sourcePaths": source_paths, "reuseReceipts": batch.get("reuseReceipts", [{"manifest": "tests/golden/WBV2-P1107-000/manifest.json", "report": "tests/golden/WBV2-P1107-000/module-receipt.json"}]), "dependencyReuse": batch.get("dependencyReuse", []), "positive": [f"{name} type is read from Blender Main" for name in batch["identifiers"]] + ["shared RNA metadata is preserved", "save/reopen preserves the batch metadata"], "negative": ["malformed blend is rejected without Main mutation", "unsupported batch data fails closed", "non-finite metadata is not synthesized"], "recovery": ["save/reopen", "worker termination", "deterministic second read"], "commands": commands, "fullFamilyParity": False, "evidencePaths": [batch["production"].format(task=module_task)], "coverageBindings": contract["selectedRecordIds"]}} diff --git a/web/tests/e2e/wbv2-material-slot-production.spec.ts b/web/tests/e2e/wbv2-material-slot-production.spec.ts index 9ce80da2a..3bdd21e0c 100644 --- a/web/tests/e2e/wbv2-material-slot-production.spec.ts +++ b/web/tests/e2e/wbv2-material-slot-production.spec.ts @@ -10,6 +10,7 @@ const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex"); const task = process.env.WBV2_BATCH_TASK ?? "WBV2-P1100-003"; const productionPath = process.env.WBV2_BATCH_PRODUCTION ?? `tests/golden/${task}/material-slot-production.json`; const sourceUnits = JSON.parse(process.env.WBV2_BATCH_UNITS ?? "[\"operator:OBJECT_OT_material_slot_add\"]") as string[]; +const slotOperation = process.env.WBV2_BATCH_SLOT_OPERATION ?? "ADD"; test("WBV2-P1100-003 preserves addMaterialSlot through Main recovery and reopen", async ({ page }) => { test.setTimeout(180_000); @@ -25,10 +26,11 @@ test("WBV2-P1100-003 preserves addMaterialSlot through Main recovery and reopen" if (!object) throw new Error("basic scene has no mesh object"); const before = materialNames(opened.snapshot); const added = await client.applyCommand({ type: "addMaterialSlot", objectId: object.id, name: "WebSlotMaterial" } as any); - const afterAdd = materialNames(added.snapshot); + const removed = input.slotOperation === "REMOVE" ? await client.applyCommand({ type: "removeMaterialSlot", objectId: object.id, slotIndex: (added.snapshot.nodes.find((node: any) => node.id === object.id)?.materialSlotIds?.length ?? 1) - 1 } as any) : added; + const afterAdd = materialNames(removed.snapshot); const undo = await client.applyCommand({ type: "undo" } as any); const redo = await client.applyCommand({ type: "redo" } as any); - const malformed = await client.applyCommand({ type: "addMaterialSlot", objectId: "object:missing", name: "Bad" } as any).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message })); + const malformed = await client.applyCommand({ type: input.slotOperation === "REMOVE" ? "removeMaterialSlot" : "addMaterialSlot", objectId: "object:missing", ...(input.slotOperation === "REMOVE" ? { slotIndex: 0 } : { name: "Bad" }) } as any).then(() => ({ code: "NO_ERROR" })).catch((error: any) => ({ code: error?.code, message: error?.message })); const afterMalformed = await client.snapshot(); const saved = await client.saveBlend(); const reopened = await reopenedClient.openBlend(saved.slice(0)); @@ -36,15 +38,21 @@ test("WBV2-P1100-003 preserves addMaterialSlot through Main recovery and reopen" } catch (error: any) { return { error: { code: error?.code, message: error?.message, detail: String(error) } } as any; } finally { client.terminate(); reopenedClient.terminate(); } - }, { bytes: Array.from(fixture), fixtureSha256 }); + }, { bytes: Array.from(fixture), fixtureSha256, slotOperation }); fs.mkdirSync(path.dirname(path.join(root, productionPath)), { recursive: true }); - fs.writeFileSync(path.join(root, productionPath), `${JSON.stringify({ schemaVersion: 1, task, operation: "MATERIAL_SLOT_MAIN_CHROMIUM", browser: "chromium", queueMutation: false, fixture: fixturePath, fixtureSha256, sourceUnits, ...result }, null, 2)}\n`); + fs.writeFileSync(path.join(root, productionPath), `${JSON.stringify({ schemaVersion: 1, task, operation: `MATERIAL_SLOT_${slotOperation}_MAIN_CHROMIUM`, browser: "chromium", queueMutation: false, fixture: fixturePath, fixtureSha256, sourceUnits, ...result }, null, 2)}\n`); expect(result.error).toBeUndefined(); expect(result.fixtureSha256).toBe(fixtureSha256); - expect(result.afterAdd).toContain("WebSlotMaterial"); + if (slotOperation === "REMOVE") expect(result.afterAdd).not.toContain("WebSlotMaterial"); + else expect(result.afterAdd).toContain("WebSlotMaterial"); expect(result.addRevision).toBe(result.beforeRevision + 1); - expect(result.undo).toEqual(result.before); - expect(result.redo).toEqual(result.afterAdd); + if (slotOperation === "REMOVE") { + expect(result.undo).toContain("WebSlotMaterial"); + expect(result.redo).not.toContain("WebSlotMaterial"); + } else { + expect(result.undo).toEqual(result.before); + expect(result.redo).toEqual(result.afterAdd); + } expect(result.malformed.code).not.toBe("NO_ERROR"); expect(result.malformedRevisionPreserved).toBe(true); expect(result.reopened).toEqual(result.afterAdd);