From d4ed6192ea4a8a641fb9fda3e23f4d69a5c67db2 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 9 Oct 2026 02:36:59 +0900 Subject: [PATCH] fix: align ticket web consumers with generic worker assignments --- .../lib/workspace/api/ticket-browser.test.ts | 40 ++++++++++++++++++- .../src/lib/workspace/api/ticket-browser.ts | 20 +++++----- .../console/worker-console.ui.test.ts | 2 +- .../workspace/home/dashboard.test-fixtures.ts | 4 +- .../src/lib/workspace/home/dashboard.ts | 6 +-- .../tickets/[ticketId]/+page.svelte | 28 ++++++------- 6 files changed, 68 insertions(+), 32 deletions(-) diff --git a/web/workspace/src/lib/workspace/api/ticket-browser.test.ts b/web/workspace/src/lib/workspace/api/ticket-browser.test.ts index 11693f801..2333b0b43 100644 --- a/web/workspace/src/lib/workspace/api/ticket-browser.test.ts +++ b/web/workspace/src/lib/workspace/api/ticket-browser.test.ts @@ -9,6 +9,7 @@ import { parseTicketDetail, parseTicketListResponse, parseTicketRecordRef, + parseTicketRoleAssignmentMutationResponse, } from "./ticket-browser.ts"; function assertEquals(actual: unknown, expected: unknown): void { @@ -81,13 +82,48 @@ Deno.test("Ticket Browser parser rejects unknown fields and out-of-range limits" ); }); +Deno.test("Ticket Browser accepts generic Worker eligibility and assignments", () => { + const fixture = ticketDetailFixture(); + const worker = { + assignment_id: "assignment-1", + runtime_id: "runtime-1", + worker_id: "worker-1", + worker_resource_key: "W-1", + }; + const parsed = parseTicketDetail({ + ...fixture, + current_worker: worker, + action_eligibility: { + ...fixture.action_eligibility, + can_start_manual_worker: true, + }, + }); + assertEquals(parsed.current_worker, worker); + assertEquals(parsed.action_eligibility.can_start_manual_worker, true); + + const assignment = { + assigned_at: "2026-10-06T00:00:00Z", + assigned_by: "user-1", + assignment_id: "assignment-1", + principal: { kind: "worker", runtime_id: "runtime-1", worker_id: "worker-1" }, + role: "worker", + ticket_id: "ticket-id", + workspace_id: "workspace-a", + }; + assertEquals(parseTicketRoleAssignmentMutationResponse({ + assignment, + ticket_id: "ticket-id", + workspace_id: "workspace-a", + }).assignment, assignment); +}); + function ticketDetailFixture() { return { action_eligibility: { blockers: [], can_assign_orchestrator: false, can_queue: false, - can_start_manual_coder: false, + can_start_manual_worker: false, can_unassign_orchestrator: false, queue_tickets: [], }, @@ -97,7 +133,7 @@ function ticketDetailFixture() { assignments: [], body: "Body", body_truncated: false, - current_coder: null, + current_worker: null, event_count: 0, event_page: { ...page, returned: 0 }, events: [], diff --git a/web/workspace/src/lib/workspace/api/ticket-browser.ts b/web/workspace/src/lib/workspace/api/ticket-browser.ts index 0eb784931..c96142943 100644 --- a/web/workspace/src/lib/workspace/api/ticket-browser.ts +++ b/web/workspace/src/lib/workspace/api/ticket-browser.ts @@ -397,7 +397,7 @@ function parseTicketRoleAssignmentRecord( "workspace_id", ], label); const role = string(item.role, `${label}.role`); - if (!["orchestrator", "coder", "owner", "contributor"].includes(role)) { + if (!["orchestrator", "worker", "owner", "contributor"].includes(role)) { throw new Error(`${label}.role is invalid`); } return { @@ -423,7 +423,7 @@ function parseActionEligibility( "blockers", "can_assign_orchestrator", "can_queue", - "can_start_manual_coder", + "can_start_manual_worker", "can_unassign_orchestrator", "queue_tickets", ], label); @@ -434,9 +434,9 @@ function parseActionEligibility( `${label}.can_assign_orchestrator`, ), can_queue: boolean(item.can_queue, `${label}.can_queue`), - can_start_manual_coder: boolean( - item.can_start_manual_coder, - `${label}.can_start_manual_coder`, + can_start_manual_worker: boolean( + item.can_start_manual_worker, + `${label}.can_start_manual_worker`, ), can_unassign_orchestrator: boolean( item.can_unassign_orchestrator, @@ -730,7 +730,7 @@ export function parseTicketDetail(value: unknown): TicketDetail { "body", "body_truncated", "created_at", - "current_coder", + "current_worker", "event_count", "event_page", "events", @@ -774,13 +774,13 @@ export function parseTicketDetail(value: unknown): TicketDetail { body: string(item.body, `${label}.body`), body_truncated: boolean(item.body_truncated, `${label}.body_truncated`), created_at: optionalNullableString(item.created_at, `${label}.created_at`), - current_coder: item.current_coder === undefined + current_worker: item.current_worker === undefined ? undefined - : item.current_coder === null + : item.current_worker === null ? null : parseTicketAssignmentSummary( - item.current_coder, - `${label}.current_coder`, + item.current_worker, + `${label}.current_worker`, ), event_count: integer(item.event_count, `${label}.event_count`), event_page: parseQueryPage(item.event_page, `${label}.event_page`), diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index abd18cdfd..5b936cd03 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -438,7 +438,7 @@ Deno.test("workspace Tickets surface provides Kanban and lifecycle controls", as ticketDetailPage.includes("mergeRequestPagePath") && ticketDetailPage.includes('mutate("close", "/close"') && ticketDetailPage.includes("mutateAssignment") && - ticketDetailPage.includes("can_start_manual_coder") && + ticketDetailPage.includes("can_start_manual_worker") && ticketDetailPage.includes("ticket.relations.outgoing"), "Ticket detail should expose typed lifecycle actions, relations, target selection, assignments, and Merge Request navigation", ); diff --git a/web/workspace/src/lib/workspace/home/dashboard.test-fixtures.ts b/web/workspace/src/lib/workspace/home/dashboard.test-fixtures.ts index 958cf04d8..13d0f5e66 100644 --- a/web/workspace/src/lib/workspace/home/dashboard.test-fixtures.ts +++ b/web/workspace/src/lib/workspace/home/dashboard.test-fixtures.ts @@ -59,7 +59,7 @@ export function fixtureDetail(key = "T-101") { complete_for_integration: false, missing: [], }, - current_coder: key === "T-101" + current_worker: key === "T-101" ? { assignment_id: "a-1", runtime_id: "internal-runtime", @@ -71,7 +71,7 @@ export function fixtureDetail(key = "T-101") { blockers: [], can_assign_orchestrator: false, can_queue: false, - can_start_manual_coder: false, + can_start_manual_worker: false, can_unassign_orchestrator: false, queue_tickets: [], }, diff --git a/web/workspace/src/lib/workspace/home/dashboard.ts b/web/workspace/src/lib/workspace/home/dashboard.ts index 6decb408c..12a0c1a17 100644 --- a/web/workspace/src/lib/workspace/home/dashboard.ts +++ b/web/workspace/src/lib/workspace/home/dashboard.ts @@ -196,10 +196,10 @@ export function loadDashboard( ? (blockedKeys.length === blockers.length ? `Blocked by ${blockedKeys.join(", ")}` : `${blockers.length} unresolved dependencies`) - : current.current_coder + : current.current_worker ? undefined - : "No coder assigned", - workerKey: current.current_coder?.worker_resource_key ?? + : "No Worker assigned", + workerKey: current.current_worker?.worker_resource_key ?? undefined, }, }; diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte index a0f792fbe..8570cfbdd 100644 --- a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte @@ -93,8 +93,8 @@ let cancellationReason = $state(""); let routeTicketSnapshot = `${initialData.ticketId}:${loadedTicket.item_revision}`; let routeGeneration = 0; - const coderAssignment = $derived( - ticket.assignments.find((assignment) => assignment.role === "coder") ?? null, + const workerAssignment = $derived( + ticket.assignments.find((assignment) => assignment.role === "worker") ?? null, ); function repositoryFor(repositoryKey: string): RepositorySummary | null { return (loadedRepositories?.items ?? []).find((repository: RepositorySummary) => @@ -124,7 +124,7 @@ return readWriteCount === 1; }); const implementationStartEligible = $derived( - ticket.action_eligibility.can_start_manual_coder, + ticket.action_eligibility.can_start_manual_worker, ); const ticketPath = $derived( @@ -235,7 +235,7 @@ async function mutateAssignment( action: string, - role: "orchestrator" | "coder", + role: "orchestrator" | "worker", principal: Record, ): Promise { if (busy) return; @@ -281,10 +281,10 @@ }); } - async function startManualCoder(event: SubmitEvent): Promise { + async function startManualWorker(event: SubmitEvent): Promise { event.preventDefault(); if (!manualRuntimeId.trim() || !manualWorkerId.trim()) return; - await mutateAssignment("start-manual", "coder", { + await mutateAssignment("start-manual", "worker", { kind: "worker", runtime_id: manualRuntimeId.trim(), worker_id: manualWorkerId.trim(), @@ -293,11 +293,11 @@ async function cancelImplementation(event: SubmitEvent): Promise { event.preventDefault(); - if (!coderAssignment || !cancellationReason.trim()) return; + if (!workerAssignment || !cancellationReason.trim()) return; if ( await mutate("cancel-implementation", "/implementation-cancellations", { operation_id: crypto.randomUUID(), - assignment_id: coderAssignment.assignment_id, + assignment_id: workerAssignment.assignment_id, reason: cancellationReason.trim(), }) ) cancellationReason = ""; @@ -561,7 +561,7 @@ {/if} {#if implementationStartEligible} -
+
{/if} - {#if ticket.state === "inprogress" && coderAssignment} + {#if ticket.state === "inprogress" && workerAssignment}
Cancel implementation

- Cancel the assigned Coder, remove its assignment, and return this Ticket to ready. + Cancel the assigned Worker, remove its assignment, and return this Ticket to ready.