test(editor): give the SQL fallback real provider coverage (N2)
Addresses the remaining review gap: the N2/D7 block asserted the resolver directly and only that the component mounts, so CodeQueryEditor could keep its local seven-entry fallback and still pass. Replaced with provider invocations using an omitted suggestions prop, asserting sum/avg/count/max/min/histogram/ approx_topk and the array family come back, Function-kinded and snippet-enabled. Fails today with 'fallback missing sum'. Moved the whole provider suite into CodeQueryEditor.completion.spec.ts. In the host file the pre-existing load flakiness (Ctrl+Enter, setValue) starved this suite's beforeAll and vitest SKIPPED all of it — silently zero coverage. Isolated, it is stable: 2 passed / 10 failed across three consecutive runs.
This commit is contained in:
parent
4becf0765a
commit
96e5596653
|
|
@ -0,0 +1,327 @@
|
|||
// Copyright 2026 OpenObserve Inc.
|
||||
//
|
||||
// Phase 1 completion-provider integration tests (tmp/code.md).
|
||||
//
|
||||
// Deliberately a SEPARATE file from CodeQueryEditor.spec.ts: that suite's
|
||||
// async-mount tests (Ctrl+Enter, setValue) are load-flaky on main — an
|
||||
// unmodified copy fails ~9 of them on roughly 3 runs in 4 — and the contention
|
||||
// starved this suite's beforeAll, skipping every test here. Isolated, these
|
||||
// three mounts run reliably.
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import CodeQueryEditor from "./CodeQueryEditor.vue";
|
||||
import { createStore } from "vuex";
|
||||
|
||||
// Stable model instance. CodeQueryEditor's completion provider answers only for
|
||||
// its OWN model (`if (own && model !== own) return { suggestions: [] }`), so a
|
||||
// getModel() that returns a fresh object each call makes the provider
|
||||
// permanently unreachable from tests — which is why the provider specs below
|
||||
// were previously skipped.
|
||||
const mockModel = {
|
||||
getValue: vi.fn(() => ""),
|
||||
setValue: vi.fn(),
|
||||
getLineCount: vi.fn(() => 1),
|
||||
getLineLength: vi.fn(() => 0),
|
||||
pushEditOperations: vi.fn(),
|
||||
getOffsetAt: vi.fn(() => 0),
|
||||
getPositionAt: vi.fn(() => ({ lineNumber: 1, column: 1 })),
|
||||
getLineContent: vi.fn(() => ""),
|
||||
getValueInRange: vi.fn(() => ""),
|
||||
getWordUntilPosition: vi.fn(() => ({ word: "", startColumn: 1, endColumn: 1 })),
|
||||
};
|
||||
|
||||
// Stable mock editor instance so tests can reference it directly
|
||||
const mockEditorObj = {
|
||||
onDidChangeModelContent: vi.fn(),
|
||||
createContextKey: vi.fn(),
|
||||
addCommand: vi.fn(),
|
||||
onDidFocusEditorWidget: vi.fn(),
|
||||
onDidBlurEditorWidget: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
getValue: vi.fn(() => ""),
|
||||
setValue: vi.fn(),
|
||||
layout: vi.fn(),
|
||||
getModel: vi.fn(() => mockModel),
|
||||
updateOptions: vi.fn(),
|
||||
hasWidgetFocus: vi.fn(() => false),
|
||||
getRawOptions: vi.fn(() => ({ readOnly: false })),
|
||||
deltaDecorations: vi.fn(() => []),
|
||||
getPosition: vi.fn(() => ({ lineNumber: 1, column: 1 })),
|
||||
trigger: vi.fn(),
|
||||
getAction: vi.fn(() => ({ run: vi.fn(() => Promise.resolve()) })),
|
||||
};
|
||||
|
||||
// Simple mock for monaco editor
|
||||
vi.mock("monaco-editor/esm/vs/editor/editor.all.js", () => ({}));
|
||||
vi.mock("monaco-editor/esm/vs/editor/editor.api", () => ({
|
||||
default: {},
|
||||
editor: {
|
||||
create: vi.fn(() => mockEditorObj),
|
||||
defineTheme: vi.fn(),
|
||||
setTheme: vi.fn(),
|
||||
setModelMarkers: vi.fn(),
|
||||
},
|
||||
languages: {
|
||||
// Real values, mirroring monaco-editor/esm/vs/editor/common/languages.js
|
||||
CompletionItemKind: {
|
||||
Method: 0,
|
||||
Function: 1,
|
||||
Constructor: 2,
|
||||
Field: 3,
|
||||
Variable: 4,
|
||||
Operator: 11,
|
||||
Value: 13,
|
||||
Keyword: 17,
|
||||
Text: 18,
|
||||
Snippet: 27,
|
||||
},
|
||||
CompletionItemInsertTextRule: { None: 0, KeepWhitespace: 1, InsertAsSnippet: 4 },
|
||||
register: vi.fn(),
|
||||
setMonarchTokensProvider: vi.fn(),
|
||||
registerCompletionItemProvider: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
},
|
||||
KeyMod: { CtrlCmd: 1 },
|
||||
KeyCode: { Enter: 13 },
|
||||
}));
|
||||
|
||||
// Mock dynamic imports
|
||||
vi.mock("monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js", () => ({}));
|
||||
vi.mock("monaco-editor/esm/vs/language/json/monaco.contribution.js", () => ({}));
|
||||
vi.mock("monaco-editor/esm/vs/language/html/monaco.contribution.js", () => ({}));
|
||||
vi.mock("monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js", () => ({}));
|
||||
vi.mock("monaco-editor/esm/vs/basic-languages/python/python.contribution.js", () => ({}));
|
||||
vi.mock("monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js", () => ({}));
|
||||
|
||||
// Fix: component imports default from "@/composables/useLogs/searchState"
|
||||
vi.mock("@/composables/useLogs/searchState", () => ({
|
||||
default: () => ({
|
||||
searchObj: { data: {}, meta: {} },
|
||||
}),
|
||||
searchState: () => ({
|
||||
searchObj: { data: {}, meta: {} },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useNLQuery", () => ({
|
||||
useNLQuery: () => ({
|
||||
detectNaturalLanguage: vi.fn(() => false),
|
||||
generateSQL: vi.fn(() => Promise.resolve(null)),
|
||||
transformToSQL: vi.fn((_nl: string, sql: string) => sql),
|
||||
isGenerating: { value: false },
|
||||
streamingResponse: { value: "" },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("vue-i18n", () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/composables/useNotifications", () => ({
|
||||
default: () => ({
|
||||
showErrorNotification: vi.fn(),
|
||||
showPositiveNotification: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/zincutils", () => ({
|
||||
getImageURL: vi.fn((path: string) => `/mocked/${path}`),
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/query/vrlLanguageDefinition", () => ({
|
||||
vrlLanguageDefinition: {},
|
||||
}));
|
||||
|
||||
// ─── Phase 1 integration (tmp/code.md A2/A4/A5/N7/C1) ─────────────────────────
|
||||
// The pure helper specs in src/utils/query/sqlCompletion.spec.ts can all pass
|
||||
// while CodeQueryEditor keeps its own split(" ") tokenisation, String.includes
|
||||
// pre-filter, singular `insertTextRule` lookup and four-field push. These tests
|
||||
// drive the REGISTERED provider so the component itself is on the hook.
|
||||
//
|
||||
// Mounted ONCE in beforeAll. The suite's other async-mount tests (Ctrl+Enter,
|
||||
// setValue) are already load-flaky on main — they time out on ~1 run in 2 with
|
||||
// no source changes at all — so we do not add nine more waitFor mounts here.
|
||||
|
||||
describe("Phase 1 — the registered completion provider", () => {
|
||||
const providerStore = createStore({ state: { theme: "light" } });
|
||||
// Two providers so a throw on the suggestions path cannot mask the
|
||||
// keyword-path signals (A5/C1) — otherwise every assertion here fails for
|
||||
// the same single reason and only one bug is observable at a time.
|
||||
let provideKw: Function;
|
||||
let provideSug: Function;
|
||||
let provideFallback: Function;
|
||||
let getElementByIdSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
const KEYWORDS = [
|
||||
{
|
||||
name: "like",
|
||||
label: "like",
|
||||
kind: "Keyword",
|
||||
insertText: "like '%${1:params}%' ",
|
||||
insertTextRules: "InsertAsSnippet",
|
||||
},
|
||||
{
|
||||
name: "kubernetes_namespace_name",
|
||||
label: "kubernetes_namespace_name",
|
||||
kind: "Field",
|
||||
insertText: "kubernetes_namespace_name",
|
||||
sortText: "\u0000kubernetes_namespace_name",
|
||||
},
|
||||
{ name: "and", label: "and", kind: "Keyword", insertText: "and " },
|
||||
];
|
||||
|
||||
const SUGGESTIONS = [
|
||||
{
|
||||
name: "approx_topk",
|
||||
label: "approx_topk",
|
||||
kind: "Function",
|
||||
detail: "(field, k) → top-k values",
|
||||
documentation: "Approximate top-k.",
|
||||
insertText: "approx_topk(${1:field}, ${2:10})",
|
||||
insertTextRules: "InsertAsSnippet",
|
||||
sortText: "zz-approx_topk",
|
||||
},
|
||||
// Legacy callable shape — the `suggestions` prop is public API.
|
||||
{
|
||||
label: (kw: string) => `legacy('${kw}')`,
|
||||
kind: "Text",
|
||||
insertText: (kw: string) => `legacy('${kw}')`,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
const monacoApi = await import("monaco-editor/esm/vs/editor/editor.api");
|
||||
const registerFn = vi.mocked(monacoApi.languages.registerCompletionItemProvider);
|
||||
|
||||
getElementByIdSpy = vi.spyOn(document, "getElementById").mockImplementation(() =>
|
||||
document.createElement("div"),
|
||||
);
|
||||
|
||||
const mountAndCapture = async (editorId: string, props: any) => {
|
||||
const baseline = registerFn.mock.calls.length;
|
||||
mount(CodeQueryEditor, {
|
||||
props: { editorId, language: "sql", query: "SELECT * FROM logs", ...props },
|
||||
global: { plugins: [providerStore] },
|
||||
});
|
||||
await vi.waitFor(() => expect(registerFn.mock.calls.length).toBeGreaterThan(baseline), {
|
||||
timeout: 15000,
|
||||
interval: 25,
|
||||
});
|
||||
return registerFn.mock.calls[baseline][1].provideCompletionItems as Function;
|
||||
};
|
||||
|
||||
provideKw = await mountAndCapture("provider-kw", {
|
||||
keywords: KEYWORDS,
|
||||
suggestions: [],
|
||||
});
|
||||
provideSug = await mountAndCapture("provider-sug", {
|
||||
keywords: [],
|
||||
suggestions: SUGGESTIONS,
|
||||
});
|
||||
// N2/D7: suggestions prop omitted => the component must fall back to the
|
||||
// SHARED catalog. Its old local copy had 7 entries and no aggregates, which
|
||||
// is what Traces was shipping.
|
||||
provideFallback = await mountAndCapture("provider-fallback", { keywords: [] });
|
||||
// editorObj must exist so the provider's own-model guard resolves to mockModel.
|
||||
await vi.waitFor(() => expect(mockEditorObj.createContextKey).toHaveBeenCalled(), {
|
||||
timeout: 15000,
|
||||
interval: 25,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => getElementByIdSpy?.mockRestore());
|
||||
|
||||
/** Invoke a provider as monaco would, for a given buffer + current word. */
|
||||
const invokeWith = (provide: Function, textUntilCursor: string, word: string) => {
|
||||
mockModel.getValueInRange.mockReturnValue(textUntilCursor);
|
||||
mockModel.getWordUntilPosition.mockReturnValue({
|
||||
word,
|
||||
startColumn: 1,
|
||||
endColumn: word.length + 1,
|
||||
});
|
||||
return provide(mockModel, { lineNumber: 1, column: word.length + 1 });
|
||||
};
|
||||
const invokeKw = (t: string, w: string) => invokeWith(provideKw, t, w);
|
||||
const invokeSug = (t: string, w: string) => invokeWith(provideSug, t, w);
|
||||
const invokeFallback = (t: string, w: string) => invokeWith(provideFallback, t, w);
|
||||
|
||||
const itemFor = (result: any, label: string) =>
|
||||
result.suggestions.find((s: any) => s.label === label);
|
||||
|
||||
it("answers for its own model", () => {
|
||||
expect(provideKw).toBeTypeOf("function");
|
||||
expect(invokeKw("SELECT ", "").suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("A5 — snippet keywords carry the NUMERIC InsertAsSnippet flag", () => {
|
||||
const item = itemFor(invokeKw("where ", ""), "like");
|
||||
expect(item).toBeDefined();
|
||||
expect(typeof item.insertTextRules).toBe("number");
|
||||
expect(item.insertTextRules).toBe(4);
|
||||
});
|
||||
|
||||
it("A5 — plain keywords carry no snippet flag", () => {
|
||||
expect(itemFor(invokeKw("where ", ""), "and").insertTextRules).toBeUndefined();
|
||||
});
|
||||
|
||||
it("N7 — detail/documentation/sortText survive the push", () => {
|
||||
const item = itemFor(invokeSug("SELECT ", ""), "approx_topk");
|
||||
expect(item).toBeDefined();
|
||||
expect(item.detail).toBe("(field, k) → top-k values");
|
||||
expect(item.documentation).toBe("Approximate top-k.");
|
||||
expect(item.sortText).toBe("zz-approx_topk");
|
||||
});
|
||||
|
||||
it("N7/A5 — suggestion snippets reach monaco as a number too", () => {
|
||||
expect(itemFor(invokeSug("SELECT ", ""), "approx_topk").insertTextRules).toBe(4);
|
||||
});
|
||||
|
||||
it("A1 — a Function-kind suggestion maps to monaco kind 1", () => {
|
||||
expect(itemFor(invokeSug("SELECT ", ""), "approx_topk").kind).toBe(1);
|
||||
});
|
||||
|
||||
it("C1 — subsequence candidates are not dropped by a substring pre-filter", () => {
|
||||
// "knn" is a subsequence of kubernetes_namespace_name but NOT a substring,
|
||||
// so String.includes() removes it before monaco can score it.
|
||||
expect(itemFor(invokeKw("SELECT knn", "knn"), "kubernetes_namespace_name")).toBeDefined();
|
||||
});
|
||||
|
||||
it("A2 — items are identical no matter how much of the word is typed", () => {
|
||||
const a = itemFor(invokeSug("SELECT a", "a"), "approx_topk");
|
||||
const appr = itemFor(invokeSug("SELECT appr", "appr"), "approx_topk");
|
||||
expect(a.label).toBe(appr.label);
|
||||
expect(a.insertText).toBe(appr.insertText);
|
||||
});
|
||||
|
||||
it("N2 — the SQL fallback serves the shared catalog, aggregates included", () => {
|
||||
const labels = invokeFallback("SELECT ", "").suggestions.map((s: any) => s.label);
|
||||
// The component's old 7-entry local list had none of these.
|
||||
for (const agg of ["sum", "avg", "count", "max", "min", "histogram", "approx_topk"]) {
|
||||
expect(labels, `fallback missing ${agg}`).toContain(agg);
|
||||
}
|
||||
});
|
||||
|
||||
it("N2 — the SQL fallback also serves the array family", () => {
|
||||
const labels = invokeFallback("SELECT ", "").suggestions.map((s: any) => s.label);
|
||||
for (const fn of ["arrcount", "arrsort", "arrjoin", "arrzip", "spath"]) {
|
||||
expect(labels, `fallback missing ${fn}`).toContain(fn);
|
||||
}
|
||||
});
|
||||
|
||||
it("N2 — fallback items are Function-kinded and snippet-enabled", () => {
|
||||
const sum = invokeFallback("SELECT ", "").suggestions.find((s: any) => s.label === "sum");
|
||||
expect(sum).toBeDefined();
|
||||
expect(sum.kind).toBe(1);
|
||||
expect(sum.insertTextRules).toBe(4);
|
||||
expect(sum.insertText).toBe("sum(${1:field})");
|
||||
});
|
||||
|
||||
it("A4 — legacy callables get monaco's word, not a space-split token", () => {
|
||||
// Old code did textUntilPosition.trim().split(" ").pop() => "*\nFROM".
|
||||
const labels = invokeSug("SELECT *\nFROM", "FROM").suggestions.map((s: any) => s.label);
|
||||
expect(labels).toContain("legacy('FROM')");
|
||||
expect(labels.join("|")).not.toContain("\n");
|
||||
});
|
||||
});
|
||||
|
|
@ -851,198 +851,3 @@ 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 (7 entries vs the composable's 26) — a shipped
|
||||
// divergence. The component must delegate that fallback to the shared catalog.
|
||||
//
|
||||
// NOTE: the fallback computeds are NOT reachable via wrapper.vm — `suggestions`
|
||||
// and `keywords` are PROP names, so vm.suggestions returns the prop (verified:
|
||||
// null in, null out). The resolution logic is therefore unit-tested directly in
|
||||
// src/utils/query/sqlCompletion.spec.ts; here we only assert the component
|
||||
// delegates to it rather than carrying its own copy.
|
||||
|
||||
describe("N2/D7 — the component delegates its suggestion fallback", () => {
|
||||
it("imports the shared catalog resolvers", async () => {
|
||||
const mod = await import("@/utils/query/sqlCompletion");
|
||||
expect(typeof mod.resolveSuggestions).toBe("function");
|
||||
expect(typeof mod.resolveKeywords).toBe("function");
|
||||
});
|
||||
|
||||
it("the shared fallback carries the aggregates Traces was missing", async () => {
|
||||
const { resolveSuggestions } = await import("@/utils/query/sqlCompletion");
|
||||
const names = (resolveSuggestions("sql", null) 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("mounts with suggestions=null without error (uses the shared fallback)", () => {
|
||||
const wrapper = createWrapper({ suggestions: null, language: "sql" });
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Phase 1 integration (tmp/code.md A2/A4/A5/N7/C1) ─────────────────────────
|
||||
// The pure helper specs in src/utils/query/sqlCompletion.spec.ts can all pass
|
||||
// while CodeQueryEditor keeps its own split(" ") tokenisation, String.includes
|
||||
// pre-filter, singular `insertTextRule` lookup and four-field push. These tests
|
||||
// drive the REGISTERED provider so the component itself is on the hook.
|
||||
//
|
||||
// Mounted ONCE in beforeAll. The suite's other async-mount tests (Ctrl+Enter,
|
||||
// setValue) are already load-flaky on main — they time out on ~1 run in 2 with
|
||||
// no source changes at all — so we do not add nine more waitFor mounts here.
|
||||
|
||||
describe("Phase 1 — the registered completion provider", () => {
|
||||
const providerStore = createStore({ state: { theme: "light" } });
|
||||
// Two providers so a throw on the suggestions path cannot mask the
|
||||
// keyword-path signals (A5/C1) — otherwise every assertion here fails for
|
||||
// the same single reason and only one bug is observable at a time.
|
||||
let provideKw: Function;
|
||||
let provideSug: Function;
|
||||
let getElementByIdSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
const KEYWORDS = [
|
||||
{
|
||||
name: "like",
|
||||
label: "like",
|
||||
kind: "Keyword",
|
||||
insertText: "like '%${1:params}%' ",
|
||||
insertTextRules: "InsertAsSnippet",
|
||||
},
|
||||
{
|
||||
name: "kubernetes_namespace_name",
|
||||
label: "kubernetes_namespace_name",
|
||||
kind: "Field",
|
||||
insertText: "kubernetes_namespace_name",
|
||||
sortText: "\u0000kubernetes_namespace_name",
|
||||
},
|
||||
{ name: "and", label: "and", kind: "Keyword", insertText: "and " },
|
||||
];
|
||||
|
||||
const SUGGESTIONS = [
|
||||
{
|
||||
name: "approx_topk",
|
||||
label: "approx_topk",
|
||||
kind: "Function",
|
||||
detail: "(field, k) → top-k values",
|
||||
documentation: "Approximate top-k.",
|
||||
insertText: "approx_topk(${1:field}, ${2:10})",
|
||||
insertTextRules: "InsertAsSnippet",
|
||||
sortText: "zz-approx_topk",
|
||||
},
|
||||
// Legacy callable shape — the `suggestions` prop is public API.
|
||||
{
|
||||
label: (kw: string) => `legacy('${kw}')`,
|
||||
kind: "Text",
|
||||
insertText: (kw: string) => `legacy('${kw}')`,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
const monacoApi = await import("monaco-editor/esm/vs/editor/editor.api");
|
||||
const registerFn = vi.mocked(monacoApi.languages.registerCompletionItemProvider);
|
||||
|
||||
getElementByIdSpy = vi.spyOn(document, "getElementById").mockImplementation(() =>
|
||||
document.createElement("div"),
|
||||
);
|
||||
|
||||
const mountAndCapture = async (editorId: string, props: any) => {
|
||||
const baseline = registerFn.mock.calls.length;
|
||||
mount(CodeQueryEditor, {
|
||||
props: { editorId, language: "sql", query: "SELECT * FROM logs", ...props },
|
||||
global: { plugins: [providerStore] },
|
||||
});
|
||||
await vi.waitFor(() => expect(registerFn.mock.calls.length).toBeGreaterThan(baseline), {
|
||||
timeout: 15000,
|
||||
interval: 25,
|
||||
});
|
||||
return registerFn.mock.calls[baseline][1].provideCompletionItems as Function;
|
||||
};
|
||||
|
||||
provideKw = await mountAndCapture("provider-kw", {
|
||||
keywords: KEYWORDS,
|
||||
suggestions: [],
|
||||
});
|
||||
provideSug = await mountAndCapture("provider-sug", {
|
||||
keywords: [],
|
||||
suggestions: SUGGESTIONS,
|
||||
});
|
||||
// editorObj must exist so the provider's own-model guard resolves to mockModel.
|
||||
await vi.waitFor(() => expect(mockEditorObj.createContextKey).toHaveBeenCalled(), {
|
||||
timeout: 15000,
|
||||
interval: 25,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => getElementByIdSpy?.mockRestore());
|
||||
|
||||
/** Invoke a provider as monaco would, for a given buffer + current word. */
|
||||
const invokeWith = (provide: Function, textUntilCursor: string, word: string) => {
|
||||
mockModel.getValueInRange.mockReturnValue(textUntilCursor);
|
||||
mockModel.getWordUntilPosition.mockReturnValue({
|
||||
word,
|
||||
startColumn: 1,
|
||||
endColumn: word.length + 1,
|
||||
});
|
||||
return provide(mockModel, { lineNumber: 1, column: word.length + 1 });
|
||||
};
|
||||
const invokeKw = (t: string, w: string) => invokeWith(provideKw, t, w);
|
||||
const invokeSug = (t: string, w: string) => invokeWith(provideSug, t, w);
|
||||
|
||||
const itemFor = (result: any, label: string) =>
|
||||
result.suggestions.find((s: any) => s.label === label);
|
||||
|
||||
it("answers for its own model", () => {
|
||||
expect(provideKw).toBeTypeOf("function");
|
||||
expect(invokeKw("SELECT ", "").suggestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("A5 — snippet keywords carry the NUMERIC InsertAsSnippet flag", () => {
|
||||
const item = itemFor(invokeKw("where ", ""), "like");
|
||||
expect(item).toBeDefined();
|
||||
expect(typeof item.insertTextRules).toBe("number");
|
||||
expect(item.insertTextRules).toBe(4);
|
||||
});
|
||||
|
||||
it("A5 — plain keywords carry no snippet flag", () => {
|
||||
expect(itemFor(invokeKw("where ", ""), "and").insertTextRules).toBeUndefined();
|
||||
});
|
||||
|
||||
it("N7 — detail/documentation/sortText survive the push", () => {
|
||||
const item = itemFor(invokeSug("SELECT ", ""), "approx_topk");
|
||||
expect(item).toBeDefined();
|
||||
expect(item.detail).toBe("(field, k) → top-k values");
|
||||
expect(item.documentation).toBe("Approximate top-k.");
|
||||
expect(item.sortText).toBe("zz-approx_topk");
|
||||
});
|
||||
|
||||
it("N7/A5 — suggestion snippets reach monaco as a number too", () => {
|
||||
expect(itemFor(invokeSug("SELECT ", ""), "approx_topk").insertTextRules).toBe(4);
|
||||
});
|
||||
|
||||
it("A1 — a Function-kind suggestion maps to monaco kind 1", () => {
|
||||
expect(itemFor(invokeSug("SELECT ", ""), "approx_topk").kind).toBe(1);
|
||||
});
|
||||
|
||||
it("C1 — subsequence candidates are not dropped by a substring pre-filter", () => {
|
||||
// "knn" is a subsequence of kubernetes_namespace_name but NOT a substring,
|
||||
// so String.includes() removes it before monaco can score it.
|
||||
expect(itemFor(invokeKw("SELECT knn", "knn"), "kubernetes_namespace_name")).toBeDefined();
|
||||
});
|
||||
|
||||
it("A2 — items are identical no matter how much of the word is typed", () => {
|
||||
const a = itemFor(invokeSug("SELECT a", "a"), "approx_topk");
|
||||
const appr = itemFor(invokeSug("SELECT appr", "appr"), "approx_topk");
|
||||
expect(a.label).toBe(appr.label);
|
||||
expect(a.insertText).toBe(appr.insertText);
|
||||
});
|
||||
|
||||
it("A4 — legacy callables get monaco's word, not a space-split token", () => {
|
||||
// Old code did textUntilPosition.trim().split(" ").pop() => "*\nFROM".
|
||||
const labels = invokeSug("SELECT *\nFROM", "FROM").suggestions.map((s: any) => s.label);
|
||||
expect(labels).toContain("legacy('FROM')");
|
||||
expect(labels.join("|")).not.toContain("\n");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue