refactor: remove worker capability summary flags
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
|
||||
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
|
||||
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
@@ -61,8 +61,6 @@ export type SubscriptionWorkerAvailability = "observed" | "unavailable";
|
||||
|
||||
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||
|
||||
export type WorkerMaintenanceState = "compacting";
|
||||
@@ -73,7 +71,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS
|
||||
|
||||
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
|
||||
|
||||
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
|
||||
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
@@ -107,7 +107,7 @@ export type RuntimeWorkerLifecycleResult = { diagnostics?: Array<Diagnostic>; ru
|
||||
|
||||
export type RuntimeWorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
|
||||
|
||||
export type RuntimeWorkerShowResponse = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
export type RuntimeWorkerShowResponse = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; };
|
||||
|
||||
@@ -123,7 +123,7 @@ export type RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest = { alias: strin
|
||||
|
||||
export type RuntimeWorkerSpawnWorkingDirectoryRequest = { repository_key: string; selector?: string | null; };
|
||||
|
||||
export type RuntimeWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<unknown>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
|
||||
export type RuntimeWorkerSummary = { diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<unknown>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };
|
||||
|
||||
@@ -137,8 +137,6 @@ export type UploadedFileRef = { artifact_id: string; availability: UploadedFileA
|
||||
|
||||
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerFileDeleteResponse = { deleted: boolean; };
|
||||
|
||||
export type WorkerFileUploadResponse = { file: UploadedFileRef; };
|
||||
@@ -163,7 +161,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS
|
||||
|
||||
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
|
||||
|
||||
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
|
||||
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:49856606e4a16d138cc04a62a6096fd40fff213587b1cbf7f0594f28994a45b0)
|
||||
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
@@ -45,8 +45,6 @@ export type UploadedFileRef = { artifact_id: string; availability: UploadedFileA
|
||||
|
||||
export type WorkerBusyState = { kind: "run"; state: WorkerRunState; } | { kind: "maintenance"; state: WorkerMaintenanceState; };
|
||||
|
||||
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||
|
||||
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||
|
||||
export type WorkerLaunchOptionsResponse = { default_profile?: string | null; diagnostics: Array<Diagnostic>; profiles: Array<WorkerLaunchProfileCandidate>; repositories: Array<WorkingDirectoryRepositoryOption>; runtimes: Array<WorkerLaunchRuntimeOption>; working_directories: Array<WorkingDirectorySummary>; workspace_id: string; };
|
||||
@@ -55,7 +53,7 @@ export type WorkerLaunchProfileCandidate = { description: string; id: string; la
|
||||
|
||||
export type WorkerLaunchRuntimeOption = { built_in: boolean; diagnostics: Array<Diagnostic>; display_name: string; runtime_id: string; status: string; worker_creation_available: boolean; working_directory_required: boolean; };
|
||||
|
||||
export type WorkerLaunchWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned: boolean; profile?: string | null; retention_state: string; runtime_id: string; singleton_key?: string | null; state: string; tags: Array<string>; workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>; worker_id: string; workspace: WorkerWorkspaceSummary; };
|
||||
export type WorkerLaunchWorkerSummary = { diagnostics?: Array<Diagnostic>; display_name: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned: boolean; profile?: string | null; retention_state: string; runtime_id: string; singleton_key?: string | null; state: string; tags: Array<string>; workdir_attachments?: Array<RuntimeWorkerWorkdirAttachmentSummary>; worker_id: string; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type WorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
|
||||
|
||||
@@ -67,7 +65,7 @@ export type WorkerState = { kind: "idle"; } | { kind: "busy"; state: WorkerBusyS
|
||||
|
||||
export type WorkerStateSnapshot = { last_command_id: number; state: WorkerState; };
|
||||
|
||||
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
export type WorkerSummary = { availability: SubscriptionWorkerAvailability; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: WorkerStateSnapshot | null; workspace: WorkerWorkspaceSummary; };
|
||||
|
||||
export type WorkerWorkdirAttachmentSummary = { alias: string; effective_permissions: ExternalWorkdirPermissions; working_directory: WorkingDirectorySummary; };
|
||||
|
||||
|
||||
@@ -56,13 +56,28 @@ const worker = {
|
||||
kind: "runtime",
|
||||
display_hint: "Runtime Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_stop: true,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
Deno.test("Worker summaries omit the removed management capabilities field", () => {
|
||||
const summary = { ...worker, resource_key: "W-1", availability: "observed" };
|
||||
assertEquals("capabilities" in parseWorkerSummary(summary), false);
|
||||
assertThrows(
|
||||
() => parseWorkerSummary({ ...summary, capabilities: {} }),
|
||||
Error,
|
||||
"unknown field capabilities",
|
||||
);
|
||||
const created = parseBrowserCreateWorkerResponse({
|
||||
workspace_id: "workspace-a",
|
||||
runtime_id: "runtime-a",
|
||||
worker_id: "worker-a",
|
||||
console_href: "/w/workspace-a/workers/W-1/console",
|
||||
worker,
|
||||
diagnostics: [],
|
||||
});
|
||||
assertEquals("capabilities" in created.worker, false);
|
||||
});
|
||||
|
||||
Deno.test("Worker summary parser enforces observation freshness", () => {
|
||||
const unavailable = {
|
||||
...worker,
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
RuntimeWorkerWorkdirAttachmentSummary,
|
||||
RuntimeWorkingDirectoryCleanupTarget,
|
||||
RuntimeWorkingDirectorySummary,
|
||||
WorkerCapabilitySummary,
|
||||
WorkerImplementationSummary,
|
||||
WorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate,
|
||||
@@ -251,21 +250,6 @@ function implementationSummary(
|
||||
};
|
||||
}
|
||||
|
||||
function capabilitySummary(
|
||||
value: unknown,
|
||||
label: string,
|
||||
): WorkerCapabilitySummary {
|
||||
const item = record(value, label);
|
||||
exact(item, ["can_stop", "can_spawn_followup"], label);
|
||||
return {
|
||||
can_stop: boolean(item.can_stop, `${label}.can_stop`),
|
||||
can_spawn_followup: boolean(
|
||||
item.can_spawn_followup,
|
||||
`${label}.can_spawn_followup`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function workerStateSnapshot(
|
||||
value: unknown,
|
||||
label: string,
|
||||
@@ -374,7 +358,6 @@ export function parseWorkerSummary(
|
||||
"pinned",
|
||||
"retention_state",
|
||||
"implementation",
|
||||
"capabilities",
|
||||
"workdir_attachments",
|
||||
"diagnostics",
|
||||
],
|
||||
@@ -423,7 +406,6 @@ export function parseWorkerSummary(
|
||||
item.implementation,
|
||||
`${label}.implementation`,
|
||||
),
|
||||
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
|
||||
workdir_attachments: item.workdir_attachments === undefined
|
||||
? undefined
|
||||
: array(
|
||||
@@ -617,7 +599,6 @@ function workerSummary(
|
||||
"pinned",
|
||||
"retention_state",
|
||||
"implementation",
|
||||
"capabilities",
|
||||
"workdir_attachments",
|
||||
"diagnostics",
|
||||
],
|
||||
@@ -641,7 +622,6 @@ function workerSummary(
|
||||
item.implementation,
|
||||
`${label}.implementation`,
|
||||
),
|
||||
capabilities: capabilitySummary(item.capabilities, `${label}.capabilities`),
|
||||
workdir_attachments: item.workdir_attachments === undefined
|
||||
? undefined
|
||||
: array(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { workerConsoleHref } from '$lib/workspace/resource-links';
|
||||
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
||||
import {
|
||||
canStopSidebarWorker,
|
||||
canDeleteSidebarWorker,
|
||||
deleteSidebarWorker,
|
||||
stopSidebarWorker,
|
||||
@@ -91,14 +92,14 @@
|
||||
}
|
||||
|
||||
async function stopWorker(worker: SidebarWorker) {
|
||||
if (busyAction || !worker.capabilities.can_stop) return;
|
||||
if (busyAction || !canStopSidebarWorker(worker)) return;
|
||||
closeWorkerMenu();
|
||||
busyAction = { workerKey: workerKey(worker), kind: 'stop' };
|
||||
try {
|
||||
await stopSidebarWorker(workspaceId, worker);
|
||||
workers = workers.map((item) =>
|
||||
workerKey(item) === workerKey(worker)
|
||||
? { ...item, state: 'stopped', capabilities: { ...item.capabilities, can_stop: false } }
|
||||
? { ...item, state: 'stopped', lifecycleState: 'stopped' }
|
||||
: item
|
||||
);
|
||||
pushWorkspaceAlert('info', `${worker.display_name || worker.label} stopped`, {
|
||||
@@ -226,7 +227,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
disabled={busyAction !== null || !worker.capabilities.can_stop}
|
||||
disabled={busyAction !== null || !canStopSidebarWorker(worker)}
|
||||
onclick={() => stopWorker(worker)}
|
||||
>
|
||||
{isBusy(worker, 'stop') ? 'Stopping…' : 'Stop'}
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
BrowserWorkerWorkingDirectorySelection
|
||||
as SharedBrowserWorkerWorkingDirectorySelection,
|
||||
Diagnostic as SharedDiagnostic,
|
||||
WorkerCapabilitySummary as SharedWorkerCapabilitySummary,
|
||||
WorkerLaunchOptionsResponse as SharedWorkerLaunchOptionsResponse,
|
||||
WorkerLaunchProfileCandidate as SharedWorkerLaunchProfileCandidate,
|
||||
WorkerLaunchRuntimeOption as SharedWorkerLaunchRuntimeOption,
|
||||
@@ -78,8 +77,6 @@ export type Runtime = {
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerCapabilities = SharedWorkerCapabilitySummary;
|
||||
|
||||
export type WorkerWorkdirAttachment = SharedWorkerWorkdirAttachmentSummary;
|
||||
|
||||
export type Worker =
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
parseRuntimeCleanupPlan,
|
||||
parseRuntimeWorkerLifecycleResult,
|
||||
} from "$lib/workspace/api/runtime-workers";
|
||||
import type {
|
||||
Diagnostic,
|
||||
Worker,
|
||||
} from "./types";
|
||||
import type { Diagnostic, Worker } from "./types";
|
||||
|
||||
type FetchFn = typeof fetch;
|
||||
type WorkerActionTarget = Pick<Worker, "runtime_id" | "worker_id" | "state">;
|
||||
@@ -25,13 +22,17 @@ async function responseError(response: Response): Promise<string> {
|
||||
const fallback = `${response.status} ${response.statusText}`.trim();
|
||||
try {
|
||||
const payload: unknown = await response.json();
|
||||
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
||||
if (
|
||||
typeof payload !== "object" || payload === null || Array.isArray(payload)
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
const record = payload as Record<string, unknown>;
|
||||
if (typeof record.message === "string") return record.message.slice(0, 512);
|
||||
const nested = record.error;
|
||||
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
|
||||
if (
|
||||
typeof nested === "object" && nested !== null && !Array.isArray(nested)
|
||||
) {
|
||||
const message = (nested as Record<string, unknown>).message;
|
||||
if (typeof message === "string") return message.slice(0, 512);
|
||||
}
|
||||
@@ -51,6 +52,14 @@ function diagnosticMessage(
|
||||
fallback;
|
||||
}
|
||||
|
||||
export function canStopSidebarWorker(worker: {
|
||||
availability: Worker["availability"];
|
||||
lifecycleState: string;
|
||||
}): boolean {
|
||||
return worker.availability === "observed" &&
|
||||
["idle", "running", "paused"].includes(worker.lifecycleState);
|
||||
}
|
||||
|
||||
export function canDeleteSidebarWorker(worker: WorkerActionTarget): boolean {
|
||||
return worker.state === "stopped";
|
||||
}
|
||||
@@ -112,7 +121,9 @@ export async function deleteSidebarWorker(
|
||||
throw new Error(await responseError(executionResponse));
|
||||
}
|
||||
|
||||
const execution = parseRuntimeCleanupExecution(await executionResponse.json());
|
||||
const execution = parseRuntimeCleanupExecution(
|
||||
await executionResponse.json(),
|
||||
);
|
||||
const outcome = execution.results.find((result) =>
|
||||
result.target_id === candidate.target_id
|
||||
);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import {
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/svelte";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import type {
|
||||
SubscriptionFrame,
|
||||
SubscriptionWorker,
|
||||
} from "$lib/generated/protocol";
|
||||
import WorkersNavSection from "./WorkersNavSection.svelte";
|
||||
import { disposeWorkspaceWorkersStore } from "./worker-subscription";
|
||||
|
||||
const transport = vi.hoisted(() => ({
|
||||
listener: null as { onFrame(frame: SubscriptionFrame): void } | null,
|
||||
close: vi.fn(),
|
||||
}));
|
||||
vi.mock("$lib/workspace/multiplexer", () => ({
|
||||
workspaceMultiplexer: () => ({
|
||||
subscribe: (
|
||||
_selector: unknown,
|
||||
listener: NonNullable<typeof transport.listener>,
|
||||
) => {
|
||||
transport.listener = listener;
|
||||
return { close: transport.close };
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
disposeWorkspaceWorkersStore("stop-test");
|
||||
transport.listener = null;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function snapshot(
|
||||
state: SubscriptionWorker["state"],
|
||||
availability: SubscriptionWorker["availability"],
|
||||
): SubscriptionFrame {
|
||||
return {
|
||||
protocol_version: 1,
|
||||
frame: "response",
|
||||
message: {
|
||||
result: "subscribed",
|
||||
payload: {
|
||||
request_id: "request-1",
|
||||
subscription_id: "subscription-1",
|
||||
selector: { topic: "workspace_workers" },
|
||||
snapshot_revision: 1,
|
||||
snapshot: {
|
||||
topic: "workers",
|
||||
data: {
|
||||
workers: [{
|
||||
worker_id: "worker-a",
|
||||
runtime_id: "runtime-a",
|
||||
resource_key: "W-1",
|
||||
availability,
|
||||
subject_revision: 1,
|
||||
state,
|
||||
// An absent foreground snapshot must not prevent stopping an active catalog Worker.
|
||||
worker_state: null,
|
||||
has_running_internal_workers: false,
|
||||
display_name: "Test worker",
|
||||
workspace_id: "stop-test",
|
||||
workdir_attachments: [],
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test.each(["idle", "running", "paused", "stopped"] as const)(
|
||||
"Stop uses observed catalog state without capabilities (%s)",
|
||||
async (state) => {
|
||||
const request = vi.fn(async (input: RequestInfo | URL) =>
|
||||
String(input).endsWith("/stop")
|
||||
? Response.json({
|
||||
state: "accepted",
|
||||
runtime_id: "runtime-a",
|
||||
worker_id: "worker-a",
|
||||
diagnostics: [],
|
||||
})
|
||||
: new Response(null, { status: 404 })
|
||||
);
|
||||
vi.stubGlobal("fetch", request);
|
||||
render(WorkersNavSection, { workspaceId: "stop-test" });
|
||||
await waitFor(() => expect(transport.listener).not.toBeNull());
|
||||
transport.listener!.onFrame(snapshot(state, "observed"));
|
||||
await fireEvent.click(
|
||||
await screen.findByRole("button", { name: "Actions for Test worker" }),
|
||||
);
|
||||
const stop = screen.getByRole("menuitem", {
|
||||
name: "Stop",
|
||||
}) as HTMLButtonElement;
|
||||
expect(stop.disabled).toBe(state === "stopped");
|
||||
if (state === "stopped") return;
|
||||
await fireEvent.click(stop);
|
||||
await waitFor(() =>
|
||||
expect(request.mock.calls.some(([url]) => String(url).endsWith("/stop")))
|
||||
.toBe(true)
|
||||
);
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", { name: "Actions for Test worker" }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByRole("menuitem", { name: "Stop" }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("Unavailable observation disables Stop even when catalog state is running", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response(null, { status: 404 })),
|
||||
);
|
||||
render(WorkersNavSection, { workspaceId: "stop-test" });
|
||||
await waitFor(() => expect(transport.listener).not.toBeNull());
|
||||
transport.listener!.onFrame(snapshot("running", "unavailable"));
|
||||
await fireEvent.click(
|
||||
await screen.findByRole("button", { name: "Actions for Test worker" }),
|
||||
);
|
||||
expect(
|
||||
(screen.getByRole("menuitem", { name: "Stop" }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -16,6 +16,8 @@ import type { Worker } from "./types";
|
||||
export type SidebarWorker = Omit<Worker, "workdir_attachments"> & {
|
||||
workdir_attachments: SidebarWorkdirAttachment[];
|
||||
has_running_internal_workers: boolean;
|
||||
/** Catalog lifecycle, independent of the possibly unknown foreground display state. */
|
||||
lifecycleState: SubscriptionWorker["state"];
|
||||
};
|
||||
|
||||
export type WorkspaceWorkersState = {
|
||||
@@ -148,6 +150,7 @@ function projectWorker(
|
||||
},
|
||||
availability: worker.availability,
|
||||
state: liveWorkerState(worker),
|
||||
lifecycleState: worker.state,
|
||||
worker_state: worker.worker_state,
|
||||
pinned: false,
|
||||
retention_state: "transient",
|
||||
@@ -155,11 +158,6 @@ function projectWorker(
|
||||
kind: "runtime_subscription_worker",
|
||||
display_hint: "Workspace-authorized Runtime Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_stop: worker.availability !== "unavailable" &&
|
||||
worker.state !== "stopped",
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
workdir_attachments: (worker.workdir_attachments ?? []).map((
|
||||
attachment,
|
||||
) => ({
|
||||
|
||||
@@ -38,10 +38,6 @@ function worker(overrides: Partial<Worker>): Worker {
|
||||
kind: "runtime_worker",
|
||||
display_hint: "Runtime Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_stop: true,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
workdir_attachments: [],
|
||||
diagnostics: [],
|
||||
...overrides,
|
||||
@@ -56,10 +52,6 @@ Deno.test("registry-only workers are not sidebar targets or console targets", ()
|
||||
kind: "backend_worker_registry",
|
||||
display_hint: "Missing Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_stop: false,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
});
|
||||
assertEquals(canShowWorkerInSidebar(registryOnly), false);
|
||||
assertEquals(canOpenWorkerConsole(registryOnly), false);
|
||||
|
||||
@@ -1935,22 +1935,6 @@
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<details class="metadata-details">
|
||||
<summary>Capabilities</summary>
|
||||
<ul>
|
||||
<li>
|
||||
stop: {worker.capabilities.can_stop
|
||||
? "available"
|
||||
: "unsupported"}
|
||||
</li>
|
||||
<li>
|
||||
follow-up spawn: {worker.capabilities
|
||||
.can_spawn_followup
|
||||
? "available"
|
||||
: "unsupported"}
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
{:else if !workerError}
|
||||
<p>Loading Worker detail…</p>
|
||||
{/if}
|
||||
|
||||
-4
@@ -62,10 +62,6 @@ function worker(workerId = "worker-a", runtimeId = "runtime-a"): Worker {
|
||||
profile: "default",
|
||||
tags: [],
|
||||
diagnostics: [],
|
||||
capabilities: {
|
||||
can_stop: true,
|
||||
can_spawn_followup: true,
|
||||
},
|
||||
workspace: { visibility: "workspace", identity: "workspace-a" },
|
||||
implementation: { kind: "builtin", display_hint: "test" },
|
||||
workdir_attachments: [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
canDeleteSidebarWorker,
|
||||
canStopSidebarWorker,
|
||||
deleteSidebarWorker,
|
||||
stopSidebarWorker,
|
||||
} from "../../src/lib/workspace/sidebar/worker-actions.ts";
|
||||
@@ -41,7 +42,6 @@ const worker = {
|
||||
runtime_id: "runtime /",
|
||||
worker_id: "worker /",
|
||||
state: "running",
|
||||
capabilities: { can_stop: true },
|
||||
} as Worker;
|
||||
|
||||
function jsonResponse(payload: unknown, status = 200): Response {
|
||||
@@ -51,6 +51,33 @@ function jsonResponse(payload: unknown, status = 200): Response {
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user