fix(editor): stop offering org VRL functions twice

Org transforms reach the editor by two paths: updateFunctionKeywords puts them
in autoCompleteKeywords (the `keywords` prop) and the server catalog reports the
same transforms into autoCompleteSuggestions (the `suggestions` prop). Monaco
concatenates both, so every org function appeared twice in Logs and Dashboards.

Worse than plain duplication: the two entries disagree on how to insert. The
legacy path emits my_fn('${1:value}') with quoted arguments; the server catalog
emits my_fn(${1:arg1}) unquoted. The user saw two identical labels that typed
different text.

The server catalog is now filtered against functionKeywords by name. The
keywords path wins because its argument quoting is what has always shipped —
changing that is a separate decision, not something to smuggle into a dedup fix.

Three tests: the org function stays in keywords and leaves suggestions, genuinely
server-only functions (date_trunc) still arrive, and building the item list the
way CodeQueryEditor does yields exactly one entry carrying the legacy quoting.

web 6823 passing.
This commit is contained in:
Prabhat Sharma 2026-08-02 13:33:45 -07:00
parent 2496f1d45d
commit 75ba683ef4
2 changed files with 79 additions and 1 deletions

View File

@ -582,3 +582,67 @@ describe("Phase 2 — server-supplied functions reach the suggestion list (B4)",
expect((c.effectiveSuggestions.value as any[]).length).toBeGreaterThan(0);
});
});
// ─── Org VRL functions arrive by TWO paths ───────────────────────────────────
// updateFunctionKeywords puts them in autoCompleteKeywords (the `keywords`
// prop) and the server catalog puts the same names in autoCompleteSuggestions
// (the `suggestions` prop). Monaco concatenates both, so the user sees each org
// function twice — and the two entries disagree on quoting, so the duplicates
// insert different text.
describe("org VRL functions are offered exactly once", () => {
beforeEach(() => vi.clearAllMocks());
const seedBothPaths = (c: ReturnType<typeof useSqlSuggestions>) => {
// legacy path (Logs/Traces/Dashboards fetch these and pass args)
c.updateFunctionKeywords([{ name: "my_vrl_fn", args: "('${1:value}')" }]);
// server catalog reports the very same org transform
c.setServerFunctions([
{ name: "my_vrl_fn", signature: "(arg1)", doc: "Org function.", kind: "vrl" },
{ name: "date_trunc", signature: "(precision, timestamp)", doc: "T.", kind: "scalar" },
]);
};
it("keeps the org function in keywords and drops it from suggestions", async () => {
const c = makeComposable({ storedValues: [] });
seedBothPaths(c);
await run(c, "SELECT * FROM stream WHERE ");
const keywords = c.effectiveKeywords.value.map((k: any) => k.label);
const suggestions = (c.effectiveSuggestions.value as any[]).map((s) => s.name);
expect(keywords).toContain("my_vrl_fn");
expect(suggestions, "server catalog re-added a function the keywords already carry").not.toContain(
"my_vrl_fn",
);
});
it("still adds server functions the keywords path does NOT carry", async () => {
const c = makeComposable({ storedValues: [] });
seedBothPaths(c);
await run(c, "SELECT * FROM stream WHERE ");
const suggestions = (c.effectiveSuggestions.value as any[]).map((s) => s.name);
expect(suggestions).toContain("date_trunc");
});
it("offers exactly one completion item for the org function", async () => {
const { buildCompletionItems } = await import("@/utils/query/sqlCompletion");
const c = makeComposable({ storedValues: [] });
seedBothPaths(c);
await run(c, "SELECT * FROM stream WHERE ");
// Mirrors what CodeQueryEditor hands monaco: keywords ++ suggestions.
const items = buildCompletionItems({
keywords: c.effectiveKeywords.value as any[],
suggestions: c.effectiveSuggestions.value as any[],
word: "",
range: {},
kinds: { Function: 1, Keyword: 17, Field: 3, Operator: 11, Value: 13, Text: 18 },
insertTextRules: { InsertAsSnippet: 4 },
});
const hits = items.filter((i: any) => i.label === "my_vrl_fn");
expect(hits).toHaveLength(1);
// The surviving entry keeps the legacy quoting that has always shipped.
expect(hits[0].insertText).toBe("my_vrl_fn('${1:value}')");
});
});

View File

@ -343,7 +343,21 @@ const useSqlSuggestions = () => {
// reassigned here — predicates and clauses would otherwise collapse into
// one lane and interleave.
autoCompleteKeywords.value.push(...defaultKeywords);
autoCompleteSuggestions.value = mergeServerFunctions(defaultSuggestions, serverFunctions.value);
// The org's own VRL transforms reach the editor through functionKeywords
// above (the `keywords` prop). The server catalog reports the same
// transforms, and monaco concatenates keywords with suggestions — so
// without this filter every org function is offered twice, by two entries
// that disagree on quoting. The keywords path wins: its argument quoting is
// what has always shipped.
const alreadyOffered = new Set(
functionKeywords.value.map((f: any) => String(f.label).toLowerCase()),
);
autoCompleteSuggestions.value = mergeServerFunctions(
defaultSuggestions,
(serverFunctions.value as any[]).filter(
(f: any) => !alreadyOffered.has(String(f?.name).toLowerCase()),
),
);
};
// Shared helper — builds the field keyword array from a fields list,