c944870566
Extensions calling ctx.ui.addAutocompleteProvider (e.g. @ff-labs/pi-fff) crashed at load with 'TypeError: ... is not a function' because omp's ExtensionAPI.ui omitted pi's autocomplete-provider API; the throw also aborted the rest of a try/catch-guarded session_start init. ExtensionUIContext now declares addAutocompleteProvider(factory). Interactive mode stacks each factory on the built-in editor provider in registration order, re-applies the stack on every slash-command refresh, and skips throwing/malformed factories; RPC, ACP, and headless contexts accept the factory as a no-op, matching upstream pi's RPC behavior. Fixes #4919
75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
import { describe, expect, it, vi } from "bun:test";
|
|
import type { ExtensionUIContext } from "../../extensibility/extensions";
|
|
import { CustomEditor } from "../components/custom-editor";
|
|
import { getEditorTheme } from "../theme/theme";
|
|
import type { InteractiveModeContext } from "../types";
|
|
import { ExtensionUiController } from "./extension-ui-controller";
|
|
|
|
function makeHarness() {
|
|
const editor = new CustomEditor(getEditorTheme());
|
|
const requestRender = vi.fn();
|
|
const addAutocompleteProvider = vi.fn();
|
|
let uiContext: ExtensionUIContext | undefined;
|
|
const ctx = {
|
|
editor,
|
|
ui: {
|
|
requestRender,
|
|
},
|
|
session: {
|
|
extensionRunner: undefined,
|
|
},
|
|
setToolUIContext(context: ExtensionUIContext, hasUI: boolean): void {
|
|
expect(hasUI).toBe(true);
|
|
uiContext = context;
|
|
},
|
|
addAutocompleteProvider,
|
|
} as unknown as InteractiveModeContext;
|
|
|
|
return {
|
|
editor,
|
|
requestRender,
|
|
addAutocompleteProvider,
|
|
async init(): Promise<ExtensionUIContext> {
|
|
await new ExtensionUiController(ctx).initHooksAndCustomTools();
|
|
expect(uiContext).toBeDefined();
|
|
return uiContext!;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("ExtensionUiController editor UI", () => {
|
|
it("requests a render after extension pasteToEditor mutates the prompt", async () => {
|
|
const harness = makeHarness();
|
|
const ui = await harness.init();
|
|
|
|
ui.pasteToEditor("hello");
|
|
ui.pasteToEditor(" world");
|
|
|
|
expect(harness.editor.getText()).toBe("hello world");
|
|
expect(harness.requestRender).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("requests a render after extension setEditorText replaces the prompt", async () => {
|
|
const harness = makeHarness();
|
|
const ui = await harness.init();
|
|
|
|
ui.setEditorText("hello");
|
|
|
|
expect(harness.editor.getText()).toBe("hello");
|
|
expect(harness.requestRender).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("bridges addAutocompleteProvider factories to the interactive mode context (#4919)", async () => {
|
|
const harness = makeHarness();
|
|
const ui = await harness.init();
|
|
|
|
expect(typeof ui.addAutocompleteProvider).toBe("function");
|
|
|
|
const factory = (current: unknown) => current as never;
|
|
ui.addAutocompleteProvider(factory);
|
|
|
|
expect(harness.addAutocompleteProvider).toHaveBeenCalledTimes(1);
|
|
expect(harness.addAutocompleteProvider).toHaveBeenCalledWith(factory);
|
|
});
|
|
});
|