FN-7699: apply cursorCliBinaryPath override to model-picker discovery
Thread the machine-local cursorCliBinaryPath operator override into /api/models Cursor CLI discovery so the model picker spawns the same cursor-agent binary already validated by sign-in/status/probe.
- register-model-routes.ts reads globalSettings.cursorCliBinaryPath, trims and normalizes blank to undefined (preserving PATH auto-detection)
- passes the normalized binaryPath through to getCursorPickerModels({ binaryPath }) for model discovery
- adds regression tests covering override-set and override-blank/unset behavior in register-model-routes-cursor-cli.test.ts
- adds changeset fn-7699-cursor-cli-binary-path-model-picker.md (patch, fix) documenting the follow-up to FN-7696
Files changed:
$(cat /tmp/diffstat.txt)
Fusion-Task-Id: FN-7699
Fusion-Task-Lineage: 9895af8e-447d-425b-af58-1af4748c013c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: The Cursor CLI binary path override now also applies to the model picker, not just sign-in/status.
|
||||
category: fix
|
||||
dev: /api/models reads globalSettings.cursorCliBinaryPath (trim/blank→undefined) and threads it as getCursorPickerModels({ binaryPath }) so model-picker discovery spawns the same machine-local cursor-agent used by auth/probe/status. Blank/undefined preserves PATH auto-detection. Follow-up to FN-7696.
|
||||
@@ -25,7 +25,11 @@ import { registerModelRoutes } from "../routes/register-model-routes.js";
|
||||
|
||||
const mockedGetCursorPickerModels = vi.mocked(getCursorPickerModels);
|
||||
|
||||
function setup(useCursorCli?: boolean, registryModels?: Array<{ provider: string; id: string; name: string; reasoning: boolean; contextWindow: number }>) {
|
||||
function setup(
|
||||
useCursorCli?: boolean,
|
||||
registryModels?: Array<{ provider: string; id: string; name: string; reasoning: boolean; contextWindow: number }>,
|
||||
cursorCliBinaryPath?: unknown,
|
||||
) {
|
||||
const getHandlers = new Map<string, (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>>();
|
||||
const router = {
|
||||
get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>) => {
|
||||
@@ -35,7 +39,7 @@ function setup(useCursorCli?: boolean, registryModels?: Array<{ provider: string
|
||||
|
||||
const store = {
|
||||
getGlobalSettingsStore: () => ({
|
||||
getSettings: vi.fn().mockResolvedValue({ useCursorCli }),
|
||||
getSettings: vi.fn().mockResolvedValue({ useCursorCli, cursorCliBinaryPath }),
|
||||
}),
|
||||
getSettingsFast: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
@@ -162,3 +166,61 @@ describe("registerModelRoutes cursor-cli merge and filter", () => {
|
||||
expect(new Set(keys).size).toBe(keys.length);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:CursorCli 2026-07-08-00:20:
|
||||
FN-7699: the machine-local cursorCliBinaryPath operator override (already
|
||||
honored by the auth/probe/status paths in register-auth-routes.ts) must also
|
||||
apply to model-picker discovery, so an operator whose cursor-agent is not on
|
||||
PATH still sees Cursor models in the picker. These tests assert the
|
||||
normalized override is threaded into getCursorPickerModels({ binaryPath })
|
||||
verbatim, that blank/undefined preserves binaryPath: undefined (PATH
|
||||
auto-detection), and that the toggle-off gate (FN-7696) is not regressed.
|
||||
*/
|
||||
describe("registerModelRoutes cursorCliBinaryPath threading", () => {
|
||||
it("threads a set cursorCliBinaryPath override into getCursorPickerModels verbatim", async () => {
|
||||
mockedGetCursorPickerModels.mockResolvedValue([
|
||||
{ provider: "cursor-cli", id: "cursor/gpt-5", name: "GPT-5", reasoning: false, contextWindow: 0 },
|
||||
]);
|
||||
const handler = setup(true, undefined, "/opt/Cursor/cursor-agent");
|
||||
const response = await invoke(handler);
|
||||
expect(mockedGetCursorPickerModels).toHaveBeenCalledWith({ binaryPath: "/opt/Cursor/cursor-agent" });
|
||||
expect(response.models.some((m) => m.provider === "cursor-cli" && m.id === "cursor/gpt-5")).toBe(true);
|
||||
});
|
||||
|
||||
it("threads a Windows-shim-style override path verbatim, with no mangling", async () => {
|
||||
mockedGetCursorPickerModels.mockResolvedValue([]);
|
||||
const winPath = "C:\\Users\\A User\\AppData\\Roaming\\npm\\cursor-agent.cmd";
|
||||
const handler = setup(true, undefined, winPath);
|
||||
await invoke(handler);
|
||||
expect(mockedGetCursorPickerModels).toHaveBeenCalledWith({ binaryPath: winPath });
|
||||
});
|
||||
|
||||
it("passes binaryPath: undefined when cursorCliBinaryPath is absent (PATH auto-detection preserved)", async () => {
|
||||
mockedGetCursorPickerModels.mockResolvedValue([]);
|
||||
const handler = setup(true, undefined, undefined);
|
||||
await invoke(handler);
|
||||
expect(mockedGetCursorPickerModels).toHaveBeenCalledWith({ binaryPath: undefined });
|
||||
});
|
||||
|
||||
it("passes binaryPath: undefined when cursorCliBinaryPath is blank/whitespace-only", async () => {
|
||||
mockedGetCursorPickerModels.mockResolvedValue([]);
|
||||
const handler = setup(true, undefined, " ");
|
||||
await invoke(handler);
|
||||
expect(mockedGetCursorPickerModels).toHaveBeenCalledWith({ binaryPath: undefined });
|
||||
});
|
||||
|
||||
it("passes binaryPath: undefined when cursorCliBinaryPath is an empty string", async () => {
|
||||
mockedGetCursorPickerModels.mockResolvedValue([]);
|
||||
const handler = setup(true, undefined, "");
|
||||
await invoke(handler);
|
||||
expect(mockedGetCursorPickerModels).toHaveBeenCalledWith({ binaryPath: undefined });
|
||||
});
|
||||
|
||||
it("does not surface cursor-cli rows or call getCursorPickerModels when useCursorCli is false, regardless of cursorCliBinaryPath", async () => {
|
||||
const handler = setup(false, undefined, "/opt/Cursor/cursor-agent");
|
||||
const response = await invoke(handler);
|
||||
expect(mockedGetCursorPickerModels).not.toHaveBeenCalled();
|
||||
expect(response.models.some((m) => m.provider === "cursor-cli")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -162,6 +162,7 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
let useDroidCli = false;
|
||||
let useLlamaCpp = false;
|
||||
let useCursorCli = false;
|
||||
let cursorCliBinaryPath: string | undefined;
|
||||
let resolvedPlanningProvider: string | undefined;
|
||||
let resolvedPlanningModelId: string | undefined;
|
||||
let customProviders: CustomProvider[] = [];
|
||||
@@ -177,6 +178,20 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
useDroidCli = globalSettings.useDroidCli === true;
|
||||
useLlamaCpp = globalSettings.useLlamaCpp === true;
|
||||
useCursorCli = (globalSettings as Record<string, unknown>).useCursorCli === true;
|
||||
/*
|
||||
FNXC:CursorCli 2026-07-08-00:20:
|
||||
FN-7699 (follow-up to FN-7696): the machine-local `cursorCliBinaryPath`
|
||||
operator override must apply to model-picker discovery too, not just
|
||||
the auth/probe/status paths (register-auth-routes.ts's
|
||||
normalizeCursorCliBinaryPath). Mirror the same trim/blank->undefined
|
||||
normalization here so a blank/unset override preserves PATH
|
||||
auto-detection byte-for-byte, and a set override threads through to
|
||||
getCursorPickerModels below so discovery spawns the exact same
|
||||
cursor-agent executable the settings card already validated.
|
||||
*/
|
||||
const rawCursorCliBinaryPath = (globalSettings as Record<string, unknown>).cursorCliBinaryPath;
|
||||
cursorCliBinaryPath =
|
||||
typeof rawCursorCliBinaryPath === "string" ? rawCursorCliBinaryPath.trim() || undefined : undefined;
|
||||
customProviders = globalSettings.customProviders ?? [];
|
||||
|
||||
const mergedSettings = await store.getSettingsFast();
|
||||
@@ -299,6 +314,15 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
the existing seenModelKeys provider/id dedup so an existing row always
|
||||
wins over a colliding Cursor row — purely additive, must never
|
||||
displace, overwrite, or filter out an existing row.
|
||||
|
||||
FNXC:CursorCli 2026-07-08-00:20:
|
||||
FN-7699: thread the normalized cursorCliBinaryPath operator override
|
||||
(see the globalSettings read block above) into getCursorPickerModels so
|
||||
discovery spawns the exact same machine-local cursor-agent executable
|
||||
already validated by the auth/probe/status paths. Blank/undefined
|
||||
preserves PATH auto-detection unchanged; the cache is keyed per
|
||||
resolved binary path so the override participates correctly in
|
||||
TTL/single-flight caching.
|
||||
*/
|
||||
if (useCursorCli) {
|
||||
// getCursorPickerModels never throws by contract (see
|
||||
@@ -306,7 +330,7 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
// a Cursor discovery failure can never reject the /models handler or
|
||||
// drop existing rows — degrade to zero Cursor rows instead.
|
||||
try {
|
||||
const cursorModels = await getCursorPickerModels();
|
||||
const cursorModels = await getCursorPickerModels({ binaryPath: cursorCliBinaryPath });
|
||||
for (const cursorModel of cursorModels) {
|
||||
const key = `${cursorModel.provider}/${cursorModel.id}`;
|
||||
if (seenModelKeys.has(key)) continue;
|
||||
|
||||
Reference in New Issue
Block a user