Skip to content

Commit b918764

Browse files
committed
fix(server): allow local-only worktree bases
1 parent 2daff8c commit b918764

6 files changed

Lines changed: 179 additions & 109 deletions

File tree

‎apps/server/src/git/GitWorkflowService.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ export class GitWorkflowService extends Context.Service<
7373
readonly cwd: string;
7474
readonly remoteName: string;
7575
}) => Effect.Effect<boolean, GitCommandError>;
76+
readonly remoteBranchExists: (input: {
77+
readonly cwd: string;
78+
readonly remoteName: string;
79+
readonly refName: string;
80+
}) => Effect.Effect<boolean, GitCommandError>;
7681
readonly resolveRemoteTrackingCommit: (input: {
7782
readonly cwd: string;
7883
readonly refName: string;
@@ -314,6 +319,10 @@ export const make = Effect.gen(function* () {
314319
ensureGitCommand("GitWorkflowService.remoteExists", input.cwd).pipe(
315320
Effect.andThen(git.remoteExists(input)),
316321
),
322+
remoteBranchExists: (input) =>
323+
ensureGitCommand("GitWorkflowService.remoteBranchExists", input.cwd).pipe(
324+
Effect.andThen(git.remoteBranchExists(input)),
325+
),
317326
resolveRemoteTrackingCommit: (input) =>
318327
ensureGitCommand("GitWorkflowService.resolveRemoteTrackingCommit", input.cwd).pipe(
319328
Effect.andThen(git.resolveRemoteTrackingCommit(input)),

‎apps/server/src/server.test.ts‎

Lines changed: 120 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -8019,6 +8019,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
80198019
bootstrapGitOperations.push("fetch");
80208020
}),
80218021
);
8022+
const remoteBranchExists = vi.fn(
8023+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteBranchExists"]>[0]) =>
8024+
Effect.sync(() => {
8025+
bootstrapGitOperations.push("remote-branch-exists");
8026+
return true;
8027+
}),
8028+
);
80228029
const fetchedOriginCommit = "0123456789abcdef0123456789abcdef01234567";
80238030
const resolveRemoteTrackingCommit = vi.fn(
80248031
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
@@ -8062,6 +8069,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
80628069
gitVcsDriver: {
80638070
remoteExists,
80648071
fetchRemote,
8072+
remoteBranchExists,
80658073
resolveRemoteTrackingCommit,
80668074
createWorktree,
80678075
},
@@ -8145,6 +8153,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
81458153
cwd: "/tmp/project",
81468154
remoteName: "origin",
81478155
});
8156+
assert.deepEqual(remoteBranchExists.mock.calls[0]?.[0], {
8157+
cwd: "/tmp/project",
8158+
remoteName: "origin",
8159+
refName: "main",
8160+
});
81488161
assert.deepEqual(resolveRemoteTrackingCommit.mock.calls[0]?.[0], {
81498162
cwd: "/tmp/project",
81508163
refName: "main",
@@ -8153,6 +8166,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
81538166
assert.deepEqual(bootstrapGitOperations, [
81548167
"remote-exists",
81558168
"fetch",
8169+
"remote-branch-exists",
81568170
"resolve-remote-commit",
81578171
"create-worktree",
81588172
]);
@@ -8180,108 +8194,122 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
81808194
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
81818195
);
81828196

8183-
it.effect(
8184-
"falls back to the local base branch when startFromOrigin is set but no origin remote exists",
8185-
() =>
8186-
Effect.gen(function* () {
8187-
const dispatchedCommands: Array<OrchestrationCommand> = [];
8188-
const remoteExists = vi.fn(
8189-
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
8190-
Effect.succeed(false),
8191-
);
8192-
const fetchRemote = vi.fn(
8193-
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) => Effect.void,
8194-
);
8195-
const resolveRemoteTrackingCommit = vi.fn(
8196-
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
8197-
Effect.succeed({
8198-
commitSha: "0123456789abcdef0123456789abcdef01234567",
8199-
remoteRefName: "origin/main",
8200-
}),
8201-
);
8202-
const createWorktree = vi.fn(
8203-
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
8204-
Effect.succeed({
8205-
worktree: {
8206-
refName: "t3code/bootstrap-refName",
8207-
path: "/tmp/bootstrap-worktree",
8208-
},
8209-
}),
8210-
);
8211-
8212-
yield* buildAppUnderTest({
8213-
layers: {
8214-
gitVcsDriver: {
8215-
remoteExists,
8216-
fetchRemote,
8217-
resolveRemoteTrackingCommit,
8218-
createWorktree,
8219-
},
8220-
orchestrationEngine: {
8221-
dispatch: (command) =>
8222-
Effect.sync(() => {
8223-
dispatchedCommands.push(command);
8224-
return { sequence: dispatchedCommands.length };
8225-
}),
8226-
readEvents: () => Stream.empty,
8197+
it.effect.each([
8198+
{ caseName: "the origin remote is missing", hasOrigin: false },
8199+
{ caseName: "the base branch exists only locally", hasOrigin: true },
8200+
])("falls back to the local base branch when $caseName", ({ hasOrigin }) =>
8201+
Effect.gen(function* () {
8202+
const dispatchedCommands: Array<OrchestrationCommand> = [];
8203+
const remoteExists = vi.fn(
8204+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
8205+
Effect.succeed(hasOrigin),
8206+
);
8207+
const fetchRemote = vi.fn(
8208+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) => Effect.void,
8209+
);
8210+
const resolveRemoteTrackingCommit = vi.fn(
8211+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
8212+
Effect.succeed({
8213+
commitSha: "0123456789abcdef0123456789abcdef01234567",
8214+
remoteRefName: "origin/main",
8215+
}),
8216+
);
8217+
const remoteBranchExists = vi.fn(
8218+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteBranchExists"]>[0]) =>
8219+
Effect.succeed(false),
8220+
);
8221+
const createWorktree = vi.fn(
8222+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
8223+
Effect.succeed({
8224+
worktree: {
8225+
refName: "t3code/bootstrap-refName",
8226+
path: "/tmp/bootstrap-worktree",
82278227
},
8228+
}),
8229+
);
8230+
8231+
yield* buildAppUnderTest({
8232+
layers: {
8233+
gitVcsDriver: {
8234+
remoteExists,
8235+
fetchRemote,
8236+
remoteBranchExists,
8237+
resolveRemoteTrackingCommit,
8238+
createWorktree,
82288239
},
8229-
});
8240+
orchestrationEngine: {
8241+
dispatch: (command) =>
8242+
Effect.sync(() => {
8243+
dispatchedCommands.push(command);
8244+
return { sequence: dispatchedCommands.length };
8245+
}),
8246+
readEvents: () => Stream.empty,
8247+
},
8248+
},
8249+
});
82308250

8231-
const createdAt = "2026-01-01T00:00:00.000Z";
8232-
const wsUrl = yield* getWsServerUrl("/ws");
8233-
yield* Effect.scoped(
8234-
withWsRpcClient(wsUrl, (client) =>
8235-
client[ORCHESTRATION_WS_METHODS.dispatchCommand]({
8236-
type: "thread.turn.start",
8237-
commandId: CommandId.make("cmd-bootstrap-turn-start-no-origin"),
8238-
threadId: ThreadId.make("thread-bootstrap-no-origin"),
8239-
message: {
8240-
messageId: MessageId.make("msg-bootstrap-no-origin"),
8241-
role: "user",
8242-
text: "hello",
8243-
attachments: [],
8251+
const createdAt = "2026-01-01T00:00:00.000Z";
8252+
const wsUrl = yield* getWsServerUrl("/ws");
8253+
yield* Effect.scoped(
8254+
withWsRpcClient(wsUrl, (client) =>
8255+
client[ORCHESTRATION_WS_METHODS.dispatchCommand]({
8256+
type: "thread.turn.start",
8257+
commandId: CommandId.make("cmd-bootstrap-turn-start-no-origin"),
8258+
threadId: ThreadId.make("thread-bootstrap-no-origin"),
8259+
message: {
8260+
messageId: MessageId.make("msg-bootstrap-no-origin"),
8261+
role: "user",
8262+
text: "hello",
8263+
attachments: [],
8264+
},
8265+
modelSelection: defaultModelSelection,
8266+
runtimeMode: "full-access",
8267+
interactionMode: "default",
8268+
bootstrap: {
8269+
createThread: {
8270+
projectId: defaultProjectId,
8271+
title: "Bootstrap Thread",
8272+
modelSelection: defaultModelSelection,
8273+
runtimeMode: "full-access",
8274+
interactionMode: "default",
8275+
branch: "main",
8276+
worktreePath: null,
8277+
createdAt,
82448278
},
8245-
modelSelection: defaultModelSelection,
8246-
runtimeMode: "full-access",
8247-
interactionMode: "default",
8248-
bootstrap: {
8249-
createThread: {
8250-
projectId: defaultProjectId,
8251-
title: "Bootstrap Thread",
8252-
modelSelection: defaultModelSelection,
8253-
runtimeMode: "full-access",
8254-
interactionMode: "default",
8255-
branch: "main",
8256-
worktreePath: null,
8257-
createdAt,
8258-
},
8259-
prepareWorktree: {
8260-
projectCwd: "/tmp/project",
8261-
baseBranch: "main",
8262-
branch: "t3code/bootstrap-refName",
8263-
startFromOrigin: true,
8264-
},
8279+
prepareWorktree: {
8280+
projectCwd: "/tmp/project",
8281+
baseBranch: "main",
8282+
branch: "t3code/bootstrap-refName",
8283+
startFromOrigin: true,
82658284
},
8266-
createdAt,
8267-
}),
8268-
),
8269-
);
8285+
},
8286+
createdAt,
8287+
}),
8288+
),
8289+
);
82708290

8271-
assert.deepEqual(remoteExists.mock.calls[0]?.[0], {
8291+
assert.deepEqual(remoteExists.mock.calls[0]?.[0], {
8292+
cwd: "/tmp/project",
8293+
remoteName: "origin",
8294+
});
8295+
assert.equal(fetchRemote.mock.calls.length, hasOrigin ? 1 : 0);
8296+
assert.equal(remoteBranchExists.mock.calls.length, hasOrigin ? 1 : 0);
8297+
if (hasOrigin) {
8298+
assert.deepEqual(remoteBranchExists.mock.calls[0]?.[0], {
82728299
cwd: "/tmp/project",
82738300
remoteName: "origin",
8274-
});
8275-
assert.equal(fetchRemote.mock.calls.length, 0);
8276-
assert.equal(resolveRemoteTrackingCommit.mock.calls.length, 0);
8277-
assert.deepEqual(createWorktree.mock.calls[0]?.[0], {
8278-
cwd: "/tmp/project",
82798301
refName: "main",
8280-
newRefName: "t3code/bootstrap-refName",
8281-
baseRefName: "main",
8282-
path: null,
82838302
});
8284-
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
8303+
}
8304+
assert.equal(resolveRemoteTrackingCommit.mock.calls.length, 0);
8305+
assert.deepEqual(createWorktree.mock.calls[0]?.[0], {
8306+
cwd: "/tmp/project",
8307+
refName: "main",
8308+
newRefName: "t3code/bootstrap-refName",
8309+
baseRefName: "main",
8310+
path: null,
8311+
});
8312+
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
82858313
);
82868314

82878315
it.effect("records setup-script failures without aborting bootstrap turn start", () =>

‎apps/server/src/vcs/GitVcsDriver.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ export interface GitRemoteExistsInput {
204204
remoteName: string;
205205
}
206206

207+
export interface GitRemoteBranchExistsInput extends GitRemoteExistsInput {
208+
refName: string;
209+
}
210+
207211
export interface GitResolveRemoteTrackingCommitInput {
208212
cwd: string;
209213
refName: string;
@@ -295,6 +299,9 @@ export class GitVcsDriver extends Context.Service<
295299
) => Effect.Effect<string | null, GitCommandError>;
296300
readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>;
297301
readonly remoteExists: (input: GitRemoteExistsInput) => Effect.Effect<boolean, GitCommandError>;
302+
readonly remoteBranchExists: (
303+
input: GitRemoteBranchExistsInput,
304+
) => Effect.Effect<boolean, GitCommandError>;
298305
readonly resolveRemoteTrackingCommit: (
299306
input: GitResolveRemoteTrackingCommitInput,
300307
) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>;

‎apps/server/src/vcs/GitVcsDriverCore.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,23 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
16651665
const driver = yield* GitVcsDriver.GitVcsDriver;
16661666
yield* driver.fetchRemote({ cwd, remoteName: "origin" });
16671667

1668+
assert.equal(
1669+
yield* driver.remoteBranchExists({
1670+
cwd,
1671+
remoteName: "origin",
1672+
refName: initialBranch,
1673+
}),
1674+
true,
1675+
);
1676+
assert.equal(
1677+
yield* driver.remoteBranchExists({
1678+
cwd,
1679+
remoteName: "origin",
1680+
refName: "local-only",
1681+
}),
1682+
false,
1683+
);
1684+
16681685
const resolvedBase = yield* driver.resolveRemoteTrackingCommit({
16691686
cwd,
16701687
refName: initialBranch,

‎apps/server/src/vcs/GitVcsDriverCore.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,15 +1291,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
12911291
}),
12921292
);
12931293

1294-
const remoteBranchExists = (
1295-
cwd: string,
1296-
remoteName: string,
1297-
refName: string,
1298-
): Effect.Effect<boolean, GitCommandError> =>
1294+
const remoteBranchExists: GitVcsDriver.GitVcsDriver["Service"]["remoteBranchExists"] = (input) =>
12991295
executeGit(
13001296
"GitVcsDriver.remoteBranchExists",
1301-
cwd,
1302-
["show-ref", "--verify", "--quiet", `refs/remotes/${remoteName}/${refName}`],
1297+
input.cwd,
1298+
["show-ref", "--verify", "--quiet", `refs/remotes/${input.remoteName}/${input.refName}`],
13031299
{
13041300
allowNonZeroExit: true,
13051301
},
@@ -1446,7 +1442,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
14461442

14471443
if (
14481444
primaryRemoteName &&
1449-
(yield* remoteBranchExists(cwd, primaryRemoteName, normalizedCandidate))
1445+
(yield* remoteBranchExists({
1446+
cwd,
1447+
remoteName: primaryRemoteName,
1448+
refName: normalizedCandidate,
1449+
}))
14501450
) {
14511451
return `${primaryRemoteName}/${normalizedCandidate}`;
14521452
}
@@ -1964,9 +1964,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
19641964
};
19651965
}
19661966

1967-
const hasRemoteBranch = yield* remoteBranchExists(cwd, publishRemoteName, branch).pipe(
1968-
Effect.orElseSucceed(() => false),
1969-
);
1967+
const hasRemoteBranch = yield* remoteBranchExists({
1968+
cwd,
1969+
remoteName: publishRemoteName,
1970+
refName: branch,
1971+
}).pipe(Effect.orElseSucceed(() => false));
19701972
if (hasRemoteBranch) {
19711973
return {
19721974
status: "skipped_up_to_date" as const,
@@ -3314,6 +3316,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
33143316
resolveDefaultBranchName,
33153317
fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)),
33163318
remoteExists,
3319+
remoteBranchExists,
33173320
resolveRemoteTrackingCommit,
33183321
fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)),
33193322
fetchRemoteTrackingBranch: (input) =>

0 commit comments

Comments
 (0)