fix: align ticket web consumers with generic worker assignments
This commit is contained in:
@@ -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: [],
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<string, string>,
|
||||
): Promise<void> {
|
||||
if (busy) return;
|
||||
@@ -281,10 +281,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function startManualCoder(event: SubmitEvent): Promise<void> {
|
||||
async function startManualWorker(event: SubmitEvent): Promise<void> {
|
||||
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<void> {
|
||||
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 @@
|
||||
</button>
|
||||
{/if}
|
||||
{#if implementationStartEligible}
|
||||
<form class="ticket-control-form" onsubmit={startManualCoder}>
|
||||
<form class="ticket-control-form" onsubmit={startManualWorker}>
|
||||
<label>Runtime ID<input bind:value={manualRuntimeId} required /></label>
|
||||
<label>Worker ID<input bind:value={manualWorkerId} required /></label>
|
||||
<button
|
||||
@@ -569,16 +569,16 @@
|
||||
type="submit"
|
||||
disabled={busy !== null || !manualRuntimeId.trim() || !manualWorkerId.trim()}
|
||||
>
|
||||
{busy === "start-manual" ? "Starting…" : "Assign Coder and start"}
|
||||
{busy === "start-manual" ? "Starting…" : "Assign Worker and start"}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
{#if ticket.state === "inprogress" && coderAssignment}
|
||||
{#if ticket.state === "inprogress" && workerAssignment}
|
||||
<details class="ticket-cancel-implementation">
|
||||
<summary>Cancel implementation</summary>
|
||||
<form class="ticket-control-form" onsubmit={cancelImplementation}>
|
||||
<p class="workspace-empty-copy">
|
||||
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.
|
||||
</p>
|
||||
<label>Reason<textarea bind:value={cancellationReason} rows="3" required></textarea></label>
|
||||
<button
|
||||
@@ -667,7 +667,7 @@
|
||||
{busy === "queue" ? "Queueing…" : `Queue ${ticket.action_eligibility.queue_tickets.length} Ticket(s)`}
|
||||
</button>
|
||||
{#if !ticket.action_eligibility.can_queue}
|
||||
<p class="workspace-empty-copy">Queue requires a valid target, an active Orchestrator assignment, no active Coder assignment, and no dependency still in planning.</p>
|
||||
<p class="workspace-empty-copy">Queue requires a valid target, an active Orchestrator assignment, no active Worker assignment, and no dependency still in planning.</p>
|
||||
{:else if ticket.action_eligibility.queue_tickets.length > 0}
|
||||
<p class="workspace-empty-copy">This operation queues: {ticket.action_eligibility.queue_tickets.join(", ")}.</p>
|
||||
{#if ticket.relations.blockers.length > 0}
|
||||
@@ -733,7 +733,7 @@
|
||||
</article>
|
||||
{/each}
|
||||
{:else}
|
||||
<p class="workspace-empty-copy">The assigned Coder has not opened a Merge Request.</p>
|
||||
<p class="workspace-empty-copy">The assigned Worker has not opened a Merge Request.</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user