test(editor): failing specs for autocomplete Phase 1 (TDD red)

Covers tmp/code.md Phase 1 findings, all currently failing:

- A1  every catalog function must be kind Function, not Text (abc glyph)
- A2  labels must be static strings, not functions of the typed token
- A3  column arguments unquoted, literal arguments quoted (incl. spath)
- A5  insertTextRules must reach monaco as a number, not the string
- N7  detail/documentation/sortText must be forwarded to monaco
- C1  candidates must not be pre-filtered by String.includes
- N1  Alerts must bind effectiveKeywords, not the base list
- N2  the suggestions fallback must be the shared catalog (Traces parity)
- NL  regression guard: static labels must not break quick-mode function
      name extraction in useNLQuery

Three suites cannot load until src/utils/query/sqlCompletion.ts exists.
This commit is contained in:
Prabhat Sharma 2026-08-02 10:33:35 -07:00
parent d76c0a6bbb
commit fa929d3d47
6 changed files with 674 additions and 0 deletions

View File

@ -828,3 +828,49 @@ describe("CodeQueryEditor", () => {
});
});
});
// ─── Phase 1 (tmp/code.md N2 / D7) ────────────────────────────────────────────
// Traces binds :keywords but no :suggestions, so it falls through to the
// component's LOCAL default list. That local copy had 7 entries while the
// composable's had 26 — a shipped divergence. After collapsing the duplicated
// catalogs the fallback must be the shared catalog.
describe("N2/D7 — the suggestions fallback is the shared catalog", () => {
it("falls back to the shared SQL_FUNCTIONS catalog when suggestions prop is null", async () => {
const { SQL_FUNCTIONS } = await import("@/utils/query/sqlCompletion");
const wrapper = mount(CodeQueryEditor, {
props: { editorId: "fallback-editor", language: "sql", suggestions: null },
global: { plugins: [store] },
});
// `suggestions` is the computed the provider reads.
expect((wrapper.vm as any).suggestions).toEqual(SQL_FUNCTIONS);
});
it("the fallback includes the aggregates Traces was missing", async () => {
const wrapper = mount(CodeQueryEditor, {
props: { editorId: "fallback-editor-2", language: "sql", suggestions: null },
global: { plugins: [store] },
});
const names = ((wrapper.vm as any).suggestions as any[]).map((s) => s.name);
for (const agg of ["sum", "avg", "count", "max", "min", "histogram", "approx_topk"]) {
expect(names, `fallback missing ${agg}`).toContain(agg);
}
});
it("an explicit empty array still suppresses all suggestions", async () => {
const wrapper = mount(CodeQueryEditor, {
props: { editorId: "fallback-editor-3", language: "sql", suggestions: [] },
global: { plugins: [store] },
});
expect((wrapper.vm as any).suggestions).toEqual([]);
});
it("uses the shared catalog for its default keywords too", async () => {
const { SQL_KEYWORDS } = await import("@/utils/query/sqlCompletion");
const wrapper = mount(CodeQueryEditor, {
props: { editorId: "fallback-editor-4", language: "sql", keywords: [] },
global: { plugins: [store] },
});
expect((wrapper.vm as any).keywords).toEqual(SQL_KEYWORDS);
});
});

View File

@ -540,3 +540,79 @@ describe("QueryEditorDialog - ODrawer Migration", () => {
expect(w.findComponent(ODrawerStub).exists()).toBe(true);
});
});
// ─── Phase 1 (tmp/code.md N1) ─────────────────────────────────────────────────
// Alerts wires the whole autocomplete pipeline (cursorIndex, popup.open,
// getSuggestions) but binds :keywords="autoCompleteKeywords" instead of
// effectiveKeywords. Result: in value context it force-opens the popup and then
// shows the BASE field/function list where field VALUES belong.
describe("QueryEditorDialog - N1 context keywords reach the editor", () => {
const keywordAwareStub = {
template: '<div class="stub-kw-editor"></div>',
props: ["query", "editorId", "keywords", "suggestions"],
emits: ["update:query", "blur"],
};
const mountWithKeywordStub = async (props: Record<string, any> = {}) =>
mount(QueryEditorDialog, {
props: {
modelValue: true,
tab: "sql",
sqlQuery: "",
promqlQuery: "",
vrlFunction: "",
streamName: "my-stream",
streamType: "logs",
columns: [
{ label: "host", value: "host" },
{ label: "level", value: "level" },
],
period: 10,
multiTimeRange: [],
savedFunctions: [],
sqlQueryErrorMsg: "",
...props,
},
global: {
plugins: [i18n, store],
stubs: {
CodeQueryEditor: keywordAwareStub,
QueryEditor: keywordAwareStub,
UnifiedQueryEditor: keywordAwareStub,
FullViewContainer: {
template: "<div><slot /><slot name='right' /></div>",
props: ["name", "label", "isExpanded"],
emits: ["update:isExpanded"],
},
O2AIChat: { template: "<div />", props: ["headerHeight", "isOpen"], emits: ["close"] },
},
},
});
it("binds a keywords source that switches to context keywords", async () => {
const wrapper = await mountWithKeywordStub();
await flushPromises();
const editor = wrapper.findComponent(keywordAwareStub);
expect(editor.exists()).toBe(true);
const vm = wrapper.vm as any;
// After the fix the template binds effectiveKeywords; the raw base list
// must not be what reaches the editor.
expect(vm.effectiveKeywords).toBeDefined();
expect(Array.isArray(editor.props("keywords"))).toBe(true);
expect(Array.isArray(vm.effectiveKeywords)).toBe(true);
expect(editor.props("keywords")).toStrictEqual(vm.effectiveKeywords);
});
it("binds effectiveSuggestions, so value context can blank the function list", async () => {
const wrapper = await mountWithKeywordStub();
await flushPromises();
const editor = wrapper.findComponent(keywordAwareStub);
const vm = wrapper.vm as any;
expect(vm.effectiveSuggestions).toBeDefined();
expect(Array.isArray(editor.props("suggestions"))).toBe(true);
expect(Array.isArray(vm.effectiveSuggestions)).toBe(true);
expect(editor.props("suggestions")).toStrictEqual(vm.effectiveSuggestions);
});
});

View File

@ -2024,4 +2024,78 @@ describe("QueryConfig.vue", () => {
h.unmount();
});
});
// ─── Phase 1 (tmp/code.md N1) ─────────────────────────────────────────────────
// QueryConfig wires the full autocomplete pipeline in handleInlineQueryUpdate
// (query, cursorIndex, org/stream context, popup.open, getSuggestions) but binds
// :keywords="autoCompleteKeywords" — the BASE list — instead of effectiveKeywords.
// In value context it therefore force-opens a popup showing field NAMES where
// field VALUES belong. Mirrors the QueryEditorDialog guard.
describe("QueryConfig — N1 context keywords reach the inline editor", () => {
let host: any;
let qc: any;
beforeEach(() => {
mockStore = createMockStore();
mockStoreInstance = mockStore;
// tab "sql" so the inline SQL editor actually renders.
const props = reactive({ ...baseQCProps(), tab: "sql" });
const Host = defineComponent({
components: { OForm, QueryConfig },
setup: () => ({
schema: addAlertSchema,
defaultValues: hostDefaults({}),
qcProps: props,
}),
template: `
<OForm :schema="schema" :default-values="defaultValues" @submit="() => {}">
<QueryConfig v-bind="qcProps" />
</OForm>
`,
});
host = mount(Host, {
global: {
mocks: { $store: mockStore },
provide: { store: mockStore },
plugins: [i18n],
stubs: {
UnifiedQueryEditor: {
name: "UnifiedQueryEditor",
template: '<div class="stub-inline-editor" />',
props: ["query", "keywords", "suggestions"],
emits: ["update:query", "focus", "blur"],
},
},
},
});
qc = host.findComponent(QueryConfig);
});
afterEach(() => host?.unmount());
it("exposes effectiveKeywords as the editor's keyword source", () => {
expect(qc.vm.effectiveKeywords).toBeDefined();
});
it("exposes effectiveSuggestions as the editor's suggestion source", () => {
expect(qc.vm.effectiveSuggestions).toBeDefined();
});
it("binds effectiveKeywords (not the base list) to the inline editor", () => {
const editor = host.findComponent({ name: "UnifiedQueryEditor" });
expect(editor.exists()).toBe(true);
// Guard against a vacuous pass where both sides are undefined.
expect(Array.isArray(editor.props("keywords"))).toBe(true);
expect(Array.isArray(qc.vm.effectiveKeywords)).toBe(true);
expect(editor.props("keywords")).toStrictEqual(qc.vm.effectiveKeywords);
});
it("binds effectiveSuggestions to the inline editor", () => {
const editor = host.findComponent({ name: "UnifiedQueryEditor" });
expect(Array.isArray(editor.props("suggestions"))).toBe(true);
expect(Array.isArray(qc.vm.effectiveSuggestions)).toBe(true);
expect(editor.props("suggestions")).toStrictEqual(qc.vm.effectiveSuggestions);
});
});
});

View File

@ -451,3 +451,29 @@ describe("useNLQuery", () => {
});
});
});
// ─── Phase 1 regression (tmp/code.md A2) ──────────────────────────────────────
// getQuickModeFunctionNames() historically recovered function names by regexing
// "match_all('x')" style labels. Once labels become static bare names (A2), that
// regex finds nothing — the extraction must key off the catalog's `name` field.
// These tests pin the OUTPUT so the refactor cannot silently empty the list.
describe("getQuickModeFunctionNames — survives static catalog labels", () => {
it("does not classify a query using a catalog function as natural language", async () => {
vi.resetModules();
vi.doMock("@/composables/useSuggestions", () => ({
default: vi.fn(() => ({
defaultSuggestions: [
{ name: "match_all", label: "match_all", kind: "Function" },
{ name: "str_match", label: "str_match", kind: "Function" },
],
})),
}));
const { useNLQuery: freshUseNLQuery } = await import("./useNLQuery");
const { detectNaturalLanguage } = freshUseNLQuery();
expect(detectNaturalLanguage("match_all('error')", "sql")).toBe(false);
expect(detectNaturalLanguage("str_match(body, 'error')", "sql")).toBe(false);
vi.doUnmock("@/composables/useSuggestions");
vi.resetModules();
});
});

View File

@ -363,3 +363,50 @@ describe("effectiveSuggestions — empty when value suggestions are shown", () =
expect(c.effectiveSuggestions.value.length).toBeGreaterThan(0);
});
});
// ─── Phase 1 (tmp/code.md): catalog wiring ────────────────────────────────────
// The composable must source its suggestions/keywords from the shared
// sqlCompletion catalog rather than its own inline copy (D7), so every surface
// gets identical content.
describe("catalog wiring — suggestions come from the shared sqlCompletion module", () => {
beforeEach(() => vi.clearAllMocks());
it("exposes the full catalog as defaultSuggestions", async () => {
const { SQL_FUNCTIONS } = await import("@/utils/query/sqlCompletion");
const c = makeComposable();
expect(c.defaultSuggestions).toEqual(SQL_FUNCTIONS);
});
it("every exposed suggestion is kind Function, never Text (A1)", () => {
const c = makeComposable();
for (const s of c.defaultSuggestions as any[]) {
expect(s.kind, `${s.name ?? s.label}`).toBe("Function");
}
});
it("every exposed suggestion has a static string label (A2)", () => {
const c = makeComposable();
for (const s of c.defaultSuggestions as any[]) {
expect(typeof s.label).toBe("string");
}
});
it("effectiveSuggestions in normal context carries the aggregates", async () => {
const c = makeComposable({ storedValues: [] });
await run(c, "SELECT * FROM stream WHERE ");
const names = (c.effectiveSuggestions.value as any[]).map((s) => s.name);
for (const agg of ["sum", "avg", "count", "max", "min", "histogram"]) {
expect(names, `missing ${agg}`).toContain(agg);
}
});
it("keeps field keywords sorted ahead of SQL keywords", async () => {
const c = makeComposable({ storedValues: [] });
c.updateFieldKeywords([{ name: "host" }, { name: "level" }]);
await run(c, "SELECT * FROM stream WHERE ");
const host = c.effectiveKeywords.value.find((k: any) => k.label === "host");
const and = c.effectiveKeywords.value.find((k: any) => k.label === "and");
expect(host.sortText < and.sortText).toBe(true);
});
});

View File

@ -0,0 +1,405 @@
// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// TDD spec for the shared SQL completion catalog (tmp/code.md Phase 1).
// Covers: A1 (kinds), A2 (static labels), A3 (argument quoting),
// A5 (insertTextRules string -> enum), N7 (field forwarding), C1 (no pre-filter),
// D7/N2 (single catalog shared by every surface).
import { describe, it, expect } from "vitest";
import {
SQL_FUNCTIONS,
SQL_KEYWORDS,
buildCompletionItems,
type SqlCompletionEntry,
} from "./sqlCompletion";
// Mirrors monaco.languages.CompletionItemKind (verified in
// monaco-editor/esm/vs/editor/common/languages.js byKind map).
const KINDS = {
Method: 0,
Function: 1,
Constructor: 2,
Field: 3,
Variable: 4,
Operator: 11,
Value: 13,
Keyword: 17,
Text: 18,
Snippet: 27,
} as const;
// Mirrors monaco.languages.CompletionItemInsertTextRule.
const INSERT_RULES = {
None: 0,
KeepWhitespace: 1,
InsertAsSnippet: 4,
} as const;
const range = { startLineNumber: 1, endLineNumber: 1, startColumn: 1, endColumn: 1 };
const build = (opts: Partial<Parameters<typeof buildCompletionItems>[0]> = {}) =>
buildCompletionItems({
keywords: [],
suggestions: [],
word: "",
range,
kinds: KINDS,
insertTextRules: INSERT_RULES,
...opts,
});
const byName = (name: string): SqlCompletionEntry => {
const found = SQL_FUNCTIONS.find((f) => f.name === name);
if (!found) throw new Error(`SQL_FUNCTIONS is missing "${name}"`);
return found;
};
// ───────────────────────────────────────────────────────────────────────────
// A1 — completion item kinds
// ───────────────────────────────────────────────────────────────────────────
describe("A1 — every function is kind Function, never Text", () => {
it("no catalog function is kind Text (the 'abc' glyph)", () => {
const textKinded = SQL_FUNCTIONS.filter((f) => f.kind === "Text");
expect(textKinded).toEqual([]);
});
it("every catalog function declares kind Function", () => {
for (const fn of SQL_FUNCTIONS) {
expect(fn.kind, `${fn.name} should be kind Function`).toBe("Function");
}
});
it("maps kind Function to monaco's numeric 1 (symbolFunction glyph)", () => {
const items = build({ suggestions: [byName("approx_topk")] });
expect(items[0].kind).toBe(KINDS.Function);
});
it("comparison operators are kind Operator, not Keyword", () => {
for (const op of ["=", "!=", "<>", ">", "<", ">=", "<="]) {
const entry = SQL_KEYWORDS.find((k) => k.label === op);
expect(entry, `SQL_KEYWORDS is missing operator "${op}"`).toBeDefined();
expect(entry!.kind, `operator ${op}`).toBe("Operator");
}
});
it("logical/predicate words are kind Keyword", () => {
for (const kw of ["and", "or", "like", "in", "not in", "between", "is null"]) {
const entry = SQL_KEYWORDS.find((k) => k.label === kw);
expect(entry, `SQL_KEYWORDS is missing keyword "${kw}"`).toBeDefined();
expect(entry!.kind, `keyword ${kw}`).toBe("Keyword");
}
});
});
// ───────────────────────────────────────────────────────────────────────────
// A2 — static labels (no typed-token interpolation)
// ───────────────────────────────────────────────────────────────────────────
describe("A2 — labels are static strings, never functions of the typed token", () => {
it("no catalog entry exposes a callable label", () => {
for (const entry of [...SQL_FUNCTIONS, ...SQL_KEYWORDS]) {
expect(typeof entry.label, `${entry.name} label`).toBe("string");
}
});
it("no catalog entry exposes a callable insertText", () => {
for (const entry of [...SQL_FUNCTIONS, ...SQL_KEYWORDS]) {
expect(typeof entry.insertText, `${entry.name} insertText`).toBe("string");
}
});
it("function labels are the bare function name — no embedded arguments", () => {
// The screenshot bug: label read "approx_topk('a', 10)" with a frozen 'a'.
expect(byName("approx_topk").label).toBe("approx_topk");
expect(byName("match_all").label).toBe("match_all");
expect(byName("histogram").label).toBe("histogram");
});
it("labels never contain a quote character", () => {
for (const fn of SQL_FUNCTIONS) {
expect(fn.label, `${fn.name} label must not embed a literal`).not.toContain("'");
}
});
it("produces identical items regardless of what the user has typed", () => {
// Guards the staleness class of bug outright: item content cannot depend on `word`.
const atA = build({ suggestions: SQL_FUNCTIONS, word: "a" });
const atAppr = build({ suggestions: SQL_FUNCTIONS, word: "appr" });
expect(atA).toEqual(atAppr);
});
});
// ───────────────────────────────────────────────────────────────────────────
// A3 — argument quoting
// ───────────────────────────────────────────────────────────────────────────
describe("A3 — column arguments are unquoted, literal arguments are quoted", () => {
// Functions whose FIRST argument is a column/expression reference.
const COLUMN_FIRST = [
"re_match",
"re_not_match",
"str_match",
"str_match_ignore_case",
"arr_descending",
"arrcount",
"arrsort",
"cast_to_arr",
"arrindex",
"arrjoin",
"arrzip",
"spath",
"to_array_string",
"sum",
"avg",
"count",
"max",
"min",
"histogram",
"approx_topk",
"approx_topk_distinct",
];
it.each(COLUMN_FIRST)("%s does not wrap its first argument in quotes", (name) => {
const insertText = byName(name).insertText;
// First argument begins right after "name(" and must not open with a quote.
const firstArg = insertText.slice(name.length + 1);
expect(firstArg.startsWith("'"), `${name} => ${insertText}`).toBe(false);
});
it("sum/avg/count/max/min take a bare column tab stop", () => {
for (const agg of ["sum", "avg", "count", "max", "min"]) {
expect(byName(agg).insertText).toBe(`${agg}(\${1:field})`);
}
});
it("histogram takes an unquoted column and a quoted interval", () => {
// Real usage in the app: histogram(_timestamp, '30 second') — useAlertForm.ts:463
expect(byName("histogram").insertText).toBe("histogram(${1:_timestamp}, '${2:30 second}')");
});
it("spath takes an unquoted column and a quoted path", () => {
// Backend tests use spath(object, 'nested.value') — first arg is an expression.
expect(byName("spath").insertText).toBe("spath(${1:field}, '${2:path}')");
});
it("approx_topk takes an unquoted column and a numeric k", () => {
expect(byName("approx_topk").insertText).toBe("approx_topk(${1:field}, ${2:10})");
});
it("arrcount takes a bare array column (arrcount_udf.rs:51)", () => {
expect(byName("arrcount").insertText).toBe("arrcount(${1:field})");
});
it("match_all quotes its search term — it is a literal", () => {
expect(byName("match_all").insertText).toBe("match_all('${1:value}')");
});
it("str_match takes an unquoted column and a quoted value", () => {
expect(byName("str_match").insertText).toBe("str_match(${1:field}, '${2:value}')");
});
});
// ───────────────────────────────────────────────────────────────────────────
// A5 — insertTextRules must reach monaco as a NUMBER
// ───────────────────────────────────────────────────────────────────────────
describe("A5 — snippet rules are mapped from string name to numeric enum", () => {
const snippetEntry: SqlCompletionEntry = {
name: "demo",
label: "demo",
kind: "Function",
insertText: "demo(${1:field})",
insertTextRules: "InsertAsSnippet",
};
it("emits the numeric InsertAsSnippet flag, never the string", () => {
const [item] = build({ suggestions: [snippetEntry] });
expect(item.insertTextRules).toBe(INSERT_RULES.InsertAsSnippet);
expect(typeof item.insertTextRules).toBe("number");
});
it("survives monaco's bitwise test (string would coerce to 0)", () => {
const [item] = build({ suggestions: [snippetEntry] });
// suggestController.js:368 — if (!(insertTextRules & 4)) escape as plain text
expect(item.insertTextRules & INSERT_RULES.InsertAsSnippet).toBeTruthy();
// Prove the current bug would fail this: "InsertAsSnippet" & 4 === 0
expect(("InsertAsSnippet" as any) & INSERT_RULES.InsertAsSnippet).toBe(0);
});
it("applies the same mapping on the keywords path", () => {
const [item] = build({
keywords: [
{
name: "like",
label: "like",
kind: "Keyword",
insertText: "like '%${1:params}%' ",
insertTextRules: "InsertAsSnippet",
},
],
});
expect(item.insertTextRules).toBe(INSERT_RULES.InsertAsSnippet);
});
it("omits insertTextRules for plain-text entries", () => {
const [item] = build({
keywords: [{ name: "and", label: "and", kind: "Keyword", insertText: "and " }],
});
expect(item.insertTextRules).toBeUndefined();
});
it("every catalog entry containing a ${…} tab stop declares InsertAsSnippet", () => {
for (const entry of [...SQL_FUNCTIONS, ...SQL_KEYWORDS]) {
if (entry.insertText.includes("${")) {
expect(entry.insertTextRules, `${entry.name} has tab stops but no snippet rule`).toBe(
"InsertAsSnippet",
);
}
}
});
});
// ───────────────────────────────────────────────────────────────────────────
// N7 — the push site must forward every field, not just four
// ───────────────────────────────────────────────────────────────────────────
describe("N7 — optional metadata is forwarded to monaco", () => {
const rich: SqlCompletionEntry = {
name: "approx_topk",
label: "approx_topk",
kind: "Function",
detail: "(field, k) → top-k values",
documentation: "Approximate top-k aggregation.",
insertText: "approx_topk(${1:field}, ${2:10})",
insertTextRules: "InsertAsSnippet",
sortText: "approx_topk",
};
it("forwards detail", () => {
expect(build({ suggestions: [rich] })[0].detail).toBe("(field, k) → top-k values");
});
it("forwards documentation", () => {
expect(build({ suggestions: [rich] })[0].documentation).toBe(
"Approximate top-k aggregation.",
);
});
it("forwards sortText", () => {
expect(build({ suggestions: [rich] })[0].sortText).toBe("approx_topk");
});
it("attaches the supplied range to every item", () => {
const items = build({ suggestions: SQL_FUNCTIONS, keywords: SQL_KEYWORDS });
expect(items.length).toBeGreaterThan(0);
for (const item of items) expect(item.range).toBe(range);
});
it("every catalog function ships a detail signature for the docs column", () => {
for (const fn of SQL_FUNCTIONS) {
expect(fn.detail, `${fn.name} needs a detail signature`).toBeTruthy();
}
});
});
// ───────────────────────────────────────────────────────────────────────────
// C1 — no substring pre-filter; monaco does the scoring
// ───────────────────────────────────────────────────────────────────────────
describe("C1 — candidates are not pre-filtered by substring", () => {
const fields = [
{ name: "kubernetes_namespace_name", label: "kubernetes_namespace_name", kind: "Field" as const, insertText: "kubernetes_namespace_name" },
{ name: "code", label: "code", kind: "Field" as const, insertText: "code" },
];
it("returns every keyword even when the typed word matches none of them", () => {
const items = build({ keywords: fields, word: "zzz" });
expect(items).toHaveLength(2);
});
it("keeps subsequence matches that String.includes would have dropped", () => {
// "knn" is a subsequence of kubernetes_namespace_name but not a substring.
const items = build({ keywords: fields, word: "knn" });
expect(items.map((i: any) => i.label)).toContain("kubernetes_namespace_name");
});
it("returns suggestions unfiltered too", () => {
const items = build({ suggestions: SQL_FUNCTIONS, word: "zzz" });
expect(items).toHaveLength(SQL_FUNCTIONS.length);
});
});
// ───────────────────────────────────────────────────────────────────────────
// D7 / N2 — one catalog, shared everywhere
// ───────────────────────────────────────────────────────────────────────────
describe("D7/N2 — the catalog is complete and internally consistent", () => {
it("carries all 26 historical suggestions", () => {
expect(SQL_FUNCTIONS.length).toBeGreaterThanOrEqual(26);
});
it("includes the aggregates Traces was missing", () => {
for (const name of ["sum", "avg", "count", "max", "min", "histogram", "approx_topk"]) {
expect(SQL_FUNCTIONS.some((f) => f.name === name), `missing ${name}`).toBe(true);
}
});
it("includes the array family", () => {
for (const name of ["arrcount", "arrsort", "arrindex", "arrjoin", "arrzip", "arr_descending"]) {
expect(SQL_FUNCTIONS.some((f) => f.name === name), `missing ${name}`).toBe(true);
}
});
it("has no duplicate function names", () => {
const names = SQL_FUNCTIONS.map((f) => f.name);
expect(new Set(names).size).toBe(names.length);
});
it("marks the backward-compatibility aliases deprecated", () => {
// sql/rewriter/match_all_raw.rs:41 rewrites these to match_all.
for (const name of ["match_all_raw", "match_all_raw_ignore_case"]) {
expect(byName(name).deprecated, `${name} should be flagged deprecated`).toBe(true);
}
});
it("does not mark match_all itself deprecated", () => {
expect(byName("match_all").deprecated).toBeFalsy();
});
});
// ───────────────────────────────────────────────────────────────────────────
// Back-compat — the `suggestions` prop is public; legacy shape must still work
// ───────────────────────────────────────────────────────────────────────────
describe("back-compat — legacy callable label/insertText entries still build", () => {
const legacy = {
label: (kw: string) => `custom_fn('${kw}')`,
kind: "Text",
insertText: (kw: string) => `custom_fn('${kw}')`,
};
it("invokes callable label/insertText with the typed word", () => {
const [item] = build({ suggestions: [legacy as any], word: "abc" });
expect(item.label).toBe("custom_fn('abc')");
expect(item.insertText).toBe("custom_fn('abc')");
});
it("still maps a legacy string kind through the enum", () => {
const [item] = build({ suggestions: [legacy as any], word: "abc" });
expect(item.kind).toBe(KINDS.Text);
});
});