package provision import ( "os" "path/filepath" "strings" "testing" ) // proposeFixture is a small census: one legacy unit, one authority unit, one // envelope, sealed, and a scan that matches it. func proposeFixture(t *testing.T) (rootReadScan, rootReadInventory, string) { t.Helper() legacy := classifiedRootRead{rootReadEntry: rootReadEntry{File: "internal/a/a.go", Unit: "Wake", Kinds: "registry:Host@10,registry:DBPath@20", Digest: strings.Repeat("1", 64)}, Class: "legacy", Task: "accepted-legacy", Basis: "accepted-legacy;rev3=J1;basis=root-route"} auth := classifiedRootRead{rootReadEntry: rootReadEntry{File: "internal/a/b.go", Unit: "Route", Kinds: "registry:Host@5", Digest: strings.Repeat("2", 64)}, Class: "authority", Task: "E4", Basis: "root-route"} fp := rootReadLegacyFingerprint(legacy) inv := rootReadInventory{ Entries: map[string]classifiedRootRead{"internal/a/a.go:Wake": legacy, "internal/a/b.go:Route": auth}, Frozen: map[string]classifiedRootRead{"internal/a/a.go:Wake": legacy}, Legacy: map[string]bool{fp: true}, Coverage: map[string]string{"internal/a": strings.Repeat("c", 64)}, } got := rootReadScan{ Entries: []rootReadEntry{legacy.rootReadEntry, auth.rootReadEntry}, Declarations: map[string]string{"internal/a/a.go:Wake": legacy.Digest, "internal/a/b.go:Route": auth.Digest}, Coverage: map[string]string{"internal/a": strings.Repeat("c", 64)}, } return got, inv, rootReadLegacySeal(inv) } // change edits the scan as a code change would: a new digest, and the // unit's read sites (kinds) as given. func change(got *rootReadScan, key, kinds, digest string) { for i, e := range got.Entries { if e.File+":"+e.Unit == key { got.Entries[i].Kinds, got.Entries[i].Digest = kinds, digest } } got.Declarations[key] = digest } func TestCensusClassifiesEachKindOfChange(t *testing.T) { got, inv, seal := proposeFixture(t) // body: the same reads at moved offsets. change(&got, "internal/a/a.go:Wake", "registry:Host@14,registry:DBPath@24", strings.Repeat("3", 64)) // reads: a kind of read the unit did not make. change(&got, "internal/a/b.go:Route", "registry:Host@5,registry:Socket@9", strings.Repeat("4", 64)) got.Coverage["internal/a"] = strings.Repeat("d", 64) got.Entries = append(got.Entries, rootReadEntry{File: "internal/a/c.go", Unit: "New", Kinds: "file:Open@1", Digest: strings.Repeat("5", 64)}) got.Declarations["internal/a/c.go:New"] = strings.Repeat("5", 64) classes := map[string]string{} for _, c := range classifyRootReads(got, inv, seal) { classes[c.Key] = c.Class } want := map[string]string{ "internal/a/a.go:Wake": censusBody, "internal/a/b.go:Route": censusReads, "internal/a": censusEnvelope, "internal/a/c.go:New": censusNew, } for k, v := range want { if classes[k] != v { t.Errorf("class of %s = %q, want %q (all: %v)", k, classes[k], v, classes) } } if len(classes) != len(want) { t.Errorf("classes = %v, want exactly %v", classes, want) } } // sites: another read of a kind the unit already makes. func TestCensusClassifiesMoreReadsOfAKnownKindAsSites(t *testing.T) { got, inv, seal := proposeFixture(t) change(&got, "internal/a/b.go:Route", "registry:Host@5,registry:Host@9", strings.Repeat("4", 64)) cs := classifyRootReads(got, inv, seal) if len(cs) != 1 || cs[0].Class != censusSites { t.Fatalf("changes = %+v, want one sites", cs) } } // The card never shows less than the census: a discrepancy no unit or // envelope change explains is an "other" decision with the census's own // message (Astra review, census-approvals P2). func TestCensusClassifiesEveryOtherDiscrepancy(t *testing.T) { got, inv, seal := proposeFixture(t) for name, tc := range map[string]struct { edit func(*rootReadScan, *rootReadInventory) string want string }{ "a wrong seal": { edit: func(*rootReadScan, *rootReadInventory) string { return strings.Repeat("e", 64) }, want: "changed legacy freeze", }, "a removed declaration back in the tree": { edit: func(g *rootReadScan, i *rootReadInventory) string { i.Removed = map[string]string{"internal/a/z.go:Gone": "E4;census-ruling-x"} g.Declarations["internal/a/z.go:Gone"] = strings.Repeat("6", 64) return seal }, want: "internal/a/z.go:Gone", }, } { t.Run(name, func(t *testing.T) { g, i := got, cloneRootReadInventory(inv) g.Declarations = cloneMap(got.Declarations) s := tc.edit(&g, &i) cs := classifyRootReads(g, i, s) if len(cs) == 0 { t.Fatal("classified nothing") } for _, c := range cs { if c.Class != censusOther || !strings.Contains(c.Key, tc.want) { t.Fatalf("changes = %+v, want an other carrying %q", cs, tc.want) } } if card := rootReadCard("x", cs, nil, s, ""); strings.Contains(card, "Nothing to approve") || !strings.Contains(card, tc.want) { t.Errorf("card:\n%s", card) } }) } } func TestCensusClassifiesARemovedUnit(t *testing.T) { got, inv, seal := proposeFixture(t) got.Entries = got.Entries[:1] delete(got.Declarations, "internal/a/b.go:Route") cs := classifyRootReads(got, inv, seal) if len(cs) != 1 || cs[0].Class != censusRemoved || cs[0].Key != "internal/a/b.go:Route" { t.Fatalf("changes = %+v, want one removed", cs) } } // The proposal is the whole job: loaded onto the inventory it leaves no // discrepancy, under the seal it reports, and every row names the ruling. func TestCensusProposalClosesEveryApprovableChange(t *testing.T) { got, inv, seal := proposeFixture(t) change(&got, "internal/a/a.go:Wake", "registry:Host@14,registry:DBPath@24", strings.Repeat("3", 64)) change(&got, "internal/a/b.go:Route", "registry:Host@5,registry:Host@9", strings.Repeat("4", 64)) got.Coverage["internal/a"] = strings.Repeat("d", 64) if len(censusDiscrepancies(got, inv, seal)) == 0 { t.Fatal("fixture change made no discrepancy") } claims := map[string]string{ "internal/a/a.go:Wake": "logs the wake's timing; no read added", "internal/a/b.go:Route": "reads the route's host again to log it", } p, err := proposeRootReads(got, inv, seal, "wake-timing", claims) if err != nil { t.Fatal(err) } path := filepath.Join(t.TempDir(), "overlay.tsv") if err := os.WriteFile(path, []byte(p.Overlay), 0o644); err != nil { t.Fatal(err) } after := cloneRootReadInventory(inv) if err := loadRootReadInventory(path, &after, true); err != nil { t.Fatal(err) } if errs := censusDiscrepancies(got, after, p.Seal); len(errs) != 0 { t.Fatalf("after the proposal: %v\n%s", errs, p.Overlay) } if rootReadLegacySeal(inv) != seal { t.Error("proposing changed the inventory it was given") } for _, line := range strings.Split(p.Overlay, "\n") { if line != "" && !strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "@coverage") && !strings.Contains(line, "rev3=census-ruling-wake-timing;basis=") { t.Errorf("row does not name the ruling: %s", line) } } for _, want := range []string{"census-ruling-wake-timing", "internal/a/a.go", "body", "sites", "+registry:Host", "internal/a", seal, p.Seal, "logs the wake's timing", censusReviewQuestion, "Recorded dispositions"} { if !strings.Contains(p.Card, want) { t.Errorf("card lacks %q:\n%s", want, p.Card) } } } func TestCensusProposalRefuses(t *testing.T) { for name, tc := range map[string]struct { edit func(*rootReadScan) claims map[string]string want string }{ "a change without a claim": { edit: func(g *rootReadScan) { change(g, "internal/a/a.go:Wake", "registry:Host@14,registry:DBPath@24", strings.Repeat("3", 64)) }, claims: map[string]string{}, want: "no claim", }, "a claim for no change": { edit: func(g *rootReadScan) { change(g, "internal/a/a.go:Wake", "registry:Host@14,registry:DBPath@24", strings.Repeat("3", 64)) }, claims: map[string]string{"internal/a/a.go:Wake": "x", "internal/a/z.go:Typo": "y"}, want: "not a change", }, "a new kind of read": { edit: func(g *rootReadScan) { change(g, "internal/a/b.go:Route", "registry:Host@5,file:Open@9", strings.Repeat("4", 64)) }, claims: map[string]string{"internal/a/b.go:Route": "opens a file"}, want: "decision", }, "a new unit": { edit: func(g *rootReadScan) { g.Entries = append(g.Entries, rootReadEntry{File: "internal/a/c.go", Unit: "New", Kinds: "file:Open@1", Digest: strings.Repeat("5", 64)}) g.Declarations["internal/a/c.go:New"] = strings.Repeat("5", 64) }, claims: map[string]string{"internal/a/c.go:New": "opens a file"}, want: "decision", }, "a claim with a tab": { edit: func(g *rootReadScan) { change(g, "internal/a/a.go:Wake", "registry:Host@14,registry:DBPath@24", strings.Repeat("3", 64)) }, claims: map[string]string{"internal/a/a.go:Wake": "a\tb"}, want: "tab", }, "nothing to propose": { edit: func(*rootReadScan) {}, claims: map[string]string{}, want: "nothing", }, } { t.Run(name, func(t *testing.T) { got, inv, seal := proposeFixture(t) tc.edit(&got) _, err := proposeRootReads(got, inv, seal, "x", tc.claims) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("err = %v, want one mentioning %q", err, tc.want) } }) } } func TestCensusClaimsFile(t *testing.T) { path := filepath.Join(t.TempDir(), "claims") if err := os.WriteFile(path, []byte("# what changed\n\ninternal/a/a.go:Wake\tlogs the timing; no read added\n"), 0o644); err != nil { t.Fatal(err) } claims, err := readCensusClaims(path) if err != nil || claims["internal/a/a.go:Wake"] != "logs the timing; no read added" || len(claims) != 1 { t.Fatalf("claims = %v, %v", claims, err) } if err := os.WriteFile(path, []byte("internal/a/a.go:Wake no tab\n"), 0o644); err != nil { t.Fatal(err) } if _, err := readCensusClaims(path); err == nil { t.Fatal("a line without a tab was accepted") } } // writeFixtureRoot is a tree with an empty chain manifest and the notes dir. func writeFixtureRoot(t *testing.T) string { t.Helper() root := t.TempDir() for _, d := range []string{"internal/provision/testdata", "docs/superpowers/notes"} { if err := os.MkdirAll(filepath.Join(root, d), 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(root, rootReadChainManifest), []byte("# manifest\n"), 0o644); err != nil { t.Fatal(err) } return root } func readFile(t *testing.T, path string) string { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return string(raw) } func TestCensusProposalWritesAndReplacesItsOwnOverlay(t *testing.T) { root := writeFixtureRoot(t) first := rootReadProposal{Overlay: "# Census overlay x, " + censusProposeMark + ".\nrow1\n", Seal: strings.Repeat("a", 64), Card: "card 1"} if err := writeRootReadProposal(root, "x", first); err != nil { t.Fatal(err) } second := rootReadProposal{Overlay: "# Census overlay x, " + censusProposeMark + ".\nrow2\n", Seal: strings.Repeat("b", 64), Card: "card 2"} if err := writeRootReadProposal(root, "x", second); err != nil { t.Fatal(err) } if got, want := readFile(t, filepath.Join(root, rootReadChainManifest)), "# manifest\nplan14-root-reads-x.tsv\t"+strings.Repeat("b", 64)+"\n"; got != want { t.Errorf("manifest = %q, want %q", got, want) } if got := readFile(t, filepath.Join(root, "internal/provision/testdata/plan14-root-reads-x.tsv")); got != second.Overlay { t.Errorf("overlay = %q", got) } if got := readFile(t, filepath.Join(root, "docs/superpowers/notes/census-proposal-x.md")); got != "card 2" { t.Errorf("card = %q", got) } } func TestCensusProposalRefusesWhatItDidNotWrite(t *testing.T) { for name, setup := range map[string]func(t *testing.T, root string){ "a hand-written overlay of that name": func(t *testing.T, root string) { if err := os.WriteFile(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-x.tsv"), []byte("# Census overlay x (by hand)\n"), 0o644); err != nil { t.Fatal(err) } }, "its overlay followed by another": func(t *testing.T, root string) { mine := rootReadProposal{Overlay: "# " + censusProposeMark + "\n", Seal: strings.Repeat("a", 64)} if err := writeRootReadProposal(root, "x", mine); err != nil { t.Fatal(err) } if err := writeRootReadProposal(root, "y", mine); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { root := writeFixtureRoot(t) setup(t, root) manifest := readFile(t, filepath.Join(root, rootReadChainManifest)) overlay, _ := os.ReadFile(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-x.tsv")) card, _ := os.ReadFile(filepath.Join(root, "docs/superpowers/notes/census-proposal-x.md")) p := rootReadProposal{Overlay: "# " + censusProposeMark + "\nnew\n", Seal: strings.Repeat("c", 64), Card: "card"} if err := writeRootReadProposal(root, "x", p); err == nil { t.Fatal("replaced an overlay it should not have") } if got := readFile(t, filepath.Join(root, rootReadChainManifest)); got != manifest { t.Errorf("manifest changed: %q", got) } if got, _ := os.ReadFile(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-x.tsv")); string(got) != string(overlay) { t.Errorf("overlay changed: %q", got) } if got, _ := os.ReadFile(filepath.Join(root, "docs/superpowers/notes/census-proposal-x.md")); string(got) != string(card) { t.Errorf("card changed: %q", got) } }) } } // Keys may contain spaces, and a directory is a prefix of its units' keys. func TestCensusNamesWholeKeys(t *testing.T) { for _, tc := range []struct { msg, key string want bool }{ {"changed root read internal/h/k.go:type gen; review class", "internal/h/k.go:type gen", true}, {"changed legacy declaration internal/h/k.go:type gen", "internal/h/k.go:type gen", true}, {"changed root read internal/h/k.go:type general; review", "internal/h/k.go:type gen", false}, {"changed root read internal/a/x.go:Foo; review", "internal/a", false}, {"changed root source envelope internal/a", "internal/a", true}, {"stale root read internal/a/x.go:Foo", "internal/a/x.go:Foo", true}, } { if got := censusNames(tc.msg, tc.key); got != tc.want { t.Errorf("censusNames(%q, %q) = %v, want %v", tc.msg, tc.key, got, tc.want) } } } // A body change to a unit whose key has a space is still an approval. func TestCensusClassifiesAKeyWithASpace(t *testing.T) { got, inv, seal := proposeFixture(t) key := "internal/a/t.go:type gen" e := classifiedRootRead{rootReadEntry: rootReadEntry{File: "internal/a/t.go", Unit: "type gen", Kinds: "registry:Host@3", Digest: strings.Repeat("7", 64)}, Class: "legacy", Task: "accepted-legacy", Basis: "accepted-legacy;rev3=J1;basis=root-route"} inv.Entries[key], inv.Frozen[key] = e, e inv.Legacy[rootReadLegacyFingerprint(e)] = true seal = rootReadLegacySeal(inv) got.Entries = append(got.Entries, e.rootReadEntry) got.Declarations[key] = e.Digest change(&got, key, "registry:Host@4", strings.Repeat("8", 64)) cs := classifyRootReads(got, inv, seal) if len(cs) != 1 || cs[0].Class != censusBody || cs[0].Key != key { t.Fatalf("changes = %+v, want one body for %q", cs, key) } } // An envelope-only card still asks the reviewer for a verdict: a re-pin // is how the census sees reads it cannot attribute to a unit (Astra // review, census-astra-approvals P1). func TestCensusCardReviewsAnEnvelopeAlone(t *testing.T) { cs := []censusChange{{Key: "internal/a", Class: censusEnvelope, OldHash: strings.Repeat("1", 64), NewHash: strings.Repeat("2", 64)}} card := rootReadCard("x", cs, nil, strings.Repeat("3", 64), "") if !strings.Contains(card, censusReviewQuestion) || !strings.Contains(card, "`internal/a`") { t.Errorf("card:\n%s", card) } }