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

@ -88,6 +88,16 @@ describe("EditReplayModal", () => {
expect(screen.getByText(/must be a JSON object/i)).toBeInTheDocument();
});
// `JSON.parse("1e400")` yields `Infinity`, which `JSON.stringify` writes back
// as `null` — so Send would dispatch a number the user never typed while the
// editor still showed what they wrote.
it("disables Send for a number that cannot survive being sent", async () => {
renderWithMantine(<EditReplayModal {...baseProps} />);
await setAceText('{"limit":1e400}');
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
expect(screen.getByText(/Numbers must be finite/)).toBeInTheDocument();
});
it("closes without sending when cancelled", async () => {
const user = userEvent.setup();
const onSend = vi.fn();