mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(evals): bind timeline handoffs to saved task identifiers
Root accepted exact staged tree464be838 after268focusedcontrols and fullE2Etypecheck; no additional source changes. Co-Authored-By: Paperclip <paperclip@paperclip.ing>
This commit is contained in:
1 parent
91310507eb
commit
5aef4bffa2
4 files changed
+26
-7
No files matched your search
@@ -258,7 +258,7 @@
|
||||
"../../server/src/services/native-runtime/status-decision-committer.ts",
|
||||
"../../server/src/services/work-timeline.ts"
|
||||
],
|
||||
"oracleVersion": "paperclip.chat-catalog.work-timeline.v8",
|
||||
"oracleVersion": "paperclip.chat-catalog.work-timeline.v9",
|
||||
"coverageGaps": [],
|
||||
"expectedRunCount": 3,
|
||||
"minimumRunCount": 3,
|
||||
|
||||
@@ -7,7 +7,7 @@ const calendarWords = new Set((["long", "short"] as const).flatMap(style => {
|
||||
}));
|
||||
|
||||
export function truthfulTimelineAccount(input: {
|
||||
reply: string; personName?: string; firstWorker: string; repairWorker: string;
|
||||
reply: string; personName?: string; taskIdentifier?: string; firstWorker: string; repairWorker: string;
|
||||
}) {
|
||||
const { reply, firstWorker, repairWorker } = input;
|
||||
if (!firstWorker || !repairWorker || firstWorker === repairWorker) return false;
|
||||
@@ -29,7 +29,8 @@ export function truthfulTimelineAccount(input: {
|
||||
const assignment = "\\b(?:reassign(?:ed|ment)?|assign(?:ed|ment)?|hand(?:ed)?(?:\\s+\\w+){0,4}\\s+(?:off|over)|handed|handoff)\\b";
|
||||
const target = `\\bto\\s+${escape(repairWorker)}\\b`;
|
||||
const actorAction = `${humanActor}(?:\\s+(?:then|later|now|personally|explicitly|actually|had|has|have|also)){0,3}\\s+${assignment}`;
|
||||
const passiveAction = `(?:\\b(?:was|got|had been|has been)\\s+${assignment}|\\b(?:the|same|this)\\s+(?:task|work)\\s+(?:reassigned|assigned|handed\\s+(?:off|over))\\b)`;
|
||||
const taskSubject = `(?:(?:the|same|this)\\s+(?:task|work)${input.taskIdentifier ? `|${escape(input.taskIdentifier)}` : ""})`;
|
||||
const passiveAction = `(?:\\b(?:was|got|had been|has been)\\s+${assignment}|\\b${taskSubject}\\s+(?:reassigned|assigned|handed\\s+(?:off|over))\\b)`;
|
||||
const handoffToRepairWorker = new RegExp(`${actorAction}${withinClause}{0,60}${target}|${passiveAction}${withinClause}{0,60}${target}${withinClause}{0,40}\\bby\\s+(?:(?:the|a|an)\\s+)?${humanActor}`, "i");
|
||||
const passiveHandoffToRepairWorker = new RegExp(`${passiveAction}${withinClause}{0,60}${target}`, "gi");
|
||||
const successorOwnership = new RegExp(`\\b${escape(repairWorker)}\\s+took\\s+(?:over\\s+(?:the\\s+)?(?:same\\s+)?(?:task|work)|over\\s+from\\s+${escape(firstWorker)}|over(?=\\s*(?:[,;.!?\\n]|$))|the\\s+same\\s+(?:task|work))\\b`, "gi");
|
||||
|
||||
@@ -24,7 +24,7 @@ function fixture() {
|
||||
const first = { id: "first", companyId: "company", agentId: "morgan", status: "succeeded", contextSnapshot: { issueId: "task" }, resultJson: { decisionId: "block-decision", assessmentId: "assessment-first", issueStatusBefore: "in_progress", issueStatusAfter: "blocked" } };
|
||||
const second = { ...first, id: "second", agentId: "riley", resultJson: { decisionId: "done-decision", assessmentId: "assessment-second", issueStatusBefore: "in_progress", issueStatusAfter: "done" } };
|
||||
const blocked = { id: "task", companyId: "company", status: "blocked", assigneeAgentId: "morgan", lastStatusDecisionId: "block-decision" };
|
||||
const done = { ...blocked, status: "done", assigneeAgentId: "riley", lastStatusDecisionId: "done-decision" };
|
||||
const done = { ...blocked, identifier: "RUN-3", status: "done", assigneeAgentId: "riley", lastStatusDecisionId: "done-decision" };
|
||||
const blockedEvent = { ...producer("blocked", "first", "block-decision"), id: "blocked", createdAt: "2026-10-26T01:00:00Z" };
|
||||
const doneEvent = { ...producer("done", "second", "done-decision"), id: "done", createdAt: "2026-10-26T03:00:00Z" };
|
||||
return { scenarioId: "activity.work-timeline", origin: "https://fixture.test", companyId: "company", leadId: "ceo",
|
||||
@@ -57,6 +57,24 @@ it("does not move the repair before a reduced-passive handoff", () => {
|
||||
o.reply = "Morgan ran verification which failed and work paused. Riley repaired the label and the check passed. Same task reassigned to Riley.";
|
||||
expect(gradeWorkTimeline(o).passed).toBe(false);
|
||||
});
|
||||
it.each([
|
||||
{ subject: "RUN-3", successor: "Riley", expected: true },
|
||||
{ subject: "RUN-9", successor: "Riley", expected: false },
|
||||
{ subject: "RUN-3", successor: "Morgan", expected: false },
|
||||
])("binds an identified task handoff to saved identity and successor: $subject / $successor", ({ subject, successor, expected }) => {
|
||||
const o = fixture();
|
||||
o.reply = `Morgan ran verification which failed and work paused.\n\n## Repair — ${subject} reassigned to ${successor}.\nRiley repaired the label and the check passed.`;
|
||||
expect(gradeWorkTimeline(o).passed).toBe(expected);
|
||||
});
|
||||
it("requires saved task identity and chronological repair for an identified handoff", () => {
|
||||
const o = fixture();
|
||||
o.reply = "Morgan ran verification which failed and work paused. RUN-3 reassigned to Riley. Riley repaired the label and the check passed.";
|
||||
Reflect.deleteProperty(o.state.task, "identifier");
|
||||
expect(gradeWorkTimeline(o).passed).toBe(false);
|
||||
o.state.task.identifier = "RUN-3";
|
||||
o.reply = "Morgan ran verification which failed and work paused. Riley repaired the label and the check passed. RUN-3 reassigned to Riley.";
|
||||
expect(gradeWorkTimeline(o).passed).toBe(false);
|
||||
});
|
||||
it("accepts the successor's natural ownership transition in the ordered account", () => {
|
||||
const o = fixture();
|
||||
o.reply = o.reply.replace("Then a human reassigned the task to Riley.", "Then Riley took over the task.");
|
||||
@@ -251,7 +269,7 @@ it("admits the selected live timeline metadata without invoking fixture callback
|
||||
const spies = callbacks.map(key => vi.spyOn(implementation, key));
|
||||
try {
|
||||
expect(await validateChatCatalogConsistency([descriptor])).toEqual({ descriptorCount: 1, fixtureCallbacksInvoked: 0 });
|
||||
expect(descriptor.oracleVersion).toBe("paperclip.chat-catalog.work-timeline.v8");
|
||||
expect(descriptor.oracleVersion).toBe("paperclip.chat-catalog.work-timeline.v9");
|
||||
expect(descriptor.sourceFiles).toEqual(implementation.sourceFiles);
|
||||
expect(descriptor).toMatchObject({ expectedRunCount: 3, minimumRunCount: 3, attemptTimeoutMs: 720000 });
|
||||
for (const spy of spies) expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
@@ -56,7 +56,7 @@ export function gradeWorkTimeline(value: unknown) {
|
||||
&& day >= monday.toISOString().slice(0, 10) && day <= today;
|
||||
});
|
||||
c.add("ordered-and-truthful-account", worker.id === first.agentId && repairWorker.id === second.agentId && dateClaimsInWindow && truthfulTimelineAccount({ reply: o.reply,
|
||||
firstWorker: typeof worker.name === "string" ? worker.name : "", repairWorker: typeof repairWorker.name === "string" ? repairWorker.name : "",
|
||||
taskIdentifier: typeof t.identifier === "string" ? t.identifier : undefined, firstWorker: typeof worker.name === "string" ? worker.name : "", repairWorker: typeof repairWorker.name === "string" ? repairWorker.name : "",
|
||||
personName: typeof o.state.personName === "string" ? o.state.personName : undefined }), "The reply attributes and orders the actual command failure/pause, saved reassignment to the different repair worker and successful repair, keeps explicit ISO dates within the current-week observation window, and does not invent a failed native run.");
|
||||
c.add("inspectable-major-milestones", hasLink(o, "issues", t) && hasLink(o, "issues", prep), "Both actual launch work items are linked for inspection.");
|
||||
const hidden = entity(o.state, "hiddenTask");
|
||||
@@ -92,7 +92,7 @@ function timeline(): ChatCatalogCase {
|
||||
const state = { worker, repairWorker, project: p, task: second.t, prepTask, hiddenTask, firstRun, secondRun, blockedTask: first.t, blockedEventId: blockedEvent.id, handoffEventId: handoff.id, doneEventId: done.id, timezone, today, zoneLabel, personId: session.user.id, personName: session.user.name };
|
||||
await ctx.input.evidence("chat-catalog-real-launch-timeline.json", { ...state, verification: verifier.receipts, events }); return state;
|
||||
}, gradeWorkTimeline);
|
||||
return { ...base, oracleVersion: "paperclip.chat-catalog.work-timeline.v8", authentication: "human-session", expectedRunCount: 3, minimumRunCount: 3, attemptTimeoutMs: 720000,
|
||||
return { ...base, oracleVersion: "paperclip.chat-catalog.work-timeline.v9", authentication: "human-session", expectedRunCount: 3, minimumRunCount: 3, attemptTimeoutMs: 720000,
|
||||
sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-work-timeline.ts", "chat-catalog-domains/internal-timeline-narrative.ts", "chat-catalog-domains/internal-timeline-verifier.ts", "chat-catalog-domains/internal-privacy.ts", "chat-catalog-domains/identity-auth.ts", "chat-catalog-domains/internal-routine-audit.ts", "../e2e/fetch-allowed-port.ts", "../../server/src/services/native-runtime/status-decision-committer.ts", "../../server/src/services/work-timeline.ts"],
|
||||
async observe(ctx, state): Promise<Timeline> { return { ...await observeInternal(ctx, state), verification: structuredClone(verifier?.receipts ?? []), timelineEvents: await ctx.input.api.get(`/api/issues/${entity(state, "task").id}/activity`), hiddenStatus: await agentStatus(ctx, ctx.input.fixtures.agent.id, `/api/issues/${entity(state, "hiddenTask").id}`) }; },
|
||||
async verifyResult(ctx, state, value) { await base.verifyResult(ctx, state, value); const source = ctx.input.page.url(); try { await ctx.input.page.goto(new URL(`/${ctx.input.fixtures.company.issuePrefix}/issues/${entity(state, "prepTask").identifier}`, ctx.input.api.baseURL).href); await expect(ctx.input.page.getByRole("heading", { name: entity(state, "prepTask").title!, exact: true })).toBeVisible(); } finally { await ctx.input.page.goto(source); await expect(ctx.input.page.getByTestId("task-chat-composer-input")).toBeVisible(); } },
|
||||
|
||||
Reference in new issue
Block a user