diff --git a/web/src/components/CodeQueryEditor.completion.spec.ts b/web/src/components/CodeQueryEditor.completion.spec.ts index 58a4cb8d72..f2a900afcb 100644 --- a/web/src/components/CodeQueryEditor.completion.spec.ts +++ b/web/src/components/CodeQueryEditor.completion.spec.ts @@ -495,10 +495,17 @@ describe("Phase 3 — C5: one provider per language, not per editor", () => { } const added = reg.mock.calls.filter((c) => c[0] === "sql").length - providersBefore; - // Monaco aggregates every provider registered for a language, so N editors - // meant N providers answering on every keystroke — N-1 of them only to - // return an empty list for a model that did not ask. - expect(added, `three editors registered ${added} SQL completion providers`).toBe(1); + const total = reg.mock.calls.filter((c) => c[0] === "sql").length; + + // Asserting `added === 1` would be order-dependent AND unsatisfiable once + // the fix lands: registration moves to module scope, so an earlier + // describe in this file has already done it and three more editors add + // ZERO. The order-independent invariant is that the module ever registers + // one SQL provider. Monaco aggregates every provider registered for a + // language, so today each editor adds another that answers every + // keystroke only to return an empty list for a model that did not ask. + expect(added, `three editors added ${added} SQL completion providers`).toBeLessThanOrEqual(1); + expect(total, `${total} SQL completion providers registered in this file`).toBe(1); }, ); }); diff --git a/web/src/utils/query/editorProviders.spec.ts b/web/src/utils/query/editorProviders.spec.ts index cfa027a8e9..68da0c58a8 100644 --- a/web/src/utils/query/editorProviders.spec.ts +++ b/web/src/utils/query/editorProviders.spec.ts @@ -114,13 +114,37 @@ describe("parseCallContext — locating the enclosing call", () => { }); }); - it("ignores a bare parenthesised group with no function name", () => { - expect(parseCallContext("WHERE (a > 1 AND ")).toBeNull(); + it("returns null when the paren has no identifier before it at all", () => { + expect(parseCallContext("WHERE (")).toBeNull(); + expect(parseCallContext("(")).toBeNull(); }); it("tolerates whitespace between the name and the paren", () => { expect(parseCallContext("SELECT sum (")).toEqual({ name: "sum", activeParameter: 0 }); }); + + // This parser is purely syntactic: it reports whatever identifier precedes + // the open paren, including a SQL keyword. It cannot do otherwise — `WHERE (` + // and `sum (` are the same shape, and the text alone does not say which is a + // function. Rejecting non-functions is the catalog's job (see below), which + // is also what keeps a column named like a keyword from breaking anything. + it("reports a preceding keyword rather than trying to judge it", () => { + expect(parseCallContext("WHERE (a > 1 AND ")).toEqual({ + name: "WHERE", + activeParameter: 0, + }); + }); +}); + +describe("keyword-shaped call sites produce no signature", () => { + it("the catalog rejects what the parser cannot", () => { + // End to end: `WHERE (` parses, then finds no function, so the provider has + // nothing to show. Neither half can make that decision alone. + const ctx = parseCallContext("WHERE (a > 1 AND ")!; + expect(ctx.name).toBe("WHERE"); + expect(findFunctionEntry(ctx.name, [], SQL_FUNCTIONS)).toBeNull(); + expect(buildSignatureHelp(findFunctionEntry(ctx.name, [], SQL_FUNCTIONS), 0)).toBeNull(); + }); }); // ───────────────────────────────────────────────────────────────────────────