From 75ba683ef41af83635fa48788de0b2026de5bbac Mon Sep 17 00:00:00 2001 From: Prabhat Sharma Date: Sun, 2 Aug 2026 13:33:45 -0700 Subject: [PATCH] fix(editor): stop offering org VRL functions twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/src/composables/useSuggestions.spec.ts | 64 ++++++++++++++++++++++ web/src/composables/useSuggestions.ts | 16 +++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/web/src/composables/useSuggestions.spec.ts b/web/src/composables/useSuggestions.spec.ts index 072975cea8..cba867e371 100644 --- a/web/src/composables/useSuggestions.spec.ts +++ b/web/src/composables/useSuggestions.spec.ts @@ -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) => { + // 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}')"); + }); +}); diff --git a/web/src/composables/useSuggestions.ts b/web/src/composables/useSuggestions.ts index d8d305c0bf..bfcabf999e 100644 --- a/web/src/composables/useSuggestions.ts +++ b/web/src/composables/useSuggestions.ts @@ -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,