Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions src/qmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1783,11 +1783,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;
}

Expand Down Expand Up @@ -2029,11 +2056,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);
Expand Down Expand Up @@ -2088,11 +2111,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;
}

Expand Down Expand Up @@ -2205,11 +2224,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;
}

Expand Down
58 changes: 58 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down