Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ apps/web/playwright-report
apps/web/src/components/__screenshots__
.vitest-*
__screenshots__/
.tanstack
.tanstack
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/La
import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";
import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts";
import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts";
import { ProjectionThreadMessageRepositoryLive } from "../src/persistence/Layers/ProjectionThreadMessages.ts";
import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts";
import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts";
import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts";
Expand Down Expand Up @@ -279,11 +280,13 @@ export const makeOrchestrationIntegrationHarness = (
const providerLayer = useRealCodex
? makeProviderServiceLive().pipe(
Layer.provide(providerSessionDirectoryLayer),
Layer.provide(ProjectionThreadMessageRepositoryLive),
Layer.provide(realCodexRegistry),
Layer.provide(AnalyticsService.layerTest),
)
: makeProviderServiceLive().pipe(
Layer.provide(providerSessionDirectoryLayer),
Layer.provide(ProjectionThreadMessageRepositoryLive),
Layer.provide(fakeRegistry!),
Layer.provide(AnalyticsService.layerTest),
);
Expand Down
8 changes: 6 additions & 2 deletions apps/server/integration/providerService.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
} from "../src/provider/Services/ProviderService.ts";
import { ServerSettingsService } from "../src/serverSettings.ts";
import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { ProjectionThreadMessageRepositoryLive } from "../src/persistence/Layers/ProjectionThreadMessages.ts";
import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";
import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts";
import { ProjectionThreadMessageRepository } from "../src/persistence/Services/ProjectionThreadMessages.ts";

import {
makeTestProviderAdapterHarness,
Expand All @@ -40,7 +42,7 @@ const makeWorkspaceDirectory = Effect.gen(function* () {
interface IntegrationFixture {
readonly cwd: string;
readonly harness: TestProviderAdapterHarness;
readonly layer: Layer.Layer<ProviderService, unknown, never>;
readonly layer: Layer.Layer<ProviderService | ProjectionThreadMessageRepository, unknown, never>;
}

const makeIntegrationFixture = Effect.gen(function* () {
Expand All @@ -58,15 +60,17 @@ const makeIntegrationFixture = Effect.gen(function* () {
const directoryLayer = ProviderSessionDirectoryLive.pipe(
Layer.provide(ProviderSessionRuntimeRepositoryLive),
);
const projectionMessageRepositoryLayer = ProjectionThreadMessageRepositoryLive;

const shared = Layer.mergeAll(
directoryLayer,
projectionMessageRepositoryLayer,
Layer.succeed(ProviderAdapterRegistry, registry),
ServerSettingsService.layerTest(DEFAULT_SERVER_SETTINGS),
AnalyticsService.layerTest,
).pipe(Layer.provide(SqlitePersistenceMemory));

const layer = makeProviderServiceLive().pipe(Layer.provide(shared));
const layer = Layer.mergeAll(shared, makeProviderServiceLive().pipe(Layer.provide(shared)));

return {
cwd,
Expand Down
207 changes: 207 additions & 0 deletions apps/server/src/git/Layers/CursorTextGeneration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { Effect, FileSystem, Layer, Result } from "effect";
import { expect } from "vitest";

import { TextGenerationError } from "@t3tools/contracts";

import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";
import { CursorTextGenerationLive } from "./CursorTextGeneration.ts";

const DEFAULT_TEST_MODEL_SELECTION = {
provider: "cursor" as const,
model: "auto",
};

const CursorTextGenerationTestLayer = CursorTextGenerationLive.pipe(
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-cursor-text-generation-test-",
}),
),
Layer.provideMerge(NodeServices.layer),
);

function makeFakeCursorBinary(
dir: string,
input: {
output: string;
exitCode?: number;
stderr?: string;
argsMustContain?: string;
stdinMustContain?: string;
},
) {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const cursorPath = `${dir}/cursor-agent`;
yield* fs.writeFileString(
cursorPath,
[
"#!/bin/sh",
'args="$*"',
'stdin_content="$(cat)"',
...(input.argsMustContain !== undefined
? [
`if ! printf "%s" "$args" | grep -F -- ${JSON.stringify(input.argsMustContain)} >/dev/null; then`,
' printf "%s\\n" "args missing expected content" >&2',
" exit 2",
"fi",
]
: []),
...(input.stdinMustContain !== undefined
? [
`if ! printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustContain)} >/dev/null; then`,
' printf "%s\\n" "stdin missing expected content" >&2',
" exit 3",
"fi",
]
: []),
...(input.stderr !== undefined
? [`printf "%s\\n" ${JSON.stringify(input.stderr)} >&2`]
: []),
"cat <<'__T3_FAKE_CURSOR_OUTPUT__'",
input.output,
"__T3_FAKE_CURSOR_OUTPUT__",
`exit ${input.exitCode ?? 0}`,
"",
].join("\n"),
);
yield* fs.chmod(cursorPath, 0o755);
return cursorPath;
});
}

function withFakeCursorBinary<A, E, R>(
input: {
output: string;
exitCode?: number;
stderr?: string;
argsMustContain?: string;
stdinMustContain?: string;
},
effect: Effect.Effect<A, E, R>,
) {
return Effect.acquireUseRelease(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cursor-text-" });
const cursorPath = yield* makeFakeCursorBinary(tempDir, input);
const serverSettings = yield* ServerSettingsService;
const previousSettings = yield* serverSettings.getSettings;
yield* serverSettings.updateSettings({
providers: {
cursor: {
binaryPath: cursorPath,
},
},
});
return {
serverSettings,
previousBinaryPath: previousSettings.providers.cursor.binaryPath,
};
}),
() => effect,
({ serverSettings, previousBinaryPath }) =>
serverSettings
.updateSettings({
providers: {
cursor: {
binaryPath: previousBinaryPath,
},
},
})
.pipe(Effect.asVoid),
);
}

it.layer(CursorTextGenerationTestLayer)("CursorTextGenerationLive", (it) => {
it.effect("uses Cursor Agent print mode with structured JSON output for thread titles", () =>
withFakeCursorBinary(
{
output: JSON.stringify({
result: '\n```json\n{"title":"Fix Cursor text generation"}\n```',
}),
argsMustContain: "--model auto",
stdinMustContain: "Return a JSON object with key: title.",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateThreadTitle({
cwd: process.cwd(),
message: "fix the Cursor text generation backend",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
});

expect(generated.title).toBe("Fix Cursor text generation");
}),
),
);

it.effect("resolves Cursor CLI model ids before spawning the agent", () =>
withFakeCursorBinary(
{
output: JSON.stringify({
result: '{"subject":"add cursor support","body":"details"}',
}),
argsMustContain: "--model claude-4.6-opus-high-fast",
stdinMustContain: "Return a JSON object with keys: subject, body",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/cursor-model",
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
modelSelection: {
provider: "cursor",
model: "claude-4.6-opus",
options: {
reasoningEffort: "high",
fastMode: true,
},
},
});

expect(generated.subject).toBe("add cursor support");
}),
),
);

it.effect("returns typed TextGenerationError when Cursor exits non-zero", () =>
withFakeCursorBinary(
{
output: "",
exitCode: 1,
stderr: "cursor execution failed",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const result = yield* textGeneration
.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/cursor-error",
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
})
.pipe(Effect.result);

expect(Result.isFailure(result)).toBe(true);
if (Result.isFailure(result)) {
expect(result.failure).toBeInstanceOf(TextGenerationError);
expect(result.failure.message).toContain(
"Cursor Agent command failed: cursor execution failed",
);
}
}),
),
);
});
Loading
Loading