diff --git a/frontend/src/adapters/http/streamGateways.ts b/frontend/src/adapters/http/streamGateways.ts index 67e0daf..fe45103 100644 --- a/frontend/src/adapters/http/streamGateways.ts +++ b/frontend/src/adapters/http/streamGateways.ts @@ -286,6 +286,16 @@ export class HttpTicketGateway implements TicketGateway { private readonly ws: WsLiveClient, ) {} + private normalizeTicketActor(actor: unknown): Ticket["createdBy"] { + if (!actor || typeof actor !== "object") return { kind: "user" }; + const rec = actor as Record; + if (rec.kind === "user" || rec.kind === "system") return { kind: rec.kind }; + if (rec.kind === "agent" && typeof rec.agentId === "string" && rec.agentId) { + return { kind: "agent", agentId: rec.agentId }; + } + return { kind: "user" }; + } + private normalizeTicket(ticket: Ticket): Ticket { return { ...ticket, @@ -294,6 +304,8 @@ export class HttpTicketGateway implements TicketGateway { ? ticket.assignedAgentIds : [], attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [], + createdBy: this.normalizeTicketActor(ticket.createdBy), + updatedBy: this.normalizeTicketActor(ticket.updatedBy), }; } @@ -306,6 +318,7 @@ export class HttpTicketGateway implements TicketGateway { assignedAgentIds: Array.isArray(item.assignedAgentIds) ? item.assignedAgentIds : [], + createdBy: this.normalizeTicketActor(item.createdBy), })) : [], }; diff --git a/frontend/src/adapters/http/ticketGateway.test.ts b/frontend/src/adapters/http/ticketGateway.test.ts new file mode 100644 index 0000000..3225491 --- /dev/null +++ b/frontend/src/adapters/http/ticketGateway.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from "vitest"; + +import { HttpTicketGateway } from "./streamGateways"; +import type { HttpInvoker } from "./httpInvoker"; +import type { WsLiveClient } from "./wsLiveClient"; + +function gateway(result: unknown): { + gw: HttpTicketGateway; + invoke: ReturnType; +} { + const invoke = vi.fn().mockResolvedValue(result); + const http = { invoke } as unknown as HttpInvoker; + const ws = {} as unknown as WsLiveClient; + return { gw: new HttpTicketGateway(http, ws), invoke }; +} + +describe("HttpTicketGateway normalization", () => { + it("normalizes legacy ticket list rows that omit assignedAgentIds and createdBy", async () => { + const { gw } = gateway({ + items: [ + { + ref: "#12", + path: ".ideai/tickets/12/issue.md", + title: "Legacy row", + status: "open", + priority: "medium", + updatedAt: 1, + }, + ], + }); + + const list = await gw.list("proj-1"); + + expect(list.items[0]?.assignedAgentIds).toEqual([]); + expect(list.items[0]?.createdBy).toEqual({ kind: "user" }); + }); + + it("normalizes legacy ticket payloads that omit array fields and actors", async () => { + const { gw } = gateway({ + id: "issue-1", + ref: "#12", + number: 12, + title: "Legacy", + description: "", + status: "open", + priority: "medium", + createdAt: 1, + updatedAt: 1, + version: 1, + }); + + const ticket = await gw.read("proj-1", "#12", true); + + expect(ticket.links).toEqual([]); + expect(ticket.assignedAgentIds).toEqual([]); + expect(ticket.attachments).toEqual([]); + expect(ticket.createdBy).toEqual({ kind: "user" }); + expect(ticket.updatedBy).toEqual({ kind: "user" }); + }); + + it("normalizes malformed ticket actors", async () => { + const { gw } = gateway({ + id: "issue-1", + ref: "#12", + number: 12, + title: "Legacy", + description: "", + status: "open", + priority: "medium", + createdBy: { kind: "agent" }, + updatedBy: { kind: "unknown" }, + createdAt: 1, + updatedAt: 1, + version: 1, + }); + + const ticket = await gw.read("proj-1", "#12", true); + + expect(ticket.createdBy).toEqual({ kind: "user" }); + expect(ticket.updatedBy).toEqual({ kind: "user" }); + }); +}); diff --git a/frontend/src/adapters/ticket.test.ts b/frontend/src/adapters/ticket.test.ts index 273ef85..8edf730 100644 --- a/frontend/src/adapters/ticket.test.ts +++ b/frontend/src/adapters/ticket.test.ts @@ -55,7 +55,7 @@ describe("TauriTicketGateway invoke payloads", () => { }); }); - it("normalizes legacy ticket list rows that omit assignedAgentIds", async () => { + it("normalizes legacy ticket list rows that omit assignedAgentIds and createdBy", async () => { invoke.mockResolvedValueOnce({ items: [ { @@ -64,7 +64,6 @@ describe("TauriTicketGateway invoke payloads", () => { title: "Legacy row", status: "open", priority: "medium", - createdBy: { kind: "user" }, updatedAt: 1, }, ], @@ -73,6 +72,7 @@ describe("TauriTicketGateway invoke payloads", () => { const list = await new TauriTicketGateway().list("proj-1"); expect(list.items[0]?.assignedAgentIds).toEqual([]); + expect(list.items[0]?.createdBy).toEqual({ kind: "user" }); }); it("wraps ticket attachment commands in the request DTO", async () => { @@ -118,7 +118,7 @@ describe("TauriTicketGateway invoke payloads", () => { ); }); - it("normalizes legacy ticket payloads that omit array fields", async () => { + it("normalizes legacy ticket payloads that omit array fields and actors", async () => { invoke.mockResolvedValueOnce({ id: "issue-1", ref: "#12", @@ -127,8 +127,6 @@ describe("TauriTicketGateway invoke payloads", () => { description: "", status: "open", priority: "medium", - createdBy: { kind: "user" }, - updatedBy: { kind: "user" }, createdAt: 1, updatedAt: 1, version: 1, @@ -139,5 +137,29 @@ describe("TauriTicketGateway invoke payloads", () => { expect(ticket.links).toEqual([]); expect(ticket.assignedAgentIds).toEqual([]); expect(ticket.attachments).toEqual([]); + expect(ticket.createdBy).toEqual({ kind: "user" }); + expect(ticket.updatedBy).toEqual({ kind: "user" }); + }); + + it("normalizes malformed ticket actors", async () => { + invoke.mockResolvedValueOnce({ + id: "issue-1", + ref: "#12", + number: 12, + title: "Legacy", + description: "", + status: "open", + priority: "medium", + createdBy: { kind: "agent" }, + updatedBy: { kind: "unknown" }, + createdAt: 1, + updatedAt: 1, + version: 1, + }); + + const ticket = await new TauriTicketGateway().read("proj-1", "#12", true); + + expect(ticket.createdBy).toEqual({ kind: "user" }); + expect(ticket.updatedBy).toEqual({ kind: "user" }); }); }); diff --git a/frontend/src/adapters/ticket.ts b/frontend/src/adapters/ticket.ts index 459660d..0a0d69d 100644 --- a/frontend/src/adapters/ticket.ts +++ b/frontend/src/adapters/ticket.ts @@ -42,6 +42,18 @@ export function isTicketVersionConflict(error: unknown): boolean { return typeof message === "string" && message.includes("version conflict"); } +const LEGACY_TICKET_ACTOR: Ticket["createdBy"] = { kind: "user" }; + +function normalizeTicketActor(actor: unknown): Ticket["createdBy"] { + if (!actor || typeof actor !== "object") return LEGACY_TICKET_ACTOR; + const rec = actor as Record; + if (rec.kind === "user" || rec.kind === "system") return { kind: rec.kind }; + if (rec.kind === "agent" && typeof rec.agentId === "string" && rec.agentId) { + return { kind: "agent", agentId: rec.agentId }; + } + return LEGACY_TICKET_ACTOR; +} + function normalizeTicket(ticket: Ticket): Ticket { return { ...ticket, @@ -50,6 +62,8 @@ function normalizeTicket(ticket: Ticket): Ticket { ? ticket.assignedAgentIds : [], attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [], + createdBy: normalizeTicketActor(ticket.createdBy), + updatedBy: normalizeTicketActor(ticket.updatedBy), }; } @@ -62,6 +76,7 @@ function normalizeTicketList(list: TicketList): TicketList { assignedAgentIds: Array.isArray(item.assignedAgentIds) ? item.assignedAgentIds : [], + createdBy: normalizeTicketActor(item.createdBy), })) : [], };