test(editor): fix hover model plumbing; require the C4 resolver end to end

Both from review were right.

1. HOVER STUBS WERE ON THE WRONG MODEL. The provider is invoked with the
   editor's own model but getWordAtPosition was still configured on the
   module-level mockModel, so a correct per-model provider would see an empty
   word: the positive test fails and the unknown-word test passes for the wrong
   reason. Rewired both onto the invoked model.

   Worth naming: my previous commit intended this fix. The replace silently did
   nothing because prettier had wrapped those lines, and I did not assert the
   substitution landed. Third time an unasserted string replace has quietly
   no-opped in this workstream; this one is asserted, and I checked no
   mockModel reference survives in either describe.

2. C4 PROVED THE COMPONENT, NOT THE WIRING. The provider test injects
   fieldValueResolver straight into CodeQueryEditor. Every production caller
   could still supply nothing and the component-level test would stay green —
   the identical shape of the Alerts binding, the SLO catalog load and the
   Traces suggestions prop, three gaps that each shipped and were each reported
   from the running app.

   So the resolver is specified to come from the composable every surface
   already uses (five tests: exposed, returns stored values, in-session values
   first, empty on lookup failure, empty with no stream context), and
   editorWiring.spec.ts now requires every editor host to bind it. That
   structural check currently names all 13 surfaces, which is the point: it
   fails per file with the path, rather than waiting for a bug report.

18 passing, 12 red in the provider suite for their intended reasons; the wiring
guard adds 13 more, all naming the surface they cover.
This commit is contained in:
Prabhat Sharma 2026-08-02 15:13:29 -07:00
parent 54d5985be8
commit 562ad79195
3 changed files with 74 additions and 19 deletions

View File

@ -590,7 +590,7 @@ describe("Phase 3 — providers return the shapes monaco requires", () => {
const provider = vi
.mocked((api.languages as any).registerHoverProvider)
.mock.calls.at(-1)![1];
mockModel.getWordAtPosition.mockReturnValue({
model.getWordAtPosition.mockReturnValue({
word: "host_name",
startColumn: 1,
endColumn: 10,
@ -605,7 +605,7 @@ describe("Phase 3 — providers return the shapes monaco requires", () => {
it("D3 — returns null for a word it knows nothing about", { timeout: 30000 }, async () => {
const { api, model } = await mountAndGet();
const provider = vi.mocked((api.languages as any).registerHoverProvider).mock.calls.at(-1)![1];
mockModel.getWordAtPosition.mockReturnValue({
model.getWordAtPosition.mockReturnValue({
word: "zzz_unknown",
startColumn: 1,
endColumn: 12,

View File

@ -612,9 +612,10 @@ describe("org VRL functions are offered exactly once", () => {
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",
);
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 () => {
@ -646,3 +647,48 @@ describe("org VRL functions are offered exactly once", () => {
expect(hits[0].insertText).toBe("my_vrl_fn('${1:value}')");
});
});
// ─── Phase 3 C4: the composable must expose the value resolver ───────────────
// The provider-level test injects a fieldValueResolver straight into
// CodeQueryEditor, which proves the component can use one but not that anything
// supplies it. Alerts bound the wrong list, the SLO form never loaded the
// catalog, and Traces omitted a prop — all "the helper works, nobody calls it".
// The resolver therefore comes from the composable every surface already uses.
describe("Phase 3 — resolveFieldValues is exposed for the editor to await", () => {
beforeEach(() => vi.clearAllMocks());
it("exposes a resolver", () => {
const c = makeComposable({ storedValues: [] });
expect(typeof (c as any).resolveFieldValues).toBe("function");
});
it("returns the stored values for a field", async () => {
const c = makeComposable({ storedValues: ["error", "warn"] });
await expect((c as any).resolveFieldValues("level")).resolves.toEqual(
expect.arrayContaining(["error", "warn"]),
);
});
it("merges in-session values ahead of stored ones", async () => {
const c = makeComposable({
storedValues: ["stored_only"],
inSessionValues: { level: ["fresh"] },
});
const values = await (c as any).resolveFieldValues("level");
expect(values[0]).toBe("fresh");
expect(values).toContain("stored_only");
});
it("resolves to an empty list rather than throwing when the lookup fails", async () => {
const c = makeComposable({ storedValues: [] });
vi.mocked(getFieldValuesForSuggestion).mockRejectedValueOnce(new Error("idb down"));
await expect((c as any).resolveFieldValues("level")).resolves.toEqual([]);
});
it("resolves to an empty list when no stream context is set", async () => {
const c = makeComposable({ storedValues: ["error"] });
c.autoCompleteData.value.streamName = "";
await expect((c as any).resolveFieldValues("level")).resolves.toEqual([]);
});
});

View File

@ -55,20 +55,29 @@ describe("editor wiring — every surface supplies both completion sources", ()
expect(editorHosts.length).toBeGreaterThan(8);
});
it.each(editorHosts.map((f) => f.path))(
"%s binds :suggestions as well as :keywords",
(path) => {
const { source } = editorHosts.find((f) => f.path === path)!;
// Omitting :suggestions is not inert — CodeQueryEditor falls back to the
// STATIC local catalog, so the surface silently loses every function the
// server reports. That is exactly how Traces ended up short.
expect(
/:suggestions\s*=/.test(source),
`${path} binds :keywords but not :suggestions, so it falls back to the ` +
`static catalog and loses the server-supplied functions`,
).toBe(true);
},
);
it.each(editorHosts.map((f) => f.path))("%s binds :suggestions as well as :keywords", (path) => {
const { source } = editorHosts.find((f) => f.path === path)!;
// Omitting :suggestions is not inert — CodeQueryEditor falls back to the
// STATIC local catalog, so the surface silently loses every function the
// server reports. That is exactly how Traces ended up short.
expect(
/:suggestions\s*=/.test(source),
`${path} binds :keywords but not :suggestions, so it falls back to the ` +
`static catalog and loses the server-supplied functions`,
).toBe(true);
});
it.each(editorHosts.map((f) => f.path))("%s supplies a field-value resolver", (path) => {
const { source } = editorHosts.find((f) => f.path === path)!;
// C4 moves the field-VALUE lookup inside the provider, which can only
// await a resolver something hands it. A surface that omits this gets a
// working editor with no value completion — silently, exactly like the
// three prop-wiring gaps before it.
expect(
/:field-value-resolver\s*=|:fieldValueResolver\s*=/.test(source),
`${path} mounts an editor without a field-value resolver`,
).toBe(true);
});
it.each(editorHosts.map((f) => f.path))("%s does not bind the base keyword list", (path) => {
const { source } = editorHosts.find((f) => f.path === path)!;