package host import ( "bytes" "context" "encoding/json" "errors" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "os" "path/filepath" "strings" "testing" "time" ) func TestClaimReviewFlagOffAdoptPreservesDrift(t *testing.T) { etc := t.TempDir() dir := filepath.Join(etc, "systemd/system") if err := os.MkdirAll(dir, 0755); err != nil { t.Fatal(err) } for _, name := range systemUnitFiles { b, err := unitsFS.ReadFile("units/" + name) if err != nil { t.Fatal(err) } if name == claimControlUnit { b = append(b, []byte("\n# operator customization\n")...) } if err := os.WriteFile(filepath.Join(dir, name), b, 0644); err != nil { t.Fatal(err) } } before, _ := os.ReadFile(filepath.Join(dir, claimControlUnit)) calls := 0 ctl := func(...string) (string, error) { calls++; return "", nil } probe := func(args ...string) (string, error) { // This box's host.env does not arm db reads, so its db-read socket // is dark (dbReadsSocketWanted), as ensureSystemUnits keeps it. if args[1] == dbReadSocketUnit { if args[0] == "is-active" { return "inactive", systemctlExit(3) } return "disabled", systemctlExit(1) } if args[0] == "is-active" { return "active", nil } return "enabled", nil } ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), ctl, probe, t.Logf) after, err := os.ReadFile(filepath.Join(dir, claimControlUnit)) if err != nil || calls != 0 || !bytes.Equal(before, after) { t.Fatalf("flag-off drift: calls=%d changed=%v err=%v", calls, !bytes.Equal(before, after), err) } } func TestClaimReviewMalformedSettingsInstallMissingControl(t *testing.T) { etc := t.TempDir() if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(etc, "carlos/host.env"), []byte("CARLOS_HOST_CLAIM='unterminated\n"), 0600); err != nil { t.Fatal(err) } ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), func(...string) (string, error) { return "", nil }, func(...string) (string, error) { return "inactive", nil }, t.Logf) got, err := os.ReadFile(filepath.Join(etc, "systemd/system", claimControlUnit)) want, _ := unitsFS.ReadFile("units/" + claimControlUnit) if err != nil || !bytes.Equal(got, want) { t.Fatalf("missing control not restored to main bytes: %v", err) } } func TestClaimReviewBootstrapToleratesHostEnvironment(t *testing.T) { for _, body := range []string{"not-a-setting\n", "CARLOS_X=foo\\\n", "CARLOS_X=\"unterminated\n", "CARLOS_HOST_CLAIM='unterminated\n"} { t.Run(body, func(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "etc/carlos"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "etc/carlos/host.env"), []byte(body), 0600); err != nil { t.Fatal(err) } if err := bootstrap(root, nil); err != nil { t.Fatalf("flag-off bootstrap failed: %v", err) } got, err := os.ReadFile(filepath.Join(root, "etc/systemd/system", claimControlUnit)) want, _ := unitsFS.ReadFile("units/" + claimControlUnit) if err != nil || !bytes.Equal(got, want) { t.Fatalf("not main bytes: %v", err) } }) } } func TestClaimReviewSystemStageUsesLiveControlSettings(t *testing.T) { for _, enabled := range []bool{false, true} { t.Run(map[bool]string{false: "off", true: "on"}[enabled], func(t *testing.T) { f := newApplyFixture(t, "stable", "v1.0.0") if err := os.MkdirAll(filepath.Join(f.a.etcDir, "carlos"), 0755); err != nil { t.Fatal(err) } flag := "" if enabled { flag = "1" } if err := os.WriteFile(filepath.Join(f.a.etcDir, "carlos/host.env"), []byte("CARLOS_HOST_CLAIM="+flag+"\n"), 0600); err != nil { t.Fatal(err) } legacy, _ := unitsFS.ReadFile("units/" + claimControlUnit) want := claimUnitContent(claimControlUnit, legacy, claimUnitSettings{enabled: enabled, data: "/data", opt: "/opt/carlos"}) livePath := filepath.Join(f.a.etcDir, "systemd/system", claimControlUnit) if err := os.WriteFile(livePath, want, 0644); err != nil { t.Fatal(err) } run := f.a.ops.run f.a.ops.run = func(ctx context.Context, limit time.Duration, name string, args ...string) (string, error) { if len(args) > 2 && args[0] == "bootstrap" { if err := bootstrapWith(args[2], nil, true); err != nil { return "", err } b, err := json.Marshal(currentBootstrapPlan()) return string(b), err } return run(ctx, limit, name, args...) } stage := filepath.Join(t.TempDir(), "stage") if _, _, err := f.a.stagePlan(t.Context(), "candidate", stage); err != nil { t.Fatal(err) } stagedPath := filepath.Join(stage, "etc/systemd/system", claimControlUnit) staged, err := os.ReadFile(stagedPath) if err != nil || !bytes.Equal(staged, legacy) { t.Fatalf("stage read live settings: %v", err) } marker := []byte("\n# candidate release setting\n") want = append(want, marker...) if err := os.WriteFile(stagedPath, append(staged, marker...), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(livePath, want, 0644); err != nil { t.Fatal(err) } if err := f.a.installUnits(t.Context(), stage, nil); err != nil { t.Fatal(err) } got, err := os.ReadFile(livePath) if err != nil || !bytes.Equal(got, want) { t.Errorf("live control rendering changed: %v", err) } if strings.Contains(strings.Join(f.calls, "\n"), "try-restart "+claimControlUnit) { t.Errorf("unchanged control restarted: %v", f.calls) } }) } } func TestClaimReviewControlEntryKeepsServiceLifetime(t *testing.T) { fixtureWithoutRootMigrationCensus(t) service, cancel := context.WithCancel(t.Context()) defer cancel() var startup context.Context store := release.NewDirStore(t.TempDir()) ctx, err := claimEntryContext(service, func(string) string { return "1" }, func(call context.Context) (release.Store, error) { startup = call if _, bounded := call.Deadline(); !bounded { t.Error("store startup has no deadline") } return store, nil }) if err != nil { t.Fatal(err) } if _, bounded := ctx.Deadline(); bounded { t.Fatal("service inherited startup deadline") } if startup.Err() != context.Canceled { t.Error("startup context not released") } if ctx.Err() != nil || claimStore(ctx) != store { t.Fatal("startup cancellation lost service or store") } sentinel := errors.New("provider stop") owner := &reviewClaimOwner{open: func(call context.Context) error { if _, bounded := call.Deadline(); bounded { t.Error("owners inherited startup deadline") } if call.Err() != nil { t.Error("owner context canceled before shutdown") } cancel() if call.Err() != context.Canceled { t.Error("owners do not follow service shutdown") } return sentinel }} reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() _, err = OpenProductionClaimAssembly(ctx, store, ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: t.TempDir(), OptDir: t.TempDir(), UnitsDir: t.TempDir()}, owner) if !errors.Is(err, sentinel) { t.Fatal(err) } } type reviewClaimOwner struct{ open func(context.Context) error } func (p *reviewClaimOwner) OpenClaimOwners(ctx context.Context, _ release.Store, _ ClaimRuntimeResources) (ClaimOwnerBindings, error) { return ClaimOwnerBindings{}, p.open(ctx) }