diff --git a/crates/app-tauri/tests/tickets_missing_carnet.rs b/crates/app-tauri/tests/tickets_missing_carnet.rs index d90a16d..985eeb9 100644 --- a/crates/app-tauri/tests/tickets_missing_carnet.rs +++ b/crates/app-tauri/tests/tickets_missing_carnet.rs @@ -96,3 +96,78 @@ async fn idea_ticket_read_is_best_effort_but_read_carnet_stays_strict_when_carne .expect_err("strict carnet read must fail when carnet.md is missing"); assert_eq!(strict_err.code, "notFound"); } + +#[tokio::test] +async fn idea_ticket_bulk_delete_deletes_each_ref_and_reports_partial_errors() { + let state = AppState::build(temp_path("appdata")); + let project_root = temp_path("project").to_string_lossy().into_owned(); + let project = state + .create_project + .execute(CreateProjectInput { + name: "App Tauri Bulk Delete".to_owned(), + root: project_root, + remote: None, + default_profile_id: None, + }) + .await + .expect("test project is created") + .project; + + let first = state + .ticket_tool_binder + .handle_ticket_tool( + &project, + "qa-agent", + "idea_ticket_create", + json!({ "title": "Bulk delete A" }), + ) + .await + .expect("first ticket create succeeds"); + let second = state + .ticket_tool_binder + .handle_ticket_tool( + &project, + "qa-agent", + "idea_ticket_create", + json!({ "title": "Bulk delete B" }), + ) + .await + .expect("second ticket create succeeds"); + let first_ref = first["ref"].as_str().expect("first ref").to_owned(); + let second_ref = second["ref"].as_str().expect("second ref").to_owned(); + + let out = state + .ticket_tool_binder + .handle_ticket_tool( + &project, + "qa-agent", + "idea_ticket_bulk_delete", + json!({ "refs": [first_ref.clone(), "#999", second_ref.clone()] }), + ) + .await + .expect("bulk delete dispatch succeeds"); + + let items = out["items"].as_array().expect("bulk items"); + assert_eq!(items.len(), 3); + assert_eq!(items[0]["ref"], first_ref); + assert_eq!(items[0]["ok"], true); + assert_eq!(items[1]["ref"], "#999"); + assert_eq!(items[1]["ok"], false); + assert_eq!(items[1]["error"]["code"], "NOT_FOUND"); + assert_eq!(items[2]["ref"], second_ref); + assert_eq!(items[2]["ok"], true); + + for deleted_ref in [first_ref, second_ref] { + let err = state + .ticket_tool_binder + .handle_ticket_tool( + &project, + "qa-agent", + "idea_ticket_read", + json!({ "ref": deleted_ref }), + ) + .await + .expect_err("deleted ticket read must fail"); + assert_eq!(err.code, "notFound"); + } +} diff --git a/frontend/src/adapters/ticket.test.ts b/frontend/src/adapters/ticket.test.ts new file mode 100644 index 0000000..874cc38 --- /dev/null +++ b/frontend/src/adapters/ticket.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const invoke = vi.fn(); +vi.mock("@tauri-apps/api/core", () => ({ + invoke: (...args: unknown[]) => invoke(...args), + Channel: class { + onmessage: ((chunk: unknown) => void) | null = null; + }, +})); + +import { TauriTicketGateway } from "./ticket"; + +describe("TauriTicketGateway invoke payloads", () => { + beforeEach(() => invoke.mockReset().mockResolvedValue({ items: [] })); + + it("bulk status wraps refs and status in the request DTO", async () => { + await new TauriTicketGateway().bulkUpdateStatus( + "proj-1", + ["#1", "#2"], + "inProgress", + ); + + expect(invoke).toHaveBeenCalledWith("ticket_bulk_update_status", { + request: { + projectId: "proj-1", + refs: ["#1", "#2"], + status: "inProgress", + }, + }); + }); + + it("bulk delete wraps refs in the request DTO", async () => { + await new TauriTicketGateway().bulkDelete("proj-1", ["#3", "#4"]); + + expect(invoke).toHaveBeenCalledWith("ticket_bulk_delete", { + request: { + projectId: "proj-1", + refs: ["#3", "#4"], + }, + }); + }); +});