Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions apps/server/src/provider/Layers/CursorProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
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";
Expand All @@ -24,7 +25,11 @@ import {
} from "./CursorProvider.ts";

const runNode = <A, E>(
effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>,
effect: Effect.Effect<
A,
E,
ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
>,
): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)));

const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () {
Expand Down Expand Up @@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = {
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", () => {
Expand Down Expand Up @@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => {
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.",
discoveryWarning: cursorAcpDiscoveryFailedMessage,
}),
).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.",
message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`,
models: [
{
slug: "claude-sonnet-4-6",
Expand Down Expand Up @@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => {
});

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 Effect.runPromise(
const provider = await runNode(
checkCursorProviderStatus(
{
enabled: true,
Expand All @@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => {
...process.env,
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
},
).pipe(Effect.provide(NodeServices.layer)),
),
);

expect(provider.models.map((model) => model.slug)).toEqual([
Expand All @@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => {
it("keeps the ACP probe runtime alive long enough to discover models", async () => {
const wrapperPath = await runNode(makeMockAgentWrapper());

const models = await Effect.runPromise(
const models = await runNode(
discoverCursorModelsViaAcp({
enabled: true,
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
}).pipe(Effect.scoped),
);

expect(models.map((model) => model.slug)).toEqual([
Expand All @@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => {
makeExitLogFixture("cursor-provider-exit-log-"),
);

await Effect.runPromise(
await runNode(
discoverCursorModelsViaAcp({
enabled: true,
binaryPath: wrapperPath,
apiEndpoint: "",
customModels: [],
}).pipe(Effect.provide(NodeServices.layer)),
}),
);

const exitLog = await runNode(waitForFileContent(exitLogPath));
Expand Down
19 changes: 17 additions & 2 deletions apps/server/src/provider/Layers/CursorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({

const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;
const CURSOR_CLI_INSTALLATION_DOCS_URL = "https://cursor.com/docs/cli/installation";
const CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE = [
"Cursor ACP model discovery failed.",
"Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.",
`See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
"Check server logs for ACP details.",
].join(" ");
export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
_meta: {
parameterizedModelPicker: true,
Expand Down Expand Up @@ -608,6 +615,14 @@ function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): s
return parts.length > 0 ? parts.join(" ") : undefined;
}

function buildCursorCliCommandMissingMessage(binaryPath: string): string {
return [
`Cursor CLI command \`${binaryPath}\` was not found.`,
`Install or enable the Cursor CLI, make sure \`${binaryPath}\` is on PATH, then restart T3 Code.`,
`See ${CURSOR_CLI_INSTALLATION_DOCS_URL}.`,
].join(" ");
}

export function buildCursorProviderSnapshot(input: {
readonly checkedAt: string;
readonly cursorSettings: CursorSettings;
Expand Down Expand Up @@ -1020,7 +1035,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
status: "error",
auth: { status: "unknown" },
message: isCommandMissingCause(error)
? "Cursor Agent CLI (`agent`) is not installed or not on PATH."
? buildCursorCliCommandMissingMessage(cursorSettings.binaryPath)
: "Failed to execute Cursor Agent CLI health check.",
},
});
Expand Down Expand Up @@ -1079,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
yield* Effect.logWarning("Cursor ACP model discovery failed", {
errorTag: causeErrorTag(discoveryExit.cause),
});
discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details.";
discoveryWarning = CURSOR_ACP_MODEL_DISCOVERY_FAILED_MESSAGE;
} else if (Option.isNone(discoveryExit.value)) {
discoveryWarning = `Cursor ACP model discovery timed out after ${CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS}ms.`;
} else if (discoveryExit.value.value.length === 0) {
Expand Down
Loading