diff --git a/apps/backend/internal/agent/lifecycle/default_scripts.go b/apps/backend/internal/agent/lifecycle/default_scripts.go index 76dc6b757c..72cc99b119 100644 --- a/apps/backend/internal/agent/lifecycle/default_scripts.go +++ b/apps/backend/internal/agent/lifecycle/default_scripts.go @@ -97,21 +97,12 @@ git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" {{github.auth_setup}} # ---- Clone repository ---- +# The kandev-managed feature-branch checkout is appended as an invariant +# postlude (see KandevBranchCheckoutPostlude) — keep it out of the default +# so old profiles snapshotting this script and the postlude never disagree. git clone --depth=1 --branch {{repository.branch}} {{repository.clone_url}} {{workspace.path}} cd {{workspace.path}} -# Switch to the kandev-managed feature branch. On resume after a destroyed -# container, prefer the remote tip if the agent had previously pushed; fall -# back to a fresh branch off the chosen base. -B is idempotent if the local -# branch somehow already exists. -if [ -n "{{worktree.branch}}" ] && [ "{{worktree.branch}}" != "{{repository.branch}}" ]; then - if git fetch --depth=1 origin "{{worktree.branch}}" 2>/dev/null; then - git checkout -B "{{worktree.branch}}" "origin/{{worktree.branch}}" - else - git checkout -b "{{worktree.branch}}" - fi -fi - # Strip embedded token from remote URL to avoid persisting credentials in .git/config git remote set-url origin "$(git remote get-url origin | sed 's|https://[^@]*@github.com/|https://github.com/|')" 2>/dev/null || true @@ -149,22 +140,13 @@ chmod +x /usr/local/bin/pnpm {{git.identity_setup}} # ---- Clone repository ---- +# The kandev-managed feature-branch checkout is appended as an invariant +# postlude (see KandevBranchCheckoutPostlude) — keep it out of the default +# so old profiles snapshotting this script and the postlude never disagree. echo "Cloning {{repository.clone_url}} (branch: {{repository.branch}})..." git clone --depth=1 --quiet --branch {{repository.branch}} {{repository.clone_url}} {{workspace.path}} cd {{workspace.path}} -# Switch to the kandev-managed feature branch. On resume after a destroyed -# sandbox, prefer the remote tip if the agent had previously pushed; fall -# back to a fresh branch off the chosen base. -B is idempotent if the local -# branch somehow already exists. -if [ -n "{{worktree.branch}}" ] && [ "{{worktree.branch}}" != "{{repository.branch}}" ]; then - if git fetch --depth=1 origin "{{worktree.branch}}" 2>/dev/null; then - git checkout -B "{{worktree.branch}}" "origin/{{worktree.branch}}" - else - git checkout -b "{{worktree.branch}}" - fi -fi - # Strip embedded token from remote URL to avoid persisting credentials in .git/config git remote set-url origin "$(git remote get-url origin | sed 's|https://[^@]*@github.com/|https://github.com/|')" 2>/dev/null || true diff --git a/apps/backend/internal/agent/lifecycle/default_scripts_test.go b/apps/backend/internal/agent/lifecycle/default_scripts_test.go index 5ae4dd6971..32089069d4 100644 --- a/apps/backend/internal/agent/lifecycle/default_scripts_test.go +++ b/apps/backend/internal/agent/lifecycle/default_scripts_test.go @@ -29,53 +29,27 @@ func TestKandevBranchCheckoutPostlude_HasInvariantSteps(t *testing.T) { } } -// TestDefaultPrepareScripts_FeatureBranchCheckout asserts that both clone-based -// remote scripts include the kandev feature-branch checkout block, that the -// block prefers the remote tip when one exists (resume-after-destroyed-sandbox -// recovery), and that it falls back to creating a fresh local branch. -func TestDefaultPrepareScripts_FeatureBranchCheckout(t *testing.T) { - cases := []struct { - executorType string - want []string - }{ - { - executorType: "local_docker", - want: []string{ - `if [ -n "{{worktree.branch}}" ] && [ "{{worktree.branch}}" != "{{repository.branch}}" ]; then`, - `if git fetch --depth=1 origin "{{worktree.branch}}" 2>/dev/null; then`, - `git checkout -B "{{worktree.branch}}" "origin/{{worktree.branch}}"`, - `git checkout -b "{{worktree.branch}}"`, - }, - }, - { - executorType: "remote_docker", - want: []string{ - `if [ -n "{{worktree.branch}}" ] && [ "{{worktree.branch}}" != "{{repository.branch}}" ]; then`, - `if git fetch --depth=1 origin "{{worktree.branch}}" 2>/dev/null; then`, - `git checkout -B "{{worktree.branch}}" "origin/{{worktree.branch}}"`, - `git checkout -b "{{worktree.branch}}"`, - }, - }, - { - executorType: "sprites", - want: []string{ - `if [ -n "{{worktree.branch}}" ] && [ "{{worktree.branch}}" != "{{repository.branch}}" ]; then`, - `if git fetch --depth=1 origin "{{worktree.branch}}" 2>/dev/null; then`, - `git checkout -B "{{worktree.branch}}" "origin/{{worktree.branch}}"`, - `git checkout -b "{{worktree.branch}}"`, - }, - }, +// TestDefaultPrepareScripts_NoInlineFeatureBranchCheckout asserts that the +// clone-based remote default scripts no longer carry an inline worktree- +// branch checkout. The checkout is owned exclusively by the postlude +// (KandevBranchCheckoutPostlude) so old stored profiles and the current +// default can never disagree about how the feature branch is materialised. +func TestDefaultPrepareScripts_NoInlineFeatureBranchCheckout(t *testing.T) { + executors := []string{"local_docker", "remote_docker", "sprites"} + forbidden := []string{ + `if [ -n "{{worktree.branch}}" ] && [ "{{worktree.branch}}" != "{{repository.branch}}" ]; then`, + `git checkout -B "{{worktree.branch}}" "origin/{{worktree.branch}}"`, } - for _, tc := range cases { - t.Run(tc.executorType, func(t *testing.T) { - script := DefaultPrepareScript(tc.executorType) + for _, executorType := range executors { + t.Run(executorType, func(t *testing.T) { + script := DefaultPrepareScript(executorType) if script == "" { - t.Fatalf("DefaultPrepareScript(%q) returned empty", tc.executorType) + t.Fatalf("DefaultPrepareScript(%q) returned empty", executorType) } - for _, want := range tc.want { - if !strings.Contains(script, want) { - t.Errorf("script for %q missing %q", tc.executorType, want) + for _, bad := range forbidden { + if strings.Contains(script, bad) { + t.Errorf("script for %q must not contain inline checkout %q (postlude owns it)", executorType, bad) } } })