Add cmd/docs, the first corestack component: `docs new adr|plan "Title"` writes records from embedded templates, and `docs check` validates them against the public format spec in docs/docs-format.md (timestamp IDs, frontmatter keys and enums, separate decision and implementation status, supersession symmetry, MADR 4 layout with required Consequences, plan sections, and repo-root/durable references), reporting path:line errors. Also adds the module skeleton (go.auvem.com/corestack, BSD-3-Clause, README, AGENTS.md, CLAUDE.md), Gitea CI running tests and docs check, a tag-triggered goreleaser release for darwin/linux binaries, and the accepted ADR adopting the format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
105 lines
3.0 KiB
Go
105 lines
3.0 KiB
Go
package docs
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var now = time.Date(2026, 10, 23, 20, 45, 12, 345_000_000, time.UTC)
|
|
|
|
// TestNewTemplates checks that fresh records fail only on their markers and
|
|
// become valid once those are replaced.
|
|
func TestNewTemplates(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
kind, path string
|
|
markers int
|
|
}{
|
|
{"adr", "docs/adr/20261023T204512345Z-use-x-y.md", 6},
|
|
{"plan", "docs/plans/20261023T204512345Z-use-x-y.md", 3},
|
|
} {
|
|
t.Run(tt.kind, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
path, err := New(root, tt.kind, `Use "X" & Y`, now)
|
|
must(t, err)
|
|
if path != tt.path {
|
|
t.Fatalf("path = %s", path)
|
|
}
|
|
res, err := Check(root)
|
|
must(t, err)
|
|
if len(res.Problems) != tt.markers {
|
|
t.Fatalf("got %v, want %d marker problems", res.Problems, tt.markers)
|
|
}
|
|
for _, p := range res.Problems {
|
|
if !strings.Contains(p.Msg, marker) {
|
|
t.Errorf("not a marker problem: %s", p)
|
|
}
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(root, path))
|
|
must(t, err)
|
|
must(t, os.WriteFile(filepath.Join(root, path), []byte(strings.ReplaceAll(string(data), marker, "x")), 0o644))
|
|
res, err = Check(root)
|
|
must(t, err)
|
|
expect(t, res.Problems, nil)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewCollisions(t *testing.T) {
|
|
root := t.TempDir()
|
|
first, err := New(root, "adr", "Same", now)
|
|
must(t, err)
|
|
second, err := New(root, "adr", "Same", now)
|
|
must(t, err)
|
|
// The same stem in the other directory also advances the timestamp.
|
|
third, err := New(root, "plan", "Same", now)
|
|
must(t, err)
|
|
want := []string{
|
|
"docs/adr/20261023T204512345Z-same.md",
|
|
"docs/adr/20261023T204512346Z-same.md",
|
|
"docs/plans/20261023T204512347Z-same.md",
|
|
}
|
|
for i, got := range []string{first, second, third} {
|
|
if got != want[i] {
|
|
t.Errorf("file %d = %s, want %s", i, got, want[i])
|
|
}
|
|
}
|
|
// Advancing across midnight keeps the date equal to the filename.
|
|
root = t.TempDir()
|
|
_, err = New(root, "adr", "Edge", time.Date(2026, 10, 23, 23, 59, 59, 999_000_000, time.UTC))
|
|
must(t, err)
|
|
got, err := New(root, "adr", "Edge", time.Date(2026, 10, 23, 23, 59, 59, 999_000_000, time.UTC))
|
|
must(t, err)
|
|
if got != "docs/adr/20261024T000000000Z-edge.md" {
|
|
t.Fatalf("got %s", got)
|
|
}
|
|
res, err := Check(root)
|
|
must(t, err)
|
|
for _, p := range res.Problems {
|
|
if !strings.Contains(p.Msg, marker) {
|
|
t.Errorf("unexpected %s", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsBadTitles(t *testing.T) {
|
|
for _, title := range []string{"", " ", "a\nb", "!!!", "Drop C #", "#", "C ##"} {
|
|
if path, err := New(t.TempDir(), "adr", title, now); err == nil {
|
|
t.Errorf("title %q created %s", title, path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSlugify(t *testing.T) {
|
|
for in, want := range map[string]string{"Use X, Y & Z!": "use-x-y-z", " --Hello-- ": "hello", "Ünï": "n"} {
|
|
if got := slugify(in); got != want {
|
|
t.Errorf("slugify(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
if got := slugify(strings.Repeat("ab ", 40)); len(got) != maxSlug || strings.HasSuffix(got, "-") {
|
|
t.Errorf("long slug = %q", got)
|
|
}
|
|
}
|