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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ release/
apps/web/.playwright
apps/web/playwright-report
apps/web/src/components/__screenshots__
task/
260 changes: 230 additions & 30 deletions apps/server/src/git/Layers/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { Effect, FileSystem, Layer, Path } from "effect";
import { expect } from "vitest";

import { ServerConfig } from "../../config.ts";
import { CodexTextGenerationLive } from "./CodexTextGeneration.ts";
import { TextGenerationError } from "../Errors.ts";
import { TextGeneration } from "../Services/TextGeneration.ts";
import { CodexTextGenerationLive } from "./CodexTextGeneration.ts";
import { GitCoreLive } from "./GitCore.ts";
import { GitServiceLive } from "./GitService.ts";

const makeCodexTextGenerationTestLayer = (stateDir: string) =>
CodexTextGenerationLive.pipe(
Layer.provide(GitCoreLive.pipe(Layer.provideMerge(GitServiceLive))),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), stateDir)),
Layer.provideMerge(NodeServices.layer),
);
Expand Down Expand Up @@ -369,37 +372,37 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
yield* fs.makeDirectory(path.join(process.cwd(), "attachments"), { recursive: true });
yield* fs.writeFile(imagePath, Buffer.from("hello"));

const textGeneration = yield* TextGeneration;
const generated = yield* textGeneration
.generateBranchName({
cwd: process.cwd(),
message: "Fix layout bug from screenshot.",
attachments: [
{
type: "image",
id: attachmentId,
name: "bug.png",
mimeType: "image/png",
sizeBytes: 5,
},
],
})
.pipe(
Effect.tap(() =>
fs.stat(imagePath).pipe(
Effect.map((fileInfo) => {
expect(fileInfo.type).toBe("File");
}),
const textGeneration = yield* TextGeneration;
const generated = yield* textGeneration
.generateBranchName({
cwd: process.cwd(),
message: "Fix layout bug from screenshot.",
attachments: [
{
type: "image",
id: attachmentId,
name: "bug.png",
mimeType: "image/png",
sizeBytes: 5,
},
],
})
.pipe(
Effect.tap(() =>
fs.stat(imagePath).pipe(
Effect.map((fileInfo) => {
expect(fileInfo.type).toBe("File");
}),
),
),
),
Effect.ensuring(
fs.remove(imagePath).pipe(Effect.catch(() => Effect.void)),
),
);
Effect.ensuring(
fs.remove(imagePath).pipe(Effect.catch(() => Effect.void)),
),
);

expect(generated.branch).toBe("fix/ui-regression");
}),
),
expect(generated.branch).toBe("fix/ui-regression");
}),
),
);

it.effect("ignores missing attachment ids for codex image inputs", () =>
Expand Down Expand Up @@ -513,4 +516,201 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => {
}),
),
);

it.effect(
"includes gitmoji instructions when config is set to 'gitmoji'",
() =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "✨ add authentication",
body: "",
}),
stdinMustContain: "subject must start with a gitmoji emoji",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/auth",
stagedSummary: "M src/auth.ts",
stagedPatch: "+export function authenticate() {}",
});

expect(generated.subject).toContain("✨");
}),
),
);

it.effect(
"uses conventional style when config is set to 'conventional'",
() =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "add authentication feature",
body: "",
}),
stdinMustNotContain: "gitmoji",
stdinMustContain: "subject must be imperative",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/auth",
stagedSummary: "M src/auth.ts",
stagedPatch: "+export function authenticate() {}",
});

expect(generated.subject).not.toContain("✨");
expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u);
}),
),
);

it.effect("defaults to conventional style when config is null", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "fix memory leak",
body: "",
}),
stdinMustNotContain: "gitmoji",
stdinMustContain: "subject must be imperative",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "fix/leak",
stagedSummary: "M src/memory.ts",
stagedPatch: "-const leak = []",
});

expect(generated.subject).not.toContain("✨");
}),
),
);

it.effect("handles case-insensitive 'gitmoji' config value", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "🐛 fix crash on startup",
body: "",
}),
stdinMustContain: "gitmoji",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "fix/crash",
stagedSummary: "M src/index.ts",
stagedPatch: "+process.on('uncaughtException', logger.error)",
});

expect(generated.subject).toMatch(/^[\p{Emoji}]/u);
}),
),
);

it.effect("handles partial match with 'gitmoji' in value", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "📝 update readme",
body: "",
}),
stdinMustContain: "gitmoji",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "docs/readme",
stagedSummary: "M README.md",
stagedPatch: "+# Installation",
});

expect(generated.subject).toMatch(/^[\p{Emoji}]/u);
}),
),
);

it.effect("ignores invalid config values and defaults to conventional", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "refactor code structure",
body: "",
}),
stdinMustNotContain: "gitmoji",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "refactor/structure",
stagedSummary: "M src/index.ts",
stagedPatch: "-export default App",
});

expect(generated.subject).not.toMatch(/^[\p{Emoji}]/u);
}),
),
);

it.effect("handles whitespace in config value gracefully", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "✨ add new endpoints",
body: "",
}),
stdinMustContain: "gitmoji",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

const generated = yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/api",
stagedSummary: "M src/api.ts",
stagedPatch: "+export const handlers = {}",
});

expect(generated.subject).toMatch(/^[\p{Emoji}]/u);
}),
),
);

it.effect("includes common gitmoji examples in prompt when enabled", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "✅ add tests",
body: "",
}),
stdinMustContain: "✨ feat: new feature",
},
Effect.gen(function* () {
const textGeneration = yield* TextGeneration;

yield* textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "test/coverage",
stagedSummary: "M test.test.ts",
stagedPatch: "+expect(true).toBe(true)",
});
}),
),
);
});
Loading