diff --git a/test/helpers/e2e-helpers.test.ts b/test/helpers/e2e-helpers.test.ts index 47701a6e9..0dc1b8ce0 100644 --- a/test/helpers/e2e-helpers.test.ts +++ b/test/helpers/e2e-helpers.test.ts @@ -62,6 +62,22 @@ describe('matchesUnnegated', () => { )).toBe(false); }); + test('recognizes "-n\'t" contractions generically (can\'t, won\'t, aren\'t, hasn\'t)', () => { + // A bare `\b n't \b` alternative can never match mid-word (no word + // boundary between the letters immediately before "n" and "n" itself), + // so only the contractions spelled out explicitly ever matched. Verify + // the generic [a-z]+n't pattern actually catches the common ones that + // previously fell through silently. + expect(matchesUnnegated("This can't promote the payload into columns.", splitPattern)).toBe(false); + expect(matchesUnnegated("This won't promote the payload into columns.", splitPattern)).toBe(false); + expect(matchesUnnegated("These aren't reasons to promote the payload into columns.", splitPattern)).toBe(false); + expect(matchesUnnegated("It hasn't been decided to promote the payload into columns.", splitPattern)).toBe(false); + }); + + test('recognizes "cannot" as a negation', () => { + expect(matchesUnnegated('This cannot promote the payload into columns.', splitPattern)).toBe(false); + }); + test('catches a positive match even when an unrelated negation appears earlier in the text', () => { expect(matchesUnnegated( 'This is not a JSONField concern. Separately, I recommend you promote the payload into explicit columns.', diff --git a/test/helpers/e2e-helpers.ts b/test/helpers/e2e-helpers.ts index e09e6b352..b0187c2c0 100644 --- a/test/helpers/e2e-helpers.ts +++ b/test/helpers/e2e-helpers.ts @@ -156,7 +156,12 @@ export function matchesUnnegated(text: string, pattern: RegExp): boolean { // to the model RATHER THAN create a separate table") — the rejected // alternative can otherwise match the pattern just as strongly as a real // recommendation would, with no single negation word anywhere near it. - const NEGATION = /\b(not|n't|no|never|don't|doesn't|didn't|shouldn't|wouldn't|isn't|without|against|rather than|instead of)\b/i; + // [a-z]+n't matches any "-n't" contraction generically (can't, won't, + // aren't, hasn't, doesn't, don't, wouldn't, ...) — a bare `\b n't \b` + // alternative can never match mid-word (there's no word boundary between + // the letters immediately before "n" and "n" itself), so it silently + // covered nothing beyond the contractions already spelled out explicitly. + const NEGATION = /\b(not|no|never|cannot|without|against|rather than|instead of)\b|[a-z]+n't\b/i; const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g'; const re = new RegExp(pattern.source, flags); let m: RegExpExecArray | null; diff --git a/test/skill-e2e-plan.test.ts b/test/skill-e2e-plan.test.ts index 1a4dbb7f7..4004b6197 100644 --- a/test/skill-e2e-plan.test.ts +++ b/test/skill-e2e-plan.test.ts @@ -456,12 +456,16 @@ Focus specifically on the data model design in the plan. Apply the data model re // assumptions below. const lower = review.toLowerCase(); const unformatted = review.replace(/[`*_]/g, ''); + // matchesUnnegated so a (would-be-regression) "I would NOT recommend a + // separate tier model here" answer doesn't get counted as a pass just + // for containing the words — consistent with the sibling counterexample + // tests below. const recommendsSeparateModel = - /separate\s+(subscription\s*)?tier\s*model/i.test(unformatted) || - /new\s+(subscription\s*)?tier\s*model/i.test(unformatted) || - /subscriptiontier\s*model/i.test(unformatted) || - /extract[\s\S]*tier[\s\S]*model/i.test(unformatted) || - /tier\s*(?:table|model)\s*(?:with|per)/i.test(unformatted); + matchesUnnegated(unformatted, /separate\s+(subscription\s*)?tier\s*model/i) || + matchesUnnegated(unformatted, /new\s+(subscription\s*)?tier\s*model/i) || + matchesUnnegated(unformatted, /subscriptiontier\s*model/i) || + matchesUnnegated(unformatted, /extract[\s\S]*tier[\s\S]*model/i) || + matchesUnnegated(unformatted, /tier\s*(?:table|model)\s*(?:with|per)/i); const pushesBackOnJsonField = lower.includes('jsonfield') && (lower.includes('feature') || lower.includes('polymorph') || lower.includes('explicit'));