Merge master into qualified test lifecycle fixes

Preserve master retention I/O settlement and held-I/O coverage alongside the controlled fairness clock and exact run completion barriers. Retain the delegated-completion fixture without changing production code.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-31 07:57:47 -05:00
commit ab0b15d234
465 files changed
+33126 -1543

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import { expect, test } from "@playwright/test";
import { idle, json, send, setup } from "./agent-chat.shared";
test.use({ trace: "retain-on-failure" });
test.setTimeout(120_000);
test("a routine delivers a first digest, unread state, source link and replyable history", async ({ page, request }) => {
const f = await setup(request);
try {
await page.goto(`/${f.company.issuePrefix}/chats/${f.agents[1].id}`);
await expect(page.getByTestId("task-chat-composer-input")).toBeVisible();
const routine = await json(await request.post(`/api/companies/${f.company.id}/routines`, { data: {
title: "Morning PR review", description: "proactive-digest-fixture", assigneeAgentId: f.agent.id,
} }));
await json(await request.post(`/api/routines/${routine.id}/run`, { data: { idempotencyKey: "browser-morning" } }));
await expect(page.getByRole("img", { name: "Unread messages from Alpha", exact: true })).toBeVisible({ timeout: 45_000 });
await expect(page.getByLabel(/unread chats/)).toBeVisible();
let chat: any;
await expect.poll(async () => {
chat = await json(await request.get(f.chatPath));
return chat?.isUnreadForMe;
}).toBe(true);
const comments = await json(await request.get(`/api/issues/${chat.id}/comments`));
const digest = comments.find((c: any) => c.metadata?.chatMessage);
expect(digest.authorAgentId).toBe(f.agent.id);
const sourceId = digest.metadata.chatMessage.sourceTaskId;
const source = await json(await request.get(`/api/issues/${sourceId}`));
expect(source.originKind).toBe("routine_execution");
expect(await json(await request.get(`/api/issues/${chat.id}/runs`))).toEqual([]);
await page.reload();
await expect(page.getByRole("img", { name: "Unread messages from Alpha", exact: true })).toBeVisible();
await page.goto(f.route);
await expect(page.getByText("Morning PR digest", { exact: true })).toBeVisible();
const sourceLink = page.getByRole("link", { name: `From ${source.identifier}`, exact: true });
await expect(sourceLink).toBeVisible();
await expect.poll(async () => (await json(await request.get(f.chatPath))).isUnreadForMe).toBe(false);
await sourceLink.click();
await expect(page).toHaveURL(new RegExp(`/issues/${source.identifier}`));
await page.goto(f.route);
await send(page, { action: "digest-followup", text: "Which was the first PR?" });
await idle(request, f.chatPath, 2);
await expect(page.getByText("The first PR in your morning digest is PR 123.", { exact: true })).toBeVisible({ timeout: 45_000 });
} finally { await f.restore(); }
});
+3 -3
View File
@@ -264,7 +264,7 @@ test("secondary chat navigation preserves layout, unique conversations, history,
data: { enableStreamlinedUi: true },
}));
await page.goto(`/${f.company.issuePrefix}/dashboard`);
await page.getByRole("link", { name: "Chat", exact: true }).click();
await page.getByRole("link", { name: /^Chat(?:\s|$)/ }).click();
// With no recent conversation, Chat opens the first human-created primary.
// Navigation alone must not create a conversation or start execution.
expect(await json(await request.get(`/api/companies/${f.company.id}/primary-agent/me`))).toMatchObject({
@@ -333,7 +333,7 @@ test("secondary chat navigation preserves layout, unique conversations, history,
await expect(editor).toContainText("Unsent draft for Alpha");
await expect(nav.getByRole("link")).toHaveCount(6);
await page.getByRole("link", { name: "Chat", exact: true }).click();
await page.getByRole("link", { name: /^Chat(?:\s|$)/ }).click();
await expect(page.getByRole("link", { name: "Configure Alpha", exact: true })).toBeVisible();
await expect(editor).toContainText("Unsent draft for Alpha");
expect(await sidebar.boundingBox()).toEqual(landingBounds);
@@ -346,7 +346,7 @@ test("secondary chat navigation preserves layout, unique conversations, history,
},
}));
await idle(request, betaPath);
await page.getByRole("link", { name: "Chat", exact: true }).click();
await page.getByRole("link", { name: /^Chat(?:\s|$)/ }).click();
await expect(page.getByRole("link", { name: "Configure Alpha", exact: true })).toBeVisible();
await compose.click();
await picker.getByRole("combobox").fill("Beta");
+3 -1
View File
@@ -56,7 +56,9 @@ for (const mobile of [false, true]) {
else await expect(artifacts).toHaveAttribute("aria-selected", "false");
await artifacts.click();
await expectTaskPanelTab(panel, mobile, "Artifacts");
await expect(panel.getByRole("heading", { name: "Arriving report", level: 2, exact: true })).toBeVisible();
const document = panel.locator("#document-report");
await expect(document.getByRole("heading", { name: "Arriving report", level: 1, exact: true })).toBeVisible();
await expect(document.locator("p").filter({ hasText: /^Durable output\.$/ })).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("artifact-opened-by-user.png"), fullPage: true });
});
}
+41 -12
View File
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { expect, test, type APIRequestContext, type APIResponse, type Locator, type Page } from "@playwright/test";
import { expectTaskPanelTab } from "./helpers/task-panel-tabs";
@@ -52,18 +53,26 @@ async function assertStillPaused(request: APIRequestContext, issueId: string) {
for (const mobile of [false, true]) {
test(`upload documents and files while a task remains paused (mobile=${mobile})`, async ({ page, request }, testInfo) => {
const { issue, panel } = await setup(page, request, mobile);
const markdown = "# Engineering handoff\n\nStart with the loop inventory and preserve current task ownership.";
const csv = "path,pattern\ndelivery,event-driven\nroutines,clock-driven";
await chooseFiles(page, panel, [
{ name: "core-handoff.md", mimeType: "text/markdown", buffer: Buffer.from("# Engineering handoff\n\nStart with the loop inventory and preserve current task ownership.") },
{ name: "loop-inventory.csv", mimeType: "text/csv", buffer: Buffer.from("path,pattern\ndelivery,event-driven\nroutines,clock-driven") },
{ name: "core-handoff.md", mimeType: "text/markdown", buffer: Buffer.from(markdown) },
{ name: "loop-inventory.csv", mimeType: "text/csv", buffer: Buffer.from(csv) },
]);
await expect(panel.getByRole("button", { name: "Read document", exact: true })).toBeVisible();
const document = panel.locator("#document-core-handoff");
await expect(document.getByRole("heading", { name: "Engineering handoff", exact: true })).toBeVisible();
await expect(document.getByText("Start with the loop inventory and preserve current task ownership.", { exact: true })).toBeVisible();
await expect(panel.getByRole("link", { name: "Download file", exact: true })).toBeVisible();
await expectTaskPanelTab(panel, mobile, "Artifacts");
const download = page.waitForEvent("download");
await panel.getByRole("link", { name: "Download file", exact: true }).click();
expect((await download).suggestedFilename()).toBe("loop-inventory.csv");
await panel.getByRole("button", { name: "Read document", exact: true }).click();
await expect(panel.getByText("Start with the loop inventory and preserve current task ownership.", { exact: true })).toBeVisible();
const downloaded = await download;
expect(downloaded.suggestedFilename()).toBe("loop-inventory.csv");
expect(await readFile((await downloaded.path())!, "utf8")).toBe(csv);
await document.getByRole("button", { name: "Collapse core-handoff document", exact: true }).click();
await expect(document.getByText("Start with the loop inventory and preserve current task ownership.", { exact: true })).toBeHidden();
await document.getByRole("button", { name: "Expand core-handoff document", exact: true }).click();
await expect(document.getByText("Start with the loop inventory and preserve current task ownership.", { exact: true })).toBeVisible();
await assertStillPaused(request, issue.id);
await page.reload();
await expect(page.getByRole("heading", { name: issue.title, exact: true })).toBeVisible();
@@ -71,8 +80,15 @@ for (const mobile of [false, true]) {
const attachments = await json(await request.get(`/api/issues/${issue.id}/attachments`));
expect(documents).toHaveLength(1);
expect(documents[0].key).toBe("core-handoff");
expect(documents[0].body).toBe(markdown);
expect(attachments).toHaveLength(1);
expect(attachments[0].originalFilename).toBe("loop-inventory.csv");
if (mobile) {
await page.getByRole("button", { name: "More actions", exact: true }).click();
await page.getByRole("button", { name: "Properties", exact: true }).click();
}
await expect(document.getByRole("heading", { name: "Engineering handoff", exact: true })).toBeVisible();
await expect(document.getByText("Start with the loop inventory and preserve current task ownership.", { exact: true })).toBeVisible();
await assertStillPaused(request, issue.id);
await page.screenshot({ path: testInfo.outputPath("uploaded-handoff-persisted.png"), fullPage: true });
});
@@ -80,8 +96,11 @@ for (const mobile of [false, true]) {
test("the entire populated panel accepts a drop and preserves an existing document", async ({ page, request }, testInfo) => {
const { issue, panel } = await setup(page, request);
await chooseFiles(page, panel, [{ name: "handoff.md", mimeType: "text/markdown", buffer: Buffer.from("# Original handoff\n\nPreserve this version.") }]);
await expect(panel.getByRole("button", { name: "Read document", exact: true })).toBeVisible();
const original = "# Original handoff\n\nPreserve this version.";
await chooseFiles(page, panel, [{ name: "handoff.md", mimeType: "text/markdown", buffer: Buffer.from(original) }]);
const document = panel.locator("#document-handoff");
await expect(document.getByRole("heading", { name: "Original handoff", exact: true })).toBeVisible();
const originalDocument = await json(await request.get(`/api/issues/${issue.id}/documents/handoff`));
const transfer = await page.evaluateHandle(() => {
const data = new DataTransfer();
data.items.add(new File(["# Additional handoff\n\nNew reference, separate document."], "handoff.md", { type: "text/markdown" }));
@@ -99,12 +118,17 @@ test("the entire populated panel accepts a drop and preserves an existing docume
await header.dispatchEvent("dragleave", { dataTransfer: transfer });
await expect(overlay).toBeHidden();
await header.dispatchEvent("dragenter", { dataTransfer: transfer });
await panel.locator("article").first().dispatchEvent("drop", { dataTransfer: transfer });
await expect(panel.getByRole("button", { name: "Read document", exact: true })).toHaveCount(2);
await document.dispatchEvent("drop", { dataTransfer: transfer });
await expect(panel.locator("#document-handoff, #document-handoff-2")).toHaveCount(2);
await expect(document.getByText("Preserve this version.", { exact: true })).toBeVisible();
await expect(panel.locator("#document-handoff-2").getByText("New reference, separate document.", { exact: true })).toBeVisible();
await expect(panel.getByText("verification.txt", { exact: true }).first()).toBeVisible();
const documents = await json(await request.get(`/api/issues/${issue.id}/documents`));
expect(documents.map((row: { key: string }) => row.key).sort()).toEqual(["handoff", "handoff-2"]);
expect(documents.find((row: { key: string }) => row.key === "handoff").body).toContain("Preserve this version.");
const preserved = documents.find((row: { key: string }) => row.key === "handoff");
expect(preserved.body).toBe(original);
expect(preserved.latestRevisionId).toBe(originalDocument.latestRevisionId);
expect(documents.find((row: { key: string }) => row.key === "handoff-2").body).toBe("# Additional handoff\n\nNew reference, separate document.");
await assertStillPaused(request, issue.id);
});
@@ -138,7 +162,12 @@ test("reserved filenames create visible documents without replacing internal rev
originals.set(key, document.latestRevisionId);
}
await chooseFiles(page, panel, reserved.map(key => ({ name: `${key}.md`, mimeType: "text/markdown", buffer: Buffer.from(`# Uploaded ${key}\n\nThis reference must stay visible.`) })));
await expect(panel.getByRole("button", { name: "Read document", exact: true })).toHaveCount(3);
await expect(panel.locator('[id^="document-upload-"]')).toHaveCount(3);
for (const key of reserved) {
const document = panel.locator(`#document-upload-${key}`);
await expect(document.getByRole("heading", { name: `Uploaded ${key}`, exact: true })).toBeVisible();
await expect(document.getByText("This reference must stay visible.", { exact: true })).toBeVisible();
}
const documents = await json(await request.get(`/api/issues/${issue.id}/documents`));
expect(documents.filter((row: { key: string }) => row.key.startsWith("upload-")).map((row: { key: string }) => row.key).sort()).toEqual(reserved.map(key => `upload-${key}`).sort());
for (const key of reserved) {
+12
View File
@@ -24,6 +24,14 @@ const task = await api(`/issues/${ctx.issueId}`);
const comment = async (body) =>
api(`/issues/${task.id}/comments`, "POST", { body });
if (!task.conversationAgentId) {
if (task.description?.includes("proactive-digest-fixture")) {
await api("/agents/me/chat/messages", "POST", {
body: "## Morning PR digest\n\nPlease review [PR 123](https://github.com/paperclipai/paperclip/pull/123), [PR 456](https://github.com/paperclipai/paperclip/pull/456), and [PR 789](https://github.com/paperclipai/paperclip/pull/789).",
idempotencyKey: "morning-digest",
});
await api(`/issues/${task.id}`, "PATCH", { status: "done", comment: "Digest delivered." });
process.exit(0);
}
const plan = await api(`/issues/${task.id}/documents/plan`);
await api(`/issues/${task.id}/documents/output`, "PUT", {
title: "Output",
@@ -182,6 +190,10 @@ if (command.action === "hold") {
},
});
await comment("Please choose an audience.");
} else if (command.action === "digest-followup") {
const digest = comments.find(c => c.metadata?.chatMessage && c.body.includes("Morning PR digest"));
if (!digest) throw new Error("Proactive digest is missing from the next human turn's history");
await comment("The first PR in your morning digest is PR 123.");
} else if (command.action === "history") {
for (let index = 0; index < 65; index++)
await comment(`History message ${String(index).padStart(2, "0")}`);