Skip to content

test: pin the null widget value contract across every persistence path - #15596

Merged
DrJKL merged 5 commits into
feature/ecs-migrationfrom
cb__widget-value-contract
Aug 22, 2026
Merged

test: pin the null widget value contract across every persistence path#15596
DrJKL merged 5 commits into
feature/ecs-migrationfrom
cb__widget-value-contract

Conversation

@christian-byrne

Copy link
Copy Markdown
Contributor

WidgetValue and NodeProperty both admit null, setProperty(name, null) propagates it to the bound widget, and LGraphNode.serialize actively mints it — val ?? null turns an undefined widget value into null on the way into widgets_values. So null is already in first-party workflow JSON with no extension involved.

Nothing asserted that any persistence path round-trips it, and on main one path does not: SubgraphNode.serialize filters through isWidgetValue, whose main predicate rejects null, so a null promoted value becomes undefined and — when every promoted widget is null — the whole widgets_values key is deleted. This branch already fixes the behaviour by widening isWidgetValue to value == null. This PR adds the assertions that were missing on both sides.

I enumerated every persistence path that filters widget values, not just the subgraph one. Verdicts:

Path Filter Round-trips null?
LGraphNode.serialize widget.serialize === false; val ?? null yes — and mints null from undefined
LGraphNode.configure (indexed + named-values) serialize === false, name in namedValues yes
SubgraphNode.serialize isWidgetValue yes on this branch, no on main
SubgraphNode._applyPromotedWidgetValues value !== undefined yes
executionUtil API prompt widget.options?.serialize === false yes
draft / autosave none of its own transparent
workflowSchema load validation z.any() yes
clipboard _serializeItems none of its own transparent
proxyWidgetMigration isWidgetValue yes on this branch, no on main (null classed as a hole)

isWidgetValue has two call sites, not one — the same predicate change fixes both.

New coverage, all mutation-verified (mutating the corresponding filter to drop null fails the test; unmutated control passes):

  • widgetValueNullContract.test.ts — workflow write, workflow read via both restore branches, draft-cache JSON text, two-pass idempotence, plus a widget.serialize === false control arm and a stored-null-vs-absent control arm
  • executionUtil.test.ts — prompt path emits null, with an options.serialize control arm
  • workflowSchema.test.ts — array and object forms preserve null

One trap worth flagging for reviewers: widget state is keyed graphId:nodeId:name and configure() restores the original graph id, so a naive save/load test re-adopts the live widgetValueStore entry and passes without reading the serialized JSON at all. My first draft did exactly that. roundTripGraph drops the store between save and load; without that line four of these tests are vacuous.

ADR 0016 records the contract and the evidence, including the reachability measurement (nine public extension repos assign null to a widget value, against a 4,280-file control arm) and the blast radius of the alternative (removing null from WidgetValue yields 19 vue-tsc errors across 11 files, against a 0-error control).

Gates run by hand — worktrees run no hooks: oxfmt, oxlint --type-aware (clean, verified against a control that emits), vue-tsc --noEmit exit 0 / 0 errors, 52 tests passing.

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bdba0930-6b5a-49d1-8fc8-903d37ce1e3a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 22, 2026
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/22/2026, 06:49:09 AM UTC

Links

🎭 Playwright: ✅ 1989 passed, 0 failed · 1 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1968 / ❌ 0 / ⚠️ 1 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 18 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 74.8 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.0 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.9 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.6 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.3 MB heap
large-graph-pan: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 4ms TBT · 89.7 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 1ms TBT · 70.8 MB heap
legacy-node-drag: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 71.6 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.3 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 83.8 MB heap
subgraph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.6 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.9 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 136ms TBT · 79.2 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 10ms TBT · 88.1 MB heap
vue-large-graph-idle: · 55.4 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 170.0 MB heap
vue-large-graph-pan: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 227ms TBT · 169.2 MB heap
workflow-execution: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.7 MB heap

ℹ️ No baseline found — significance unavailable.

Absolute values
Metric Value
canvas-idle: avg frame time 17ms
canvas-idle: p95 frame time 17ms
canvas-idle: layout duration 0ms
canvas-idle: style recalc duration 6ms
canvas-idle: layout count 0
canvas-idle: style recalc count 7
canvas-idle: task duration 559ms
canvas-idle: script duration 10ms
canvas-idle: TBT 0ms
canvas-idle: heap used 74.8 MB
canvas-idle: DOM nodes -281
canvas-idle: event listeners -157
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 4ms
canvas-mouse-sweep: style recalc duration 44ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 79
canvas-mouse-sweep: task duration 1276ms
canvas-mouse-sweep: script duration 160ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 60.0 MB
canvas-mouse-sweep: DOM nodes -281
canvas-mouse-sweep: event listeners -170
canvas-zoom-sweep: avg frame time 17ms
canvas-zoom-sweep: p95 frame time 17ms
canvas-zoom-sweep: layout duration 2ms
canvas-zoom-sweep: style recalc duration 19ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 31
canvas-zoom-sweep: task duration 389ms
canvas-zoom-sweep: script duration 13ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 69.9 MB
canvas-zoom-sweep: DOM nodes 74
canvas-zoom-sweep: event listeners 19
dom-widget-clipping: avg frame time 17ms
dom-widget-clipping: p95 frame time 17ms
dom-widget-clipping: layout duration 0ms
dom-widget-clipping: style recalc duration 7ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 10
dom-widget-clipping: task duration 444ms
dom-widget-clipping: script duration 34ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 68.6 MB
dom-widget-clipping: DOM nodes 15
dom-widget-clipping: event listeners 1
large-graph-idle: avg frame time 17ms
large-graph-idle: p95 frame time 17ms
large-graph-idle: layout duration 0ms
large-graph-idle: style recalc duration 3ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 4
large-graph-idle: task duration 549ms
large-graph-idle: script duration 39ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 69.3 MB
large-graph-idle: DOM nodes -272
large-graph-idle: event listeners -154
large-graph-pan: avg frame time 17ms
large-graph-pan: p95 frame time 17ms
large-graph-pan: layout duration 0ms
large-graph-pan: style recalc duration 11ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 65
large-graph-pan: task duration 2581ms
large-graph-pan: script duration 928ms
large-graph-pan: TBT 4ms
large-graph-pan: heap used 89.7 MB
large-graph-pan: DOM nodes -141
large-graph-pan: event listeners -87
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 9ms
large-graph-zoom: style recalc duration 13ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 66
large-graph-zoom: task duration 2657ms
large-graph-zoom: script duration 921ms
large-graph-zoom: TBT 1ms
large-graph-zoom: heap used 70.8 MB
large-graph-zoom: DOM nodes -105
large-graph-zoom: event listeners -68
legacy-node-drag: avg frame time 17ms
legacy-node-drag: p95 frame time 17ms
legacy-node-drag: layout duration 0ms
legacy-node-drag: style recalc duration 6ms
legacy-node-drag: layout count 0
legacy-node-drag: style recalc count 41
legacy-node-drag: task duration 2098ms
legacy-node-drag: script duration 210ms
legacy-node-drag: TBT 0ms
legacy-node-drag: heap used 71.6 MB
legacy-node-drag: DOM nodes -144
legacy-node-drag: event listeners 94
minimap-idle: avg frame time 17ms
minimap-idle: p95 frame time 17ms
minimap-idle: layout duration 0ms
minimap-idle: style recalc duration 5ms
minimap-idle: layout count 0
minimap-idle: style recalc count 4
minimap-idle: task duration 587ms
minimap-idle: script duration 41ms
minimap-idle: TBT 0ms
minimap-idle: heap used 68.3 MB
minimap-idle: DOM nodes -273
minimap-idle: event listeners -169
subgraph-dom-widget-clipping: avg frame time 17ms
subgraph-dom-widget-clipping: p95 frame time 17ms
subgraph-dom-widget-clipping: layout duration 0ms
subgraph-dom-widget-clipping: style recalc duration 10ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 45
subgraph-dom-widget-clipping: task duration 547ms
subgraph-dom-widget-clipping: script duration 144ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 83.8 MB
subgraph-dom-widget-clipping: DOM nodes 16
subgraph-dom-widget-clipping: event listeners 8
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 7ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 7
subgraph-idle: task duration 536ms
subgraph-idle: script duration 9ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 57.6 MB
subgraph-idle: DOM nodes -297
subgraph-idle: event listeners -216
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 5ms
subgraph-mouse-sweep: style recalc duration 42ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 79
subgraph-mouse-sweep: task duration 1157ms
subgraph-mouse-sweep: script duration 116ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 69.9 MB
subgraph-mouse-sweep: DOM nodes -297
subgraph-mouse-sweep: event listeners -216
subgraph-transition-enter: avg frame time 17ms
subgraph-transition-enter: p95 frame time 17ms
subgraph-transition-enter: layout duration 14ms
subgraph-transition-enter: style recalc duration 32ms
subgraph-transition-enter: layout count 13
subgraph-transition-enter: style recalc count 18
subgraph-transition-enter: task duration 1025ms
subgraph-transition-enter: script duration 32ms
subgraph-transition-enter: TBT 136ms
subgraph-transition-enter: heap used 79.2 MB
subgraph-transition-enter: DOM nodes 13671
subgraph-transition-enter: event listeners 2383
viewport-pan-sweep: avg frame time 17ms
viewport-pan-sweep: p95 frame time 17ms
viewport-pan-sweep: layout duration 0ms
viewport-pan-sweep: style recalc duration 42ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 248
viewport-pan-sweep: task duration 9225ms
viewport-pan-sweep: script duration 3160ms
viewport-pan-sweep: TBT 10ms
viewport-pan-sweep: heap used 88.1 MB
viewport-pan-sweep: DOM nodes -280
viewport-pan-sweep: event listeners -159
vue-large-graph-idle: avg frame time 18ms
vue-large-graph-idle: p95 frame time 17ms
vue-large-graph-idle: layout duration 0ms
vue-large-graph-idle: style recalc duration 0ms
vue-large-graph-idle: layout count 0
vue-large-graph-idle: style recalc count 0
vue-large-graph-idle: task duration 18519ms
vue-large-graph-idle: script duration 398ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 170.0 MB
vue-large-graph-idle: DOM nodes -6548
vue-large-graph-idle: event listeners -7475
vue-large-graph-pan: avg frame time 18ms
vue-large-graph-pan: p95 frame time 17ms
vue-large-graph-pan: layout duration 0ms
vue-large-graph-pan: style recalc duration 27ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 181
vue-large-graph-pan: task duration 23793ms
vue-large-graph-pan: script duration 1029ms
vue-large-graph-pan: TBT 227ms
vue-large-graph-pan: heap used 169.2 MB
vue-large-graph-pan: DOM nodes -6548
vue-large-graph-pan: event listeners -7466
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 17ms
workflow-execution: layout count 3
workflow-execution: style recalc count 8
workflow-execution: task duration 99ms
workflow-execution: script duration 6ms
workflow-execution: TBT 0ms
workflow-execution: heap used 64.7 MB
workflow-execution: DOM nodes 122
workflow-execution: event listeners 37
Raw data
{
  "timestamp": "2026-08-22T07:01:02.118Z",
  "gitSha": "5aa2526a2c3e872a85cadde285e88b991137aa15",
  "branch": "cb__widget-value-contract",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2092.576000000008,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 4.744,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 570.2230000000001,
      "heapDeltaBytes": 5946428,
      "heapUsedBytes": 75002492,
      "domNodes": -282,
      "jsHeapTotalBytes": 5234688,
      "scriptDurationMs": 9.572000000000001,
      "eventListeners": -159,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-idle",
      "durationMs": 2070.938999999953,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.1979999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 548.659,
      "heapDeltaBytes": 12895128,
      "heapUsedBytes": 81798200,
      "domNodes": -279,
      "jsHeapTotalBytes": 4972544,
      "scriptDurationMs": 11.017,
      "eventListeners": -155,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2268.8980000000356,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 44.42400000000001,
      "layouts": 12,
      "layoutDurationMs": 4.656,
      "taskDurationMs": 1235.96,
      "heapDeltaBytes": -1810320,
      "heapUsedBytes": 67173360,
      "domNodes": -282,
      "jsHeapTotalBytes": 5496832,
      "scriptDurationMs": 160.983,
      "eventListeners": -155,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2241.1189999999124,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 44.43000000000001,
      "layouts": 12,
      "layoutDurationMs": 3.589,
      "taskDurationMs": 1315.6509999999998,
      "heapDeltaBytes": -11290948,
      "heapUsedBytes": 58627976,
      "domNodes": -279,
      "jsHeapTotalBytes": 6807552,
      "scriptDurationMs": 159.353,
      "eventListeners": -185,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1746.1640000000216,
      "styleRecalcs": 30,
      "styleRecalcDurationMs": 19.928,
      "layouts": 6,
      "layoutDurationMs": 2.698,
      "taskDurationMs": 374.481,
      "heapDeltaBytes": 4010000,
      "heapUsedBytes": 73540048,
      "domNodes": 74,
      "jsHeapTotalBytes": 5505024,
      "scriptDurationMs": 11.644,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1736.1799999999903,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 17.279000000000003,
      "layouts": 6,
      "layoutDurationMs": 0.827,
      "taskDurationMs": 404.505,
      "heapDeltaBytes": 3662256,
      "heapUsedBytes": 73040960,
      "domNodes": 74,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 14.335,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 706.7090000000462,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 6.4799999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 415.21100000000007,
      "heapDeltaBytes": -6986400,
      "heapUsedBytes": 62234804,
      "domNodes": 16,
      "jsHeapTotalBytes": 6291456,
      "scriptDurationMs": 32.428,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 743.0480000000443,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 6.629000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 472.40700000000004,
      "heapDeltaBytes": 19611280,
      "heapUsedBytes": 81725004,
      "domNodes": 14,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 35.106,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2050.399000000027,
      "styleRecalcs": 3,
      "styleRecalcDurationMs": 3.0279999999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 545.248,
      "heapDeltaBytes": -17531692,
      "heapUsedBytes": 72939756,
      "domNodes": -272,
      "jsHeapTotalBytes": -1576960,
      "scriptDurationMs": 41.001,
      "eventListeners": -155,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2039.7689999999784,
      "styleRecalcs": 4,
      "styleRecalcDurationMs": 3.594,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 551.9449999999999,
      "heapDeltaBytes": -18126016,
      "heapUsedBytes": 72487908,
      "domNodes": -271,
      "jsHeapTotalBytes": -2625536,
      "scriptDurationMs": 37.168000000000006,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 3209.6460000000206,
      "styleRecalcs": 62,
      "styleRecalcDurationMs": 9.598000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2528.68,
      "heapDeltaBytes": 25101356,
      "heapUsedBytes": 90082196,
      "domNodes": 0,
      "jsHeapTotalBytes": 487424,
      "scriptDurationMs": 916.116,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666696,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 3316.6230000000496,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 13.338999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2633.946,
      "heapDeltaBytes": 6656292,
      "heapUsedBytes": 97995660,
      "domNodes": -282,
      "jsHeapTotalBytes": 4677632,
      "scriptDurationMs": 938.959,
      "eventListeners": -181,
      "totalBlockingTimeMs": 7,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 4356.878999999992,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 12.786999999999999,
      "layouts": 60,
      "layoutDurationMs": 8.485,
      "taskDurationMs": 2539.327,
      "heapDeltaBytes": 11186392,
      "heapUsedBytes": 80104108,
      "domNodes": 24,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 876.0709999999999,
      "eventListeners": 12,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333338,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 4599.658999999974,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 13.267999999999999,
      "layouts": 60,
      "layoutDurationMs": 8.87,
      "taskDurationMs": 2774.797,
      "heapDeltaBytes": -27931220,
      "heapUsedBytes": 68380160,
      "domNodes": -234,
      "jsHeapTotalBytes": 2355200,
      "scriptDurationMs": 965.3389999999999,
      "eventListeners": -147,
      "totalBlockingTimeMs": 1,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 3189.7140000000377,
      "styleRecalcs": 41,
      "styleRecalcDurationMs": 5.8439999999999985,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2105.381,
      "heapDeltaBytes": -1056292,
      "heapUsedBytes": 70738036,
      "domNodes": -288,
      "jsHeapTotalBytes": -1613824,
      "scriptDurationMs": 204.82500000000002,
      "eventListeners": -1,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 3159.057999999959,
      "styleRecalcs": 41,
      "styleRecalcDurationMs": 5.9670000000000005,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 2091.0190000000002,
      "heapDeltaBytes": 13210248,
      "heapUsedBytes": 79368860,
      "domNodes": 0,
      "jsHeapTotalBytes": 417792,
      "scriptDurationMs": 214.456,
      "eventListeners": 188,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2046.3100000000054,
      "styleRecalcs": 4,
      "styleRecalcDurationMs": 4.046000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 580.7390000000001,
      "heapDeltaBytes": -21593884,
      "heapUsedBytes": 70159768,
      "domNodes": -277,
      "jsHeapTotalBytes": 4190208,
      "scriptDurationMs": 39.219,
      "eventListeners": -185,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "minimap-idle",
      "durationMs": 2061.8590000000268,
      "styleRecalcs": 4,
      "styleRecalcDurationMs": 6.620999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 593.37,
      "heapDeltaBytes": -19348368,
      "heapUsedBytes": 73126604,
      "domNodes": -269,
      "jsHeapTotalBytes": -1052672,
      "scriptDurationMs": 42.77899999999999,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 737.3269999999934,
      "styleRecalcs": 44,
      "styleRecalcDurationMs": 8.336000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 514.6789999999999,
      "heapDeltaBytes": 17959468,
      "heapUsedBytes": 88084372,
      "domNodes": 14,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 138.21699999999998,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 798.1359999999995,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 11.587,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 580.283,
      "heapDeltaBytes": 17919204,
      "heapUsedBytes": 87635072,
      "domNodes": 18,
      "jsHeapTotalBytes": 5505024,
      "scriptDurationMs": 150.475,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2017.6329999999894,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 5.675,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 474.36,
      "heapDeltaBytes": 397544,
      "heapUsedBytes": 64025236,
      "domNodes": -299,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 7.589,
      "eventListeners": -231,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2039.3669999999702,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.828999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 598.447,
      "heapDeltaBytes": -12558860,
      "heapUsedBytes": 56857788,
      "domNodes": -295,
      "jsHeapTotalBytes": 5234688,
      "scriptDurationMs": 11.283999999999999,
      "eventListeners": -201,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1872.1720000000346,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 39.964,
      "layouts": 16,
      "layoutDurationMs": 4.898,
      "taskDurationMs": 1117.7659999999998,
      "heapDeltaBytes": -3171368,
      "heapUsedBytes": 66462928,
      "domNodes": -296,
      "jsHeapTotalBytes": 5758976,
      "scriptDurationMs": 110.398,
      "eventListeners": -201,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1968.6719999999696,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 43.365,
      "layouts": 16,
      "layoutDurationMs": 4.882,
      "taskDurationMs": 1196.1840000000002,
      "heapDeltaBytes": 10591380,
      "heapUsedBytes": 80219492,
      "domNodes": -297,
      "jsHeapTotalBytes": 5496832,
      "scriptDurationMs": 120.64299999999999,
      "eventListeners": -231,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1401.8690000000333,
      "styleRecalcs": 18,
      "styleRecalcDurationMs": 32.407000000000004,
      "layouts": 13,
      "layoutDurationMs": 14.106,
      "taskDurationMs": 1024.6620000000003,
      "heapDeltaBytes": -9358772,
      "heapUsedBytes": 83084184,
      "domNodes": 13671,
      "jsHeapTotalBytes": 10747904,
      "scriptDurationMs": 32.049,
      "eventListeners": 2383,
      "totalBlockingTimeMs": 136,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 12314.523999999949,
      "styleRecalcs": 248,
      "styleRecalcDurationMs": 42.016,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 9013.66,
      "heapDeltaBytes": 1678952,
      "heapUsedBytes": 92320904,
      "domNodes": -277,
      "jsHeapTotalBytes": 4644864,
      "scriptDurationMs": 3105.283,
      "eventListeners": -159,
      "totalBlockingTimeMs": 20,
      "frameDurationMs": 16.669999999999952,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 12369.847999999933,
      "styleRecalcs": 247,
      "styleRecalcDurationMs": 42.77499999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 9435.827,
      "heapDeltaBytes": 2145940,
      "heapUsedBytes": 92509972,
      "domNodes": -282,
      "jsHeapTotalBytes": 4382720,
      "scriptDurationMs": 3214.385,
      "eventListeners": -159,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333338,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 18692.655000000002,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 18158.436999999998,
      "heapDeltaBytes": -32593576,
      "heapUsedBytes": 177134392,
      "domNodes": -6548,
      "jsHeapTotalBytes": -1146880,
      "scriptDurationMs": 386.68,
      "eventListeners": -7475,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.77333333333336,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 19162.12399999995,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 18879.808,
      "heapDeltaBytes": -31076892,
      "heapUsedBytes": 179436476,
      "domNodes": -6548,
      "jsHeapTotalBytes": -1122304,
      "scriptDurationMs": 408.451,
      "eventListeners": -7475,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 23739.390000000014,
      "styleRecalcs": 180,
      "styleRecalcDurationMs": 23.73699999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 23479.928,
      "heapDeltaBytes": -43849876,
      "heapUsedBytes": 166800956,
      "domNodes": -6548,
      "jsHeapTotalBytes": -1523712,
      "scriptDurationMs": 951.94,
      "eventListeners": -7467,
      "totalBlockingTimeMs": 255,
      "frameDurationMs": 17.77333333333348,
      "p95FrameDurationMs": 16.700000000004366
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 24221.140000000105,
      "styleRecalcs": 182,
      "styleRecalcDurationMs": 29.79200000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 24106.83,
      "heapDeltaBytes": -17885472,
      "heapUsedBytes": 187957096,
      "domNodes": -6548,
      "jsHeapTotalBytes": -4702208,
      "scriptDurationMs": 1106.0259999999998,
      "eventListeners": -7465,
      "totalBlockingTimeMs": 198,
      "frameDurationMs": 17.776666666666763,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "workflow-execution",
      "durationMs": 130.87399999994886,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 15.862000000000002,
      "layouts": 3,
      "layoutDurationMs": 1.441,
      "taskDurationMs": 102.545,
      "heapDeltaBytes": 3538588,
      "heapUsedBytes": 70083268,
      "domNodes": 131,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 7.528,
      "eventListeners": 25,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "workflow-execution",
      "durationMs": 140.39900000000216,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 18.769,
      "layouts": 3,
      "layoutDurationMs": 1.002,
      "taskDurationMs": 95.72699999999998,
      "heapDeltaBytes": 3408732,
      "heapUsedBytes": 65659008,
      "domNodes": 112,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 5.302,
      "eventListeners": 49,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    }
  ]
}

@github-actions github-actions Bot added the risk:R3 PR risk grade (advisory shadow check; grader-owned) label Aug 22, 2026
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@                   Coverage Diff                    @@
##             feature/ecs-migration   #15596   +/-   ##
========================================================
  Coverage                         ?   82.29%           
========================================================
  Files                            ?     1900           
  Lines                            ?   107781           
  Branches                         ?    34113           
========================================================
  Hits                             ?    88696           
  Misses                           ?    18747           
  Partials                         ?      338           
Flag Coverage Δ
e2e 67.97% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

christian-byrne and others added 2 commits August 22, 2026 04:19
`WidgetValue` and `NodeProperty` both admit `null`, and `LGraphNode.serialize`
mints it (`val ?? null`), but no test asserted that any persistence path
round-trips it. `main`'s `SubgraphNode.serialize` silently drops it.

Adds round-trip coverage for the paths that had none:

- workflow write / read, via both the indexed and named-values restore
  branches, plus draft-cache JSON text and a two-pass idempotence check
- API prompt (`graphToPrompt`), with an `options.serialize` control arm
- load-time schema validation, array and object forms

Each assertion was mutation-verified: mutating the corresponding filter to
drop null fails the test, and the unmutated control passes.

Documents the contract in ADR 0016.
@DrJKL
DrJKL force-pushed the cb__widget-value-contract branch from fced8a9 to 68542bc Compare August 22, 2026 04:28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christian-byrne An ADR, huh?

@DrJKL

DrJKL commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

The ADR does not appear necessary for this PR. This change does not introduce an architectural decision; it tests an existing type and behavior contract. The ADR adds disproportionate scope and maintenance burden. Suggest removing the ADR and its README entry while keeping the focused tests.

@dosubot dosubot Bot added size:XS This PR changes 0-9 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Aug 22, 2026
@DrJKL
DrJKL merged commit 7ec4ffc into feature/ecs-migration Aug 22, 2026
89 of 92 checks passed
@DrJKL
DrJKL deleted the cb__widget-value-contract branch August 22, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:R3 PR risk grade (advisory shadow check; grader-owned) size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants