multica/apps/web/features/issues/utils/search.test.ts
2026-04-08 11:30:53 +08:00

134 lines
4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Agent, Issue, MemberWithUser } from "@/shared/types";
import { filterIssuesBySearch, getSearchConstrainedStatuses, parseIssueSearch } from "./search";
function makeIssue(overrides: Partial<Issue> = {}): Issue {
return {
id: "i-1",
workspace_id: "ws-1",
number: 1,
identifier: "MUL-1",
title: "Test issue",
description: null,
status: "todo",
priority: "medium",
assignee_type: null,
assignee_id: null,
creator_type: "member",
creator_id: "u-1",
parent_issue_id: null,
position: 0,
due_date: null,
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-01T00:00:00Z",
...overrides,
};
}
const members: Pick<MemberWithUser, "user_id" | "name">[] = [
{ user_id: "u-1", name: "Alice Chen" },
{ user_id: "u-2", name: "Bob Wong" },
];
const agents: Pick<Agent, "id" | "name">[] = [
{ id: "a-1", name: "Fixer Bot" },
{ id: "a-2", name: "Review Agent" },
];
const context = {
members,
agents,
now: new Date("2026-04-08T10:00:00Z"),
};
const issues: Issue[] = [
makeIssue({
id: "1",
number: 11,
identifier: "MUL-11",
title: "Fix login redirect loop",
description: "Users bounce back to the sign-in page.",
status: "todo",
priority: "high",
assignee_type: "member",
assignee_id: "u-1",
}),
makeIssue({
id: "2",
number: 12,
identifier: "MUL-12",
title: "Improve issue search",
description: "Search title, description, and actor names.",
status: "in_progress",
priority: "urgent",
assignee_type: "agent",
assignee_id: "a-1",
creator_type: "agent",
creator_id: "a-2",
due_date: "2026-04-08T12:00:00Z",
}),
makeIssue({
id: "3",
number: 18,
identifier: "MUL-18",
title: "Archive old tickets",
description: "",
status: "done",
priority: "low",
creator_id: "u-2",
due_date: "2026-04-07T09:00:00Z",
}),
];
describe("parseIssueSearch", () => {
it("extracts structured filters and quoted text", () => {
const parsed = parseIssueSearch(
'status:todo priority:high assignee:"Alice" "redirect loop"',
context,
);
expect(parsed.statusFilters).toEqual(["todo"]);
expect(parsed.priorityFilters).toEqual(["high"]);
expect(parsed.assigneeFilters).toEqual([{ type: "member", id: "u-1" }]);
expect(parsed.textTerms).toEqual(["redirect loop"]);
});
it("recognizes issue numbers and lifecycle shortcuts", () => {
const parsed = parseIssueSearch("#18 is:closed", context);
expect(parsed.issueNumber).toBe(18);
expect(parsed.lifecycle).toBe("closed");
expect(getSearchConstrainedStatuses(parsed)).toEqual(["done", "cancelled"]);
});
});
describe("filterIssuesBySearch", () => {
it("matches free text across title, description, identifier, and actor names", () => {
const parsed = parseIssueSearch("Fixer review MUL-12", context);
const result = filterIssuesBySearch(issues, parsed, context);
expect(result.map((issue) => issue.id)).toEqual(["2"]);
});
it("filters by assignee, creator, and explicit status tokens", () => {
const parsed = parseIssueSearch("assignee:Fixer creator:Review status:in-progress", context);
const result = filterIssuesBySearch(issues, parsed, context);
expect(result.map((issue) => issue.id)).toEqual(["2"]);
});
it("filters by due state and description presence", () => {
const todayParsed = parseIssueSearch("due:today has:description", context);
const overdueParsed = parseIssueSearch("due:overdue", context);
expect(filterIssuesBySearch(issues, todayParsed, context).map((issue) => issue.id)).toEqual(["2"]);
expect(filterIssuesBySearch(issues, overdueParsed, context).map((issue) => issue.id)).toEqual(["3"]);
});
it("supports unassigned and closed search flows", () => {
const parsed = parseIssueSearch("is:closed is:unassigned", context);
const result = filterIssuesBySearch(issues, parsed, context);
expect(result.map((issue) => issue.id)).toEqual(["3"]);
});
});