Files
Bohan Jiang a9e82c7973 MUL-7282: fix(comments): delete only the selected comment and keep its replies (#8323)
Deleting a comment removed every reply under it, through the parent_id ON DELETE CASCADE. Now delete removes only that comment: one with replies becomes a tombstone (deleted_at set, body/attachments/reactions/resolution cleared) so the replies keep their parent, and one without replies is removed along with any tombstone ancestor left empty.

Tombstones are read-only and are never agent input. Every write to a comment's children takes its owners first (issue -> comment -> child), and a comment commits together with the attachments it was posted with. Clients only promise that replies are kept when the server declares comment_delete_keep_replies_supported, and then delete through DELETE /api/comments/{id}/keep-replies, which older servers do not route.
2026-09-13 19:13:43 +08:00

41 lines
1.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "./api";
// api.ts refuses to load without a base URL; set it before the import runs.
vi.hoisted(() => {
process.env.EXPO_PUBLIC_API_URL = "https://api.example.test";
});
// The real store pulls in expo-secure-store; the client only needs the slug.
vi.mock("@/data/workspace-store", () => ({ getCurrentSlug: () => null }));
describe("api.deleteComment", () => {
const fetchMock = vi.fn(async () => new Response(null, { status: 204 }));
beforeEach(() => {
fetchMock.mockClear();
vi.stubGlobal("fetch", fetchMock);
vi.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
// #8296: servers that keep a deleted comment's replies also route
// /keep-replies; older servers do not, so a keep-replies delete that reaches
// one fails instead of deleting the replies too.
it.each([
[{ keepReplies: true }, "https://api.example.test/api/comments/comment-1/keep-replies"],
[{ keepReplies: false }, "https://api.example.test/api/comments/comment-1"],
[undefined, "https://api.example.test/api/comments/comment-1"],
])("with %j sends DELETE %s", async (opts, url) => {
await api.deleteComment("comment-1", opts);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(url, expect.objectContaining({ method: "DELETE" }));
});
});