Pārlūkot izejas kodu

Merge pull request #228 from amsminn/fix-empty-results-format

fix(cli): prevent parser breakage on empty results across output formats
Tobias Lütke 2 mēneši atpakaļ
vecāks
revīzija
8bd93366ad
2 mainītis faili ar 89 papildinājumiem un 16 dzēšanām
  1. 31 16
      src/qmd.ts
  2. 58 0
      test/cli.test.ts

+ 31 - 16
src/qmd.ts

@@ -1800,11 +1800,38 @@ function shortPath(dirpath: string): string {
   return dirpath;
 }
 
+type EmptySearchReason = "no_results" | "min_score";
+
+// Emit format-safe empty output for search commands.
+function printEmptySearchResults(format: OutputFormat, reason: EmptySearchReason = "no_results"): void {
+  if (format === "json") {
+    console.log("[]");
+    return;
+  }
+  if (format === "csv") {
+    console.log("docid,score,file,title,context,line,snippet");
+    return;
+  }
+  if (format === "xml") {
+    console.log("<results></results>");
+    return;
+  }
+  if (format === "md" || format === "files") {
+    return;
+  }
+
+  if (reason === "min_score") {
+    console.log("No results found above minimum score threshold.");
+    return;
+  }
+  console.log("No results found.");
+}
+
 function outputResults(results: { file: string; displayPath: string; title: string; body: string; score: number; context?: string | null; chunkPos?: number; hash?: string; docid?: string }[], query: string, opts: OutputOptions): void {
   const filtered = results.filter(r => r.score >= opts.minScore).slice(0, opts.limit);
 
   if (filtered.length === 0) {
-    console.log("No results found above minimum score threshold.");
+    printEmptySearchResults(opts.format, "min_score");
     return;
   }
 
@@ -2046,11 +2073,7 @@ function search(query: string, opts: OutputOptions): void {
   closeDb();
 
   if (resultsWithContext.length === 0) {
-    if (opts.format === "json") {
-      console.log("[]");
-    } else {
-      console.log("No results found.");
-    }
+    printEmptySearchResults(opts.format);
     return;
   }
   outputResults(resultsWithContext, query, opts);
@@ -2105,11 +2128,7 @@ async function vectorSearch(query: string, opts: OutputOptions, _model: string =
     closeDb();
 
     if (results.length === 0) {
-      if (opts.format === "json") {
-        console.log("[]");
-      } else {
-        console.log("No results found.");
-      }
+      printEmptySearchResults(opts.format);
       return;
     }
 
@@ -2222,11 +2241,7 @@ async function querySearch(query: string, opts: OutputOptions, _embedModel: stri
     closeDb();
 
     if (results.length === 0) {
-      if (opts.format === "json") {
-        console.log("[]");
-      } else {
-        console.log("No results found.");
-      }
+      printEmptySearchResults(opts.format);
       return;
     }
 

+ 58 - 0
test/cli.test.ts

@@ -314,6 +314,64 @@ describe("CLI Search Command", () => {
     expect(stdout).toContain("No results");
   });
 
+  test("returns empty JSON array for non-matching query with --json", async () => {
+    const { stdout, exitCode } = await runQmd(["search", "xyznonexistent123", "--json"]);
+    expect(exitCode).toBe(0);
+    expect(JSON.parse(stdout)).toEqual([]);
+  });
+
+  test("returns CSV header only for non-matching query with --csv", async () => {
+    const { stdout, exitCode } = await runQmd(["search", "xyznonexistent123", "--csv"]);
+    expect(exitCode).toBe(0);
+    expect(stdout.trim()).toBe("docid,score,file,title,context,line,snippet");
+  });
+
+  test("returns empty XML container for non-matching query with --xml", async () => {
+    const { stdout, exitCode } = await runQmd(["search", "xyznonexistent123", "--xml"]);
+    expect(exitCode).toBe(0);
+    expect(stdout.trim()).toBe("<results></results>");
+  });
+
+  test("returns empty output for non-matching query with --md", async () => {
+    const { stdout, exitCode } = await runQmd(["search", "xyznonexistent123", "--md"]);
+    expect(exitCode).toBe(0);
+    expect(stdout.trim()).toBe("");
+  });
+
+  test("returns empty output for non-matching query with --files", async () => {
+    const { stdout, exitCode } = await runQmd(["search", "xyznonexistent123", "--files"]);
+    expect(exitCode).toBe(0);
+    expect(stdout.trim()).toBe("");
+  });
+
+  test("returns min-score threshold message for default CLI output", async () => {
+    const { stdout, exitCode } = await runQmd(["search", "test", "--min-score", "2"]);
+    expect(exitCode).toBe(0);
+    expect(stdout).toContain("No results found above minimum score threshold.");
+  });
+
+  test("returns format-safe empty output when --min-score filters all results", async () => {
+    const json = await runQmd(["search", "test", "--json", "--min-score", "2"]);
+    expect(json.exitCode).toBe(0);
+    expect(JSON.parse(json.stdout)).toEqual([]);
+
+    const csv = await runQmd(["search", "test", "--csv", "--min-score", "2"]);
+    expect(csv.exitCode).toBe(0);
+    expect(csv.stdout.trim()).toBe("docid,score,file,title,context,line,snippet");
+
+    const xml = await runQmd(["search", "test", "--xml", "--min-score", "2"]);
+    expect(xml.exitCode).toBe(0);
+    expect(xml.stdout.trim()).toBe("<results></results>");
+
+    const md = await runQmd(["search", "test", "--md", "--min-score", "2"]);
+    expect(md.exitCode).toBe(0);
+    expect(md.stdout.trim()).toBe("");
+
+    const files = await runQmd(["search", "test", "--files", "--min-score", "2"]);
+    expect(files.exitCode).toBe(0);
+    expect(files.stdout.trim()).toBe("");
+  });
+
   test("requires query argument", async () => {
     const { stdout, stderr, exitCode } = await runQmd(["search"]);
     expect(exitCode).toBe(1);