Files
multica/packages/views/issues/components/thread-utils.test.ts
Jiayuan Zhang 8f92b5fdeb feat(search): add fold/unfold all comments commands to the command palette (#5417)
On an issue page, Cmd+K now offers Fold All Comments / Unfold All
Comments. Folding collapses every thread card via the persisted
comment-collapse store; unfolding also expands resolved threads, whose
session-only expand state moves from issue-detail useState into a new
core resolved-expand store so the palette can drive it (MUL-4763).

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-15 11:21:54 +08:00

130 lines
4.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TimelineEntry } from "@multica/core/types";
import {
collectThreadReplies,
resolvedThreadRootIds,
rootCommentIds,
} from "./thread-utils";
function comment(id: string, createdAt: string, parentId: string | null): TimelineEntry {
return {
type: "comment",
id,
actor_type: "member",
actor_id: "user-1",
content: id,
parent_id: parentId,
created_at: createdAt,
updated_at: createdAt,
comment_type: "comment",
} as TimelineEntry;
}
function bucketByParent(entries: TimelineEntry[]): Map<string, TimelineEntry[]> {
const map = new Map<string, TimelineEntry[]>();
for (const e of entries) {
if (!e.parent_id) continue;
const list = map.get(e.parent_id) ?? [];
list.push(e);
map.set(e.parent_id, list);
}
return map;
}
describe("collectThreadReplies", () => {
it("orders a late nested reply after earlier sibling replies (#3691)", () => {
// R1 (50m ago) triggered a slow agent; R2 (30m) and R3 (10m) arrived while
// it ran; D (3m ago) is the agent's reply, forced to nest under R1. A
// depth-first walk yields R1-D-R2-R3; the thread must read R1-R2-R3-D.
const r1 = comment("r1", "2026-06-11T10:00:00Z", "root");
const r2 = comment("r2", "2026-06-11T10:20:00Z", "root");
const r3 = comment("r3", "2026-06-11T10:40:00Z", "root");
const d = comment("d", "2026-06-11T10:47:00Z", "r1");
const out = collectThreadReplies("root", bucketByParent([r1, r2, r3, d]));
expect(out.map((e) => e.id)).toEqual(["r1", "r2", "r3", "d"]);
});
it("still returns every descendant across nesting levels", () => {
const r1 = comment("r1", "2026-06-11T10:00:00Z", "root");
const d1 = comment("d1", "2026-06-11T10:05:00Z", "r1");
const d2 = comment("d2", "2026-06-11T10:10:00Z", "d1");
const out = collectThreadReplies("root", bucketByParent([r1, d1, d2]));
expect(out.map((e) => e.id)).toEqual(["r1", "d1", "d2"]);
});
it("breaks created_at ties by id so the order is deterministic", () => {
const b = comment("b", "2026-06-11T10:00:00Z", "root");
const a = comment("a", "2026-06-11T10:00:00Z", "b");
const out = collectThreadReplies("root", bucketByParent([b, a]));
expect(out.map((e) => e.id)).toEqual(["a", "b"]);
});
});
function activity(id: string, createdAt: string): TimelineEntry {
return {
type: "activity",
id,
actor_type: "member",
actor_id: "user-1",
action: "status_changed",
created_at: createdAt,
} as TimelineEntry;
}
describe("rootCommentIds", () => {
it("returns top-level comments only, skipping replies and activities", () => {
const entries = [
activity("act-1", "2026-06-11T09:00:00Z"),
comment("root-1", "2026-06-11T10:00:00Z", null),
comment("reply-1", "2026-06-11T10:05:00Z", "root-1"),
comment("root-2", "2026-06-11T11:00:00Z", null),
];
expect(rootCommentIds(entries)).toEqual(["root-1", "root-2"]);
});
});
describe("resolvedThreadRootIds", () => {
it("includes root-resolved and reply-resolved threads, excludes unresolved", () => {
const rootResolved = {
...comment("root-resolved", "2026-06-11T10:00:00Z", null),
resolved_at: "2026-06-11T12:00:00Z",
};
const replyResolvedRoot = comment("root-reply-resolved", "2026-06-11T10:10:00Z", null);
const resolutionReply = {
...comment("reply-resolution", "2026-06-11T10:20:00Z", "root-reply-resolved"),
resolved_at: "2026-06-11T12:30:00Z",
};
const openRoot = comment("root-open", "2026-06-11T10:30:00Z", null);
const openReply = comment("reply-open", "2026-06-11T10:40:00Z", "root-open");
const ids = resolvedThreadRootIds([
activity("act-1", "2026-06-11T09:00:00Z"),
rootResolved,
replyResolvedRoot,
resolutionReply,
openRoot,
openReply,
]);
expect(ids).toEqual(["root-resolved", "root-reply-resolved"]);
});
it("detects a resolution on a nested reply", () => {
const root = comment("root", "2026-06-11T10:00:00Z", null);
const reply = comment("reply", "2026-06-11T10:05:00Z", "root");
const nested = {
...comment("nested", "2026-06-11T10:10:00Z", "reply"),
resolved_at: "2026-06-11T12:00:00Z",
};
expect(resolvedThreadRootIds([root, reply, nested])).toEqual(["root"]);
});
});