feat: publish shared task and reply activities through MCP
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { activityProposals, expect, test } from "../activity-fixtures";
|
||||
import { ActivitiesPage } from "../models/ActivitiesPage";
|
||||
|
||||
test("shares published chores and reply activities with Home, Messages and MCP without losing user changes", async ({
|
||||
page,
|
||||
context,
|
||||
homePage,
|
||||
workspacePage,
|
||||
messagesPage,
|
||||
activitiesPage,
|
||||
activityMcp,
|
||||
}) => {
|
||||
await homePage.goTo();
|
||||
await expect(activitiesPage.card("buy-detergent")).toHaveCount(0);
|
||||
expect((await activityMcp.context()).activities).toEqual([]);
|
||||
await activityMcp.publish();
|
||||
await expect(activitiesPage.card("buy-detergent")).toBeVisible();
|
||||
await expect(activitiesPage.card("review-times")).toBeVisible();
|
||||
await expect(activitiesPage.card("send-documents")).toBeVisible();
|
||||
await activitiesPage.complete("Buy detergent").click();
|
||||
await expect(activitiesPage.card("buy-detergent")).toHaveAttribute("data-status", "done");
|
||||
await activitiesPage.openReply("review-times");
|
||||
await expect(page).toHaveURL(/\/support\?.*activity=review-times/);
|
||||
await expect(activitiesPage.details).toContainText(
|
||||
"Read the review materials and choose two times",
|
||||
);
|
||||
await expect(activitiesPage.sources).toContainText(
|
||||
"Please review the materials and suggest two times.",
|
||||
);
|
||||
await expect(activitiesPage.sources).toContainText("not the full conversation history");
|
||||
await expect(activitiesPage.action("Send the completed documents")).toBeVisible();
|
||||
const edited = "I checked the materials. Thursday at 14:00 works for me.";
|
||||
await activitiesPage.saveReply(edited);
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await activityMcp.context()).activities.find((item) => item.id === "review-times")
|
||||
?.userOverrides.draft,
|
||||
)
|
||||
.toBe(edited);
|
||||
await activitiesPage.later.click();
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await activityMcp.context()).activities.find((item) => item.id === "review-times")
|
||||
?.userState,
|
||||
)
|
||||
.toBe("deferred");
|
||||
const before = await activityMcp.context();
|
||||
expect(before.activities.find((item) => item.id === "send-documents")?.userState).toBe(
|
||||
"available",
|
||||
);
|
||||
const refreshed = activityProposals.map((proposal) =>
|
||||
proposal.kind === "reply"
|
||||
? { ...proposal, reply: { ...proposal.reply, draft: "A newer agent suggestion" } }
|
||||
: proposal,
|
||||
);
|
||||
await activityMcp.publish(refreshed);
|
||||
const after = await activityMcp.context();
|
||||
expect(after.activities).toHaveLength(3);
|
||||
expect(after.activities.find((item) => item.id === "review-times")).toMatchObject({
|
||||
userState: "deferred",
|
||||
userOverrides: { draft: edited },
|
||||
effective: { reply: { draft: edited } },
|
||||
proposal: { reply: { draft: "A newer agent suggestion" } },
|
||||
});
|
||||
expect(after.activities.find((item) => item.id === "buy-detergent")?.userState).toBe("completed");
|
||||
await page.reload();
|
||||
await expect(activitiesPage.draft).toHaveValue(edited);
|
||||
await expect(
|
||||
activitiesPage.details.getByRole("button", {
|
||||
name: /^(Send|Mark complete|Try sending)$/,
|
||||
exact: true,
|
||||
}),
|
||||
).toHaveCount(0);
|
||||
await activitiesPage.action("Send the completed documents").click();
|
||||
await expect(activitiesPage.draft).toHaveValue("A newer agent suggestion");
|
||||
await messagesPage.showConversationList();
|
||||
await workspacePage.navigateTo("Home");
|
||||
await expect(activitiesPage.card("review-times")).toContainText("Set aside");
|
||||
const device = await context.newPage();
|
||||
try {
|
||||
const other = new ActivitiesPage(device);
|
||||
await device.goto("/");
|
||||
await expect(other.card("buy-detergent")).toHaveAttribute("data-status", "done");
|
||||
await activityMcp.publish([
|
||||
{
|
||||
id: "later-chore",
|
||||
kind: "task",
|
||||
title: "Take the recycling out",
|
||||
nextAction: "Bring the paper bag downstairs",
|
||||
sources: [],
|
||||
},
|
||||
]);
|
||||
await expect(activitiesPage.card("later-chore")).toBeVisible();
|
||||
await expect(other.card("later-chore")).toBeVisible();
|
||||
await device.reload();
|
||||
await expect(other.card("later-chore")).toBeVisible();
|
||||
} finally {
|
||||
await device.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("retries publication without duplicates and rejects invalid batches without partial writes", async ({
|
||||
activityMcp,
|
||||
}) => {
|
||||
const input = {
|
||||
requestId: "stable-publication",
|
||||
expectedRevision: 0,
|
||||
activities: activityProposals,
|
||||
};
|
||||
const receipt = await activityMcp.tool("publish_activities", input);
|
||||
expect(receipt.ok).toBe(true);
|
||||
expect(await activityMcp.tool("publish_activities", input)).toEqual(receipt);
|
||||
expect(
|
||||
await activityMcp.tool("publish_activities", { ...input, requestId: "stale" }),
|
||||
).toMatchObject({ ok: false, error: { code: "revision-conflict" } });
|
||||
const snapshot = await activityMcp.context();
|
||||
expect(
|
||||
await activityMcp.tool("publish_activities", {
|
||||
requestId: "invalid",
|
||||
expectedRevision: snapshot.revision,
|
||||
activities: [
|
||||
{
|
||||
id: "valid-sibling",
|
||||
kind: "task",
|
||||
title: "Valid sibling",
|
||||
nextAction: "Do the chore",
|
||||
sources: [],
|
||||
},
|
||||
{
|
||||
id: "invalid-sibling",
|
||||
kind: "reply",
|
||||
title: "Missing reply target",
|
||||
nextAction: "Check",
|
||||
sources: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toMatchObject({ ok: false, error: { code: "invalid-input" } });
|
||||
expect(await activityMcp.context()).toEqual(snapshot);
|
||||
});
|
||||
Reference in New Issue
Block a user