fix: map internal worker terminal lifecycles

This commit is contained in:
2026-08-27 12:27:55 +09:00
parent 975b4fa700
commit 3a7a3307ef
9 changed files with 225 additions and 52 deletions
@@ -5,7 +5,7 @@
workspaceWorkersStore,
type SidebarWorker,
} from './worker-subscription';
import { canShowWorkerInSidebar } from './workers';
import { canShowWorkerInSidebar, sidebarWorkerActivity } from './workers';
const COLLAPSED_WORKER_COUNT = 6;
@@ -69,6 +69,7 @@
<ul class="nav-list" aria-label="Workers">
{#each visibleWorkers as worker (`${worker.runtime_id}:${worker.worker_id}`)}
{@const href = workerConsoleHref(worker, workspaceId)}
{@const activity = sidebarWorkerActivity(worker)}
<li>
<a
href={href}
@@ -77,11 +78,11 @@
aria-current={currentPath === href ? 'page' : undefined}
>
<span class="worker-status-indicator">
{#if worker.state === 'running'}
{#if activity === 'worker-running'}
<span class="worker-status-spinner"><Spinner label="Running" /></span>
{:else if worker.has_running_internal_workers}
{:else if activity === 'subworker-running'}
<span class="worker-status-spinner is-subworker"><Spinner label="SubWorker running" /></span>
{:else if worker.state === 'idle'}
{:else if activity === 'idle'}
<span class="worker-status-dot" aria-label="Idle"></span>
{/if}
</span>
@@ -14,13 +14,18 @@ declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
function worker(runtimeId: string, workerId: string, revision: number): SubscriptionWorker {
function worker(
runtimeId: string,
workerId: string,
revision: number,
hasRunningInternalWorkers = false,
): SubscriptionWorker {
return {
worker_id: workerId,
runtime_id: runtimeId,
subject_revision: revision,
state: 'idle',
has_running_internal_workers: false,
has_running_internal_workers: hasRunningInternalWorkers,
workspace_id: 'workspace-test',
display_name: null,
profile: null,
@@ -88,3 +93,28 @@ Deno.test('workspace Worker reducer ignores stale events and removes composite s
assertEquals(projection.workers.size, 0);
assertEquals(projection.revisions.get('runtime-a:1'), 4);
});
Deno.test('fatal child stop replaces the running-child sidebar projection', () => {
const projection = createWorkspaceWorkersProjection();
projection.workers.set('runtime-a:1', worker('runtime-a', '1', 1, true));
projection.revisions.set('runtime-a:1', 1);
applyWorkspaceWorkersFrame(projection, {
protocol_version: 1,
frame: 'event',
message: {
event: 'event',
data: {
subscription_id: 'subscription-1',
subject_revision: 2,
payload: {
event: 'worker_upserted',
data: { worker: worker('runtime-a', '1', 2, false) },
},
},
},
});
assertEquals(projection.workers.get('runtime-a:1')?.has_running_internal_workers, false);
assertEquals(projection.revisions.get('runtime-a:1'), 2);
});
@@ -2,6 +2,7 @@ import {
canOpenWorkerConsole,
canShowWorkerInSidebar,
compareWorkersForSidebar,
sidebarWorkerActivity,
} from "./workers.ts";
import type { Worker } from "./types.ts";
@@ -77,3 +78,21 @@ Deno.test("sidebar workers sort running then idle then stopped", () => {
workers.sort(compareWorkersForSidebar);
assertEquals(workers.map((candidate) => candidate.worker_id).join(","), "2,1,4,3");
});
Deno.test("fatal child stop clears the sidebar SubWorker spinner activity", () => {
const parent = { state: "idle", has_running_internal_workers: true };
assertEquals(sidebarWorkerActivity(parent), "subworker-running");
parent.has_running_internal_workers = false;
assertEquals(sidebarWorkerActivity(parent), "idle");
});
Deno.test("stopped parents do not fall back to the idle indicator", () => {
assertEquals(
sidebarWorkerActivity({
state: "stopped",
has_running_internal_workers: false,
}),
"none",
);
});
@@ -1,5 +1,24 @@
import type { Worker } from './types';
export type SidebarWorkerActivity =
| 'worker-running'
| 'subworker-running'
| 'idle'
| 'none';
type WorkerActivitySource = Pick<Worker, 'state'> & {
has_running_internal_workers: boolean;
};
export function sidebarWorkerActivity(
worker: WorkerActivitySource,
): SidebarWorkerActivity {
if (worker.state === 'running') return 'worker-running';
if (worker.has_running_internal_workers) return 'subworker-running';
if (worker.state === 'idle') return 'idle';
return 'none';
}
export function canShowWorkerInSidebar(worker: Worker): boolean {
return worker.implementation.kind !== 'backend_worker_registry';
}