// @effect-diagnostics nodeBuiltinImport:offimport * as NodePath from "node:path";import * as NodeOS from "node:os";import * as NodeURL from "node:url";import * as NodeFS from "node:fs"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import * as Clock from "effect/Clock";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Schema from "effect/Schema";import { createModelSelection } from "@t3tools/shared/model";import { expect } from "vite-plus/test"; import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts"; import * as ServerConfig from "../config.ts";import * as TextGeneration from "./TextGeneration.ts";import { makeCursorTextGeneration } from "./CursorTextGeneration.ts";const decodeCursorSettings = Schema.decodeSync(CursorSettings); const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));const mockAgentPath = NodePath.join(__dirname, "../../scripts/acp-mock-agent.ts"); function shellSingleQuote(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`;} const CursorTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3code-cursor-text-generation-test-",}).pipe(Layer.provideMerge(NodeServices.layer)); function makeAcpAgentWrapper(dir: string, env: Record<string, string>): string { const binDir = NodePath.join(dir, "bin"); const agentPath = NodePath.join(binDir, "agent"); NodeFS.mkdirSync(binDir, { recursive: true }); NodeFS.writeFileSync( agentPath, [ "#!/bin/sh", ...Object.entries(env).map(([key, value]) => `export ${key}=${shellSingleQuote(value)}`), 'if [ "$1" != "acp" ]; then', ' printf "%s\\n" "unexpected args: $*" >&2', " exit 11", "fi", `exec node ${JSON.stringify(mockAgentPath)}`, "", ].join("\n"), "utf8", ); NodeFS.chmodSync(agentPath, 0o755); return agentPath;} function withFakeAcpAgent<A, E, R>( env: Record<string, string>, effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,) { return Effect.gen(function* () { const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-cursor-text-acp-")); yield* Effect.addFinalizer(() => Effect.sync(() => { NodeFS.rmSync(tempDir, { recursive: true, force: true }); }), ); const agentPath = makeAcpAgentWrapper(tempDir, env); const config = decodeCursorSettings({ binaryPath: agentPath }); const textGeneration = yield* makeCursorTextGeneration(config); return yield* effectFn(textGeneration); }).pipe(Effect.scoped);} function waitForFileContent(path: string): Effect.Effect<string> { return Effect.gen(function* () { const deadline = (yield* Clock.currentTimeMillis) + 5_000; for (;;) { const result = yield* Effect.exit(Effect.sync(() => NodeFS.readFileSync(path, "utf8"))); if (Exit.isSuccess(result)) { return result.value; } { if ((yield* Clock.currentTimeMillis) >= deadline) { return yield* Effect.die(result.cause); } } yield* Effect.sleep(25); } });} it.layer(CursorTextGenerationTestLayer)("CursorTextGeneration", (it) => { it.effect("uses ACP model config options instead of raw CLI model ids", () => { const requestLogDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3code-cursor-text-log-"), ); const requestLogPath = NodePath.join(requestLogDir, "requests.ndjson"); return withFakeAcpAgent( { T3_ACP_REQUEST_LOG_PATH: requestLogPath, T3_ACP_PROMPT_RESPONSE_TEXT: JSON.stringify({ subject: "Add generated commit message", body: "- verify cursor acp model config path", }), }, (textGeneration) => Effect.gen(function* () { const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "feature/cursor-text-generation", stagedSummary: "M apps/server/src/textGeneration/CursorTextGeneration.ts", stagedPatch: "diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/apps/server/src/textGeneration/CursorTextGeneration.ts", modelSelection: { ...createModelSelection(ProviderInstanceId.make("cursor"), "gpt-5.4", [ { id: "reasoning", value: "xhigh" }, { id: "fastMode", value: true }, { id: "contextWindow", value: "1m" }, ]), }, }); expect(generated.subject).toBe("Add generated commit message"); expect(generated.body).toBe("- verify cursor acp model config path"); const requests = NodeFS.readFileSync(requestLogPath, "utf8") .trim() .split("\n") .filter((line) => line.length > 0) .map( (line) => JSON.parse(line) as { method?: string; params?: Record<string, unknown> }, ); expect( requests.find((request) => request.method === "initialize")?.params?.clientCapabilities, ).toMatchObject({ _meta: { parameterizedModelPicker: true, }, }); expect( requests.some( (request) => request.method === "session/set_config_option" && request.params?.configId === "model" && request.params?.value === "gpt-5.4", ), ).toBe(true); expect( requests.some( (request) => request.method === "session/set_config_option" && request.params?.configId === "reasoning" && request.params?.value === "extra-high", ), ).toBe(true); expect( requests.some( (request) => request.method === "session/set_config_option" && request.params?.configId === "context" && request.params?.value === "1m", ), ).toBe(true); expect( requests.some( (request) => request.method === "session/set_config_option" && request.params?.configId === "fast" && request.params?.value === "true", ), ).toBe(true); expect( requests.find((request) => request.method === "session/prompt")?.params?.prompt, ).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "text", text: expect.stringContaining("Staged patch:"), }), ]), ); NodeFS.rmSync(requestLogDir, { recursive: true, force: true }); }), ); }); it.effect("accepts json objects with extra assistant text around them", () => withFakeAcpAgent( { T3_ACP_PROMPT_RESPONSE_TEXT: 'Sure, here is the JSON:\n```json\n{\n "subject": "Update README dummy comment with attribution and date",\n "body": ""\n}\n```\nDone.', }, (textGeneration) => Effect.gen(function* () { const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "feature/cursor-noisy-json", stagedSummary: "M README.md", stagedPatch: "diff --git a/README.md b/README.md", modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "composer-2", }, }); expect(generated.subject).toBe("Update README dummy comment with attribution and date"); expect(generated.body).toBe(""); }), ), ); it.effect("generates thread titles through Cursor ACP text generation", () => withFakeAcpAgent( { T3_ACP_PROMPT_RESPONSE_TEXT: JSON.stringify({ title: '"Trim reconnect spinner status after resume."', }), }, (textGeneration) => Effect.gen(function* () { const generated = yield* textGeneration.generateThreadTitle({ cwd: process.cwd(), message: "Fix the reconnect spinner after a resumed session.", modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "composer-2", }, }); expect(generated.title).toBe("Trim reconnect spinner status after resume."); }), ), ); it.effect("closes the ACP child process after text generation completes", () => { const exitLogDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3code-cursor-text-exit-log-"), ); const exitLogPath = NodePath.join(exitLogDir, "exit.log"); return withFakeAcpAgent( { T3_ACP_EXIT_LOG_PATH: exitLogPath, T3_ACP_PROMPT_RESPONSE_TEXT: JSON.stringify({ subject: "Close runtime after generation", body: "", }), }, (textGeneration) => Effect.gen(function* () { const generated = yield* textGeneration.generateCommitMessage({ cwd: process.cwd(), branch: "feature/cursor-runtime-close", stagedSummary: "M apps/server/src/textGeneration/CursorTextGeneration.ts", stagedPatch: "diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/apps/server/src/textGeneration/CursorTextGeneration.ts", modelSelection: { instanceId: ProviderInstanceId.make("cursor"), model: "composer-2", }, }); expect(generated.subject).toBe("Close runtime after generation"); const exitLog = yield* waitForFileContent(exitLogPath); expect(exitLog).toContain("exit:0"); NodeFS.rmSync(exitLogDir, { recursive: true, force: true }); }), ); });});
// @effect-diagnostics nodeBuiltinImport:off
import * as NodePath from "node:path";
import * as NodeOS from "node:os";
import * as NodeURL from "node:url";
import * as NodeFS from "node:fs";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import * as Clock from "effect/Clock";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import { createModelSelection } from "@t3tools/shared/model";
import { expect } from "vite-plus/test";
import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts";
import * as ServerConfig from "../config.ts";
import * as TextGeneration from "./TextGeneration.ts";
import { makeCursorTextGeneration } from "./CursorTextGeneration.ts";
const decodeCursorSettings = Schema.decodeSync(CursorSettings);
const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
const mockAgentPath = NodePath.join(__dirname, "../../scripts/acp-mock-agent.ts");
function shellSingleQuote(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
const CursorTextGenerationTestLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
prefix: "t3code-cursor-text-generation-test-",
}).pipe(Layer.provideMerge(NodeServices.layer));
function makeAcpAgentWrapper(dir: string, env: Record<string, string>): string {
const binDir = NodePath.join(dir, "bin");
const agentPath = NodePath.join(binDir, "agent");
NodeFS.mkdirSync(binDir, { recursive: true });
NodeFS.writeFileSync(
agentPath,
[
"#!/bin/sh",
...Object.entries(env).map(([key, value]) => `export ${key}=${shellSingleQuote(value)}`),
'if [ "$1" != "acp" ]; then',
' printf "%s\\n" "unexpected args: $*" >&2',
" exit 11",
"fi",
`exec node ${JSON.stringify(mockAgentPath)}`,
"",
].join("\n"),
"utf8",
);
NodeFS.chmodSync(agentPath, 0o755);
return agentPath;
}
function withFakeAcpAgent<A, E, R>(
env: Record<string, string>,
effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
) {
return Effect.gen(function* () {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3code-cursor-text-acp-"));
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
NodeFS.rmSync(tempDir, { recursive: true, force: true });
}),
);
const agentPath = makeAcpAgentWrapper(tempDir, env);
const config = decodeCursorSettings({ binaryPath: agentPath });
const textGeneration = yield* makeCursorTextGeneration(config);
return yield* effectFn(textGeneration);
}).pipe(Effect.scoped);
}
function waitForFileContent(path: string): Effect.Effect<string> {
return Effect.gen(function* () {
const deadline = (yield* Clock.currentTimeMillis) + 5_000;
for (;;) {
const result = yield* Effect.exit(Effect.sync(() => NodeFS.readFileSync(path, "utf8")));
if (Exit.isSuccess(result)) {
return result.value;
}
{
if ((yield* Clock.currentTimeMillis) >= deadline) {
return yield* Effect.die(result.cause);
}
}
yield* Effect.sleep(25);
}
});
}
it.layer(CursorTextGenerationTestLayer)("CursorTextGeneration", (it) => {
it.effect("uses ACP model config options instead of raw CLI model ids", () => {
const requestLogDir = NodeFS.mkdtempSync(
NodePath.join(NodeOS.tmpdir(), "t3code-cursor-text-log-"),
);
const requestLogPath = NodePath.join(requestLogDir, "requests.ndjson");
return withFakeAcpAgent(
{
T3_ACP_REQUEST_LOG_PATH: requestLogPath,
T3_ACP_PROMPT_RESPONSE_TEXT: JSON.stringify({
subject: "Add generated commit message",
body: "- verify cursor acp model config path",
}),
},
(textGeneration) =>
Effect.gen(function* () {
const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/cursor-text-generation",
stagedSummary: "M apps/server/src/textGeneration/CursorTextGeneration.ts",
stagedPatch:
"diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/apps/server/src/textGeneration/CursorTextGeneration.ts",
modelSelection: {
...createModelSelection(ProviderInstanceId.make("cursor"), "gpt-5.4", [
{ id: "reasoning", value: "xhigh" },
{ id: "fastMode", value: true },
{ id: "contextWindow", value: "1m" },
]),
},
});
expect(generated.subject).toBe("Add generated commit message");
expect(generated.body).toBe("- verify cursor acp model config path");
const requests = NodeFS.readFileSync(requestLogPath, "utf8")
.trim()
.split("\n")
.filter((line) => line.length > 0)
.map(
(line) => JSON.parse(line) as { method?: string; params?: Record<string, unknown> },
);
expect(
requests.find((request) => request.method === "initialize")?.params?.clientCapabilities,
).toMatchObject({
_meta: {
parameterizedModelPicker: true,
},
});
expect(
requests.some(
(request) =>
request.method === "session/set_config_option" &&
request.params?.configId === "model" &&
request.params?.value === "gpt-5.4",
),
).toBe(true);
expect(
requests.some(
(request) =>
request.method === "session/set_config_option" &&
request.params?.configId === "reasoning" &&
request.params?.value === "extra-high",
),
).toBe(true);
expect(
requests.some(
(request) =>
request.method === "session/set_config_option" &&
request.params?.configId === "context" &&
request.params?.value === "1m",
),
).toBe(true);
expect(
requests.some(
(request) =>
request.method === "session/set_config_option" &&
request.params?.configId === "fast" &&
request.params?.value === "true",
),
).toBe(true);
expect(
requests.find((request) => request.method === "session/prompt")?.params?.prompt,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "text",
text: expect.stringContaining("Staged patch:"),
}),
]),
);
NodeFS.rmSync(requestLogDir, { recursive: true, force: true });
}),
);
});
it.effect("accepts json objects with extra assistant text around them", () =>
withFakeAcpAgent(
{
T3_ACP_PROMPT_RESPONSE_TEXT:
'Sure, here is the JSON:\n```json\n{\n "subject": "Update README dummy comment with attribution and date",\n "body": ""\n}\n```\nDone.',
},
(textGeneration) =>
Effect.gen(function* () {
const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/cursor-noisy-json",
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
modelSelection: {
instanceId: ProviderInstanceId.make("cursor"),
model: "composer-2",
},
});
expect(generated.subject).toBe("Update README dummy comment with attribution and date");
expect(generated.body).toBe("");
}),
),
);
it.effect("generates thread titles through Cursor ACP text generation", () =>
withFakeAcpAgent(
{
T3_ACP_PROMPT_RESPONSE_TEXT: JSON.stringify({
title: '"Trim reconnect spinner status after resume."',
}),
},
(textGeneration) =>
Effect.gen(function* () {
const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "Fix the reconnect spinner after a resumed session.",
modelSelection: {
instanceId: ProviderInstanceId.make("cursor"),
model: "composer-2",
},
});
expect(generated.title).toBe("Trim reconnect spinner status after resume.");
}),
),
);
it.effect("closes the ACP child process after text generation completes", () => {
const exitLogDir = NodeFS.mkdtempSync(
NodePath.join(NodeOS.tmpdir(), "t3code-cursor-text-exit-log-"),
);
const exitLogPath = NodePath.join(exitLogDir, "exit.log");
return withFakeAcpAgent(
{
T3_ACP_EXIT_LOG_PATH: exitLogPath,
T3_ACP_PROMPT_RESPONSE_TEXT: JSON.stringify({
subject: "Close runtime after generation",
body: "",
}),
},
(textGeneration) =>
Effect.gen(function* () {
const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/cursor-runtime-close",
stagedSummary: "M apps/server/src/textGeneration/CursorTextGeneration.ts",
stagedPatch:
"diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/apps/server/src/textGeneration/CursorTextGeneration.ts",
modelSelection: {
instanceId: ProviderInstanceId.make("cursor"),
model: "composer-2",
},
});
expect(generated.subject).toBe("Close runtime after generation");
const exitLog = yield* waitForFileContent(exitLogPath);
expect(exitLog).toContain("exit:0");
NodeFS.rmSync(exitLogDir, { recursive: true, force: true });
}),
);
});
});