Skip to content

Commit dd5a8f3

Browse files
committed
test(vnext): enforce namespace coverage floor
1 parent 06a02eb commit dd5a8f3

3 files changed

Lines changed: 114 additions & 19 deletions

File tree

src/vnext/__tests__/namespace-catalog-boundary.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ describe("namespace catalog boundary", () => {
6161
expect(context?.id).toBe("namespaces");
6262
context?.search(request(), new AbortController().signal);
6363
expect(receivers).toEqual([undefined]);
64+
expect(Reflect.apply(
65+
resolveSqlNamespaceCatalogProvider,
66+
undefined,
67+
[{}],
68+
)).toBeNull();
6469
expect(Object.isFrozen(handle)).toBe(true);
6570
});
6671

@@ -136,6 +141,24 @@ describe("namespace catalog boundary", () => {
136141
const accessor = { ...valid };
137142
Object.defineProperty(accessor, "prefix", { get: vi.fn() });
138143
cases.push(accessor);
144+
cases.push({
145+
...valid,
146+
qualifier: new Proxy([], {
147+
getOwnPropertyDescriptor: (_target, key) => {
148+
if (key === "length") throw new Error("length trap");
149+
return undefined;
150+
},
151+
}),
152+
});
153+
cases.push({
154+
...valid,
155+
qualifier: new Proxy([{ quoted: false, value: "x" }], {
156+
getOwnPropertyDescriptor: (target, key) => {
157+
if (key === "0") throw new Error("element trap");
158+
return Reflect.getOwnPropertyDescriptor(target, key);
159+
},
160+
}),
161+
});
139162
for (const value of cases) {
140163
expect(createSqlNamespaceCatalogSearchRequest(value).status)
141164
.toBe("malformed");
@@ -208,6 +231,41 @@ describe("namespace catalog boundary", () => {
208231
});
209232
});
210233

234+
it("orders tied identities and accepts quoted paths without detail", () => {
235+
const provider = captured();
236+
const base = {
237+
canonicalPath: [{
238+
quoted: true,
239+
role: "schema",
240+
value: "Main",
241+
}],
242+
insertText: "\"Main\"",
243+
matchQuality: "exact",
244+
};
245+
const decoded = decodeSqlNamespaceCatalogSearchResponse(
246+
provider,
247+
request(),
248+
{
249+
containers: [
250+
{ ...base, containerEntityId: "z" },
251+
{ ...base, containerEntityId: "a" },
252+
],
253+
coverage: "complete",
254+
epoch,
255+
status: "ready",
256+
},
257+
);
258+
expect(decoded).toMatchObject({
259+
status: "accepted",
260+
value: {
261+
containers: [
262+
{ containerEntityId: "a" },
263+
{ containerEntityId: "z" },
264+
],
265+
},
266+
});
267+
});
268+
211269
it("rejects malformed, conflicting, stale, and oversized responses", () => {
212270
const provider = captured();
213271
const valid = {
@@ -245,6 +303,7 @@ describe("namespace catalog boundary", () => {
245303
(_, index) => container(String(index), "schema", String(index)),
246304
) },
247305
conflicting,
306+
{ ...valid, containers: [null] },
248307
{ epoch, extra: true, status: "loading" },
249308
{ code: "bad", epoch, retry: "never", status: "failed" },
250309
{ code: "unknown", epoch, retry: "bad", status: "failed" },
@@ -261,5 +320,10 @@ describe("namespace catalog boundary", () => {
261320
value,
262321
).status).toBe("malformed");
263322
}
323+
expect(Reflect.apply(
324+
decodeSqlNamespaceCatalogSearchResponse,
325+
undefined,
326+
[{}, request(), valid],
327+
)).toMatchObject({ status: "malformed" });
264328
});
265329
});

src/vnext/__tests__/namespace-catalog-coordinator.test.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
prepareSqlNamespaceCatalogSearch,
99
} from "../namespace-completion.js";
1010
import type {
11+
SqlNamespaceCatalogResolvedContainer,
1112
SqlNamespaceCatalogSearchRequest,
1213
} from "../namespace-catalog-types.js";
1314
import type { SqlCatalogEpoch } from "../relation-completion-types.js";
@@ -91,12 +92,15 @@ describe("namespace catalog coordinator", () => {
9192
searches.push(request);
9293
return response(request);
9394
});
94-
const outcome = await owner.request(input()).result;
95+
const outcome = await owner.request({
96+
...input(),
97+
prefix: { quoted: true, value: "ma" },
98+
}).result;
9599
expect(searches).toHaveLength(1);
96100
expect(searches[0]).toMatchObject({
97101
dialectId: "duckdb",
98102
limit: 10,
99-
prefix: { value: "ma" },
103+
prefix: { quoted: true, value: "ma" },
100104
qualifier: [{ value: "memory" }],
101105
scope: "connection",
102106
});
@@ -169,7 +173,7 @@ describe("namespace catalog coordinator", () => {
169173
});
170174
expect(signals[1]?.aborted).toBe(true);
171175
pending[0]?.resolve({});
172-
pending[1]?.resolve({});
176+
pending[1]?.reject(new Error("late"));
173177
await Promise.resolve();
174178
});
175179

@@ -188,6 +192,7 @@ describe("namespace catalog coordinator", () => {
188192
const first = owner.request(input("first"));
189193
const second = other.owner.request(input("second"));
190194
owner.dispose();
195+
owner.dispose();
191196
await expect(first.result).resolves.toEqual({
192197
reason: "disposed",
193198
status: "unavailable",
@@ -199,6 +204,7 @@ describe("namespace catalog coordinator", () => {
199204
await Promise.resolve();
200205
expect(secondSettled).toBe(false);
201206
coordinator.dispose();
207+
coordinator.dispose();
202208
await expect(second.result).resolves.toEqual({
203209
reason: "disposed",
204210
status: "unavailable",
@@ -276,10 +282,12 @@ describe("namespace catalog coordinator", () => {
276282
scope: "scope",
277283
});
278284
if (prepared.status !== "prepared") throw new Error("prepared");
279-
expect(await prepared.owner.request({
285+
const invalidTicket = prepared.owner.request({
280286
...input(),
281287
limit: 0,
282-
}).result).toMatchObject({
288+
});
289+
invalidTicket.cancel();
290+
expect(await invalidTicket.result).toMatchObject({
283291
reason: "invalid-request",
284292
status: "unavailable",
285293
});
@@ -372,21 +380,38 @@ describe("namespace completion composer", () => {
372380
const catalog = catalogResponse();
373381
const main = catalog.containers[0];
374382
if (!main) throw new Error("main");
383+
const parent = main.canonicalPath[0];
384+
const child = main.canonicalPath[1];
385+
if (!child) throw new Error("child");
386+
const copied: SqlNamespaceCatalogResolvedContainer = {
387+
...main,
388+
canonicalPath: [
389+
parent,
390+
{ ...child, quoted: true },
391+
],
392+
containerEntityId: "main-copy",
393+
provenance: {
394+
...main.provenance,
395+
containerEntityId: "main-copy",
396+
},
397+
};
398+
const tied: SqlNamespaceCatalogResolvedContainer = {
399+
...main,
400+
containerEntityId: "aaa",
401+
provenance: {
402+
...main.provenance,
403+
containerEntityId: "aaa",
404+
},
405+
};
375406
const duplicated = {
376407
...readyOutcome,
377408
response: {
378409
...catalog,
379410
containers: [
380411
...catalog.containers,
381412
main,
382-
{
383-
...main,
384-
containerEntityId: "main-copy",
385-
provenance: {
386-
...main.provenance,
387-
containerEntityId: "main-copy",
388-
},
389-
},
413+
copied,
414+
tied,
390415
],
391416
},
392417
};
@@ -398,7 +423,7 @@ describe("namespace completion composer", () => {
398423
replacementRange: { from: 10, to: 12 },
399424
});
400425
expect(all?.value.items.map((value) => value.label))
401-
.toEqual(["main", "main", "metrics"]);
426+
.toEqual(["main", "main", "main", "metrics"]);
402427
const composition = composeSqlNamespaceCompletion({
403428
matchPrefix: (candidate, prefix) =>
404429
candidate.value.startsWith(prefix.value)
@@ -421,6 +446,10 @@ describe("namespace completion composer", () => {
421446
edit: { from: 10, insert: "main", to: 12 },
422447
label: "main",
423448
role: "schema",
449+
}, {
450+
edit: { from: 10, insert: "main", to: 12 },
451+
label: "main",
452+
role: "schema",
424453
}],
425454
},
426455
});

src/vnext/namespace-catalog-coordinator.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,18 +356,20 @@ function request(
356356
status: "usable",
357357
}));
358358
}
359-
let resolveResult:
360-
(value: SqlNamespaceCatalogSearchOutcome) => void =
361-
(): void => {};
359+
const resolver: {
360+
value:
361+
| ((value: SqlNamespaceCatalogSearchOutcome) => void)
362+
| null;
363+
} = { value: null };
362364
const result = new Promise<SqlNamespaceCatalogSearchOutcome>(
363365
(resolve) => {
364-
resolveResult = resolve;
366+
resolver.value = resolve;
365367
},
366368
);
367369
const consumer: ConsumerState = {
368370
controller: new AbortController(),
369371
owner,
370-
resolve: resolveResult,
372+
resolve: resolver.value,
371373
settled: false,
372374
};
373375
owner.active = consumer;

0 commit comments

Comments
 (0)