diff --git a/src/browser/api.ts b/src/browser/api.ts
index 449e57e23..64cef7b35 100644
--- a/src/browser/api.ts
+++ b/src/browser/api.ts
@@ -211,8 +211,15 @@ const webApi: IPCApi = {
},
workspace: {
list: () => invokeIPC(IPC_CHANNELS.WORKSPACE_LIST),
- create: (projectPath, branchName, trunkBranch) =>
- invokeIPC(IPC_CHANNELS.WORKSPACE_CREATE, projectPath, branchName, trunkBranch),
+ create: (projectPath, branchName, trunkBranch, runtimeConfig, options) =>
+ invokeIPC(
+ IPC_CHANNELS.WORKSPACE_CREATE,
+ projectPath,
+ branchName,
+ trunkBranch,
+ runtimeConfig,
+ options
+ ),
remove: (workspaceId, options) =>
invokeIPC(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId, options),
rename: (workspaceId, newName) =>
diff --git a/src/browser/components/ChatInput/CreationControls.tsx b/src/browser/components/ChatInput/CreationControls.tsx
index dd231843b..d1443e977 100644
--- a/src/browser/components/ChatInput/CreationControls.tsx
+++ b/src/browser/components/ChatInput/CreationControls.tsx
@@ -7,6 +7,8 @@ interface CreationControlsProps {
branches: string[];
trunkBranch: string;
onTrunkBranchChange: (branch: string) => void;
+ fetchLatest: boolean;
+ onFetchLatestChange: (value: boolean) => void;
runtimeMode: RuntimeMode;
sshHost: string;
onRuntimeChange: (mode: RuntimeMode, host: string) => void;
@@ -74,6 +76,30 @@ export function CreationControls(props: CreationControlsProps) {
+
+ {/* Fetch latest option */}
+
+
+
+ ?
+
+ Fetch latest:
+
+ Runs git fetch origin --prune before branching so new workspaces start from
+ the freshest remote state.
+
+
+
);
}
diff --git a/src/browser/components/ChatInput/index.tsx b/src/browser/components/ChatInput/index.tsx
index 2dd851af9..e76bdbed3 100644
--- a/src/browser/components/ChatInput/index.tsx
+++ b/src/browser/components/ChatInput/index.tsx
@@ -993,6 +993,8 @@ export const ChatInput: React.FC = (props) => {
branches={creationState.branches}
trunkBranch={creationState.trunkBranch}
onTrunkBranchChange={creationState.setTrunkBranch}
+ fetchLatest={creationState.fetchLatest}
+ onFetchLatestChange={creationState.setFetchLatest}
runtimeMode={creationState.runtimeMode}
sshHost={creationState.sshHost}
onRuntimeChange={creationState.setRuntimeOptions}
diff --git a/src/browser/components/ChatInput/useCreationWorkspace.ts b/src/browser/components/ChatInput/useCreationWorkspace.ts
index 89c0ba4ac..234a9f425 100644
--- a/src/browser/components/ChatInput/useCreationWorkspace.ts
+++ b/src/browser/components/ChatInput/useCreationWorkspace.ts
@@ -36,6 +36,8 @@ interface UseCreationWorkspaceReturn {
branches: string[];
trunkBranch: string;
setTrunkBranch: (branch: string) => void;
+ fetchLatest: boolean;
+ setFetchLatest: (value: boolean) => void;
runtimeMode: RuntimeMode;
sshHost: string;
setRuntimeOptions: (mode: RuntimeMode, host: string) => void;
@@ -62,7 +64,7 @@ export function useCreationWorkspace({
const [isSending, setIsSending] = useState(false);
// Centralized draft workspace settings with automatic persistence
- const { settings, setRuntimeOptions, setTrunkBranch, getRuntimeString } =
+ const { settings, setRuntimeOptions, setTrunkBranch, setFetchLatest, getRuntimeString } =
useDraftWorkspaceSettings(projectPath, branches, recommendedTrunk);
// Get send options from shared hook (uses project-scoped storage key)
@@ -107,6 +109,7 @@ export function useCreationWorkspace({
runtimeConfig,
projectPath, // Pass projectPath when workspaceId is null
trunkBranch: settings.trunkBranch, // Pass selected trunk branch from settings
+ fetchLatest: settings.fetchLatest, // Fetch remote updates before branching when requested
});
if (!result.success) {
@@ -139,6 +142,7 @@ export function useCreationWorkspace({
getRuntimeString,
sendMessageOptions,
settings.trunkBranch,
+ settings.fetchLatest,
]
);
@@ -146,6 +150,8 @@ export function useCreationWorkspace({
branches,
trunkBranch: settings.trunkBranch,
setTrunkBranch,
+ fetchLatest: settings.fetchLatest,
+ setFetchLatest,
runtimeMode: settings.runtimeMode,
sshHost: settings.sshHost,
setRuntimeOptions,
diff --git a/src/browser/contexts/WorkspaceContext.test.tsx b/src/browser/contexts/WorkspaceContext.test.tsx
index 119f12817..e605f9dd5 100644
--- a/src/browser/contexts/WorkspaceContext.test.tsx
+++ b/src/browser/contexts/WorkspaceContext.test.tsx
@@ -163,7 +163,13 @@ describe("WorkspaceContext", () => {
result = await ctx().createWorkspace("/gamma", "feature", "main");
});
- expect(workspaceApi.create).toHaveBeenCalledWith("/gamma", "feature", "main", undefined);
+ expect(workspaceApi.create).toHaveBeenCalledWith(
+ "/gamma",
+ "feature",
+ "main",
+ undefined,
+ undefined
+ );
expect(projectsApi.list).toHaveBeenCalled();
expect(result!.workspaceId).toBe("ws-new");
expect(result!.projectPath).toBe("/gamma");
diff --git a/src/browser/contexts/WorkspaceContext.tsx b/src/browser/contexts/WorkspaceContext.tsx
index 2289fe5c0..2e18351d9 100644
--- a/src/browser/contexts/WorkspaceContext.tsx
+++ b/src/browser/contexts/WorkspaceContext.tsx
@@ -222,7 +222,8 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
projectPath: string,
branchName: string,
trunkBranch: string,
- runtimeConfig?: RuntimeConfig
+ runtimeConfig?: RuntimeConfig,
+ options?: { fetchLatest?: boolean }
) => {
console.assert(
typeof trunkBranch === "string" && trunkBranch.trim().length > 0,
@@ -232,7 +233,8 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
projectPath,
branchName,
trunkBranch,
- runtimeConfig
+ runtimeConfig,
+ options
);
if (result.success) {
// Backend has already updated the config - reload projects to get updated state
diff --git a/src/browser/hooks/useDraftWorkspaceSettings.ts b/src/browser/hooks/useDraftWorkspaceSettings.ts
index 8908a7df4..747ad679e 100644
--- a/src/browser/hooks/useDraftWorkspaceSettings.ts
+++ b/src/browser/hooks/useDraftWorkspaceSettings.ts
@@ -13,6 +13,7 @@ import {
getModelKey,
getRuntimeKey,
getTrunkBranchKey,
+ getFetchLatestKey,
getProjectScopeId,
} from "@/common/constants/storage";
import type { UIMode } from "@/common/types/mode";
@@ -33,6 +34,7 @@ export interface DraftWorkspaceSettings {
runtimeMode: RuntimeMode;
sshHost: string;
trunkBranch: string;
+ fetchLatest: boolean;
}
/**
@@ -52,6 +54,7 @@ export function useDraftWorkspaceSettings(
settings: DraftWorkspaceSettings;
setRuntimeOptions: (mode: RuntimeMode, host: string) => void;
setTrunkBranch: (branch: string) => void;
+ setFetchLatest: (value: boolean) => void;
getRuntimeString: () => string | undefined;
} {
// Global AI settings (read-only from global state)
@@ -74,7 +77,13 @@ export function useDraftWorkspaceSettings(
{ listener: true }
);
- // Project-scoped trunk branch preference (persisted per project)
+ // Project-scoped workspace creation preferences (persisted per project)
+ const [fetchLatest, setFetchLatest] = usePersistedState(
+ getFetchLatestKey(projectPath),
+ false,
+ { listener: true }
+ );
+
const [trunkBranch, setTrunkBranch] = usePersistedState(
getTrunkBranchKey(projectPath),
"",
@@ -112,9 +121,11 @@ export function useDraftWorkspaceSettings(
runtimeMode,
sshHost,
trunkBranch,
+ fetchLatest,
},
setRuntimeOptions,
setTrunkBranch,
+ setFetchLatest,
getRuntimeString,
};
}
diff --git a/src/browser/utils/chatCommands.ts b/src/browser/utils/chatCommands.ts
index 39f63800b..c9693311f 100644
--- a/src/browser/utils/chatCommands.ts
+++ b/src/browser/utils/chatCommands.ts
@@ -71,6 +71,7 @@ export interface CreateWorkspaceOptions {
runtime?: string;
startMessage?: string;
sendMessageOptions?: SendMessageOptions;
+ fetchLatest?: boolean;
}
export interface CreateWorkspaceResult {
@@ -107,12 +108,15 @@ export async function createNewWorkspace(
// Parse runtime config if provided
const runtimeConfig = parseRuntimeString(effectiveRuntime, options.workspaceName);
+ const creationOptions =
+ options.fetchLatest !== undefined ? { fetchLatest: options.fetchLatest } : undefined;
const result = await window.api.workspace.create(
options.projectPath,
options.workspaceName,
effectiveTrunk,
- runtimeConfig
+ runtimeConfig,
+ creationOptions
);
if (!result.success) {
diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts
index 5a2b2f121..9f5de6233 100644
--- a/src/common/constants/storage.ts
+++ b/src/common/constants/storage.ts
@@ -108,6 +108,15 @@ export function getTrunkBranchKey(projectPath: string): string {
return `trunkBranch:${projectPath}`;
}
+/**
+ * Get the localStorage key for the fetch-latest preference for a project
+ * Stores the last fetch option state when creating a workspace
+ * Format: "fetchLatest:{projectPath}"
+ */
+export function getFetchLatestKey(projectPath: string): string {
+ return `fetchLatest:${projectPath}`;
+}
+
/**
* Get the localStorage key for the 1M context preference (global)
* Format: "use1MContext"
diff --git a/src/common/types/ipc.ts b/src/common/types/ipc.ts
index 07824b772..c64f43c6f 100644
--- a/src/common/types/ipc.ts
+++ b/src/common/types/ipc.ts
@@ -188,6 +188,7 @@ export interface SendMessageOptions {
maxOutputTokens?: number;
providerOptions?: MuxProviderOptions;
mode?: string; // Mode name - frontend narrows to specific values, backend accepts any string
+ fetchLatest?: boolean;
muxMetadata?: MuxFrontendMetadata; // Frontend-defined metadata, backend treats as black-box
}
@@ -233,7 +234,8 @@ export interface IPCApi {
projectPath: string,
branchName: string,
trunkBranch: string,
- runtimeConfig?: RuntimeConfig
+ runtimeConfig?: RuntimeConfig,
+ options?: { fetchLatest?: boolean }
): Promise<
{ success: true; metadata: FrontendWorkspaceMetadata } | { success: false; error: string }
>;
@@ -260,6 +262,7 @@ export interface IPCApi {
runtimeConfig?: RuntimeConfig;
projectPath?: string; // Required when workspaceId is null
trunkBranch?: string; // Optional - trunk branch to branch from (when workspaceId is null)
+ fetchLatest?: boolean; // Optional - fetch origin before branching (when workspaceId is null)
}
): Promise<
| Result
diff --git a/src/desktop/preload.ts b/src/desktop/preload.ts
index 61807a03f..941db1ae4 100644
--- a/src/desktop/preload.ts
+++ b/src/desktop/preload.ts
@@ -54,13 +54,14 @@ const api: IPCApi = {
},
workspace: {
list: () => ipcRenderer.invoke(IPC_CHANNELS.WORKSPACE_LIST),
- create: (projectPath, branchName, trunkBranch: string, runtimeConfig?) =>
+ create: (projectPath, branchName, trunkBranch: string, runtimeConfig?, options?) =>
ipcRenderer.invoke(
IPC_CHANNELS.WORKSPACE_CREATE,
projectPath,
branchName,
trunkBranch,
- runtimeConfig
+ runtimeConfig,
+ options
),
remove: (workspaceId: string, options?: { force?: boolean }) =>
ipcRenderer.invoke(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId, options),
diff --git a/src/node/runtime/LocalRuntime.ts b/src/node/runtime/LocalRuntime.ts
index 81012cd12..178cc4a43 100644
--- a/src/node/runtime/LocalRuntime.ts
+++ b/src/node/runtime/LocalRuntime.ts
@@ -318,7 +318,7 @@ export class LocalRuntime implements Runtime {
}
async createWorkspace(params: WorkspaceCreationParams): Promise {
- const { projectPath, branchName, trunkBranch, initLogger } = params;
+ const { projectPath, branchName, trunkBranch, initLogger, fetchLatest = false } = params;
try {
// Compute workspace path using the canonical method
@@ -344,6 +344,34 @@ export class LocalRuntime implements Runtime {
// Workspace doesn't exist, proceed with creation
}
+ if (fetchLatest) {
+ initLogger.logStep("Fetching latest from origin...");
+ try {
+ using proc = execAsync(`git -C "${projectPath}" fetch origin --prune`);
+ await proc.result;
+ initLogger.logStep("Origin fetch complete");
+ } catch (error) {
+ const errorMessage = getErrorMessage(error);
+ return {
+ success: false,
+ error: `Failed to fetch origin: ${errorMessage}`,
+ };
+ }
+ }
+
+ let baseRef = trunkBranch;
+ if (fetchLatest) {
+ try {
+ using proc = execAsync(
+ `git -C "${projectPath}" rev-parse --verify "origin/${trunkBranch}"`
+ );
+ await proc.result;
+ baseRef = `origin/${trunkBranch}`;
+ } catch {
+ // Remote branch missing - fall back to local trunk branch
+ }
+ }
+
// Check if branch exists locally
const localBranches = await listLocalBranches(projectPath);
const branchExists = localBranches.includes(branchName);
@@ -358,7 +386,7 @@ export class LocalRuntime implements Runtime {
} else {
// Branch doesn't exist, create it from trunk
using proc = execAsync(
- `git -C "${projectPath}" worktree add -b "${branchName}" "${workspacePath}" "${trunkBranch}"`
+ `git -C "${projectPath}" worktree add -b "${branchName}" "${workspacePath}" "${baseRef}"`
);
await proc.result;
}
diff --git a/src/node/runtime/Runtime.ts b/src/node/runtime/Runtime.ts
index 4e01a0ceb..243be69c5 100644
--- a/src/node/runtime/Runtime.ts
+++ b/src/node/runtime/Runtime.ts
@@ -121,6 +121,8 @@ export interface WorkspaceCreationParams {
directoryName: string;
/** Logger for streaming creation progress and init hook output */
initLogger: InitLogger;
+ /** Whether to fetch remote updates before branching */
+ fetchLatest?: boolean;
/** Optional abort signal for cancellation */
abortSignal?: AbortSignal;
}
@@ -149,6 +151,8 @@ export interface WorkspaceInitParams {
workspacePath: string;
/** Logger for streaming initialization progress and output */
initLogger: InitLogger;
+ /** Whether to fetch remote updates before branching */
+ fetchLatest?: boolean;
/** Optional abort signal for cancellation */
abortSignal?: AbortSignal;
}
diff --git a/src/node/runtime/SSHRuntime.ts b/src/node/runtime/SSHRuntime.ts
index 22588e873..841fe83d4 100644
--- a/src/node/runtime/SSHRuntime.ts
+++ b/src/node/runtime/SSHRuntime.ts
@@ -547,14 +547,79 @@ export class SSHRuntime implements Runtime {
private async syncProjectToRemote(
projectPath: string,
workspacePath: string,
+ trunkBranch: string,
initLogger: InitLogger,
- abortSignal?: AbortSignal
+ abortSignal?: AbortSignal,
+ fetchLatest = false
): Promise {
// Short-circuit if already aborted
if (abortSignal?.aborted) {
throw new Error("Sync operation aborted before starting");
}
+ if (fetchLatest) {
+ initLogger.logStep("Fetching latest from origin...");
+ try {
+ using proc = execAsync(`cd ${shescape.quote(projectPath)} && git fetch origin --prune`);
+ await proc.result;
+ initLogger.logStep("Origin fetch complete");
+
+ try {
+ using remoteRevProc = execAsync(
+ `git -C "${projectPath}" rev-parse --verify "refs/remotes/origin/${trunkBranch}"`
+ );
+ const { stdout: remoteStdout } = await remoteRevProc.result;
+ const remoteRef = remoteStdout.trim();
+
+ let localRef: string | null = null;
+ try {
+ using localRevProc = execAsync(
+ `git -C "${projectPath}" rev-parse --verify "refs/heads/${trunkBranch}"`
+ );
+ const { stdout: localStdout } = await localRevProc.result;
+ localRef = localStdout.trim();
+ } catch {
+ // Local branch missing - we'll create it from the remote ref below
+ }
+
+ let canUpdate = true;
+ if (localRef) {
+ if (localRef === remoteRef) {
+ canUpdate = false; // Already up to date
+ } else {
+ try {
+ using ffCheckProc = execAsync(
+ `git -C "${projectPath}" merge-base --is-ancestor ${localRef} ${remoteRef}`
+ );
+ await ffCheckProc.result;
+ } catch {
+ canUpdate = false;
+ initLogger.logStderr(
+ `Skipping fast-forward of ${trunkBranch}: local branch has diverged from origin/${trunkBranch}`
+ );
+ }
+ }
+ }
+
+ if (canUpdate) {
+ using updateRefProc = execAsync(
+ `git -C "${projectPath}" update-ref refs/heads/${trunkBranch} ${remoteRef}`
+ );
+ await updateRefProc.result;
+ initLogger.logStep(`Fast-forwarded ${trunkBranch} to origin/${trunkBranch}`);
+ }
+ } catch (error) {
+ initLogger.logStderr(
+ `Unable to fast-forward ${trunkBranch} to origin/${trunkBranch}: ${getErrorMessage(error)}`
+ );
+ }
+ } catch (error) {
+ const errorMsg = getErrorMessage(error);
+ initLogger.logStderr(`Failed to fetch origin: ${errorMsg}`);
+ throw new Error(`Failed to fetch origin: ${errorMsg}`);
+ }
+ }
+
// Use timestamp-based bundle path to avoid conflicts (simpler than $$)
const timestamp = Date.now();
const bundleTempPath = `~/.mux-bundle-${timestamp}.bundle`;
@@ -856,13 +921,28 @@ export class SSHRuntime implements Runtime {
}
async initWorkspace(params: WorkspaceInitParams): Promise {
- const { projectPath, branchName, trunkBranch, workspacePath, initLogger, abortSignal } = params;
+ const {
+ projectPath,
+ branchName,
+ trunkBranch,
+ workspacePath,
+ initLogger,
+ abortSignal,
+ fetchLatest = false,
+ } = params;
try {
// 1. Sync project to remote (opportunistic rsync with scp fallback)
initLogger.logStep("Syncing project files to remote...");
try {
- await this.syncProjectToRemote(projectPath, workspacePath, initLogger, abortSignal);
+ await this.syncProjectToRemote(
+ projectPath,
+ workspacePath,
+ trunkBranch,
+ initLogger,
+ abortSignal,
+ fetchLatest
+ );
} catch (error) {
const errorMsg = getErrorMessage(error);
initLogger.logStderr(`Failed to sync project: ${errorMsg}`);
diff --git a/src/node/services/ipcMain.ts b/src/node/services/ipcMain.ts
index 7e711a82a..3225f0c99 100644
--- a/src/node/services/ipcMain.ts
+++ b/src/node/services/ipcMain.ts
@@ -219,6 +219,7 @@ export class IpcMain {
trunkBranch: recommendedTrunk,
directoryName: branchName,
initLogger,
+ fetchLatest: options.fetchLatest ?? false,
});
if (!createResult.success || !createResult.workspacePath) {
@@ -267,6 +268,7 @@ export class IpcMain {
trunkBranch: recommendedTrunk,
workspacePath: createResult.workspacePath,
initLogger,
+ fetchLatest: options.fetchLatest ?? false,
})
.catch((error: unknown) => {
const errorMsg = error instanceof Error ? error.message : String(error);
@@ -435,7 +437,8 @@ export class IpcMain {
projectPath: string,
branchName: string,
trunkBranch: string,
- runtimeConfig?: RuntimeConfig
+ runtimeConfig?: RuntimeConfig,
+ options?: { fetchLatest?: boolean }
) => {
// Validate workspace name
const validation = validateWorkspaceName(branchName);
@@ -499,6 +502,7 @@ export class IpcMain {
trunkBranch: normalizedTrunkBranch,
directoryName: branchName, // Use branch name as directory name
initLogger,
+ fetchLatest: options?.fetchLatest ?? false,
});
if (!createResult.success || !createResult.workspacePath) {
@@ -560,6 +564,7 @@ export class IpcMain {
trunkBranch: normalizedTrunkBranch,
workspacePath: createResult.workspacePath,
initLogger,
+ fetchLatest: options?.fetchLatest ?? false,
})
.catch((error: unknown) => {
const errorMsg = error instanceof Error ? error.message : String(error);
diff --git a/tests/ipcMain/createWorkspace.test.ts b/tests/ipcMain/createWorkspace.test.ts
index 9d20e3d72..c90390bfe 100644
--- a/tests/ipcMain/createWorkspace.test.ts
+++ b/tests/ipcMain/createWorkspace.test.ts
@@ -1,3 +1,4 @@
+import * as os from "os";
/**
* Integration tests for WORKSPACE_CREATE IPC handler
*
@@ -128,7 +129,8 @@ async function createWorkspaceWithCleanup(
projectPath: string,
branchName: string,
trunkBranch: string,
- runtimeConfig?: RuntimeConfig
+ runtimeConfig?: RuntimeConfig,
+ options?: { fetchLatest?: boolean }
): Promise<{
result:
| { success: true; metadata: FrontendWorkspaceMetadata }
@@ -140,7 +142,8 @@ async function createWorkspaceWithCleanup(
projectPath,
branchName,
trunkBranch,
- runtimeConfig
+ runtimeConfig,
+ options
);
const cleanup = async () => {
@@ -362,6 +365,126 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => {
},
TEST_TIMEOUT_MS
);
+
+ test.concurrent(
+ "bases new workspace branch on origin head when fetchLatest is enabled",
+ async () => {
+ const env = await createTestEnvironment();
+ const tempGitRepo = await createTempGitRepo();
+ const remoteBareDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-remote-bare-"));
+ const remoteCloneBase = await fs.mkdtemp(path.join(os.tmpdir(), "mux-remote-clone-"));
+ const remoteCloneDir = path.join(remoteCloneBase, "clone");
+
+ let cleanupBaseline: (() => Promise) | undefined;
+ let cleanupFetch: (() => Promise) | undefined;
+
+ try {
+ await execAsync("git init --bare .", { cwd: remoteBareDir });
+
+ const trunkBranch = await detectDefaultTrunkBranch(tempGitRepo);
+ await execAsync(`git remote add origin "${remoteBareDir}"`, { cwd: tempGitRepo });
+ await execAsync(`git push -u origin ${trunkBranch}`, { cwd: tempGitRepo });
+
+ await execAsync(`git clone "${remoteBareDir}" "${remoteCloneDir}"`);
+ await execAsync(`git config user.email "test@example.com"`, { cwd: remoteCloneDir });
+ await execAsync(`git config user.name "Test User"`, { cwd: remoteCloneDir });
+ await execAsync(`echo "remote" >> REMOTE.md`, { cwd: remoteCloneDir });
+ await execAsync(`git add REMOTE.md`, { cwd: remoteCloneDir });
+ await execAsync(`git commit -m "Upstream commit"`, { cwd: remoteCloneDir });
+ await execAsync(`git push origin ${trunkBranch}`, { cwd: remoteCloneDir });
+
+ const remoteHead = (
+ await execAsync(`git rev-parse HEAD`, { cwd: remoteCloneDir })
+ ).stdout.trim();
+ const localHeadBefore = (
+ await execAsync(`git rev-parse ${trunkBranch}`, { cwd: tempGitRepo })
+ ).stdout.trim();
+ expect(remoteHead).not.toBe(localHeadBefore);
+
+ const baselineBranchName = generateBranchName("no-fetch");
+ const baselineRuntimeConfig = getRuntimeConfig(baselineBranchName);
+ const baseline = await createWorkspaceWithCleanup(
+ env,
+ tempGitRepo,
+ baselineBranchName,
+ trunkBranch,
+ baselineRuntimeConfig,
+ { fetchLatest: false }
+ );
+ cleanupBaseline = baseline.cleanup;
+ expect(baseline.result.success).toBe(true);
+ if (!baseline.result.success) {
+ throw new Error(
+ `Baseline workspace creation failed: ${baseline.result.error ?? "unknown error"}`
+ );
+ }
+
+ if (type === "ssh") {
+ await new Promise((resolve) => setTimeout(resolve, getInitWaitTime()));
+ }
+
+ const baselineHeadResult = await env.mockIpcRenderer.invoke(
+ IPC_CHANNELS.WORKSPACE_EXECUTE_BASH,
+ baseline.result.metadata.id,
+ "git rev-parse HEAD"
+ );
+ expect(baselineHeadResult.success).toBe(true);
+ expect(baselineHeadResult.data.success).toBe(true);
+ const baselineHead = baselineHeadResult.data.output.trim();
+ expect(baselineHead).toBe(localHeadBefore);
+
+ await cleanupBaseline();
+ cleanupBaseline = undefined;
+
+ const fetchBranchName = generateBranchName("fetch-latest");
+ const fetchRuntimeConfig = getRuntimeConfig(fetchBranchName);
+ const fetchWorkspace = await createWorkspaceWithCleanup(
+ env,
+ tempGitRepo,
+ fetchBranchName,
+ trunkBranch,
+ fetchRuntimeConfig,
+ { fetchLatest: true }
+ );
+ cleanupFetch = fetchWorkspace.cleanup;
+ expect(fetchWorkspace.result.success).toBe(true);
+ if (!fetchWorkspace.result.success) {
+ throw new Error(
+ `Fetch-enabled workspace creation failed: ${fetchWorkspace.result.error ?? "unknown error"}`
+ );
+ }
+
+ if (type === "ssh") {
+ await new Promise((resolve) => setTimeout(resolve, getInitWaitTime()));
+ }
+
+ const fetchHeadResult = await env.mockIpcRenderer.invoke(
+ IPC_CHANNELS.WORKSPACE_EXECUTE_BASH,
+ fetchWorkspace.result.metadata.id,
+ "git rev-parse HEAD"
+ );
+ expect(fetchHeadResult.success).toBe(true);
+ expect(fetchHeadResult.data.success).toBe(true);
+ const fetchHead = fetchHeadResult.data.output.trim();
+ expect(fetchHead).toBe(remoteHead);
+
+ await cleanupFetch();
+ cleanupFetch = undefined;
+ } finally {
+ if (cleanupBaseline) {
+ await cleanupBaseline();
+ }
+ if (cleanupFetch) {
+ await cleanupFetch();
+ }
+ await cleanupTestEnvironment(env);
+ await cleanupTempGitRepo(tempGitRepo);
+ await cleanupTempGitRepo(remoteCloneBase);
+ await cleanupTempGitRepo(remoteBareDir);
+ }
+ },
+ TEST_TIMEOUT_MS
+ );
});
describe("Init hook execution", () => {
@@ -370,7 +493,6 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => {
async () => {
const env = await createTestEnvironment();
const tempGitRepo = await createTempGitRepo();
-
try {
// Create and commit init hook
await createInitHook(
diff --git a/tests/ipcMain/helpers.ts b/tests/ipcMain/helpers.ts
index 71a38169a..eea6319d6 100644
--- a/tests/ipcMain/helpers.ts
+++ b/tests/ipcMain/helpers.ts
@@ -94,7 +94,8 @@ export async function createWorkspace(
projectPath: string,
branchName: string,
trunkBranch?: string,
- runtimeConfig?: import("../../src/common/types/runtime").RuntimeConfig
+ runtimeConfig?: import("../../src/common/types/runtime").RuntimeConfig,
+ options?: { fetchLatest?: boolean }
): Promise<
{ success: true; metadata: FrontendWorkspaceMetadata } | { success: false; error: string }
> {
@@ -108,7 +109,8 @@ export async function createWorkspace(
projectPath,
branchName,
resolvedTrunk,
- runtimeConfig
+ runtimeConfig,
+ options
)) as { success: true; metadata: FrontendWorkspaceMetadata } | { success: false; error: string };
}
@@ -146,7 +148,8 @@ export async function createWorkspaceWithInit(
branchName: string,
runtimeConfig?: RuntimeConfig,
waitForInit: boolean = false,
- isSSH: boolean = false
+ isSSH: boolean = false,
+ options?: { fetchLatest?: boolean }
): Promise<{ workspaceId: string; workspacePath: string; cleanup: () => Promise }> {
const trunkBranch = await detectDefaultTrunkBranch(projectPath);
@@ -155,7 +158,8 @@ export async function createWorkspaceWithInit(
projectPath,
branchName,
trunkBranch,
- runtimeConfig
+ runtimeConfig,
+ options
);
if (!result.success) {