chore: merge hare/develop into develop

This commit is contained in:
2026-08-26 19:44:31 +09:00
32 changed files with 700 additions and 1419 deletions
@@ -115,14 +115,12 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
"top workspace page should not own the Worker list",
);
assert(
workersPage.includes("workerConsoleHref(worker, data.workspaceId)") &&
workersPage.includes('<table class="workers-table">') &&
workersPage.includes(
"workerDisplayName = worker.display_name || worker.label",
) &&
workersPage.includes("worker <code>{worker.worker_id}</code>") &&
workersPage.includes("workerHref") &&
workersPage.includes("workers-table") &&
workersPage.includes("workerDisplayName") &&
workersPage.includes("worker.resource_key") &&
workersPage.includes("Delete ${workerDisplayName}"),
"dedicated Workers page should expose a table, console link target, and icon actions per Worker",
"dedicated Workers page should expose a table, canonical Worker link target, and icon actions per Worker",
);
assert(
workersNav.includes("href={`/w/${workspaceId}/workers`}") &&
@@ -218,14 +216,16 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
"Tickets and Objectives should each be a single sidebar link",
);
assert(
ticketsLoad.includes("?limit=1000") &&
ticketsLoad.includes("Object.entries(LANE_STATES)") &&
ticketsLoad.includes('limit: "30"') &&
ticketsLoad.includes('states: states.join(",")') &&
ticketsLoad.includes("/tickets?${search}") &&
!ticketsLoad.includes("/tickets/query") &&
ticketsPage.includes('class="ticket-kanban"') &&
ticketsPage.includes('class="ticket-lane-cards"') &&
ticketsPage.includes("lane.tickets.slice(0, lane.visibleCount)") &&
ticketsPage.includes("handleLaneScroll") &&
ticketsPage.includes("revealNextTickets"),
"Tickets list should fetch lightweight summaries once and incrementally reveal each Kanban lane",
ticketsPage.includes("laneState") &&
ticketsPage.includes("loadMore(lane.id)") &&
ticketsPage.includes("handleLaneScroll(event, lane.id)"),
"Tickets list should fetch lightweight paginated summaries for each Kanban lane",
);
assert(
ticketPanelModel.includes('label: "Ready + Planning"') &&
@@ -250,16 +250,17 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as
assert(
ticketDetailLoad.includes("/repositories") &&
ticketDetailPage.includes('mutate("state", "/state"') &&
ticketDetailPage.includes('mutate("queue", "/queue"') &&
ticketDetailPage.includes("async function queueTicket") &&
ticketDetailPage.includes("`${ticketPath}/queue`") &&
!ticketDetailPage.includes("/merge-request/merge") &&
ticketDetailPage.includes("mergeRequest.selector_from") &&
ticketDetailPage.includes("currentReview?.kind") &&
ticketDetailPage.includes("mergeEvent?.kind") &&
!ticketDetailPage.includes('mutate("review", "/review"') &&
ticketDetailPage.includes("mergeRequest.review_status") &&
ticketDetailPage.includes("mergeRequestPagePath") &&
ticketDetailPage.includes('mutate("close", "/close"') &&
ticketDetailPage.includes("ticketWorkerLaunchHref") &&
ticketDetailPage.includes("mutateAssignment") &&
ticketDetailPage.includes("can_start_manual_coder") &&
ticketDetailPage.includes("ticket.relations.outgoing"),
"Ticket detail should expose typed lifecycle actions, relations, target selection, and role Worker launch",
"Ticket detail should expose typed lifecycle actions, relations, target selection, assignments, and Merge Request navigation",
);
});
@@ -319,17 +320,19 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
);
});
Deno.test("root layout does not keep legacy unscoped route compatibility", async () => {
Deno.test("root layout keeps Workspace selection explicit", async () => {
const layoutLoad = await Deno.readTextFile(
new URL("./../../../routes/+layout.ts", import.meta.url),
);
assert(
!layoutLoad.includes("scopedCompatibilityRoute") &&
!layoutLoad.includes('pathname === "/runtimes"') &&
!layoutLoad.includes("return workspaceRoute(workspaceId, pathname)") &&
layoutLoad.includes("workspaceRoute(workspace.data.workspace_id)"),
"root layout should bootstrap the workspace entry only, not preserve legacy unscoped routes",
layoutLoad.includes("export const load") &&
layoutLoad.includes("() => ({})") &&
!layoutLoad.includes("scopedCompatibilityRoute") &&
!layoutLoad.includes("/api/workspace") &&
!layoutLoad.includes("workspaceRoute") &&
!layoutLoad.includes("redirect("),
"root layout should not infer, bootstrap, or redirect through a singleton Workspace",
);
});
@@ -811,9 +814,11 @@ Deno.test("Account UI owns browser passkey session state without workspace autho
"SidebarOverride should register and clean up the child-provided sidebar snippet",
);
assert(
rootLayoutLoad.includes('"/account"') &&
rootLayoutLoad.includes('"/login/device"'),
"Root layout should not redirect account and device-login public routes to a workspace",
rootLayoutLoad.includes("export const load") &&
rootLayoutLoad.includes("() => ({})") &&
!rootLayoutLoad.includes("workspaceRoute") &&
!rootLayoutLoad.includes("redirect("),
"Root layout should leave account and device-login routes public by avoiding Workspace redirects entirely",
);
});
@@ -33,7 +33,7 @@ export type RuntimeConnectionSummary = {
built_in: boolean;
config_managed: boolean;
active: boolean;
can_spawn_worker: boolean;
worker_creation_available: boolean;
restart_required: boolean;
status: string;
diagnostics: Diagnostic[];
@@ -29,30 +29,15 @@ export type Diagnostic = {
message: string;
};
export type RuntimeCapabilities = {
can_list_hosts: boolean;
can_list_workers: boolean;
can_get_worker: boolean;
can_spawn_worker: boolean;
can_stop_worker: boolean;
has_workspace_fs: boolean;
has_shell: boolean;
has_git: boolean;
supports_worktrees: boolean;
supports_backend_internal_tools: boolean;
workspace_scope: string;
os: string;
arch: string;
max_workers: number;
};
export type Runtime = {
runtime_id: string;
label: string;
kind: string;
status: string;
host_ids: string[];
capabilities: RuntimeCapabilities;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Diagnostic[];
};
@@ -64,7 +49,8 @@ export type Host = {
status: string;
observed_at: string;
last_seen_at: string | null;
capabilities: RuntimeCapabilities;
os: string;
arch: string;
diagnostics: Diagnostic[];
};
@@ -100,7 +86,7 @@ export type WorkerLaunchRuntimeOption = {
runtime_id: string;
display_name: string;
built_in: boolean;
can_spawn_worker: boolean;
worker_creation_available: boolean;
working_directory_required: boolean;
status: string;
diagnostics: Diagnostic[];
@@ -23,7 +23,7 @@ const options: WorkerLaunchOptionsResponse = {
runtime_id: "remote",
display_name: "Remote",
status: "active",
can_spawn_worker: true,
worker_creation_available: true,
built_in: false,
working_directory_required: true,
diagnostics: [],
@@ -32,7 +32,7 @@ const options: WorkerLaunchOptionsResponse = {
runtime_id: "embedded",
display_name: "Embedded",
status: "active",
can_spawn_worker: true,
worker_creation_available: true,
built_in: true,
working_directory_required: false,
diagnostics: [],
@@ -30,9 +30,9 @@ export function defaultWorkerLaunchForm(
): WorkerLaunchFormState {
const preferredRuntime =
options?.runtimes.find((runtime) =>
runtime.can_spawn_worker && runtime.status === "active"
runtime.worker_creation_available && runtime.status === "active"
) ??
options?.runtimes.find((runtime) => runtime.can_spawn_worker) ??
options?.runtimes.find((runtime) => runtime.worker_creation_available) ??
options?.runtimes[0];
const preferredProfile = options?.profiles.find((candidate) =>
candidate.id === options.default_profile
@@ -88,7 +88,7 @@
</div>
<div>
<dt>Platform</dt>
<dd>{host.capabilities.os} / {host.capabilities.arch}</dd>
<dd>{host.os} / {host.arch}</dd>
</div>
</dl>
</article>
@@ -183,7 +183,7 @@
built_in: true,
config_managed: false,
active: false,
can_spawn_worker: false,
worker_creation_available: false,
restart_required: false,
status: 'unknown',
diagnostics: []
@@ -5,7 +5,7 @@
let { data }: PageProps = $props();
function runtimePlatform(runtime: Runtime): string {
return `${runtime.capabilities.os} / ${runtime.capabilities.arch}`;
return `${runtime.os} / ${runtime.arch}`;
}
</script>
@@ -37,7 +37,6 @@
<th>Kind</th>
<th>Status</th>
<th>Platform</th>
<th>Capacity</th>
<th>Workdirs</th>
</tr>
</thead>
@@ -51,7 +50,6 @@
<td>{runtime.kind}</td>
<td>{runtime.status}</td>
<td>{runtimePlatform(runtime)}</td>
<td>{runtime.capabilities.max_workers} workers</td>
<td>
<a class="inline-link" href={`/w/${data.workspaceId}/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}/workdirs`}>
Open workdirs
@@ -313,7 +313,7 @@
<select class="worker-inline-select runtime-select" bind:value={runtimeId} required aria-label="Runtime">
{#if options?.runtimes.length}
{#each options.runtimes as runtime}
<option value={runtime.runtime_id} disabled={!runtime.can_spawn_worker}>
<option value={runtime.runtime_id} disabled={!runtime.worker_creation_available}>
{runtime.display_name}
</option>
{/each}
+3
View File
@@ -5,6 +5,9 @@ export default defineConfig({
plugins: [sveltekit()],
server: {
host: "localhost",
port: 5173,
strictPort: true,
allowedHosts: ["develop.hareworks.net"],
watch: {
ignored: [