diff --git a/packages/opencode-router/src/pod-manager.test.ts b/packages/opencode-router/src/pod-manager.test.ts index 05b93af39f39..b5b23532bdfd 100644 --- a/packages/opencode-router/src/pod-manager.test.ts +++ b/packages/opencode-router/src/pod-manager.test.ts @@ -333,6 +333,37 @@ describe("listUserSessions", () => { // Must NOT try to look up "calm-snails-dream" on remote (it's always a new branch) expect(script).not.toContain('ls-remote --exit-code --heads origin "calm-snails-dream"') }) + + it("ensurePod git-init script skips fetch/checkout when /workspace has uncommitted changes from a prior session", async () => { + // Pod restarts re-mount the PVC which can contain uncommitted edits from the previous + // session. Running `git checkout` against a dirty workspace aborts with "local changes + // would be overwritten" and the init container crashloops. The script must detect the + // dirty state and skip the git phase, leaving the workspace exactly as the user left it. + fakePVCs = [] + fakePods = [] + createPodCalls = [] + + const { ensurePod } = await import("./pod-manager.js") + const session = { email: EMAIL, repoUrl: REPO, branch: "resilient-branch", sourceBranch: "main" } + + await (ensurePod as any)(computeHash(EMAIL, REPO, "resilient-branch"), session) + + expect(createPodCalls).toHaveLength(1) + const pod = (createPodCalls[0] as any).body + const script: string = pod.spec.initContainers[0].args[0] + // Dirty-workspace guard must check both tracked diffs and untracked files + expect(script).toContain("$GIT diff --quiet HEAD") + expect(script).toContain("$GIT ls-files --others --exclude-standard") + // The guard wraps fetch + both checkout branches so none of them run when dirty + const guardIdx = script.indexOf("$GIT diff --quiet HEAD") + const fetchIdx = script.indexOf("$GIT fetch --all") + const checkoutIdx = script.indexOf("$GIT checkout") + expect(guardIdx).toBeGreaterThan(-1) + expect(fetchIdx).toBeGreaterThan(guardIdx) + expect(checkoutIdx).toBeGreaterThan(guardIdx) + // Must NOT introduce an auto-stash — work is preserved by leaving the workspace untouched + expect(script).not.toContain("stash push") + }) }) // --------------------------------------------------------------------------- diff --git a/packages/opencode-router/src/pod-manager.ts b/packages/opencode-router/src/pod-manager.ts index 17ed6fd2d8ad..b3966d6897d2 100644 --- a/packages/opencode-router/src/pod-manager.ts +++ b/packages/opencode-router/src/pod-manager.ts @@ -658,12 +658,23 @@ export async function ensurePod( ` git clone "${repoUrl}" /workspace`, `fi`, `cd /workspace`, - `$GIT fetch --all`, - `if $GIT rev-parse --verify "${branch}" >/dev/null 2>&1; then`, - ` $GIT checkout "${branch}"`, + // If the workspace carries uncommitted work from a prior session, skip + // fetch + checkout entirely and start the pod against the existing + // state. The repo was initialized on an earlier pod start so the user + // can keep working; otherwise `git checkout` would abort with "local + // changes would be overwritten" and the pod crashloops in Init:0/1. + // To get back on the session branch, commit or discard the changes + // from inside the pod and restart. + `if ! $GIT diff --quiet HEAD 2>/dev/null || [ -n "$($GIT ls-files --others --exclude-standard 2>/dev/null)" ]; then`, + ` echo "opencode-init: uncommitted changes detected in /workspace; skipping git fetch/checkout to preserve work"`, `else`, - ` $GIT checkout -B "${sourceBranch}" "origin/${sourceBranch}"`, - ` $GIT checkout -b "${branch}"`, + ` $GIT fetch --all`, + ` if $GIT rev-parse --verify "${branch}" >/dev/null 2>&1; then`, + ` $GIT checkout "${branch}"`, + ` else`, + ` $GIT checkout -B "${sourceBranch}" "origin/${sourceBranch}"`, + ` $GIT checkout -b "${branch}"`, + ` fi`, `fi`, ] : [