import * as path from "node:path";import * as os from "node:os";import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises";import { fileURLToPath } from "node:url"; import * as NodeServices from "@effect/platform-node/NodeServices";import { Effect } from "effect";import { describe, expect, it } from "vitest";import type * as EffectAcpSchema from "effect-acp/schema";import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";import { createModelCapabilities } from "@t3tools/shared/model"; import { buildCursorProviderSnapshot, buildCursorCapabilitiesFromConfigOptions, buildCursorDiscoveredModelsFromConfigOptions, discoverCursorModelCapabilitiesViaAcp, discoverCursorModelsViaAcp, getCursorFallbackModels, getCursorParameterizedModelPickerUnsupportedMessage, parseCursorAboutOutput, parseCursorCliConfigChannel, parseCursorVersionDate, resolveCursorAcpBaseModelId, resolveCursorAcpConfigUpdates,} from "./CursorProvider.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url));const mockAgentPath = path.join(__dirname, "../../../scripts/acp-mock-agent.ts"); function selectDescriptor( id: string, label: string, options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,) { return { id, label, type: "select" as const, options: [...options], ...(options.find((option) => option.isDefault)?.id ? { currentValue: options.find((option) => option.isDefault)?.id } : {}), };} function booleanDescriptor(id: string, label: string, currentValue?: boolean) { return { id, label, type: "boolean" as const, ...(typeof currentValue === "boolean" ? { currentValue } : {}), };} async function makeMockAgentWrapper(extraEnv?: Record<string, string>) { const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-provider-mock-")); const wrapperPath = path.join(dir, "fake-agent.sh"); const envExports = Object.entries(extraEnv ?? {}) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .join("\n"); const script = `#!/bin/sh${envExports}exec ${JSON.stringify("bun")} ${JSON.stringify(mockAgentPath)} "$@"`; await writeFile(wrapperPath, script, "utf8"); await chmod(wrapperPath, 0o755); return wrapperPath;} async function waitForFileContent(filePath: string, attempts = 40): Promise<string> { for (let attempt = 0; attempt < attempts; attempt += 1) { try { const content = await readFile(filePath, "utf8"); if (content.trim().length > 0) { return content; } } catch {} await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error(`Timed out waiting for file content at ${filePath}`);} const parameterizedGpt54ConfigOptions = [ { type: "select", currentValue: "gpt-5.4-medium-fast", options: [{ name: "GPT-5.4", value: "gpt-5.4-medium-fast" }], category: "model", id: "model", name: "Model", }, { type: "select", currentValue: "medium", options: [ { name: "None", value: "none" }, { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, { name: "Extra High", value: "extra-high" }, ], category: "thought_level", id: "reasoning", name: "Reasoning", }, { type: "select", currentValue: "272k", options: [ { name: "272K", value: "272k" }, { name: "1M", value: "1m" }, ], category: "model_config", id: "context", name: "Context", }, { type: "select", currentValue: "false", options: [ { name: "Off", value: "false" }, { name: "Fast", value: "true" }, ], category: "model_config", id: "fast", name: "Fast", },] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const parameterizedClaudeConfigOptions = [ { type: "select", currentValue: "claude-4.6-opus-high-thinking", options: [{ name: "Opus 4.6", value: "claude-4.6-opus-high-thinking" }], category: "model", id: "model", name: "Model", }, { type: "select", currentValue: "high", options: [ { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, ], category: "thought_level", id: "reasoning", name: "Reasoning", }, { type: "boolean", currentValue: true, category: "model_config", id: "thinking", name: "Thinking", },] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const parameterizedClaudeModelOptionConfigOptions = [ { type: "select", currentValue: "claude-opus-4-6", options: [{ name: "Opus 4.6", value: "claude-opus-4-6" }], category: "model", id: "model", name: "Model", }, { type: "select", currentValue: "high", options: [ { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, ], category: "thought_level", id: "reasoning", name: "Reasoning", }, { type: "select", currentValue: "max", options: [ { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, { name: "Max", value: "max" }, ], category: "model_option", id: "effort", name: "Effort", }, { type: "select", currentValue: "true", options: [ { name: "Off", value: "false" }, { name: "Fast", value: "true" }, ], category: "model_config", id: "fast", name: "Fast", }, { type: "select", currentValue: "true", options: [ { name: "Off", value: "false" }, { name: ":icon-brain:", value: "true" }, ], category: "model_config", id: "thinking", name: "Thinking", },] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const sessionNewCursorConfigOptions = [ { type: "select", currentValue: "agent", options: [ { name: "Agent", value: "agent", description: "Full agent capabilities with tool access" }, ], category: "mode", id: "mode", name: "Mode", description: "Controls how the agent executes tasks", }, { type: "select", currentValue: "composer-2", options: [ { name: "Auto", value: "default" }, { name: "Composer 2", value: "composer-2" }, { name: "GPT-5.4", value: "gpt-5.4" }, { name: "Sonnet 4.6", value: "claude-sonnet-4-6" }, { name: "Opus 4.6", value: "claude-opus-4-6" }, { name: "Codex 5.3 Spark", value: "gpt-5.3-codex-spark" }, ], category: "model", id: "model", name: "Model", description: "Controls which model is used for responses", }, { type: "select", currentValue: "true", options: [ { name: "Off", value: "false" }, { name: "Fast", value: "true" }, ], category: "model_config", id: "fast", name: "Fast", description: "Faster speeds.", },] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const baseCursorSettings: CursorSettings = { enabled: true, binaryPath: "agent", apiEndpoint: "", customModels: [],}; const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] }); describe("getCursorFallbackModels", () => { it("does not publish any built-in cursor models before ACP discovery", () => { expect( getCursorFallbackModels({ customModels: ["internal/cursor-model"], }).map((model) => model.slug), ).toEqual(["internal/cursor-model"]); });}); describe("buildCursorProviderSnapshot", () => { it("downgrades ready status to warning when ACP model discovery times out", () => { expect( buildCursorProviderSnapshot({ checkedAt: "2026-01-01T00:00:00.000Z", cursorSettings: baseCursorSettings, parsed: { version: "2026.04.09-f2b0fcd", status: "ready", auth: { status: "authenticated", type: "Team", label: "Cursor Team Subscription" }, }, discoveryWarning: "Cursor ACP model discovery timed out after 15000ms.", }), ).toMatchObject({ status: "warning", message: "Cursor ACP model discovery timed out after 15000ms.", models: [], }); }); it("preserves provider error state while appending discovery warnings", () => { expect( buildCursorProviderSnapshot({ checkedAt: "2026-01-01T00:00:00.000Z", cursorSettings: { ...baseCursorSettings, customModels: ["claude-sonnet-4-6"], }, parsed: { version: "2026.04.09-f2b0fcd", status: "error", auth: { status: "unauthenticated" }, message: "Cursor Agent is not authenticated. Run `agent login` and try again.", }, discoveryWarning: "Cursor ACP model discovery failed. Check server logs for details.", }), ).toMatchObject({ status: "error", message: "Cursor Agent is not authenticated. Run `agent login` and try again. Cursor ACP model discovery failed. Check server logs for details.", models: [ { slug: "claude-sonnet-4-6", isCustom: true, }, ], }); });}); describe("buildCursorCapabilitiesFromConfigOptions", () => { it("derives model capabilities from parameterized Cursor ACP config options", () => { expect(buildCursorCapabilitiesFromConfigOptions(parameterizedGpt54ConfigOptions)).toEqual( createModelCapabilities({ optionDescriptors: [ selectDescriptor("reasoning", "Reasoning", [ { id: "low", label: "Low" }, { id: "medium", label: "Medium", isDefault: true }, { id: "high", label: "High" }, { id: "xhigh", label: "Extra High" }, ]), selectDescriptor("contextWindow", "Context", [ { id: "272k", label: "272K", isDefault: true }, { id: "1m", label: "1M" }, ]), booleanDescriptor("fastMode", "Fast", false), ], }), ); }); it("detects boolean thinking toggles from model_config options", () => { expect(buildCursorCapabilitiesFromConfigOptions(parameterizedClaudeConfigOptions)).toEqual( createModelCapabilities({ optionDescriptors: [ selectDescriptor("reasoning", "Reasoning", [ { id: "low", label: "Low" }, { id: "medium", label: "Medium" }, { id: "high", label: "High", isDefault: true }, ]), booleanDescriptor("thinking", "Thinking", true), ], }), ); }); it("prefers the newer model_option effort control over legacy thought_level", () => { expect( buildCursorCapabilitiesFromConfigOptions(parameterizedClaudeModelOptionConfigOptions), ).toEqual( createModelCapabilities({ optionDescriptors: [ selectDescriptor("reasoning", "Effort", [ { id: "low", label: "Low" }, { id: "medium", label: "Medium" }, { id: "high", label: "High" }, { id: "max", label: "Max", isDefault: true }, ]), booleanDescriptor("fastMode", "Fast", true), booleanDescriptor("thinking", "Thinking", true), ], }), ); });}); describe("buildCursorDiscoveredModelsFromConfigOptions", () => { it("publishes ACP model choices immediately from session/new config options", () => { expect(buildCursorDiscoveredModelsFromConfigOptions(sessionNewCursorConfigOptions)).toEqual([ { slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities, }, { slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [booleanDescriptor("fastMode", "Fast", true)], }), }, { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities, }, { slug: "claude-sonnet-4-6", name: "Sonnet 4.6", isCustom: false, capabilities: emptyCapabilities, }, { slug: "claude-opus-4-6", name: "Opus 4.6", isCustom: false, capabilities: emptyCapabilities, }, { slug: "gpt-5.3-codex-spark", name: "Codex 5.3 Spark", isCustom: false, capabilities: emptyCapabilities, }, ]); });}); describe("discoverCursorModelsViaAcp", () => { it("keeps the ACP probe runtime alive long enough to discover models", async () => { const wrapperPath = await makeMockAgentWrapper(); const models = await Effect.runPromise( discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); expect(models.map((model) => model.slug)).toEqual([ "default", "composer-2", "gpt-5.4", "claude-opus-4-6", ]); }); it("closes the ACP probe runtime after discovery completes", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "cursor-provider-exit-log-")); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = await makeMockAgentWrapper({ T3_ACP_EXIT_LOG_PATH: exitLogPath, }); await Effect.runPromise( discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], }).pipe(Effect.provide(NodeServices.layer)), ); const exitLog = await waitForFileContent(exitLogPath); expect(exitLog).toContain("SIGTERM"); });}); describe("discoverCursorModelCapabilitiesViaAcp", () => { it("discovers model capabilities through Cursor's list_available_models extension method", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "cursor-capabilities-exit-log-")); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = await makeMockAgentWrapper({ T3_ACP_EXIT_LOG_PATH: exitLogPath, }); const existingModels: ReadonlyArray<ServerProviderModel> = [ { slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities }, { slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: emptyCapabilities }, { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities }, { slug: "claude-opus-4-6", name: "Opus 4.6", isCustom: false, capabilities: emptyCapabilities, }, ]; const models = await Effect.runPromise( discoverCursorModelCapabilitiesViaAcp( { enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], }, existingModels,Lines 1–500 of 695. Load more
import * as path from "node:path"; import * as os from "node:os"; import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { Effect } from "effect"; import { describe, expect, it } from "vitest"; import type * as EffectAcpSchema from "effect-acp/schema"; import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; import { buildCursorProviderSnapshot, buildCursorCapabilitiesFromConfigOptions, buildCursorDiscoveredModelsFromConfigOptions, discoverCursorModelCapabilitiesViaAcp, discoverCursorModelsViaAcp, getCursorFallbackModels, getCursorParameterizedModelPickerUnsupportedMessage, parseCursorAboutOutput, parseCursorCliConfigChannel, parseCursorVersionDate, resolveCursorAcpBaseModelId, resolveCursorAcpConfigUpdates, } from "./CursorProvider.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const mockAgentPath = path.join(__dirname, "../../../scripts/acp-mock-agent.ts"); function selectDescriptor( id: string, label: string, options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>, ) { return { id, label, type: "select" as const, options: [...options], ...(options.find((option) => option.isDefault)?.id ? { currentValue: options.find((option) => option.isDefault)?.id } : {}), }; } function booleanDescriptor(id: string, label: string, currentValue?: boolean) { return { id, label, type: "boolean" as const, ...(typeof currentValue === "boolean" ? { currentValue } : {}), }; } async function makeMockAgentWrapper(extraEnv?: Record<string, string>) { const dir = await mkdtemp(path.join(os.tmpdir(), "cursor-provider-mock-")); const wrapperPath = path.join(dir, "fake-agent.sh"); const envExports = Object.entries(extraEnv ?? {}) .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) .join("\n"); const script = `#!/bin/sh ${envExports} exec ${JSON.stringify("bun")} ${JSON.stringify(mockAgentPath)} "$@" `; await writeFile(wrapperPath, script, "utf8"); await chmod(wrapperPath, 0o755); return wrapperPath; } async function waitForFileContent(filePath: string, attempts = 40): Promise<string> { for (let attempt = 0; attempt < attempts; attempt += 1) { try { const content = await readFile(filePath, "utf8"); if (content.trim().length > 0) { return content; } } catch {} await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error(`Timed out waiting for file content at ${filePath}`); } const parameterizedGpt54ConfigOptions = [ { type: "select", currentValue: "gpt-5.4-medium-fast", options: [{ name: "GPT-5.4", value: "gpt-5.4-medium-fast" }], category: "model", id: "model", name: "Model", }, { type: "select", currentValue: "medium", options: [ { name: "None", value: "none" }, { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, { name: "Extra High", value: "extra-high" }, ], category: "thought_level", id: "reasoning", name: "Reasoning", }, { type: "select", currentValue: "272k", options: [ { name: "272K", value: "272k" }, { name: "1M", value: "1m" }, ], category: "model_config", id: "context", name: "Context", }, { type: "select", currentValue: "false", options: [ { name: "Off", value: "false" }, { name: "Fast", value: "true" }, ], category: "model_config", id: "fast", name: "Fast", }, ] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const parameterizedClaudeConfigOptions = [ { type: "select", currentValue: "claude-4.6-opus-high-thinking", options: [{ name: "Opus 4.6", value: "claude-4.6-opus-high-thinking" }], category: "model", id: "model", name: "Model", }, { type: "select", currentValue: "high", options: [ { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, ], category: "thought_level", id: "reasoning", name: "Reasoning", }, { type: "boolean", currentValue: true, category: "model_config", id: "thinking", name: "Thinking", }, ] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const parameterizedClaudeModelOptionConfigOptions = [ { type: "select", currentValue: "claude-opus-4-6", options: [{ name: "Opus 4.6", value: "claude-opus-4-6" }], category: "model", id: "model", name: "Model", }, { type: "select", currentValue: "high", options: [ { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, ], category: "thought_level", id: "reasoning", name: "Reasoning", }, { type: "select", currentValue: "max", options: [ { name: "Low", value: "low" }, { name: "Medium", value: "medium" }, { name: "High", value: "high" }, { name: "Max", value: "max" }, ], category: "model_option", id: "effort", name: "Effort", }, { type: "select", currentValue: "true", options: [ { name: "Off", value: "false" }, { name: "Fast", value: "true" }, ], category: "model_config", id: "fast", name: "Fast", }, { type: "select", currentValue: "true", options: [ { name: "Off", value: "false" }, { name: ":icon-brain:", value: "true" }, ], category: "model_config", id: "thinking", name: "Thinking", }, ] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const sessionNewCursorConfigOptions = [ { type: "select", currentValue: "agent", options: [ { name: "Agent", value: "agent", description: "Full agent capabilities with tool access" }, ], category: "mode", id: "mode", name: "Mode", description: "Controls how the agent executes tasks", }, { type: "select", currentValue: "composer-2", options: [ { name: "Auto", value: "default" }, { name: "Composer 2", value: "composer-2" }, { name: "GPT-5.4", value: "gpt-5.4" }, { name: "Sonnet 4.6", value: "claude-sonnet-4-6" }, { name: "Opus 4.6", value: "claude-opus-4-6" }, { name: "Codex 5.3 Spark", value: "gpt-5.3-codex-spark" }, ], category: "model", id: "model", name: "Model", description: "Controls which model is used for responses", }, { type: "select", currentValue: "true", options: [ { name: "Off", value: "false" }, { name: "Fast", value: "true" }, ], category: "model_config", id: "fast", name: "Fast", description: "Faster speeds.", }, ] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>; const baseCursorSettings: CursorSettings = { enabled: true, binaryPath: "agent", apiEndpoint: "", customModels: [], }; const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] }); describe("getCursorFallbackModels", () => { it("does not publish any built-in cursor models before ACP discovery", () => { expect( getCursorFallbackModels({ customModels: ["internal/cursor-model"], }).map((model) => model.slug), ).toEqual(["internal/cursor-model"]); }); }); describe("buildCursorProviderSnapshot", () => { it("downgrades ready status to warning when ACP model discovery times out", () => { expect( buildCursorProviderSnapshot({ checkedAt: "2026-01-01T00:00:00.000Z", cursorSettings: baseCursorSettings, parsed: { version: "2026.04.09-f2b0fcd", status: "ready", auth: { status: "authenticated", type: "Team", label: "Cursor Team Subscription" }, }, discoveryWarning: "Cursor ACP model discovery timed out after 15000ms.", }), ).toMatchObject({ status: "warning", message: "Cursor ACP model discovery timed out after 15000ms.", models: [], }); }); it("preserves provider error state while appending discovery warnings", () => { expect( buildCursorProviderSnapshot({ checkedAt: "2026-01-01T00:00:00.000Z", cursorSettings: { ...baseCursorSettings, customModels: ["claude-sonnet-4-6"], }, parsed: { version: "2026.04.09-f2b0fcd", status: "error", auth: { status: "unauthenticated" }, message: "Cursor Agent is not authenticated. Run `agent login` and try again.", }, discoveryWarning: "Cursor ACP model discovery failed. Check server logs for details.", }), ).toMatchObject({ status: "error", message: "Cursor Agent is not authenticated. Run `agent login` and try again. Cursor ACP model discovery failed. Check server logs for details.", models: [ { slug: "claude-sonnet-4-6", isCustom: true, }, ], }); }); }); describe("buildCursorCapabilitiesFromConfigOptions", () => { it("derives model capabilities from parameterized Cursor ACP config options", () => { expect(buildCursorCapabilitiesFromConfigOptions(parameterizedGpt54ConfigOptions)).toEqual( createModelCapabilities({ optionDescriptors: [ selectDescriptor("reasoning", "Reasoning", [ { id: "low", label: "Low" }, { id: "medium", label: "Medium", isDefault: true }, { id: "high", label: "High" }, { id: "xhigh", label: "Extra High" }, ]), selectDescriptor("contextWindow", "Context", [ { id: "272k", label: "272K", isDefault: true }, { id: "1m", label: "1M" }, ]), booleanDescriptor("fastMode", "Fast", false), ], }), ); }); it("detects boolean thinking toggles from model_config options", () => { expect(buildCursorCapabilitiesFromConfigOptions(parameterizedClaudeConfigOptions)).toEqual( createModelCapabilities({ optionDescriptors: [ selectDescriptor("reasoning", "Reasoning", [ { id: "low", label: "Low" }, { id: "medium", label: "Medium" }, { id: "high", label: "High", isDefault: true }, ]), booleanDescriptor("thinking", "Thinking", true), ], }), ); }); it("prefers the newer model_option effort control over legacy thought_level", () => { expect( buildCursorCapabilitiesFromConfigOptions(parameterizedClaudeModelOptionConfigOptions), ).toEqual( createModelCapabilities({ optionDescriptors: [ selectDescriptor("reasoning", "Effort", [ { id: "low", label: "Low" }, { id: "medium", label: "Medium" }, { id: "high", label: "High" }, { id: "max", label: "Max", isDefault: true }, ]), booleanDescriptor("fastMode", "Fast", true), booleanDescriptor("thinking", "Thinking", true), ], }), ); }); }); describe("buildCursorDiscoveredModelsFromConfigOptions", () => { it("publishes ACP model choices immediately from session/new config options", () => { expect(buildCursorDiscoveredModelsFromConfigOptions(sessionNewCursorConfigOptions)).toEqual([ { slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities, }, { slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: createModelCapabilities({ optionDescriptors: [booleanDescriptor("fastMode", "Fast", true)], }), }, { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities, }, { slug: "claude-sonnet-4-6", name: "Sonnet 4.6", isCustom: false, capabilities: emptyCapabilities, }, { slug: "claude-opus-4-6", name: "Opus 4.6", isCustom: false, capabilities: emptyCapabilities, }, { slug: "gpt-5.3-codex-spark", name: "Codex 5.3 Spark", isCustom: false, capabilities: emptyCapabilities, }, ]); }); }); describe("discoverCursorModelsViaAcp", () => { it("keeps the ACP probe runtime alive long enough to discover models", async () => { const wrapperPath = await makeMockAgentWrapper(); const models = await Effect.runPromise( discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), ); expect(models.map((model) => model.slug)).toEqual([ "default", "composer-2", "gpt-5.4", "claude-opus-4-6", ]); }); it("closes the ACP probe runtime after discovery completes", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "cursor-provider-exit-log-")); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = await makeMockAgentWrapper({ T3_ACP_EXIT_LOG_PATH: exitLogPath, }); await Effect.runPromise( discoverCursorModelsViaAcp({ enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], }).pipe(Effect.provide(NodeServices.layer)), ); const exitLog = await waitForFileContent(exitLogPath); expect(exitLog).toContain("SIGTERM"); }); }); describe("discoverCursorModelCapabilitiesViaAcp", () => { it("discovers model capabilities through Cursor's list_available_models extension method", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "cursor-capabilities-exit-log-")); const exitLogPath = path.join(tempDir, "exit.log"); const wrapperPath = await makeMockAgentWrapper({ T3_ACP_EXIT_LOG_PATH: exitLogPath, }); const existingModels: ReadonlyArray<ServerProviderModel> = [ { slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities }, { slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: emptyCapabilities }, { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities }, { slug: "claude-opus-4-6", name: "Opus 4.6", isCustom: false, capabilities: emptyCapabilities, }, ]; const models = await Effect.runPromise( discoverCursorModelCapabilitiesViaAcp( { enabled: true, binaryPath: wrapperPath, apiEndpoint: "", customModels: [], }, existingModels,