fix(tickets): normalise createdBy manquant sur les payloads legacy
Les tickets legacy sans champ createdBy faisaient noircir la fenêtre tickets côté frontend. Les adapters (ticket.ts, streamGateways.ts) normalisent désormais ce champ absent, avec couverture de test dédiée. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -286,6 +286,16 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
private readonly ws: WsLiveClient,
|
private readonly ws: WsLiveClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private normalizeTicketActor(actor: unknown): Ticket["createdBy"] {
|
||||||
|
if (!actor || typeof actor !== "object") return { kind: "user" };
|
||||||
|
const rec = actor as Record<string, unknown>;
|
||||||
|
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 {
|
private normalizeTicket(ticket: Ticket): Ticket {
|
||||||
return {
|
return {
|
||||||
...ticket,
|
...ticket,
|
||||||
@ -294,6 +304,8 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
? ticket.assignedAgentIds
|
? ticket.assignedAgentIds
|
||||||
: [],
|
: [],
|
||||||
attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [],
|
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)
|
assignedAgentIds: Array.isArray(item.assignedAgentIds)
|
||||||
? item.assignedAgentIds
|
? item.assignedAgentIds
|
||||||
: [],
|
: [],
|
||||||
|
createdBy: this.normalizeTicketActor(item.createdBy),
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
|
|||||||
82
frontend/src/adapters/http/ticketGateway.test.ts
Normal file
82
frontend/src/adapters/http/ticketGateway.test.ts
Normal file
@ -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<typeof vi.fn>;
|
||||||
|
} {
|
||||||
|
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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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({
|
invoke.mockResolvedValueOnce({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
@ -64,7 +64,6 @@ describe("TauriTicketGateway invoke payloads", () => {
|
|||||||
title: "Legacy row",
|
title: "Legacy row",
|
||||||
status: "open",
|
status: "open",
|
||||||
priority: "medium",
|
priority: "medium",
|
||||||
createdBy: { kind: "user" },
|
|
||||||
updatedAt: 1,
|
updatedAt: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -73,6 +72,7 @@ describe("TauriTicketGateway invoke payloads", () => {
|
|||||||
const list = await new TauriTicketGateway().list("proj-1");
|
const list = await new TauriTicketGateway().list("proj-1");
|
||||||
|
|
||||||
expect(list.items[0]?.assignedAgentIds).toEqual([]);
|
expect(list.items[0]?.assignedAgentIds).toEqual([]);
|
||||||
|
expect(list.items[0]?.createdBy).toEqual({ kind: "user" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("wraps ticket attachment commands in the request DTO", async () => {
|
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({
|
invoke.mockResolvedValueOnce({
|
||||||
id: "issue-1",
|
id: "issue-1",
|
||||||
ref: "#12",
|
ref: "#12",
|
||||||
@ -127,8 +127,6 @@ describe("TauriTicketGateway invoke payloads", () => {
|
|||||||
description: "",
|
description: "",
|
||||||
status: "open",
|
status: "open",
|
||||||
priority: "medium",
|
priority: "medium",
|
||||||
createdBy: { kind: "user" },
|
|
||||||
updatedBy: { kind: "user" },
|
|
||||||
createdAt: 1,
|
createdAt: 1,
|
||||||
updatedAt: 1,
|
updatedAt: 1,
|
||||||
version: 1,
|
version: 1,
|
||||||
@ -139,5 +137,29 @@ describe("TauriTicketGateway invoke payloads", () => {
|
|||||||
expect(ticket.links).toEqual([]);
|
expect(ticket.links).toEqual([]);
|
||||||
expect(ticket.assignedAgentIds).toEqual([]);
|
expect(ticket.assignedAgentIds).toEqual([]);
|
||||||
expect(ticket.attachments).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" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -42,6 +42,18 @@ export function isTicketVersionConflict(error: unknown): boolean {
|
|||||||
return typeof message === "string" && message.includes("version conflict");
|
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<string, unknown>;
|
||||||
|
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 {
|
function normalizeTicket(ticket: Ticket): Ticket {
|
||||||
return {
|
return {
|
||||||
...ticket,
|
...ticket,
|
||||||
@ -50,6 +62,8 @@ function normalizeTicket(ticket: Ticket): Ticket {
|
|||||||
? ticket.assignedAgentIds
|
? ticket.assignedAgentIds
|
||||||
: [],
|
: [],
|
||||||
attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [],
|
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)
|
assignedAgentIds: Array.isArray(item.assignedAgentIds)
|
||||||
? item.assignedAgentIds
|
? item.assignedAgentIds
|
||||||
: [],
|
: [],
|
||||||
|
createdBy: normalizeTicketActor(item.createdBy),
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user