| index 849a38bc0..27908173f 100644 |
| --- a/internal/host/agent_layout_test.go |
| +++ b/internal/host/agent_layout_test.go |
| @@ -521,13 +521,19 @@ func TestLayoutStatusReportWithNoPassArmedIsUnmanagedByPathShape(t *testing.T) { |
| {Host: "unflagged.example.com", Kind: "instance", DBPath: provision.NestedDBPath("/data", "unflagged.example.com"), Socket: provision.NestedSocket("unflagged.example.com")}, |
| {Host: "flat.example.com", Kind: "instance", Hibernate: true, DBPath: provision.DefaultDBPath("/data", "flat.example.com"), Socket: provision.DefaultSocket("flat.example.com")}, |
| {Host: "external.example.com", Kind: "instance", Socket: provision.DefaultSocket("external.example.com")}, |
| + // Worker rows are judged by path shape like instance rows (sidecar |
| + // isolation D8): a nested worker would be stranded by a downgrade |
| + // exactly as a nested instance would. |
| {Host: "side.example.com", Kind: "sidecar"}, |
| + {Host: "bot.shop.abc123" + provision.SidecarSuffix, Kind: "sidecar", DBPath: provision.NestedDBPath("/data", "bot.shop.abc123"+provision.SidecarSuffix)}, |
| + {Host: "unit.shop.abc123" + provision.SidecarSuffix, Kind: "sidecar", Backing: registry.BackingUnit, DBPath: provision.NestedDBPath("/data", "unit.shop.abc123"+provision.SidecarSuffix)}, |
| {Host: "static.example.com", Kind: "static"}, |
| }) |
| if got == nil || got.Mode != LayoutModeUnmanaged { |
| t.Fatalf("report = %+v, want Mode unmanaged with no pass armed", got) |
| } |
| - want := map[string]string{"nested.example.com": "nested", "unflagged.example.com": "nested", "flat.example.com": "flat", "external.example.com": "external"} |
| + want := map[string]string{"nested.example.com": "nested", "unflagged.example.com": "nested", "flat.example.com": "flat", "external.example.com": "external", |
| + "side.example.com": "flat", "bot.shop.abc123" + provision.SidecarSuffix: "nested"} |
| if len(got.Rows) != len(want) { |
| t.Fatalf("rows = %+v, want %v", got.Rows, want) |
| } |
| @@ -617,12 +623,13 @@ func TestLayoutStatusReportModeFollowsTheLastIntentSeen(t *testing.T) { |
| } |
| |
| // TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes pins R15's row |
| -// selection: an ActivatorManaged row goes through the pass's own Classify |
| -// (proven here by a row shaped to classify as foreign, so the reason comes |
| -// through too), an unflagged socket instance route reports "external", and |
| -// everything else — a sidecar, a static route, a service route, and an |
| -// instance route with no socket at all — carries no row, because the |
| -// report is not the place any of those are decided. |
| +// selection as widened by sidecar isolation D8: an ActivatorManaged row and |
| +// a WorkerManaged row go through the pass's own Classify (proven here by |
| +// rows shaped to classify as foreign, so the reason comes through too), an |
| +// unflagged socket instance route reports "external", and everything else |
| +// — a static route, a service route, and an instance route with no socket |
| +// at all — carries no row, because the report is not the place any of |
| +// those are decided. |
| func TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes(t *testing.T) { |
| resetLayoutGlobals(t) |
| dd, sd := t.TempDir(), t.TempDir() |
| @@ -636,7 +643,8 @@ func TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes(t *testing.T) { |
| Socket: "/elsewhere/managed.sock", DBPath: "/elsewhere/managed.db", Status: "ready"}, |
| // Hibernate=false with a socket: externally supervised. |
| {Host: "external.example.com", Kind: "instance", Hibernate: false, Socket: "/elsewhere/external.sock"}, |
| - // A sidecar, explicitly out of scope (R15, finding S6). |
| + // A sidecar: in scope since D8 reopened R15 and S6; with no |
| + // database path it is foreign, and the reason says so. |
| {Host: "side.example.com", Kind: "sidecar"}, |
| // Not instance-kind at all. |
| {Host: "static.example.com", Kind: "static"}, |
| @@ -653,8 +661,8 @@ func TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes(t *testing.T) { |
| if report.Mode != "nested" { |
| t.Errorf("mode = %q, want nested", report.Mode) |
| } |
| - if len(report.Rows) != 2 { |
| - t.Fatalf("rows = %+v, want exactly managed and external", report.Rows) |
| + if len(report.Rows) != 3 { |
| + t.Fatalf("rows = %+v, want exactly managed, external and the sidecar", report.Rows) |
| } |
| managed := report.Rows["managed.example.com"] |
| if managed.Layout != "foreign" || managed.Reason == "" { |
| @@ -664,7 +672,11 @@ func TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes(t *testing.T) { |
| if external.Layout != "external" || external.Reason != "" { |
| t.Errorf("external row = %+v, want external with no reason", external) |
| } |
| - for _, host := range []string{"side.example.com", "static.example.com", "svc.example.com", "nosock.example.com"} { |
| + side := report.Rows["side.example.com"] |
| + if side.Layout != "foreign" || !strings.Contains(side.Reason, "worker db") { |
| + t.Errorf("sidecar row = %+v, want foreign with Classify's worker reason", side) |
| + } |
| + for _, host := range []string{"static.example.com", "svc.example.com", "nosock.example.com"} { |
| if _, ok := report.Rows[host]; ok { |
| t.Errorf("%s has a row, want none", host) |
| } |
| @@ -1044,3 +1056,85 @@ func TestLayoutLegRetriesWhileTheDefaultIsBlocked(t *testing.T) { |
| t.Fatal("a typo waits on nothing") |
| } |
| } |
| + |
| +// TestLayoutReportListsWorkerRows pins sidecar isolation D8's status rule: |
| +// worker rows are reported through the pass's own Classify with the |
| +// existing per-row words (flat, nested, fenced, foreign), whether or not |
| +// this agent's workers readiness lets the pass move them, so carlos status |
| +// --layout, the console's downgrade guard and the migrate guard all see a |
| +// nested worker. A unit-backed or tunnelled sidecar row is nobody's to |
| +// move and carries no row. |
| +func TestLayoutReportListsWorkerRows(t *testing.T) { |
| + resetLayoutGlobals(t) |
| + dd, sd := t.TempDir(), t.TempDir() |
| + reg := restartTestRegistry(t) |
| + armedLayoutPass.Store(&layout.Pass{Registry: reg, DataDir: dd, SocketDir: sd}) |
| + noteLayoutIntent(release.LayoutModeNested) |
| + flat := "flat.shop.abc123" + provision.SidecarSuffix |
| + nested := "nested.shop.abc123" + provision.SidecarSuffix |
| + fenced := "moving.shop.abc123" + provision.SidecarSuffix |
| + socketed := "odd.shop.abc123" + provision.SidecarSuffix |
| + unit := "unit.shop.abc123" + provision.SidecarSuffix |
| + if err := os.MkdirAll(filepath.Join(dd, "layout"), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(provision.LayoutJournalPath(dd, fenced), []byte("{}"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + routes := []registry.Route{ |
| + {Host: flat, Kind: "sidecar", Hibernate: true, Status: "ready", DBPath: provision.DefaultDBPath(dd, flat)}, |
| + {Host: nested, Kind: "sidecar", Status: "ready", DBPath: provision.NestedDBPath(dd, nested)}, |
| + {Host: fenced, Kind: "sidecar", Status: "ready", DBPath: provision.DefaultDBPath(dd, fenced)}, |
| + {Host: socketed, Kind: "sidecar", Status: "ready", DBPath: provision.NestedDBPath(dd, socketed), Socket: filepath.Join(sd, "x.sock")}, |
| + {Host: unit, Kind: "sidecar", Status: "ready", Backing: registry.BackingUnit, DBPath: provision.NestedDBPath(dd, unit)}, |
| + {Host: "inst.example.com", Kind: "instance", Hibernate: true, Socket: filepath.Join(sd, "inst.example.com.sock"), DBPath: provision.DefaultDBPath(dd, "inst.example.com")}, |
| + } |
| + for _, rt := range routes { |
| + if err := reg.Upsert(rt); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + report := layoutStatusReport(routes) |
| + if report == nil || report.Mode != "nested" { |
| + t.Fatalf("report = %+v", report) |
| + } |
| + want := map[string]string{flat: "flat", nested: "nested", fenced: "fenced", socketed: "foreign", "inst.example.com": "flat"} |
| + if len(report.Rows) != len(want) { |
| + t.Fatalf("rows = %+v, want %v", report.Rows, want) |
| + } |
| + for host, word := range want { |
| + if got := report.Rows[host]; got.Layout != word { |
| + t.Errorf("%s = %+v, want %s", host, got, word) |
| + } |
| + } |
| + if r := report.Rows[fenced]; !strings.Contains(r.Reason, provision.LayoutJournalPath(dd, fenced)) { |
| + t.Errorf("fenced worker reason = %q, want the journal path", r.Reason) |
| + } |
| + if r := report.Rows[socketed]; !strings.Contains(r.Reason, "worker db") { |
| + t.Errorf("socketed sidecar reason = %q, want Classify's worker reason", r.Reason) |
| + } |
| + if _, listed := report.Rows[unit]; listed { |
| + t.Errorf("a unit-backed sidecar row was reported: %+v", report.Rows[unit]) |
| + } |
| +} |
| + |
| +// The pass is wired to the agent-start workers readiness (D11) through the |
| +// same cached accessor the reconciler's mint reads, and to the production |
| +// replicator observation (D9). |
| +func TestNewLayoutPassWiresWorkersReadinessAndReplicator(t *testing.T) { |
| + resetLayoutGlobals(t) |
| + prev := startWorkers.Load() |
| + t.Cleanup(func() { startWorkers.Store(prev) }) |
| + startWorkers.Store(nil) |
| + p := newLayoutPass(restartTestRegistry(t), nil, t.TempDir(), func() error { return nil }, true) |
| + if p.WorkersReady == nil || p.WorkersReady() { |
| + t.Fatalf("WorkersReady with nothing armed = %v, want wired and false", p.WorkersReady != nil && p.WorkersReady()) |
| + } |
| + startWorkers.Store(&WorkersReadiness{ready: true, word: workersNested}) |
| + if !p.WorkersReady() { |
| + t.Fatal("WorkersReady does not read the armed readiness") |
| + } |
| + if p.Replicator == nil { |
| + t.Fatal("Replicator is not wired") |
| + } |
| +} |