diff --git a/go.mod b/go.mod
index 05eec89..63add03 100644
--- a/go.mod
+++ b/go.mod
@@ -3,27 +3,9 @@ module dappco.re/go/core/docs
go 1.26.0
require (
- dappco.re/go/core/html v0.1.8
- dappco.re/go/core/log v0.0.4
- github.com/stretchr/testify v1.11.1
+ dappco.re/go v0.9.0
github.com/yuin/goldmark v1.7.16
gopkg.in/yaml.v3 v3.0.1
)
-require (
- dappco.re/go/core v0.5.0
- dappco.re/go/core/api v0.2.0
- dappco.re/go/core/i18n v0.2.0
- dappco.re/go/core/io v0.2.0
- dappco.re/go/core/log v0.1.0
- dappco.re/go/core/process v0.3.0
- dappco.re/go/core/scm v0.4.0
- dappco.re/go/core/store v0.2.0
- dappco.re/go/core/ws v0.3.0
- dappco.re/go/core v0.3.3 // indirect
- dappco.re/go/core/i18n v0.1.8 // indirect
- dappco.re/go/core/inference v0.1.7 // indirect
- github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- golang.org/x/text v0.35.0 // indirect
-)
+replace dappco.re/go => ../go
diff --git a/go.sum b/go.sum
index 66b27db..30d04fa 100644
--- a/go.sum
+++ b/go.sum
@@ -1,31 +1,6 @@
-forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
-forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
-forge.lthn.ai/core/go-html v0.1.8 h1:yfeM9Gt0Vsq0x6px23QfuptKlSNmHW6LINUcM8QjJ9g=
-forge.lthn.ai/core/go-html v0.1.8/go.mod h1:7iEq9BqIHRxooz0Ks5fCd4SRSZBem20RRP9kmmfAYjc=
-forge.lthn.ai/core/go-i18n v0.1.8 h1:+G/w5KVmu9NUoS//fRUseBKHQO+sNeB+RSlmhlz7OYM=
-forge.lthn.ai/core/go-i18n v0.1.8/go.mod h1:4IdsNILKU7nVSJf8gEjD+Iu2YfqpM0hrRNJFclPJI/k=
-forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q=
-forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
-forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
-forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
-github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/help/ax7_test.go b/pkg/help/ax7_test.go
new file mode 100644
index 0000000..7ddff1c
--- /dev/null
+++ b/pkg/help/ax7_test.go
@@ -0,0 +1,635 @@
+package help
+
+import (
+ . "dappco.re/go"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "slices"
+)
+
+func ax7EmptyCatalog() *Catalog {
+ return &Catalog{
+ topics: make(map[string]*Topic),
+ index: newSearchIndex(),
+ }
+}
+
+func ax7Topic() *Topic {
+ return &Topic{
+ ID: "agent-guide",
+ Title: "Agent Guide",
+ Content: "# Agent Guide\n\nRun the **agent** from the terminal.\n",
+ Tags: []string{"cli", "agent"},
+ Sections: []Section{
+ {ID: "agent-guide", Title: "Agent Guide", Level: 1, Content: "Run the agent."},
+ },
+ Related: []string{"config"},
+ }
+}
+
+func ax7Catalog() *Catalog {
+ c := ax7EmptyCatalog()
+ c.Add(ax7Topic())
+ c.Add(&Topic{ID: "config", Title: "Configuration", Content: "Configure the agent.", Tags: []string{"setup"}})
+ return c
+}
+
+func TestAX7_RenderMarkdown_Good(t *T) {
+ html, err := RenderMarkdown("# Agent\n\nRun the **agent**.")
+ RequireNoError(t, err)
+ AssertContains(t, html, "
Agent
")
+ AssertContains(t, html, "agent")
+}
+
+func TestAX7_RenderMarkdown_Bad(t *T) {
+ html, err := RenderMarkdown("")
+ RequireNoError(t, err)
+ AssertEqual(t, "", html)
+ AssertNotContains(t, html, "")
+}
+
+func TestAX7_RenderMarkdown_Ugly(t *T) {
+ html, err := RenderMarkdown(`
agent
`)
+ RequireNoError(t, err)
+ AssertContains(t, html, `agent
`)
+ AssertNotContains(t, html, "<div")
+}
+
+func TestAX7_Generate_Good(t *T) {
+ dir := t.TempDir()
+ err := Generate(ax7Catalog(), dir)
+ RequireNoError(t, err)
+ _, err = os.Stat(filepath.Join(dir, "index.html"))
+ AssertNoError(t, err)
+}
+
+func TestAX7_Generate_Bad(t *T) {
+ dir := t.TempDir()
+ outputFile := filepath.Join(dir, "site")
+ RequireNoError(t, os.WriteFile(outputFile, []byte("not a directory"), 0o644))
+ err := Generate(ax7Catalog(), outputFile)
+ AssertError(t, err)
+}
+
+func TestAX7_Generate_Ugly(t *T) {
+ dir := t.TempDir()
+ err := Generate(ax7EmptyCatalog(), dir)
+ RequireNoError(t, err)
+ data, err := os.ReadFile(filepath.Join(dir, "search-index.json"))
+ RequireNoError(t, err)
+ AssertContains(t, string(data), "[]")
+}
+
+func TestAX7_NewServer_Good(t *T) {
+ c := ax7Catalog()
+ srv := NewServer(c, ":8080")
+ AssertNotNil(t, srv)
+ AssertEqual(t, c, srv.catalog)
+ AssertNotNil(t, srv.mux)
+}
+
+func TestAX7_NewServer_Bad(t *T) {
+ srv := NewServer(nil, "")
+ AssertNotNil(t, srv)
+ AssertNil(t, srv.catalog)
+ AssertEqual(t, "", srv.addr)
+}
+
+func TestAX7_NewServer_Ugly(t *T) {
+ srv := NewServer(ax7Catalog(), "127.0.0.1:-1")
+ AssertNotNil(t, srv)
+ AssertEqual(t, "127.0.0.1:-1", srv.addr)
+ AssertNotNil(t, srv.mux)
+}
+
+func TestAX7_Server_ServeHTTP_Good(t *T) {
+ srv := NewServer(ax7Catalog(), ":0")
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, req)
+ AssertEqual(t, http.StatusOK, rec.Code)
+}
+
+func TestAX7_Server_ServeHTTP_Bad(t *T) {
+ srv := NewServer(ax7Catalog(), ":0")
+ req := httptest.NewRequest(http.MethodGet, "/topics/missing", nil)
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, req)
+ AssertEqual(t, http.StatusNotFound, rec.Code)
+}
+
+func TestAX7_Server_ServeHTTP_Ugly(t *T) {
+ srv := NewServer(ax7Catalog(), ":0")
+ req := httptest.NewRequest(http.MethodGet, "/api/search", nil)
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, req)
+ AssertEqual(t, http.StatusBadRequest, rec.Code)
+}
+
+func TestAX7_Server_ListenAndServe_Good(t *T) {
+ srv := NewServer(ax7Catalog(), "127.0.0.1:-1")
+ err := srv.ListenAndServe()
+ AssertError(t, err)
+ AssertNotNil(t, srv.mux)
+}
+
+func TestAX7_Server_ListenAndServe_Bad(t *T) {
+ srv := NewServer(ax7Catalog(), "bad addr")
+ err := srv.ListenAndServe()
+ AssertError(t, err)
+ AssertEqual(t, "bad addr", srv.addr)
+}
+
+func TestAX7_Server_ListenAndServe_Ugly(t *T) {
+ var srv *Server
+ AssertPanics(t, func() {
+ _ = srv.ListenAndServe()
+ })
+ AssertNil(t, srv)
+}
+
+func TestAX7_ParseHelpText_Good(t *T) {
+ topic := ParseHelpText("agent run", "Usage:\n agent run [flags]\n\nFlags:\n --fast")
+ AssertEqual(t, "agent-run", topic.ID)
+ AssertContains(t, topic.Content, "## Usage")
+ AssertContains(t, topic.Content, "--fast")
+}
+
+func TestAX7_ParseHelpText_Bad(t *T) {
+ topic := ParseHelpText("empty", "")
+ AssertEqual(t, "empty", topic.ID)
+ AssertEqual(t, "", topic.Content)
+ AssertEqual(t, []string{"cli", "empty"}, topic.Tags)
+}
+
+func TestAX7_ParseHelpText_Ugly(t *T) {
+ topic := ParseHelpText("cli", "Root command.\nSee also: agent run, config")
+ AssertEqual(t, []string{"cli"}, topic.Tags)
+ AssertEqual(t, []string{"agent-run", "config"}, topic.Related)
+ AssertNotContains(t, topic.Content, "See also")
+}
+
+func TestAX7_IngestCLIHelp_Good(t *T) {
+ c := IngestCLIHelp(map[string]string{"agent run": "Run agent.", "agent stop": "Stop agent."})
+ topics := c.List()
+ AssertLen(t, topics, 2)
+ AssertNotEmpty(t, c.Search("agent"))
+}
+
+func TestAX7_IngestCLIHelp_Bad(t *T) {
+ c := IngestCLIHelp(map[string]string{})
+ topics := c.List()
+ AssertEmpty(t, topics)
+ AssertNil(t, c.Search(""))
+}
+
+func TestAX7_IngestCLIHelp_Ugly(t *T) {
+ c := IngestCLIHelp(map[string]string{"cli": "CLI root command."})
+ topic, err := c.Get("cli")
+ RequireNoError(t, err)
+ AssertEqual(t, []string{"cli"}, topic.Tags)
+ AssertEqual(t, "Cli", topic.Title)
+}
+
+func TestAX7_DefaultCatalog_Good(t *T) {
+ c := DefaultCatalog()
+ topics := c.List()
+ AssertGreaterOrEqual(t, len(topics), 2)
+ AssertNotEmpty(t, c.Search("configuration"))
+}
+
+func TestAX7_DefaultCatalog_Bad(t *T) {
+ c := DefaultCatalog()
+ topic, err := c.Get("missing")
+ AssertNil(t, topic)
+ AssertError(t, err, "topic not found")
+}
+
+func TestAX7_DefaultCatalog_Ugly(t *T) {
+ c := DefaultCatalog()
+ list := c.List()
+ list[0] = nil
+ topic, err := c.Get("getting-started")
+ RequireNoError(t, err)
+ AssertEqual(t, "Getting Started", topic.Title)
+}
+
+func TestAX7_Catalog_Add_Good(t *T) {
+ c := ax7EmptyCatalog()
+ topic := ax7Topic()
+ c.Add(topic)
+ got, err := c.Get("agent-guide")
+ RequireNoError(t, err)
+ AssertEqual(t, topic, got)
+}
+
+func TestAX7_Catalog_Add_Bad(t *T) {
+ c := ax7EmptyCatalog()
+ c.Add(&Topic{ID: "agent", Title: "Old"})
+ c.Add(&Topic{ID: "agent", Title: "New"})
+ got, err := c.Get("agent")
+ RequireNoError(t, err)
+ AssertEqual(t, "New", got.Title)
+}
+
+func TestAX7_Catalog_Add_Ugly(t *T) {
+ c := ax7EmptyCatalog()
+ c.Add(&Topic{ID: "", Title: "Empty ID", Content: "edge"})
+ got, err := c.Get("")
+ RequireNoError(t, err)
+ AssertEqual(t, "Empty ID", got.Title)
+}
+
+func TestAX7_Catalog_List_Good(t *T) {
+ c := ax7Catalog()
+ topics := c.List()
+ AssertLen(t, topics, 2)
+ AssertNotNil(t, topics[0])
+}
+
+func TestAX7_Catalog_List_Bad(t *T) {
+ c := ax7EmptyCatalog()
+ topics := c.List()
+ AssertEmpty(t, topics)
+ AssertNotNil(t, c.index)
+}
+
+func TestAX7_Catalog_List_Ugly(t *T) {
+ c := ax7EmptyCatalog()
+ c.Add(&Topic{ID: "dup", Title: "One"})
+ c.Add(&Topic{ID: "dup", Title: "Two"})
+ topics := c.List()
+ AssertLen(t, topics, 1)
+ AssertEqual(t, "Two", topics[0].Title)
+}
+
+func TestAX7_Catalog_All_Good(t *T) {
+ c := ax7Catalog()
+ topics := slices.Collect(c.All())
+ AssertLen(t, topics, 2)
+ AssertNotNil(t, topics[0])
+}
+
+func TestAX7_Catalog_All_Bad(t *T) {
+ c := ax7EmptyCatalog()
+ topics := slices.Collect(c.All())
+ AssertEmpty(t, topics)
+ AssertNotNil(t, c.topics)
+}
+
+func TestAX7_Catalog_All_Ugly(t *T) {
+ c := ax7EmptyCatalog()
+ c.Add(&Topic{ID: "", Title: "Empty"})
+ topics := slices.Collect(c.All())
+ AssertLen(t, topics, 1)
+ AssertEqual(t, "", topics[0].ID)
+}
+
+func TestAX7_Catalog_Search_Good(t *T) {
+ c := ax7Catalog()
+ results := c.Search("agent")
+ AssertNotEmpty(t, results)
+ AssertEqual(t, "agent-guide", results[0].Topic.ID)
+}
+
+func TestAX7_Catalog_Search_Bad(t *T) {
+ c := ax7Catalog()
+ results := c.Search("")
+ AssertNil(t, results)
+ AssertNotNil(t, c.index)
+}
+
+func TestAX7_Catalog_Search_Ugly(t *T) {
+ c := ax7Catalog()
+ results := c.Search("!@#$")
+ AssertEmpty(t, results)
+ AssertNotNil(t, c.topics)
+}
+
+func TestAX7_Catalog_SearchResults_Good(t *T) {
+ c := ax7Catalog()
+ results := slices.Collect(c.SearchResults("agent"))
+ AssertNotEmpty(t, results)
+ AssertEqual(t, "agent-guide", results[0].Topic.ID)
+}
+
+func TestAX7_Catalog_SearchResults_Bad(t *T) {
+ c := ax7Catalog()
+ results := slices.Collect(c.SearchResults(""))
+ AssertEmpty(t, results)
+ AssertNotNil(t, c.index)
+}
+
+func TestAX7_Catalog_SearchResults_Ugly(t *T) {
+ c := ax7Catalog()
+ results := slices.Collect(c.SearchResults("zzzz"))
+ AssertEmpty(t, results)
+ AssertNotNil(t, c.topics)
+}
+
+func TestAX7_LoadContentDir_Good(t *T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "agent.md")
+ RequireNoError(t, os.WriteFile(path, []byte("---\ntitle: Agent\n---\n\n# Agent\n"), 0o644))
+ c, err := LoadContentDir(dir)
+ RequireNoError(t, err)
+ AssertLen(t, c.List(), 1)
+}
+
+func TestAX7_LoadContentDir_Bad(t *T) {
+ c, err := LoadContentDir(filepath.Join(t.TempDir(), "missing"))
+ AssertNil(t, c)
+ AssertError(t, err)
+ AssertContains(t, err.Error(), "walking directory")
+}
+
+func TestAX7_LoadContentDir_Ugly(t *T) {
+ dir := t.TempDir()
+ RequireNoError(t, os.WriteFile(filepath.Join(dir, "skip.txt"), []byte("# Skip\n"), 0o644))
+ RequireNoError(t, os.WriteFile(filepath.Join(dir, "KEEP.MD"), []byte("# Keep\n"), 0o644))
+ c, err := LoadContentDir(dir)
+ RequireNoError(t, err)
+ AssertLen(t, c.List(), 1)
+}
+
+func TestAX7_Catalog_Get_Good(t *T) {
+ c := ax7Catalog()
+ topic, err := c.Get("agent-guide")
+ RequireNoError(t, err)
+ AssertEqual(t, "Agent Guide", topic.Title)
+}
+
+func TestAX7_Catalog_Get_Bad(t *T) {
+ c := ax7Catalog()
+ topic, err := c.Get("missing")
+ AssertNil(t, topic)
+ AssertError(t, err)
+ AssertContains(t, err.Error(), "missing")
+}
+
+func TestAX7_Catalog_Get_Ugly(t *T) {
+ c := ax7EmptyCatalog()
+ c.Add(&Topic{ID: "", Title: "Empty ID"})
+ topic, err := c.Get("")
+ RequireNoError(t, err)
+ AssertEqual(t, "Empty ID", topic.Title)
+}
+
+func TestAX7_ParseTopic_Good(t *T) {
+ topic, err := ParseTopic("agent.md", []byte("---\ntitle: Agent\n---\n\n# Agent\n\nBody"))
+ RequireNoError(t, err)
+ AssertEqual(t, "Agent", topic.Title)
+ AssertEqual(t, "agent", topic.ID)
+}
+
+func TestAX7_ParseTopic_Bad(t *T) {
+ topic, err := ParseTopic("bad.md", []byte("---\n: bad\n---\n# Bad\n"))
+ RequireNoError(t, err)
+ AssertEqual(t, "Bad", topic.Title)
+ AssertContains(t, topic.Content, "---")
+}
+
+func TestAX7_ParseTopic_Ugly(t *T) {
+ topic, err := ParseTopic("empty.md", nil)
+ RequireNoError(t, err)
+ AssertEqual(t, "empty", topic.ID)
+ AssertEmpty(t, topic.Sections)
+}
+
+func TestAX7_ExtractFrontmatter_Good(t *T) {
+ fm, body := ExtractFrontmatter("---\ntitle: Agent\norder: 2\n---\n# Body")
+ RequireNotEmpty(t, body)
+ AssertNotNil(t, fm)
+ AssertEqual(t, "Agent", fm.Title)
+}
+
+func TestAX7_ExtractFrontmatter_Bad(t *T) {
+ content := "---\n: bad\n---\n# Body"
+ fm, body := ExtractFrontmatter(content)
+ AssertNil(t, fm)
+ AssertEqual(t, content, body)
+}
+
+func TestAX7_ExtractFrontmatter_Ugly(t *T) {
+ fm, body := ExtractFrontmatter("---\r\n\r\n---\r\n# Body")
+ AssertNotNil(t, fm)
+ AssertEqual(t, "", fm.Title)
+ AssertContains(t, body, "# Body")
+}
+
+func TestAX7_ExtractSections_Good(t *T) {
+ sections := ExtractSections("# Agent\n\nIntro\n## Run\n\nSteps")
+ AssertLen(t, sections, 2)
+ AssertEqual(t, "agent", sections[0].ID)
+ AssertContains(t, sections[1].Content, "Steps")
+}
+
+func TestAX7_ExtractSections_Bad(t *T) {
+ sections := ExtractSections("No markdown heading here.")
+ AssertEmpty(t, sections)
+ AssertLen(t, sections, 0)
+ AssertFalse(t, len(sections) > 0)
+}
+
+func TestAX7_ExtractSections_Ugly(t *T) {
+ sections := ExtractSections("# One\n## Two\n### Three")
+ AssertLen(t, sections, 3)
+ AssertEqual(t, "", sections[0].Content)
+ AssertEqual(t, "", sections[1].Content)
+}
+
+func TestAX7_AllSections_Good(t *T) {
+ sections := slices.Collect(AllSections("# Agent\n\nIntro\n## Run\n\nSteps"))
+ AssertLen(t, sections, 2)
+ AssertEqual(t, "Agent", sections[0].Title)
+ AssertEqual(t, "Run", sections[1].Title)
+}
+
+func TestAX7_AllSections_Bad(t *T) {
+ sections := slices.Collect(AllSections("plain text"))
+ AssertEmpty(t, sections)
+ AssertLen(t, sections, 0)
+ AssertFalse(t, len(sections) > 0)
+}
+
+func TestAX7_AllSections_Ugly(t *T) {
+ count := 0
+ for section := range AllSections("# One\n## Two") {
+ AssertEqual(t, "One", section.Title)
+ count++
+ break
+ }
+ AssertEqual(t, 1, count)
+}
+
+func TestAX7_GenerateID_Good(t *T) {
+ id := GenerateID("Agent Guide")
+ AssertEqual(t, "agent-guide", id)
+ AssertNotContains(t, id, " ")
+ AssertContains(t, id, "-")
+}
+
+func TestAX7_GenerateID_Bad(t *T) {
+ id := GenerateID("!@#$%^&*()")
+ AssertEqual(t, "", id)
+ AssertEmpty(t, id)
+ AssertNotContains(t, id, "-")
+}
+
+func TestAX7_GenerateID_Ugly(t *T) {
+ id := GenerateID("日本語 Agent 🚀")
+ AssertContains(t, id, "日本語")
+ AssertContains(t, id, "agent")
+ AssertNotContains(t, id, "🚀")
+}
+
+func TestAX7_RenderIndexPage_Good(t *T) {
+ html := RenderIndexPage(ax7Catalog().List())
+ AssertContains(t, html, `role="banner"`)
+ AssertContains(t, html, "Agent Guide")
+ AssertContains(t, html, `role="main"`)
+}
+
+func TestAX7_RenderIndexPage_Bad(t *T) {
+ html := RenderIndexPage(nil)
+ AssertContains(t, html, "No topics available")
+ AssertContains(t, html, `0 topics`)
+ AssertContains(t, html, `role="contentinfo"`)
+}
+
+func TestAX7_RenderIndexPage_Ugly(t *T) {
+ html := RenderIndexPage([]*Topic{{ID: "x", Title: ``, Content: "safe"}})
+ AssertNotContains(t, html, ``,
+ ID: "xss",
+ Title: ``,
Content: "Safe content.",
}
html := RenderIndexPage([]*Topic{topic})
// Title must be escaped
- assert.NotContains(t, html, `