| index aab960ce0..372e83818 100644 |
| --- a/internal/appconfig/syncexec_test.go |
| +++ b/internal/appconfig/syncexec_test.go |
| @@ -94,7 +94,7 @@ func TestSyncExecWritesConfigFileAndBouncesOnChange(t *testing.T) { |
| } |
| var bounced []string |
| bounce := func(h string) error { bounced = append(bounced, h); return nil } |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(filepath.Join(dd, "inst.example.com.config.env")) |
| @@ -112,7 +112,7 @@ func TestSyncExecWritesConfigFileAndBouncesOnChange(t *testing.T) { |
| } |
| // Second pass, nothing changed: no bounce. |
| bounced = nil |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec (second): %v", err) |
| } |
| if len(bounced) != 0 { |
| @@ -140,7 +140,7 @@ func TestSyncExecRemovesFileWhenManifestDeleted(t *testing.T) { |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if _, err := os.Stat(path); !os.IsNotExist(err) { |
| @@ -185,7 +185,7 @@ func TestSyncExecMassRemovalGuardRefusesWhenEveryGroupResolvesAbsent(t *testing. |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if _, err := os.Stat(pathA); err != nil { |
| @@ -230,7 +230,7 @@ func TestSyncExecMassRemovalGuardDoesNotBlockNormalConvergence(t *testing.T) { |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if _, err := os.Stat(pathA); err != nil { |
| @@ -282,7 +282,7 @@ func TestSyncExecRendersEachRoutesBoundEnvironment(t *testing.T) { |
| t.Fatalf("Upsert %s: %v", rt.Host, err) |
| } |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| want := map[string]string{ |
| @@ -329,7 +329,7 @@ func TestSyncExecLeavesFileUntouchedWhenBoundEnvironmentIsMissing(t *testing.T) |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(path) |
| @@ -392,7 +392,7 @@ func TestSyncExecOverlayOnlyAppDropsStaleKeys(t *testing.T) { |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(path) |
| @@ -440,7 +440,7 @@ func TestSyncExecMassRemovalGuardIgnoresErroredGroups(t *testing.T) { |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| for _, h := range []string{"a.example.com", "b.example.com", "c.example.com"} { |
| @@ -476,7 +476,7 @@ func TestSyncExecNeverTouchesInstanceEnvOrOutOfScopeRoutes(t *testing.T) { |
| t.Fatalf("Upsert %s: %v", rt.Host, err) |
| } |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if got, _ := os.ReadFile(own); string(got) != "CARLOS_ADMIN_TOKEN=tok\n" { |
| @@ -516,7 +516,7 @@ func TestSyncExecReapsOnlyItsOwnNamespace(t *testing.T) { |
| t.Fatalf("stage %s: %v", name, err) |
| } |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| for name, keep := range files { |
| @@ -564,7 +564,7 @@ func TestSyncExecReapsOnlyItsOwnNamespaceWithAddrShapeInTheMix(t *testing.T) { |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| for name, keep := range files { |
| @@ -592,7 +592,7 @@ func TestSyncExecEmptyRegistryRemovesNothing(t *testing.T) { |
| if err := os.WriteFile(path, []byte("X=1\n"), 0o600); err != nil { |
| t.Fatalf("stage: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if _, err := os.Stat(path); err != nil { |
| @@ -640,7 +640,7 @@ func TestSyncExecOptedInAddrRouteMaterializesFile(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env")) |
| @@ -665,7 +665,7 @@ func TestSyncExecOptedInAddrRouteMaterializesFile(t *testing.T) { |
| t.Fatal(err) |
| } |
| time.Sleep(10 * time.Millisecond) // ensure a rewrite would move mtime |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec (second pass): %v", err) |
| } |
| after, err := os.Stat(path) |
| @@ -697,7 +697,7 @@ func TestSyncExecUnoptedAddrRouteIsSkipped(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if _, err := os.Stat(filepath.Join(dd, "demo.go.tito.io.config.env")); !os.IsNotExist(err) { |
| @@ -741,7 +741,7 @@ func TestSyncExecUnrecognizedExecDeliveryValueIgnoredNotFatal(t *testing.T) { |
| } |
| var syncErr error |
| out := captureStdout(t, func() { |
| - syncErr = SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }) |
| + syncErr = SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, "") |
| }) |
| if syncErr != nil { |
| t.Fatalf("SyncExec: %v", syncErr) |
| @@ -790,7 +790,7 @@ func TestSyncExecOptedInAddrRouteFetchFailureLogsAndLeavesFileAsIs(t *testing.T) |
| } |
| // First pass materializes normally — this is the file a transient |
| // blip must leave exactly as it is. |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec (initial): %v", err) |
| } |
| path := filepath.Join(dd, "demo.go.tito.io.config.env") |
| @@ -805,7 +805,7 @@ func TestSyncExecOptedInAddrRouteFetchFailureLogsAndLeavesFileAsIs(t *testing.T) |
| bounce := func(h string) error { bounced = append(bounced, h); return nil } |
| var syncErr error |
| out := captureStdout(t, func() { |
| - syncErr = SyncExec(ctx, reg, failing, pk, dd, bounce) |
| + syncErr = SyncExec(ctx, reg, failing, pk, dd, bounce, "") |
| }) |
| if syncErr != nil { |
| t.Fatalf("SyncExec (blip): %v", syncErr) |
| @@ -873,7 +873,7 @@ func TestSyncExecOptedInAddrRouteIsNeverBounced(t *testing.T) { |
| bounced = append(bounced, h) |
| return nil |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec (initial write): %v", err) |
| } |
| if len(bounced) != 0 { |
| @@ -884,7 +884,7 @@ func TestSyncExecOptedInAddrRouteIsNeverBounced(t *testing.T) { |
| if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"TITOGO_DEMO_BANNER": "2"}); err != nil { |
| t.Fatalf("SetEnv (change): %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec (changed content): %v", err) |
| } |
| if len(bounced) != 0 { |
| @@ -933,7 +933,7 @@ func TestSyncExecSocketRouteStillBouncesAlongsideOptedInAddrRoute(t *testing.T) |
| bounced = append(bounced, h) |
| return nil |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| if !slices.Contains(bounced, "inst.example.com") { |
| @@ -979,7 +979,7 @@ func TestSyncExecOptedInAddrRouteHonorsBoundEnvironment(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env")) |
| @@ -1018,7 +1018,7 @@ func TestSyncExecOptedInAddrAppWithNoVarsConvergesToEmptyFile(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env")) |
| @@ -1073,7 +1073,7 @@ func TestSyncExecMassRemovalGuardUnaffectedByOptedInAddrAppWithContent(t *testin |
| if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| bounced = append(bounced, h) |
| return nil |
| - }); err != nil { |
| + }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env")) |
| @@ -1128,7 +1128,7 @@ func TestSyncExecOptOutLeavesStaleFileButWarnsEveryPass(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec (opted in): %v", err) |
| } |
| path := filepath.Join(dd, "demo.go.tito.io.config.env") |
| @@ -1144,7 +1144,7 @@ func TestSyncExecOptOutLeavesStaleFileButWarnsEveryPass(t *testing.T) { |
| bounce := func(h string) error { bounced = append(bounced, h); return nil } |
| var syncErr error |
| out := captureStdout(t, func() { |
| - syncErr = SyncExec(ctx, reg, s, pk, dd, bounce) |
| + syncErr = SyncExec(ctx, reg, s, pk, dd, bounce, "") |
| }) |
| if syncErr != nil { |
| t.Fatalf("SyncExec (opted out): %v", syncErr) |
| @@ -1170,7 +1170,7 @@ func TestSyncExecOptOutLeavesStaleFileButWarnsEveryPass(t *testing.T) { |
| // changed must repeat it, since nothing about the risk (stale |
| // plaintext secrets, forever) has gone away. |
| out2 := captureStdout(t, func() { |
| - syncErr = SyncExec(ctx, reg, s, pk, dd, bounce) |
| + syncErr = SyncExec(ctx, reg, s, pk, dd, bounce, "") |
| }) |
| if syncErr != nil { |
| t.Fatalf("SyncExec (opted out, second pass): %v", syncErr) |
| @@ -1205,7 +1205,7 @@ func TestSyncExecNoOptOutWarningWhenNeverOptedIn(t *testing.T) { |
| } |
| var syncErr error |
| out := captureStdout(t, func() { |
| - syncErr = SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }) |
| + syncErr = SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, "") |
| }) |
| if syncErr != nil { |
| t.Fatalf("SyncExec: %v", syncErr) |
| @@ -1245,7 +1245,7 @@ func TestSyncExecAddrFileCarriesHeaderSocketFileDoesNot(t *testing.T) { |
| t.Fatalf("Upsert %s: %v", rt.Host, err) |
| } |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| addrGot, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env")) |
| @@ -1299,7 +1299,7 @@ func TestSyncExecConvergesToEmptyFileWhenAllVarsUnset(t *testing.T) { |
| |
| var bounced []string |
| bounce := func(h string) error { bounced = append(bounced, h); return nil } |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec (set): %v", err) |
| } |
| got, err := os.ReadFile(path) |
| @@ -1316,7 +1316,7 @@ func TestSyncExecConvergesToEmptyFileWhenAllVarsUnset(t *testing.T) { |
| t.Fatalf("Unset (all vars): %v", err) |
| } |
| bounced = nil |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec (unset): %v", err) |
| } |
| got, err = os.ReadFile(path) |
| @@ -1353,7 +1353,7 @@ func TestReapSparesLiveRoutesInstanceEnvWithConfigSuffix(t *testing.T) { |
| t.Fatalf("Upsert: %v", err) |
| } |
| out := captureStdout(t, func() { |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| }) |
| @@ -1416,7 +1416,7 @@ func TestSyncExecRefusesConfigNamespaceCollision(t *testing.T) { |
| var bounced []string |
| bounce := func(h string) error { bounced = append(bounced, h); return nil } |
| out = captureStdout(t, func() { |
| - if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| }) |
| @@ -1487,7 +1487,7 @@ func TestSyncExecFetchFailureLeavesOtherGroupsConverging(t *testing.T) { |
| failing := &failingKeyStore{Store: s, failKey: Key("bab", "appa"), err: blipErr} |
| var bounced []string |
| bounce := func(h string) error { bounced = append(bounced, h); return nil } |
| - if err := SyncExec(ctx, reg, failing, pk, dd, bounce); err != nil { |
| + if err := SyncExec(ctx, reg, failing, pk, dd, bounce, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(stalePath) |
| @@ -1708,7 +1708,7 @@ func TestSyncExecOptInResolvesPerEnvironment(t *testing.T) { |
| t.Fatalf("Upsert %s: %v", rt.Host, err) |
| } |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| assertConfigFile(t, dd, "connect.go.tito.io", tc.wantRelayFile) |
| @@ -1757,7 +1757,7 @@ func TestSyncExecEnvScopedOptInDeliversTheMergedBundle(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| got, err := os.ReadFile(filepath.Join(dd, "connect.go.tito.io"+configEnvSuffix)) |
| @@ -1802,7 +1802,7 @@ func TestSyncExecEnvBundleFetchErrorLeavesFileAlone(t *testing.T) { |
| }); err != nil { |
| t.Fatalf("Upsert: %v", err) |
| } |
| - if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec: %v", err) |
| } |
| path := filepath.Join(dd, "connect.go.tito.io"+configEnvSuffix) |
| @@ -1814,7 +1814,7 @@ func TestSyncExecEnvBundleFetchErrorLeavesFileAlone(t *testing.T) { |
| blip := &failingKeyStore{Store: s, failKey: EnvironmentKey("bab", "titogo", "relay"), |
| err: errors.New("503 SlowDown")} |
| out := captureStdout(t, func() { |
| - if err := SyncExec(ctx, reg, blip, pk, dd, func(string) error { return nil }); err != nil { |
| + if err := SyncExec(ctx, reg, blip, pk, dd, func(string) error { return nil }, ""); err != nil { |
| t.Fatalf("SyncExec (blip): %v", err) |
| } |
| }) |
| @@ -1829,3 +1829,70 @@ func TestSyncExecEnvBundleFetchErrorLeavesFileAlone(t *testing.T) { |
| t.Fatalf("a fetch blip against the bound bundle must say so loudly and name the environment; got %q", out) |
| } |
| } |
| + |
| +// The #344 guard, from the 2026-08-28 incident: the domains reconcile |
| +// mis-stamped a route's account, the config sync keyed the manifest under |
| +// the stamped account, found nothing, and DELETED the tenant's delivered |
| +// config — secrets included — bouncing the instance into a crash loop. An |
| +// absent manifest may remove the file only when the lookup account agrees |
| +// with the account the host itself names; a platform host's <sqid> label |
| +// is not a moving part, the row's account column is. |
| +func TestSyncExecHoldsConfigWhenHostNamesADifferentAccount(t *testing.T) { |
| + ctx, s, reg, dd, priv, _ := execKit(t) |
| + pk, _ := ParsePrivateKey(priv, "test") |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "birthday-alarm.bde.oncarlos.test", Kind: "instance", App: "birthday-alarm", |
| + Account: "bab", // the mis-stamp |
| + Status: "ready", Socket: "/run/carlos/birthday.sock", |
| + }); err != nil { |
| + t.Fatalf("Upsert: %v", err) |
| + } |
| + // A second, healthy group so the mass-removal guard (which needs EVERY |
| + // group absent) stays out of the way — this guard is per-route. |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "inst.example.com", Kind: "instance", App: "eleven", Account: "bab", |
| + Status: "ready", Socket: "/run/carlos/inst.sock", |
| + }); err != nil { |
| + t.Fatalf("Upsert: %v", err) |
| + } |
| + if err := SetEnv(ctx, s, "bab", "eleven", map[string]string{"A": "1"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + path := filepath.Join(dd, "birthday-alarm.bde.oncarlos.test.config.env") |
| + if err := os.WriteFile(path, []byte("BIRTHDAYALARM_INSTANCE_KEY=deadbeef\n"), 0o600); err != nil { |
| + t.Fatalf("stage: %v", err) |
| + } |
| + var bounced []string |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error { |
| + bounced = append(bounced, h) |
| + return nil |
| + }, "oncarlos.test"); err != nil { |
| + t.Fatalf("SyncExec: %v", err) |
| + } |
| + if _, err := os.Stat(path); err != nil { |
| + t.Fatalf("delivered config removed under a mis-stamped account: %v", err) |
| + } |
| + for _, h := range bounced { |
| + if h == "birthday-alarm.bde.oncarlos.test" { |
| + t.Fatalf("held route was bounced: %v", bounced) |
| + } |
| + } |
| + // And the guard steps aside when the row and the host agree: the same |
| + // absent manifest under the RIGHT account is a real deletion. |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "birthday-alarm.bab.oncarlos.test", Kind: "instance", App: "birthday-alarm", |
| + Account: "bab", Status: "ready", Socket: "/run/carlos/birthday2.sock", |
| + }); err != nil { |
| + t.Fatalf("Upsert: %v", err) |
| + } |
| + agreed := filepath.Join(dd, "birthday-alarm.bab.oncarlos.test.config.env") |
| + if err := os.WriteFile(agreed, []byte("OLD=1\n"), 0o600); err != nil { |
| + t.Fatalf("stage: %v", err) |
| + } |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }, "oncarlos.test"); err != nil { |
| + t.Fatalf("SyncExec: %v", err) |
| + } |
| + if _, err := os.Stat(agreed); !os.IsNotExist(err) { |
| + t.Fatalf("agreeing host's stale file survived, stat err = %v", err) |
| + } |
| +} |