Skip to content

Commit 1f750a5

Browse files
committed
test: add integration tests for squash-merge detection
Added two new SSH-specific tests: - 'should allow deletion of squash-merged branches without force flag' Simulates a squash-merge scenario where branch content matches main but commits differ, verifies deletion succeeds without force. - 'should block deletion when branch has genuinely unmerged content' Verifies branches with content not in main still require force.
1 parent 552d94c commit 1f750a5

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed

tests/ipcMain/removeWorkspace.test.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,5 +799,193 @@ describeIntegration("Workspace deletion integration tests", () => {
799799
},
800800
TEST_TIMEOUT_SSH_MS
801801
);
802+
803+
test.concurrent(
804+
"should allow deletion of squash-merged branches without force flag",
805+
async () => {
806+
const env = await createTestEnvironment();
807+
const tempGitRepo = await createTempGitRepo();
808+
809+
try {
810+
const branchName = generateBranchName("squash-merge-test");
811+
const runtimeConfig = getRuntimeConfig(branchName);
812+
const { workspaceId } = await createWorkspaceWithInit(
813+
env,
814+
tempGitRepo,
815+
branchName,
816+
runtimeConfig,
817+
true, // waitForInit
818+
true // isSSH
819+
);
820+
821+
// Configure git for committing
822+
await executeBash(env, workspaceId, 'git config user.email "test@example.com"');
823+
await executeBash(env, workspaceId, 'git config user.name "Test User"');
824+
825+
// Get the current workspace path (inside SSH container)
826+
const pwdResult = await executeBash(env, workspaceId, "pwd");
827+
const workspacePath = pwdResult.output.trim();
828+
829+
// Create a bare repo inside the SSH container to act as "origin"
830+
// This avoids issues with host paths not being accessible in container
831+
const originPath = `${workspacePath}/../.test-origin-${branchName}`;
832+
await executeBash(env, workspaceId, `git clone --bare . "${originPath}"`);
833+
834+
// Point origin to the bare repo
835+
await executeBash(env, workspaceId, `git remote set-url origin "${originPath}"`);
836+
837+
// Create feature commits on the branch
838+
await executeBash(env, workspaceId, 'echo "feature1" > feature.txt');
839+
await executeBash(env, workspaceId, "git add feature.txt");
840+
await executeBash(env, workspaceId, 'git commit -m "Feature commit 1"');
841+
842+
await executeBash(env, workspaceId, 'echo "feature2" >> feature.txt');
843+
await executeBash(env, workspaceId, "git add feature.txt");
844+
await executeBash(env, workspaceId, 'git commit -m "Feature commit 2"');
845+
846+
// Get the feature branch's final file content
847+
const featureContent = await executeBash(env, workspaceId, "cat feature.txt");
848+
849+
// Simulate squash-merge: create a temp worktree, add the squash commit to main, push
850+
// We need to work around bare repo limitations by using a temp checkout
851+
const tempCheckoutPath = `${workspacePath}/../.test-temp-checkout-${branchName}`;
852+
await executeBash(
853+
env,
854+
workspaceId,
855+
`git clone "${originPath}" "${tempCheckoutPath}" && ` +
856+
`cd "${tempCheckoutPath}" && ` +
857+
`git config user.email "test@example.com" && ` +
858+
`git config user.name "Test User" && ` +
859+
// Checkout main (or master, depending on git version)
860+
`(git checkout main 2>/dev/null || git checkout master) && ` +
861+
// Create squash commit with same content
862+
`printf '%s' '${featureContent.output.trim().replace(/'/g, "'\\''")}' > feature.txt && ` +
863+
`git add feature.txt && ` +
864+
`git commit -m "Squash: Feature commits" && ` +
865+
`git push origin HEAD`
866+
);
867+
868+
// Cleanup temp checkout
869+
await executeBash(env, workspaceId, `rm -rf "${tempCheckoutPath}"`);
870+
871+
// Fetch the updated origin in the workspace
872+
await executeBash(env, workspaceId, "git fetch origin");
873+
874+
// Verify we have unpushed commits (branch commits are not ancestors of origin/main)
875+
const logResult = await executeBash(
876+
env,
877+
workspaceId,
878+
"git log --branches --not --remotes --oneline"
879+
);
880+
// Should show commits since our branch commits != squash commit SHA
881+
expect(logResult.output.trim()).not.toBe("");
882+
883+
// Now attempt deletion without force - should succeed because content matches
884+
const deleteResult = await env.mockIpcRenderer.invoke(
885+
IPC_CHANNELS.WORKSPACE_REMOVE,
886+
workspaceId
887+
);
888+
889+
// Should succeed - squash-merge detection should recognize content is in main
890+
expect(deleteResult.success).toBe(true);
891+
892+
// Cleanup the bare repo we created
893+
// Note: This runs after workspace is deleted, may fail if path is gone
894+
try {
895+
using cleanupProc = execAsync(`rm -rf "${originPath}"`);
896+
await cleanupProc.result;
897+
} catch {
898+
// Ignore cleanup errors
899+
}
900+
901+
// Verify workspace was removed from config
902+
const config = env.config.loadConfigOrDefault();
903+
const project = config.projects.get(tempGitRepo);
904+
if (project) {
905+
const stillInConfig = project.workspaces.some((w) => w.id === workspaceId);
906+
expect(stillInConfig).toBe(false);
907+
}
908+
} finally {
909+
await cleanupTestEnvironment(env);
910+
await cleanupTempGitRepo(tempGitRepo);
911+
}
912+
},
913+
TEST_TIMEOUT_SSH_MS
914+
);
915+
916+
test.concurrent(
917+
"should block deletion when branch has genuinely unmerged content",
918+
async () => {
919+
const env = await createTestEnvironment();
920+
const tempGitRepo = await createTempGitRepo();
921+
922+
try {
923+
const branchName = generateBranchName("unmerged-content-test");
924+
const runtimeConfig = getRuntimeConfig(branchName);
925+
const { workspaceId } = await createWorkspaceWithInit(
926+
env,
927+
tempGitRepo,
928+
branchName,
929+
runtimeConfig,
930+
true, // waitForInit
931+
true // isSSH
932+
);
933+
934+
// Configure git for committing
935+
await executeBash(env, workspaceId, 'git config user.email "test@example.com"');
936+
await executeBash(env, workspaceId, 'git config user.name "Test User"');
937+
938+
// Get the current workspace path (inside SSH container)
939+
const pwdResult = await executeBash(env, workspaceId, "pwd");
940+
const workspacePath = pwdResult.output.trim();
941+
942+
// Create a bare repo inside the SSH container to act as "origin"
943+
const originPath = `${workspacePath}/../.test-origin-${branchName}`;
944+
await executeBash(env, workspaceId, `git clone --bare . "${originPath}"`);
945+
946+
// Point origin to the bare repo
947+
await executeBash(env, workspaceId, `git remote set-url origin "${originPath}"`);
948+
949+
// Create feature commits with unique content (not in origin)
950+
await executeBash(env, workspaceId, 'echo "unique-unmerged-content" > unique.txt');
951+
await executeBash(env, workspaceId, "git add unique.txt");
952+
await executeBash(env, workspaceId, 'git commit -m "Unique commit"');
953+
954+
// Fetch origin (main doesn't have our content - we didn't push)
955+
await executeBash(env, workspaceId, "git fetch origin");
956+
957+
// Attempt deletion without force - should fail because content differs
958+
const deleteResult = await env.mockIpcRenderer.invoke(
959+
IPC_CHANNELS.WORKSPACE_REMOVE,
960+
workspaceId
961+
);
962+
963+
// Should fail - genuinely unmerged content
964+
expect(deleteResult.success).toBe(false);
965+
expect(deleteResult.error).toMatch(/unpushed|changes/i);
966+
967+
// Verify workspace still exists
968+
const stillExists = await workspaceExists(env, workspaceId);
969+
expect(stillExists).toBe(true);
970+
971+
// Cleanup: force delete
972+
await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId, {
973+
force: true,
974+
});
975+
976+
// Cleanup the bare repo
977+
try {
978+
using cleanupProc = execAsync(`rm -rf "${originPath}"`);
979+
await cleanupProc.result;
980+
} catch {
981+
// Ignore cleanup errors
982+
}
983+
} finally {
984+
await cleanupTestEnvironment(env);
985+
await cleanupTempGitRepo(tempGitRepo);
986+
}
987+
},
988+
TEST_TIMEOUT_SSH_MS
989+
);
802990
});
803991
});

0 commit comments

Comments
 (0)