Give hq a Go module and the checks' shared reading, so the four checks can become Go tests (issue 1296)

This commit is contained in:
2026-10-23 15:06:25 +02:00
parent fa7629d02b
commit dd45f31307
4 changed files with 197 additions and 2 deletions
+39
View File
@@ -0,0 +1,39 @@
package checks
import (
"reflect"
"testing"
"testing/fstest"
)
func TestMarkdownFilesSkipsToolFolders(t *testing.T) {
fsys := fstest.MapFS{
"a.md": {Data: []byte("x")},
"b/c.md": {Data: []byte("x")},
"b/c.txt": {Data: []byte("x")},
".git/d.md": {Data: []byte("x")},
".claude/e.md": {Data: []byte("x")},
".work/f/g.md": {Data: []byte("x")},
}
got := markdownFiles(fsys)
want := []string{"a.md", "b/c.md"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("markdownFiles = %q, want %q", got, want)
}
}
func TestFrontmatterReadsScalarsAndLists(t *testing.T) {
f := frontmatter("---\nlayer: to-be\ndecisions:\n - ADR 0001\n - ADR 0002\ncode: []\ncodes: [a, b]\n---\nbody\n")
if f.scalar("layer") != "to-be" {
t.Errorf("layer = %q", f.scalar("layer"))
}
if !reflect.DeepEqual(f.list("decisions"), []string{"ADR 0001", "ADR 0002"}) {
t.Errorf("decisions = %q", f.list("decisions"))
}
if len(f.list("code")) != 0 || !reflect.DeepEqual(f.list("codes"), []string{"a", "b"}) {
t.Errorf("code = %q, codes = %q", f.list("code"), f.list("codes"))
}
if frontmatter("no frontmatter") != nil || frontmatter("---\nunclosed") != nil {
t.Error("a document without a closed block has no frontmatter")
}
}
+151
View File
@@ -0,0 +1,151 @@
// Package checks holds hq's four checks as Go tests (ADR 0527): records, index, cycle and words.
//
// The package has tests only. Each check is a function over an fs.FS that returns its problems, so
// the test of the real tree reads the repository's root, and the test of the check itself reads a
// deliberately broken tree built in memory and must see the problem named (every check here failed on
// a broken fixture before it passed). The shared reading of Markdown and frontmatter is in this file.
package checks
import (
"io/fs"
"os"
"regexp"
"sort"
"strings"
"testing"
)
// rootFS is hq's tree: the checks run from 00-META/checks, two folders under the root.
func rootFS() fs.FS { return os.DirFS("../..") }
// skipDirs are folders no check reads: git's, the agent tooling's and a session's scratch folder.
var skipDirs = map[string]bool{".git": true, ".claude": true, ".work": true}
// markdownFiles lists every Markdown file of the tree, sorted, in slash paths from the root.
func markdownFiles(fsys fs.FS) []string {
var out []string
_ = fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
if p != "." && skipDirs[d.Name()] {
return fs.SkipDir
}
return nil
}
if strings.HasSuffix(p, ".md") {
out = append(out, p)
}
return nil
})
sort.Strings(out)
return out
}
func readFile(fsys fs.FS, p string) string {
b, err := fs.ReadFile(fsys, p)
if err != nil {
return ""
}
return string(b)
}
// Front is a document's frontmatter: a scalar field holds a string, a list field a []string.
type Front map[string]any
func (f Front) scalar(key string) string {
s, _ := f[key].(string)
return s
}
func (f Front) list(key string) []string {
switch v := f[key].(type) {
case []string:
return v
case string:
if strings.TrimSpace(v) != "" {
return []string{v}
}
}
return nil
}
var (
fmItem = regexp.MustCompile(`^\s+-\s*(.+?)\s*$`)
fmPair = regexp.MustCompile(`^([A-Za-z_-]+):\s*(.*)$`)
)
// frontmatter reads the block between the first two --- lines. It is not a YAML parser on purpose: a
// dependency to read a few scalar fields and one list would cost more than it returns. It returns nil
// when the document has no frontmatter.
func frontmatter(text string) Front {
if !strings.HasPrefix(text, "---\n") {
return nil
}
end := strings.Index(text[4:], "\n---")
if end == -1 {
return nil
}
out := Front{}
key := ""
for _, line := range strings.Split(text[4:4+end], "\n") {
if m := fmItem.FindStringSubmatch(line); m != nil && key != "" {
list, _ := out[key].([]string)
out[key] = append(list, m[1])
continue
}
m := fmPair.FindStringSubmatch(line)
if m == nil {
continue
}
key = m[1]
value := strings.TrimSpace(m[2])
switch {
case strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]"):
list := []string{}
for _, v := range strings.Split(value[1:len(value)-1], ",") {
if v = strings.TrimSpace(v); v != "" {
list = append(list, v)
}
}
out[key] = list
case value == "":
out[key] = []string{}
default:
out[key] = value
}
}
return out
}
func unquote(v string) string {
v = strings.TrimSpace(v)
if len(v) >= 2 && v[0] == v[len(v)-1] && (v[0] == '"' || v[0] == '\'') {
return v[1 : len(v)-1]
}
return v
}
// lineOf is the 1-based line of byte offset i in text.
func lineOf(text string, i int) int { return strings.Count(text[:i], "\n") + 1 }
// expectNone fails the test with every problem a check found in the real tree.
func expectNone(t *testing.T, problems []string) {
t.Helper()
for _, p := range problems {
t.Error(p)
}
}
// expectProblem fails unless some problem contains want: the broken fixture must be caught, by name.
func expectProblem(t *testing.T, problems []string, want string) {
t.Helper()
for _, p := range problems {
if strings.Contains(p, want) {
return
}
}
t.Errorf("no problem mentions %q; got %q", want, problems)
}
+4 -2
View File
@@ -54,8 +54,10 @@ in running prose; a quotation keeps its words.
## Ground rules
- **Markdown only** — but for the checks in `00-META/checks/` and `merge-check.sh` at the root, which
runs them on every pull request as its `mesh/repo-check` (ADR 0238), and
- **Markdown only** — but for the four checks, Go tests in the package `00-META/checks/` with `go.mod` at the
root and no other Go code (ADR 0527), which the checker runs on every pull request as `mesh/check`;
until they replace them, the Python checks beside them and `merge-check.sh` at the root keep running as
`mesh/repo-check` (ADR 0238); and
`00-META/process/playbooks.html`, a drawn overview of the playbooks kept for a later docs site (the operator,
2026-10-19); a change to a playbook's steps updates it in the same pull request. Nothing checks that
yet: issue 1222 asks for the check.
+3
View File
@@ -0,0 +1,3 @@
module github.com/novox/hq
go 1.26.0