//go:build linux package host import ( "errors" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/rootstate" ) func TestRunAdoptDisabledClaimsArmsSystemUnitsBeforeRegistryOpenFailure(t *testing.T) { if os.Getenv("CARLOS_TEST_ADOPT_ORDER_HELPER") == "1" { os.Unsetenv("CARLOS_HOST_CLAIM") probe := func(args ...string) (string, error) { switch args[0] { case "is-active": return "inactive\n", errors.New("exit status 3") case "is-enabled": return "disabled\n", errors.New("exit status 1") } return "", nil } systemctl := func(args ...string) (string, error) { f, err := os.OpenFile(os.Getenv("CARLOS_TEST_ADOPT_SYSTEMCTL_CALLS"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { return "", err } defer f.Close() _, err = fmt.Fprintln(f, strings.Join(args, " ")) return "", err } ensure := func() { ensureSystemUnits( os.Getenv("CARLOS_TEST_ADOPT_ETC"), os.Getenv("CARLOS_TEST_ADOPT_SYSTEM_LOCK"), systemctl, probe, log.Printf, ) } runAdoptWithEnsureSystemUnits([]string{"-registry", os.Getenv("CARLOS_TEST_ADOPT_REGISTRY")}, nil, ensure, noCensusAdoptSource(func() (rootstate.Store, error) { return &plan14ReleaseStore{}, nil })) return } root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "data"), 0o755); err != nil { t.Fatal(err) } blocker := filepath.Join(root, "not-a-directory") if err := os.WriteFile(blocker, []byte("block registry parent"), 0o600); err != nil { t.Fatal(err) } registryPath := filepath.Join(blocker, "registry.db") etc := filepath.Join(root, "etc") callsPath := filepath.Join(root, "systemctl.calls") cmd := exec.Command(os.Args[0], "-test.run=^TestRunAdoptDisabledClaimsArmsSystemUnitsBeforeRegistryOpenFailure$", "-test.v") cmd.Dir = root cmd.Env = isolatedAdoptOrderEnv(t, "CARLOS_HOST_CLAIM=", "CARLOS_DATA_DIR="+filepath.Join(root, "data"), "CARLOS_OPT_DIR="+filepath.Join(root, "opt"), "CARLOS_TEST_ADOPT_ORDER_HELPER=1", "CARLOS_TEST_ADOPT_REGISTRY="+registryPath, "CARLOS_TEST_ADOPT_ETC="+etc, "CARLOS_TEST_ADOPT_SYSTEM_LOCK="+filepath.Join(root, "system.lock"), "CARLOS_TEST_ADOPT_SYSTEMCTL_CALLS="+callsPath, ) out, err := cmd.CombinedOutput() if err == nil { t.Fatalf("helper succeeded; want fatal registry-open failure:\n%s", out) } got := string(out) unitErr := "adopt: self-update units installed and armed" registryErr := "adopt: open registry " + registryPath + ":" unitAt, registryAt := strings.Index(got, unitErr), strings.Index(got, registryErr) if unitAt < 0 { t.Fatalf("unit arming was not reported before exit:\n%s", got) } if registryAt < 0 { t.Fatalf("registry error does not match the adopt error contract:\n%s", got) } if unitAt > registryAt { t.Fatalf("registry failed before unit repair ran:\n%s", got) } for _, unit := range systemUnitFiles { if _, err := os.Stat(filepath.Join(etc, "systemd", "system", unit)); err != nil { t.Errorf("unit repair did not install %s before registry failure: %v", unit, err) } } calls, err := os.ReadFile(callsPath) if err != nil { t.Fatalf("read systemctl calls: %v", err) } called := string(calls) for _, want := range []string{ "daemon-reload", "enable carlos-system-update.path carlos-system-recover.service carlos-instance-control.socket", "start carlos-system-update.path carlos-instance-control.socket", } { if !strings.Contains(called, want) { t.Errorf("systemctl calls lack %q:\n%s", want, called) } } } func isolatedAdoptOrderEnv(t *testing.T, extra ...string) []string { t.Helper() home := t.TempDir() drop := map[string]bool{ "CARLOS_SERVICE_TOKEN": true, "CARLOS_CREDENTIALS": true, "CARLOS_CONSOLE": true, "CARLOS_DESTINATION": true, "HOME": true, } for _, kv := range extra { if key, _, ok := strings.Cut(kv, "="); ok { drop[key] = true } } var env []string for _, kv := range os.Environ() { key, _, _ := strings.Cut(kv, "=") if !drop[key] { env = append(env, kv) } } env = append(env, "CARLOS_CREDENTIALS="+filepath.Join(home, "credentials"), "CARLOS_CONSOLE=", "CARLOS_DESTINATION=", "HOME="+home, ) return append(env, extra...) }