Skip to content

Commit eec89a5

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 eec89a5

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

tests/ipcMain/removeWorkspace.test.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,5 +799,227 @@ 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 (add if doesn't exist, set-url if it does)
835+
await executeBash(
836+
env,
837+
workspaceId,
838+
`git remote get-url origin >/dev/null 2>&1 && git remote set-url origin "${originPath}" || git remote add origin "${originPath}"`
839+
);
840+
841+
// Create feature commits on the branch
842+
await executeBash(env, workspaceId, 'echo "feature1" > feature.txt');
843+
await executeBash(env, workspaceId, "git add feature.txt");
844+
await executeBash(env, workspaceId, 'git commit -m "Feature commit 1"');
845+
846+
await executeBash(env, workspaceId, 'echo "feature2" >> feature.txt');
847+
await executeBash(env, workspaceId, "git add feature.txt");
848+
await executeBash(env, workspaceId, 'git commit -m "Feature commit 2"');
849+
850+
// Get the feature branch's final file content
851+
const featureContent = await executeBash(env, workspaceId, "cat feature.txt");
852+
853+
// Simulate squash-merge: create a temp worktree, add the squash commit to main, push
854+
// We need to work around bare repo limitations by using a temp checkout
855+
const tempCheckoutPath = `${workspacePath}/../.test-temp-checkout-${branchName}`;
856+
await executeBash(
857+
env,
858+
workspaceId,
859+
`git clone "${originPath}" "${tempCheckoutPath}" && ` +
860+
`cd "${tempCheckoutPath}" && ` +
861+
`git config user.email "test@example.com" && ` +
862+
`git config user.name "Test User" && ` +
863+
// Checkout main (or master, depending on git version)
864+
`(git checkout main 2>/dev/null || git checkout master) && ` +
865+
// Create squash commit with same content
866+
`printf '%s' '${featureContent.output.trim().replace(/'/g, "'\\''")}' > feature.txt && ` +
867+
`git add feature.txt && ` +
868+
`git commit -m "Squash: Feature commits" && ` +
869+
`git push origin HEAD`
870+
);
871+
872+
// Cleanup temp checkout
873+
await executeBash(env, workspaceId, `rm -rf "${tempCheckoutPath}"`);
874+
875+
// Fetch the updated origin in the workspace
876+
await executeBash(env, workspaceId, "git fetch origin");
877+
878+
// Verify we have unpushed commits (branch commits are not ancestors of origin/main)
879+
const logResult = await executeBash(
880+
env,
881+
workspaceId,
882+
"git log --branches --not --remotes --oneline"
883+
);
884+
// Should show commits since our branch commits != squash commit SHA
885+
expect(logResult.output.trim()).not.toBe("");
886+
887+
// Now attempt deletion without force - should succeed because content matches
888+
const deleteResult = await env.mockIpcRenderer.invoke(
889+
IPC_CHANNELS.WORKSPACE_REMOVE,
890+
workspaceId
891+
);
892+
893+
// Should succeed - squash-merge detection should recognize content is in main
894+
expect(deleteResult.success).toBe(true);
895+
896+
// Cleanup the bare repo we created
897+
// Note: This runs after workspace is deleted, may fail if path is gone
898+
try {
899+
using cleanupProc = execAsync(`rm -rf "${originPath}"`);
900+
await cleanupProc.result;
901+
} catch {
902+
// Ignore cleanup errors
903+
}
904+
905+
// Verify workspace was removed from config
906+
const config = env.config.loadConfigOrDefault();
907+
const project = config.projects.get(tempGitRepo);
908+
if (project) {
909+
const stillInConfig = project.workspaces.some((w) => w.id === workspaceId);
910+
expect(stillInConfig).toBe(false);
911+
}
912+
} finally {
913+
await cleanupTestEnvironment(env);
914+
await cleanupTempGitRepo(tempGitRepo);
915+
}
916+
},
917+
TEST_TIMEOUT_SSH_MS
918+
);
919+
920+
test.concurrent(
921+
"should block deletion when branch has genuinely unmerged content",
922+
async () => {
923+
const env = await createTestEnvironment();
924+
const tempGitRepo = await createTempGitRepo();
925+
926+
try {
927+
const branchName = generateBranchName("unmerged-content-test");
928+
const runtimeConfig = getRuntimeConfig(branchName);
929+
const { workspaceId } = await createWorkspaceWithInit(
930+
env,
931+
tempGitRepo,
932+
branchName,
933+
runtimeConfig,
934+
true, // waitForInit
935+
true // isSSH
936+
);
937+
938+
// Configure git for committing
939+
await executeBash(env, workspaceId, 'git config user.email "test@example.com"');
940+
await executeBash(env, workspaceId, 'git config user.name "Test User"');
941+
942+
// Get the current workspace path (inside SSH container)
943+
const pwdResult = await executeBash(env, workspaceId, "pwd");
944+
const workspacePath = pwdResult.output.trim();
945+
946+
// Create a bare repo inside the SSH container to act as "origin"
947+
const originPath = `${workspacePath}/../.test-origin-${branchName}`;
948+
await executeBash(env, workspaceId, `git clone --bare . "${originPath}"`);
949+
950+
// Point origin to the bare repo (add if doesn't exist, set-url if it does)
951+
await executeBash(
952+
env,
953+
workspaceId,
954+
`git remote get-url origin >/dev/null 2>&1 && git remote set-url origin "${originPath}" || git remote add origin "${originPath}"`
955+
);
956+
957+
// Create feature commits with unique content (not in origin)
958+
await executeBash(env, workspaceId, 'echo "unique-unmerged-content" > unique.txt');
959+
await executeBash(env, workspaceId, "git add unique.txt");
960+
await executeBash(env, workspaceId, 'git commit -m "Unique commit"');
961+
962+
// Fetch origin (main doesn't have our content - we didn't push)
963+
await executeBash(env, workspaceId, "git fetch origin");
964+
965+
// Debug: verify the setup is correct before attempting delete
966+
const debugBranches = await executeBash(env, workspaceId, "git branch -a");
967+
const debugLog = await executeBash(
968+
env,
969+
workspaceId,
970+
"git log --oneline --all | head -10"
971+
);
972+
const debugRemote = await executeBash(env, workspaceId, "git remote -v");
973+
console.log(
974+
"DEBUG unmerged test - branches:",
975+
debugBranches.output,
976+
"log:",
977+
debugLog.output,
978+
"remote:",
979+
debugRemote.output
980+
);
981+
982+
// Attempt deletion without force - should fail because content differs
983+
const deleteResult = await env.mockIpcRenderer.invoke(
984+
IPC_CHANNELS.WORKSPACE_REMOVE,
985+
workspaceId
986+
);
987+
988+
// Should fail - genuinely unmerged content
989+
if (deleteResult.success) {
990+
// Provide debug info if test would fail
991+
throw new Error(
992+
`Expected deletion to fail but it succeeded.\n` +
993+
`Branches: ${debugBranches.output}\n` +
994+
`Log: ${debugLog.output}\n` +
995+
`Remote: ${debugRemote.output}\n` +
996+
`Result: ${JSON.stringify(deleteResult)}`
997+
);
998+
}
999+
expect(deleteResult.error).toMatch(/unpushed|changes/i);
1000+
1001+
// Verify workspace still exists
1002+
const stillExists = await workspaceExists(env, workspaceId);
1003+
expect(stillExists).toBe(true);
1004+
1005+
// Cleanup: force delete
1006+
await env.mockIpcRenderer.invoke(IPC_CHANNELS.WORKSPACE_REMOVE, workspaceId, {
1007+
force: true,
1008+
});
1009+
1010+
// Cleanup the bare repo
1011+
try {
1012+
using cleanupProc = execAsync(`rm -rf "${originPath}"`);
1013+
await cleanupProc.result;
1014+
} catch {
1015+
// Ignore cleanup errors
1016+
}
1017+
} finally {
1018+
await cleanupTestEnvironment(env);
1019+
await cleanupTempGitRepo(tempGitRepo);
1020+
}
1021+
},
1022+
TEST_TIMEOUT_SSH_MS
1023+
);
8021024
});
8031025
});

0 commit comments

Comments
 (0)