feat: preview task canvases with workspace data (#3887)

This commit is contained in:
Carlos Florêncio
2026-09-23 20:51:55 +01:00
committed by GitHub
parent 9874ac4cf5
commit 4bebf7c511
79 changed files with 3369 additions and 282 deletions
@@ -13,6 +13,7 @@ import {
publishTaskCanvas,
removeCanvas,
promoteCanvas,
seedCanvasWorkspacePreview,
seedTaskCanvas,
waitForTaskCanvas,
} from "./canvas-fixture";
@@ -348,6 +349,117 @@ test.describe("Plugin-backed canvases on mobile", () => {
}
});
test("reviews workspace data for a legacy task canvas before enabling it", async ({
testPage,
apiClient,
backend,
seedData,
}) => {
test.setTimeout(150_000);
const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
const canvasIds: string[] = [];
try {
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true);
canvasIds.push(seeded.canvas.id);
const activeCanvas = await approvePendingCanvasThroughHost(
testPage,
apiClient,
seeded.canvas,
);
const legacyCanvas = { ...activeCanvas, data_scope_kind: "task" };
const permissionDigest = "legacy-task-canvas-review";
const grantGeneration = activeCanvas.grant_generation ?? 0;
let workspaceDataEnabled = false;
let submittedReview: Record<string, unknown> | undefined;
const canvasURL = new RegExp(`/api/v1/canvases/${activeCanvas.id}(?:\\?.*)?$`);
await testPage.route(canvasURL, async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
...activeCanvas,
data_scope_kind: workspaceDataEnabled ? "workspace" : "task",
}),
});
});
await testPage.route(
new RegExp(`/api/v1/canvases/${activeCanvas.id}/workspace-data-preview(?:\\?.*)?$`),
async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
canvas: legacyCanvas,
active_release_id: activeCanvas.active_release_id,
permission_digest: permissionDigest,
grant_generation: grantGeneration,
current_data_scope_kind: "task",
target_data_scope_kind: "workspace",
permissions: {
reads: ["tasks"],
writes: ["messages"],
events: ["task.updated"],
shared_state: false,
external_origins: [],
},
}),
});
},
);
await testPage.route(
new RegExp(`/api/v1/canvases/${activeCanvas.id}/workspace-data(?:\\?.*)?$`),
async (route) => {
submittedReview = route.request().postDataJSON() as Record<string, unknown>;
workspaceDataEnabled = true;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ...legacyCanvas, data_scope_kind: "workspace" }),
});
},
);
await testPage.goto(canvasHref(activeCanvas.id));
await expect(testPage.getByTestId("canvas-data-scope")).toHaveText("Task data");
await testPage.getByTestId("canvas-mobile-actions").tap();
const actions = testPage.getByTestId("canvas-mobile-actions-sheet");
await actions.getByTestId("canvas-action-enable-workspace-data").tap();
const review = testPage.getByTestId("canvas-workspace-data-dialog");
await expect(review).toBeVisible();
await expect(review.getByTestId("canvas-workspace-data-release")).toHaveText(
activeCanvas.active_release_id ?? "",
);
await expect(review.getByTestId("canvas-workspace-data-current-scope")).toHaveText(
"Task data",
);
await expect(review.getByTestId("canvas-workspace-data-target-scope")).toHaveText(
"Workspace data",
);
await expect(review.getByText("Read task data")).toBeVisible();
await review.getByRole("button", { name: "Enable workspace data", exact: true }).tap();
await expect(review).toHaveCount(0);
await expect(testPage.getByTestId("canvas-data-scope")).toHaveText("Workspace data");
expect(submittedReview).toEqual({
expected_release_id: activeCanvas.active_release_id,
expected_permission_digest: permissionDigest,
expected_grant_generation: grantGeneration,
});
await testPage.getByTestId("canvas-mobile-actions").tap();
await expect(
testPage
.getByTestId("canvas-mobile-actions-sheet")
.getByTestId("canvas-action-enable-workspace-data"),
).toHaveCount(0);
} finally {
await Promise.all(canvasIds.map((canvasId) => removeCanvas(apiClient, canvasId)));
await releaseFeature();
}
});
test("uses a focused route, workspace navigation, and an inset action drawer", async ({
testPage,
apiClient,
@@ -358,8 +470,12 @@ test.describe("Plugin-backed canvases on mobile", () => {
const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
const canvasIds: string[] = [];
let workspacePreview: Awaited<ReturnType<typeof seedCanvasWorkspacePreview>> | undefined;
try {
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true);
workspacePreview = await seedCanvasWorkspacePreview(apiClient, seedData);
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true, {
foreignWorkspaceId: workspacePreview.foreignWorkspaceId,
});
canvasIds.push(seeded.canvas.id);
const activeCanvas = await approvePendingCanvasThroughHost(
testPage,
@@ -390,6 +506,21 @@ test.describe("Plugin-backed canvases on mobile", () => {
);
await expect(fixture.getByTestId("canvas-fixture-context")).toHaveText(seeded.taskId);
await expect(fixture.getByTestId("canvas-fixture-sse-status")).toHaveText("connected");
const expectedTaskCount = (await apiClient.listTasks(seedData.workspaceId)).tasks.length;
expect(expectedTaskCount).toBeGreaterThanOrEqual(2);
await fixture.getByTestId("canvas-fixture-refresh").tap();
await expect(fixture.getByTestId("canvas-fixture-refresh-status")).toHaveText("refreshed");
await expect(fixture.getByTestId("canvas-fixture-task-count")).toHaveText(
String(expectedTaskCount),
);
await expect(fixture.locator(".task-item")).toHaveCount(expectedTaskCount);
await expect(fixture.getByTestId("canvas-fixture-task-ids")).toContainText(seeded.taskId);
await expect(fixture.getByTestId("canvas-fixture-task-ids")).toContainText(
workspacePreview.workspaceTaskId,
);
await expect(fixture.getByTestId("canvas-fixture-foreign-workspace-status")).toHaveText(
"denied:403",
);
const actionsButton = testPage.getByTestId("canvas-mobile-actions");
await expect(actionsButton).toBeVisible();
@@ -409,7 +540,7 @@ test.describe("Plugin-backed canvases on mobile", () => {
const promotionDialog = testPage.getByTestId("canvas-promotion-dialog");
await expect(promotionDialog).toBeVisible();
await expect(promotionDialog.getByTestId("canvas-promotion-target-scope")).toHaveText(
"workspace",
"Workspace",
);
await promotionDialog.getByRole("button", { name: "Confirm promotion", exact: true }).tap();
@@ -480,6 +611,7 @@ test.describe("Plugin-backed canvases on mobile", () => {
).toBeVisible();
} finally {
await Promise.all(canvasIds.map((canvasId) => removeCanvas(apiClient, canvasId)));
await workspacePreview?.cleanup();
await releaseFeature();
}
});