fix: keep bypass/mute state on frontend 1.53+ node shell state (#1123)

ComfyUI frontend 1.53 turned LGraphNode.mode into a prototype accessor
backed by node._state, and serialize() now reads that state directly.
Redefining mode on the instance shadowed the setter, so bypass/mute
never reached the serialized workflow and silently reverted to Always
on save/reload or workflow tab switch.

Add interceptModeChange() in web/comfyui/utils.js: it delegates to the
prototype accessor when one exists (observing changes only), and falls
back to the legacy closure accessor on older frontends. Use it in
lora_loader.js and in the Vue widgets' setupModeChangeHandler, which
covers the LoRA provider/aggregator nodes with the same latent bug.
This commit is contained in:
Will Miao
2026-09-25 18:12:01 +08:00
parent dae18b3d1d
commit 067e605e75
8 changed files with 360 additions and 120 deletions
@@ -277,5 +277,44 @@ describe("Node mode change handling", () => {
new Set(["LoaderLora1", "LoaderLora2"])
);
});
it("should keep bypass state in the shell state on ECS frontends (issue #1123)", async () => {
// ComfyUI frontend >= 1.53 backs `mode` with a prototype accessor over
// `node._state.mode` and serializes from `_state`; the interceptor must
// delegate to it instead of shadowing it.
class EcsLGraphNode {
constructor() {
this._state = { mode: 0 };
}
get mode() {
return this._state.mode;
}
set mode(value) {
this._state.mode = value;
}
}
const ecsNode = new EcsLGraphNode();
Object.assign(ecsNode, {
comfyClass: "Lora Loader (LoraManager)",
widgets: [
{ name: "text", value: "", options: {}, callback: null },
{ name: "loras", value: [], options: {}, callback: null },
],
addInput: vi.fn(),
graph: {},
});
const nodeType = { comfyClass: "Lora Loader (LoraManager)", prototype: {} };
await extension.beforeRegisterNodeDef(nodeType, {}, {});
nodeType.prototype.onNodeCreated.call(ecsNode);
// Bypass the node: the write must reach the shell state that
// serialization reads from.
ecsNode.mode = 4;
expect(ecsNode._state.mode).toBe(4);
expect(ecsNode.mode).toBe(4);
expect(updateConnectedTriggerWords).toHaveBeenCalledWith(ecsNode, expect.anything());
});
});
});