multi-collection-filter.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * Unit tests for multi-collection filter logic (PR #191).
  3. *
  4. * Tests the filterByCollections post-filter and the resolveCollectionFilter
  5. * behavior for single-collection vs multi-collection search.
  6. */
  7. import { describe, test, expect } from "vitest";
  8. import { parseArgs } from "node:util";
  9. // Reproduce the filterByCollections logic from qmd.ts for testing
  10. // (the function is private in qmd.ts)
  11. function filterByCollections<T extends { filepath?: string; file?: string }>(
  12. results: T[],
  13. collectionNames: string[],
  14. ): T[] {
  15. if (collectionNames.length <= 1) return results;
  16. const prefixes = collectionNames.map((n) => `qmd://${n}/`);
  17. return results.filter((r) => {
  18. const path = r.filepath || r.file || "";
  19. return prefixes.some((p) => path.startsWith(p));
  20. });
  21. }
  22. describe("filterByCollections", () => {
  23. const results = [
  24. { filepath: "qmd://docs/readme.md", file: "qmd://docs/readme.md" },
  25. { filepath: "qmd://notes/todo.md", file: "qmd://notes/todo.md" },
  26. { filepath: "qmd://journals/2024/jan.md", file: "qmd://journals/2024/jan.md" },
  27. { filepath: "qmd://docs/api.md", file: "qmd://docs/api.md" },
  28. ];
  29. test("returns all results when no collections specified", () => {
  30. expect(filterByCollections(results, [])).toEqual(results);
  31. });
  32. test("returns all results for single collection (no-op, handled by SQL filter)", () => {
  33. expect(filterByCollections(results, ["docs"])).toEqual(results);
  34. });
  35. test("filters to matching collections when multiple specified", () => {
  36. const filtered = filterByCollections(results, ["docs", "journals"]);
  37. expect(filtered).toHaveLength(3);
  38. expect(filtered.map((r) => r.filepath)).toEqual([
  39. "qmd://docs/readme.md",
  40. "qmd://journals/2024/jan.md",
  41. "qmd://docs/api.md",
  42. ]);
  43. });
  44. test("filters correctly with two collections", () => {
  45. const filtered = filterByCollections(results, ["notes", "journals"]);
  46. expect(filtered).toHaveLength(2);
  47. expect(filtered.map((r) => r.filepath)).toEqual([
  48. "qmd://notes/todo.md",
  49. "qmd://journals/2024/jan.md",
  50. ]);
  51. });
  52. test("returns empty when no results match collections", () => {
  53. const filtered = filterByCollections(results, ["archive", "trash"]);
  54. expect(filtered).toHaveLength(0);
  55. });
  56. test("uses file field when filepath is missing", () => {
  57. const fileOnlyResults = [
  58. { file: "qmd://docs/readme.md" },
  59. { file: "qmd://notes/todo.md" },
  60. ];
  61. const filtered = filterByCollections(fileOnlyResults, ["docs", "notes"]);
  62. expect(filtered).toHaveLength(2);
  63. });
  64. test("uses filepath over file when both present", () => {
  65. const mixedResults = [
  66. { filepath: "qmd://docs/readme.md", file: "qmd://notes/todo.md" },
  67. ];
  68. const filtered = filterByCollections(mixedResults, ["docs", "notes"]);
  69. expect(filtered).toHaveLength(1);
  70. // Should match via filepath (docs), not file (notes)
  71. expect(filtered[0].filepath).toBe("qmd://docs/readme.md");
  72. });
  73. });
  74. describe("resolveCollectionFilter input normalization", () => {
  75. // Test the array normalization logic without the DB dependency
  76. function normalizeCollectionInput(raw: string | string[] | undefined): string[] {
  77. if (!raw) return [];
  78. return Array.isArray(raw) ? raw : [raw];
  79. }
  80. test("undefined returns empty array", () => {
  81. expect(normalizeCollectionInput(undefined)).toEqual([]);
  82. });
  83. test("single string returns single-element array", () => {
  84. expect(normalizeCollectionInput("docs")).toEqual(["docs"]);
  85. });
  86. test("array passes through", () => {
  87. expect(normalizeCollectionInput(["docs", "notes"])).toEqual(["docs", "notes"]);
  88. });
  89. test("empty string returns single-element array", () => {
  90. expect(normalizeCollectionInput("")).toEqual([]);
  91. });
  92. });
  93. describe("collection option type from parseArgs", () => {
  94. // Verify that parseArgs with `multiple: true` produces string[]
  95. test("parseArgs multiple:true produces array for repeated flags", () => {
  96. const { values } = parseArgs({
  97. args: ["-c", "docs", "-c", "notes"],
  98. options: {
  99. collection: { type: "string", short: "c", multiple: true },
  100. },
  101. strict: true,
  102. });
  103. expect(values.collection).toEqual(["docs", "notes"]);
  104. });
  105. test("parseArgs multiple:true produces array for single flag", () => {
  106. const { values } = parseArgs({
  107. args: ["-c", "docs"],
  108. options: {
  109. collection: { type: "string", short: "c", multiple: true },
  110. },
  111. strict: true,
  112. });
  113. expect(values.collection).toEqual(["docs"]);
  114. });
  115. test("parseArgs multiple:true produces undefined when flag absent", () => {
  116. const { values } = parseArgs({
  117. args: [],
  118. options: {
  119. collection: { type: "string", short: "c", multiple: true },
  120. },
  121. strict: true,
  122. });
  123. expect(values.collection).toBeUndefined();
  124. });
  125. });