import * as NodeOS from "node:os";
import * as NodeServices from "@effect/platform-node/NodeServices";
import type * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import { describe, expect, it } from "vite-plus/test";
import type * as EffectAcpSchema from "effect-acp/schema";
import type { CursorSettings } from "@t3tools/contracts";
import { createModelCapabilities } from "@t3tools/shared/model";
import {
buildCursorProviderSnapshot,
buildCursorCapabilitiesFromConfigOptions,
checkCursorProviderStatus,
discoverCursorModelsViaAcp,
getCursorFallbackModels,
getCursorParameterizedModelPickerUnsupportedMessage,
parseCursorAboutOutput,
parseCursorCliConfigChannel,
parseCursorVersionDate,
resolveCursorAcpBaseModelId,
resolveCursorAcpConfigUpdates,
} from "./CursorProvider.ts";
const runNode = (
effect: Effect.Effect<
A,
E,
ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | Path.Path
>,
): Promise => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));
const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () {
const path = yield* Path.Path;
return yield* path.fromFileUrl(new URL("../../../scripts/acp-mock-agent.ts", import.meta.url));
});
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 } : {}),
};
}
const makeMockAgentWrapper = Effect.fn("makeMockAgentWrapper")(function* (
extraEnv?: Record,
) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const mockAgentPath = yield* resolveMockAgentPath();
const dir = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix: "cursor-provider-mock-",
});
const wrapperPath = path.join(dir, "fake-agent.sh");
const mockAgentCommand = ["node", mockAgentPath].map((arg) => JSON.stringify(arg)).join(" ");
const envExports = Object.entries(extraEnv ?? {})
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
.join("\n");
const script = `#!/bin/sh
${envExports}
exec ${mockAgentCommand} "$@"
`;
yield* fileSystem.writeFileString(wrapperPath, script);
yield* fileSystem.chmod(wrapperPath, 0o755);
return wrapperPath;
});
const makeMockAgentWithAboutWrapper = Effect.fn("makeMockAgentWithAboutWrapper")(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const mockAgentPath = yield* resolveMockAgentPath();
const dir = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix: "cursor-provider-about-mock-",
});
const wrapperPath = path.join(dir, "fake-agent.sh");
const mockAgentCommand = ["node", mockAgentPath].map((arg) => JSON.stringify(arg)).join(" ");
const script = `#!/bin/sh
if [ "$1" = "about" ]; then
printf 'CLI Version 2026.04.09-f2b0fcd\\n'
printf 'User Email cursor@example.com\\n'
exit 0
fi
exec ${mockAgentCommand} "$@"
`;
yield* fileSystem.writeFileString(wrapperPath, script);
yield* fileSystem.chmod(wrapperPath, 0o755);
return wrapperPath;
});
const waitForFileContent = Effect.fn("waitForFileContent")(function* (
filePath: string,
attempts = 40,
) {
const fileSystem = yield* FileSystem.FileSystem;
for (let attempt = 0; attempt < attempts; attempt += 1) {
const content = yield* fileSystem
.readFileString(filePath)
.pipe(Effect.catch(() => Effect.void));
if (content !== undefined) {
if (content.trim().length > 0) {
return content;
}
}
yield* Effect.sleep("50 millis");
}
return yield* Effect.die(`Timed out waiting for file content at ${filePath}`);
});
const makeProviderStatusEnvFixture = Effect.fn("makeProviderStatusEnvFixture")(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix: "cursor-provider-status-env-",
});
return {
requestLogPath: path.join(tempDir, "requests.ndjson"),
wrapperPath: yield* makeMockAgentWithAboutWrapper(),
};
});
const makeExitLogFixture = Effect.fn("makeExitLogFixture")(function* (prefix: string) {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const tempDir = yield* fileSystem.makeTempDirectory({
directory: NodeOS.tmpdir(),
prefix,
});
const exitLogPath = path.join(tempDir, "exit.log");
return {
exitLogPath,
wrapperPath: yield* makeMockAgentWrapper({
T3_ACP_EXIT_LOG_PATH: exitLogPath,
}),
};
});
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;
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;
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;
const baseCursorSettings: CursorSettings = {
enabled: true,
binaryPath: "cursor-agent",
apiEndpoint: "",
customModels: [],
};
const cursorAcpDiscoveryFailedMessage = [
"Cursor ACP model discovery failed.",
"Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.",
"See https://cursor.com/docs/cli/installation.",
"Check server logs for ACP details.",
].join(" ");
const missingCursorBinaryPath = "/definitely/not/installed/t3-cursor-agent";
const cursorCliCommandMissingMessage = [
`Cursor CLI command \`${missingCursorBinaryPath}\` was not found.`,
`Install or enable the Cursor CLI, make sure \`${missingCursorBinaryPath}\` is on PATH, then restart T3 Code.`,
"See https://cursor.com/docs/cli/installation.",
].join(" ");
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: cursorAcpDiscoveryFailedMessage,
}),
).toMatchObject({
status: "error",
message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`,
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("checkCursorProviderStatus", () => {
it("reports the install docs when the Cursor CLI command is missing", async () => {
const provider = await runNode(
checkCursorProviderStatus({
enabled: true,
binaryPath: missingCursorBinaryPath,
apiEndpoint: "",
customModels: [],
}),
);
expect(provider).toMatchObject({
installed: false,
status: "error",
auth: { status: "unknown" },
message: cursorCliCommandMissingMessage,
});
});
it("passes the injected environment to ACP model discovery", async () => {
const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());
const provider = await runNode(
checkCursorProviderStatus(
{
enabled: true,
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
},
{
...process.env,
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
},
),
);
expect(provider.models.map((model) => model.slug)).toEqual([
"default",
"composer-2",
"gpt-5.4",
"claude-opus-4-6",
]);
await expect(runNode(waitForFileContent(requestLogPath))).resolves.toContain("initialize");
});
});
describe("discoverCursorModelsViaAcp", () => {
it("keeps the ACP probe runtime alive long enough to discover models", async () => {
const wrapperPath = await runNode(makeMockAgentWrapper());
const models = await runNode(
discoverCursorModelsViaAcp({
enabled: true,
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
}).pipe(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 { exitLogPath, wrapperPath } = await runNode(
makeExitLogFixture("cursor-provider-exit-log-"),
);
await runNode(
discoverCursorModelsViaAcp({
enabled: true,
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
}),
);
const exitLog = await runNode(waitForFileContent(exitLogPath));
expect(exitLog).toContain("SIGTERM");
});
});
describe("parseCursorAboutOutput", () => {
it("parses json about output and forwards subscription metadata", () => {
expect(
parseCursorAboutOutput({
code: 0,
stdout: JSON.stringify({
cliVersion: "2026.04.09-f2b0fcd",
subscriptionTier: "Team",
userEmail: "jmarminge@gmail.com",
}),
stderr: "",
}),
).toEqual({
version: "2026.04.09-f2b0fcd",
status: "ready",
auth: {
status: "authenticated",
email: "jmarminge@gmail.com",
type: "Team",
label: "Cursor Team Subscription",
},
});
});
it("treats json about output with a logged-out email as unauthenticated", () => {
expect(
parseCursorAboutOutput({
code: 0,
stdout: JSON.stringify({
cliVersion: "2026.04.09-f2b0fcd",
subscriptionTier: "Team",
userEmail: "Not logged in",
}),
stderr: "",
}),
).toEqual({
version: "2026.04.09-f2b0fcd",
status: "error",
auth: {
status: "unauthenticated",
},
message: "Cursor Agent is not authenticated. Run `agent login` and try again.",
});
});
it("treats json about output with a null email as unauthenticated", () => {
expect(
parseCursorAboutOutput({
code: 0,
stdout: JSON.stringify({
cliVersion: "2026.04.09-f2b0fcd",
subscriptionTier: null,
userEmail: null,
}),
stderr: "",
}),
).toEqual({
version: "2026.04.09-f2b0fcd",
status: "error",
auth: {
status: "unauthenticated",
},
message: "Cursor Agent is not authenticated. Run `agent login` and try again.",
});
});
});
describe("Cursor parameterized model picker preview gating", () => {
it("parses Cursor CLI version dates from build versions", () => {
expect(parseCursorVersionDate("2026.04.08-c4e73a3")).toBe(20260408);
expect(parseCursorVersionDate("2026.04.09")).toBe(20260409);
expect(parseCursorVersionDate("not-a-version")).toBeUndefined();
});
it("parses the Cursor CLI channel from cli-config.json", () => {
expect(parseCursorCliConfigChannel('{ "channel": "lab" }')).toBe("lab");
expect(parseCursorCliConfigChannel('{ "channel": "stable" }')).toBe("stable");
expect(parseCursorCliConfigChannel('{ "version": 1 }')).toBeUndefined();
expect(parseCursorCliConfigChannel("not-json")).toBeUndefined();
});
it("returns no warning when the preview requirements are met", () => {
expect(
getCursorParameterizedModelPickerUnsupportedMessage({
version: "2026.04.08-c4e73a3",
channel: "lab",
}),
).toBeUndefined();
});
it("explains when the Cursor Agent version is too old", () => {
expect(
getCursorParameterizedModelPickerUnsupportedMessage({
version: "2026.04.07-c4e73a3",
channel: "lab",
}),
).toContain("too old");
});
it("explains when the Cursor Agent channel is not lab", () => {
expect(
getCursorParameterizedModelPickerUnsupportedMessage({
version: "2026.04.08-c4e73a3",
channel: "stable",
}),
).toContain("lab channel");
});
});
describe("resolveCursorAcpBaseModelId", () => {
it("drops bracket traits without rewriting raw ACP model ids", () => {
expect(resolveCursorAcpBaseModelId("gpt-5.4[reasoning=medium,context=272k]")).toBe("gpt-5.4");
expect(resolveCursorAcpBaseModelId("gpt-5.4-medium-fast")).toBe("gpt-5.4-medium-fast");
expect(resolveCursorAcpBaseModelId("claude-4.6-opus-high-thinking")).toBe(
"claude-4.6-opus-high-thinking",
);
expect(resolveCursorAcpBaseModelId("composer-2")).toBe("composer-2");
expect(resolveCursorAcpBaseModelId("auto")).toBe("auto");
});
});
describe("resolveCursorAcpConfigUpdates", () => {
it("maps Cursor model options onto separate ACP config option updates", () => {
expect(
resolveCursorAcpConfigUpdates(parameterizedGpt54ConfigOptions, [
{ id: "reasoning", value: "xhigh" },
{ id: "fastMode", value: true },
{ id: "contextWindow", value: "1m" },
]),
).toEqual([
{ configId: "reasoning", value: "extra-high" },
{ configId: "context", value: "1m" },
{ configId: "fast", value: "true" },
]);
});
it("maps boolean thinking toggles when the model exposes them separately", () => {
expect(
resolveCursorAcpConfigUpdates(parameterizedClaudeConfigOptions, [
{ id: "thinking", value: false },
]),
).toEqual([{ configId: "thinking", value: false }]);
});
it("maps explicit fastMode: false so the adapter can clear a prior fast selection", () => {
expect(
resolveCursorAcpConfigUpdates(parameterizedGpt54ConfigOptions, [
{ id: "fastMode", value: false },
]),
).toEqual([{ configId: "fast", value: "false" }]);
});
it("writes Cursor effort changes through the newer model_option config when available", () => {
expect(
resolveCursorAcpConfigUpdates(parameterizedClaudeModelOptionConfigOptions, [
{ id: "reasoning", value: "max" },
{ id: "thinking", value: false },
]),
).toEqual([
{ configId: "effort", value: "max" },
{ configId: "thinking", value: "false" },
]);
});
});