package main import ( "context" "encoding/json" "strings" "testing" "github.com/novox/mesh-controller/internal/link" ) // A holder paused stays paused across its restart: the flag is kept in its workspace (novox/hq ADR // 0219), and what it says about itself follows. func TestAPausedHolderStaysPausedAcrossARestart(t *testing.T) { workspace := t.TempDir() var said []link.HolderState h := newHolder("ace", link.TheBuildMachine, workspace, func(s link.HolderState) error { said = append(said, s) return nil }) if h.Paused() { t.Fatal("a new holder starts paused") } if _, err := h.handlers()["pause"](context.Background(), json.RawMessage(`{}`)); err != nil { t.Fatal(err) } if !h.Paused() || len(said) != 1 || !said[0].Paused || said[0].On != "ace" { t.Fatalf("paused: %v, said %+v", h.Paused(), said) } again := newHolder("ace", link.TheBuildMachine, workspace, nil) if !again.Paused() { t.Fatal("restarted, the holder forgot it was paused") } if _, err := again.handlers()["resume"](context.Background(), json.RawMessage(`{}`)); err != nil { t.Fatal(err) } if newHolder("ace", link.TheBuildMachine, workspace, nil).Paused() { t.Fatal("resumed, the holder came back paused") } } // kill ends the build with that id — its context, which ends its commands — removes what it left by // label, and refuses an id it is not building. func TestKillEndsTheBuildRunningHereAndNoOther(t *testing.T) { h := newHolder("ace", link.TheBuildMachine, t.TempDir(), nil) var removed []string h.remove = func(_ context.Context, _ string, name string, args ...string) (string, error) { removed = append(removed, name+" "+strings.Join(args, " ")) if args[0] == "ps" { return "c1\n", nil } return "", nil } kill := h.handlers()["kill"] if _, err := kill(context.Background(), json.RawMessage(`{"id":"build-1"}`)); err == nil { t.Fatal("killed a build while none ran") } building, cancel := context.WithCancel(context.Background()) r := h.begin(link.BuildRequest{ID: "build-1", Repository: "novox/a"}, cancel) h.stepped(r, "image") if c := h.current(); c.Running == nil || c.Running.ID != "build-1" || c.Running.Step != "image" { t.Fatalf("current says %+v", c) } if _, err := kill(context.Background(), json.RawMessage(`{"id":"build-2"}`)); err == nil || !strings.Contains(err.Error(), "build-1") { t.Fatalf("killed another id: %v", err) } // The build's own goroutine: it ends when its context does, as a build's commands do. go func() { <-building.Done() h.end(r) }() answer, err := kill(context.Background(), json.RawMessage(`{"id":"build-1"}`)) if err != nil { t.Fatal(err) } if building.Err() == nil { t.Fatal("the build's context was not cancelled") } if !h.wasKilled(r) { t.Error("the build is not marked killed, so it would be announced as an ordinary failure") } said := answer.(map[string]any)["said"].(string) if !strings.Contains(said, "1 container(s)") || !strings.Contains(said, link.KilledByHand) { t.Errorf("kill said %q", said) } if len(removed) != 2 || !strings.Contains(removed[0], "label=mesh.build=build-1") { t.Errorf("removed %v", removed) } if c := h.current(); c.Running != nil { t.Errorf("after the kill current says %+v", c) } }