180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
import {
|
|
canDeleteSidebarWorker,
|
|
deleteSidebarWorker,
|
|
stopSidebarWorker,
|
|
} from "../../src/lib/workspace/sidebar/worker-actions.ts";
|
|
import type { Worker } from "../../src/lib/workspace/sidebar/types.ts";
|
|
|
|
function assert(
|
|
condition: unknown,
|
|
message = "assertion failed",
|
|
): asserts condition {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
function assertEquals(actual: unknown, expected: unknown): void {
|
|
const actualJson = JSON.stringify(actual);
|
|
const expectedJson = JSON.stringify(expected);
|
|
if (actualJson !== expectedJson) {
|
|
throw new Error(`expected ${expectedJson}, received ${actualJson}`);
|
|
}
|
|
}
|
|
|
|
async function assertRejects(
|
|
operation: () => Promise<unknown>,
|
|
message: string,
|
|
): Promise<void> {
|
|
try {
|
|
await operation();
|
|
} catch (cause) {
|
|
assert(cause instanceof Error, "expected an Error");
|
|
assert(
|
|
cause.message.includes(message),
|
|
`expected error containing ${message}`,
|
|
);
|
|
return;
|
|
}
|
|
throw new Error("expected operation to reject");
|
|
}
|
|
|
|
const worker = {
|
|
runtime_id: "runtime /",
|
|
worker_id: "worker /",
|
|
state: "running",
|
|
capabilities: { can_stop: true },
|
|
} as Worker;
|
|
|
|
function jsonResponse(payload: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(payload), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
Deno.test("sidebar Stop uses the workspace-scoped Worker lifecycle endpoint", async () => {
|
|
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
requests.push({ url: input.toString(), init });
|
|
return jsonResponse({ state: "accepted", diagnostics: [] });
|
|
}) as typeof fetch;
|
|
|
|
await stopSidebarWorker("team space", worker, fetchFn);
|
|
|
|
assertEquals(requests.length, 1);
|
|
assertEquals(
|
|
requests[0]?.url,
|
|
"/api/w/team%20space/runtimes/runtime%20%2F/workers/worker%20%2F/stop",
|
|
);
|
|
assertEquals(requests[0]?.init?.method, "POST");
|
|
assertEquals(JSON.parse(String(requests[0]?.init?.body)), {
|
|
reason: "stopped from Workspace sidebar",
|
|
});
|
|
});
|
|
|
|
Deno.test("sidebar Stop rejects non-accepted lifecycle responses", async () => {
|
|
const fetchFn = (() =>
|
|
Promise.resolve(
|
|
jsonResponse({
|
|
state: "rejected",
|
|
diagnostics: [{ severity: "error", message: "Worker cannot stop" }],
|
|
}),
|
|
)) as typeof fetch;
|
|
|
|
await assertRejects(
|
|
() => stopSidebarWorker("workspace", worker, fetchFn),
|
|
"Worker cannot stop",
|
|
);
|
|
});
|
|
|
|
Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", async () => {
|
|
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
requests.push({ url: input.toString(), init });
|
|
if (requests.length === 1) {
|
|
return jsonResponse({
|
|
revision: 7,
|
|
digest: "digest-7",
|
|
candidates: [],
|
|
workers: [{
|
|
target_id: "worker-target",
|
|
runtime_id: worker.runtime_id,
|
|
runtime_worker_id: worker.worker_id,
|
|
blocking_reason: null,
|
|
}],
|
|
workdirs: [],
|
|
diagnostics: [],
|
|
});
|
|
}
|
|
return jsonResponse({
|
|
results: [{
|
|
target_id: "worker-target",
|
|
status: "deleted",
|
|
message: null,
|
|
}],
|
|
diagnostics: [],
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
await deleteSidebarWorker("team", { ...worker, state: "stopped" }, fetchFn);
|
|
|
|
assertEquals(requests.map((request) => request.url), [
|
|
"/api/w/team/runtimes/runtime%20%2F/cleanup-plan",
|
|
"/api/w/team/runtimes/runtime%20%2F/cleanup-executions",
|
|
]);
|
|
assertEquals(requests[1]?.init?.method, "POST");
|
|
assertEquals(JSON.parse(String(requests[1]?.init?.body)), {
|
|
expected_plan_revision: 7,
|
|
expected_plan_digest: "digest-7",
|
|
worker_target_ids: ["worker-target"],
|
|
workdir_target_ids: [],
|
|
confirm_dirty_discard_target_ids: [],
|
|
});
|
|
});
|
|
|
|
Deno.test("sidebar Delete reports cleanup-plan blocking reasons", async () => {
|
|
const fetchFn = (() =>
|
|
Promise.resolve(
|
|
jsonResponse({
|
|
revision: 8,
|
|
digest: "digest-8",
|
|
candidates: [],
|
|
workers: [{
|
|
target_id: "worker-target",
|
|
runtime_id: worker.runtime_id,
|
|
runtime_worker_id: worker.worker_id,
|
|
blocking_reason: "Worker is pinned",
|
|
}],
|
|
workdirs: [],
|
|
diagnostics: [],
|
|
}),
|
|
)) as typeof fetch;
|
|
|
|
await assertRejects(
|
|
() => deleteSidebarWorker("team", { ...worker, state: "stopped" }, fetchFn),
|
|
"Worker is pinned",
|
|
);
|
|
});
|
|
|
|
Deno.test("sidebar Delete is enabled only for stopped Workers", () => {
|
|
assert(!canDeleteSidebarWorker(worker));
|
|
assert(canDeleteSidebarWorker({ ...worker, state: "stopped" }));
|
|
});
|
|
|
|
Deno.test("Worker navigation exposes an accessible hover action menu", async () => {
|
|
const source = await Deno.readTextFile(
|
|
new URL(
|
|
"../../src/lib/workspace/sidebar/WorkersNavSection.svelte",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
const styles = await Deno.readTextFile(
|
|
new URL("../../src/lib/workspace/sidebar/sidebar.css", import.meta.url),
|
|
);
|
|
|
|
assert(source.includes('aria-haspopup="menu"'));
|
|
assert(source.includes('role="menuitem"'));
|
|
assert(source.includes("stopSidebarWorker(workspaceId, worker)"));
|
|
assert(source.includes("deleteSidebarWorker(workspaceId, worker)"));
|
|
assert(styles.includes(".worker-nav-item:hover .worker-actions-trigger"));
|
|
});
|