Add validation evidence for the v4 Fix node (recreate) fix
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
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."
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 89 KiB |