multi-collection-filter.test.ts 4.9 KB

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