| ... | @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -4,6 +4,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| | 7 | import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 7 | import { describe, expect, it } from "vite-plus/test"; | 8 | import { describe, expect, it } from "vite-plus/test"; |
| 8 | import type * as EffectAcpSchema from "effect-acp/schema"; | 9 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 9 | import type { CursorSettings } from "@t3tools/contracts"; | 10 | import type { CursorSettings } from "@t3tools/contracts"; |
| ... | @@ -24,7 +25,11 @@ import { | ... | @@ -24,7 +25,11 @@ import { |
| 24 | } from "./CursorProvider.ts"; | 25 | } from "./CursorProvider.ts"; |
| 25 | | 26 | |
| 26 | const runNode = <A, E>( | 27 | const runNode = <A, E>( |
| 27 | effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>, | 28 | effect: Effect.Effect< |
| | 29 | A, |
| | 30 | E, |
| | 31 | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path |
| | 32 | >, |
| 28 | ): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); | 33 | ): Promise<A> => Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer))); |
| 29 | | 34 | |
| 30 | const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () { | 35 | const resolveMockAgentPath = Effect.fn("resolveMockAgentPath")(function* () { |
| ... | @@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = { | ... | @@ -293,6 +298,18 @@ const baseCursorSettings: CursorSettings = { |
| 293 | apiEndpoint: "", | 298 | apiEndpoint: "", |
| 294 | customModels: [], | 299 | customModels: [], |
| 295 | }; | 300 | }; |
| | 301 | const cursorAcpDiscoveryFailedMessage = [ |
| | 302 | "Cursor ACP model discovery failed.", |
| | 303 | "Cursor CLI setup may be incomplete; install or enable the Cursor CLI, restart T3 Code, and try again.", |
| | 304 | "See https://cursor.com/docs/cli/installation.", |
| | 305 | "Check server logs for ACP details.", |
| | 306 | ].join(" "); |
| | 307 | const missingCursorBinaryPath = "/definitely/not/installed/t3-cursor-agent"; |
| | 308 | const cursorCliCommandMissingMessage = [ |
| | 309 | `Cursor CLI command \`${missingCursorBinaryPath}\` was not found.`, |
| | 310 | `Install or enable the Cursor CLI, make sure \`${missingCursorBinaryPath}\` is on PATH, then restart T3 Code.`, |
| | 311 | "See https://cursor.com/docs/cli/installation.", |
| | 312 | ].join(" "); |
| 296 | | 313 | |
| 297 | describe("getCursorFallbackModels", () => { | 314 | describe("getCursorFallbackModels", () => { |
| 298 | it("does not publish any built-in cursor models before ACP discovery", () => { | 315 | it("does not publish any built-in cursor models before ACP discovery", () => { |
| ... | @@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => { | ... | @@ -338,12 +355,11 @@ describe("buildCursorProviderSnapshot", () => { |
| 338 | auth: { status: "unauthenticated" }, | 355 | auth: { status: "unauthenticated" }, |
| 339 | message: "Cursor Agent is not authenticated. Run `agent login` and try again.", | 356 | message: "Cursor Agent is not authenticated. Run `agent login` and try again.", |
| 340 | }, | 357 | }, |
| 341 | discoveryWarning: "Cursor ACP model discovery failed. Check server logs for details.", | 358 | discoveryWarning: cursorAcpDiscoveryFailedMessage, |
| 342 | }), | 359 | }), |
| 343 | ).toMatchObject({ | 360 | ).toMatchObject({ |
| 344 | status: "error", | 361 | status: "error", |
| 345 | message: | 362 | message: `Cursor Agent is not authenticated. Run \`agent login\` and try again. ${cursorAcpDiscoveryFailedMessage}`, |
| 346 | "Cursor Agent is not authenticated. Run `agent login` and try again. Cursor ACP model discovery failed. Check server logs for details.", | | |
| 347 | models: [ | 363 | models: [ |
| 348 | { | 364 | { |
| 349 | slug: "claude-sonnet-4-6", | 365 | slug: "claude-sonnet-4-6", |
| ... | @@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => { | ... | @@ -411,10 +427,28 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => { |
| 411 | }); | 427 | }); |
| 412 | | 428 | |
| 413 | describe("checkCursorProviderStatus", () => { | 429 | describe("checkCursorProviderStatus", () => { |
| | 430 | it("reports the install docs when the Cursor CLI command is missing", async () => { |
| | 431 | const provider = await runNode( |
| | 432 | checkCursorProviderStatus({ |
| | 433 | enabled: true, |
| | 434 | binaryPath: missingCursorBinaryPath, |
| | 435 | apiEndpoint: "", |
| | 436 | customModels: [], |
| | 437 | }), |
| | 438 | ); |
| | 439 | |
| | 440 | expect(provider).toMatchObject({ |
| | 441 | installed: false, |
| | 442 | status: "error", |
| | 443 | auth: { status: "unknown" }, |
| | 444 | message: cursorCliCommandMissingMessage, |
| | 445 | }); |
| | 446 | }); |
| | 447 | |
| 414 | it("passes the injected environment to ACP model discovery", async () => { | 448 | it("passes the injected environment to ACP model discovery", async () => { |
| 415 | const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture()); | 449 | const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture()); |
| 416 | | 450 | |
| 417 | const provider = await Effect.runPromise( | 451 | const provider = await runNode( |
| 418 | checkCursorProviderStatus( | 452 | checkCursorProviderStatus( |
| 419 | { | 453 | { |
| 420 | enabled: true, | 454 | enabled: true, |
| ... | @@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => { | ... | @@ -426,7 +460,7 @@ describe("checkCursorProviderStatus", () => { |
| 426 | ...process.env, | 460 | ...process.env, |
| 427 | T3_ACP_REQUEST_LOG_PATH: requestLogPath, | 461 | T3_ACP_REQUEST_LOG_PATH: requestLogPath, |
| 428 | }, | 462 | }, |
| 429 | ).pipe(Effect.provide(NodeServices.layer)), | 463 | ), |
| 430 | ); | 464 | ); |
| 431 | | 465 | |
| 432 | expect(provider.models.map((model) => model.slug)).toEqual([ | 466 | expect(provider.models.map((model) => model.slug)).toEqual([ |
| ... | @@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => { | ... | @@ -443,13 +477,13 @@ describe("discoverCursorModelsViaAcp", () => { |
| 443 | it("keeps the ACP probe runtime alive long enough to discover models", async () => { | 477 | it("keeps the ACP probe runtime alive long enough to discover models", async () => { |
| 444 | const wrapperPath = await runNode(makeMockAgentWrapper()); | 478 | const wrapperPath = await runNode(makeMockAgentWrapper()); |
| 445 | | 479 | |
| 446 | const models = await Effect.runPromise( | 480 | const models = await runNode( |
| 447 | discoverCursorModelsViaAcp({ | 481 | discoverCursorModelsViaAcp({ |
| 448 | enabled: true, | 482 | enabled: true, |
| 449 | binaryPath: wrapperPath, | 483 | binaryPath: wrapperPath, |
| 450 | apiEndpoint: "", | 484 | apiEndpoint: "", |
| 451 | customModels: [], | 485 | customModels: [], |
| 452 | }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), | 486 | }).pipe(Effect.scoped), |
| 453 | ); | 487 | ); |
| 454 | | 488 | |
| 455 | expect(models.map((model) => model.slug)).toEqual([ | 489 | expect(models.map((model) => model.slug)).toEqual([ |
| ... | @@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => { | ... | @@ -465,13 +499,13 @@ describe("discoverCursorModelsViaAcp", () => { |
| 465 | makeExitLogFixture("cursor-provider-exit-log-"), | 499 | makeExitLogFixture("cursor-provider-exit-log-"), |
| 466 | ); | 500 | ); |
| 467 | | 501 | |
| 468 | await Effect.runPromise( | 502 | await runNode( |
| 469 | discoverCursorModelsViaAcp({ | 503 | discoverCursorModelsViaAcp({ |
| 470 | enabled: true, | 504 | enabled: true, |
| 471 | binaryPath: wrapperPath, | 505 | binaryPath: wrapperPath, |
| 472 | apiEndpoint: "", | 506 | apiEndpoint: "", |
| 473 | customModels: [], | 507 | customModels: [], |
| 474 | }).pipe(Effect.provide(NodeServices.layer)), | 508 | }), |
| 475 | ); | 509 | ); |
| 476 | | 510 | |
| 477 | const exitLog = await runNode(waitForFileContent(exitLogPath)); | 511 | const exitLog = await runNode(waitForFileContent(exitLogPath)); |