merge fix/tickets-createdby-legacy-normalization dans develop (fenêtre tickets noircissant sur createdBy legacy)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 14:19:54 +02:00
4 changed files with 137 additions and 5 deletions

View File

@ -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<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 {
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),
}))
: [],
};

View 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" });
});
});

View File

@ -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" });
});
});

View File

@ -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<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 {
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),
}))
: [],
};