From b705d7ddb5eb5c48ca048db677293a53d3ca36b0 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:08:55 +0000 Subject: [PATCH 01/10] Add TDD test for trunk branch bug in createWorkspace This test demonstrates that the trunk branch parameter is not being respected when creating a new workspace. The test: 1. Creates a custom trunk branch with unique content 2. Creates another branch with different content 3. Attempts to create a workspace specifying the custom trunk 4. Verifies the workspace contains content from custom trunk Expected behavior: workspace should be created from specified trunk Current behavior (bug): SSH runtime creates from HEAD instead of trunk This test should: - PASS for LocalRuntime (correctly uses trunk branch) - FAIL for SSHRuntime (ignores trunk branch, uses HEAD) The bug is in src/runtime/SSHRuntime.ts:592 where trunkBranch is renamed to _trunkBranch (unused) and line 618 creates branches from HEAD instead. --- tests/ipcMain/createWorkspace.test.ts | 132 ++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/tests/ipcMain/createWorkspace.test.ts b/tests/ipcMain/createWorkspace.test.ts index db6fc25a9f..86234a539e 100644 --- a/tests/ipcMain/createWorkspace.test.ts +++ b/tests/ipcMain/createWorkspace.test.ts @@ -273,6 +273,138 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => { }, TEST_TIMEOUT_MS ); + + test.concurrent( + "creates new branch from specified trunk branch, not from default branch", + async () => { + const env = await createTestEnvironment(); + const tempGitRepo = await createTempGitRepo(); + + try { + // Create a custom trunk branch with a unique commit + const customTrunkBranch = "custom-trunk"; + await execAsync( + `git checkout -b ${customTrunkBranch} && echo "custom-trunk-content" > trunk-file.txt && git add . && git commit -m "Custom trunk commit"`, + { cwd: tempGitRepo } + ); + + // Create a different branch (which will become the default if we checkout to it) + const otherBranch = "other-branch"; + await execAsync( + `git checkout -b ${otherBranch} && echo "other-content" > other-file.txt && git add . && git commit -m "Other branch commit"`, + { cwd: tempGitRepo } + ); + + // Switch back to the original default branch + const defaultBranch = await detectDefaultTrunkBranch(tempGitRepo); + await execAsync(`git checkout ${defaultBranch}`, { cwd: tempGitRepo }); + + // Now create a workspace specifying custom-trunk as the trunk branch + const newBranchName = generateBranchName("from-custom-trunk"); + const runtimeConfig = getRuntimeConfig(newBranchName); + + const { result, cleanup } = await createWorkspaceWithCleanup( + env, + tempGitRepo, + newBranchName, + customTrunkBranch, // Specify custom trunk branch + runtimeConfig + ); + + expect(result.success).toBe(true); + if (!result.success) { + throw new Error( + `Failed to create workspace from custom trunk '${customTrunkBranch}': ${result.error}` + ); + } + + // Wait for workspace initialization to complete + await new Promise((resolve) => setTimeout(resolve, getInitWaitTime())); + + // Verify the new branch was created from custom-trunk, not from default branch + const workspacePath = result.metadata.namedWorkspacePath; + + // For LocalRuntime, check the worktree directly + // For SSHRuntime, we need to check the remote workspace + if (type === "local") { + // Check that trunk-file.txt exists (from custom-trunk) + const trunkFileExists = await fs + .access(path.join(workspacePath, "trunk-file.txt")) + .then(() => true) + .catch(() => false); + expect(trunkFileExists).toBe(true); + + // Check that other-file.txt does NOT exist (from other-branch) + const otherFileExists = await fs + .access(path.join(workspacePath, "other-file.txt")) + .then(() => true) + .catch(() => false); + expect(otherFileExists).toBe(false); + + // Verify git log shows the custom trunk commit + const { stdout: logOutput } = await execAsync( + `git log --oneline --all`, + { cwd: workspacePath } + ); + expect(logOutput).toContain("Custom trunk commit"); + } else if (type === "ssh" && sshConfig) { + // For SSH runtime, check files on the remote host + const checkFileCmd = `test -f ${workspacePath}/trunk-file.txt && echo "exists" || echo "missing"`; + const checkFileStream = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.RUNTIME_EXEC, + result.metadata.id, + checkFileCmd, + { cwd: workspacePath, timeout: 10 } + ); + + // Read stdout to check if file exists + let checkOutput = ""; + const reader = checkFileStream.stdout.getReader(); + const decoder = new TextDecoder(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + checkOutput += decoder.decode(value, { stream: true }); + } + } finally { + reader.releaseLock(); + } + + expect(checkOutput.trim()).toBe("exists"); + + // Check that other-file.txt does NOT exist + const checkOtherFileCmd = `test -f ${workspacePath}/other-file.txt && echo "exists" || echo "missing"`; + const checkOtherStream = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.RUNTIME_EXEC, + result.metadata.id, + checkOtherFileCmd, + { cwd: workspacePath, timeout: 10 } + ); + + let checkOtherOutput = ""; + const otherReader = checkOtherStream.stdout.getReader(); + try { + while (true) { + const { done, value } = await otherReader.read(); + if (done) break; + checkOtherOutput += decoder.decode(value, { stream: true }); + } + } finally { + otherReader.releaseLock(); + } + + expect(checkOtherOutput.trim()).toBe("missing"); + } + + await cleanup(); + } finally { + await cleanupTestEnvironment(env); + await cleanupTempGitRepo(tempGitRepo); + } + }, + TEST_TIMEOUT_MS + ); }); describe("Init hook execution", () => { From aedcbec9cfe6314bed8a08062bd6f5b1cd5105b9 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:09:19 +0000 Subject: [PATCH 02/10] Add documentation for running trunk branch bug test --- tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md | 79 ++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md diff --git a/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md b/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md new file mode 100644 index 0000000000..78bbbd3516 --- /dev/null +++ b/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md @@ -0,0 +1,79 @@ +# Running the Trunk Branch Bug Test + +This document describes how to run the test that demonstrates the trunk branch bug in `createWorkspace`. + +## Bug Description + +When creating a new workspace, the `trunkBranch` parameter should specify which branch to create the new branch from. However, in SSHRuntime, this parameter is being ignored and new branches are created from HEAD instead. + +**Location of bug:** `src/runtime/SSHRuntime.ts:592` and `618` +- Line 592: `trunkBranch` is destructured as `_trunkBranch` (underscore indicates unused) +- Line 618: Branch is created from `HEAD` instead of using `trunkBranch` + +## Test Location + +The test is located in `tests/ipcMain/createWorkspace.test.ts` in the "Branch handling" describe block within the runtime matrix. + +Test name: `"creates new branch from specified trunk branch, not from default branch"` + +## Running the Test + +### Prerequisites + +1. Set the `TEST_INTEGRATION` environment variable to enable integration tests: + ```bash + export TEST_INTEGRATION=1 + ``` + +2. Ensure Docker is installed and running (required for SSH runtime tests) + +### Run the specific test + +```bash +# Run just this test for both runtimes +./node_modules/.bin/jest tests/ipcMain/createWorkspace.test.ts -t "creates new branch from specified trunk branch" +``` + +Or using make: +```bash +TEST_INTEGRATION=1 make test +``` + +## Expected Results + +### LocalRuntime (PASS ✓) +The test should **PASS** for LocalRuntime because it correctly uses the `trunkBranch` parameter when creating a new branch via `git worktree add -b`. + +### SSHRuntime (FAIL ✗) +The test should **FAIL** for SSHRuntime because it ignores the `trunkBranch` parameter and creates branches from `HEAD` instead. + +The failure will manifest as: +- `trunk-file.txt` will NOT exist (it should exist if branch was created from custom-trunk) +- The test assertion `expect(checkOutput.trim()).toBe("exists")` will fail + +## Test Scenario + +The test creates the following git structure: + +``` +main (initial) ← custom-trunk (+ trunk-file.txt) + ↑ + Should branch from here + +main (initial) ← other-branch (+ other-file.txt) + ↑ + HEAD might be here (bug) +``` + +When creating a workspace with `trunkBranch: "custom-trunk"`: +- **Expected:** New branch contains `trunk-file.txt` (from custom-trunk) +- **Actual (bug):** New branch might be from HEAD/default, missing `trunk-file.txt` + +## Test Coverage + +This test is part of the runtime matrix and runs for both: +- `{ type: "local" }` - LocalRuntime +- `{ type: "ssh" }` - SSHRuntime + +This ensures parity between runtime implementations. + From 344f7ae105161eaab45b17b3f2af637304fe47f4 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:12:03 +0000 Subject: [PATCH 03/10] Simplify trunk branch test to avoid runtime branching - Add readStream() helper to read from ReadableStream - Use RUNTIME_EXEC IPC for both local and SSH runtimes - Remove conditional logic based on runtime type - Reduces test code from ~80 lines to ~35 lines - Same verification logic now runs for both runtimes This makes the test cleaner and easier to maintain while ensuring parity between runtime implementations. --- tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md | 9 ++ tests/ipcMain/createWorkspace.test.ts | 124 ++++++++++--------------- 2 files changed, 59 insertions(+), 74 deletions(-) diff --git a/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md b/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md index 78bbbd3516..618b8d3499 100644 --- a/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md +++ b/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md @@ -77,3 +77,12 @@ This test is part of the runtime matrix and runs for both: This ensures parity between runtime implementations. +## Implementation Details + +The test uses a unified approach for both runtimes: +- Uses `RUNTIME_EXEC` IPC channel to execute shell commands (works for both local and SSH) +- Avoids runtime-specific branching logic +- Uses helper function `readStream()` to read command output from ReadableStream + +This approach simplifies the test and ensures the same verification logic runs for both runtimes. + diff --git a/tests/ipcMain/createWorkspace.test.ts b/tests/ipcMain/createWorkspace.test.ts index 86234a539e..e4b9f4e24e 100644 --- a/tests/ipcMain/createWorkspace.test.ts +++ b/tests/ipcMain/createWorkspace.test.ts @@ -119,6 +119,25 @@ async function commitChanges(repoPath: string, message: string): Promise { }); } +/** + * Read a ReadableStream to a string + */ +async function readStream(stream: ReadableStream): Promise { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let result = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + result += decoder.decode(value, { stream: true }); + } + } finally { + reader.releaseLock(); + } + return result; +} + /** * Create workspace and handle cleanup on test failure * Returns result and cleanup function @@ -322,80 +341,37 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => { await new Promise((resolve) => setTimeout(resolve, getInitWaitTime())); // Verify the new branch was created from custom-trunk, not from default branch - const workspacePath = result.metadata.namedWorkspacePath; - - // For LocalRuntime, check the worktree directly - // For SSHRuntime, we need to check the remote workspace - if (type === "local") { - // Check that trunk-file.txt exists (from custom-trunk) - const trunkFileExists = await fs - .access(path.join(workspacePath, "trunk-file.txt")) - .then(() => true) - .catch(() => false); - expect(trunkFileExists).toBe(true); - - // Check that other-file.txt does NOT exist (from other-branch) - const otherFileExists = await fs - .access(path.join(workspacePath, "other-file.txt")) - .then(() => true) - .catch(() => false); - expect(otherFileExists).toBe(false); - - // Verify git log shows the custom trunk commit - const { stdout: logOutput } = await execAsync( - `git log --oneline --all`, - { cwd: workspacePath } - ); - expect(logOutput).toContain("Custom trunk commit"); - } else if (type === "ssh" && sshConfig) { - // For SSH runtime, check files on the remote host - const checkFileCmd = `test -f ${workspacePath}/trunk-file.txt && echo "exists" || echo "missing"`; - const checkFileStream = await env.mockIpcRenderer.invoke( - IPC_CHANNELS.RUNTIME_EXEC, - result.metadata.id, - checkFileCmd, - { cwd: workspacePath, timeout: 10 } - ); - - // Read stdout to check if file exists - let checkOutput = ""; - const reader = checkFileStream.stdout.getReader(); - const decoder = new TextDecoder(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - checkOutput += decoder.decode(value, { stream: true }); - } - } finally { - reader.releaseLock(); - } - - expect(checkOutput.trim()).toBe("exists"); - - // Check that other-file.txt does NOT exist - const checkOtherFileCmd = `test -f ${workspacePath}/other-file.txt && echo "exists" || echo "missing"`; - const checkOtherStream = await env.mockIpcRenderer.invoke( - IPC_CHANNELS.RUNTIME_EXEC, - result.metadata.id, - checkOtherFileCmd, - { cwd: workspacePath, timeout: 10 } - ); - - let checkOtherOutput = ""; - const otherReader = checkOtherStream.stdout.getReader(); - try { - while (true) { - const { done, value } = await otherReader.read(); - if (done) break; - checkOtherOutput += decoder.decode(value, { stream: true }); - } - } finally { - otherReader.releaseLock(); - } - - expect(checkOtherOutput.trim()).toBe("missing"); - } + // Use RUNTIME_EXEC to check files (works for both local and SSH runtimes) + + // Check that trunk-file.txt exists (from custom-trunk) + const checkTrunkFileStream = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.RUNTIME_EXEC, + result.metadata.id, + `test -f trunk-file.txt && echo "exists" || echo "missing"`, + { timeout: 10 } + ); + const trunkFileOutput = await readStream(checkTrunkFileStream.stdout); + expect(trunkFileOutput.trim()).toBe("exists"); + + // Check that other-file.txt does NOT exist (from other-branch) + const checkOtherFileStream = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.RUNTIME_EXEC, + result.metadata.id, + `test -f other-file.txt && echo "exists" || echo "missing"`, + { timeout: 10 } + ); + const otherFileOutput = await readStream(checkOtherFileStream.stdout); + expect(otherFileOutput.trim()).toBe("missing"); + + // Verify git log shows the custom trunk commit + const gitLogStream = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.RUNTIME_EXEC, + result.metadata.id, + `git log --oneline --all`, + { timeout: 10 } + ); + const logOutput = await readStream(gitLogStream.stdout); + expect(logOutput).toContain("Custom trunk commit"); await cleanup(); } finally { From c8c5cecce5057f6115aff45bb75c04741ab5a890 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:41:08 +0000 Subject: [PATCH 04/10] Fix test to use WORKSPACE_EXECUTE_BASH and add File polyfill - Changed from non-existent RUNTIME_EXEC to WORKSPACE_EXECUTE_BASH - Fixed result structure to access .result.stdout - Added File polyfill to tests/setup.ts for Node 18 + undici compatibility - Remove stray documentation file Test now compiles correctly. Actual execution requires Docker for SSH tests. --- tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md | 88 -------------------------- tests/ipcMain/createWorkspace.test.ts | 29 +++++---- tests/setup.ts | 13 ++++ 3 files changed, 29 insertions(+), 101 deletions(-) delete mode 100644 tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md diff --git a/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md b/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md deleted file mode 100644 index 618b8d3499..0000000000 --- a/tests/ipcMain/RUN_TRUNK_BRANCH_TEST.md +++ /dev/null @@ -1,88 +0,0 @@ -# Running the Trunk Branch Bug Test - -This document describes how to run the test that demonstrates the trunk branch bug in `createWorkspace`. - -## Bug Description - -When creating a new workspace, the `trunkBranch` parameter should specify which branch to create the new branch from. However, in SSHRuntime, this parameter is being ignored and new branches are created from HEAD instead. - -**Location of bug:** `src/runtime/SSHRuntime.ts:592` and `618` -- Line 592: `trunkBranch` is destructured as `_trunkBranch` (underscore indicates unused) -- Line 618: Branch is created from `HEAD` instead of using `trunkBranch` - -## Test Location - -The test is located in `tests/ipcMain/createWorkspace.test.ts` in the "Branch handling" describe block within the runtime matrix. - -Test name: `"creates new branch from specified trunk branch, not from default branch"` - -## Running the Test - -### Prerequisites - -1. Set the `TEST_INTEGRATION` environment variable to enable integration tests: - ```bash - export TEST_INTEGRATION=1 - ``` - -2. Ensure Docker is installed and running (required for SSH runtime tests) - -### Run the specific test - -```bash -# Run just this test for both runtimes -./node_modules/.bin/jest tests/ipcMain/createWorkspace.test.ts -t "creates new branch from specified trunk branch" -``` - -Or using make: -```bash -TEST_INTEGRATION=1 make test -``` - -## Expected Results - -### LocalRuntime (PASS ✓) -The test should **PASS** for LocalRuntime because it correctly uses the `trunkBranch` parameter when creating a new branch via `git worktree add -b`. - -### SSHRuntime (FAIL ✗) -The test should **FAIL** for SSHRuntime because it ignores the `trunkBranch` parameter and creates branches from `HEAD` instead. - -The failure will manifest as: -- `trunk-file.txt` will NOT exist (it should exist if branch was created from custom-trunk) -- The test assertion `expect(checkOutput.trim()).toBe("exists")` will fail - -## Test Scenario - -The test creates the following git structure: - -``` -main (initial) ← custom-trunk (+ trunk-file.txt) - ↑ - Should branch from here - -main (initial) ← other-branch (+ other-file.txt) - ↑ - HEAD might be here (bug) -``` - -When creating a workspace with `trunkBranch: "custom-trunk"`: -- **Expected:** New branch contains `trunk-file.txt` (from custom-trunk) -- **Actual (bug):** New branch might be from HEAD/default, missing `trunk-file.txt` - -## Test Coverage - -This test is part of the runtime matrix and runs for both: -- `{ type: "local" }` - LocalRuntime -- `{ type: "ssh" }` - SSHRuntime - -This ensures parity between runtime implementations. - -## Implementation Details - -The test uses a unified approach for both runtimes: -- Uses `RUNTIME_EXEC` IPC channel to execute shell commands (works for both local and SSH) -- Avoids runtime-specific branching logic -- Uses helper function `readStream()` to read command output from ReadableStream - -This approach simplifies the test and ensures the same verification logic runs for both runtimes. - diff --git a/tests/ipcMain/createWorkspace.test.ts b/tests/ipcMain/createWorkspace.test.ts index e4b9f4e24e..b23f8b28fb 100644 --- a/tests/ipcMain/createWorkspace.test.ts +++ b/tests/ipcMain/createWorkspace.test.ts @@ -341,36 +341,39 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => { await new Promise((resolve) => setTimeout(resolve, getInitWaitTime())); // Verify the new branch was created from custom-trunk, not from default branch - // Use RUNTIME_EXEC to check files (works for both local and SSH runtimes) + // Use WORKSPACE_EXECUTE_BASH to check files (works for both local and SSH runtimes) // Check that trunk-file.txt exists (from custom-trunk) - const checkTrunkFileStream = await env.mockIpcRenderer.invoke( - IPC_CHANNELS.RUNTIME_EXEC, + const checkTrunkFileResult = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, result.metadata.id, `test -f trunk-file.txt && echo "exists" || echo "missing"`, - { timeout: 10 } + { timeout_secs: 10 } ); - const trunkFileOutput = await readStream(checkTrunkFileStream.stdout); + expect(checkTrunkFileResult.success).toBe(true); + const trunkFileOutput = await readStream(checkTrunkFileResult.result.stdout); expect(trunkFileOutput.trim()).toBe("exists"); // Check that other-file.txt does NOT exist (from other-branch) - const checkOtherFileStream = await env.mockIpcRenderer.invoke( - IPC_CHANNELS.RUNTIME_EXEC, + const checkOtherFileResult = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, result.metadata.id, `test -f other-file.txt && echo "exists" || echo "missing"`, - { timeout: 10 } + { timeout_secs: 10 } ); - const otherFileOutput = await readStream(checkOtherFileStream.stdout); + expect(checkOtherFileResult.success).toBe(true); + const otherFileOutput = await readStream(checkOtherFileResult.result.stdout); expect(otherFileOutput.trim()).toBe("missing"); // Verify git log shows the custom trunk commit - const gitLogStream = await env.mockIpcRenderer.invoke( - IPC_CHANNELS.RUNTIME_EXEC, + const gitLogResult = await env.mockIpcRenderer.invoke( + IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, result.metadata.id, `git log --oneline --all`, - { timeout: 10 } + { timeout_secs: 10 } ); - const logOutput = await readStream(gitLogStream.stdout); + expect(gitLogResult.success).toBe(true); + const logOutput = await readStream(gitLogResult.result.stdout); expect(logOutput).toContain("Custom trunk commit"); await cleanup(); diff --git a/tests/setup.ts b/tests/setup.ts index daf20d3bf5..ad8144a9b2 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -9,3 +9,16 @@ require("disposablestack/auto"); assert.equal(typeof Symbol.dispose, "symbol"); assert.equal(typeof Symbol.asyncDispose, "symbol"); + +// Polyfill File for Node 18 (undici needs it) +if (typeof globalThis.File === "undefined") { + (globalThis as any).File = class File extends Blob { + constructor(bits: BlobPart[], name: string, options?: FilePropertyBag) { + super(bits, options); + this.name = name; + this.lastModified = options?.lastModified ?? Date.now(); + } + name: string; + lastModified: number; + }; +} From a43afa1c67252398726cfa247266c4fbf84c6a2b Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:46:54 +0000 Subject: [PATCH 05/10] Fix formatting: remove trailing whitespace --- tests/ipcMain/createWorkspace.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ipcMain/createWorkspace.test.ts b/tests/ipcMain/createWorkspace.test.ts index b23f8b28fb..86119f720a 100644 --- a/tests/ipcMain/createWorkspace.test.ts +++ b/tests/ipcMain/createWorkspace.test.ts @@ -342,7 +342,7 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => { // Verify the new branch was created from custom-trunk, not from default branch // Use WORKSPACE_EXECUTE_BASH to check files (works for both local and SSH runtimes) - + // Check that trunk-file.txt exists (from custom-trunk) const checkTrunkFileResult = await env.mockIpcRenderer.invoke( IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, From 7a78c152682dd5372d61a5af777670095bc29803 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 19:53:29 +0000 Subject: [PATCH 06/10] Fix WORKSPACE_EXECUTE_BASH API usage in test - Use .data.output instead of .result.stdout (which doesn't exist) - Add .data.success checks as per API structure - Remove unused readStream() helper function - Remove timeout_secs param (not needed for WORKSPACE_EXECUTE_BASH) This matches the actual API used in other tests (e.g., executeBash.test.ts) --- tests/ipcMain/createWorkspace.test.ts | 40 ++++++--------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/tests/ipcMain/createWorkspace.test.ts b/tests/ipcMain/createWorkspace.test.ts index 86119f720a..44fcbb07a1 100644 --- a/tests/ipcMain/createWorkspace.test.ts +++ b/tests/ipcMain/createWorkspace.test.ts @@ -119,25 +119,6 @@ async function commitChanges(repoPath: string, message: string): Promise { }); } -/** - * Read a ReadableStream to a string - */ -async function readStream(stream: ReadableStream): Promise { - const reader = stream.getReader(); - const decoder = new TextDecoder(); - let result = ""; - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - result += decoder.decode(value, { stream: true }); - } - } finally { - reader.releaseLock(); - } - return result; -} - /** * Create workspace and handle cleanup on test failure * Returns result and cleanup function @@ -347,34 +328,31 @@ describeIntegration("WORKSPACE_CREATE with both runtimes", () => { const checkTrunkFileResult = await env.mockIpcRenderer.invoke( IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, result.metadata.id, - `test -f trunk-file.txt && echo "exists" || echo "missing"`, - { timeout_secs: 10 } + `test -f trunk-file.txt && echo "exists" || echo "missing"` ); expect(checkTrunkFileResult.success).toBe(true); - const trunkFileOutput = await readStream(checkTrunkFileResult.result.stdout); - expect(trunkFileOutput.trim()).toBe("exists"); + expect(checkTrunkFileResult.data.success).toBe(true); + expect(checkTrunkFileResult.data.output.trim()).toBe("exists"); // Check that other-file.txt does NOT exist (from other-branch) const checkOtherFileResult = await env.mockIpcRenderer.invoke( IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, result.metadata.id, - `test -f other-file.txt && echo "exists" || echo "missing"`, - { timeout_secs: 10 } + `test -f other-file.txt && echo "exists" || echo "missing"` ); expect(checkOtherFileResult.success).toBe(true); - const otherFileOutput = await readStream(checkOtherFileResult.result.stdout); - expect(otherFileOutput.trim()).toBe("missing"); + expect(checkOtherFileResult.data.success).toBe(true); + expect(checkOtherFileResult.data.output.trim()).toBe("missing"); // Verify git log shows the custom trunk commit const gitLogResult = await env.mockIpcRenderer.invoke( IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, result.metadata.id, - `git log --oneline --all`, - { timeout_secs: 10 } + `git log --oneline --all` ); expect(gitLogResult.success).toBe(true); - const logOutput = await readStream(gitLogResult.result.stdout); - expect(logOutput).toContain("Custom trunk commit"); + expect(gitLogResult.data.success).toBe(true); + expect(gitLogResult.data.output).toContain("Custom trunk commit"); await cleanup(); } finally { From 94d934a1866566f8b7a1663a0d1ebc84e938c62b Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:11:27 +0000 Subject: [PATCH 07/10] Fix: Use trunkBranch parameter in SSHRuntime instead of HEAD Previously, SSHRuntime ignored the trunkBranch parameter and always created new branches from HEAD. This caused new branches to be created from whatever HEAD was pointing to, rather than the specified trunk. Changes: - Use trunkBranch parameter instead of _trunkBranch (unused variable) - Pass trunkBranch to 'git checkout -b' instead of hardcoded 'HEAD' - Update comment to reflect correct behavior This brings SSHRuntime in line with LocalRuntime, which correctly uses the trunkBranch parameter when creating new branches via 'git worktree add -b'. Fixes the integration test: 'creates new branch from specified trunk branch, not from default branch' --- src/runtime/SSHRuntime.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index d4be4d4330..68a1056d92 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -669,13 +669,7 @@ export class SSHRuntime implements Runtime { } async initWorkspace(params: WorkspaceInitParams): Promise { - const { - projectPath, - branchName, - trunkBranch: _trunkBranch, - workspacePath, - initLogger, - } = params; + const { projectPath, branchName, trunkBranch, workspacePath, initLogger } = params; try { // 1. Sync project to remote (opportunistic rsync with scp fallback) @@ -694,11 +688,9 @@ export class SSHRuntime implements Runtime { initLogger.logStep("Files synced successfully"); // 2. Checkout branch remotely - // Note: After git clone, HEAD is already checked out to the default branch from the bundle - // We create new branches from HEAD instead of the trunkBranch name to avoid issues - // where the local repo's trunk name doesn't match the cloned repo's default branch + // If branch exists, check it out; otherwise create it from the specified trunk branch initLogger.logStep(`Checking out branch: ${branchName}`); - const checkoutCmd = `(git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} HEAD)`; + const checkoutCmd = `(git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} ${shescape.quote(trunkBranch)})`; const checkoutStream = await this.exec(checkoutCmd, { cwd: workspacePath, // Use the full workspace path for git operations From fd98c61aca28c4902fbf5c6fcf9c7811f5e911ab Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:33:19 +0000 Subject: [PATCH 08/10] Fix SSH trunk branch: use origin/ prefix for remote tracking branches After git clone from bundle, branches exist as remote tracking branches (origin/*), not as local branches. When creating a new branch from trunk, we need to reference it as 'origin/trunkBranch' to properly resolve the ref. Updated checkout command to try: 1. Checkout branch if it exists locally 2. Create from origin/trunkBranch (remote tracking branch) 3. Fallback to trunkBranch (local branch, shouldn't be needed) This ensures the trunk branch ref is properly resolved after git clone. --- src/runtime/SSHRuntime.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index 68a1056d92..c508578c44 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -688,9 +688,11 @@ export class SSHRuntime implements Runtime { initLogger.logStep("Files synced successfully"); // 2. Checkout branch remotely - // If branch exists, check it out; otherwise create it from the specified trunk branch + // If branch exists locally, check it out; otherwise create it from the specified trunk branch + // Note: After git clone from bundle, branches exist as origin/* refs, so we need to check both + // local branch and origin/branch when creating the new branch initLogger.logStep(`Checking out branch: ${branchName}`); - const checkoutCmd = `(git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} ${shescape.quote(trunkBranch)})`; + const checkoutCmd = `(git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} origin/${shescape.quote(trunkBranch)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} ${shescape.quote(trunkBranch)})`; const checkoutStream = await this.exec(checkoutCmd, { cwd: workspacePath, // Use the full workspace path for git operations From 61e53d43704ad48a2bc80b439ac6d9c8ff2e5b7e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:56:31 +0000 Subject: [PATCH 09/10] Fix SSHRuntime trunk branch handling - Create local tracking branches for all remote refs after clone - This ensures branch names work directly without needing 'origin/' prefix - Simplify checkout command to use branch names directly - Add debug logging to help diagnose issues The issue was that after git clone from bundle, all branches exist as origin/* remote refs. When we later remove the origin remote, these refs disappear. By creating local tracking branches first, we ensure the branch names are available for checkout operations, fixing the trunk branch parameter handling. --- src/runtime/SSHRuntime.ts | 48 +++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index c508578c44..f0145375cb 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -480,7 +480,21 @@ export class SSHRuntime implements Runtime { throw new Error(`Failed to clone repository: ${cloneStderr || cloneStdout}`); } - // Step 4: Update origin remote if we have an origin URL + // Step 4: Create local tracking branches for all remote branches + // This ensures that branch names like "custom-trunk" can be used directly + // in git checkout commands, rather than needing "origin/custom-trunk" + initLogger.logStep(`Creating local tracking branches...`); + const createTrackingBranchesStream = await this.exec( + `cd ${cloneDestPath} && for branch in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin/ | grep -v 'origin/HEAD'); do localname=\${branch#origin/}; git show-ref --verify --quiet refs/heads/$localname || git branch $localname $branch; done`, + { + cwd: "~", + timeout: 30, + } + ); + await createTrackingBranchesStream.exitCode; + // Don't fail if this fails - some branches may already exist + + // Step 5: Update origin remote if we have an origin URL if (originUrl) { initLogger.logStep(`Setting origin remote to ${originUrl}...`); const setOriginStream = await this.exec( @@ -689,10 +703,24 @@ export class SSHRuntime implements Runtime { // 2. Checkout branch remotely // If branch exists locally, check it out; otherwise create it from the specified trunk branch - // Note: After git clone from bundle, branches exist as origin/* refs, so we need to check both - // local branch and origin/branch when creating the new branch + // Note: We've already created local branches for all remote refs in syncProjectToRemote initLogger.logStep(`Checking out branch: ${branchName}`); - const checkoutCmd = `(git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} origin/${shescape.quote(trunkBranch)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} ${shescape.quote(trunkBranch)})`; + + // DEBUG: Log git state after clone + initLogger.logStep(`[DEBUG] Inspecting git state after clone...`); + const debugStream = await this.exec( + `echo "=== Current branch ===" && git branch && echo "=== All branches ===" && git branch -a && echo "=== HEAD ===" && git rev-parse HEAD && echo "=== Files ===" && ls -la`, + { cwd: workspacePath, timeout: 30 } + ); + const [debugOut] = await Promise.all([ + streamToString(debugStream.stdout), + debugStream.exitCode, + ]); + initLogger.logStdout(debugOut); + + // Try to checkout existing branch, or create new branch from trunk + // Since we've created local branches for all remote refs, we can use branch names directly + const checkoutCmd = `git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} ${shescape.quote(trunkBranch)}`; const checkoutStream = await this.exec(checkoutCmd, { cwd: workspacePath, // Use the full workspace path for git operations @@ -716,6 +744,18 @@ export class SSHRuntime implements Runtime { } initLogger.logStep("Branch checked out successfully"); + // DEBUG: Log git state after checkout + initLogger.logStep(`[DEBUG] Inspecting git state after checkout...`); + const debugStream2 = await this.exec( + `echo "=== Current branch ===" && git branch && echo "=== HEAD ===" && git rev-parse HEAD && echo "=== Files ===" && ls -la && echo "=== Last commits ===" && git log --oneline -5`, + { cwd: workspacePath, timeout: 30 } + ); + const [debugOut2] = await Promise.all([ + streamToString(debugStream2.stdout), + debugStream2.exitCode, + ]); + initLogger.logStdout(debugOut2); + // 3. Run .cmux/init hook if it exists // Note: runInitHook calls logComplete() internally if hook exists const hookExists = await checkInitHookExists(projectPath); From dea972b9a1e16e411d78652a55ecbcce1111b832 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 27 Oct 2025 01:54:29 +0000 Subject: [PATCH 10/10] Remove debug logging from SSHRuntime --- src/runtime/SSHRuntime.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index f0145375cb..da70e5604b 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -706,18 +706,6 @@ export class SSHRuntime implements Runtime { // Note: We've already created local branches for all remote refs in syncProjectToRemote initLogger.logStep(`Checking out branch: ${branchName}`); - // DEBUG: Log git state after clone - initLogger.logStep(`[DEBUG] Inspecting git state after clone...`); - const debugStream = await this.exec( - `echo "=== Current branch ===" && git branch && echo "=== All branches ===" && git branch -a && echo "=== HEAD ===" && git rev-parse HEAD && echo "=== Files ===" && ls -la`, - { cwd: workspacePath, timeout: 30 } - ); - const [debugOut] = await Promise.all([ - streamToString(debugStream.stdout), - debugStream.exitCode, - ]); - initLogger.logStdout(debugOut); - // Try to checkout existing branch, or create new branch from trunk // Since we've created local branches for all remote refs, we can use branch names directly const checkoutCmd = `git checkout ${shescape.quote(branchName)} 2>/dev/null || git checkout -b ${shescape.quote(branchName)} ${shescape.quote(trunkBranch)}`; @@ -744,18 +732,6 @@ export class SSHRuntime implements Runtime { } initLogger.logStep("Branch checked out successfully"); - // DEBUG: Log git state after checkout - initLogger.logStep(`[DEBUG] Inspecting git state after checkout...`); - const debugStream2 = await this.exec( - `echo "=== Current branch ===" && git branch && echo "=== HEAD ===" && git rev-parse HEAD && echo "=== Files ===" && ls -la && echo "=== Last commits ===" && git log --oneline -5`, - { cwd: workspacePath, timeout: 30 } - ); - const [debugOut2] = await Promise.all([ - streamToString(debugStream2.stdout), - debugStream2.exitCode, - ]); - initLogger.logStdout(debugOut2); - // 3. Run .cmux/init hook if it exists // Note: runInitHook calls logComplete() internally if hook exists const hookExists = await checkInitHookExists(projectPath);