feat: preview task canvases with workspace data (#3887)
This commit is contained in:
@@ -4,9 +4,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ApiError } from "@/lib/api/client";
|
||||
import type { Canvas, CanvasRelease } from "@/lib/api/domains/canvas-api";
|
||||
|
||||
const ENABLE_WORKSPACE_DATA_TEXT = vi.hoisted(() => "Enable workspace data");
|
||||
const READ_TASK_DATA_TEXT = vi.hoisted(() => "Read task data");
|
||||
|
||||
const {
|
||||
mockRequestCanvasPromotion,
|
||||
mockConfirmCanvasPromotion,
|
||||
mockRequestCanvasWorkspaceData,
|
||||
mockEnableCanvasWorkspaceData,
|
||||
mockListCanvasReleases,
|
||||
mockApproveCanvasRelease,
|
||||
mockRejectCanvasRelease,
|
||||
@@ -14,6 +19,8 @@ const {
|
||||
} = vi.hoisted(() => ({
|
||||
mockRequestCanvasPromotion: vi.fn(),
|
||||
mockConfirmCanvasPromotion: vi.fn(),
|
||||
mockRequestCanvasWorkspaceData: vi.fn(),
|
||||
mockEnableCanvasWorkspaceData: vi.fn(),
|
||||
mockListCanvasReleases: vi.fn(),
|
||||
mockApproveCanvasRelease: vi.fn(),
|
||||
mockRejectCanvasRelease: vi.fn(),
|
||||
@@ -28,6 +35,24 @@ const translate = vi.hoisted(() => {
|
||||
"canvases:promoteCanvasDescription": "Review permissions before promotion.",
|
||||
"canvases:loadingPermissions": "Loading requested permissions",
|
||||
"canvases:promotionScopeChange": "Promotion changes the canvas scope.",
|
||||
"canvases:promotionDataAccessPreserved":
|
||||
"Promotion changes placement only. Data access stays the same.",
|
||||
"canvases:promotionDataAccessChanges": "Promotion expands data access.",
|
||||
"canvases:promotionSourceScope": "Placement before promotion",
|
||||
"canvases:promotionTargetScope": "Placement after promotion",
|
||||
"canvases:promotionCurrentDataScope": "Data access before promotion",
|
||||
"canvases:promotionTargetDataScope": "Data access after promotion",
|
||||
"canvases:taskDataScope": "Task data",
|
||||
"canvases:workspaceDataScope": "Workspace data",
|
||||
"canvases:taskPlacementScope": "Task",
|
||||
"canvases:workspacePlacementScope": "Workspace",
|
||||
"canvases:enableWorkspaceData": ENABLE_WORKSPACE_DATA_TEXT,
|
||||
"canvases:enableWorkspaceDataHelp": "Enable declared permissions for this workspace.",
|
||||
"canvases:workspaceDataReviewDescription": "Review permissions for {{title}}.",
|
||||
"canvases:workspaceDataReviewScopeChange":
|
||||
"Only data access changes. The canvas stays in this task.",
|
||||
"canvases:enablingWorkspaceData": "Enabling workspace data",
|
||||
"canvases:activeRelease": "Active release",
|
||||
"canvases:noAdditionalPermissions": "No additional permissions were requested.",
|
||||
"canvases:permissionDeclaration": "Declared permissions",
|
||||
"canvases:missingPermissions": "Permissions still needed",
|
||||
@@ -50,13 +75,15 @@ const translate = vi.hoisted(() => {
|
||||
"canvases:permissionExternalOrigins": "External origins",
|
||||
"canvases:permissionExternalOrigin": "External HTTPS origin",
|
||||
"canvases:permissionSharedState": "Read and write shared canvas state",
|
||||
"canvases:permissionReadTasks": "Read task data",
|
||||
"canvases:permissionReadTasks": READ_TASK_DATA_TEXT,
|
||||
"canvases:permissionReadWorkflows": "Read workflow data",
|
||||
"canvases:permissionWriteTasks": "Write task data",
|
||||
"canvases:permissionWriteMessages": "Write task messages",
|
||||
"canvases:permissionEventTaskUpdated": "Receive task update events",
|
||||
"canvases:permissionEventWorkflowUpdated": "Receive workflow update events",
|
||||
"canvases:unsupportedPermission": "Unsupported permission: {{value}}",
|
||||
"canvases:workspaceDataReviewStale": "The review changed. Open it again.",
|
||||
"canvases:workspaceOwnerRequired": "Only the workspace owner can enable workspace data.",
|
||||
"canvases:selectRelease": "Select a release to review",
|
||||
"canvases:releaseOption": "{{date}} ({{status}})",
|
||||
"canvases:releaseDate": "Created",
|
||||
@@ -71,11 +98,9 @@ const translate = vi.hoisted(() => {
|
||||
"canvases:sourceActorSystem": "Kandev",
|
||||
"canvases:sourceActorUnknown": "Unknown author",
|
||||
"canvases:sharedState": "Shared state",
|
||||
"canvases:promotionSourceScope": "Source scope",
|
||||
"canvases:promotionSourceActor": "Source actor",
|
||||
"canvases:promotionSourceTask": "Source task",
|
||||
"canvases:promotionSourceSession": "Source session",
|
||||
"canvases:promotionTargetScope": "Target scope",
|
||||
"canvases:promotionPlacement": "Workspace placement",
|
||||
"common:cancel": "Cancel",
|
||||
"common:close": "Close",
|
||||
@@ -114,13 +139,19 @@ vi.mock("@kandev/ui/dialog", () => ({
|
||||
vi.mock("@/lib/api/domains/canvas-api", () => ({
|
||||
approveCanvasRelease: mockApproveCanvasRelease,
|
||||
confirmCanvasPromotion: mockConfirmCanvasPromotion,
|
||||
enableCanvasWorkspaceData: mockEnableCanvasWorkspaceData,
|
||||
listCanvasReleases: mockListCanvasReleases,
|
||||
rejectCanvasRelease: mockRejectCanvasRelease,
|
||||
requestCanvasWorkspaceData: mockRequestCanvasWorkspaceData,
|
||||
requestCanvasPromotion: mockRequestCanvasPromotion,
|
||||
rollbackCanvas: mockRollbackCanvas,
|
||||
}));
|
||||
|
||||
import { CanvasPromotionDialog, CanvasReleaseDialog } from "./canvas-lifecycle-dialogs";
|
||||
import {
|
||||
CanvasPromotionDialog,
|
||||
CanvasReleaseDialog,
|
||||
CanvasWorkspaceDataDialog,
|
||||
} from "./canvas-lifecycle-dialogs";
|
||||
|
||||
const canvas: Canvas = {
|
||||
id: "canvas-1",
|
||||
@@ -143,6 +174,8 @@ const COPY = {
|
||||
rollbackRelease: "Roll back release",
|
||||
promotingCanvas: "Promoting canvas",
|
||||
confirmPromotion: "Confirm promotion",
|
||||
enableWorkspaceData: ENABLE_WORKSPACE_DATA_TEXT,
|
||||
enablingWorkspaceData: "Enabling workspace data",
|
||||
cancel: "Cancel",
|
||||
} as const;
|
||||
|
||||
@@ -180,6 +213,8 @@ beforeEach(() => {
|
||||
source_session_name: "Source session name",
|
||||
current_scope: "task",
|
||||
target_scope: "workspace",
|
||||
current_data_scope_kind: "task",
|
||||
target_data_scope_kind: "workspace",
|
||||
placement: "workspace_sidebar",
|
||||
permissions: {
|
||||
reads: [TASKS_READ_PERMISSION],
|
||||
@@ -190,6 +225,16 @@ beforeEach(() => {
|
||||
},
|
||||
});
|
||||
mockConfirmCanvasPromotion.mockReset();
|
||||
mockRequestCanvasWorkspaceData.mockReset().mockResolvedValue({
|
||||
canvas,
|
||||
active_release_id: "release-1",
|
||||
permission_digest: "digest-1",
|
||||
grant_generation: 7,
|
||||
current_data_scope_kind: "task",
|
||||
target_data_scope_kind: "workspace",
|
||||
permissions: { reads: ["tasks"] },
|
||||
});
|
||||
mockEnableCanvasWorkspaceData.mockReset();
|
||||
mockListCanvasReleases.mockReset().mockResolvedValue({ releases: [] });
|
||||
mockApproveCanvasRelease.mockReset();
|
||||
mockRejectCanvasRelease.mockReset();
|
||||
@@ -206,7 +251,7 @@ describe("CanvasPromotionDialog", () => {
|
||||
render(<CanvasPromotionDialog canvas={canvas} open onOpenChange={vi.fn()} />);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("canvas-promotion-source-scope").textContent).toContain("task"),
|
||||
expect(screen.getByTestId("canvas-promotion-source-scope").textContent).toContain("Task"),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("canvas-promotion-source-task").textContent).toContain(
|
||||
@@ -219,7 +264,7 @@ describe("CanvasPromotionDialog", () => {
|
||||
"workspace_sidebar",
|
||||
);
|
||||
expect(screen.getByText("API reads")).toBeTruthy();
|
||||
expect(screen.getByText("Read task data")).toBeTruthy();
|
||||
expect(screen.getByText(READ_TASK_DATA_TEXT)).toBeTruthy();
|
||||
expect(screen.getByText("API writes")).toBeTruthy();
|
||||
expect(screen.getByText("Write task data")).toBeTruthy();
|
||||
expect(screen.getByText("Events")).toBeTruthy();
|
||||
@@ -227,6 +272,11 @@ describe("CanvasPromotionDialog", () => {
|
||||
expect(screen.getByText("Shared state")).toBeTruthy();
|
||||
expect(screen.getByText("External origins")).toBeTruthy();
|
||||
expect(screen.getByText("https://example.test")).toBeTruthy();
|
||||
expect(screen.getByTestId("canvas-promotion-current-data-scope").textContent).toBe("Task data");
|
||||
expect(screen.getByTestId("canvas-promotion-target-data-scope").textContent).toBe(
|
||||
"Workspace data",
|
||||
);
|
||||
expect(screen.getByText("Promotion expands data access.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("maps stable API error codes to localized copy", async () => {
|
||||
@@ -309,6 +359,72 @@ describe("CanvasPromotionDialog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CanvasWorkspaceDataDialog", () => {
|
||||
it("shows the active release, current and target data scopes, and declared permissions", async () => {
|
||||
render(<CanvasWorkspaceDataDialog canvas={canvas} open onOpenChange={vi.fn()} />);
|
||||
|
||||
expect((await screen.findByTestId("canvas-workspace-data-release")).textContent).toBe(
|
||||
"release-1",
|
||||
);
|
||||
expect(screen.getByTestId("canvas-workspace-data-current-scope").textContent).toBe("Task data");
|
||||
expect(screen.getByTestId("canvas-workspace-data-target-scope").textContent).toBe(
|
||||
"Workspace data",
|
||||
);
|
||||
expect(screen.getByText(READ_TASK_DATA_TEXT)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("enables exactly the reviewed release and grant generation", async () => {
|
||||
const updated = { ...canvas, data_scope_kind: "workspace" };
|
||||
const onCompleted = vi.fn();
|
||||
const onOpenChange = vi.fn();
|
||||
mockEnableCanvasWorkspaceData.mockResolvedValue(updated);
|
||||
|
||||
render(
|
||||
<CanvasWorkspaceDataDialog
|
||||
canvas={canvas}
|
||||
open
|
||||
onOpenChange={onOpenChange}
|
||||
onCompleted={onCompleted}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: COPY.enableWorkspaceData }));
|
||||
await waitFor(() =>
|
||||
expect(mockEnableCanvasWorkspaceData).toHaveBeenCalledWith(canvas.id, {
|
||||
expected_release_id: "release-1",
|
||||
expected_permission_digest: "digest-1",
|
||||
expected_grant_generation: 7,
|
||||
}),
|
||||
);
|
||||
await waitFor(() => expect(onCompleted).toHaveBeenCalledWith(updated));
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("cancels without changing workspace data access", async () => {
|
||||
const onOpenChange = vi.fn();
|
||||
render(<CanvasWorkspaceDataDialog canvas={canvas} open onOpenChange={onOpenChange} />);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: COPY.cancel }));
|
||||
|
||||
expect(mockEnableCanvasWorkspaceData).not.toHaveBeenCalled();
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("uses a full-height phone review with one scroll region and safe-area actions", async () => {
|
||||
responsive.isMobile = true;
|
||||
render(<CanvasWorkspaceDataDialog canvas={canvas} open onOpenChange={vi.fn()} />);
|
||||
|
||||
await screen.findByTestId("canvas-workspace-data-release");
|
||||
expect(screen.getByTestId("canvas-workspace-data-dialog").className).toContain("h-dvh");
|
||||
expect(screen.getByTestId("canvas-workspace-data-review-scroll").className).toContain(
|
||||
"overflow-y-auto",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: COPY.enableWorkspaceData }).className).toContain(
|
||||
"min-h-12",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CanvasReleaseDialog validation", () => {
|
||||
it("localizes stable release validation codes", async () => {
|
||||
mockListCanvasReleases.mockResolvedValue({
|
||||
@@ -353,7 +469,7 @@ describe("CanvasReleaseDialog permission review", () => {
|
||||
expect(screen.getByTestId("canvas-release-permissions-release-pending")).toBeTruthy(),
|
||||
);
|
||||
expect(screen.getByText("Declared permissions")).toBeTruthy();
|
||||
expect(screen.getByText("Read task data")).toBeTruthy();
|
||||
expect(screen.getByText(READ_TASK_DATA_TEXT)).toBeTruthy();
|
||||
expect(screen.getByText("Write task messages")).toBeTruthy();
|
||||
expect(screen.getByText(EXTERNAL_ORIGIN)).toBeTruthy();
|
||||
expect(screen.getByText("New")).toBeTruthy();
|
||||
|
||||
Reference in New Issue
Block a user