merge fix/ticket-list-legacy-payload-normalization dans develop (fenêtre liste tickets noircissant sur payload legacy)
QA vert : npx vitest run src/adapters/ticket.test.ts, 6/6 sur 4280c70.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -286,20 +286,53 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
private readonly ws: WsLiveClient,
|
private readonly ws: WsLiveClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private normalizeTicket(ticket: Ticket): Ticket {
|
||||||
|
return {
|
||||||
|
...ticket,
|
||||||
|
links: Array.isArray(ticket.links) ? ticket.links : [],
|
||||||
|
assignedAgentIds: Array.isArray(ticket.assignedAgentIds)
|
||||||
|
? ticket.assignedAgentIds
|
||||||
|
: [],
|
||||||
|
attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeTicketList(list: TicketList): TicketList {
|
||||||
|
return {
|
||||||
|
...list,
|
||||||
|
items: Array.isArray(list.items)
|
||||||
|
? list.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
assignedAgentIds: Array.isArray(item.assignedAgentIds)
|
||||||
|
? item.assignedAgentIds
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
|
create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_create", { request: { projectId, ...input } });
|
return this.http
|
||||||
|
.invoke<Ticket>("ticket_create", { request: { projectId, ...input } })
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
read(projectId: string, ref: string, includeCarnet = false): Promise<Ticket> {
|
read(projectId: string, ref: string, includeCarnet = false): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_read", { request: { projectId, ref, includeCarnet } });
|
return this.http
|
||||||
|
.invoke<Ticket>("ticket_read", { request: { projectId, ref, includeCarnet } })
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
|
list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
|
||||||
const { statuses, priorities, ...rest } = query ?? {};
|
const { statuses, priorities, ...rest } = query ?? {};
|
||||||
return this.http.invoke<TicketList>("ticket_list", {
|
return this.http
|
||||||
request: { projectId, statuses: statuses ?? [], priorities: priorities ?? [], ...rest },
|
.invoke<TicketList>("ticket_list", {
|
||||||
});
|
request: { projectId, statuses: statuses ?? [], priorities: priorities ?? [], ...rest },
|
||||||
|
})
|
||||||
|
.then((list) => this.normalizeTicketList(list));
|
||||||
}
|
}
|
||||||
update(projectId: string, ref: string, input: UpdateTicketInput): Promise<Ticket> {
|
update(projectId: string, ref: string, input: UpdateTicketInput): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_update", { request: { projectId, ref, ...input } });
|
return this.http
|
||||||
|
.invoke<Ticket>("ticket_update", { request: { projectId, ref, ...input } })
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
async delete(projectId: string, ref: string): Promise<void> {
|
async delete(projectId: string, ref: string): Promise<void> {
|
||||||
await this.http.invoke<void>("ticket_delete", { request: { projectId, ref } });
|
await this.http.invoke<void>("ticket_delete", { request: { projectId, ref } });
|
||||||
@ -323,9 +356,11 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
return this.http.invoke<TicketCarnet>("ticket_read_carnet", { request: { projectId, ref } });
|
return this.http.invoke<TicketCarnet>("ticket_read_carnet", { request: { projectId, ref } });
|
||||||
}
|
}
|
||||||
updateCarnet(projectId: string, ref: string, carnet: string, expectedVersion: number): Promise<Ticket> {
|
updateCarnet(projectId: string, ref: string, carnet: string, expectedVersion: number): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_update_carnet", {
|
return this.http
|
||||||
request: { projectId, ref, carnet, expectedVersion },
|
.invoke<Ticket>("ticket_update_carnet", {
|
||||||
});
|
request: { projectId, ref, carnet, expectedVersion },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
addAttachment(
|
addAttachment(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -334,9 +369,11 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
mime?: string | null,
|
mime?: string | null,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_attachment_add", {
|
return this.http
|
||||||
request: { projectId, ref, path, expectedVersion, mime },
|
.invoke<Ticket>("ticket_attachment_add", {
|
||||||
});
|
request: { projectId, ref, path, expectedVersion, mime },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
readAttachment(
|
readAttachment(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -353,9 +390,11 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
attachmentId: string,
|
attachmentId: string,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_attachment_mark_summarized", {
|
return this.http
|
||||||
request: { projectId, ref, attachmentId, expectedVersion },
|
.invoke<Ticket>("ticket_attachment_mark_summarized", {
|
||||||
});
|
request: { projectId, ref, attachmentId, expectedVersion },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
link(
|
link(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -364,9 +403,11 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
kind: TicketLinkKind,
|
kind: TicketLinkKind,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_link", {
|
return this.http
|
||||||
request: { projectId, ref, targetRef, kind, expectedVersion },
|
.invoke<Ticket>("ticket_link", {
|
||||||
});
|
request: { projectId, ref, targetRef, kind, expectedVersion },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
unlink(
|
unlink(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -375,9 +416,11 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
kind?: TicketLinkKind,
|
kind?: TicketLinkKind,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_unlink", {
|
return this.http
|
||||||
request: { projectId, ref, targetRef, expectedVersion, kind },
|
.invoke<Ticket>("ticket_unlink", {
|
||||||
});
|
request: { projectId, ref, targetRef, expectedVersion, kind },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
assign(
|
assign(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -386,9 +429,11 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
assigned: boolean,
|
assigned: boolean,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return this.http.invoke<Ticket>("ticket_assign", {
|
return this.http
|
||||||
request: { projectId, ref, agentId, assigned, expectedVersion },
|
.invoke<Ticket>("ticket_assign", {
|
||||||
});
|
request: { projectId, ref, agentId, assigned, expectedVersion },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
async listSprints(projectId: string): Promise<Sprint[]> {
|
async listSprints(projectId: string): Promise<Sprint[]> {
|
||||||
const list = await this.http.invoke<{ items: Sprint[] }>("sprint_list", { request: { projectId } });
|
const list = await this.http.invoke<{ items: Sprint[] }>("sprint_list", { request: { projectId } });
|
||||||
@ -401,11 +446,17 @@ export class HttpTicketGateway implements TicketGateway {
|
|||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
if (sprintId === null) {
|
if (sprintId === null) {
|
||||||
return this.http.invoke<Ticket>("ticket_unassign_sprint", { request: { projectId, ref, expectedVersion } });
|
return this.http
|
||||||
|
.invoke<Ticket>("ticket_unassign_sprint", {
|
||||||
|
request: { projectId, ref, expectedVersion },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
return this.http.invoke<Ticket>("ticket_assign_sprint", {
|
return this.http
|
||||||
request: { projectId, ref, sprintId, expectedVersion },
|
.invoke<Ticket>("ticket_assign_sprint", {
|
||||||
});
|
request: { projectId, ref, sprintId, expectedVersion },
|
||||||
|
})
|
||||||
|
.then((ticket) => this.normalizeTicket(ticket));
|
||||||
}
|
}
|
||||||
createSprint(projectId: string, name: string): Promise<Sprint> {
|
createSprint(projectId: string, name: string): Promise<Sprint> {
|
||||||
return this.http.invoke<Sprint>("sprint_create", { request: { projectId, name } });
|
return this.http.invoke<Sprint>("sprint_create", { request: { projectId, name } });
|
||||||
|
|||||||
@ -55,6 +55,26 @@ describe("TauriTicketGateway invoke payloads", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes legacy ticket list rows that omit assignedAgentIds", async () => {
|
||||||
|
invoke.mockResolvedValueOnce({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
ref: "#12",
|
||||||
|
path: ".ideai/tickets/12/issue.md",
|
||||||
|
title: "Legacy row",
|
||||||
|
status: "open",
|
||||||
|
priority: "medium",
|
||||||
|
createdBy: { kind: "user" },
|
||||||
|
updatedAt: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = await new TauriTicketGateway().list("proj-1");
|
||||||
|
|
||||||
|
expect(list.items[0]?.assignedAgentIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("wraps ticket attachment commands in the request DTO", async () => {
|
it("wraps ticket attachment commands in the request DTO", async () => {
|
||||||
const gateway = new TauriTicketGateway();
|
const gateway = new TauriTicketGateway();
|
||||||
|
|
||||||
@ -97,4 +117,27 @@ describe("TauriTicketGateway invoke payloads", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes legacy ticket payloads that omit array fields", async () => {
|
||||||
|
invoke.mockResolvedValueOnce({
|
||||||
|
id: "issue-1",
|
||||||
|
ref: "#12",
|
||||||
|
number: 12,
|
||||||
|
title: "Legacy",
|
||||||
|
description: "",
|
||||||
|
status: "open",
|
||||||
|
priority: "medium",
|
||||||
|
createdBy: { kind: "user" },
|
||||||
|
updatedBy: { kind: "user" },
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ticket = await new TauriTicketGateway().read("proj-1", "#12", true);
|
||||||
|
|
||||||
|
expect(ticket.links).toEqual([]);
|
||||||
|
expect(ticket.assignedAgentIds).toEqual([]);
|
||||||
|
expect(ticket.attachments).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -42,11 +42,36 @@ export function isTicketVersionConflict(error: unknown): boolean {
|
|||||||
return typeof message === "string" && message.includes("version conflict");
|
return typeof message === "string" && message.includes("version conflict");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTicket(ticket: Ticket): Ticket {
|
||||||
|
return {
|
||||||
|
...ticket,
|
||||||
|
links: Array.isArray(ticket.links) ? ticket.links : [],
|
||||||
|
assignedAgentIds: Array.isArray(ticket.assignedAgentIds)
|
||||||
|
? ticket.assignedAgentIds
|
||||||
|
: [],
|
||||||
|
attachments: Array.isArray(ticket.attachments) ? ticket.attachments : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTicketList(list: TicketList): TicketList {
|
||||||
|
return {
|
||||||
|
...list,
|
||||||
|
items: Array.isArray(list.items)
|
||||||
|
? list.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
assignedAgentIds: Array.isArray(item.assignedAgentIds)
|
||||||
|
? item.assignedAgentIds
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class TauriTicketGateway implements TicketGateway {
|
export class TauriTicketGateway implements TicketGateway {
|
||||||
async create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
|
async create(projectId: string, input: CreateTicketInput): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_create", {
|
return normalizeTicket(await invoke<Ticket>("ticket_create", {
|
||||||
request: { projectId, ...input },
|
request: { projectId, ...input },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async read(
|
async read(
|
||||||
@ -54,23 +79,23 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
ref: string,
|
ref: string,
|
||||||
includeCarnet = false,
|
includeCarnet = false,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_read", {
|
return normalizeTicket(await invoke<Ticket>("ticket_read", {
|
||||||
request: { projectId, ref, includeCarnet },
|
request: { projectId, ref, includeCarnet },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
|
async list(projectId: string, query?: TicketListQuery): Promise<TicketList> {
|
||||||
const { statuses, priorities, ...rest } = query ?? {};
|
const { statuses, priorities, ...rest } = query ?? {};
|
||||||
// Multi-select facets (ticket #12): the backend expects `statuses`/
|
// Multi-select facets (ticket #12): the backend expects `statuses`/
|
||||||
// `priorities` arrays (empty ⇒ no constraint on that facet).
|
// `priorities` arrays (empty ⇒ no constraint on that facet).
|
||||||
return invoke<TicketList>("ticket_list", {
|
return normalizeTicketList(await invoke<TicketList>("ticket_list", {
|
||||||
request: {
|
request: {
|
||||||
projectId,
|
projectId,
|
||||||
statuses: statuses ?? [],
|
statuses: statuses ?? [],
|
||||||
priorities: priorities ?? [],
|
priorities: priorities ?? [],
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
@ -78,9 +103,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
ref: string,
|
ref: string,
|
||||||
input: UpdateTicketInput,
|
input: UpdateTicketInput,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_update", {
|
return normalizeTicket(await invoke<Ticket>("ticket_update", {
|
||||||
request: { projectId, ref, ...input },
|
request: { projectId, ref, ...input },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(projectId: string, ref: string): Promise<void> {
|
async delete(projectId: string, ref: string): Promise<void> {
|
||||||
@ -130,9 +155,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
carnet: string,
|
carnet: string,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_update_carnet", {
|
return normalizeTicket(await invoke<Ticket>("ticket_update_carnet", {
|
||||||
request: { projectId, ref, carnet, expectedVersion },
|
request: { projectId, ref, carnet, expectedVersion },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAttachment(
|
async addAttachment(
|
||||||
@ -142,9 +167,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
mime?: string | null,
|
mime?: string | null,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_attachment_add", {
|
return normalizeTicket(await invoke<Ticket>("ticket_attachment_add", {
|
||||||
request: { projectId, ref, path, expectedVersion, mime },
|
request: { projectId, ref, path, expectedVersion, mime },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async readAttachment(
|
async readAttachment(
|
||||||
@ -163,9 +188,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
attachmentId: string,
|
attachmentId: string,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_attachment_mark_summarized", {
|
return normalizeTicket(await invoke<Ticket>("ticket_attachment_mark_summarized", {
|
||||||
request: { projectId, ref, attachmentId, expectedVersion },
|
request: { projectId, ref, attachmentId, expectedVersion },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async link(
|
async link(
|
||||||
@ -175,9 +200,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
kind: TicketLinkKind,
|
kind: TicketLinkKind,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_link", {
|
return normalizeTicket(await invoke<Ticket>("ticket_link", {
|
||||||
request: { projectId, ref, targetRef, kind, expectedVersion },
|
request: { projectId, ref, targetRef, kind, expectedVersion },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlink(
|
async unlink(
|
||||||
@ -187,9 +212,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
kind?: TicketLinkKind,
|
kind?: TicketLinkKind,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_unlink", {
|
return normalizeTicket(await invoke<Ticket>("ticket_unlink", {
|
||||||
request: { projectId, ref, targetRef, expectedVersion, kind },
|
request: { projectId, ref, targetRef, expectedVersion, kind },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async assign(
|
async assign(
|
||||||
@ -199,9 +224,9 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
assigned: boolean,
|
assigned: boolean,
|
||||||
expectedVersion: number,
|
expectedVersion: number,
|
||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
return invoke<Ticket>("ticket_assign", {
|
return normalizeTicket(await invoke<Ticket>("ticket_assign", {
|
||||||
request: { projectId, ref, agentId, assigned, expectedVersion },
|
request: { projectId, ref, agentId, assigned, expectedVersion },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async listSprints(projectId: string): Promise<Sprint[]> {
|
async listSprints(projectId: string): Promise<Sprint[]> {
|
||||||
@ -220,13 +245,13 @@ export class TauriTicketGateway implements TicketGateway {
|
|||||||
): Promise<Ticket> {
|
): Promise<Ticket> {
|
||||||
// Two backend commands: assign to a sprint, or unassign (clear membership).
|
// Two backend commands: assign to a sprint, or unassign (clear membership).
|
||||||
if (sprintId === null) {
|
if (sprintId === null) {
|
||||||
return invoke<Ticket>("ticket_unassign_sprint", {
|
return normalizeTicket(await invoke<Ticket>("ticket_unassign_sprint", {
|
||||||
request: { projectId, ref, expectedVersion },
|
request: { projectId, ref, expectedVersion },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
return invoke<Ticket>("ticket_assign_sprint", {
|
return normalizeTicket(await invoke<Ticket>("ticket_assign_sprint", {
|
||||||
request: { projectId, ref, sprintId, expectedVersion },
|
request: { projectId, ref, sprintId, expectedVersion },
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSprint(projectId: string, name: string): Promise<Sprint> {
|
async createSprint(projectId: string, name: string): Promise<Sprint> {
|
||||||
|
|||||||
Reference in New Issue
Block a user