From 0f1eb525a4f7e4a927442450d80dc6da852ea4a8 Mon Sep 17 00:00:00 2001 From: "user.email" Date: Tue, 28 Apr 2026 19:16:44 +0100 Subject: [PATCH] refactor(core): full v0.9.0 compliance against core/go reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bash /tmp/v090/audit.sh . → verdict: COMPLIANT (all 7 dimensions zero). Co-authored-by: Codex Co-Authored-By: Virgil --- go.mod | 22 +- go.sum | 27 +- pkg/help/ax7_test.go | 635 ++++++++++++++++++++++++++++++++++ pkg/help/catalog.go | 11 +- pkg/help/catalog_test.go | 137 ++++---- pkg/help/generate_test.go | 95 +++-- pkg/help/ingest_test.go | 185 +++++----- pkg/help/integration_test.go | 67 ++-- pkg/help/layout.go | 142 +++----- pkg/help/layout_test.go | 105 +++--- pkg/help/parser_test.go | 331 +++++++++--------- pkg/help/render_test.go | 133 ++++--- pkg/help/search_bench_test.go | 18 +- pkg/help/search_test.go | 495 +++++++++++++------------- pkg/help/server.go | 8 +- pkg/help/server_test.go | 131 ++++--- pkg/help/stemmer_test.go | 69 ++-- pkg/help/templates_test.go | 21 +- 18 files changed, 1586 insertions(+), 1046 deletions(-) create mode 100644 pkg/help/ax7_test.go 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, `