merge: compose classification tags with Ticket case migration
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
export type Array_of_WorkspaceTag = Array<WorkspaceTag>;
|
||||
|
||||
export type BrowserAppendTicketEventRequest = { author?: string | null; body: string; role: BrowserTicketThreadRole; };
|
||||
|
||||
export type BrowserEditTicketRequest = { author?: string | null; body?: string | null; new_string?: string | null; old_string?: string | null; replace_all?: boolean; target?: BrowserTicketTargetEdit | null; title?: string | null; };
|
||||
@@ -89,6 +91,8 @@ export type TicketAssignmentRole = "orchestrator" | "worker" | "owner" | "contri
|
||||
|
||||
export type TicketAssignmentSummary = { assignment_id: string; runtime_id: string; worker_id: string; worker_resource_key?: string | null; };
|
||||
|
||||
export type TicketAttention = "unresolved_review" | "missing_commit" | "blocked" | "unblocked" | "awaiting_review" | "unresolved_changes" | "stale_after_rescope" | "missing_evidence";
|
||||
|
||||
export type TicketCaseAction = { action: "close"; reason: TicketClosureReason; } | { action: "reopen"; } | { action: "correct_closure"; reason: TicketClosureReason; };
|
||||
|
||||
export type TicketCaseHistoryEntry = { action: TicketCaseAction; at: string; author: string; explanation: string; operation_key: string; previous_closure_operation_key?: string | null; references: Array<TicketReference>; };
|
||||
@@ -99,7 +103,7 @@ export type TicketClosureJudgment = { at: string; author: string; explanation: s
|
||||
|
||||
export type TicketClosureReason = "completed" | "not_planned" | "duplicate" | "unknown" | { other: string; };
|
||||
|
||||
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; closure_history: Array<TicketCaseHistoryEntry>; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: BrowserTicketState; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
|
||||
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; closure_history: Array<TicketCaseHistoryEntry>; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: BrowserTicketState; tags: Array<WorkspaceTag>; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
|
||||
|
||||
export type TicketDetailDerivedRelation = { at: string; author: string; forward_kind: string; inverse_kind: string; note?: string | null; source_resource_key?: string | null; source_ticket: string; };
|
||||
|
||||
@@ -121,7 +125,7 @@ export type TicketIntakeSummary = { author?: string | null; body: MarkdownText;
|
||||
|
||||
export type TicketLegacyWorkflowSnapshot = { queued_at?: string | null; queued_by?: string | null; resolution?: string | null; status: string; workflow_state: string; workflow_state_explicit: boolean; };
|
||||
|
||||
export type TicketListItemSummary = { closure_operation_key?: string | null; current_closure?: TicketClosureJudgment | null; id: string; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; priority: string; queued_at?: string | null; queued_by?: string | null; record_source: string; resource_key: string; state: BrowserTicketState; title: string; updated_at?: string | null; workspace_action_priority: string; };
|
||||
export type TicketListItemSummary = { closure_operation_key?: string | null; current_closure?: TicketClosureJudgment | null; id: string; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; priority: string; queued_at?: string | null; queued_by?: string | null; record_source: string; resource_key: string; state: BrowserTicketState; tags?: Array<WorkspaceTag>; title: string; updated_at?: string | null; workspace_action_priority: string; };
|
||||
|
||||
export type TicketListResponse = { invalid_records: Array<InvalidProjectRecord>; items: Array<TicketListItemSummary>; limit: number & { readonly "__yoiIntegerRange": "0..1000"; }; page: QueryPage; record_authority: string; workspace_id: string; };
|
||||
|
||||
@@ -129,6 +133,12 @@ export type TicketMarkReadyRequest = { intake_summary?: TicketIntakeSummary | nu
|
||||
|
||||
export type TicketMergeRequestSummary = { current_subject_ref?: string | null; integration_evidence_error?: string | null; merge_request_id: string; repository_key: string; review_excerpt?: string | null; review_requested_at?: string | null; review_status: string; review_subject_ref?: string | null; review_submitted_at?: string | null; selector_from?: string | null; selector_to: string; source_ref_observation: TicketSourceRefObservation; state: string; updated_at: string; };
|
||||
|
||||
export type TicketQueryItem = { blocker_count: number; closure_operation_key?: string | null; content_digest: string; created_at?: string | null; current_closure?: TicketClosureJudgment | null; current_worker?: TicketAssignmentSummary | null; evidence: TicketEvidenceSummary; id: string; legacy_handoff?: TicketLegacyWorkflowSnapshot | null; linked_objective_ids: Array<string>; linked_objective_keys: Array<string>; matched_fields: Array<string>; matching_event?: TicketEvidenceEvent | null; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; readiness?: string | null; relation_count: number; resource_key: string; snippet?: string | null; state: BrowserTicketState; tags: Array<WorkspaceTag>; title: string; unresolved_blocker_count: number; unresolved_review_count: number; updated_at?: string | null; workspace_action_priority: string; };
|
||||
|
||||
export type TicketQueryRequest = { attention?: Array<TicketAttention>; cursor?: string | null; event_kinds?: Array<string>; evidence?: Array<string>; limit?: number & { readonly "__yoiIntegerRange": "0..1000"; } | null; linked_objective_id?: string | null; query?: string | null; related_ticket_id?: string | null; relation_kind?: string | null; review_status?: string | null; sort?: string | null; states?: Array<TicketState>; tag_ids?: Array<string>; tag_ids_any?: Array<string>; tag_ids_none?: Array<string>; updated_after?: string | null; updated_before?: string | null; };
|
||||
|
||||
export type TicketQueryResponse = { items: Array<TicketQueryItem>; page: QueryPage; record_authority: string; };
|
||||
|
||||
export type TicketQueueOutcome = { queued_tickets: Array<string>; requested_ticket: string; };
|
||||
|
||||
export type TicketRef = { id: string; resource_key?: string | null; slug: string; status: TicketStatus; };
|
||||
@@ -147,10 +157,18 @@ export type TicketState = "open" | "closed";
|
||||
|
||||
export type TicketStatus = "Open" | "Closed";
|
||||
|
||||
export type TicketSummary = { id: string; kind: string; labels: Array<string>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; resource_key?: string | null; slug: string; status: ExtensibleTicketStatus; title: string; updated_at?: string | null; workflow_state: TicketState; workflow_state_explicit: boolean; };
|
||||
export type TicketSummary = { id: string; kind: string; labels: Array<string>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; resource_key?: string | null; slug: string; status: ExtensibleTicketStatus; tags?: Array<WorkspaceTag>; title: string; updated_at?: string | null; workflow_state: TicketState; workflow_state_explicit: boolean; };
|
||||
|
||||
export type TicketTagSetRequest = { expected_tag_ids: Array<string>; operation_key: string; tag_ids: Array<string>; };
|
||||
|
||||
export type TicketTarget = { access: TicketTargetAccess; ref_selector?: string | null; repository_key: string; };
|
||||
|
||||
export type TicketTargetAccess = "read_only" | "read_write";
|
||||
|
||||
export type TicketWorkerAttachmentBinding = { alias: string; connection_id: string; working_directory_id: string; };
|
||||
|
||||
export type WorkspaceTag = { deleted: boolean; id: string; last_operation_key: string; name: string; workspace_id: string; };
|
||||
|
||||
export type WorkspaceTagAction = { kind: "create"; name: string; } | { expected_last_operation_key: string; kind: "rename"; name: string; tag_id: string; } | { expected_last_operation_key: string; kind: "delete"; tag_id: string; };
|
||||
|
||||
export type WorkspaceTagMutation = { action: WorkspaceTagAction; operation_key: string; };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
|
||||
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Input contract: 0.1.0 (sha256:ba017607e313ba3eb500e4eb78f94c2bd95a08acde96688b8d2a0014abd94ea3)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
parseTicketListResponse,
|
||||
parseTicketRecordRef,
|
||||
parseTicketRoleAssignmentMutationResponse,
|
||||
parseTicketSummary,
|
||||
} from "./ticket-browser.ts";
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
@@ -48,6 +49,7 @@ Deno.test("Ticket Browser parser accepts the generated Ticket list shape", () =>
|
||||
id: "ticket-id",
|
||||
resource_key: "T-1",
|
||||
title: "Ticket",
|
||||
tags: [],
|
||||
state: "open",
|
||||
priority: "P2",
|
||||
updated_at: null,
|
||||
@@ -176,6 +178,7 @@ function ticketDetailFixture() {
|
||||
resource_key: "T-1",
|
||||
risk_flags: [],
|
||||
state: "open",
|
||||
tags: [],
|
||||
targets: [
|
||||
{
|
||||
repository_key: "docs",
|
||||
@@ -452,6 +455,14 @@ Deno.test("Ticket Browser parser validates the canonical Merge Request thread",
|
||||
);
|
||||
});
|
||||
|
||||
const tags = ["ready", "closed"].map((name) => ({
|
||||
workspace_id: "workspace-a",
|
||||
id: `tag-${name}`,
|
||||
name,
|
||||
last_operation_key: `create-${name}`,
|
||||
deleted: false,
|
||||
}));
|
||||
|
||||
const closure = {
|
||||
operation_key: "close-1",
|
||||
reason: "not_planned",
|
||||
@@ -499,10 +510,11 @@ const legacyHandoff = {
|
||||
resolution: "Historical result, not a current case state",
|
||||
};
|
||||
|
||||
Deno.test("case projections retain reasons, actors, references and migration history after reopening", () => {
|
||||
Deno.test("tags named ready and closed coexist with case judgment and retained reopen history without inferring state", () => {
|
||||
const fixture = {
|
||||
...ticketDetailFixture(),
|
||||
state: "open",
|
||||
tags,
|
||||
closure_operation_key: "correct-1",
|
||||
current_closure: null,
|
||||
closure_history: closureHistory,
|
||||
@@ -513,11 +525,16 @@ Deno.test("case projections retain reasons, actors, references and migration his
|
||||
assertEquals(parsed.closure_operation_key, "correct-1");
|
||||
assertEquals(parsed.closure_history, closureHistory);
|
||||
assertEquals(parsed.legacy_handoff, legacyHandoff);
|
||||
assertEquals(
|
||||
parseTicketDetail({ ...fixture, state: "closed", current_closure: closure })
|
||||
.current_closure,
|
||||
closure,
|
||||
);
|
||||
assertEquals(parsed.tags, tags);
|
||||
const closed = parseTicketDetail({
|
||||
...fixture,
|
||||
state: "closed",
|
||||
current_closure: closure,
|
||||
});
|
||||
assertEquals(closed.state, "closed");
|
||||
assertEquals(closed.current_closure, closure);
|
||||
assertEquals(closed.closure_history, closureHistory);
|
||||
assertEquals(closed.tags, tags);
|
||||
const list = parseTicketListResponse({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 30,
|
||||
@@ -529,6 +546,7 @@ Deno.test("case projections retain reasons, actors, references and migration his
|
||||
resource_key: "T-1",
|
||||
title: "Ticket",
|
||||
state: "closed",
|
||||
tags,
|
||||
priority: "P2",
|
||||
record_source: "sqlite",
|
||||
workspace_action_priority: "background",
|
||||
@@ -537,10 +555,73 @@ Deno.test("case projections retain reasons, actors, references and migration his
|
||||
legacy_handoff: legacyHandoff,
|
||||
}],
|
||||
});
|
||||
assertEquals(list.items[0].state, "closed");
|
||||
assertEquals(list.items[0].tags, tags);
|
||||
assertEquals(list.items[0].current_closure, closure);
|
||||
assertEquals(list.items[0].legacy_handoff, legacyHandoff);
|
||||
});
|
||||
|
||||
Deno.test("Ticket tag projections reject missing collections and malformed identities, types and unknown fields", () => {
|
||||
const parsers = [
|
||||
(tags: unknown) => parseTicketDetail({ ...ticketDetailFixture(), tags }),
|
||||
(tags: unknown) =>
|
||||
parseTicketSummary({
|
||||
id: "ticket-id",
|
||||
slug: "ticket",
|
||||
title: "Ticket",
|
||||
kind: "task",
|
||||
labels: [],
|
||||
priority: "P2",
|
||||
status: "Open",
|
||||
workflow_state: "open",
|
||||
workflow_state_explicit: true,
|
||||
tags,
|
||||
}),
|
||||
(tags: unknown) =>
|
||||
parseTicketListResponse({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 30,
|
||||
page,
|
||||
invalid_records: [],
|
||||
record_authority: "sqlite",
|
||||
items: [{
|
||||
id: "ticket-id",
|
||||
resource_key: "T-1",
|
||||
title: "Ticket",
|
||||
state: "open",
|
||||
priority: "P2",
|
||||
record_source: "sqlite",
|
||||
workspace_action_priority: "background",
|
||||
tags,
|
||||
}],
|
||||
}),
|
||||
];
|
||||
const tag = tags[0];
|
||||
for (const parse of parsers) {
|
||||
for (const value of [undefined, null, {}, "ready"]) {
|
||||
assertThrows(() => parse(value), "tags must be an array");
|
||||
}
|
||||
for (
|
||||
const [value, message] of [
|
||||
[{ ...tag, id: undefined }, "id must be a string"],
|
||||
[{ ...tag, id: 1 }, "id must be a string"],
|
||||
[{ ...tag, id: " " }, "id must be nonempty"],
|
||||
[{ ...tag, id: "é".repeat(65) }, "at most 128 UTF-8 bytes"],
|
||||
[{ ...tag, workspace_id: false }, "workspace_id must be a string"],
|
||||
[{ ...tag, name: null }, "name must be a string"],
|
||||
[
|
||||
{ ...tag, last_operation_key: 1 },
|
||||
"last_operation_key must be a string",
|
||||
],
|
||||
[{ ...tag, deleted: "false" }, "deleted must be a boolean"],
|
||||
[{ ...tag, state: "closed" }, "unknown field state"],
|
||||
] as const
|
||||
) {
|
||||
assertThrows(() => parse([value]), message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("case decoders reject legacy current states and malformed closure judgments", () => {
|
||||
const fixture = ticketDetailFixture();
|
||||
for (const state of ["planning", "ready", "queued", "inprogress", "done"]) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import type {
|
||||
TicketRoleAssignmentSummary,
|
||||
TicketSummary,
|
||||
TicketTarget,
|
||||
WorkspaceTag,
|
||||
} from "#lib/generated/ticket-api.ts";
|
||||
|
||||
export const TICKET_BROWSER_API_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
||||
@@ -331,6 +332,31 @@ function caseProjection(item: Record<string, unknown>, label: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseWorkspaceTag(value: unknown, label: string): WorkspaceTag {
|
||||
const item = object(value, label);
|
||||
exact(item, [
|
||||
"workspace_id",
|
||||
"id",
|
||||
"name",
|
||||
"last_operation_key",
|
||||
"deleted",
|
||||
], label);
|
||||
const id = string(item.id, `${label}.id`);
|
||||
if (id.trim().length === 0 || new TextEncoder().encode(id).length > 128) {
|
||||
throw new Error(`${label}.id must be nonempty and at most 128 UTF-8 bytes`);
|
||||
}
|
||||
return {
|
||||
workspace_id: string(item.workspace_id, `${label}.workspace_id`),
|
||||
id,
|
||||
name: string(item.name, `${label}.name`),
|
||||
last_operation_key: string(
|
||||
item.last_operation_key,
|
||||
`${label}.last_operation_key`,
|
||||
),
|
||||
deleted: boolean(item.deleted, `${label}.deleted`),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseTicketSummary(value: unknown): TicketSummary {
|
||||
const label = "Ticket summary";
|
||||
const item = object(value, label);
|
||||
@@ -345,6 +371,7 @@ export function parseTicketSummary(value: unknown): TicketSummary {
|
||||
"resource_key",
|
||||
"slug",
|
||||
"status",
|
||||
"tags",
|
||||
"title",
|
||||
"updated_at",
|
||||
"workflow_state",
|
||||
@@ -371,6 +398,7 @@ export function parseTicketSummary(value: unknown): TicketSummary {
|
||||
),
|
||||
slug: string(item.slug, `${label}.slug`),
|
||||
status,
|
||||
tags: array(item.tags, `${label}.tags`, parseWorkspaceTag),
|
||||
title: string(item.title, `${label}.title`),
|
||||
updated_at: optionalNullableString(item.updated_at, `${label}.updated_at`),
|
||||
workflow_state: parseTicketState(
|
||||
@@ -400,6 +428,7 @@ function parseTicketListItem(
|
||||
"record_source",
|
||||
"resource_key",
|
||||
"state",
|
||||
"tags",
|
||||
"title",
|
||||
"updated_at",
|
||||
"workspace_action_priority",
|
||||
@@ -413,6 +442,7 @@ function parseTicketListItem(
|
||||
record_source: string(item.record_source, `${label}.record_source`),
|
||||
resource_key: string(item.resource_key, `${label}.resource_key`),
|
||||
state: parseTicketState(item.state, `${label}.state`),
|
||||
tags: array(item.tags, `${label}.tags`, parseWorkspaceTag),
|
||||
title: string(item.title, `${label}.title`),
|
||||
updated_at: optionalNullableString(item.updated_at, `${label}.updated_at`),
|
||||
workspace_action_priority: string(
|
||||
@@ -960,6 +990,7 @@ export function parseTicketDetail(value: unknown): TicketDetail {
|
||||
"resource_key",
|
||||
"risk_flags",
|
||||
"state",
|
||||
"tags",
|
||||
"targets",
|
||||
"title",
|
||||
"updated_at",
|
||||
@@ -1036,6 +1067,7 @@ export function parseTicketDetail(value: unknown): TicketDetail {
|
||||
resource_key: string(item.resource_key, `${label}.resource_key`),
|
||||
risk_flags: strings(item.risk_flags, `${label}.risk_flags`),
|
||||
state: parseTicketState(item.state, `${label}.state`),
|
||||
tags: array(item.tags, `${label}.tags`, parseWorkspaceTag),
|
||||
targets: array(item.targets, `${label}.targets`, parseTicketTarget),
|
||||
title: string(item.title, `${label}.title`),
|
||||
updated_at: optionalNullableString(item.updated_at, `${label}.updated_at`),
|
||||
|
||||
@@ -34,6 +34,13 @@ const summary = {
|
||||
title: "Case",
|
||||
kind: "task",
|
||||
labels: [],
|
||||
tags: [{
|
||||
workspace_id: "workspace one",
|
||||
id: "tag-closed",
|
||||
name: "closed",
|
||||
last_operation_key: "create-tag",
|
||||
deleted: false,
|
||||
}],
|
||||
priority: "P2",
|
||||
status: "Closed",
|
||||
workflow_state: "closed",
|
||||
@@ -73,11 +80,9 @@ Deno.test("case actions carry content/state/last-closure CAS without browser aut
|
||||
assertEquals(JSON.parse(String(options.body)), request);
|
||||
return Promise.resolve(Response.json(summary));
|
||||
};
|
||||
assertEquals(
|
||||
(await updateTicketCase("workspace one", "T/1", request))
|
||||
.workflow_state,
|
||||
"closed",
|
||||
);
|
||||
const result = await updateTicketCase("workspace one", "T/1", request);
|
||||
assertEquals(result.workflow_state, "closed");
|
||||
assertEquals(result.tags, summary.tags);
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
|
||||
@@ -25,6 +25,7 @@ export function fixtureTicket(
|
||||
current_closure: null,
|
||||
legacy_handoff: null,
|
||||
state,
|
||||
tags: [],
|
||||
priority: "normal",
|
||||
queued_at: null,
|
||||
queued_by: null,
|
||||
|
||||
@@ -29,15 +29,35 @@ Deno.test("ticketLanes separates open and closed cases and orders updates determ
|
||||
id: "old",
|
||||
state: "open",
|
||||
title: "Open old",
|
||||
tags: [{
|
||||
workspace_id: "workspace-a",
|
||||
id: "tag-closed",
|
||||
name: "closed",
|
||||
last_operation_key: "create-closed",
|
||||
deleted: false,
|
||||
}],
|
||||
updated_at: "2026-07-29T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "new",
|
||||
state: "open",
|
||||
title: "Open new",
|
||||
tags: [],
|
||||
updated_at: "2026-07-30T12:00:00Z",
|
||||
},
|
||||
{ id: "closed", state: "closed", title: "Closed", updated_at: null },
|
||||
{
|
||||
id: "closed",
|
||||
state: "closed",
|
||||
title: "Closed",
|
||||
tags: [{
|
||||
workspace_id: "workspace-a",
|
||||
id: "tag-ready",
|
||||
name: "ready",
|
||||
last_operation_key: "create-ready",
|
||||
deleted: false,
|
||||
}],
|
||||
updated_at: null,
|
||||
},
|
||||
] as TicketSummary[];
|
||||
const lanes = ticketLanes(tickets);
|
||||
assertEquals(lanes.map((lane) => lane.id), ["open", "closed"]);
|
||||
|
||||
Reference in New Issue
Block a user