package site import ( "os" "path/filepath" "strings" "testing" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/content" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/search" ) // newSite loads the repository's own content. If this fails, every other test // here would be testing nothing. func newSite(t *testing.T) *Site { t.Helper() root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } s, err := New(Config{Root: root, OutDir: filepath.Join(t.TempDir(), "dist")}) if err != nil { t.Fatalf("load site: %v", err) } return s } // TestSixAreasHaveLandingPages is the DoD line "L1 (6) and L2 (6) pages // complete": every area has a published level 1 page and at least one level 2 // page, and every area's L1 fragment is the default stream. func TestSixAreasHaveLandingPages(t *testing.T) { s := newSite(t) if len(content.Areas) != 6 { t.Fatalf("%d areas, want 6", len(content.Areas)) } if got := len(s.StreamKeys()); got != 6 { t.Fatalf("stream keys = %d, want the six area landing fragments", got) } for i, a := range content.Areas { l1, ok := s.Content.AreaLanding(a.Key) if !ok { t.Errorf("%s: no level 1 page", a.Key) continue } if !l1.Public { t.Errorf("%s: level 1 page %s is not public", a.Key, l1.ID) } if f, ok := s.Fragment(s.StreamKeys()[i]); !ok { t.Errorf("%s: stream key %s is not a fragment", a.Key, s.StreamKeys()[i]) } else if f.DocID != l1.ID { t.Errorf("%s: stream fragment %s belongs to %s, want %s", a.Key, f.ID, f.DocID, l1.ID) } levels := map[int]int{} for _, d := range s.PublicInArea(a.Key) { levels[d.Level]++ } if levels[2] == 0 { t.Errorf("%s: no level 2 pages", a.Key) } if levels[3] == 0 { t.Errorf("%s: no level 3 pages", a.Key) } } } // TestEveryArtefactIsReachableWithinThreeClicks is acceptance test C4 against // the view models: landing → area → artefact page is three clicks, and every // artefact has a working link. func TestEveryArtefactIsReachableWithinThreeClicks(t *testing.T) { s := newSite(t) for _, area := range content.Areas { l1, ok := s.Content.AreaLanding(area.Key) if !ok { continue } linkedFromL1 := map[string]bool{} for _, l := range s.Doc(l1).SubPages { linkedFromL1[l.Href] = true } for _, l := range s.Doc(l1).Evidence { linkedFromL1[l.Href] = true } for _, d := range s.PublicInArea(area.Key) { if d.Level == 1 { continue } if d.Level == 2 { if !linkedFromL1[d.URL] { t.Errorf("%s/level 2 page %s is not linked from the area landing page", area.Key, d.URL) } continue } // Level 3: linked from an L2 page's drill-down or evidence table. reachable := false for _, l2 := range s.PublicInArea(area.Key) { if l2.Level != 2 { continue } view := s.Doc(l2) for _, l := range view.SubPages { if l.Href == d.URL { reachable = true } } for _, l := range view.Evidence { if l.Href == d.URL { reachable = true } } } if !reachable { t.Errorf("%s/level 3 page %s is not linked from any level 2 page", area.Key, d.URL) } } } } // TestEveryArtefactHasAStageFragment keeps the panel honest: if an evidence row // offers hx-get, the fragment it names must exist. func TestEveryArtefactHasAStageFragment(t *testing.T) { s := newSite(t) for _, d := range s.PublicDocs() { for _, l := range s.evidence(d) { if l.HXGet == "" { t.Errorf("%s: evidence %q has no stage fragment", d.ID, l.Label) continue } id := strings.TrimPrefix(l.HXGet, "/stage/") if _, ok := s.Fragment(id); !ok { t.Errorf("%s: evidence %q points at missing fragment %q", d.ID, l.Label, id) } } } } // TestBuildWritesEveryPublishedPage runs the real build once and checks the // files the server depends on. func TestBuildWritesEveryPublishedPage(t *testing.T) { s := newSite(t) if err := s.Build(); err != nil { t.Fatalf("Build: %v", err) } for _, url := range s.PublishedPages() { file := s.filePathForURL(url) if !strings.HasPrefix(file, s.Config.OutDir) { t.Errorf("%s: output escapes the build directory", url) } if _, err := os.ReadFile(file); err != nil { t.Errorf("%s: %v", url, err) } } for _, name := range []string{IndexFile, ManifestFile, BuildInfoFile, "static/site.css", "static/htmx.min.js", "static/chroma.css"} { if _, err := os.ReadFile(filepath.Join(s.Config.OutDir, filepath.FromSlash(name))); err != nil { t.Errorf("build output %s: %v", name, err) } } info, err := LoadBuildInfo(s.Config.OutDir) if err != nil { t.Fatalf("build info: %v", err) } if info.Pages == 0 || info.Fragments == 0 || info.Indexed == 0 { t.Errorf("build info is empty: %+v", info) } // The manifest order is the server's default stream: six document // fragments first, and they are the six area landings. if len(s.Manifest.Entries) < 6 { t.Fatalf("manifest has %d entries", len(s.Manifest.Entries)) } for _, e := range s.Manifest.Entries[:6] { if !strings.HasPrefix(e.ID, "doc-") { t.Errorf("manifest entry %s is not a document fragment; the default stream must be the six areas", e.ID) } } } // copyTree copies a directory tree. The freshness test needs its own content // tree so it can change a page without touching the repository. func copyTree(src, dst string) error { return filepath.WalkDir(src, func(p string, d os.DirEntry, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, p) if err != nil { return err } target := filepath.Join(dst, rel) if d.IsDir() { return os.MkdirAll(target, 0o755) } raw, err := os.ReadFile(p) if err != nil { return err } return os.WriteFile(target, raw, 0o644) }) } // TestSearchIndexFollowsContent is the AUD-151 freshness line: a content change // followed by a build is reflected in search. The deploy path is exactly this — // deploy/deploy.sh runs `make dist` before it stages a release, and the build // rewrites dist/search.db from content/ every time (search.Build removes the old // file), so search can never serve an index from a previous content tree. func TestSearchIndexFollowsContent(t *testing.T) { root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } dir := t.TempDir() contentDir := filepath.Join(dir, "content") if err := copyTree(filepath.Join(root, "content"), contentDir); err != nil { t.Fatalf("copy content: %v", err) } out := filepath.Join(dir, "dist") build := func() { t.Helper() s, err := New(Config{Root: root, ContentDir: contentDir, OutDir: out}) if err != nil { t.Fatalf("load site: %v", err) } if err := s.Build(); err != nil { t.Fatalf("build: %v", err) } } query := func(q string) []search.Result { t.Helper() idx, err := search.Open(filepath.Join(out, IndexFile)) if err != nil { t.Fatalf("open index: %v", err) } defer idx.Close() results, err := idx.Query(q, 20) if err != nil { t.Fatalf("query %q: %v", q, err) } return results } const token = "kumquatprotocol" build() if got := query(token); len(got) != 0 { t.Fatalf("token is already indexed in %d results; the test proves nothing", len(got)) } page := filepath.Join(contentDir, "edge", "edge-fleet.md") raw, err := os.ReadFile(page) if err != nil { t.Fatal(err) } added := string(raw) + "\n\nA " + token + " harness keeps the nodes in step.\n" if err := os.WriteFile(page, []byte(added), 0o644); err != nil { t.Fatal(err) } build() results := query(token) if len(results) == 0 { t.Fatal("content change is not in the rebuilt index") } if results[0].URL != "/edge/edge-fleet/" { t.Errorf("token found at %s, want /edge/edge-fleet/", results[0].URL) } } // TestImagesReserveTheirSpace is the layout-shift guard: every image a content // page carries must declare its intrinsic size and load lazily, so a page of // diagrams and screenshots never reflows as the files arrive. func TestImagesReserveTheirSpace(t *testing.T) { s := newSite(t) seen := 0 for _, d := range s.Content.Docs { html := d.HTML for i := 0; ; i++ { start := strings.Index(html, "") if end < 0 { break } tag := html[start : start+end+1] html = html[start+end+1:] seen++ for _, want := range []string{`width="`, `height="`, `loading="lazy"`, `alt="`} { if !strings.Contains(tag, want) { t.Errorf("%s: image tag missing %s: %s", d.Path, want, tag) } } } } if seen == 0 { t.Fatal("no images found in content; the guard is not exercising anything") } }