Skip to content

Commit 68139ab

Browse files
greynewellclaude
andcommitted
test: boost coverage in schema, taxonomy, shards, and find packages
- schema: GenerateRecipeSchema (basic + author/times/nutrition) and GenerateCollectionPageSchema (50% → 87%) - taxonomy: BuildAll (basic, min_entities filter, multi-value, empty), extractValues (single, missing, enrichment), getEnrichmentOverrides (simple, missing, array path, not-array cases) → 51% → 97% - shards/render: RenderAll tests (empty, writes shards, dry-run, unknown file) → 36% → 43% - shards/zip: new zip_test.go covering isShardFile, matchPattern, shouldInclude (basic/dir/ext/shard/minified/large), buildExclusions (no config, custom config, invalid JSON) - find/zip: add TestIsWorktreeClean_NonGitDir to cover previously-uncovered branch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 25dd71a commit 68139ab

5 files changed

Lines changed: 491 additions & 0 deletions

File tree

internal/archdocs/pssg/schema/jsonld_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,103 @@ func TestComputeTotalTime(t *testing.T) {
257257
}
258258
}
259259
}
260+
261+
// ── GenerateRecipeSchema ──────────────────────────────────────────────────────
262+
263+
func TestGenerateRecipeSchema_Basic(t *testing.T) {
264+
g := NewGenerator(
265+
config.SiteConfig{BaseURL: "https://example.com", Name: "My Site"},
266+
config.SchemaConfig{DatePublished: "2024-01-15"},
267+
)
268+
e := &entity.Entity{
269+
Slug: "pasta-carbonara",
270+
Fields: map[string]interface{}{
271+
"title": "Pasta Carbonara",
272+
"description": "A classic Italian pasta dish.",
273+
},
274+
}
275+
schema := g.GenerateRecipeSchema(e, "https://example.com/pasta-carbonara.html")
276+
if schema["@type"] != "Recipe" {
277+
t.Errorf("@type: got %v, want Recipe", schema["@type"])
278+
}
279+
if schema["name"] != "Pasta Carbonara" {
280+
t.Errorf("name: got %v", schema["name"])
281+
}
282+
if schema["url"] != "https://example.com/pasta-carbonara.html" {
283+
t.Errorf("url: got %v", schema["url"])
284+
}
285+
}
286+
287+
func TestGenerateRecipeSchema_WithAuthorAndTimes(t *testing.T) {
288+
g := NewGenerator(
289+
config.SiteConfig{BaseURL: "https://example.com"},
290+
config.SchemaConfig{},
291+
)
292+
e := &entity.Entity{
293+
Slug: "soup",
294+
Fields: map[string]interface{}{
295+
"title": "Tomato Soup",
296+
"author": "Chef Alice",
297+
"prep_time": "PT10M",
298+
"cook_time": "PT30M",
299+
"recipe_category": "Soup",
300+
"cuisine": "Italian",
301+
"servings": float64(4),
302+
"calories": float64(200),
303+
},
304+
}
305+
schema := g.GenerateRecipeSchema(e, "https://example.com/soup.html")
306+
if _, ok := schema["author"]; !ok {
307+
t.Error("schema should have author")
308+
}
309+
if schema["prepTime"] != "PT10M" {
310+
t.Errorf("prepTime: got %v", schema["prepTime"])
311+
}
312+
if schema["cookTime"] != "PT30M" {
313+
t.Errorf("cookTime: got %v", schema["cookTime"])
314+
}
315+
if _, ok := schema["totalTime"]; !ok {
316+
t.Error("schema should have totalTime when both prep and cook are set")
317+
}
318+
if schema["recipeCategory"] != "Soup" {
319+
t.Errorf("recipeCategory: got %v", schema["recipeCategory"])
320+
}
321+
if schema["recipeCuisine"] != "Italian" {
322+
t.Errorf("recipeCuisine: got %v", schema["recipeCuisine"])
323+
}
324+
}
325+
326+
// ── GenerateCollectionPageSchema ──────────────────────────────────────────────
327+
328+
func TestGenerateCollectionPageSchema_Basic(t *testing.T) {
329+
g := NewGenerator(config.SiteConfig{BaseURL: "https://example.com"}, config.SchemaConfig{})
330+
items := []ItemListEntry{
331+
{Name: "Pasta", URL: "https://example.com/pasta.html"},
332+
{Name: "Soup", URL: "https://example.com/soup.html"},
333+
}
334+
schema := g.GenerateCollectionPageSchema("Italian Recipes", "Best Italian food", "https://example.com/italian/", items, "https://example.com/img.png")
335+
if schema["@type"] != "CollectionPage" {
336+
t.Errorf("@type: got %v, want CollectionPage", schema["@type"])
337+
}
338+
if schema["name"] != "Italian Recipes" {
339+
t.Errorf("name: got %v", schema["name"])
340+
}
341+
if schema["image"] != "https://example.com/img.png" {
342+
t.Errorf("image: got %v", schema["image"])
343+
}
344+
main, ok := schema["mainEntity"].(map[string]interface{})
345+
if !ok {
346+
t.Fatal("mainEntity should be a map")
347+
}
348+
if main["numberOfItems"] != 2 {
349+
t.Errorf("numberOfItems: got %v, want 2", main["numberOfItems"])
350+
}
351+
}
352+
353+
func TestGenerateCollectionPageSchema_NoImage(t *testing.T) {
354+
g := NewGenerator(config.SiteConfig{}, config.SchemaConfig{})
355+
schema := g.GenerateCollectionPageSchema("Name", "Desc", "https://example.com/", nil, "")
356+
if _, ok := schema["image"]; ok {
357+
t.Error("should not set image when imageURL is empty")
358+
}
359+
}

internal/archdocs/pssg/taxonomy/taxonomy_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,153 @@ package taxonomy
33
import (
44
"testing"
55

6+
"github.com/supermodeltools/cli/internal/archdocs/pssg/config"
67
"github.com/supermodeltools/cli/internal/archdocs/pssg/entity"
78
)
89

10+
// ── BuildAll / buildOne / extractValues ───────────────────────────────────────
11+
12+
func TestBuildAll_Basic(t *testing.T) {
13+
entities := []*entity.Entity{
14+
{Slug: "pasta", Fields: map[string]interface{}{"cuisine": "Italian"}},
15+
{Slug: "ramen", Fields: map[string]interface{}{"cuisine": "Japanese"}},
16+
{Slug: "sushi", Fields: map[string]interface{}{"cuisine": "Japanese"}},
17+
}
18+
tc := config.TaxonomyConfig{Name: "cuisine", Field: "cuisine", MinEntities: 1}
19+
taxes := BuildAll(entities, []config.TaxonomyConfig{tc}, nil)
20+
21+
if len(taxes) != 1 {
22+
t.Fatalf("expected 1 taxonomy, got %d", len(taxes))
23+
}
24+
tax := taxes[0]
25+
if tax.Name != "cuisine" {
26+
t.Errorf("tax name: got %q, want cuisine", tax.Name)
27+
}
28+
// 2 unique cuisines: Italian (1 entity), Japanese (2 entities)
29+
if len(tax.Entries) != 2 {
30+
t.Errorf("expected 2 entries, got %d", len(tax.Entries))
31+
}
32+
}
33+
34+
func TestBuildAll_MinEntitiesFilter(t *testing.T) {
35+
entities := []*entity.Entity{
36+
{Slug: "pasta", Fields: map[string]interface{}{"cuisine": "Italian"}},
37+
{Slug: "ramen", Fields: map[string]interface{}{"cuisine": "Japanese"}},
38+
{Slug: "sushi", Fields: map[string]interface{}{"cuisine": "Japanese"}},
39+
}
40+
tc := config.TaxonomyConfig{Name: "cuisine", Field: "cuisine", MinEntities: 2}
41+
taxes := BuildAll(entities, []config.TaxonomyConfig{tc}, nil)
42+
43+
// Only Japanese (2 entities) passes the min_entities=2 filter.
44+
if len(taxes[0].Entries) != 1 {
45+
t.Errorf("expected 1 entry (only Japanese), got %d", len(taxes[0].Entries))
46+
}
47+
if taxes[0].Entries[0].Name != "Japanese" {
48+
t.Errorf("expected Japanese, got %q", taxes[0].Entries[0].Name)
49+
}
50+
}
51+
52+
func TestBuildAll_MultiValue(t *testing.T) {
53+
entities := []*entity.Entity{
54+
{Slug: "pasta", Fields: map[string]interface{}{"tags": []string{"italian", "pasta"}}},
55+
{Slug: "pizza", Fields: map[string]interface{}{"tags": []string{"italian", "baked"}}},
56+
}
57+
tc := config.TaxonomyConfig{Name: "tags", Field: "tags", MultiValue: true, MinEntities: 1}
58+
taxes := BuildAll(entities, []config.TaxonomyConfig{tc}, nil)
59+
60+
// Should have 3 unique tags: italian (2), pasta (1), baked (1)
61+
if len(taxes[0].Entries) != 3 {
62+
t.Errorf("multi-value: expected 3 entries, got %d: %v", len(taxes[0].Entries), taxes[0].Entries)
63+
}
64+
}
65+
66+
func TestBuildAll_Empty(t *testing.T) {
67+
taxes := BuildAll(nil, nil, nil)
68+
if taxes != nil {
69+
t.Errorf("nil input: want nil, got %v", taxes)
70+
}
71+
}
72+
73+
func TestExtractValues_SingleValue(t *testing.T) {
74+
e := &entity.Entity{Fields: map[string]interface{}{"cuisine": "Italian"}}
75+
tc := config.TaxonomyConfig{Field: "cuisine"}
76+
got := extractValues(e, tc, nil)
77+
if len(got) != 1 || got[0] != "Italian" {
78+
t.Errorf("single value: got %v, want [Italian]", got)
79+
}
80+
}
81+
82+
func TestExtractValues_Missing(t *testing.T) {
83+
e := &entity.Entity{Fields: map[string]interface{}{}}
84+
tc := config.TaxonomyConfig{Field: "cuisine"}
85+
if got := extractValues(e, tc, nil); got != nil {
86+
t.Errorf("missing field: want nil, got %v", got)
87+
}
88+
}
89+
90+
func TestExtractValues_EnrichmentOverride(t *testing.T) {
91+
e := &entity.Entity{
92+
Slug: "pasta",
93+
Fields: map[string]interface{}{"cuisine": "Italian"},
94+
}
95+
tc := config.TaxonomyConfig{
96+
Field: "cuisine",
97+
EnrichmentOverrideField: "override_cuisine",
98+
}
99+
enrichment := map[string]map[string]interface{}{
100+
"pasta": {"override_cuisine": "Mediterranean"},
101+
}
102+
got := extractValues(e, tc, enrichment)
103+
if len(got) != 1 || got[0] != "Mediterranean" {
104+
t.Errorf("enrichment override: got %v, want [Mediterranean]", got)
105+
}
106+
}
107+
108+
// ── getEnrichmentOverrides ────────────────────────────────────────────────────
109+
110+
func TestGetEnrichmentOverrides_SimpleField(t *testing.T) {
111+
data := map[string]interface{}{"cuisine": "Italian"}
112+
got := getEnrichmentOverrides(data, "cuisine")
113+
if len(got) != 1 || got[0] != "Italian" {
114+
t.Errorf("simple field: got %v, want [Italian]", got)
115+
}
116+
}
117+
118+
func TestGetEnrichmentOverrides_SimpleField_Missing(t *testing.T) {
119+
data := map[string]interface{}{}
120+
if got := getEnrichmentOverrides(data, "cuisine"); got != nil {
121+
t.Errorf("missing field: want nil, got %v", got)
122+
}
123+
}
124+
125+
func TestGetEnrichmentOverrides_ArrayPath(t *testing.T) {
126+
data := map[string]interface{}{
127+
"ingredients": []interface{}{
128+
map[string]interface{}{"normalizedName": "tomato"},
129+
map[string]interface{}{"normalizedName": "basil"},
130+
map[string]interface{}{"normalizedName": ""}, // empty — should be skipped
131+
},
132+
}
133+
got := getEnrichmentOverrides(data, "ingredients[].normalizedName")
134+
if len(got) != 2 || got[0] != "tomato" || got[1] != "basil" {
135+
t.Errorf("array path: got %v, want [tomato basil]", got)
136+
}
137+
}
138+
139+
func TestGetEnrichmentOverrides_ArrayField_Missing(t *testing.T) {
140+
data := map[string]interface{}{}
141+
if got := getEnrichmentOverrides(data, "ingredients[].name"); got != nil {
142+
t.Errorf("missing array: want nil, got %v", got)
143+
}
144+
}
145+
146+
func TestGetEnrichmentOverrides_ArrayField_NotArray(t *testing.T) {
147+
data := map[string]interface{}{"ingredients": "not-an-array"}
148+
if got := getEnrichmentOverrides(data, "ingredients[].name"); got != nil {
149+
t.Errorf("non-array: want nil, got %v", got)
150+
}
151+
}
152+
9153
// TestGroupByLetterASCII verifies that ASCII entry names are grouped correctly.
10154
func TestGroupByLetterASCII(t *testing.T) {
11155
entries := []Entry{

internal/find/zip_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ func TestIsGitRepo_NotGit(t *testing.T) {
1414
}
1515
}
1616

17+
func TestIsWorktreeClean_NonGitDir(t *testing.T) {
18+
if isWorktreeClean(t.TempDir()) {
19+
t.Error("non-git dir should not be considered clean")
20+
}
21+
}
22+
1723
func TestWalkZip_IncludesFiles(t *testing.T) {
1824
src := t.TempDir()
1925
if err := os.WriteFile(filepath.Join(src, "main.go"), []byte("package main"), 0600); err != nil {

internal/shards/render_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,81 @@ func TestUpdateGitignore_NoTrailingNewlineHandled(t *testing.T) {
471471
}
472472
}
473473

474+
// ── RenderAll ─────────────────────────────────────────────────────────────────
475+
476+
func TestRenderAll_EmptyFiles(t *testing.T) {
477+
dir := t.TempDir()
478+
c := makeRenderCache(shardIR(nil, nil))
479+
n, err := RenderAll(dir, c, nil, false)
480+
if err != nil {
481+
t.Fatalf("RenderAll(empty): %v", err)
482+
}
483+
if n != 0 {
484+
t.Errorf("expected 0 written, got %d", n)
485+
}
486+
}
487+
488+
func TestRenderAll_WritesShards(t *testing.T) {
489+
ir := shardIR(
490+
[]api.Node{
491+
{ID: "fa", Labels: []string{"File"}, Properties: map[string]any{"filePath": "src/a.go"}},
492+
{ID: "fb", Labels: []string{"File"}, Properties: map[string]any{"filePath": "src/b.go"}},
493+
{ID: "fn1", Labels: []string{"Function"}, Properties: map[string]any{"name": "doWork", "filePath": "src/a.go"}},
494+
},
495+
[]api.Relationship{
496+
{ID: "r1", Type: "imports", StartNode: "fa", EndNode: "fb"},
497+
},
498+
)
499+
dir := t.TempDir()
500+
c := makeRenderCache(ir)
501+
n, err := RenderAll(dir, c, []string{"src/a.go"}, false)
502+
if err != nil {
503+
t.Fatalf("RenderAll: %v", err)
504+
}
505+
if n != 1 {
506+
t.Errorf("expected 1 written, got %d", n)
507+
}
508+
}
509+
510+
func TestRenderAll_DryRun(t *testing.T) {
511+
ir := shardIR(
512+
[]api.Node{
513+
{ID: "fa", Labels: []string{"File"}, Properties: map[string]any{"filePath": "src/a.go"}},
514+
{ID: "fb", Labels: []string{"File"}, Properties: map[string]any{"filePath": "src/b.go"}},
515+
},
516+
[]api.Relationship{
517+
{ID: "r1", Type: "imports", StartNode: "fa", EndNode: "fb"},
518+
},
519+
)
520+
dir := t.TempDir()
521+
c := makeRenderCache(ir)
522+
n, err := RenderAll(dir, c, []string{"src/a.go"}, true)
523+
if err != nil {
524+
t.Fatalf("RenderAll dryRun: %v", err)
525+
}
526+
if n != 1 {
527+
t.Errorf("dryRun: expected 1 counted, got %d", n)
528+
}
529+
// No actual files written.
530+
entries, _ := os.ReadDir(dir)
531+
if len(entries) != 0 {
532+
t.Errorf("dry-run should not create files, found %d", len(entries))
533+
}
534+
}
535+
536+
func TestRenderAll_SkipsEmptyContent(t *testing.T) {
537+
// A file not in the cache produces empty content → no shard written.
538+
dir := t.TempDir()
539+
c := makeRenderCache(shardIR(nil, nil))
540+
n, err := RenderAll(dir, c, []string{"src/unknown.go"}, false)
541+
if err != nil {
542+
t.Fatalf("RenderAll: %v", err)
543+
}
544+
if n != 0 {
545+
t.Errorf("unknown file should produce 0 written, got %d", n)
546+
}
547+
}
548+
474549
// ── Hook ─────────────────────────────────────────────────────────────────────
475550

476551
func TestHook_InvalidJSONExitsCleanly(t *testing.T) {

0 commit comments

Comments
 (0)