diff --git a/web/src/components/alerts/QueryEditorDialog.spec.ts b/web/src/components/alerts/QueryEditorDialog.spec.ts index 9505081550..1d4792347f 100644 --- a/web/src/components/alerts/QueryEditorDialog.spec.ts +++ b/web/src/components/alerts/QueryEditorDialog.spec.ts @@ -541,20 +541,27 @@ describe("QueryEditorDialog - ODrawer Migration", () => { }); }); -// ─── Phase 1 (tmp/code.md N1) ───────────────────────────────────────────────── -// QueryEditorDialog wires the whole autocomplete pipeline in handleQueryUpdate -// (query, cursorIndex, org/stream context, popup.open, getSuggestions) but binds -// :keywords="autoCompleteKeywords" — the BASE list — instead of effectiveKeywords. +// ─── Phase 1 (N1) / Phase 3 (C4) — autocomplete reaches the editor ─────────── +// These two tests used to drive a VALUE context and assert that values arrived +// through `keywords` while `suggestions` went blank. That was the only way to +// tell effectiveKeywords from autoCompleteKeywords, which are identical unless +// a context is active — and it was a fair test until the value round trip was +// removed: the completion PROVIDER now resolves values itself, so nothing +// pushes them through the props any more. // -// Comparing the bound prop against vm.effectiveKeywords would pass even with the -// bug, because effectiveKeywords returns autoCompleteKeywords verbatim when no -// context is active. Drive a real VALUE context instead. +// What replaces each half: +// - the N1 invariant (bind the context-aware list, never the base one) is now +// enforced for EVERY editor host in utils/query/editorWiring.spec.ts, which +// needs no context to make the distinction visible; +// - what this file can still prove is its own wiring — that the resolver the +// provider awaits arrives, and resolves against the stream context the +// dialog sets. -describe("QueryEditorDialog - N1 context keywords reach the editor", () => { +describe("QueryEditorDialog - autocomplete wiring reaches the editor", () => { const editorStubDef = { name: "UnifiedQueryEditor", template: '
', - props: ["query", "keywords", "suggestions"], + props: ["query", "keywords", "suggestions", "fieldValueResolver"], emits: ["update:query", "blur", "focus", "language-change", "ask-ai", "run-query"], methods: { // handleQueryUpdate reads these off queryEditorRef. @@ -594,8 +601,7 @@ describe("QueryEditorDialog - N1 context keywords reach the editor", () => { name: "ODrawer", props: ["open", "size", "showClose", "bleed", "persistent", "title", "width"], emits: ["update:open"], - template: - "
", + template: "
", }, FullViewContainer: { template: "
", @@ -612,27 +618,35 @@ describe("QueryEditorDialog - N1 context keywords reach the editor", () => { expect(wrapper.findComponent({ name: "UnifiedQueryEditor" }).exists()).toBe(true); }); - it("delivers field VALUES to the editor once the cursor is after an operator", async () => { + it("hands the editor a resolver that produces the stored values", async () => { const wrapper = mountWithStub(); const editor = wrapper.findComponent({ name: "UnifiedQueryEditor" }); + // Drives handleQueryUpdate, which is where the dialog sets the org/stream + // context the lookup is keyed on. Without that the resolver returns [] and + // this fails — which is the wiring worth guarding. await editor.vm.$emit("update:query", "level = "); await flushPromises(); - const delivered = (editor.props("keywords") ?? []) as any[]; - expect(delivered.some((k) => k.kind === "Value")).toBe(true); + const resolve = editor.props("fieldValueResolver") as (f: string) => Promise; + expect(typeof resolve, "no resolver reached the editor").toBe("function"); + await expect(resolve("level")).resolves.toEqual(expect.arrayContaining(["error", "warn"])); }); - it("blanks the function suggestions while in value context", async () => { + it("leaves the catalog suggestions alone in a value position", async () => { + // The provider swaps the whole list out for values itself. The parent + // blanking the catalog was part of the removed round trip, and doing it + // here would now take the functions away for no one's benefit. const wrapper = mountWithStub(); const editor = wrapper.findComponent({ name: "UnifiedQueryEditor" }); await editor.vm.$emit("update:query", "level = "); await flushPromises(); - expect(editor.props("suggestions")).toEqual([]); + const suggestions = (editor.props("suggestions") ?? []) as any[]; + expect(suggestions.some((s: any) => s.name === "match_all")).toBe(true); }); }); -// Stored field values for the N1 value-context probe above. Only useSuggestions +// Stored field values for the resolver test above. Only useSuggestions // consumes this module, so mocking it does not affect the rest of the suite. vi.mock("@/composables/fieldValueStore", () => ({ getFieldValuesForSuggestion: vi.fn().mockResolvedValue(["error", "warn"]), diff --git a/web/src/utils/query/editorProviders.spec.ts b/web/src/utils/query/editorProviders.spec.ts index 1f7a8802b8..5b651d1dcb 100644 --- a/web/src/utils/query/editorProviders.spec.ts +++ b/web/src/utils/query/editorProviders.spec.ts @@ -528,7 +528,7 @@ describe("rankNumericFieldsFirst", () => { }, { label: "value", kind: "Field", detail: "Float64", sortText: `${FIELD_LANE}value` }, { label: "duration_ms", kind: "Field", detail: "Int64", sortText: `${FIELD_LANE}duration_ms` }, - { label: "avg", kind: "Function", detail: "(field)", sortText: "avg" }, + { label: "avg", kind: "Function", detail: "(field)", sortText: "\u0001avg" }, ] as any[]; // localeCompare ignores the control characters the lanes are built from, so diff --git a/web/src/utils/query/sqlCompletion.spec.ts b/web/src/utils/query/sqlCompletion.spec.ts index 37ea33e2e1..dd44bc226e 100644 --- a/web/src/utils/query/sqlCompletion.spec.ts +++ b/web/src/utils/query/sqlCompletion.spec.ts @@ -331,7 +331,7 @@ describe("N7 — optional metadata is forwarded to monaco", () => { documentation: "Approximate top-k aggregation.", insertText: "approx_topk(${1:field}, ${2:10})", insertTextRules: "InsertAsSnippet", - sortText: "approx_topk", + sortText: "\u0002approx_topk", }; it("forwards detail", () => { @@ -347,7 +347,7 @@ describe("N7 — optional metadata is forwarded to monaco", () => { }); it("forwards sortText", () => { - expect(build({ suggestions: [rich] })[0].sortText).toBe("approx_topk"); + expect(build({ suggestions: [rich] })[0].sortText).toBe("\u0002approx_topk"); }); it("attaches the supplied range to every item", () => {