Files
hq/00-META/checks/cycle_test.go
T

170 lines
6.9 KiB
Go

package checks
import (
"fmt"
"io/fs"
"path"
"sort"
"strings"
"testing"
"testing/fstest"
)
// The development cycle, checked (cycle.py until ADR 0527; ADR 0080). Everything here reads frontmatter only,
// because a status this repository holds lives in frontmatter and nowhere else. The decision records and research
// efforts live in the knowledge base (ADR 0290), whose rules judge them.
var designStatuses = map[string]bool{"proposed": true, "designed": true, "in-progress": true, "implemented": true, "abandoned": true}
// The keys an issue report carried while its status lived here. The issue tracker holds them now (ADR 0275), and
// `issue-tracker.move` enforces the cycle's rules at the write. A report carrying one is a status in two places.
var trackerKeys = []string{"status", "located-in", "fixed-by", "amended-design", "replay", "replay-none", "resolved"}
// checkDesigns: every design parses, carries `layer:` matching its folder and a known `status:`; a to-be design
// names a decision, and once in-progress or implemented names its owning code. As-is designs need no decisions.
func checkDesigns(fsys fs.FS) []string {
var out []string
for _, layer := range []struct{ folder, name string }{{"00-as-is", "as-is"}, {"01-to-be", "to-be"}} {
entries, _ := fs.ReadDir(fsys, "03-DESIGN/"+layer.folder)
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || e.Name() == "README.md" {
continue
}
p := path.Join("03-DESIGN", layer.folder, e.Name())
front := frontmatter(readFile(fsys, p))
if front == nil {
out = append(out, "cycle: "+p+": no frontmatter")
continue
}
if front.scalar("layer") != layer.name {
out = append(out, fmt.Sprintf("cycle: %s: layer is %q; this directory is %s", p, front.scalar("layer"), layer.name))
}
status := front.scalar("status")
if !designStatuses[status] {
var known []string
for s := range designStatuses {
known = append(known, s)
}
sort.Strings(known)
out = append(out, fmt.Sprintf("cycle: %s: status %q is not one of %v", p, status, known))
}
if layer.name == "to-be" {
if len(front.list("decisions")) == 0 {
out = append(out, "cycle: "+p+": names no decisions -- no design without a decision")
}
if (status == "in-progress" || status == "implemented") && len(front.list("code")) == 0 {
out = append(out, fmt.Sprintf("cycle: %s: status %s but code: names no owner -- no development without a design that says where", p, status))
}
}
}
}
return out
}
// checkIssues: no two issue folders share a number (issue 155: the number is how a record is cited), every report
// has frontmatter with `opened:`, and none carries a key the tracker holds.
func checkIssues(fsys fs.FS) []string {
var out []string
entries, _ := fs.ReadDir(fsys, "04-ISSUES")
seen := map[string]string{}
for _, e := range entries {
if !e.IsDir() {
continue
}
number := strings.SplitN(e.Name(), "-", 2)[0]
if number == "" || strings.Trim(number, "0123456789") != "" {
continue
}
if first, ok := seen[number]; ok {
out = append(out, fmt.Sprintf("cycle: 04-ISSUES/%s: is numbered %s, and so is %s -- an issue number is how it is cited, and two records "+
"answering to one means a citation that resolves to whichever the reader opened. Take the next free number across main AND every open pull request", e.Name(), number, first))
} else {
seen[number] = e.Name()
}
}
for _, e := range entries {
p := path.Join("04-ISSUES", e.Name(), "00-report.md")
if !e.IsDir() {
continue
}
if _, err := fs.Stat(fsys, p); err != nil {
continue
}
front := frontmatter(readFile(fsys, p))
if front == nil {
out = append(out, "cycle: "+p+": no frontmatter")
continue
}
if front.scalar("opened") == "" {
out = append(out, "cycle: "+p+": opened: says nothing -- the day the issue was opened, YYYY-MM-DD")
continue
}
var carried []string
for _, k := range trackerKeys {
if _, ok := front[k]; ok {
carried = append(carried, "`"+k+":`")
}
}
if len(carried) > 0 {
out = append(out, fmt.Sprintf("cycle: %s: carries %s, but an issue's status, owner, fix and replay live in the issue tracker (ADR 0275): "+
"move it with `issue-tracker.move`, and keep no copy here", p, strings.Join(carried, ", ")))
}
}
return out
}
func TestCycleDesignsHoldTheChain(t *testing.T) { expectNone(t, checkDesigns(rootFS())) }
func TestCycleIssuesHoldTheChain(t *testing.T) { expectNone(t, checkIssues(rootFS())) }
func design(layer, status, extra string) *fstest.MapFile {
return doc("---\nlayer: " + layer + "\nstatus: " + status + "\n" + extra + "---\nbody\n")
}
func TestCycleDesignsCatchesEachBreak(t *testing.T) {
broken := fstest.MapFS{
"03-DESIGN/00-as-is/a-nofront.md": doc("no frontmatter"),
"03-DESIGN/00-as-is/b-wronglayer.md": design("to-be", "implemented", ""),
"03-DESIGN/00-as-is/c-badstatus.md": design("as-is", "done", ""),
"03-DESIGN/01-to-be/d-nodecision.md": design("to-be", "designed", ""),
"03-DESIGN/01-to-be/e-nocode.md": design("to-be", "in-progress", "decisions: [ADR 0001]\n"),
"03-DESIGN/01-to-be/README.md": doc("not a design"),
}
got := checkDesigns(broken)
expectProblem(t, got, "a-nofront.md: no frontmatter")
expectProblem(t, got, `b-wronglayer.md: layer is "to-be"; this directory is as-is`)
expectProblem(t, got, `c-badstatus.md: status "done" is not one of`)
expectProblem(t, got, "d-nodecision.md: names no decisions")
expectProblem(t, got, "e-nocode.md: status in-progress but code: names no owner")
if len(got) != 5 {
t.Errorf("exactly the five breaks, as-is needs no decisions and a README is no design: %q", got)
}
}
func TestCycleDesignsAcceptsASoundPair(t *testing.T) {
sound := fstest.MapFS{
"03-DESIGN/00-as-is/a.md": design("as-is", "implemented", ""),
"03-DESIGN/01-to-be/b.md": design("to-be", "in-progress", "decisions: [ADR 0001]\ncode: [mesh-sdk]\n"),
}
expectNone(t, checkDesigns(sound))
}
func TestCycleIssuesCatchesEachBreak(t *testing.T) {
report := func(front string) *fstest.MapFile { return doc("---\n" + front + "---\nbody\n") }
broken := fstest.MapFS{
"04-ISSUES/100-a/00-report.md": report("opened: 2026-10-01\n"),
"04-ISSUES/100-b/00-report.md": report("opened: 2026-10-01\n"),
"04-ISSUES/101-noopened/00-report.md": report("title: x\n"),
"04-ISSUES/102-tracker/00-report.md": report("opened: 2026-10-01\nstatus: open\nreplay: none\n"),
"04-ISSUES/103-nofront/00-report.md": doc("plain"),
"04-ISSUES/notes/readme.md": doc("not an issue"),
}
got := checkIssues(broken)
expectProblem(t, got, "04-ISSUES/100-b: is numbered 100, and so is 100-a")
expectProblem(t, got, "101-noopened/00-report.md: opened: says nothing")
expectProblem(t, got, "102-tracker/00-report.md: carries `status:`, `replay:`")
expectProblem(t, got, "103-nofront/00-report.md: no frontmatter")
if len(got) != 4 {
t.Errorf("exactly the four breaks: %q", got)
}
}