From 8a1ef6fe3a8c4eeba192f7420a41d21b00e4cf5a Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Wed, 29 Jul 2026 08:48:11 +0200 Subject: [PATCH 1/5] fix(acceptance): emit a real calendar date for date-typed fields (validValue) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live deep-chain build (Organization→Project→Task) parked Task: fast gate green but e2e acceptance timed out with the task-form never hiding — i.e. the create was failing, not a transient hiccup as the build's own note guessed. Root cause: validValue (acceptance-spec.ts) had no case for date/datetime/timestamp types, so a dueDate field got the generic '${name}-${seed}' sample ('dueDate-2'). That passes the UI Zod (z.string) and API TypeBox (t.String), but the service inserts it into a timestamptz column and Postgres throws 'invalid input syntax for type timestamp with time zone' -> create 500s -> onSuccess/closeForm never fires -> form stays visible -> 10s waitFor(hidden) times out -> false park. Confirmed at the boundary against the live DB: 'dueDate-3'::timestamptz -> ERROR: invalid input syntax '2024-06-15'::timestamptz -> 2024-06-15 00:00:00+00 (accepted) Fix: validValue returns '2024-06-15' for date/datetime/timestamp types. Date-only so it stays a substring of whatever timestamp representation the row cell renders back (the shows assertion is toContainText). Boolean fields were already fine (checkbox path). Regression test added. typecheck/lint/format clean, 3193/3206 suite green. Follow-ups (filed): empty-optional-date '' also 500s a timestamptz insert (generated form should coerce '' -> undefined for optional date/number); e2e seed resilience to transient socket hang-ups / vitest worker timeouts under load. --- .../src/loop/acceptance/acceptance-spec.ts | 13 +++++ packages/core/tests/acceptance-spec.test.ts | 49 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/core/src/loop/acceptance/acceptance-spec.ts b/packages/core/src/loop/acceptance/acceptance-spec.ts index 70b19c5e..aeaabdac 100644 --- a/packages/core/src/loop/acceptance/acceptance-spec.ts +++ b/packages/core/src/loop/acceptance/acceptance-spec.ts @@ -50,6 +50,19 @@ function validValue( return String(seed + 1); } + if ( + field.type === "date" || + field.type === "datetime" || + field.type === "timestamp" + ) { + // A real calendar date. Date-typed fields land in a DB date/timestamp column, so the generic + // `${name}-${seed}` sample would pass string-level validation (Zod z.string / TypeBox t.String) + // yet throw at the insert ("invalid input syntax for type timestamp") → the create 500s and the + // form never closes → false e2e park. Date-only ("YYYY-MM-DD") so it stays a substring of + // whatever timestamp representation the row cell renders back for the shows assertion. + return "2024-06-15"; + } + if (/url|website/i.test(field.name)) { return `https://example${seed}.com`; } diff --git a/packages/core/tests/acceptance-spec.test.ts b/packages/core/tests/acceptance-spec.test.ts index 2bc09751..5ff254a7 100644 --- a/packages/core/tests/acceptance-spec.test.ts +++ b/packages/core/tests/acceptance-spec.test.ts @@ -540,3 +540,52 @@ test("fieldIsMentioned matches on WORD BOUNDARIES, not raw substring", () => { // And it isn't tripped by an unrelated word either. expect(fieldIsMentioned(acceptField("name"), "the total amount")).toBe(false); }); + +test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (not a `${name}-${seed}` string a timestamp column rejects)", () => { + // Regression: a `date`/`datetime`/`timestamp` field used to fall through to the generic + // "${name}-${seed}" sample (e.g. "dueDate-2"). That passes z.string()/t.String() but the app + // inserts it into a timestamptz column, where Postgres throws "invalid input syntax for type + // timestamp" → the create 500s and the form never closes → false e2e park. + const datePlan: IProductPlan = { + product: "Tracker", + slices: [ + { + entity: { + id: "Task", + desc: "t", + fields: [ + { name: "title", type: "string" }, + { name: "dueDate", type: "date" }, + { name: "startsAt", type: "datetime" }, + ], + relationships: [], + rules: ["title is required"], + }, + ui: { + screens: ["list", "form"], + action: "add", + shows: ["title", "dueDate"], + nav: "Tasks", + }, + verification: { + mustRemainTrue: ["x"], + mustNotHappen: ["a task can be saved without a title"], + acceptanceCheck: "create a task", + }, + }, + ], + }; + + const task = planToAcceptanceSpec(datePlan).entities[0]; + + for (const name of ["dueDate", "startsAt"]) { + const field = task?.fields.find((f) => f.name === name); + + expect(field, `expected field ${name}`).toBeDefined(); + // NOT the generic garbage sample. + expect(field?.valid).not.toContain(`${name}-`); + // A real, parseable YYYY-MM-DD date (what a timestamp column accepts). + expect(field?.valid).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(Number.isNaN(new Date(field?.valid ?? "").getTime())).toBe(false); + } +}); From dab29cf3ce218088e7593315834d3bb5b4aefa70 Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Wed, 29 Jul 2026 08:55:40 +0200 Subject: [PATCH 2/5] test(acceptance): cover timestamp type + strict calendar-date check (panel r1 finding) - Exercise date, datetime AND timestamp (production branch handles all three). - Replace the weak !isNaN(new Date()) check, which accepts normalized-impossible dates (new Date('2024-02-31') rolls to 2024-03-02), with a strict UTC round-trip that rejects them. Self-checks the checker rejects '2024-02-31' and accepts '2024-06-15'. --- packages/core/tests/acceptance-spec.test.ts | 32 ++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/core/tests/acceptance-spec.test.ts b/packages/core/tests/acceptance-spec.test.ts index 5ff254a7..c26c1829 100644 --- a/packages/core/tests/acceptance-spec.test.ts +++ b/packages/core/tests/acceptance-spec.test.ts @@ -546,6 +546,29 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n // "${name}-${seed}" sample (e.g. "dueDate-2"). That passes z.string()/t.String() but the app // inserts it into a timestamptz column, where Postgres throws "invalid input syntax for type // timestamp" → the create 500s and the form never closes → false e2e park. + // A strict calendar-date check: rejects impossible dates that `new Date()` would silently + // normalize (e.g. "2024-02-31" rolls to 2024-03-02, which a naive !isNaN check would accept). + const isRealCalendarDate = (s: string): boolean => { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s); + + if (!m) { + return false; + } + + const [year, month, day] = [Number(m[1]), Number(m[2]), Number(m[3])]; + const dt = new Date(Date.UTC(year, month - 1, day)); + + return ( + dt.getUTCFullYear() === year && + dt.getUTCMonth() === month - 1 && + dt.getUTCDate() === day + ); + }; + + // Sanity-check the checker itself: it must reject a normalized-impossible date. + expect(isRealCalendarDate("2024-02-31")).toBe(false); + expect(isRealCalendarDate("2024-06-15")).toBe(true); + const datePlan: IProductPlan = { product: "Tracker", slices: [ @@ -557,6 +580,7 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n { name: "title", type: "string" }, { name: "dueDate", type: "date" }, { name: "startsAt", type: "datetime" }, + { name: "loggedAt", type: "timestamp" }, ], relationships: [], rules: ["title is required"], @@ -578,14 +602,14 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n const task = planToAcceptanceSpec(datePlan).entities[0]; - for (const name of ["dueDate", "startsAt"]) { + // Cover every date-ish type the production branch handles: date, datetime, timestamp. + for (const name of ["dueDate", "startsAt", "loggedAt"]) { const field = task?.fields.find((f) => f.name === name); expect(field, `expected field ${name}`).toBeDefined(); // NOT the generic garbage sample. expect(field?.valid).not.toContain(`${name}-`); - // A real, parseable YYYY-MM-DD date (what a timestamp column accepts). - expect(field?.valid).toMatch(/^\d{4}-\d{2}-\d{2}$/); - expect(Number.isNaN(new Date(field?.valid ?? "").getTime())).toBe(false); + // A real, valid calendar date (what a timestamp column accepts) — not a normalized impossible one. + expect(isRealCalendarDate(field?.valid ?? "")).toBe(true); } }); From 53e62e59bd3c292a484093586ec4261ef5f0b002 Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Wed, 29 Jul 2026 09:04:16 +0200 Subject: [PATCH 3/5] fix(acceptance): explicit field TYPE beats name heuristics in validValue (panel r2 finding) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A date-typed field whose NAME matches the email heuristic (emailVerifiedAt, lastEmailSentAt) was getting user@example.com because the email name-check ran before the date-type branch — an email string 500s a timestamp-column insert, the same false-park class. Reordered so explicit type checks (email/number/date) run first, then name-based heuristics for string fields. Test covers emailVerifiedAt(timestamp) → real date, not an email. --- packages/core/src/loop/acceptance/acceptance-spec.ts | 12 +++++++++--- packages/core/tests/acceptance-spec.test.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/core/src/loop/acceptance/acceptance-spec.ts b/packages/core/src/loop/acceptance/acceptance-spec.ts index aeaabdac..6f11d4b8 100644 --- a/packages/core/src/loop/acceptance/acceptance-spec.ts +++ b/packages/core/src/loop/acceptance/acceptance-spec.ts @@ -40,9 +40,10 @@ function validValue( field: { name: string; type: string }, seed: number ): string { - const isEmail = field.type === "email" || /email/i.test(field.name); - - if (isEmail) { + // Explicit TYPE wins over name heuristics. A date-typed field must get a real date even when its + // name matches the email heuristic (e.g. `emailVerifiedAt`, `lastEmailSentAt`) — otherwise it gets + // an email string that 500s the timestamp-column insert, the exact false-park this branch fixes. + if (field.type === "email") { return `user${seed}@example.com`; } @@ -63,6 +64,11 @@ function validValue( return "2024-06-15"; } + // Name-based heuristics for otherwise-untyped (string) fields. + if (/email/i.test(field.name)) { + return `user${seed}@example.com`; + } + if (/url|website/i.test(field.name)) { return `https://example${seed}.com`; } diff --git a/packages/core/tests/acceptance-spec.test.ts b/packages/core/tests/acceptance-spec.test.ts index c26c1829..b100811d 100644 --- a/packages/core/tests/acceptance-spec.test.ts +++ b/packages/core/tests/acceptance-spec.test.ts @@ -581,6 +581,8 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n { name: "dueDate", type: "date" }, { name: "startsAt", type: "datetime" }, { name: "loggedAt", type: "timestamp" }, + // Name matches the email heuristic but the TYPE is a date — type must win. + { name: "emailVerifiedAt", type: "timestamp" }, ], relationships: [], rules: ["title is required"], @@ -602,13 +604,15 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n const task = planToAcceptanceSpec(datePlan).entities[0]; - // Cover every date-ish type the production branch handles: date, datetime, timestamp. - for (const name of ["dueDate", "startsAt", "loggedAt"]) { + // Cover every date-ish type the production branch handles, incl. a date field whose name + // matches the email heuristic (emailVerifiedAt) — TYPE must take precedence over name. + for (const name of ["dueDate", "startsAt", "loggedAt", "emailVerifiedAt"]) { const field = task?.fields.find((f) => f.name === name); expect(field, `expected field ${name}`).toBeDefined(); - // NOT the generic garbage sample. + // NOT the generic garbage sample, and NOT an email (the name-precedence trap). expect(field?.valid).not.toContain(`${name}-`); + expect(field?.valid).not.toContain("@"); // A real, valid calendar date (what a timestamp column accepts) — not a normalized impossible one. expect(isRealCalendarDate(field?.valid ?? "")).toBe(true); } From f37b9cd9a83f81787e7c47a06fe37b4669ac39e3 Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Wed, 29 Jul 2026 09:15:32 +0200 Subject: [PATCH 4/5] fix(acceptance): apply email type-precedence symmetrically to negativesFor (panel r3 finding) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 reviewers converged: negativesFor still used f.type === 'email' || /email/i.test(name), so a date/number field named like an email (emailVerifiedAt) got a 'not-an-email' negative. That value reaches a timestamp/number column → 500 (not the expected 400/422) → false park — the same class validValue was just fixed for. Extracted a shared isEmailField(field) helper (explicit type wins over name; number/date types are never email) and used it in BOTH validValue and negativesFor so they can never disagree. Test asserts the negatives array: emailVerifiedAt(timestamp) gets NO not-an-email negative, a real email field DOES (positive control). --- .../src/loop/acceptance/acceptance-spec.ts | 43 ++++++++++++------- packages/core/tests/acceptance-spec.test.ts | 18 +++++++- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/core/src/loop/acceptance/acceptance-spec.ts b/packages/core/src/loop/acceptance/acceptance-spec.ts index 6f11d4b8..0c606958 100644 --- a/packages/core/src/loop/acceptance/acceptance-spec.ts +++ b/packages/core/src/loop/acceptance/acceptance-spec.ts @@ -35,15 +35,34 @@ export function testIdsFor(key: string): ITestIds { }; } +function isDateType(type: string): boolean { + return type === "date" || type === "datetime" || type === "timestamp"; +} + +// Whether a field should be treated as an email. Explicit TYPE wins over the name heuristic: a +// `date`/`number`-typed field named like an email (e.g. `emailVerifiedAt`) is NOT an email — else +// both the positive sample and the invalid-email negative feed a non-string value into a +// date/number column and 500 the insert (a false park). Shared by validValue AND negativesFor so +// the two can never disagree about whether a field is an email. +function isEmailField(field: { name: string; type: string }): boolean { + if (field.type === "email") { + return true; + } + + if (field.type === "number" || isDateType(field.type)) { + return false; + } + + return /email/i.test(field.name); +} + // Deterministic valid sample per field, seeded off (entityIndex, fieldName) — no Date/random. function validValue( field: { name: string; type: string }, seed: number ): string { - // Explicit TYPE wins over name heuristics. A date-typed field must get a real date even when its - // name matches the email heuristic (e.g. `emailVerifiedAt`, `lastEmailSentAt`) — otherwise it gets - // an email string that 500s the timestamp-column insert, the exact false-park this branch fixes. - if (field.type === "email") { + // Explicit TYPE wins over name heuristics (see isEmailField). + if (isEmailField(field)) { return `user${seed}@example.com`; } @@ -51,11 +70,7 @@ function validValue( return String(seed + 1); } - if ( - field.type === "date" || - field.type === "datetime" || - field.type === "timestamp" - ) { + if (isDateType(field.type)) { // A real calendar date. Date-typed fields land in a DB date/timestamp column, so the generic // `${name}-${seed}` sample would pass string-level validation (Zod z.string / TypeBox t.String) // yet throw at the insert ("invalid input syntax for type timestamp") → the create 500s and the @@ -64,11 +79,6 @@ function validValue( return "2024-06-15"; } - // Name-based heuristics for otherwise-untyped (string) fields. - if (/email/i.test(field.name)) { - return `user${seed}@example.com`; - } - if (/url|website/i.test(field.name)) { return `https://example${seed}.com`; } @@ -96,7 +106,10 @@ function negativesFor( out.push({ field: f.name, value: "", why: `${f.name} is required` }); } - if (!isForeignKey && (f.type === "email" || /email/i.test(f.name))) { + // Use the SAME email determination as validValue (type wins over name), so a date/number + // field named like an email (emailVerifiedAt) never gets a "not-an-email" negative — that + // value would reach a timestamp/number column and 500 (not the expected 400/422) → false park. + if (!isForeignKey && isEmailField(f)) { out.push({ field: f.name, value: "not-an-email", diff --git a/packages/core/tests/acceptance-spec.test.ts b/packages/core/tests/acceptance-spec.test.ts index b100811d..7b4fabe4 100644 --- a/packages/core/tests/acceptance-spec.test.ts +++ b/packages/core/tests/acceptance-spec.test.ts @@ -581,8 +581,11 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n { name: "dueDate", type: "date" }, { name: "startsAt", type: "datetime" }, { name: "loggedAt", type: "timestamp" }, - // Name matches the email heuristic but the TYPE is a date — type must win. - { name: "emailVerifiedAt", type: "timestamp" }, + // Name matches the email heuristic but the TYPE is a date — type must win, in BOTH + // the positive sample (validValue) and the negatives (no "not-an-email"). + { name: "emailVerifiedAt", type: "timestamp", optional: true }, + // A genuine email field — positive control: it SHOULD get the not-an-email negative. + { name: "contactEmail", type: "email", optional: true }, ], relationships: [], rules: ["title is required"], @@ -616,4 +619,15 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n // A real, valid calendar date (what a timestamp column accepts) — not a normalized impossible one. expect(isRealCalendarDate(field?.valid ?? "")).toBe(true); } + + // Negatives must apply the SAME type-precedence: a date-typed field named like an email gets + // NO "not-an-email" negative (that value would 500 a timestamp insert, not 400/422 → false park), + // while a genuinely email-typed field DOES. + const emailNegs = (fieldName: string): boolean => + (task?.negatives ?? []).some( + (n) => n.field === fieldName && n.value === "not-an-email" + ); + + expect(emailNegs("emailVerifiedAt")).toBe(false); + expect(emailNegs("contactEmail")).toBe(true); }); From 4eab46059054d96efcbb421254a918a1c6a4b05b Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Wed, 29 Jul 2026 09:31:28 +0200 Subject: [PATCH 5/5] test(acceptance): cover number field named like email (isEmailField type-precedence, panel r4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isEmailField also guards number types (emailCount type=number → number sample + negative-number negative, never email). Added that case to the regression suite: numeric valid value (no '@'), no not-an-email negative, and the '-1' number negative present. --- packages/core/tests/acceptance-spec.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/core/tests/acceptance-spec.test.ts b/packages/core/tests/acceptance-spec.test.ts index 7b4fabe4..530fcdfb 100644 --- a/packages/core/tests/acceptance-spec.test.ts +++ b/packages/core/tests/acceptance-spec.test.ts @@ -586,6 +586,9 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n { name: "emailVerifiedAt", type: "timestamp", optional: true }, // A genuine email field — positive control: it SHOULD get the not-an-email negative. { name: "contactEmail", type: "email", optional: true }, + // Number type whose name matches the email heuristic — type must win here too: a + // numeric sample + the negative-number negative, never an email value/negative. + { name: "emailCount", type: "number", optional: true }, ], relationships: [], rules: ["title is required"], @@ -630,4 +633,17 @@ test("planToAcceptanceSpec: date-typed fields get a REAL calendar date sample (n expect(emailNegs("emailVerifiedAt")).toBe(false); expect(emailNegs("contactEmail")).toBe(true); + + // A NUMBER field named like an email must also resolve by type, not name: numeric valid sample + // (no "@"), and the negative-number negative — never a not-an-email negative. + const emailCount = task?.fields.find((f) => f.name === "emailCount"); + + expect(emailCount?.valid).not.toContain("@"); + expect(emailCount?.valid).toMatch(/^\d+$/); + expect(emailNegs("emailCount")).toBe(false); + expect( + (task?.negatives ?? []).some( + (n) => n.field === "emailCount" && n.value === "-1" + ) + ).toBe(true); });