diff --git a/frontend/src/editor/graphAdapter.test.ts b/frontend/src/editor/graphAdapter.test.ts index e42abe4..2cdbc8e 100644 --- a/frontend/src/editor/graphAdapter.test.ts +++ b/frontend/src/editor/graphAdapter.test.ts @@ -123,4 +123,44 @@ describe("toGraph — schema-typed coercion on save", () => { ); expect(g.nodes[0].config.status).toBe(200); }); + + it("hex literals on integer fields are not coerced (backend rejects them too)", () => { + const g = toGraph( + [{ + id: "n1", type: "vc", + position: { x: 0, y: 0 }, + data: { typeId: "test.numberfield", config: { status: "0x1F" } }, + }], + [], + [numberNodeType], + ); + // Kept as string so backend surfaces the parse error at run time. + expect(g.nodes[0].config.status).toBe("0x1F"); + }); + + it("scientific-notation strings on integer fields are not coerced (backend rejects them)", () => { + const g = toGraph( + [{ + id: "n1", type: "vc", + position: { x: 0, y: 0 }, + data: { typeId: "test.numberfield", config: { status: "1e2" } }, + }], + [], + [numberNodeType], + ); + expect(g.nodes[0].config.status).toBe("1e2"); + }); + + it("scientific-notation strings on number fields ARE coerced (backend accepts them)", () => { + const g = toGraph( + [{ + id: "n1", type: "vc", + position: { x: 0, y: 0 }, + data: { typeId: "test.numberfield", config: { ratio: "1e2" } }, + }], + [], + [numberNodeType], + ); + expect(g.nodes[0].config.ratio).toBe(100); + }); }); diff --git a/frontend/src/editor/graphAdapter.ts b/frontend/src/editor/graphAdapter.ts index cdaba51..6012d55 100644 --- a/frontend/src/editor/graphAdapter.ts +++ b/frontend/src/editor/graphAdapter.ts @@ -79,11 +79,21 @@ export function coerceConfigForSave( } switch (prop.type) { case "integer": { + // Match backend long.TryParse(NumberStyles.Integer): sign + digits, + // optional leading/trailing whitespace. No hex, no scientific. + if (!/^\s*-?\d+\s*$/.test(value)) { out[name] = value; break; } const n = Number(value); - out[name] = Number.isInteger(n) ? n : value; + out[name] = Number.isSafeInteger(n) ? n : value; break; } case "number": { + // Match backend double.TryParse(NumberStyles.Float): sign + digits + + // optional fractional + optional exponent, optional whitespace. + // Rejects hex (`0x…`). Backend Float style accepts scientific + // notation, so we do too here. + if (!/^\s*-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?\s*$/.test(value)) { + out[name] = value; break; + } const n = Number(value); out[name] = Number.isFinite(n) ? n : value; break; diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index 5b97934..2b77dd4 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -70,7 +70,7 @@ export default function EditorPage() { () => apiFetch(`/api/workflows/${id}`, { method: "PUT", - body: { name: wf.data!.name, graph: toGraph(nodes, edges) }, + body: { name: wf.data!.name, graph: toGraph(nodes, edges, nts.data ?? []) }, }), { onSuccess: () => id && invalidate(qk.workflow(id)) }, );