test(editor): repair two specs the removed value path left behind

Both were fair tests until the value round trip was deleted. They drove a VALUE
context and asserted the values arrived through `keywords` while `suggestions`
went blank -- which was the only way to tell effectiveKeywords from
autoCompleteKeywords, since the two are identical unless a context is active.
The provider now resolves values itself, so nothing pushes them through props.

Replaced, not deleted:
  - the N1 invariant they guarded (bind the context-aware list, never the base
    one) is enforced for EVERY editor host in editorWiring.spec.ts, which needs
    no context to make the distinction visible;
  - what this file can still prove is its own wiring: the resolver reaches the
    editor and resolves against the stream context the dialog sets. Verified
    non-vacuous -- removing the :field-value-resolver binding fails it.
  - "blanks the suggestions" is now the OPPOSITE assertion, because blanking
    them was part of the removed round trip: the catalog must survive.

Found by an external review, not by me, because I ran only the specs I had
touched. The full suite also surfaced a second miss: sourceHygiene.spec.ts, a
guard this repo already had, was failing on three RAW C0 control characters I
had committed in sortText fixtures -- \x01 in editorProviders.spec.ts and \x02
twice in sqlCompletion.spec.ts, now written as escapes. That is the third time
today a raw control character got into source from my editing path; the repo has
a test for it and I was not running it.

Full suite: 40367 total, 39935 passed, 24 failed -- 9 load-flaky
CodeQueryEditor and 15 synthetics journey specs, all confirmed pre-existing.
Zero related to this workstream, which was not true before this commit.
This commit is contained in:
Prabhat Sharma 2026-08-02 17:18:46 -07:00
parent 449e2d2dda
commit ff89c82c08
3 changed files with 34 additions and 20 deletions

View File

@ -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: '<div class="stub-kw-editor" />',
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:
"<div><slot name='header-left' /><slot name='header-right' /><slot /></div>",
template: "<div><slot name='header-left' /><slot name='header-right' /><slot /></div>",
},
FullViewContainer: {
template: "<div><slot /><slot name='right' /></div>",
@ -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<string[]>;
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"]),

View File

@ -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

View File

@ -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", () => {