fix(editor): strip {json, binary} wrapper on Load-from-execution pin

The pin loader was storing port items verbatim in the on-wire shape
[{json: ..., binary: null}]. The executor's WrapPin treats each pin-array
element as one item's whole payload, so the wrapper leaked in: scripts saw
items[0] == {"json": ..., "binary": null} and items[0]["test"] raised
KeyError instead of returning the value.

Now the loader unwraps each item to its .json payload before storing.
Updated the existing test assertion and added a multi-item regression.
This commit is contained in:
Claude
2026-08-31 11:02:34 +00:00
parent 3fa541a032
commit e4e55ea01a
2 changed files with 45 additions and 3 deletions

View File

@@ -119,7 +119,40 @@ describe("PinnedDataSection", () => {
expect(ta.value).toContain(`"loaded": true`);
});
const calls = (props.onChange as ReturnType<typeof vi.fn>).mock.calls;
expect(calls.some(([v]) => JSON.stringify(v) === `[{"json":{"loaded":true}}]`)).toBe(true);
// Wrapper {json, binary} stripped — pin stores raw payloads so scripts see
// `items[0]["loaded"]`, not `items[0]["json"]["loaded"]`.
expect(calls.some(([v]) => JSON.stringify(v) === `[{"loaded":true}]`)).toBe(true);
});
it("Load from execution strips {json, binary} wrapper across multiple items", async () => {
server.use(
http.get("/api/workflows/:id/executions", () =>
HttpResponse.json([
{ id: "e1", workflowId: "wf1", trigger: "manual", status: "completed",
startedAt: "2025-01-01T00:00:00Z", endedAt: null, error: null } satisfies ExecutionSummary,
])),
http.get("/api/executions/e1", () =>
HttpResponse.json({
id: "e1", workflowId: "wf1", trigger: "manual", status: "completed",
startedAt: "", endedAt: "", error: null,
nodes: [
{ nodeId: "n1", status: "completed", startedAt: "", endedAt: "", error: null,
inputs: [[
{ json: { test: "hello" }, binary: null },
{ json: { test: "world" }, binary: null },
]], outputs: [] },
],
} satisfies ExecutionDetail)),
);
const { props } = renderSection({ value: null });
await screen.findByRole("option", { name: /e1/i });
await userEvent.selectOptions(screen.getByLabelText(/load from execution/i), "e1");
await waitFor(() => {
const calls = (props.onChange as ReturnType<typeof vi.fn>).mock.calls;
expect(calls.some(([v]) =>
JSON.stringify(v) === `[{"test":"hello"},{"test":"world"}]`
)).toBe(true);
});
});
it("Load-from-execution for a trigger uses outputs[0] not inputs[0]", async () => {

View File

@@ -77,8 +77,17 @@ export function PinnedDataSection({
if (!row) return;
const source = side === "inputs" ? row.inputs : row.outputs;
const port0 = Array.isArray(source) ? source[0] : undefined;
if (port0 === undefined) return;
const pretty = JSON.stringify(port0, null, 2);
if (!Array.isArray(port0)) return;
// Strip the on-wire {json, binary} wrapper — the executor's WrapPin treats
// each pin array element as one item's whole payload. Storing the wrapper
// verbatim leaks `items[0] == {json:..., binary:null}` into scripts and
// makes property access (`items[0]["test"]`) raise KeyError.
const payloads = port0.map((it) =>
it && typeof it === "object" && "json" in (it as object)
? (it as { json: unknown }).json
: it,
);
const pretty = JSON.stringify(payloads, null, 2);
commit(pretty);
setLoadEid(""); // reset the dropdown to "latest"
}, [loadEid, loadedExec.data]);