diff --git a/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.test.ts b/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.test.ts index 8d2d96088c0..f9fdd366ca7 100644 --- a/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.test.ts @@ -155,3 +155,7 @@ it.each([ it("rejects an asserted pass whose command exit is nonzero", () => { expect(grade(bulletedAccount.replace("exit code 0, matching the file", "exit code 2"))).toBe(false); }); + +it.each(["Friday", "5 PM", "the end of the day"])("does not invent an actor from a by-deadline adjunct: %s", deadline => { + expect(grade(account.replace("You reassigned the task to Riley.", `You reassigned the task to Riley by ${deadline}.`))).toBe(true); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.ts b/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.ts index a979681faf4..c06f57fa528 100644 --- a/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.ts +++ b/tests/runner-e2e/chat-catalog-domains/internal-timeline-narrative.ts @@ -1,4 +1,11 @@ /** Textual calibration only; the owning grader separately verifies command/audit joins. */ +const calendarWords = new Set((["long", "short"] as const).flatMap(style => { + const month = new Intl.DateTimeFormat("en-US", { month: style, timeZone: "UTC" }); + const weekday = new Intl.DateTimeFormat("en-US", { weekday: style, timeZone: "UTC" }); + return Array.from({ length: 12 }, (_, index) => [month.format(Date.UTC(2020, index, 1)), + weekday.format(Date.UTC(2020, 0, index + 1))]).flat().map(word => word.toLowerCase()); +})); + export function truthfulTimelineAccount(input: { reply: string; personName?: string; firstWorker: string; repairWorker: string; }) { @@ -31,9 +38,17 @@ export function truthfulTimelineAccount(input: { }; const handoffMatches = (text: string) => { if (differentNamedPerson(text.replace(/^\s*by\s+/i, "")) || /\b(?:not|never|no)\b[^.!?]{0,50}\b(?:reassign|assign|hand|handoff)/i.test(text)) return []; - const byActors = [...text.matchAll(/\bby\s+(?:(the|a|an)\s+)?/gi)] - .filter(actor => actor[1] || !/^[a-z]+ing\b/.test(text.slice(actor.index + actor[0].length))); - if (byActors.some(actor => !new RegExp(`^${humanActor}`, "i").test(text.slice(actor.index + actor[0].length)))) return []; + const knownHuman = new RegExp(`^${humanActor}`, "i"); + const knownWorker = new RegExp(`^(?:${escape(firstWorker)}|${escape(repairWorker)})\\b`, "i"); + const byActors = [...text.matchAll(/\bby\s+(?:(the|a|an)\s+)?/gi)].filter(actor => { + const after = text.slice(actor.index + actor[0].length); + if (knownHuman.test(after) || knownWorker.test(after)) return true; + if (!actor[1] && /^[a-z]+ing\b/.test(after)) return false; + const word = /^[\p{L}]+/u.exec(after)?.[0]?.toLowerCase(); + return !calendarWords.has(word ?? "") && (/^(?:(?:native|test)\s+)?(?:agent|worker|runner|system|automation|harness|CEO)\b/i.test(after) + || /^[A-Z][\p{L}'-]+\b/u.test(after)); + }); + if (byActors.some(actor => !knownHuman.test(text.slice(actor.index + actor[0].length)))) return []; const actorUnspecified = [...text.matchAll(passiveHandoffToRepairWorker), ...text.matchAll(successorOwnership)].filter(() => !/\b(?:automatically|autonomously|self-assigned)\b/i.test(text)); return [...text.matchAll(new RegExp(handoffToRepairWorker.source, "gi")), ...actorUnspecified].filter(match => {