Add validation evidence for the v4 Fix node (recreate) fix

This commit is contained in:
Dr.Lt.Data
2026-11-13 22:11:22 +09:00
commit e8177e67ba
12 changed files with 288 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Validation evidence: v4 "Fix node (recreate)" with string node ids
Implementation: `5a14b552389885e214526ffc47e11b10c70d49a3` on `fix/v4-node-fixer-recreate`, targeting `manager-v4`.
- `v4-node-fixer-recreate-pr-assets/annotated/`: the captures with boxes and captions laid over them
- `v4-node-fixer-recreate-pr-assets/screens/`: the unmodified browser captures
- `v4-node-fixer-recreate-pr-assets/evidence.json`: what was captured, from which `node_fixer.js`, with hashes
- `v4-node-fixer-recreate-pr-assets/capture_evidence.cjs`: the script that made the captures
- `manifest.json`: checksums of the files here
+45
View File
@@ -0,0 +1,45 @@
{
"implementation_head": "5a14b552389885e214526ffc47e11b10c70d49a3",
"files": {
"v4-node-fixer-recreate-pr-assets/annotated/green-2-clicked.png": {
"bytes": 83290,
"sha256": "80bdbaf612dd7b943093da8bba5c46600aa17b4feba5d7112af26111f856ef6d"
},
"v4-node-fixer-recreate-pr-assets/annotated/green-3-moved.png": {
"bytes": 111977,
"sha256": "b525d594b2075240182abf4fe3f511f0e659725b40e8a5076e620edf020d5eb5"
},
"v4-node-fixer-recreate-pr-assets/annotated/red-2-clicked.png": {
"bytes": 102151,
"sha256": "418639b288004017e7483822016d444ccd20e0537d5f5e5dffa3923683b6ca07"
},
"v4-node-fixer-recreate-pr-assets/annotated/red-3-moved.png": {
"bytes": 111159,
"sha256": "c60dcbb406fa0ca826474b4eed63c265c745f942e85c1fdfef505c2fd56145f0"
},
"v4-node-fixer-recreate-pr-assets/capture_evidence.cjs": {
"bytes": 5600,
"sha256": "5a0095ea2318b1039114352e1c463d911dc75039ff79801ab66f1ae95ed49f45"
},
"v4-node-fixer-recreate-pr-assets/evidence.json": {
"bytes": 3511,
"sha256": "4cb612b6a83a6a7116006501acb210d648e95e3e64696526d7582da9efbde782"
},
"v4-node-fixer-recreate-pr-assets/screens/green-2-clicked.png": {
"bytes": 69923,
"sha256": "da8466d7f2bb993a664c106f3f8c0522dae9699668594581ae5b222f257e8d88"
},
"v4-node-fixer-recreate-pr-assets/screens/green-3-moved.png": {
"bytes": 90285,
"sha256": "f4469655d55aa5ef68fe295ea48af9b9603690c76fa532c06fe7425f7afbc01f"
},
"v4-node-fixer-recreate-pr-assets/screens/red-2-clicked.png": {
"bytes": 88270,
"sha256": "eb73c9fa42d4b96e817de3e3ff46e48fb97a01729a722bc1451a39f118bb1b8a"
},
"v4-node-fixer-recreate-pr-assets/screens/red-3-moved.png": {
"bytes": 90941,
"sha256": "09dc655c7519d9b3dbbcd53d151e83dfb0c3a0e8c6b0424256229900ba8a14fd"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

@@ -0,0 +1,119 @@
// Captures the Red/Green screens for "Fix node (recreate)".
// Same steps on both sides; only the served node_fixer.js differs.
//
// usage: node capture_evidence.cjs <playwright-core dir> <base url> <label> <out dir>
// label: "red" or "green" (names the output files only)
//
// Steps:
// 1. build CheckpointLoaderSimple -> KSampler -> VAEDecode on an empty canvas
// 2. capture -> <label>-1-before.png
// 3. right-click the KSampler title, click "Fix node (recreate)"
// 4. capture -> <label>-2-clicked.png
// 5. click an empty spot of the canvas (closes a context menu left open)
// 6. drag whatever node is at the KSampler title 290px down
// 7. capture -> <label>-3-moved.png
// Step 6 is there because the replacement is created exactly on top of the
// original, so a leftover original cannot be seen without moving the top node.
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const [pwDir, baseURL, label, outDir] = process.argv.slice(2);
const { chromium } = require(pwDir);
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
const DRAG_DY = 290;
(async () => {
const browser = await chromium.launch({ channel: 'chrome', headless: true });
const page = await (await browser.newContext({ viewport: { width: 1280, height: 800 }, locale: 'en-US' })).newPage();
const pageErrors = [];
page.on('pageerror', (e) => pageErrors.push(e.message));
await page.goto(baseURL, { waitUntil: 'load' });
await page.waitForFunction(() => !!window.LiteGraph?.registered_node_types?.KSampler, { timeout: 60_000 });
await page.waitForTimeout(4000);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
const served = await page.evaluate(async () => {
const text = await (await fetch('/extensions/comfyui-manager-legacy/node_fixer.js')).text();
return { text, line: text.split('\n').find((l) => l.includes('src_node.connect')).trim() };
});
const original = await page.evaluate(() => {
const { app, LiteGraph } = window;
const graph = app.canvas.graph;
graph.clear();
const add = (type, x) => { const n = LiteGraph.createNode(type); n.pos = [x, 150]; graph.add(n); return n; };
const loader = add('CheckpointLoaderSimple', 50);
const sampler = add('KSampler', 500);
const decoder = add('VAEDecode', 900);
loader.connect(0, sampler, 'model');
sampler.connect(0, decoder, 'samples');
app.canvas.ds.offset = [0, 0];
app.canvas.ds.scale = 1;
app.canvas.draw(true, true);
return { id: sampler.id, idType: typeof sampler.id, x: sampler.pos[0] + 60, y: sampler.pos[1] - LiteGraph.NODE_TITLE_HEIGHT / 2 };
});
await page.waitForTimeout(500);
const readNodes = () => page.evaluate((originalId) => {
const { app, LiteGraph } = window;
return app.canvas.graph.nodes.map((n) => ({
id: n.id, type: n.type,
isOriginal: n.id === originalId,
inputsFrom: Object.fromEntries(n.inputs.filter((i) => i.link != null)
.map((i) => [i.name, n.getInputNode(n.findInputSlot(i.name))?.type ?? null])),
outputsTo: (n.outputs?.[0] ? n.getOutputNodes(0) ?? [] : []).map((t) => t.type),
// client-space box including the title bar (canvas at 0,0, scale 1)
box: [n.pos[0], n.pos[1] - LiteGraph.NODE_TITLE_HEIGHT, n.size[0], n.size[1] + LiteGraph.NODE_TITLE_HEIGHT].map(Math.round),
}));
}, original.id);
const shot = async (name) => {
const file = path.join(outDir, name);
await page.screenshot({ path: file });
return { file: name, sha256: sha256(fs.readFileSync(file)) };
};
const before = { nodes: await readNodes(), capture: await shot(`${label}-1-before.png`) };
await page.mouse.click(original.x, original.y, { button: 'right' });
await page.locator('.litecontextmenu .litemenu-entry', { hasText: 'Fix node (recreate)' }).click();
await page.waitForTimeout(500);
const clicked = {
contextMenuOpen: await page.locator('.litecontextmenu').count() > 0,
nodes: await readNodes(),
capture: await shot(`${label}-2-clicked.png`),
};
await page.mouse.click(1000, 520);
await page.waitForTimeout(300);
await page.mouse.move(original.x, original.y);
await page.mouse.down();
await page.mouse.move(original.x, original.y + DRAG_DY, { steps: 15 });
await page.mouse.up();
await page.mouse.move(40, 760);
await page.waitForTimeout(700);
const after = { nodes: await readNodes(), capture: await shot(`${label}-3-moved.png`) };
const result = {
label,
capturedAt: new Date().toISOString(),
baseURL,
frontend: await page.evaluate(() => window.__COMFYUI_FRONTEND_VERSION__ ?? null),
viewport: [1280, 800],
nodeFixer: { sha256: sha256(Buffer.from(served.text, 'utf-8')), connectLine: served.line },
originalSampler: { id: original.id, idType: original.idType },
dragDy: DRAG_DY,
pageErrors,
before,
clicked,
after,
};
fs.writeFileSync(path.join(outDir, `${label}.json`), JSON.stringify(result, null, 2) + '\n');
await browser.close();
console.log(JSON.stringify({
label, frontend: result.frontend, idType: original.idType, line: served.line, pageErrors, menuOpenAfterClick: clicked.contextMenuOpen,
after: after.nodes.map((n) => `${n.type}#${n.id}${n.isOriginal ? '(original)' : ''} in=${JSON.stringify(n.inputsFrom)} out=${JSON.stringify(n.outputsTo)} box=${n.box}`),
}, null, 1));
})().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,115 @@
{
"scenario": "Fix node (recreate) on a KSampler wired CheckpointLoaderSimple -> KSampler -> VAEDecode",
"steps": "see the header of capture_evidence.cjs",
"red": {
"frontend": "1.57.6",
"nodeIdType": "string",
"capturedAt": "2026-11-13T13:03:30.402Z",
"nodeFixer": {
"sha256": "92e88ee61108c4679acb903ecbcd279c2c56f68f61b760a5945f5136b4b8c1cf",
"connectLine": "src_node.connect(link.origin_slot, dest.id, input.name);"
},
"pageErrors": [
"t.findInputSlot is not a function"
],
"contextMenuOpenAfterClick": true,
"kSamplersAtEnd": [
{
"id": "2",
"isOriginal": true,
"inputsFrom": {
"model": "CheckpointLoaderSimple"
},
"outputsTo": [
"VAEDecode"
]
},
{
"id": "4",
"isOriginal": false,
"inputsFrom": {},
"outputsTo": []
}
],
"nodeCountAtEnd": 4
},
"green": {
"frontend": "1.57.6",
"nodeIdType": "string",
"capturedAt": "2026-11-13T13:03:38.668Z",
"nodeFixer": {
"sha256": "d857b9ae4a6a7b171f52d309fbffcc9f60c68eb1a806b3e9bf2be7ebc6fdc305",
"connectLine": "src_node.connect(link.origin_slot, dest, input.name);"
},
"pageErrors": [],
"contextMenuOpenAfterClick": false,
"kSamplersAtEnd": [
{
"id": "4",
"isOriginal": false,
"inputsFrom": {
"model": "CheckpointLoaderSimple"
},
"outputsTo": [
"VAEDecode"
]
}
],
"nodeCountAtEnd": 3
},
"figures": [
{
"figure": "red-2-clicked",
"side": "red",
"step": "Right after clicking \"Fix node (recreate)\"",
"raw": {
"file": "screens/red-2-clicked.png",
"sha256": "eb73c9fa42d4b96e817de3e3ff46e48fb97a01729a722bc1451a39f118bb1b8a"
},
"annotated": {
"file": "annotated/red-2-clicked.png",
"sha256": "418639b288004017e7483822016d444ccd20e0537d5f5e5dffa3923683b6ca07"
}
},
{
"figure": "green-2-clicked",
"side": "green",
"step": "Right after clicking \"Fix node (recreate)\"",
"raw": {
"file": "screens/green-2-clicked.png",
"sha256": "da8466d7f2bb993a664c106f3f8c0522dae9699668594581ae5b222f257e8d88"
},
"annotated": {
"file": "annotated/green-2-clicked.png",
"sha256": "80bdbaf612dd7b943093da8bba5c46600aa17b4feba5d7112af26111f856ef6d"
}
},
{
"figure": "red-3-moved",
"side": "red",
"step": "After dragging the node at the KSampler title 290px down",
"raw": {
"file": "screens/red-3-moved.png",
"sha256": "09dc655c7519d9b3dbbcd53d151e83dfb0c3a0e8c6b0424256229900ba8a14fd"
},
"annotated": {
"file": "annotated/red-3-moved.png",
"sha256": "c60dcbb406fa0ca826474b4eed63c265c745f942e85c1fdfef505c2fd56145f0"
}
},
{
"figure": "green-3-moved",
"side": "green",
"step": "After dragging the node at the KSampler title 290px down",
"raw": {
"file": "screens/green-3-moved.png",
"sha256": "f4469655d55aa5ef68fe295ea48af9b9603690c76fa532c06fe7425f7afbc01f"
},
"annotated": {
"file": "annotated/green-3-moved.png",
"sha256": "b525d594b2075240182abf4fe3f511f0e659725b40e8a5076e620edf020d5eb5"
}
}
],
"note": "annotated/*.png = screens/*.png with an HTML overlay rendered on top; screens/*.png are the unmodified captures."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB