mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 14:37:20 +02:00
331 lines
11 KiB
Go
331 lines
11 KiB
Go
package update
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestApplyRefusesRepositorySubdirectory(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
sub := filepath.Join(tr.install, "settings")
|
|
if err := os.Mkdir(sub, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
head := mustGit(t, tr.install, "rev-parse", "HEAD")
|
|
tr.upstreamCommit(t, "second", map[string]string{"run.sh": "new script\n"})
|
|
_, err := Apply(context.Background(), Options{Root: sub, Repo: tr.upstream, BinaryName: "none"}, nil)
|
|
if err == nil {
|
|
t.Fatal("updating a repository subdirectory must be refused")
|
|
}
|
|
if got := mustGit(t, tr.install, "rev-parse", "HEAD"); got != head {
|
|
t.Fatal("refusal changed parent repository")
|
|
}
|
|
}
|
|
|
|
func TestInstallLockReleasedWhenProcessIsKilled(t *testing.T) {
|
|
if root := os.Getenv("CSAI_TEST_LOCK_ROOT"); root != "" {
|
|
release, err := LockInstall(Options{Root: root})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer release()
|
|
fmt.Println("locked")
|
|
time.Sleep(time.Hour)
|
|
return
|
|
}
|
|
root := t.TempDir()
|
|
bin, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, bin, "-test.run=^TestInstallLockReleasedWhenProcessIsKilled$")
|
|
cmd.Env = append(os.Environ(), "CSAI_TEST_LOCK_ROOT="+root)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer cmd.Process.Kill()
|
|
line, err := bufio.NewReader(stdout).ReadString('\n')
|
|
if err != nil || line != "locked\n" {
|
|
t.Fatalf("child did not acquire lock: %q %v", line, err)
|
|
}
|
|
if release, err := LockInstall(Options{Root: root}); err == nil {
|
|
release()
|
|
t.Fatal("parent acquired child's lock")
|
|
}
|
|
if err := cmd.Process.Kill(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = cmd.Wait()
|
|
release, err := LockInstall(Options{Root: root})
|
|
if err != nil {
|
|
t.Fatalf("crashed process left a stale lock: %v", err)
|
|
}
|
|
release()
|
|
}
|
|
|
|
func TestFirstBuildDoesNotUseAnUnrelatedPreviousBinary(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
scriptFakeGo(t)
|
|
bin := filepath.Join(tr.install, Binary)
|
|
writeFile(t, bin+".prev", "unrelated old binary")
|
|
tr.upstreamCommit(t, "second", map[string]string{"run.sh": "new script\n"})
|
|
if _, err := Apply(context.Background(), buildOpts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
head := mustGit(t, tr.install, "rev-parse", "HEAD")
|
|
if _, err := Rollback(context.Background(), buildOpts(tr)); err == nil {
|
|
t.Fatal("first build must not roll back to an unrelated .prev")
|
|
}
|
|
if got := mustGit(t, tr.install, "rev-parse", "HEAD"); got != head {
|
|
t.Fatal("refusal reset source")
|
|
}
|
|
}
|
|
|
|
func TestInterruptedAdoptionCanResumeFromUnbornHead(t *testing.T) {
|
|
requireGit(t)
|
|
tr, dir := newAdoptFixture(t)
|
|
scriptFakeGo(t)
|
|
role := filepath.Join(dir, "roles", "shipped.yaml")
|
|
original := readFile(t, role)
|
|
mustGit(t, dir, "init", "-q")
|
|
mustGit(t, dir, "remote", "add", "origin", tr.upstream)
|
|
branch, _, _, err := fetchSource(context.Background(), dir, tr.upstream)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scan, err := scanAdoption(context.Background(), dir, dir, "origin/"+branch)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backup := filepath.Join(dir, ".update-backup", "interrupted")
|
|
for _, rel := range append(scan.changed, scan.kept...) {
|
|
dst := filepath.Join(backup, "overwritten", rel)
|
|
if Protected(rel) {
|
|
dst = filepath.Join(backup, rel)
|
|
}
|
|
if err := copyFile(filepath.Join(dir, rel), dst, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
target := mustGit(t, dir, "rev-parse", "origin/"+branch)
|
|
if err := writeBuildPending(dir, buildPendingState{Commit: target, AdoptBranch: branch, BackupDir: backup, KeptContent: scan.kept, OverwrittenContent: scan.changed, PreviousBinaryAbsent: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Apply(context.Background(), Options{Root: dir, Repo: tr.upstream}, nil); err != nil {
|
|
t.Fatalf("resume: %v", err)
|
|
}
|
|
if got := readFile(t, role); got != original {
|
|
t.Fatalf("resume lost role: %q", got)
|
|
}
|
|
if buildPending(dir) {
|
|
t.Fatal("resume left a pending marker")
|
|
}
|
|
}
|
|
|
|
func TestInterruptedRollbackResumesAfterSourceReset(t *testing.T) {
|
|
for _, swapped := range []bool{false, true} {
|
|
t.Run(fmt.Sprint(swapped), func(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
scriptFakeGo(t)
|
|
bin := filepath.Join(tr.install, Binary)
|
|
writeFile(t, bin, "original binary")
|
|
tr.upstreamCommit(t, "second", map[string]string{"run.sh": "new script\n"})
|
|
if _, err := Apply(context.Background(), buildOpts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, _ := readState(tr.install)
|
|
role := filepath.Join(tr.install, "roles", "shipped.yaml")
|
|
writeFile(t, role, "name: operator edit after update\n")
|
|
changes, _, _ := localChanges(context.Background(), tr.install)
|
|
backup, kept, deleted, err := stashProtectedEdits(context.Background(), tr.install, st.PreviousCommit, changes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
liveHash, _ := fileSHA256(bin)
|
|
previousHash, _ := fileSHA256(bin + ".prev")
|
|
pending := rollbackPendingState{State: st, BackupDir: backup, Kept: kept, Deleted: deleted, RestoreBinary: true, LiveSHA256: liveHash, PreviousSHA256: previousHash}
|
|
if err := writeRollbackPending(tr.install, pending); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustGit(t, tr.install, "reset", "--hard", st.PreviousCommit)
|
|
if swapped {
|
|
if err := restorePrevBinary(bin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := Apply(context.Background(), buildOpts(tr), nil); err == nil {
|
|
t.Fatal("update must refuse an interrupted rollback")
|
|
}
|
|
if _, err := Rollback(context.Background(), buildOpts(tr)); err != nil {
|
|
t.Fatalf("resume rollback: %v", err)
|
|
}
|
|
if got := readFile(t, bin); got != "original binary" {
|
|
t.Fatalf("wrong binary after resume: %q", got)
|
|
}
|
|
if got := readFile(t, role); got != "name: operator edit after update\n" {
|
|
t.Fatalf("role lost after resume: %q", got)
|
|
}
|
|
if pending, err := readRollbackPending(tr.install); err != nil || pending != nil {
|
|
t.Fatal("rollback journal not cleared")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRollbackRemovesVersionInsertedIntoUnversionedConfig(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
scriptFakeGo(t)
|
|
writeFile(t, filepath.Join(tr.install, Binary), "old binary")
|
|
before := readFile(t, filepath.Join(tr.install, "config.yaml"))
|
|
tr.upstreamCommit(t, "release", map[string]string{"config.example.yaml": "version: v2.0.0\n"})
|
|
if _, err := Apply(context.Background(), buildOpts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Rollback(context.Background(), buildOpts(tr)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := readFile(t, filepath.Join(tr.install, "config.yaml")); got != before {
|
|
t.Fatalf("rollback did not restore an unversioned config: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyRefusesIgnoredSourceCollision(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
writeFile(t, filepath.Join(tr.install, ".git", "info", "exclude"), "local.go\n")
|
|
writeFile(t, filepath.Join(tr.install, "local.go"), "package operator\n")
|
|
head := mustGit(t, tr.install, "rev-parse", "HEAD")
|
|
tr.upstreamCommit(t, "add source", map[string]string{"local.go": "package upstream\n"})
|
|
if _, err := Apply(context.Background(), opts(tr), nil); err == nil {
|
|
t.Fatal("ignored source collision must be refused")
|
|
}
|
|
if got := readFile(t, filepath.Join(tr.install, "local.go")); got != "package operator\n" {
|
|
t.Fatal("ignored source was lost")
|
|
}
|
|
if got := mustGit(t, tr.install, "rev-parse", "HEAD"); got != head {
|
|
t.Fatal("refusal changed HEAD")
|
|
}
|
|
}
|
|
|
|
func TestBuildRefusesSourceChangesMadeAfterPreflight(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
scriptFakeGo(t)
|
|
bin := filepath.Join(tr.install, Binary)
|
|
writeFile(t, bin, "old binary")
|
|
tr.upstreamCommit(t, "second", map[string]string{"run.sh": "new script\n"})
|
|
_, err := Apply(context.Background(), buildOpts(tr), func(step Step) {
|
|
if step.Phase == "build" {
|
|
writeFile(t, filepath.Join(tr.install, "run.sh"), "operator edit during build\n")
|
|
}
|
|
})
|
|
if ue, ok := err.(*Error); !ok || ue.Reason != "local_source_edits" {
|
|
t.Fatalf("want refusal of changed source, got %v", err)
|
|
}
|
|
if got := readFile(t, bin); got != "old binary" {
|
|
t.Fatal("unmatched binary installed")
|
|
}
|
|
if !buildPending(tr.install) {
|
|
t.Fatal("refusal must keep the recovery marker")
|
|
}
|
|
}
|
|
|
|
func TestApplyKeepsProtectedDeletion(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
role := filepath.Join(tr.install, "roles", "shipped.yaml")
|
|
if err := os.Remove(role); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr.upstreamCommit(t, "rewrite role", map[string]string{"roles/shipped.yaml": "name: new upstream\n"})
|
|
if _, err := Apply(context.Background(), opts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Lstat(role); !os.IsNotExist(err) {
|
|
t.Fatalf("deleted role resurrected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAdoptRefusesExistingInvalidGitMetadata(t *testing.T) {
|
|
requireGit(t)
|
|
tr, dir := newAdoptFixture(t)
|
|
metadata := filepath.Join(dir, ".git", "operator-note")
|
|
writeFile(t, metadata, "keep this metadata")
|
|
_, err := Adopt(context.Background(), Options{Root: dir, Repo: tr.upstream, BinaryName: "none"}, nil)
|
|
if err == nil {
|
|
t.Fatal("existing .git must never be taken over")
|
|
}
|
|
if got := readFile(t, metadata); got != "keep this metadata" {
|
|
t.Fatal("existing metadata changed")
|
|
}
|
|
}
|
|
|
|
func TestAdoptionRefusesFileDirectoryConflict(t *testing.T) {
|
|
requireGit(t)
|
|
tr, dir := newAdoptFixture(t)
|
|
if err := os.Remove(filepath.Join(dir, "run.sh")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
valuable := filepath.Join(dir, "run.sh", "local-note")
|
|
writeFile(t, valuable, "operator content")
|
|
if _, err := Preview(context.Background(), Options{Root: dir, Repo: tr.upstream}); err == nil {
|
|
t.Fatal("preview must report a directory conflict")
|
|
}
|
|
if _, err := Adopt(context.Background(), Options{Root: dir, Repo: tr.upstream, BinaryName: "none"}, nil); err == nil {
|
|
t.Fatal("adoption must refuse a directory conflict")
|
|
}
|
|
if got := readFile(t, valuable); got != "operator content" {
|
|
t.Fatal("directory content lost")
|
|
}
|
|
}
|
|
|
|
func TestApplyKeepsIgnoredConfigWhenUpstreamStartsTrackingIt(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
writeFile(t, filepath.Join(tr.install, ".git", "info", "exclude"), "config.yaml\n")
|
|
before := readFile(t, filepath.Join(tr.install, "config.yaml"))
|
|
tr.upstreamCommit(t, "track example config", map[string]string{"config.yaml": "server:\n port: 9999\n"})
|
|
if _, err := Apply(context.Background(), opts(tr), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := readFile(t, filepath.Join(tr.install, "config.yaml")); got != before {
|
|
t.Fatalf("ignored config overwritten: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdateOperationsShareInstallationLock(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
release, err := LockInstall(opts(tr))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer release()
|
|
for _, run := range []func() error{
|
|
func() error { _, err := Check(context.Background(), opts(tr)); return err },
|
|
func() error { _, err := Apply(context.Background(), opts(tr), nil); return err },
|
|
func() error { _, err := Rollback(context.Background(), opts(tr)); return err },
|
|
func() error { _, err := Adopt(context.Background(), opts(tr), nil); return err },
|
|
} {
|
|
if ue, ok := run().(*Error); !ok || ue.Reason != "update_busy" {
|
|
t.Fatalf("concurrent operation was not refused: %v", ue)
|
|
}
|
|
}
|
|
}
|