226 lines
7.0 KiB
TypeScript
226 lines
7.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
archiveCanvas,
|
|
confirmCanvasPromotion,
|
|
enableCanvasWorkspaceData,
|
|
getCanvasRuntime,
|
|
listTaskCanvases,
|
|
listWorkspaceCanvases,
|
|
requestCanvasWorkspaceData,
|
|
requestCanvasPromotion,
|
|
type CanvasPromotionPreview,
|
|
type CanvasRuntimeResponse,
|
|
} from "./canvas-api";
|
|
|
|
const fetchSpy = vi.fn<typeof fetch>();
|
|
const API_BASE_URL = "http://api.test";
|
|
|
|
beforeEach(() => {
|
|
fetchSpy.mockReset();
|
|
vi.stubGlobal("fetch", fetchSpy);
|
|
});
|
|
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
function jsonResponse(body: unknown) {
|
|
return new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
describe("workspace canvas collection", () => {
|
|
it("requests only workspace-scoped canvases and can include archived records", async () => {
|
|
fetchSpy.mockResolvedValueOnce(jsonResponse({ canvases: [] }));
|
|
|
|
await listWorkspaceCanvases("workspace / one", {
|
|
includeArchived: true,
|
|
baseUrl: API_BASE_URL,
|
|
});
|
|
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
`${API_BASE_URL}/api/v1/workspaces/workspace%20%2F%20one/canvases?include_archived=true`,
|
|
expect.objectContaining({ credentials: "include" }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("task canvas collection", () => {
|
|
it("keeps the task and workspace scope in the request", async () => {
|
|
fetchSpy.mockResolvedValueOnce(jsonResponse({ canvases: [] }));
|
|
|
|
await listTaskCanvases("task-1", {
|
|
workspaceId: "workspace-1",
|
|
includeArchived: false,
|
|
baseUrl: API_BASE_URL,
|
|
});
|
|
|
|
expect(fetchSpy.mock.calls[0][0]).toBe(
|
|
`${API_BASE_URL}/api/v1/tasks/task-1/canvases?workspace_id=workspace-1`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("canvas host and lifecycle endpoints", () => {
|
|
it("preserves runtime binding and release fields from the host contract", async () => {
|
|
const runtimeResponse: CanvasRuntimeResponse = {
|
|
runtime_url: "https://runtime.test/canvas?token=renewed",
|
|
release_id: "release-7",
|
|
web_app_key: "canvas-app",
|
|
placement: "task_panel",
|
|
expires_in_seconds: 900,
|
|
canvas: {
|
|
id: "canvas-1",
|
|
plugin_instance_id: "instance-1",
|
|
plugin_id: "plugin-1",
|
|
workspace_id: "workspace-1",
|
|
task_id: "task-1",
|
|
scope_kind: "task",
|
|
title: "Canvas",
|
|
status: "active",
|
|
active_release_id: "release-7",
|
|
active_release_status: "valid",
|
|
},
|
|
binding: {
|
|
scope_kind: "task",
|
|
grant_generation: 4,
|
|
},
|
|
};
|
|
fetchSpy.mockResolvedValueOnce(jsonResponse(runtimeResponse));
|
|
|
|
await expect(getCanvasRuntime("canvas-1", { baseUrl: API_BASE_URL })).resolves.toEqual(
|
|
runtimeResponse,
|
|
);
|
|
});
|
|
|
|
it("preserves promotion source, placement, and permission review fields", async () => {
|
|
const preview: CanvasPromotionPreview = {
|
|
canvas_id: "canvas-1",
|
|
title: "Canvas",
|
|
origin_task_id: "task-1",
|
|
source_actor_kind: "task_agent",
|
|
source_user_id: "user-1",
|
|
source_task_id: "task-1",
|
|
source_session_id: "session-1",
|
|
permissions: {
|
|
reads: ["tasks.read"],
|
|
writes: ["tasks.write"],
|
|
events: ["task.updated"],
|
|
shared_state: true,
|
|
external_origins: ["https://example.test"],
|
|
},
|
|
current_scope: "task",
|
|
target_scope: "workspace",
|
|
placement: "workspace_sidebar",
|
|
active_release_id: "release-7",
|
|
permission_digest: "digest-7",
|
|
grant_generation: 4,
|
|
};
|
|
fetchSpy.mockResolvedValueOnce(jsonResponse(preview));
|
|
|
|
await expect(requestCanvasPromotion("canvas-1", { baseUrl: API_BASE_URL })).resolves.toEqual(
|
|
preview,
|
|
);
|
|
});
|
|
|
|
it("uses the same canvas identity for runtime and promotion actions", async () => {
|
|
fetchSpy
|
|
.mockResolvedValueOnce(jsonResponse({ runtime_url: "https://runtime.test/canvas" }))
|
|
.mockResolvedValueOnce(jsonResponse({ canvas_id: "canvas-1", permissions: [] }))
|
|
.mockResolvedValueOnce(jsonResponse({ id: "canvas-1", status: "active" }));
|
|
|
|
await getCanvasRuntime("canvas-1", { baseUrl: API_BASE_URL });
|
|
await requestCanvasPromotion("canvas-1", { baseUrl: API_BASE_URL });
|
|
await confirmCanvasPromotion(
|
|
"canvas-1",
|
|
{
|
|
expected_release_id: "release-7",
|
|
expected_permission_digest: "digest-7",
|
|
expected_grant_generation: 4,
|
|
},
|
|
{ baseUrl: API_BASE_URL },
|
|
);
|
|
|
|
expect(fetchSpy.mock.calls.map(([url]) => url)).toEqual([
|
|
`${API_BASE_URL}/api/v1/canvases/canvas-1/runtime`,
|
|
`${API_BASE_URL}/api/v1/canvases/canvas-1/promotion-preview`,
|
|
`${API_BASE_URL}/api/v1/canvases/canvas-1/promotion`,
|
|
]);
|
|
expect(fetchSpy.mock.calls[2][1]).toMatchObject({
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
expected_release_id: "release-7",
|
|
expected_permission_digest: "digest-7",
|
|
expected_grant_generation: 4,
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("workspace data scope endpoint", () => {
|
|
it("reviews and confirms a task canvas workspace data upgrade", async () => {
|
|
const preview = {
|
|
canvas: {
|
|
id: "canvas-1",
|
|
plugin_instance_id: "instance-1",
|
|
plugin_id: "plugin-1",
|
|
workspace_id: "workspace-1",
|
|
task_id: "task-1",
|
|
scope_kind: "task",
|
|
data_scope_kind: "task",
|
|
title: "Canvas",
|
|
status: "active",
|
|
},
|
|
active_release_id: "release-3",
|
|
permission_digest: "digest-3",
|
|
grant_generation: 9,
|
|
current_data_scope_kind: "task",
|
|
target_data_scope_kind: "workspace",
|
|
permissions: { reads: ["tasks"] },
|
|
};
|
|
fetchSpy
|
|
.mockResolvedValueOnce(jsonResponse(preview))
|
|
.mockResolvedValueOnce(jsonResponse({ ...preview.canvas, data_scope_kind: "workspace" }));
|
|
|
|
await expect(
|
|
requestCanvasWorkspaceData("canvas-1", { baseUrl: API_BASE_URL }),
|
|
).resolves.toEqual(preview);
|
|
await enableCanvasWorkspaceData(
|
|
"canvas-1",
|
|
{
|
|
expected_release_id: "release-3",
|
|
expected_permission_digest: "digest-3",
|
|
expected_grant_generation: 9,
|
|
},
|
|
{ baseUrl: API_BASE_URL },
|
|
);
|
|
|
|
expect(fetchSpy.mock.calls[0][0]).toBe(
|
|
`${API_BASE_URL}/api/v1/canvases/canvas-1/workspace-data-preview`,
|
|
);
|
|
expect(fetchSpy.mock.calls[1][0]).toBe(
|
|
`${API_BASE_URL}/api/v1/canvases/canvas-1/workspace-data`,
|
|
);
|
|
expect(fetchSpy.mock.calls[1][1]).toMatchObject({
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
expected_release_id: "release-3",
|
|
expected_permission_digest: "digest-3",
|
|
expected_grant_generation: 9,
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("canvas archive endpoint", () => {
|
|
it("archives with a mutation request and preserves the API response", async () => {
|
|
fetchSpy.mockResolvedValueOnce(jsonResponse({ id: "canvas-1", status: "archived" }));
|
|
|
|
await expect(archiveCanvas("canvas-1", { baseUrl: API_BASE_URL })).resolves.toMatchObject({
|
|
status: "archived",
|
|
});
|
|
|
|
expect(fetchSpy.mock.calls[0][1]).toMatchObject({ method: "POST" });
|
|
});
|
|
});
|