From a42c6b65426e7b26380b1950326dfedf6f7a14be Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Sat, 25 Jul 2026 13:07:11 +0200 Subject: [PATCH] fix(acceptance): match field names on word boundaries, not raw substring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fieldIsMentioned` (acceptance-spec.ts) associated a field with a plan constraint via `constraintLower.includes(field.name.toLowerCase())` — a raw SUBSTRING match. A short field name is then matched by any longer word that merely contains it: `id` inside `valid`, `age` inside `manage`, so a constraint like "must be a valid email" wrongly reports field `id` as mentioned and fabricates a spurious negative acceptance case for it. Match on WORD BOUNDARIES instead (`\b\b`, escaped), for both the raw and humanized forms. Genuine whole-word mentions (`email` in "a valid email", the humanized "first name" of `firstName`) still match; substring false positives no longer do. `fieldIsMentioned` is now exported and unit-tested (positive: email / name / humanized firstName; negative: id-in-valid, age-in-manage, and an unrelated word). Existing acceptance-spec suite unaffected (12 pass); typecheck + lint clean. --- .../src/loop/acceptance/acceptance-spec.ts | 17 +++++++--- packages/core/tests/acceptance-spec.test.ts | 31 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/core/src/loop/acceptance/acceptance-spec.ts b/packages/core/src/loop/acceptance/acceptance-spec.ts index a6be9c9c..7768cc2f 100644 --- a/packages/core/src/loop/acceptance/acceptance-spec.ts +++ b/packages/core/src/loop/acceptance/acceptance-spec.ts @@ -225,22 +225,29 @@ function buildEntityAcceptance( }; } +function escapeRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + /** * Check if a field is mentioned (by exact name or humanized form) in a constraint. + * Matches on WORD BOUNDARIES, not raw substring: a short field name like `id`/`age` must not + * match a longer word that merely contains it (`valid`, `manage`), which would wrongly + * associate the field with an unrelated constraint and fabricate a spurious negative test. */ -function fieldIsMentioned( +export function fieldIsMentioned( field: IAcceptField, constraintLower: string ): boolean { - const exactMatch = constraintLower.includes(field.name.toLowerCase()); + const mentions = (needle: string): boolean => + needle.length > 0 && + new RegExp(`\\b${escapeRegExp(needle)}\\b`, "u").test(constraintLower); const humanized = field.name .replace(/([A-Z])/g, " $1") .toLowerCase() .trim(); - const humanizedMatch = - humanized.length > 0 && constraintLower.includes(humanized); - return exactMatch || humanizedMatch; + return mentions(field.name.toLowerCase()) || mentions(humanized); } /** diff --git a/packages/core/tests/acceptance-spec.test.ts b/packages/core/tests/acceptance-spec.test.ts index b8bdfad4..91505f6a 100644 --- a/packages/core/tests/acceptance-spec.test.ts +++ b/packages/core/tests/acceptance-spec.test.ts @@ -3,7 +3,9 @@ import type { IProductPlan } from "../src/loop/planning/plan-types"; import { planToAcceptanceSpec, testIdsFor, + fieldIsMentioned, } from "../src/loop/acceptance/acceptance-spec"; +import type { IAcceptField } from "../src/loop/acceptance/acceptance.types"; const plan: IProductPlan = { product: "CRM", @@ -452,3 +454,32 @@ test("FIX 7: mustNotHappen does not duplicate negatives when field already has o // Should NOT have duplicates — only ONE negative for name="" expect(nameEmptyNegatives.length).toBe(1); }); + +function acceptField(name: string): IAcceptField { + return { name, type: "string", optional: false, valid: "x", invalid: [] }; +} + +test("fieldIsMentioned matches on WORD BOUNDARIES, not raw substring", () => { + // Positive: the field name (or its humanized form) appears as a whole word. + expect( + fieldIsMentioned(acceptField("email"), "a valid email is required") + ).toBe(true); + expect(fieldIsMentioned(acceptField("name"), "name must be unique")).toBe( + true + ); + // Humanized: camelCase field, spaced constraint. + expect( + fieldIsMentioned(acceptField("firstName"), "the first name is required") + ).toBe(true); + + // Negative (the bug): a short field name must NOT match a longer word that contains it — + // `id` inside `valid`, `age` inside `manage` — which would fabricate a spurious negative. + expect(fieldIsMentioned(acceptField("id"), "must be a valid email")).toBe( + false + ); + expect(fieldIsMentioned(acceptField("age"), "manage the records")).toBe( + false + ); + // And it isn't tripped by an unrelated word either. + expect(fieldIsMentioned(acceptField("name"), "the total amount")).toBe(false); +});