Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions packages/core/src/loop/acceptance/acceptance-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
31 changes: 31 additions & 0 deletions packages/core/tests/acceptance-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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);
});