mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 14:37:20 +02:00
The review found two failure paths that reported success for work the engine had not done. Rollback: it refused only when *non-protected* source files were modified, and then ran `git reset --hard` to the previous commit - which discards every local change to tracked files, not just the update's. Edits to roles/skills/tools (the update itself keeps those via its own stash) and edits made after the update were silently wiped, while Result said nothing: deployment.md's promise that "work done afterwards is not swept away" did not hold. Rollback now copies every protected file that differs right now into `.update-backup/rollback_<ts>/` before the reset, sets the copies back after it, re-applies hand deletions, and names what it kept in Result.KeptContent like an update does. Retry: Apply merges and writes the state file before it builds, so a failed (or toolchain-less) build leaves HEAD on the new commit with the old binary still in place. The retry then took the `!snap.UpdateAvailable` no-op branch, aligned the version and returned err=nil - the job was recorded as succeeded, the page said "update finished", the CLI printed that source and binary were in place, and the old executable kept running. The tree now carries a `.update-build-pending` marker, written the moment the merge lands (and when an adoption commits to its work tree) and cleared only after the new binary is in place. The no-op branch reads it: with a toolchain it compiles and reports the build (commits=0, binaryBuilt=true); without one it keeps refusing exactly like the first attempt, so "install go and click again" is now a real instruction. The console stops treating "nothing to pull" as "nothing to do" (the retry button stays live, the confirm dialog and the done toast say the build is what will happen), and the CLI's printResult no longer swallows the build-only result. Both paths are covered by regression tests that fail on the pre-fix tree: two rollback scenarios driven through real git (edit before and after the update, a hand deletion), and four retry scenarios driven through a scripted go on PATH (failed build, state-write failure, an adoption's failed build retried by the next update, and no toolchain then toolchain).
84 lines
3.4 KiB
Go
84 lines
3.4 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestRollbackKeepsOperatorEditsToProtectedFiles pins the promise docs make - a rollback
|
|
// undoes the update, not the operator's content. reset --hard discards every local change
|
|
// to tracked files, not just the update's, so both an edit made before the update and one
|
|
// made after it have to be put back on top of the old commit.
|
|
func TestRollbackKeepsOperatorEditsToProtectedFiles(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
|
|
// Edited before the update: Apply itself keeps this one (its stash covers it), which is
|
|
// exactly the shape the rollback then used to wipe.
|
|
writeFile(t, filepath.Join(tr.install, "roles", "shipped.yaml"), "name: shipped\ndescription: mine before\n")
|
|
tr.upstreamCommit(t, "upstream moves code and rewrites roles", map[string]string{
|
|
"internal_service.go": "package service\n\nconst Version = \"2\"\n",
|
|
"roles/shipped.yaml": "name: shipped\ndescription: upstream rewrite\n",
|
|
"roles/extra.yaml": "name: extra\ndescription: upstream copy\n",
|
|
})
|
|
if _, err := Apply(context.Background(), opts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Edited after the update: no stash ever saw this one.
|
|
writeFile(t, filepath.Join(tr.install, "roles", "extra.yaml"), "name: extra\ndescription: mine after\n")
|
|
|
|
// The binary pair a swap leaves behind, so the rollback has one to restore.
|
|
bin := filepath.Join(tr.install, Binary)
|
|
writeFile(t, bin, "new binary bytes")
|
|
writeFile(t, bin+".prev", "old binary bytes")
|
|
|
|
res, err := Rollback(context.Background(), Options{Root: tr.install})
|
|
if err != nil {
|
|
t.Fatalf("rollback failed: %v", err)
|
|
}
|
|
if got := readFile(t, filepath.Join(tr.install, "internal_service.go")); !strings.Contains(got, "Version = \"1\"") {
|
|
t.Errorf("code after rollback = %q, want the old version back", got)
|
|
}
|
|
if got := readFile(t, filepath.Join(tr.install, "roles", "shipped.yaml")); !strings.Contains(got, "mine before") {
|
|
t.Errorf("an edit made before the update was wiped by the rollback: %q", got)
|
|
}
|
|
if got := readFile(t, filepath.Join(tr.install, "roles", "extra.yaml")); !strings.Contains(got, "mine after") {
|
|
t.Errorf("an edit made after the update was wiped by the rollback: %q", got)
|
|
}
|
|
want := "roles/extra.yaml,roles/shipped.yaml"
|
|
if got := strings.Join(res.KeptContent, ","); got != want {
|
|
t.Errorf("keptContent = %v, want %s", res.KeptContent, want)
|
|
}
|
|
}
|
|
|
|
// TestRollbackKeepsAHandDeletionOfAProtectedFile: a deletion is a local change like any
|
|
// other. reset --hard would bring the old commit's copy back; the rollback has to re-apply
|
|
// the deletion instead.
|
|
func TestRollbackKeepsAHandDeletionOfAProtectedFile(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
tr.upstreamCommit(t, "upstream moves", map[string]string{"internal_service.go": "package service\n\nconst Version = \"2\"\n"})
|
|
if _, err := Apply(context.Background(), opts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gone := filepath.Join(tr.install, "roles", "shipped.yaml")
|
|
if err := os.Remove(gone); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bin := filepath.Join(tr.install, Binary)
|
|
writeFile(t, bin, "new binary bytes")
|
|
writeFile(t, bin+".prev", "old binary bytes")
|
|
|
|
res, err := Rollback(context.Background(), Options{Root: tr.install})
|
|
if err != nil {
|
|
t.Fatalf("rollback failed: %v", err)
|
|
}
|
|
if _, err := os.Stat(gone); !os.IsNotExist(err) {
|
|
t.Errorf("a file deleted by hand was resurrected by the rollback (%v); result %+v", err, res)
|
|
}
|
|
}
|