Use content digests, key fingerprints, binding and trust identities, immutable mutation receipts, and observation tokens instead of state-update ordinals across API, storage, Runtime, and Web contracts. Preserve historical replay and secret material through frozen migrations; preflight uncertain removals before cutover, resume provisioning receipts, validate encrypted migration candidates, and align fresh/upgraded schemas. Include reviewed regression fixes, generated artifacts, and validation evidence.
424 lines
13 KiB
TypeScript
424 lines
13 KiB
TypeScript
import {
|
|
canDeleteSidebarWorker,
|
|
canRestoreWorker,
|
|
canStopSidebarWorker,
|
|
createRestoreRequest,
|
|
deleteSidebarWorker,
|
|
restoreErrorNotice,
|
|
restoreNotice,
|
|
restoreWorkspaceWorker,
|
|
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",
|
|
} 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 follows observed catalog lifecycle, not foreground display state", () => {
|
|
for (const lifecycleState of ["idle", "running", "paused"]) {
|
|
const target = {
|
|
availability: "observed" as const,
|
|
lifecycleState,
|
|
state: "unknown",
|
|
};
|
|
assert(canStopSidebarWorker(target));
|
|
assert(!canStopSidebarWorker({ ...target, availability: "unavailable" }));
|
|
}
|
|
for (
|
|
const lifecycleState of [
|
|
"stopped",
|
|
"missing",
|
|
"execution_unavailable",
|
|
"unknown",
|
|
]
|
|
) {
|
|
const target = {
|
|
availability: "observed" as const,
|
|
lifecycleState,
|
|
state: "running",
|
|
};
|
|
assert(!canStopSidebarWorker(target));
|
|
}
|
|
});
|
|
|
|
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",
|
|
runtime_id: worker.runtime_id,
|
|
worker_id: worker.worker_id,
|
|
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",
|
|
runtime_id: worker.runtime_id,
|
|
worker_id: worker.worker_id,
|
|
diagnostics: [{
|
|
code: "cannot_stop",
|
|
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({
|
|
workspace_id: "team",
|
|
runtime_id: worker.runtime_id,
|
|
generated_at: "2026-01-01T00:00:00Z",
|
|
|
|
digest: "digest-7",
|
|
workers: [{
|
|
target_id: "worker-target",
|
|
action: "worker_delete",
|
|
worker_id: worker.worker_id,
|
|
runtime_id: worker.runtime_id,
|
|
runtime_worker_id: worker.worker_id,
|
|
reason: "stopped worker",
|
|
blocking_reason: null,
|
|
pinned: false,
|
|
retention_state: "stopped",
|
|
linked_workdir_ids: [],
|
|
running_linked: false,
|
|
}],
|
|
workdirs: [],
|
|
diagnostics: [],
|
|
});
|
|
}
|
|
return jsonResponse({
|
|
workspace_id: "team",
|
|
runtime_id: worker.runtime_id,
|
|
executed_at: "2026-01-01T00:00:01Z",
|
|
results: [{
|
|
target_id: "worker-target",
|
|
action: "worker_delete",
|
|
status: "deleted",
|
|
message: "",
|
|
}],
|
|
plan_after: {
|
|
workspace_id: "team",
|
|
runtime_id: worker.runtime_id,
|
|
generated_at: "2026-01-01T00:00:01Z",
|
|
|
|
digest: "digest-8",
|
|
workers: [],
|
|
workdirs: [],
|
|
diagnostics: [],
|
|
},
|
|
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_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({
|
|
workspace_id: "team",
|
|
runtime_id: worker.runtime_id,
|
|
generated_at: "2026-01-01T00:00:00Z",
|
|
|
|
digest: "digest-8",
|
|
workers: [{
|
|
target_id: "worker-target",
|
|
action: "worker_delete",
|
|
worker_id: worker.worker_id,
|
|
runtime_id: worker.runtime_id,
|
|
runtime_worker_id: worker.worker_id,
|
|
reason: "stopped worker",
|
|
blocking_reason: "Worker is pinned",
|
|
pinned: true,
|
|
retention_state: "pinned",
|
|
linked_workdir_ids: [],
|
|
running_linked: false,
|
|
}],
|
|
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"));
|
|
assert(source.includes("deleteSidebarWorker"));
|
|
assert(styles.includes(".worker-nav-item:hover .worker-actions-trigger"));
|
|
});
|
|
|
|
Deno.test("Restore eligibility uses observed catalog state and backend token, not foreground state", () => {
|
|
const stopped = {
|
|
availability: "observed" as const,
|
|
state: "unknown",
|
|
lifecycleState: "stopped",
|
|
restore_observation_token: "generation",
|
|
};
|
|
assert(canRestoreWorker(stopped));
|
|
assert(!canRestoreWorker({ ...stopped, availability: "unavailable" }));
|
|
assert(
|
|
!canRestoreWorker({ ...stopped, lifecycleState: "idle", state: "stopped" }),
|
|
);
|
|
assert(!canRestoreWorker({ ...stopped, restore_observation_token: null }));
|
|
assert(
|
|
canRestoreWorker({
|
|
availability: "observed",
|
|
state: "stopped",
|
|
restore_observation_token: "generation",
|
|
}),
|
|
);
|
|
});
|
|
Deno.test("Restore scoped POST encodes identities and sends immutable intent without assignment query", async () => {
|
|
const request = createRestoreRequest({
|
|
restore_observation_token: "generation",
|
|
});
|
|
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
requests.push({ url: input.toString(), init });
|
|
return jsonResponse({
|
|
workspace_id: "team space",
|
|
runtime_id: worker.runtime_id,
|
|
worker_id: worker.worker_id,
|
|
result: { state: "accepted" },
|
|
});
|
|
}) as typeof fetch;
|
|
await restoreWorkspaceWorker("team space", worker, request, fetchFn);
|
|
await restoreWorkspaceWorker("team space", worker, request, fetchFn);
|
|
assertEquals(
|
|
requests[0].url,
|
|
"/api/w/team%20space/runtimes/runtime%20%2F/workers/worker%20%2F/restore",
|
|
);
|
|
assertEquals(requests[0].init?.method, "POST");
|
|
assertEquals(JSON.parse(String(requests[0].init?.body)), request);
|
|
assertEquals(requests[1].init?.body, requests[0].init?.body);
|
|
assert(request.request_id.length > 0);
|
|
});
|
|
Deno.test("Restore classification never promotes nonaccepted results and preserves unknown outcome recovery", async () => {
|
|
for (
|
|
const state of [
|
|
"accepted",
|
|
"rejected",
|
|
"rolled_back",
|
|
"reconciliation_required",
|
|
] as const
|
|
) {
|
|
const response = await restoreWorkspaceWorker(
|
|
"team",
|
|
worker,
|
|
{ expected_observation_token: "g", request_id: "id" },
|
|
(() =>
|
|
Promise.resolve(jsonResponse({
|
|
workspace_id: "team",
|
|
runtime_id: worker.runtime_id,
|
|
worker_id: worker.worker_id,
|
|
result: {
|
|
state,
|
|
diagnostics: [{
|
|
code: "result",
|
|
severity: "warning",
|
|
message: "backend reason",
|
|
}],
|
|
},
|
|
}))) as typeof fetch,
|
|
);
|
|
const notice = restoreNotice(response);
|
|
assertEquals(notice.level === "info", state === "accepted");
|
|
assertEquals(notice.retry, state === "reconciliation_required");
|
|
if (state !== "accepted") assert(notice.message.includes("backend reason"));
|
|
}
|
|
for (const status of [400, 403, 409, 500]) {
|
|
try {
|
|
await restoreWorkspaceWorker(
|
|
"team",
|
|
worker,
|
|
{ expected_observation_token: "g", request_id: "id" },
|
|
(() =>
|
|
Promise.resolve(
|
|
jsonResponse({ error: "conflict", message: "changed" }, status),
|
|
)) as typeof fetch,
|
|
);
|
|
throw new Error("must reject");
|
|
} catch (error) {
|
|
const notice = restoreErrorNotice(error);
|
|
assertEquals(notice.retry, status >= 500);
|
|
if (status === 409) {
|
|
assertEquals(notice.title, "Worker observation changed");
|
|
}
|
|
}
|
|
}
|
|
assert(restoreErrorNotice(new TypeError("network failed")).retry);
|
|
await assertRejects(
|
|
() =>
|
|
restoreWorkspaceWorker(
|
|
"team",
|
|
worker,
|
|
{ expected_observation_token: "g", request_id: "id" },
|
|
(() =>
|
|
Promise.resolve(
|
|
jsonResponse({
|
|
workspace_id: "other",
|
|
runtime_id: worker.runtime_id,
|
|
worker_id: worker.worker_id,
|
|
result: { state: "accepted" },
|
|
}),
|
|
)) as typeof fetch,
|
|
),
|
|
"requested target",
|
|
);
|
|
await assertRejects(
|
|
() =>
|
|
restoreWorkspaceWorker(
|
|
"team",
|
|
worker,
|
|
{ expected_observation_token: "g", request_id: "id" },
|
|
(() =>
|
|
Promise.resolve(
|
|
jsonResponse({
|
|
workspace_id: "team",
|
|
runtime_id: worker.runtime_id,
|
|
worker_id: worker.worker_id,
|
|
result: { state: "future" },
|
|
}),
|
|
)) as typeof fetch,
|
|
),
|
|
"unknown value",
|
|
);
|
|
});
|
|
|
|
Deno.test("Restore bounds the response before decoding and retains uncertain outcome instead of success", async () => {
|
|
let response = new Response("{}", {
|
|
headers: { "content-length": String(8 * 1024 * 1024 + 1) },
|
|
});
|
|
let caught: unknown;
|
|
try {
|
|
await restoreWorkspaceWorker("team", worker, {
|
|
expected_observation_token: "g",
|
|
request_id: "id",
|
|
}, (() => Promise.resolve(response)) as typeof fetch);
|
|
} catch (error) {
|
|
caught = error;
|
|
}
|
|
assert(caught instanceof Error);
|
|
assert(restoreErrorNotice(caught).retry);
|
|
response = new Response("{}", {
|
|
status: 409,
|
|
headers: { "content-length": String(64 * 1024 + 1) },
|
|
});
|
|
try {
|
|
await restoreWorkspaceWorker("team", worker, {
|
|
expected_observation_token: "g",
|
|
request_id: "id",
|
|
}, (() => Promise.resolve(response)) as typeof fetch);
|
|
} catch (error) {
|
|
caught = error;
|
|
}
|
|
assertEquals(restoreErrorNotice(caught).title, "Worker observation changed");
|
|
assertEquals(restoreErrorNotice(caught).retry, false);
|
|
});
|