| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "go/ast" |
| 5 | "go/parser" |
| 6 | "go/token" |
| 7 | "io/fs" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "slices" |
| 11 | "sort" |
| 12 | "strings" |
| 13 | "testing" |
| 14 | ) |
| 15 | |
| 16 | // Ruling 6 for step 8 readiness: nothing changes an effect on a claims-off |
| 17 | // or no-census box. Every new or widened census entry point is reached only |
| 18 | // from claim-mode code. journal-migrate refuses without CARLOS_HOST_CLAIM |
| 19 | // (RunSystemJournalMigrate), the root migration runs only under it, the |
| 20 | // claim assembly and root inventory startup exist only with claims on, and |
| 21 | // the retry directory is prepared only with a census or claim units on. This |
| 22 | // pins the callers, so a new claims-off caller fails here first. |
| 23 | func TestStep8CensusChangesReachOnlyClaimMode(t *testing.T) { |
| 24 | callers := map[string][]string{} |
| 25 | fset := token.NewFileSet() |
| 26 | entries, err := os.ReadDir(".") |
| 27 | if err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | for _, e := range entries { |
| 31 | name := e.Name() |
| 32 | if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { |
| 33 | continue |
| 34 | } |
| 35 | f, err := parser.ParseFile(fset, name, nil, 0) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | for _, decl := range f.Decls { |
| 40 | fn, ok := decl.(*ast.FuncDecl) |
| 41 | if !ok || fn.Body == nil { |
| 42 | continue |
| 43 | } |
| 44 | ast.Inspect(fn.Body, func(n ast.Node) bool { |
| 45 | switch x := n.(type) { |
| 46 | case *ast.CallExpr: |
| 47 | if id, ok := x.Fun.(*ast.Ident); ok { |
| 48 | callers[id.Name] = append(callers[id.Name], fn.Name.Name) |
| 49 | } |
| 50 | case *ast.CompositeLit: |
| 51 | // The collector's registry listing: routes: r.Registry.List. |
| 52 | if typ, ok := x.Type.(*ast.Ident); ok && typ.Name == "rootMigrationCollector" { |
| 53 | for _, el := range x.Elts { |
| 54 | if kv, ok := el.(*ast.KeyValueExpr); ok { |
| 55 | if k, ok := kv.Key.(*ast.Ident); ok && k.Name == "routes" { |
| 56 | callers["collector.routes"] = append(callers["collector.routes"], fn.Name.Name) |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | return true |
| 63 | }) |
| 64 | } |
| 65 | } |
| 66 | want := map[string][]string{ |
| 67 | "claimMigrationSharedPaths": {"migrateRootClaimAuthority"}, |
| 68 | "verifyMigrationWindow": {"migrateRootClaimAuthority", "verifyJournalMigrateWindow"}, |
| 69 | "verifyJournalMigrateWindow": {"RunSystemJournalMigrate"}, |
| 70 | "prepareRootFloorDirs": {"migrateRootClaimAuthority"}, |
| 71 | "prepareRootFloorDirsOwned": {"prepareRootFloorDirs"}, |
| 72 | "repairRootFloorDir": {"prepareRootFloorDirsOwned"}, |
| 73 | "walkNoFollowDir": {"consoleDir", "repairRootFloorDir"}, |
| 74 | "claimBoxDataCensus": {"claimDataCensus"}, |
| 75 | "claimDataCensus": {"claimGlobalSharedPaths"}, |
| 76 | "claimGlobalSharedPaths": {"OpenProductionClaimAssembly", "claimMigrationSharedPaths", "recoverRootInventoryStartup"}, |
| 77 | "migrateRootClaimAuthority": {"migrateClaimJournalsWith"}, |
| 78 | "prepareRootRetryUnitDirectory": {"installActivationControl", "readClaimUnitSettings"}, |
| 79 | "collector.routes": {"migrateRootClaimAuthority"}, |
| 80 | } |
| 81 | for name, w := range want { |
| 82 | got := slices.Compact(sortedCopy(callers[name])) |
| 83 | if !slices.Equal(got, w) { |
| 84 | t.Errorf("%s is called from %v, want %v", name, got, w) |
| 85 | } |
| 86 | } |
| 87 | checkBackfillDoneCallers(t) |
| 88 | checkRosterBindingCallers(t) |
| 89 | } |
| 90 | |
| 91 | // Ruling 6 for roster binding (design-roster-revision-holds.md, task 10): a |
| 92 | // claims-off or no-census box never builds a claim assembly or claim owners, |
| 93 | // so it computes no binding, compares none, and settles nothing. The binding |
| 94 | // is computed only by the claim roster adapter and the cleanup identity, both |
| 95 | // built only by OpenClaimOwners, which only the claim assembly and the claim |
| 96 | // journal migration call. SameBinding is called only at the four claim-mode |
| 97 | // sites; the cold handover's fresh admission and obsolete settlement are |
| 98 | // wired only into root's claim handler. The migration-state and floor seams, |
| 99 | // the obsolete-intent settlement and the stop numeral bound keep their |
| 100 | // callers. The whole module's non-test sources are scanned, so a new caller |
| 101 | // anywhere fails here first. |
| 102 | func checkRosterBindingCallers(t *testing.T) { |
| 103 | t.Helper() |
| 104 | callers := map[string][]string{} |
| 105 | fset := token.NewFileSet() |
| 106 | for _, root := range []string{"../..", "../../cmd", "../../internal"} { |
| 107 | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { |
| 108 | if err != nil { |
| 109 | return err |
| 110 | } |
| 111 | if d.IsDir() { |
| 112 | if path != root && (root == "../.." || strings.HasPrefix(d.Name(), ".") || d.Name() == "testdata" || d.Name() == "vendor") { |
| 113 | return filepath.SkipDir |
| 114 | } |
| 115 | return nil |
| 116 | } |
| 117 | if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { |
| 118 | return nil |
| 119 | } |
| 120 | f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution) |
| 121 | if err != nil { |
| 122 | return err |
| 123 | } |
| 124 | rel, _ := filepath.Rel("../..", filepath.Dir(path)) |
| 125 | for _, decl := range f.Decls { |
| 126 | fn, ok := decl.(*ast.FuncDecl) |
| 127 | if !ok || fn.Body == nil { |
| 128 | continue |
| 129 | } |
| 130 | name := rel + " " + fn.Name.Name |
| 131 | if fn.Recv != nil && len(fn.Recv.List) == 1 { |
| 132 | typ := fn.Recv.List[0].Type |
| 133 | if star, ok := typ.(*ast.StarExpr); ok { |
| 134 | typ = star.X |
| 135 | } |
| 136 | if id, ok := typ.(*ast.Ident); ok { |
| 137 | name = rel + " " + id.Name + "." + fn.Name.Name |
| 138 | } |
| 139 | } |
| 140 | ast.Inspect(fn.Body, func(n ast.Node) bool { |
| 141 | switch x := n.(type) { |
| 142 | case *ast.CallExpr: |
| 143 | callee := "" |
| 144 | switch f := x.Fun.(type) { |
| 145 | case *ast.Ident: |
| 146 | callee = f.Name |
| 147 | case *ast.SelectorExpr: |
| 148 | callee = f.Sel.Name |
| 149 | } |
| 150 | switch callee { |
| 151 | case "BoxBinding", "SameBinding", "verifyClaimCleanupEnrollment", "OpenClaimOwners", "SettleObsoleteColdHandover", "obsoleteIntent", "settleObsoleteIntent", "claimRootHTTP", "rootStopCoordinator", |
| 152 | "openRootMigrationState", "rootMigrationStartupFloors", "SettleObsoleteRootInventory", "boundRosterRevision": |
| 153 | callers[callee] = append(callers[callee], name) |
| 154 | } |
| 155 | case *ast.CompositeLit: |
| 156 | typ := "" |
| 157 | switch f := x.Type.(type) { |
| 158 | case *ast.Ident: |
| 159 | typ = f.Name |
| 160 | case *ast.SelectorExpr: |
| 161 | typ = f.Sel.Name |
| 162 | } |
| 163 | if typ == "productionClaimRoster" { |
| 164 | callers["productionClaimRoster{}"] = append(callers["productionClaimRoster{}"], name) |
| 165 | } |
| 166 | if typ == "ColdHandover" { |
| 167 | for _, el := range x.Elts { |
| 168 | if kv, ok := el.(*ast.KeyValueExpr); ok { |
| 169 | if k, ok := kv.Key.(*ast.Ident); ok && (k.Name == "Admit" || k.Name == "Obsolete") { |
| 170 | callers["ColdHandover{"+k.Name+"}"] = append(callers["ColdHandover{"+k.Name+"}"], name) |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | case *ast.AssignStmt: |
| 176 | for _, lhs := range x.Lhs { |
| 177 | if sel, ok := lhs.(*ast.SelectorExpr); ok && sel.Sel.Name == "Obsolete" { |
| 178 | callers["."+sel.Sel.Name+"="] = append(callers["."+sel.Sel.Name+"="], name) |
| 179 | } |
| 180 | // The seams stand in for root-owned state only in |
| 181 | // tests: a production reassignment would disable |
| 182 | // the migration directory read or the floor check. |
| 183 | if id, ok := lhs.(*ast.Ident); ok && (id.Name == "openRootMigrationState" || id.Name == "rootMigrationStartupFloors") { |
| 184 | callers[id.Name+"="] = append(callers[id.Name+"="], name) |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | return true |
| 189 | }) |
| 190 | } |
| 191 | return nil |
| 192 | }) |
| 193 | if err != nil { |
| 194 | t.Fatal(err) |
| 195 | } |
| 196 | } |
| 197 | want := map[string][]string{ |
| 198 | "BoxBinding": {"internal/host productionClaimRoster.Actor", "internal/host verifyClaimCleanupEnrollment"}, |
| 199 | "productionClaimRoster{}": {"internal/host ProductionClaimOwnerProvider.OpenClaimOwners"}, |
| 200 | "verifyClaimCleanupEnrollment": {"internal/host ProductionClaimOwnerProvider.OpenClaimOwners"}, |
| 201 | "OpenClaimOwners": {"internal/host OpenProductionClaimAssembly", "internal/host migrateClaimJournalsWith"}, |
| 202 | "SameBinding": {"internal/host ClaimOwnerBindings.claimEvidenceCoordinator", "internal/host claimBoundIdentity", "internal/host claimRegionalEnrollment.VerifyRegional", "internal/host servingParkRoot.prepareOrCommitServingReboot"}, |
| 203 | "ColdHandover{Admit}": {"internal/host ClaimRootHTTP.coldHandover"}, |
| 204 | ".Obsolete=": {"internal/host ClaimRootHTTP.RecoverPending"}, |
| 205 | "SettleObsoleteColdHandover": {"internal/hostbox ColdHandover.settleObsolete"}, |
| 206 | "obsoleteIntent": {"internal/host ClaimRootHTTP.RecoverPending", "internal/host ClaimRootHTTP.recoverRootInventory"}, |
| 207 | "settleObsoleteIntent": {"internal/host ClaimRootHTTP.recoverRootInventory"}, |
| 208 | "claimRootHTTP": {"internal/host NewProductionRootClaimStop"}, |
| 209 | "rootStopCoordinator": {"internal/host NewProductionRootClaimStop"}, |
| 210 | // The root-owned migration directory's seam (census ruling (a), |
| 211 | // platform-d8) and fix round 1's floor seam: a third caller fails. |
| 212 | "openRootMigrationState": {"internal/host recoverRootInventoryStartup", "internal/host verifyRootMigrationStartup"}, |
| 213 | "rootMigrationStartupFloors": {"internal/host verifyRootMigrationStartup"}, |
| 214 | // Fix round 1: the obsolete-intent settlement and the numeral bound. |
| 215 | "SettleObsoleteRootInventory": {"internal/host ClaimRootHTTP.settleObsoleteIntent"}, |
| 216 | "boundRosterRevision": {"internal/host ClaimStopCoordinator.FenceAndStop", "internal/host ClaimStopCoordinator.witnessNoProcess"}, |
| 217 | } |
| 218 | for name, w := range want { |
| 219 | got := slices.Compact(sortedCopy(callers[name])) |
| 220 | if !slices.Equal(got, w) { |
| 221 | t.Errorf("%s is called from %v, want %v", name, got, w) |
| 222 | } |
| 223 | } |
| 224 | for _, seam := range []string{"openRootMigrationState=", "rootMigrationStartupFloors="} { |
| 225 | if got := callers[seam]; len(got) != 0 { |
| 226 | t.Errorf("%s is reassigned in production by %v, want nowhere", strings.TrimSuffix(seam, "="), got) |
| 227 | } |
| 228 | } |
| 229 | // Obsolete is set only by the assignment above, never in a literal. (An |
| 230 | // Admit assignment is not pinned: other types' Admit fields share the name.) |
| 231 | if got := callers["ColdHandover{Obsolete}"]; len(got) != 0 { |
| 232 | t.Errorf("ColdHandover{Obsolete} is set in %v, want nowhere", got) |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | // Ruling 6 for backfill-done (claim trial Option B): the new signer phase is |
| 237 | // reached only through carlos system claims run on the trusted signer, and |
| 238 | // its copy of the census's report check is reached only by Done. |
| 239 | // The whole module's non-test sources are scanned, so a call from a box, |
| 240 | // console or agent path fails here first. Backfill.Done is matched as a |
| 241 | // three-argument Done call (ctx, signer, etag): context's Done takes none. |
| 242 | func checkBackfillDoneCallers(t *testing.T) { |
| 243 | t.Helper() |
| 244 | callers := map[string][]string{} |
| 245 | fset := token.NewFileSet() |
| 246 | for _, root := range []string{"../..", "../../cmd", "../../internal"} { |
| 247 | err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { |
| 248 | if err != nil { |
| 249 | return err |
| 250 | } |
| 251 | if d.IsDir() { |
| 252 | if path != root && (root == "../.." || strings.HasPrefix(d.Name(), ".") || d.Name() == "testdata" || d.Name() == "vendor") { |
| 253 | return filepath.SkipDir |
| 254 | } |
| 255 | return nil |
| 256 | } |
| 257 | if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { |
| 258 | return nil |
| 259 | } |
| 260 | f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution) |
| 261 | if err != nil { |
| 262 | return err |
| 263 | } |
| 264 | rel, _ := filepath.Rel("../..", filepath.Dir(path)) |
| 265 | for _, decl := range f.Decls { |
| 266 | fn, ok := decl.(*ast.FuncDecl) |
| 267 | if !ok || fn.Body == nil { |
| 268 | continue |
| 269 | } |
| 270 | name := rel + " " + fn.Name.Name |
| 271 | ast.Inspect(fn.Body, func(n ast.Node) bool { |
| 272 | call, ok := n.(*ast.CallExpr) |
| 273 | if !ok { |
| 274 | return true |
| 275 | } |
| 276 | switch f := call.Fun.(type) { |
| 277 | case *ast.Ident: |
| 278 | if f.Name == "verifyCensusReport" { |
| 279 | callers[f.Name] = append(callers[f.Name], name) |
| 280 | } |
| 281 | case *ast.SelectorExpr: |
| 282 | if f.Sel.Name == "backfillDone" || (f.Sel.Name == "Done" && len(call.Args) == 3) { |
| 283 | callers[f.Sel.Name] = append(callers[f.Sel.Name], name) |
| 284 | } |
| 285 | } |
| 286 | return true |
| 287 | }) |
| 288 | } |
| 289 | return nil |
| 290 | }) |
| 291 | if err != nil { |
| 292 | t.Fatal(err) |
| 293 | } |
| 294 | } |
| 295 | want := map[string][]string{ |
| 296 | "Done": {"internal/claimactivation backfillDone"}, |
| 297 | "backfillDone": {"internal/claimactivation Run"}, |
| 298 | "verifyCensusReport": {"internal/hostops Done"}, |
| 299 | } |
| 300 | for name, w := range want { |
| 301 | got := slices.Compact(sortedCopy(callers[name])) |
| 302 | if !slices.Equal(got, w) { |
| 303 | t.Errorf("%s is called from %v, want %v", name, got, w) |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | func sortedCopy(s []string) []string { |
| 309 | out := append([]string(nil), s...) |
| 310 | sort.Strings(out) |
| 311 | return out |
| 312 | } |
| 313 | |