fix: route worker consoles through logical resources
This commit is contained in:
@@ -13439,6 +13439,14 @@ fn browser_worker_spawn_policy(
|
||||
}
|
||||
}
|
||||
|
||||
fn browser_worker_console_href(workspace_id: &str, resource_key: &str) -> String {
|
||||
format!(
|
||||
"/w/{}/workers/{}/console",
|
||||
encode_path_segment(workspace_id),
|
||||
encode_path_segment(resource_key)
|
||||
)
|
||||
}
|
||||
|
||||
async fn create_workspace_worker(
|
||||
State(api): State<WorkspaceApi>,
|
||||
headers: HeaderMap,
|
||||
@@ -13750,18 +13758,22 @@ fn browser_worker_response_from_summary(
|
||||
let runtime_id = worker.worker.runtime_id.clone();
|
||||
let worker_id = worker.worker.worker_id.clone();
|
||||
let workspace_id = api.workspace_id().to_string();
|
||||
let console_href = format!(
|
||||
"/w/{}/runtimes/{}/workers/{}/console",
|
||||
encode_path_segment(&workspace_id),
|
||||
encode_path_segment(&runtime_id),
|
||||
encode_path_segment(&worker_id)
|
||||
);
|
||||
let resource_key = api
|
||||
.store
|
||||
.resource_key(&workspace_id, WorkspaceResourceKind::Worker, &worker_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::Store(format!(
|
||||
"Workspace Worker `{worker_id}` has no resource key after registration"
|
||||
))
|
||||
})?;
|
||||
let worker = worker_launch_worker_summary(worker);
|
||||
let console_href = browser_worker_console_href(&workspace_id, &resource_key);
|
||||
Ok(BrowserCreateWorkerResponse {
|
||||
workspace_id,
|
||||
runtime_id,
|
||||
worker_id,
|
||||
console_href,
|
||||
worker: worker_launch_worker_summary(worker),
|
||||
worker,
|
||||
diagnostics: diagnostics
|
||||
.into_iter()
|
||||
.map(workspace_api::Diagnostic::from)
|
||||
@@ -17193,6 +17205,13 @@ mod tests {
|
||||
SqliteWorkspaceStore, UserRecord, WorkspaceRecord, WorkspaceRuntimeBinding,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn browser_worker_console_href_uses_logical_worker_route() {
|
||||
let href = browser_worker_console_href("workspace/one", "W-7");
|
||||
assert_eq!(href, "/w/workspace%2Fone/workers/W-7/console");
|
||||
assert!(!href.contains("/runtimes/"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn workspace_mutation_gate_serializes_deletion_with_active_mutations() {
|
||||
let locks = Arc::new(AsyncMutex::new(HashMap::new()));
|
||||
|
||||
@@ -17,7 +17,7 @@ context ではない。`crypto.randomUUID()` は secure context 限定なので
|
||||
Worker Console の `connectProtocolTransport` は、最初に `protocolState` を
|
||||
`"connecting"` に設定してから `WorkspaceMultiplexer.subscribe()` を呼ぶ。
|
||||
|
||||
- `web/workspace/src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte`
|
||||
- `web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte`
|
||||
- `protocolState = "connecting"`
|
||||
- 直後に `workspaceMultiplexer(...).subscribe(...)`
|
||||
- `web/workspace/src/lib/workspace/multiplexer.ts`
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
resolveConsoleWorkerView,
|
||||
segmentsToText,
|
||||
selectConsoleTimelineLines,
|
||||
workerConsoleHref,
|
||||
} from "./model.ts";
|
||||
import { workerConsoleHref } from "../resource-links.ts";
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => void): void;
|
||||
@@ -461,14 +461,13 @@ Deno.test("reload snapshot projects provenance-annotated history entries", () =>
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("workerConsoleHref encodes runtime and worker target authority", () => {
|
||||
Deno.test("workerConsoleHref uses logical Worker resource authority", () => {
|
||||
assert(
|
||||
workerConsoleHref({
|
||||
runtime_id: "local runtime",
|
||||
worker_id: "worker/one",
|
||||
}, "workspace-1") ===
|
||||
"/w/workspace-1/runtimes/local%20runtime/workers/worker%2Fone/console",
|
||||
"href should contain encoded runtime_id and worker_id segments",
|
||||
workerConsoleHref("workspace-1", {
|
||||
resource_key: "W-123",
|
||||
display_name: "Worker One",
|
||||
}) === "/w/workspace-1/workers/W-123-worker-one/console",
|
||||
"href should contain the canonical logical Worker reference",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
WorkerStatus,
|
||||
} from "$lib/generated/protocol";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
import { workspaceRoute } from "$lib/workspace/api/http";
|
||||
import {
|
||||
applyRunActivityEvent,
|
||||
emptyRunActivityStats,
|
||||
@@ -220,31 +219,6 @@ export type WorkerTarget = {
|
||||
worker_id: string;
|
||||
};
|
||||
|
||||
export function workerConsoleHref(
|
||||
target: WorkerTarget,
|
||||
workspaceId: string,
|
||||
): string {
|
||||
return workspaceRoute(
|
||||
workspaceId,
|
||||
`/runtimes/${encodeURIComponent(target.runtime_id)}/workers/${
|
||||
encodeURIComponent(
|
||||
target.worker_id,
|
||||
)
|
||||
}/console`,
|
||||
);
|
||||
}
|
||||
|
||||
export function workerConsolePath(
|
||||
workspaceId: string,
|
||||
runtimeId: string,
|
||||
workerId: string,
|
||||
): string {
|
||||
return workerConsoleHref(
|
||||
{ runtime_id: runtimeId, worker_id: workerId },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export type ConsoleEventInput = {
|
||||
eventId: string;
|
||||
event: ProtocolEvent;
|
||||
|
||||
@@ -371,7 +371,7 @@ Deno.test("root layout keeps Workspace selection explicit", 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",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -395,7 +395,7 @@ Deno.test("Worker Console uses protocol observation events without transcript fe
|
||||
Deno.test("Worker Console owns its narrower centered shell width", async () => {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -494,7 +494,7 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
|
||||
Deno.test("Worker Console exposes a foldable timeline beside the scroll body", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -529,7 +529,7 @@ Deno.test("Worker Console exposes a foldable timeline beside the scroll body", a
|
||||
Deno.test("Worker Console removes redundant chrome and uses shared alerts", async () => {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -558,7 +558,7 @@ Deno.test("Worker Console removes redundant chrome and uses shared alerts", asyn
|
||||
Deno.test("Worker Console composer keeps a compact bounded chip editor", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -592,7 +592,7 @@ Deno.test("Worker Console composer keeps a compact bounded chip editor", async (
|
||||
Deno.test("Worker Console paste chips preserve typed draft and target authority", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -724,24 +724,28 @@ Deno.test("workspace Worker sidebar links New to the dedicated create page", asy
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Worker Console page is routed by runtime_id and worker_id through backend APIs", async () => {
|
||||
Deno.test("Worker Console route resolves logical Worker authority before Runtime APIs", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const routeLoad = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.ts",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.ts",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
assert(
|
||||
routeLoad.includes("workspaceId") &&
|
||||
routeLoad.includes("runtimeId") && routeLoad.includes("workerId"),
|
||||
"route load should expose workspace and target ids",
|
||||
routeLoad.includes("params.workerRef") &&
|
||||
routeLoad.includes("resourceKey(params.workerRef)") &&
|
||||
routeLoad.includes("/workers/${encodeURIComponent(reference)}") &&
|
||||
routeLoad.includes("canonicalResourceReference") &&
|
||||
routeLoad.includes("runtimeId: result.data?.runtime_id") &&
|
||||
routeLoad.includes("workerId: result.data?.worker_id"),
|
||||
"route load should resolve the logical Worker reference before exposing its execution target",
|
||||
);
|
||||
assert(
|
||||
consolePage.includes("workspaceApiPath(workspaceId, path)") &&
|
||||
@@ -951,7 +955,7 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
|
||||
Deno.test("Workspace Worker list and Console share the multiplexed connection", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -988,7 +992,7 @@ Deno.test("Workspace Worker list and Console share the multiplexed connection",
|
||||
Deno.test("Web Console renders the client-projected Worker task store", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -1027,7 +1031,7 @@ Deno.test("Web Console renders the client-projected Worker task store", async ()
|
||||
Deno.test("Web Console switches main and direct SubWorker views from the Tasks row", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
@@ -1071,7 +1075,7 @@ Deno.test("Web Console switches main and direct SubWorker views from the Tasks r
|
||||
Deno.test("Web Console uses Notify while running and exposes durable pending controls", async () => {
|
||||
const consolePage = await Deno.readTextFile(
|
||||
new URL(
|
||||
"./../../../routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ Deno.test("Ticket detail breadcrumbs expose the Ticket list and current id", ()
|
||||
Deno.test("Worker console breadcrumbs use the logical Workers route and display name", () => {
|
||||
assertEquals(
|
||||
buildWorkspaceBreadcrumbs(
|
||||
"/w/workspace/runtimes/runtime-a/workers/worker-7/console",
|
||||
"/w/workspace/workers/W-7-review-worker/console",
|
||||
"workspace",
|
||||
{ workerName: "Review Worker" },
|
||||
),
|
||||
@@ -39,12 +39,12 @@ Deno.test("Worker console breadcrumbs use the logical Workers route and display
|
||||
Deno.test("Worker console breadcrumbs fall back to Worker id", () => {
|
||||
assertEquals(
|
||||
buildWorkspaceBreadcrumbs(
|
||||
"/w/workspace/runtimes/runtime-a/workers/worker-7/console",
|
||||
"/w/workspace/workers/W-7-review-worker/console",
|
||||
"workspace",
|
||||
),
|
||||
[
|
||||
{ label: "workers", href: "/w/workspace/workers" },
|
||||
{ label: "worker-7" },
|
||||
{ label: "W-7-review-worker" },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,13 +24,13 @@ export function buildWorkspaceBreadcrumbs(
|
||||
.map(decodeURIComponent);
|
||||
|
||||
if (
|
||||
segments[0] === "runtimes" &&
|
||||
segments[2] === "workers" &&
|
||||
segments[3]
|
||||
segments[0] === "workers" &&
|
||||
segments[1] &&
|
||||
segments[2] === "console"
|
||||
) {
|
||||
return [
|
||||
{ label: "workers", href: `${workspaceRoot}/workers` },
|
||||
{ label: context.workerName?.trim() || segments[3] },
|
||||
{ label: context.workerName?.trim() || segments[1] },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -45,3 +45,10 @@ export function workerHref(
|
||||
const reference = canonicalResourceReference(worker.resource_key, worker.display_name);
|
||||
return `/w/${encodeURIComponent(workspaceId)}/workers/${encodeURIComponent(reference)}`;
|
||||
}
|
||||
|
||||
export function workerConsoleHref(
|
||||
workspaceId: string,
|
||||
worker: { resource_key: string; display_name: string },
|
||||
): string {
|
||||
return `${workerHref(workspaceId, worker)}/console`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Spinner from '$lib/workspace/console/Spinner.svelte';
|
||||
import { workerConsoleHref } from '$lib/workspace/console/model';
|
||||
import { workerConsoleHref } from '$lib/workspace/resource-links';
|
||||
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
||||
import {
|
||||
canDeleteSidebarWorker,
|
||||
@@ -168,7 +168,7 @@
|
||||
{#if error}<p class="section-state error">{error}</p>{/if}
|
||||
<ul class="nav-list" aria-label="Workers">
|
||||
{#each visibleWorkers as worker (`${worker.runtime_id}:${worker.worker_id}`)}
|
||||
{@const href = workerConsoleHref(worker, workspaceId)}
|
||||
{@const href = workerConsoleHref(workspaceId, worker)}
|
||||
{@const activity = sidebarWorkerActivity(worker)}
|
||||
{@const key = workerKey(worker)}
|
||||
{@const label = worker.display_name || worker.label}
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, params }) => {
|
||||
const worker = await loadJson<Worker>(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/runtimes/${encodeURIComponent(params.runtimeId)}/workers/${
|
||||
encodeURIComponent(params.workerId)
|
||||
}`,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
runtimeId: params.runtimeId,
|
||||
workerId: params.workerId,
|
||||
worker: worker.data,
|
||||
workerError: worker.error,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||
import { workerConsoleHref } from '$lib/workspace/resource-links';
|
||||
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
|
||||
import type { PageData } from './$types';
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -18,10 +18,7 @@
|
||||
</div>
|
||||
<a
|
||||
class="button-primary"
|
||||
href={workspaceRoute(
|
||||
data.workspaceId,
|
||||
`/runtimes/${data.worker.runtime_id}/workers/${data.worker.worker_id}/console`,
|
||||
)}
|
||||
href={workerConsoleHref(data.workspaceId, data.worker)}
|
||||
>Open console</a>
|
||||
</header>
|
||||
<dl class="resource-meta">
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||
import {
|
||||
canonicalResourceReference,
|
||||
resourceKey,
|
||||
} from "$lib/workspace/resource-links";
|
||||
import type { Worker } from "$lib/workspace/sidebar/types";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ fetch, params }) => {
|
||||
const reference = resourceKey(params.workerRef);
|
||||
const result = await loadJson<Worker>(
|
||||
fetch,
|
||||
workspaceApiPath(
|
||||
params.workspaceId,
|
||||
`/workers/${encodeURIComponent(reference)}`,
|
||||
),
|
||||
);
|
||||
if (result.data?.resource_key) {
|
||||
const canonical = canonicalResourceReference(
|
||||
result.data.resource_key,
|
||||
result.data.display_name,
|
||||
);
|
||||
if (params.workerRef !== canonical) {
|
||||
redirect(
|
||||
308,
|
||||
`/w/${encodeURIComponent(params.workspaceId)}/workers/${encodeURIComponent(canonical)}/console`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: params.workspaceId,
|
||||
runtimeId: result.data?.runtime_id ?? "",
|
||||
workerId: result.data?.worker_id ?? "",
|
||||
worker: result.data,
|
||||
workerError: result.error,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -186,7 +186,7 @@ Deno.test("Composer input uses boundary-aware Up and Down history navigation", a
|
||||
);
|
||||
const consoleSource = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"../src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -87,7 +87,7 @@ Deno.test("sidebar running status reuses the green symbol spinner", async () =>
|
||||
Deno.test("running status is Composer-side above mini Tasks", async () => {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"../src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||
Deno.test("Worker Console exposes Overview and Normal display modes", async () => {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../src/routes/w/[workspaceId]/runtimes/[runtimeId]/workers/[workerId]/console/+page.svelte",
|
||||
"../src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user