fix(activity): guard timeline inference with scoped ownership history

Use one ownership projection for window-independent history presence and recorded events. Preserve ordered narrative facts across punctuation.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-03 09:17:45 -06:00
1 parent 80318e4567
commit 8b8f676e83
4 files changed
+217 -55

No files matched your search

@@ -1,8 +1,8 @@
import { randomUUID } from "node:crypto";
import express from "express";
import { eq } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";
import request from "supertest";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
activityLog,
agentWakeupRequests,
@@ -136,7 +136,7 @@ describeEmbeddedPostgres("work timeline aggregation", () => {
expect(result.from.toISOString()).toBe("2026-02-12T00:00:00.000Z");
});
it("aggregates runs, human events, approvals, and delegation edges", async () => {
it("aggregates runs, human events, approvals, and recorded assignments", async () => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const parentIssueId = randomUUID();
const childIssueId = randomUUID();
@@ -256,15 +256,33 @@ describeEmbeddedPostgres("work timeline aggregation", () => {
"created",
"commented",
"approved",
"delegated",
"assigned",
]));
expect(result.edges).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "delegation", issueId: childIssueId }),
expect.objectContaining({ kind: "assignment", issueId: childIssueId }),
]));
});
it("does not retain an inferred delegation when assignment history was recorded", async () => {
const { companyId, agentAId, agentBId } = await seedBase();
const parentId = randomUUID(), childId = randomUUID();
await db.insert(issues).values([
{ id: parentId, companyId, title: "Parent", status: "done", assigneeAgentId: agentAId,
createdAt: new Date("2026-03-01T10:00:00Z"), updatedAt: new Date("2026-03-05T10:00:00Z") },
{ id: childId, companyId, parentId, title: "Child", status: "done", assigneeAgentId: agentBId,
createdAt: new Date("2026-03-03T10:00:00Z"), updatedAt: new Date("2026-03-05T10:00:00Z") },
]);
await db.insert(activityLog).values({ companyId, actorType: "agent", actorId: agentAId,
action: "issue.assigned", entityType: "issue", entityId: childId,
details: { assigneeAgentId: agentBId }, createdAt: new Date("2026-03-05T12:00:00Z") });
const response = await request(createApp()).get(`/api/companies/${companyId}/timeline`)
.query({ from: "2026-03-05T00:00:00Z", to: "2026-03-06T00:00:00Z" });
expect(response.status).toBe(200);
expect(response.body.events).toContainEqual(expect.objectContaining({ issueId: childId, kind: "assigned" }));
expect(response.body.events.filter((event: { kind: string }) => event.kind === "delegated")).toEqual([]);
expect(response.body.edges.filter((edge: { kind: string }) => edge.kind === "delegation")).toEqual([]);
});
it("preserves delegation edges when parent and child cross a page boundary", async () => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const parentIssueId = randomUUID();
@@ -513,6 +531,113 @@ describeEmbeddedPostgres("work timeline aggregation", () => {
expect(maxActiveChecks).toBeLessThanOrEqual(16);
});
it.each(["before", "after"] as const)("does not invent creation ownership from a reassignment %s the query window", async when => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const issueId = randomUUID();
await db.insert(issues).values({ id: issueId, companyId, title: "Historical launch owner",
status: "done", priority: "medium", assigneeAgentId: agentBId, createdByUserId: userId,
createdAt: new Date("2026-03-01T10:00:00Z"), updatedAt: new Date("2026-03-05T10:00:00Z") });
await db.insert(activityLog).values({ companyId, actorType: "user", actorId: userId,
entityType: "issue", entityId: issueId, action: "issue.updated",
createdAt: new Date(when === "before" ? "2026-03-02T10:00:00Z" : "2026-03-06T10:00:00Z"),
details: { assigneeAgentId: agentBId, changes: { assigneeAgentId: { from: agentAId, to: agentBId } } } });
const response = await request(createApp()).get(`/api/companies/${companyId}/timeline`)
.query({ issueId, from: "2026-03-05T00:00:00Z", to: "2026-03-06T00:00:00Z" });
expect(response.status, JSON.stringify(response.body)).toBe(200);
expect(response.body.edges.filter((edge: { kind: string }) => edge.kind === "assignment")).toEqual([]);
expect(response.body.events.filter((event: { kind: string }) => event.kind === "assigned")).toEqual([]);
expect(response.body.events).toContainEqual(expect.objectContaining({ issueId, kind: "created" }));
});
it.each([
["child", "2026-03-02T10:00:00Z"], ["child", "2026-03-05T12:00:00Z"],
["parent", "2026-03-02T10:00:00Z"], ["parent", "2026-03-06T10:00:00Z"],
] as const)("does not infer a creation delegation after %s ownership changed at %s", async (changedIssue, at) => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const parentId = randomUUID(), childId = randomUUID();
await db.insert(issues).values([
{ id: parentId, companyId, title: "Launch parent", status: "done", priority: "medium",
createdByUserId: userId, assigneeAgentId: agentAId,
createdAt: new Date("2026-03-01T10:00:00Z"), updatedAt: new Date("2026-03-05T10:00:00Z") },
{ id: childId, companyId, parentId, title: "Launch child", status: "done", priority: "medium",
createdByAgentId: agentAId, assigneeAgentId: agentBId,
createdAt: new Date("2026-03-03T10:00:00Z"), updatedAt: new Date("2026-03-05T10:00:00Z") },
]);
const target = changedIssue === "parent" ? agentAId : agentBId;
const previous = changedIssue === "parent" ? agentBId : agentAId;
await db.insert(activityLog).values({ companyId, actorType: "user", actorId: userId,
entityType: "issue", entityId: changedIssue === "parent" ? parentId : childId,
action: "issue.updated", createdAt: new Date(at),
details: { assigneeAgentId: target, changes: { assigneeAgentId: { from: previous, to: target } } } });
const response = await request(createApp()).get(`/api/companies/${companyId}/timeline`)
.query({ limit: 1, from: "2026-03-05T00:00:00Z", to: "2026-03-06T00:00:00Z" });
expect(response.status, JSON.stringify(response.body)).toBe(200);
expect(response.body.pagination).toMatchObject({ totalIssues: 2, hasMore: true });
expect(response.body.edges.filter((edge: { kind: string }) => edge.kind === "delegation")).toEqual([]);
expect(response.body.events.filter((event: { kind: string }) => event.kind === "delegated")).toEqual([]);
if (changedIssue === "child" && at.startsWith("2026-03-05")) {
expect(response.body.events).toContainEqual({ actorId: `user:${userId}`, issueId: childId, kind: "assigned", at: new Date(at).toISOString() });
expect(response.body.edges).toContainEqual({ fromActorId: `user:${userId}`, toActorId: `agent:${agentBId}`, issueId: childId, kind: "assignment", at: new Date(at).toISOString() });
}
});
it("checks ownership history once for the readable page and its readable parents only", async () => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const parentId = randomUUID(), childId = randomUUID(), hiddenId = randomUUID(), unpagedId = randomUUID();
const foreignCompanyId = randomUUID();
await db.insert(companies).values({ id: foreignCompanyId, name: "Foreign", issuePrefix: "TF" });
await db.insert(issues).values([parentId, unpagedId, childId, hiddenId].map((id, index) => ({
id, companyId, title: `Scoped ${index}`, status: "done", assigneeAgentId: id === parentId ? agentAId : agentBId,
parentId: id === childId ? parentId : null, createdByUserId: userId,
createdAt: new Date(Date.parse("2026-03-01T10:00:00Z") + index * 1000), updatedAt: new Date("2026-03-05T10:00:00Z"),
})));
const ownershipRow = (entityId: string, scopedCompanyId = companyId) => ({ companyId: scopedCompanyId,
actorType: "user", actorId: userId, entityType: "issue", entityId, action: "issue.updated",
createdAt: new Date("2026-03-02T10:00:00Z"), details: { changes: { assigneeAgentId: { from: agentBId, to: agentAId } } } });
await db.insert(activityLog).values([ownershipRow(parentId), ownershipRow(hiddenId), ownershipRow(childId, foreignCompanyId),
...Array.from({ length: 1000 }, () => ownershipRow(unpagedId))]);
const queries: Array<{ query: string; parameters: unknown[] }> = [];
const previousDebug = db.$client.options.debug;
let result;
db.$client.options.debug = (_connection, query, parameters) => queries.push({ query, parameters });
try {
result = await workTimelineService(db).getTimeline({ companyId, limit: 1,
from: new Date("2026-03-05T00:00:00Z"), to: new Date("2026-03-06T00:00:00Z"),
canReadIssue: async issue => issue.id !== hiddenId });
} finally { db.$client.options.debug = previousDebug; }
const history = queries.filter(row => /^select distinct /i.test(row.query) && row.query.includes('"activity_log"'));
expect(history).toHaveLength(1);
expect(history[0]!.query).not.toContain("created_at");
const scopedIds = history[0]!.parameters.filter(value => typeof value === "string" && /^[a-f0-9-]{36}$/.test(value));
expect(new Set(scopedIds)).toEqual(new Set([companyId, parentId, childId]));
const rows = await db.$client.unsafe(history[0]!.query, history[0]!.parameters as any[]);
expect(rows.map(row => row.entity_id)).toEqual([parentId]);
const explain = await db.$client.unsafe(`explain (analyze, format json) ${history[0]!.query}`, history[0]!.parameters as any[]);
const plan = explain[0]!["QUERY PLAN"][0].Plan;
expect(plan["Actual Rows"]).toBe(1);
expect(result.edges).toContainEqual(expect.objectContaining({ issueId: childId, kind: "assignment", toActorId: `agent:${agentBId}` }));
expect(result.edges.some(edge => edge.kind === "delegation")).toBe(false);
expect(result.events.every(event => event.issueId === childId)).toBe(true);
console.info("Timeline ownership lookup", JSON.stringify({ queryCount: history.length, scopedIssueCount: 2,
returnedIssueCount: rows.length, actualPlanRows: plan["Actual Rows"], planNode: plan["Node Type"] }));
});
it("fails the route when ownership history cannot be read instead of inventing creation ownership", async () => {
const { companyId, agentAId } = await seedBase();
const issueId = randomUUID();
await db.insert(issues).values({ id: issueId, companyId, title: "History failure", status: "done", assigneeAgentId: agentAId,
createdAt: new Date("2026-03-05T10:00:00Z"), updatedAt: new Date("2026-03-05T10:00:00Z") });
const selectDistinct = db.selectDistinct.bind(db);
const fault = vi.spyOn(db, "selectDistinct").mockImplementationOnce(() => selectDistinct({ issueId: sql<string>`1 / 0` }) as any);
try {
const response = await request(createApp()).get(`/api/companies/${companyId}/timeline`)
.query({ issueId, from: "2026-03-05T00:00:00Z", to: "2026-03-06T00:00:00Z" });
expect(fault).toHaveBeenCalledOnce();
expect(response.status).toBe(500);
expect(response.body).not.toHaveProperty("edges");
} finally { fault.mockRestore(); }
});
it("serves the recorded human reassignment instead of inferring the current owner at creation", async () => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const issueId = randomUUID();
@@ -550,12 +675,14 @@ describeEmbeddedPostgres("work timeline aggregation", () => {
const { companyId, userId, agentAId, agentBId } = await seedBase();
const createdAt = new Date("2026-03-05T10:00:00Z");
const changedAt = new Date("2026-03-05T11:00:00Z");
for (const kind of ["absent", "null", "unchanged", "self-assignment"] as const) {
for (const kind of ["absent", "null", "unchanged", "canonical-empty", "canonical-unchanged", "self-assignment"] as const) {
const issueId = randomUUID();
const assigneeAgentId = kind === "null" ? null : kind === "self-assignment" ? agentBId : agentAId;
await db.insert(issues).values({ id: issueId, companyId, title: kind, status: "done",
priority: "medium", assigneeAgentId, createdByUserId: userId, createdAt, updatedAt: changedAt });
const details = kind === "absent" ? { status: "done" }
: kind === "canonical-empty" ? { assigneeAgentId, changes: {} }
: kind === "canonical-unchanged" ? { assigneeAgentId, changes: { assigneeAgentId: { from: agentAId, to: agentAId } } }
: { assigneeAgentId, _previous: { assigneeAgentId: agentAId } };
await db.insert(activityLog).values({ companyId, actorType: "agent", actorId: agentAId,
action: "issue.updated", entityType: "issue", entityId: issueId, details, createdAt: changedAt });
+48 -42
View File
@@ -1,5 +1,5 @@
import { withAgentAppearance } from "@paperclipai/shared";
import { and, asc, desc, eq, gte, inArray, isNull, lte, or, sql } from "drizzle-orm";
import { and, asc, desc, eq, gte, inArray, isNull, lte, or, sql, type SQL } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
activityLog,
@@ -119,8 +119,30 @@ function dateIso(value: Date | null | undefined) {
return value ? value.toISOString() : null;
}
function readString(value: unknown) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
/** One ownership contract for history detection and recorded event projection. */
function timelineOwnershipProjection() {
const record = (value: SQL) => sql`case when jsonb_typeof(${value}) = 'object' then ${value} else '{}'::jsonb end`;
const details = record(sql`${activityLog.details}`);
const previous = record(sql`${details} -> '_previous'`);
const changes = record(sql`${details} -> 'changes'`);
const canonical = sql`${activityLog.action} = 'issue.updated' and jsonb_typeof(${details} -> 'changes') = 'object'`;
const changed = ["assigneeAgentId", "assigneeUserId"].map(key => {
const change = record(sql`${changes} -> ${key}`);
return sql`case when ${canonical} then
(${change} ? 'from' and ${change} ? 'to' and (${change} -> 'from') is distinct from (${change} -> 'to'))
else (${details} ? ${key} and (not (${previous} ? ${key}) or (${details} -> ${key}) is distinct from (${previous} -> ${key}))) end`;
});
const stringValue = (value: SQL) => sql<string | null>`case when jsonb_typeof(${value}) = 'string'
then nullif(btrim(${value} #>> '{}'), '') else null end`;
const target = (key: string, legacyKey: string) => sql<string | null>`case when ${canonical}
then ${stringValue(sql`${changes} -> ${key} -> 'to'`)}
else coalesce(${stringValue(sql`${details} -> ${key}`)}, ${stringValue(sql`${details} -> ${legacyKey}`)}) end`;
return {
recorded: sql<boolean>`coalesce(position('assign' in ${activityLog.action}) > 0 or
(${activityLog.action} = 'issue.updated' and (${sql.join(changed, sql` or `)})), false)`,
agentId: target("assigneeAgentId", "toAgentId"),
userId: target("assigneeUserId", "toUserId"),
};
}
function readNumber(value: unknown) {
@@ -486,6 +508,19 @@ export function workTimelineService(db: Db) {
};
}
const ownership = timelineOwnershipProjection();
// Read only presence, for the page and its already-readable parents. This
// returns at most 2 * MAX_LIMIT IDs, regardless of history length or window.
const ownershipIssueIds = maybeUuidList(pagedIssues.flatMap(issue => [
issue.id, ...(issue.parentId && issueById.has(issue.parentId) ? [issue.parentId] : []),
]));
const ownershipRows = await db.selectDistinct({ issueId: activityLog.entityId })
.from(activityLog)
.where(and(eq(activityLog.companyId, input.companyId), eq(activityLog.entityType, "issue"),
inArray(activityLog.entityId, ownershipIssueIds), ownership.recorded))
.limit(ownershipIssueIds.length);
const ownershipHistory = new Set(ownershipRows.map(row => row.issueId));
const actorIds = new Set<string>();
const events: WorkTimelineEvent[] = [];
const edges: WorkTimelineEdge[] = [];
@@ -501,7 +536,7 @@ export function workTimelineService(db: Db) {
});
const assigneeActorId = actorForIssueAssignee(issue);
if (assigneeActorId) {
if (assigneeActorId && !ownershipHistory.has(issue.id)) {
actorIds.add(assigneeActorId);
edges.push({
fromActorId: creatorActorId,
@@ -514,7 +549,8 @@ export function workTimelineService(db: Db) {
const parent = issue.parentId ? issueById.get(issue.parentId) : null;
const parentActorId = parent ? actorForIssueAssignee(parent) ?? actorForIssueCreator(parent) : null;
if (parentActorId && assigneeActorId && parentActorId !== assigneeActorId) {
if (parentActorId && assigneeActorId && parentActorId !== assigneeActorId
&& !ownershipHistory.has(issue.id) && parent && !ownershipHistory.has(parent.id)) {
actorIds.add(parentActorId);
edges.push({
fromActorId: parentActorId,
@@ -645,7 +681,9 @@ export function workTimelineService(db: Db) {
actorType: activityLog.actorType,
actorId: activityLog.actorId,
action: activityLog.action,
details: activityLog.details,
assignmentRecorded: ownership.recorded,
assignmentAgentId: ownership.agentId,
assignmentUserId: ownership.userId,
createdAt: activityLog.createdAt,
})
.from(activityLog)
@@ -728,42 +766,10 @@ export function workTimelineService(db: Db) {
});
}
// Canonical task edits use issue.updated, not an assignment-specific action.
// The presence of a field is distinct from a null (unassignment) value.
const assignments = logRows.flatMap((row) => {
const details = row.details && typeof row.details === "object" && !Array.isArray(row.details)
? row.details as Record<string, unknown> : {};
const previous = details._previous && typeof details._previous === "object" && !Array.isArray(details._previous)
? details._previous as Record<string, unknown> : {};
const changes = row.action === "issue.updated" && details.changes && typeof details.changes === "object" && !Array.isArray(details.changes)
? details.changes as Record<string, unknown> : null;
const changeFor = (key: string) => {
const value = changes?.[key];
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
};
// PATCH payload fields include repeated values; its saved change receipt
// alone identifies actual changes. Older records retain their prior shape.
const changed = ["assigneeAgentId", "assigneeUserId"].some((key) => {
if (changes) {
const change = changeFor(key);
return Object.hasOwn(change, "from") && Object.hasOwn(change, "to") && change.from !== change.to;
}
return Object.hasOwn(details, key) && (!Object.hasOwn(previous, key) || details[key] !== previous[key]);
});
if (!row.action.includes("assign") && !(row.action === "issue.updated" && changed)) return [];
const targetAgentId = changes ? readString(changeFor("assigneeAgentId").to)
: readString(details.assigneeAgentId) ?? readString(details.toAgentId);
const targetUserId = changes ? readString(changeFor("assigneeUserId").to)
: readString(details.assigneeUserId) ?? readString(details.toUserId);
return [{ row, toActorId: targetAgentId ? actorId("agent", targetAgentId)
: targetUserId ? actorId("user", targetUserId) : null }];
});
const recordedAssignmentIssues = new Set(assignments.map(({ row }) => row.issueId));
// A current owner does not prove that they owned the task when it was created.
for (let index = edges.length - 1; index >= 0; index--) {
if (edges[index]!.kind === "assignment" && recordedAssignmentIssues.has(edges[index]!.issueId)) edges.splice(index, 1);
}
for (const { row, toActorId } of assignments) {
for (const row of logRows) {
if (!row.assignmentRecorded) continue;
const toActorId = row.assignmentAgentId ? actorId("agent", row.assignmentAgentId)
: row.assignmentUserId ? actorId("user", row.assignmentUserId) : null;
if (!toActorId) continue;
const logActorType = row.actorType === "agent" || row.actorType === "user" || row.actorType === "plugin"
? row.actorType : "system";
@@ -10,9 +10,10 @@ export function truthfulTimelineAccount(input: {
// Search milestone clauses, not the first occurrence of a worker or outcome.
// A summary or heading may mention the repair before the ordered account.
const clauses = [...body.matchAll(/[^.!?\n]+(?:[.!?]|$)/g)].map(match => ({ text: match[0], at: match.index }));
const failures = clauses.filter(({ text }) => mentions(text, firstWorker)
const failureOutcome = /\b(?:fail(?:ed|ure|ing)?|exit(?:ed)?(?:\s+with)?(?:\s+(?:code|status))?\s*[:=]?\s*1)\b/gi;
const failures = clauses.flatMap(({ text, at }) => mentions(text, firstWorker)
&& /\b(?:verification|verify_launch_html|check|test|command)\b/i.test(text)
&& /\b(?:fail(?:ed|ure|ing)?|exit(?:ed)?(?:\s+with)?(?:\s+(?:code|status))?\s*[:=]?\s*1)\b/i.test(text));
? [...text.matchAll(failureOutcome)].map(match => ({ at: at + match.index })) : []);
const humanActors = ["you", "human operator", "board member", "human", "person", "user", "operator", "board", input.personName].filter(Boolean).map(name => escape(name!)).join("|");
const humanActor = `\\b(?:${humanActors})\\b`;
const assignment = "\\b(?:reassign(?:ed|ment)?|assign(?:ed|ment)?|hand(?:ed)?(?:\\s+\\w+){0,4}\\s+(?:off|over)|handed|handoff)\\b";
@@ -25,11 +26,21 @@ export function truthfulTimelineAccount(input: {
return !!namedRole && !["The", "A", "An", input.personName].some(name => name?.toLowerCase() === namedRole[1]!.toLowerCase())
|| /\b(?:another|different|other)\s+(?:human|person|user|operator)\b/i.test(text);
};
const handoffs = clauses.filter(({ text }) => handoffToRepairWorker.test(text) && !differentNamedPerson(text)
&& !/\b(?:not|never|no)\b[^.!?]{0,50}\b(?:reassign|assign|hand|handoff)/i.test(text));
const repairs = clauses.filter(({ text }) => mentions(text, repairWorker)
&& /\b(?:repair(?:ed)?|fix(?:ed)?|label)\b/i.test(text)
&& /\b(?:pass(?:ed)?|succeed(?:ed)?|exit(?:ed)?(?:\s+with)?(?:\s+(?:code|status))?\s*[:=]?\s*0)\b/i.test(text));
const handoffs = clauses.flatMap(({ text, at }) => {
if (differentNamedPerson(text) || /\b(?:not|never|no)\b[^.!?]{0,50}\b(?:reassign|assign|hand|handoff)/i.test(text)) return [];
return [...text.matchAll(new RegExp(handoffToRepairWorker.source, "gi"))].filter(match => {
// An explicit other person's role must not become the current human just
// because earlier milestones share this sentence.
if (!/^(?:human|person|user|operator|board)\b/i.test(match[0])) return true;
const prefix = /([A-Z][\p{L}'-]+(?:\s+[A-Z][\p{L}'-]+)*)\s*,?\s*(?:the\s+)?$/u.exec(text.slice(0, match.index));
return !prefix || ["The", "A", "An", "Then", input.personName].some(name => name?.toLowerCase() === prefix[1]!.toLowerCase());
}).map(match => ({ at: at + match.index }));
});
const repairAction = new RegExp(`\\b${escape(repairWorker)}\\b(?:\\s+(?:then|later|now|personally|explicitly|actually|had|has|have|also|who)){0,3}\\s+\\b(?:repair(?:ed)?|fix(?:ed)?)\\b|\\b(?:was|got|had been|has been)\\s+(?:repaired|fixed)\\b[^.!?\\n]{0,40}\\bby\\s+${escape(repairWorker)}\\b`, "gi");
const repairSuccess = /\b(?:pass(?:ed)?|succeed(?:ed)?|exit(?:ed)?(?:\s+with)?(?:\s+(?:code|status))?\s*[:=]?\s*0)\b/i;
const repairs = clauses.flatMap(({ text, at }) => [...text.matchAll(repairAction)]
.filter(match => repairSuccess.test(text.slice(match.index + match[0].length)))
.map(match => ({ at: at + match.index })));
const ordered = failures.some(failure => handoffs.some(handoff => handoff.at > failure.at
&& /\b(?:paused|blocked|stopped)\b/i.test(body.slice(failure.at, handoff.at))
&& repairs.some(repair => repair.at > handoff.at)));
@@ -113,6 +113,24 @@ it.each(["missing link", "hidden work", "source mutation", "wrong command", "wro
expect(gradeWorkTimeline(o).passed).toBe(false);
});
it.each([
"Today UTC: Morgan ran verification with exit code 1 and paused, you reassigned the task to Riley, and Riley fixed the label and the check passed with exit code 0.",
"Today UTC: Morgan ran verification with exit code 1 and paused; you reassigned the task to Riley; Riley fixed the label and the check passed with exit code 0.",
"Today UTC: Morgan ran verification with exit code 1 and paused then you reassigned the task to Riley and Riley fixed the label and the check passed with exit code 0.",
])("orders matched facts independently of milestone punctuation: %s", reply => {
const o = fixture(); o.reply = reply; expect(gradeWorkTimeline(o).passed).toBe(true);
});
it.each([
"Today UTC: you reassigned the task to Riley, Morgan ran verification with exit code 1 and paused, and Riley fixed the label and the check passed with exit code 0.",
"Today UTC: Morgan ran verification with exit code 1 and paused, Morgan reassigned the task to Riley while you watched, and Riley fixed the label and the check passed with exit code 0.",
"Today UTC: Morgan ran verification with exit code 1 and paused, you reassigned the task to Riley, and Morgan fixed the label and the check passed with exit code 0.",
"Today UTC: Morgan ran verification with exit code 1 and paused, Riley fixed the label and the check passed with exit code 0, then you reassigned the task to Riley.",
"Today UTC: Morgan ran verification with exit code 1 and paused, Jordan the human reassigned the task to Riley, and Riley fixed the label and the check passed with exit code 0.",
"Today UTC: Morgan ran verification with exit code 1 and paused, and Riley fixed the label and the check passed with exit code 0.",
])("retains ordering and actor requirements inside one sentence: %s", reply => {
const o = fixture(); o.reply = reply; expect(gradeWorkTimeline(o).passed).toBe(false);
});
it("admits the selected live timeline metadata without invoking fixture callbacks", async () => {
const descriptor = findChatCatalogCase("activity.work-timeline")!;
const implementation = workTimelineCases[0]!;