fix(web): reject non-finite numbers, and keep the branch picker with the raw draft

Two more from Copilot's second pass, both real.

- Every JSON-draft parser now rejects a value `JSON.stringify` cannot write
  back. `JSON.parse("1e400")` yields `Infinity`, which serializes as `null` —
  so an editor showing `1e400` would have sent `null`, which is exactly the
  misreport `isSerializableJson` was written for. `parseJsonObjectDraft`
  already guarded; the raw-arguments editor and the Edit-and-replay modal did
  not, and neither did `SchemaJsonField`'s `parseJsonDraft` — the last
  predates this PR but is the same defect in the same family, so it is fixed
  with them rather than left as the odd one out.

- Editing a root union's discriminator in the raw document now moves the
  Variant picker. Nothing in `values` names a branch, so the index is held by
  the form and was only re-derived on `resetKey` or a rewritten union; a raw
  edit had no other signal, so `{"kind":"email"}` → `{"kind":"sms",…}` left
  the picker on Email and switching back to the widgets rendered the Email
  branch over SMS values. The current branch is kept when the values identify
  none, since the discriminator is absent every time it is retyped.

The raw editor is keyed by entity only, not by entity-and-branch like the
per-field editors: it holds the whole arguments object, so a branch move must
re-sync it rather than remount it — a remount would reformat the text being
typed and drop the caret to the top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-27 03:01:47 -04:00
commit b4083ee5fb

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { Button, Group, Modal, Stack, Text } from "@mantine/core";
import { JsonEditor } from "../../elements/JsonEditor/JsonEditor";
import { isJsonObject } from "../../../utils/jsonObjectDraft";
import { isSerializableJson } from "@inspector/core/json/jsonUtils.js";
export interface EditReplayModalProps {
opened: boolean;
@ -34,7 +35,8 @@ const Footer = Group.withProps({ justify: "flex-end", gap: "sm" });
* genuinely carries none, and an empty editor wearing a red error would read as
* broken. A non-object is reported separately from text that is not JSON at
* all: JSON-RPC `params` must be a structured value, so `42` is a shape that
* cannot be sent however finished it is.
* cannot be sent however finished it is. A number that overflows is a third
* case, and the subtlest — see the note on that branch.
*/
function parseParamsDraft(
text: string,
@ -51,6 +53,17 @@ function parseParamsDraft(
if (!isJsonObject(parsed)) {
return { ok: false, error: "Params must be a JSON object (`{ … }`)" };
}
// `JSON.parse` accepts numeric literals it cannot represent: `1e400` parses to
// `Infinity`, which `JSON.stringify` then writes as `null`. Accepting it here
// would show the user one value and send another — the one thing an inspector
// must not do. Same guard, same reason, as `parseJsonObjectDraft`.
if (!isSerializableJson(parsed)) {
return {
ok: false,
error:
"Numbers must be finite — a value like `1e400` overflows and would be sent as null",
};
}
return { ok: true, value: parsed };
}