Run each checks/<x> over the real tree as a Go test, the shrink-only lists against the trunk's copy; the words check within the unit bound (issue 1293, E5a)

This commit is contained in:
2026-10-23 15:28:39 +02:00
parent 643de08882
commit df9e3bd7e2
22 changed files with 435 additions and 8 deletions
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/config
go 1.22
require checks/trunk v0.0.0
replace checks/trunk => ../trunk
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"path/filepath"
"strings"
"testing"
"checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): the list of modules not yet moved gains none
// the trunk's copy at the merge base does not hold. Without a trunk copy the modules in seed are the trunk's.
func TestTheUnmovedListHoldsNoModuleTheTrunksDoesNot(t *testing.T) {
found, err := Check(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
unmoved, err := Unmoved("unmoved")
if err != nil {
t.Fatal(err)
}
base := seed
if path, ok := trunk.Copy(t, "checks/config/unmoved"); ok {
if base, err = Unmoved(path); err != nil {
t.Fatal(err)
}
}
refused, stale, added := Judge(found, unmoved, base)
for _, m := range refused {
t.Errorf("%s says %s and is not on checks/config/unmoved", m, strings.Join(found[m], ", "))
}
for _, m := range stale {
t.Errorf("checks/config/unmoved names %s, which says no old name: take it out", m)
}
for _, m := range added {
t.Errorf("checks/config/unmoved gains %s, which the trunk's copy does not hold: the list only shrinks", m)
}
}
+12
View File
@@ -61,3 +61,15 @@ func TestAModuleThatIsNotTypeScriptIsNotJudged(t *testing.T) {
t.Fatalf("%v %v", got, err)
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): every TypeScript module that names packages
// beyond the SDK commits its lock.
func TestTheCatalogueCommitsTheLocksItNeeds(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
for _, f := range found {
t.Errorf("%s names packages beyond the SDK (%v) and has no package-lock.json: commit one made by the build module's toolchain (novox/hq ADR 0469 §5)", f.Module, f.Packages)
}
}
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/owned
go 1.22
require checks/trunk v0.0.0
replace checks/trunk => ../trunk
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"testing"
"checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): every rule holds, and the system-known list
// holds no line the trunk's copy at the merge base does not. A trunk with none had no list: its first copy.
func TestTheCatalogueKeepsEveryRuleOfOwnedPathsAndTheSystemListOnlyShrinks(t *testing.T) {
var base []string
if path, ok := trunk.Copy(t, "checks/owned/system-known"); ok {
lines, err := readLines(path)
if err != nil {
t.Fatal(err)
}
base = append([]string{}, lines...)
}
found, err := Check("../..", base)
if err != nil {
t.Fatal(err)
}
for _, f := range found {
t.Errorf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8)", f.Module, f.ID, f.Why, f.Rule)
}
}
+7
View File
@@ -114,3 +114,10 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
t.Errorf("got %v", got)
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
func TestTheCoveredModulesNameNoNode(t *testing.T) {
if code := check("../.."); code != 0 {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
}
}
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/paths
go 1.22
require checks/trunk v0.0.0
replace checks/trunk => ../trunk
+50
View File
@@ -0,0 +1,50 @@
package main
import (
"path/filepath"
"testing"
"checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no module names another's state on its machine,
// the known list holds only what still occurs, and neither list gains a line the trunk's copies at the merge base do not hold.
func TestNoModuleNamesAnothersStateAndTheListsOnlyShrink(t *testing.T) {
root := filepath.Join("..", "..")
found, err := Check(root)
if err != nil {
t.Fatal(err)
}
shared, sharedWhy, err := list("shared")
if err != nil {
t.Fatal(err)
}
known, knownWhy, err := list("known")
if err != nil {
t.Fatal(err)
}
refused, gone := Judge(found, shared, known)
for _, f := range refused {
t.Error(f)
}
for _, k := range gone {
t.Errorf("%q is in checks/paths/known and no longer occurs: take it out, so it cannot come back", k)
}
sharedPath, okShared := trunk.Copy(t, "checks/paths/shared")
knownPath, okKnown := trunk.Copy(t, "checks/paths/known")
if !okShared || !okKnown {
t.Log("NOT COMPARED: the trunk's lists were not both found, so additions to them were not judged")
return
}
baseShared, _, err := list(sharedPath)
if err != nil {
t.Fatal(err)
}
baseKnown, _, err := list(knownPath)
if err != nil {
t.Fatal(err)
}
for _, g := range append(Grown("shared", shared, sharedWhy, baseShared), Grown("known", known, knownWhy, baseKnown)...) {
t.Error(g)
}
}
+1
View File
@@ -1,3 +1,4 @@
# Paths every program on a machine reads as the machine's own, not as a message from the module that
# writes them (checks/paths, novox/hq ADR 0260). One per line, with why.
/etc/hosts # every resolver on the machine reads it; the hostname module writes it for all of them
/var/lib/mesh-build-node-cache # allow: the Go build module gives its integration level the node's installed Node toolchains read-only, so the Node build module's tests build offline (issue 1891, ADR 0533)
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/predefined
go 1.22
require checks/trunk v0.0.0
replace checks/trunk => ../trunk
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"path/filepath"
"sort"
"testing"
"checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): predefined.json removes no key a module still
// names, judged against the trunk's copy at the merge base. A trunk with none had no keys; with no trunk to read, nothing can be
// seen removed, and the test says so.
func TestThePredefinedListRemovesNoKeyAModuleStillNames(t *testing.T) {
tree, err := Keys(filepath.Join("..", "..", File))
if err != nil {
t.Fatal(err)
}
path, ok := trunk.Copy(t, File)
if !ok {
t.Log("no trunk copy of " + File + ": no removal can be seen")
return
}
base, err := Keys(path)
if err != nil {
t.Fatal(err)
}
named, err := Named(filepath.Join("..", ".."))
if err != nil {
t.Fatal(err)
}
removed := Removed(base, tree, named)
keys := make([]string, 0, len(removed))
for k := range removed {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
t.Errorf("%q is removed from %s and still named by %v", k, File, removed[k])
}
}
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/secrets
go 1.22
require checks/trunk v0.0.0
replace checks/trunk => ../trunk
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"testing"
"checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): the known list gains no line the trunk's copy
// at the merge base does not hold. Without a trunk copy the lines in seed are the trunk's.
func TestTheKnownListHoldsNoLineTheTrunksDoesNot(t *testing.T) {
known, err := Known("known")
if err != nil {
t.Fatal(err)
}
base := seed
if path, ok := trunk.Copy(t, "checks/secrets/known"); ok {
if base, err = Known(path); err != nil {
t.Fatal(err)
}
}
for _, line := range Added(known, base) {
t.Errorf("checks/secrets/known gains %q, which the trunk's copy does not hold: the list only shrinks — protect the key instead (novox/hq issue 342)", line)
}
}
+4
View File
@@ -1,3 +1,7 @@
module git.novox.be/novox/mesh-catalog/checks/templates
go 1.22
require checks/trunk v0.0.0
replace checks/trunk => ../trunk
+36
View File
@@ -0,0 +1,36 @@
package main
import (
"testing"
"checks/trunk"
)
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): the list of modules keeping file text inline
// gains none the trunk's copy at the merge base does not hold. Without a trunk copy the modules in seed are the trunk's.
func TestTheInlineListHoldsNoModuleTheTrunksDoesNot(t *testing.T) {
found, err := Check("../..")
if err != nil {
t.Fatal(err)
}
listed, err := Listed(listFile)
if err != nil {
t.Fatal(err)
}
base := seed
if path, ok := trunk.Copy(t, "checks/templates/inline"); ok {
if base, err = Listed(path); err != nil {
t.Fatal(err)
}
}
refused, stale, added := Judge(found, listed, base)
for _, m := range refused {
t.Error(Refusal(m, found[m], TemplatesDir("../..", m)))
}
for _, m := range stale {
t.Errorf("checks/templates/inline names %s, which keeps no file text inline: take it out", m)
}
for _, m := range added {
t.Errorf("checks/templates/inline gains %s, which the trunk's copy does not hold: the list only shrinks", m)
}
}
+3
View File
@@ -0,0 +1,3 @@
module checks/trunk
go 1.22
+90
View File
@@ -0,0 +1,90 @@
// Package trunk is where a catalogue check finds the trunk's copy of a list that may only shrink (novox/hq ADR 0529 clause 2):
// the file as the catalogue's base commit holds it, to judge a change's list against.
//
// A check in a merge check finds it in the clone of the change's own repository at its base commit, which the checker places
// beside the change in MESH_CHECK_BESIDE as it places a sibling, under the repository's name. Run by hand, without that
// directory, it reads the file from git at the merge base with origin/<MESH_CHECK_BASE, main when unset>, as the script this
// replaces did. A trunk that holds no such file had none: the list's first copy, which nothing was added to. Where no base
// can be found at all, the comparison is not made, and the test says so in its log: NOT COMPARED, never a pass that looks like one.
package trunk
import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// Repository is the catalogue's name beside a check.
const Repository = "mesh-catalog"
// BesideEnv is where a merge check says the clones beside the one checked are.
const BesideEnv = "MESH_CHECK_BESIDE"
// Copy is the trunk's copy of the file at rel (a path from the catalogue's root), written to a file of the test's own: its
// path, and whether there is one. False with a log line when the trunk holds none or cannot be read; the caller then judges
// the list as a first copy.
func Copy(t testing.TB, rel string) (string, bool) {
t.Helper()
raw, found, how := Read(rel, "../..")
if how != "" {
t.Log(how)
}
if !found {
return "", false
}
path := filepath.Join(t.TempDir(), filepath.Base(rel))
if err := os.WriteFile(path, raw, 0o644); err != nil {
t.Fatal(err)
}
return path, true
}
// Read is the trunk's copy of rel, whether the trunk holds one, and a line to log when the comparison was not made or
// where it was made. root is the catalogue's checkout (for git).
func Read(rel, root string) (content []byte, found bool, say string) {
if beside := os.Getenv(BesideEnv); beside != "" {
dir := filepath.Join(beside, Repository)
if _, err := os.Stat(dir); err == nil {
raw, err := os.ReadFile(filepath.Join(dir, rel))
if errors.Is(err, os.ErrNotExist) {
return nil, false, "the trunk holds no " + rel + " (read from " + dir + "): its first copy"
}
if err != nil {
return nil, false, "NOT COMPARED WITH THE TRUNK: " + err.Error()
}
return raw, true, "judged against the trunk's " + rel + " in " + dir
}
}
base := "origin/" + firstOf(os.Getenv("MESH_CHECK_BASE"), "main")
mergeBase, err := git(root, "merge-base", "HEAD", base)
if err != nil {
return nil, false, "NOT COMPARED WITH THE TRUNK: no merge base with " + base + ", and no clone of the trunk in " + BesideEnv +
"; " + rel + " is held to the list it was first written with"
}
raw, err := git(root, "show", strings.TrimSpace(string(mergeBase))+":"+rel)
if err != nil {
if _, exists := git(root, "cat-file", "-e", strings.TrimSpace(string(mergeBase))+":"+rel); exists != nil {
return nil, false, "the trunk holds no " + rel + " at the merge base: its first copy"
}
return nil, false, "NOT COMPARED WITH THE TRUNK: " + err.Error()
}
return raw, true, "judged against the trunk's " + rel + " at the merge base with " + base
}
func git(dir string, args ...string) ([]byte, error) {
cmd := exec.Command("git", args...)
cmd.Dir = dir
return cmd.Output()
}
func firstOf(s ...string) string {
for _, x := range s {
if x != "" {
return x
}
}
return ""
}
+42
View File
@@ -0,0 +1,42 @@
package trunk
import (
"os"
"path/filepath"
"testing"
)
func TestTheCloneBesideTheCheckIsTheTrunk(t *testing.T) {
beside := t.TempDir()
dir := filepath.Join(beside, Repository, "checks", "x")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "known"), []byte("a\nb\n"), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv(BesideEnv, beside)
raw, found, say := Read("checks/x/known", t.TempDir())
if !found || string(raw) != "a\nb\n" || say == "" {
t.Fatalf("read %q found %v, said %q", raw, found, say)
}
if _, found, say := Read("checks/x/absent", t.TempDir()); found || say == "" {
t.Fatalf("a file the trunk does not hold was found (%v), said %q", found, say)
}
}
func TestWithNoCloneAndNoGitTheComparisonIsSaidNotMade(t *testing.T) {
t.Setenv(BesideEnv, "")
_, found, say := Read("checks/x/known", t.TempDir()) // not a git checkout
if found || say == "" || say[:12] != "NOT COMPARED" {
t.Fatalf("found %v, said %q; wanted NOT COMPARED", found, say)
}
}
func TestACloneBesideWithoutTheCatalogueFallsBackToGit(t *testing.T) {
t.Setenv(BesideEnv, t.TempDir()) // the siblings are there, the catalogue's own base is not
_, found, say := Read("checks/x/known", t.TempDir())
if found || say == "" {
t.Fatalf("found %v, said %q", found, say)
}
}
+3
View File
@@ -58,6 +58,9 @@ func checkLines(lines []retiredLine, texts []text) []string {
for _, l := range lines {
rx := linePattern(l)
for _, t := range texts {
if !strings.Contains(t.s, l.daemon) { // the pattern holds the daemon's name whole: the cheap test first
continue
}
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
line := t.line
if line > 0 {
+28 -8
View File
@@ -43,14 +43,20 @@ func main() {
if len(os.Args) > 1 {
root = os.Args[1]
}
os.Exit(run(root))
}
// run reads every text a module can show under root and answers the command's exit code: 0 clean, 1 a finding, 2 a list or a
// file unreadable. The command prints what it found; a test of the real catalogue reads the code.
func run(root string) int {
words, err := readList(filepath.Join(root, "retired-words"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
return 2
}
if len(words) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-words lists no word — the check would pass on anything")
os.Exit(2)
return 2
}
var texts []text
files := 0
@@ -89,7 +95,7 @@ func main() {
})
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
return 2
}
findings := check(words, texts)
for _, f := range findings {
@@ -97,17 +103,17 @@ func main() {
}
if len(findings) > 0 {
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
os.Exit(1)
return 1
}
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
lines, err := readLines(filepath.Join(root, "retired-lines"))
if err != nil {
fmt.Fprintln(os.Stderr, "words:", err)
os.Exit(2)
return 2
}
if len(lines) == 0 {
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
os.Exit(2)
return 2
}
if findings := checkLines(lines, texts); len(findings) > 0 {
for _, f := range findings {
@@ -115,10 +121,11 @@ func main() {
}
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
len(findings))
os.Exit(1)
return 1
}
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
len(texts), files)
return 0
}
// readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words.
@@ -152,9 +159,22 @@ func pattern(word string) *regexp.Regexp {
func check(words []string, texts []text) []string {
var out []string
// A text can hold a word only if it holds the word's first part, in any case: the regular expression is run on those alone,
// which keeps the check of the whole catalogue within the unit level's bound.
lower := make([]string, len(texts))
for i, t := range texts {
lower[i] = strings.ToLower(t.s)
}
for _, w := range words {
rx := pattern(w)
for _, t := range texts {
first := ""
if parts := strings.Fields(w); len(parts) > 0 {
first = strings.ToLower(parts[0])
}
for i, t := range texts {
if !strings.Contains(lower[i], first) {
continue
}
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
line := t.line
if line > 0 {
+8
View File
@@ -87,3 +87,11 @@ func TestRetiredLinesAreReadAsDaemonAndCommand(t *testing.T) {
t.Fatal("a line with no command was read")
}
}
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no word the glossary retired, and no
// daemon's line, in any text a module's tools can show an agent.
func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) {
if code := run("../.."); code != 0 {
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
}
}
BIN
View File
Binary file not shown.