From d4e5b474706b0557616087a47b133240c6cbe1c6 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Sun, 25 Oct 2026 04:06:53 -0700 Subject: [PATCH] Client: Coerce literal `data-herb-set` on seeded states to the held type Motivation: a state declared with an expression default (kind `seeded`) received a String from a literal `data-herb-set` (e.g. `page=1`), while `data-herb-increment` wrote a Number. `coerceState` only converts for boolean, integer and nil, so seeded kinds fell through to the raw text. The state's type then depended on which action wrote it last, and the `Herb-State` header could carry `"page":"1"` after `"page":2`. Approach: in `assignedValue`, when the declared kind is `seeded`, read the state's current value and coerce the literal to its type: a number for an integer-looking literal when the state holds a number, a boolean for `true`/`false` when it holds a boolean. Otherwise (string, null) the raw text is kept as before. Limitation: if the seeded state currently holds null, there is no type to follow and the literal is still written as a String. The `$value` path is unchanged. Validation: in javascript/packages/client, `npx vitest run test/actions.test.ts` passes (44 tests). The new test fails without the src change and passes with it. Full `npx vitest run`: 772 tests pass; one test file fails to collect because the generated `state-kinds` module is missing in this checkout, unrelated to this change (`tsc` reports the same missing module). Report: https://github.com/marcoroth/herb/issues/2743 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-sonnet-5-5 (via Claude Code) --- .../packages/client/src/actions/actions.ts | 18 ++++++++++++++++++ .../packages/client/test/actions.test.ts | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/javascript/packages/client/src/actions/actions.ts b/javascript/packages/client/src/actions/actions.ts index 8159e8ed8..19f236e42 100644 --- a/javascript/packages/client/src/actions/actions.ts +++ b/javascript/packages/client/src/actions/actions.ts @@ -466,9 +466,27 @@ export class Actions implements ElementObserverDelegate, InstructionsDelegate { return null } + if (kind === "seeded") { + return { value: this.seededLiteral(name, scope, raw) } + } + return { value: coerceState(raw, kind) } } + private seededLiteral(name: string, scope: StateScope, raw: string): StateValue { + const current = this.state.getState(name, { scope }) + + if (typeof current === "number" && /^-?\d+$/.test(raw)) { + return Number(raw) + } + + if (typeof current === "boolean" && (raw === "true" || raw === "false")) { + return raw === "true" + } + + return raw + } + private toggle(element: Element, rest: string): void { const groups: StateGroups = new Map() diff --git a/javascript/packages/client/test/actions.test.ts b/javascript/packages/client/test/actions.test.ts index 7d9ed0e00..9bf2ca239 100644 --- a/javascript/packages/client/test/actions.test.ts +++ b/javascript/packages/client/test/actions.test.ts @@ -28,6 +28,7 @@ const PAGE = `` + `` + `` + + `` + `` + `
` + `