Skip to content
Open
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
265 changes: 260 additions & 5 deletions apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const adapter = {

interface HarnessOptions {
readonly createWorktree?: GitWorkflow.GitWorkflowService["Service"]["createWorktree"];
readonly renameBranch?: GitWorkflow.GitWorkflowService["Service"]["renameBranch"];
readonly runSetup?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"];
readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"];
readonly generateBranchName?: TextGeneration.TextGeneration["Service"]["generateBranchName"];
Expand All @@ -83,9 +84,9 @@ function makeHarness(options: HarnessOptions = {}) {
const outbox = EffectOutbox.layer.pipe(Layer.provide(database));
const createWorktree = vi.fn(
options.createWorktree ??
(() =>
((input) =>
Effect.succeed({
worktree: { path: "/repo-worktrees/feature", refName: "feature", headSha: "abc" },
worktree: { path: "/repo-worktrees/feature", refName: input.newRefName, headSha: "abc" },
} as never)),
);
const runSetup = vi.fn(
Expand All @@ -94,6 +95,12 @@ function makeHarness(options: HarnessOptions = {}) {
const generateBranchName = vi.fn(
options.generateBranchName ?? (() => Effect.succeed({ branch: "generated-branch" })),
);
const generateTitle = vi.fn(
options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),
);
const renameBranch = vi.fn(
options.renameBranch ?? ((input) => Effect.succeed({ branch: input.newBranch })),
);
const externalServices = Layer.mergeAll(
Layer.succeed(ProjectService.ProjectService, {
create: () => Effect.die("unused"),
Expand All @@ -106,6 +113,7 @@ function makeHarness(options: HarnessOptions = {}) {
}),
Layer.mock(GitWorkflow.GitWorkflowService)({
createWorktree,
renameBranch,
fetchRemote: () => Effect.void,
removeWorktree: () => Effect.void,
resolveRemoteTrackingCommit: () =>
Expand All @@ -115,8 +123,7 @@ function makeHarness(options: HarnessOptions = {}) {
runForThread: runSetup,
}),
Layer.mock(TextGeneration.TextGeneration)({
generateThreadTitle:
options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })),
generateThreadTitle: generateTitle,
generateBranchName,
}),
ServerSettings.layerTest(options.serverSettings),
Expand All @@ -128,7 +135,9 @@ function makeHarness(options: HarnessOptions = {}) {
return {
layer: Layer.mergeAll(launch, threadManagement, outbox, database),
createWorktree,
renameBranch,
generateBranchName,
generateTitle,
runSetup,
};
}
Expand All @@ -137,13 +146,16 @@ function launchInput(input: {
readonly command: string;
readonly thread: string;
readonly message?: string;
readonly title?: string;
readonly titleSeed?: string;
readonly workspace?: ThreadLaunch.ThreadLaunchWorkspaceStrategy;
}) {
return {
commandId: CommandId.make(input.command),
threadId: ThreadId.make(input.thread),
projectId,
title: "New thread",
title: input.title ?? "New thread",
...(input.titleSeed === undefined ? {} : { titleSeed: input.titleSeed }),
modelSelection,
runtimeMode: "full-access" as const,
interactionMode: "default" as const,
Expand Down Expand Up @@ -548,6 +560,249 @@ it.effect("falls back when the source control writer is unavailable", () =>
}),
);

it.effect("replaces a title seeded from the first message with a generated title", () =>
Effect.gen(function* () {
const harness = makeHarness();
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:seeded-title",
thread: "thread:launch:seeded-title",
message: "fix the login bug please",
title: "fix the login bug please",
}),
);
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.thread.title === "Generated title")),
);
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("replaces a title matching the client title seed with a generated title", () =>
Effect.gen(function* () {
const harness = makeHarness();
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:title-seed",
thread: "thread:launch:title-seed",
message: "the full message body with extra context appended",
title: "Image: screenshot.png",
titleSeed: "Image: screenshot.png",
}),
);
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.thread.title === "Generated title")),
);
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("keeps a user-provided title instead of generating one", () =>
Effect.gen(function* () {
const harness = makeHarness();
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:custom-title",
thread: "thread:launch:custom-title",
message: "fix the login bug please",
title: "My custom title",
}),
);
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.runs[0]?.status === "starting")),
);
assert.equal(harness.generateTitle.mock.calls.length, 0);
assert.equal(
(yield* threads.getThreadProjection(launched.threadId)).thread.title,
"My custom title",
);
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("names the worktree itself when the client provides no branch", () =>
Effect.gen(function* () {
const harness = makeHarness();
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:server-named-branch",
thread: "thread:launch:server-named-branch",
message: "Build the feature",
workspace: { type: "worktree", baseRef: "main" },
}),
);
yield* waitUntil(() => Effect.sync(() => harness.createWorktree.mock.calls.length === 1));
assert.match(
harness.createWorktree.mock.calls[0]?.[0].newRefName ?? "",
/^t3code\/[0-9a-f]{8}$/u,
);
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")),
);
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("renames a temporary t3code/<hash> branch off the provisioning critical path", () =>
Effect.gen(function* () {
const branchNameStarted = yield* Deferred.make<void>();
const allowBranchName = yield* Deferred.make<void>();
const harness = makeHarness({
createWorktree: (input) =>
Effect.succeed({
worktree: { path: "/repo-worktrees/temp", refName: input.newRefName, headSha: "abc" },
} as never),
generateBranchName: () =>
Deferred.succeed(branchNameStarted, undefined).pipe(
Effect.andThen(Deferred.await(allowBranchName)),
Effect.as({ branch: "generated-branch" }),
),
});
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:temp-branch",
thread: "thread:launch:temp-branch",
message: "Build the feature",
workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" },
}),
);
yield* Deferred.await(branchNameStarted);
assert.equal(harness.createWorktree.mock.calls[0]?.[0].newRefName, "t3code/abcd1234");
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.runs[0]?.status === "starting")),
);
assert.equal(
(yield* threads.getThreadProjection(launched.threadId)).thread.branch,
"t3code/abcd1234",
);
yield* Deferred.succeed(allowBranchName, undefined);
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")),
);
assert.deepEqual(harness.renameBranch.mock.calls[0]?.[0], {
cwd: "/repo-worktrees/temp",
oldBranch: "t3code/abcd1234",
newBranch: "generated-branch",
});
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("keeps an explicit branch name instead of generating one", () =>
Effect.gen(function* () {
const harness = makeHarness();
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
yield* launches.launch(
launchInput({
command: "command:launch:explicit-branch",
thread: "thread:launch:explicit-branch",
message: "Build the feature",
workspace: { type: "worktree", baseRef: "main", branch: "my-feature" },
}),
);
yield* waitUntil(() => Effect.sync(() => harness.createWorktree.mock.calls.length === 1));
assert.equal(harness.generateBranchName.mock.calls.length, 0);
assert.equal(harness.createWorktree.mock.calls[0]?.[0].newRefName, "my-feature");
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("keeps the temporary branch when branch generation fails", () =>
Effect.gen(function* () {
const harness = makeHarness({
createWorktree: (input) =>
Effect.succeed({
worktree: { path: "/repo-worktrees/temp", refName: input.newRefName, headSha: "abc" },
} as never),
generateBranchName: () => Effect.die("branch generation is down"),
});
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:branch-fallback",
thread: "thread:launch:branch-fallback",
message: "Build the feature",
workspace: { type: "worktree", baseRef: "main", branch: "t3code/abcd1234" },
}),
);
yield* waitUntil(() => Effect.sync(() => harness.generateBranchName.mock.calls.length === 1));
assert.equal(harness.createWorktree.mock.calls[0]?.[0].newRefName, "t3code/abcd1234");
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.runs[0]?.status === "starting")),
);
assert.equal(harness.renameBranch.mock.calls.length, 0);
assert.equal(
(yield* threads.getThreadProjection(launched.threadId)).thread.branch,
"t3code/abcd1234",
);
}).pipe(Effect.provide(harness.layer));
}),
);

it.effect("renames a temporary branch on an existing worktree to a generated name", () =>
Effect.gen(function* () {
const harness = makeHarness();
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const threads = yield* ThreadManagement.ThreadManagementService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:existing-worktree-rename",
thread: "thread:launch:existing-worktree-rename",
message: "Build the feature",
workspace: {
type: "existing_worktree",
worktreePath: "/repo-worktrees/t3code-abcd1234",
branch: "t3code/abcd1234",
},
}),
);
yield* waitUntil(() =>
threads
.getThreadProjection(launched.threadId)
.pipe(Effect.map((projection) => projection.thread.branch === "generated-branch")),
);
assert.deepEqual(harness.renameBranch.mock.calls[0]?.[0], {
cwd: "/repo-worktrees/t3code-abcd1234",
oldBranch: "t3code/abcd1234",
newBranch: "generated-branch",
});
}).pipe(Effect.provide(harness.layer));
}),
);

for (const failurePoint of ["worktree", "setup"] as const) {
it.effect(
`${failurePoint} failure keeps the thread and message visible and emits failure items`,
Expand Down
Loading
Loading