workspace: disable archived worker console links
This commit is contained in:
@@ -436,6 +436,15 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.worker-nav-item.disabled {
|
||||
cursor: default;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.worker-nav-item.disabled .item-title {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.worker-title-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
|
||||
|
||||
@@ -14,7 +14,10 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||
new URL("./../../routes/w/[workspaceId]/+page.svelte", import.meta.url),
|
||||
);
|
||||
const workersPage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/workers/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../routes/w/[workspaceId]/workers/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const workersNav = await Deno.readTextFile(
|
||||
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url),
|
||||
@@ -24,8 +27,8 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||
);
|
||||
|
||||
assert(
|
||||
workspacePage.includes('href={`/w/${workspaceId}/runtimes`}') &&
|
||||
workspacePage.includes('href={`/w/${workspaceId}/workers`}'),
|
||||
workspacePage.includes("href={`/w/${workspaceId}/runtimes`}") &&
|
||||
workspacePage.includes("href={`/w/${workspaceId}/workers`}"),
|
||||
"top workspace page should link to Runtimes and Workers pages",
|
||||
);
|
||||
assert(
|
||||
@@ -35,15 +38,15 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||
);
|
||||
assert(
|
||||
workersPage.includes("workerConsoleHref(worker, data.workspaceId)") &&
|
||||
workersPage.includes("<table class=\"workers-table\">") &&
|
||||
workersPage.includes('<table class="workers-table">') &&
|
||||
workersPage.includes("Open Console"),
|
||||
"dedicated Workers page should expose a table and attach action per Worker",
|
||||
);
|
||||
assert(
|
||||
workersNav.includes('href={`/w/${workspaceId}/workers`}') &&
|
||||
workersNav.includes("workerConsoleHref(worker, workspaceId)") &&
|
||||
workersNav.includes("aria-current"),
|
||||
"Workers sidebar should link to the Worker list page and target Console routes",
|
||||
workersNav.includes("href={`/w/${workspaceId}/workers`}") &&
|
||||
workersNav.includes("canOpenWorkerConsole(worker)") &&
|
||||
workersNav.includes('aria-disabled="true"'),
|
||||
"Workers sidebar should link to the Worker list page and render archived Workers as disabled rows",
|
||||
);
|
||||
assert(
|
||||
!sidebar.includes("CompanionNavSection") &&
|
||||
@@ -54,12 +57,17 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||
|
||||
Deno.test("Worker Console uses protocol observation events without transcript fetch", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
consolePage.includes("seenObservationEventIds") &&
|
||||
consolePage.includes("rememberObservationEvent(frame.envelope.event_id)") &&
|
||||
consolePage.includes(
|
||||
"rememberObservationEvent(frame.envelope.event_id)",
|
||||
) &&
|
||||
consolePage.includes("projectConsole(observedEvents.map") &&
|
||||
!consolePage.includes("/transcript") &&
|
||||
!consolePage.includes("WorkerTranscriptProjection"),
|
||||
@@ -69,7 +77,10 @@ Deno.test("Worker Console uses protocol observation events without transcript fe
|
||||
|
||||
Deno.test("Decodal source editor keeps imperative EditorView out of reactive state", async () => {
|
||||
const editor = await Deno.readTextFile(
|
||||
new URL("../workspace-settings/DecodalSourceEditor.svelte", import.meta.url),
|
||||
new URL(
|
||||
"../workspace-settings/DecodalSourceEditor.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
@@ -89,18 +100,27 @@ Deno.test("workspace Runtime management pages expose Runtimes and Runtime-owned
|
||||
new URL("../workspace-sidebar/RuntimesNavSection.svelte", import.meta.url),
|
||||
);
|
||||
const runtimesPage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/runtimes/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../routes/w/[workspaceId]/runtimes/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const workdirsPage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const workdirsLoad = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts", import.meta.url),
|
||||
new URL(
|
||||
"./../../routes/w/[workspaceId]/runtimes/[runtimeId]/workdirs/+page.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
sidebar.includes("RuntimesNavSection") &&
|
||||
runtimesNav.includes('href={runtimesHref}') &&
|
||||
runtimesNav.includes("href={runtimesHref}") &&
|
||||
runtimesNav.includes("/runtimes"),
|
||||
"sidebar should expose Runtime management navigation",
|
||||
);
|
||||
@@ -123,18 +143,21 @@ Deno.test("workspace Worker sidebar links New to the dedicated create page", asy
|
||||
new URL("../workspace-sidebar/WorkersNavSection.svelte", import.meta.url),
|
||||
);
|
||||
const newWorkerPage = await Deno.readTextFile(
|
||||
new URL("./../../routes/w/[workspaceId]/workers/new/+page.svelte", import.meta.url),
|
||||
new URL(
|
||||
"./../../routes/w/[workspaceId]/workers/new/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
workersNav.includes('href={`/w/${workspaceId}/workers/new`}') &&
|
||||
!workersNav.includes('worker-launch-form') &&
|
||||
!workersNav.includes('createWorker()'),
|
||||
workersNav.includes("href={`/w/${workspaceId}/workers/new`}") &&
|
||||
!workersNav.includes("worker-launch-form") &&
|
||||
!workersNav.includes("createWorker()"),
|
||||
"Workers sidebar should link to the dedicated New Worker page instead of owning the form",
|
||||
);
|
||||
assert(
|
||||
newWorkerPage.includes('worker-launch-form') &&
|
||||
newWorkerPage.includes('buildBrowserCreateWorkerRequest') &&
|
||||
newWorkerPage.includes("worker-launch-form") &&
|
||||
newWorkerPage.includes("buildBrowserCreateWorkerRequest") &&
|
||||
newWorkerPage.includes("/workers/launch-options"),
|
||||
"New Worker page should own launch options and creation form behavior",
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { workspaceApiPath } from '$lib/workspace-api/http';
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import { canOpenWorkerConsole } from './workers';
|
||||
import type { ListResponse, Worker } from './types';
|
||||
|
||||
const MAX_VISIBLE_WORKERS = 6;
|
||||
@@ -99,17 +100,31 @@
|
||||
<ul class="nav-list" aria-label="Workers">
|
||||
{#each workers as worker (`${worker.runtime_id}:${worker.worker_id}`)}
|
||||
{@const href = workerConsoleHref(worker, workspaceId)}
|
||||
{@const consoleAvailable = canOpenWorkerConsole(worker)}
|
||||
<li>
|
||||
<a href={href} class="nav-item worker-nav-item" class:active={currentPath === href} aria-current={currentPath === href ? 'page' : undefined}>
|
||||
<span class="worker-title-row">
|
||||
<span class="item-title">{worker.label}</span>
|
||||
<span class="worker-task-title">-</span>
|
||||
</span>
|
||||
<span class="item-meta">
|
||||
{worker.role ? `${worker.role} · ` : ''}{worker.state} · 🖥 {worker.host_id}
|
||||
{worker.working_directory ? ` · wd:${worker.working_directory.repository_id}@${worker.working_directory.resolved_commit.slice(0, 8)}` : ''}
|
||||
</span>
|
||||
</a>
|
||||
{#if consoleAvailable}
|
||||
<a href={href} class="nav-item worker-nav-item" class:active={currentPath === href} aria-current={currentPath === href ? 'page' : undefined}>
|
||||
<span class="worker-title-row">
|
||||
<span class="item-title">{worker.label}</span>
|
||||
<span class="worker-task-title">-</span>
|
||||
</span>
|
||||
<span class="item-meta">
|
||||
{worker.role ? `${worker.role} · ` : ''}{worker.state} · 🖥 {worker.host_id}
|
||||
{worker.working_directory ? ` · wd:${worker.working_directory.repository_id}@${worker.working_directory.resolved_commit.slice(0, 8)}` : ''}
|
||||
</span>
|
||||
</a>
|
||||
{:else}
|
||||
<div class="nav-item worker-nav-item disabled" aria-disabled="true">
|
||||
<span class="worker-title-row">
|
||||
<span class="item-title">{worker.label}</span>
|
||||
<span class="worker-task-title">archived</span>
|
||||
</span>
|
||||
<span class="item-meta">
|
||||
{worker.role ? `${worker.role} · ` : ''}{worker.state} · 🖥 {worker.host_id}
|
||||
{worker.working_directory ? ` · wd:${worker.working_directory.repository_id}@${worker.working_directory.resolved_commit.slice(0, 8)}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { canOpenWorkerConsole } from "./workers.ts";
|
||||
import type { Worker } from "./types.ts";
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
};
|
||||
|
||||
function assertEquals<T>(actual: T, expected: T): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`expected ${String(expected)}, got ${String(actual)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function worker(overrides: Partial<Worker>): Worker {
|
||||
return {
|
||||
runtime_id: "arc",
|
||||
worker_id: "worker-1",
|
||||
host_id: "host",
|
||||
label: "worker-1",
|
||||
role: null,
|
||||
profile: null,
|
||||
workspace: { visibility: "workspace", identity: "workspace" },
|
||||
state: "running",
|
||||
pinned: false,
|
||||
retention_state: "normal",
|
||||
last_seen_at: null,
|
||||
implementation: {
|
||||
kind: "runtime_worker",
|
||||
display_hint: "Runtime Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_accept_input: true,
|
||||
can_stop: true,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
working_directory: null,
|
||||
diagnostics: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("canOpenWorkerConsole rejects archived registry-only workers", () => {
|
||||
assertEquals(
|
||||
canOpenWorkerConsole(worker({
|
||||
state: "archived",
|
||||
implementation: {
|
||||
kind: "backend_worker_registry",
|
||||
display_hint: "Archived Worker",
|
||||
},
|
||||
capabilities: {
|
||||
can_accept_input: false,
|
||||
can_stop: false,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
})),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("canOpenWorkerConsole accepts live runtime workers", () => {
|
||||
assertEquals(canOpenWorkerConsole(worker({ state: "running" })), true);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Worker } from "./types";
|
||||
|
||||
export function canOpenWorkerConsole(worker: Worker): boolean {
|
||||
return worker.state !== "archived" &&
|
||||
worker.implementation.kind !== "backend_worker_registry";
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { workspaceApiPath } from '$lib/workspace-api/http';
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import { canOpenWorkerConsole } from '$lib/workspace-sidebar/workers';
|
||||
import type { Worker } from '$lib/workspace-sidebar/types';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
@@ -91,7 +92,11 @@
|
||||
<td><span class="pill {worker.pinned ? 'success' : 'muted'}">{worker.retention_state ?? 'normal'}</span></td>
|
||||
<td>{workerDirectory(worker)}</td>
|
||||
<td>
|
||||
<a class="inline-link" href={workerConsoleHref(worker, data.workspaceId)}>Open Console</a>
|
||||
{#if canOpenWorkerConsole(worker)}
|
||||
<a class="inline-link" href={workerConsoleHref(worker, data.workspaceId)}>Open Console</a>
|
||||
{:else}
|
||||
<span class="muted" aria-disabled="true">Archived</span>
|
||||
{/if}
|
||||
<button type="button" onclick={() => setPinned(worker, !worker.pinned)}>
|
||||
{worker.pinned ? 'Unpin' : 'Pin'}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user