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
26 changes: 18 additions & 8 deletions src/lib/search-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type ScopeDocumentRow = {
};

type ScopeLabelRow = {
id: string;
document_id: string;
label: string;
label_type: DocumentLabelType;
Expand Down Expand Up @@ -326,15 +327,24 @@ export async function resolveSearchScope(args: {
hasValues(filters.labelTypesAny);
let labelsByDocument = new Map<string, ScopeLabelRow[]>();
if (needsLabels) {
const { data: labelRows, error: labelError } = await args.supabase
.from("document_labels")
.select("document_id,label,label_type")
.in("document_id", candidateIds)
.in("label_type", [...labelTypes]);
if (labelError) throw new Error(labelError.message);
labelsByDocument = new Map();
for (const label of (labelRows ?? []) as ScopeLabelRow[]) {
labelsByDocument.set(label.document_id, [...(labelsByDocument.get(label.document_id) ?? []), label]);
for (let offset = 0; ; offset += documentScopeQueryPageSize) {
let labelQuery = args.supabase
.from("document_labels")
.select("id,document_id,label,label_type")
.in("document_id", candidateIds)
.in("label_type", [...labelTypes])
// `id` is unique, so range pages cannot silently skip or repeat rows
// when PostgREST applies its default 1,000-row response cap.
.order("id", { ascending: true })
.range(offset, offset + documentScopeQueryPageSize - 1);
if (args.signal) labelQuery = labelQuery.abortSignal(args.signal);
const { data: labelRows, error: labelError } = await labelQuery;
if (labelError) throw new Error(labelError.message);
for (const label of (labelRows ?? []) as ScopeLabelRow[]) {
labelsByDocument.set(label.document_id, [...(labelsByDocument.get(label.document_id) ?? []), label]);
}
if ((labelRows ?? []).length < documentScopeQueryPageSize) break;
}
}

Expand Down
21 changes: 8 additions & 13 deletions tests/reconciliation-preflight.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { spawnSync } from "node:child_process";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
classifyReconciliationState,
collectReconciliationState,
collectProcessDiagnostics,
parseWorktreePorcelain,
} from "../scripts/reconciliation-preflight.mjs";
Expand Down Expand Up @@ -103,18 +102,14 @@ describe("reconciliation preflight", () => {
});

it("emits parseable metadata-only JSON without fetching", () => {
const script = path.resolve(process.cwd(), "scripts", "reconciliation-preflight.mjs");
const result = spawnSync(process.execPath, [script, "--json"], {
cwd: process.cwd(),
encoding: "utf8",
});

expect(result.status).toBe(0);
expect(result.stderr).toBe("");
const payload = JSON.parse(result.stdout);
// Exercise the exported collector in-process. Spawning a second Node/Vite
// module graph here made this otherwise synchronous contract contend with
// the full suite for CPU and occasionally exceed the global test timeout.
const stdout = JSON.stringify(collectReconciliationState());
const payload = JSON.parse(stdout);
expect(payload).toMatchObject({ cachedRefsOnly: true, fetched: false });
expect(payload.integrationBase).toBe("dedicated-worktree-required");
expect(payload.processDiagnostics).toMatchObject({ skipped: true, rawCommandLinesSerialized: false });
expect(result.stdout).not.toContain("commandLine");
}, 60_000);
expect(stdout).not.toContain("commandLine");
});
});
57 changes: 57 additions & 0 deletions tests/search-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,63 @@ import { describe, expect, it } from "vitest";
import { activeScopeFilterCount, resolveSearchScope, searchScopeFiltersSchema } from "@/lib/search-scope";

describe("search scope filters", () => {
it("paginates label enumeration beyond the PostgREST 1,000-row cap", async () => {
const documentIds = ["00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002"];
const documents = documentIds.map((id) => ({ id, metadata: {}, import_batch_id: null }));
const labels = Array.from({ length: 1_001 }, (_, index) => ({
id: String(index).padStart(4, "0"),
document_id: index === 1_000 ? documentIds[1] : documentIds[0],
label: index === 1_000 ? "target-service" : `other-service-${index}`,
label_type: "service",
}));
const requestedRanges: Array<[string, number, number]> = [];
const requestedOrders: Array<[string, string, boolean]> = [];

const from = (table: string) => {
let start = 0;
let end = 999;
const builder = {
select: () => builder,
eq: () => builder,
is: () => builder,
in: () => builder,
or: () => builder,
order: (column: string, options?: { ascending?: boolean }) => {
requestedOrders.push([table, column, options?.ascending !== false]);
return builder;
},
abortSignal: () => builder,
range: (nextStart: number, nextEnd: number) => {
start = nextStart;
end = nextEnd;
requestedRanges.push([table, start, end]);
return builder;
},
then: (resolve: (value: { data: unknown[]; error: null }) => unknown) => {
const rows = table === "documents" ? documents : labels;
return Promise.resolve(resolve({ data: rows.slice(start, end + 1), error: null }));
},
};
return builder;
};

await expect(
resolveSearchScope({
supabase: { from } as never,
accessScope: { includePublic: true },
filters: { services: ["target-service"] },
}),
).resolves.toMatchObject({ documentIds: [documentIds[1]], matchedDocumentCount: 1 });
expect(requestedRanges.filter(([table]) => table === "document_labels")).toEqual([
["document_labels", 0, 999],
["document_labels", 1_000, 1_999],
]);
expect(requestedOrders.filter(([table]) => table === "document_labels")).toEqual([
["document_labels", "id", true],
["document_labels", "id", true],
]);
});

it("accepts smart document label filter groups", () => {
const filters = searchScopeFiltersSchema.parse({
services: ["mental-health"],
Expand Down
Loading