Skip to content

Commit fcb508d

Browse files
greynewellclaude
andcommitted
test: add coverage for config, focus/extractTypes, and build utility functions
- config: add tests for ShardsEnabled (nil/true/false), applyEnv (SUPERMODEL_API_KEY, SUPERMODEL_API_BASE, SUPERMODEL_SHARDS=false), applyDefaults (from file) - focus: add extractTypes tests (class detection, other-file exclusion) and extract with includeTypes=true integration - build: add shareImageURL, countTaxEntries, countFieldDistribution (basic, limit, empty), toBreadcrumbItems tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0d645c1 commit fcb508d

3 files changed

Lines changed: 267 additions & 0 deletions

File tree

internal/archdocs/pssg/build/build_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"github.com/supermodeltools/cli/internal/archdocs/pssg/config"
1111
"github.com/supermodeltools/cli/internal/archdocs/pssg/entity"
12+
"github.com/supermodeltools/cli/internal/archdocs/pssg/render"
13+
"github.com/supermodeltools/cli/internal/archdocs/pssg/taxonomy"
1214
)
1315

1416
func newBuilder(outDir string) *Builder {
@@ -125,6 +127,92 @@ func TestGenerateSearchIndex_DisabledSearch(t *testing.T) {
125127
}
126128
}
127129

130+
// ── shareImageURL ─────────────────────────────────────────────────────────────
131+
132+
func TestShareImageURL(t *testing.T) {
133+
got := shareImageURL("https://example.com", "recipe-soup.png")
134+
want := "https://example.com/images/share/recipe-soup.png"
135+
if got != want {
136+
t.Errorf("shareImageURL: got %q, want %q", got, want)
137+
}
138+
}
139+
140+
// ── countTaxEntries ───────────────────────────────────────────────────────────
141+
142+
func TestCountTaxEntries(t *testing.T) {
143+
taxes := []taxonomy.Taxonomy{
144+
{Entries: []taxonomy.Entry{{}, {}}},
145+
{Entries: []taxonomy.Entry{{}}},
146+
}
147+
if got := countTaxEntries(taxes); got != 3 {
148+
t.Errorf("countTaxEntries: got %d, want 3", got)
149+
}
150+
if got := countTaxEntries(nil); got != 0 {
151+
t.Errorf("countTaxEntries(nil): got %d, want 0", got)
152+
}
153+
}
154+
155+
// ── countFieldDistribution ────────────────────────────────────────────────────
156+
157+
func TestCountFieldDistribution(t *testing.T) {
158+
entities := []*entity.Entity{
159+
{Fields: map[string]interface{}{"cuisine": "Italian"}},
160+
{Fields: map[string]interface{}{"cuisine": "Italian"}},
161+
{Fields: map[string]interface{}{"cuisine": "French"}},
162+
{Fields: map[string]interface{}{"cuisine": ""}}, // empty, should be skipped
163+
}
164+
result := countFieldDistribution(entities, "cuisine", 10)
165+
if len(result) != 2 {
166+
t.Fatalf("want 2 entries, got %d", len(result))
167+
}
168+
// Should be sorted desc by count
169+
if result[0].Name != "Italian" || result[0].Count != 2 {
170+
t.Errorf("first entry: got {%s %d}, want {Italian 2}", result[0].Name, result[0].Count)
171+
}
172+
if result[1].Name != "French" || result[1].Count != 1 {
173+
t.Errorf("second entry: got {%s %d}, want {French 1}", result[1].Name, result[1].Count)
174+
}
175+
}
176+
177+
func TestCountFieldDistribution_Limit(t *testing.T) {
178+
entities := []*entity.Entity{
179+
{Fields: map[string]interface{}{"tag": "a"}},
180+
{Fields: map[string]interface{}{"tag": "a"}},
181+
{Fields: map[string]interface{}{"tag": "b"}},
182+
{Fields: map[string]interface{}{"tag": "b"}},
183+
{Fields: map[string]interface{}{"tag": "c"}},
184+
}
185+
result := countFieldDistribution(entities, "tag", 2)
186+
if len(result) != 2 {
187+
t.Errorf("limit=2: want 2 entries, got %d", len(result))
188+
}
189+
}
190+
191+
func TestCountFieldDistribution_Empty(t *testing.T) {
192+
if got := countFieldDistribution(nil, "field", 10); len(got) != 0 {
193+
t.Errorf("nil entities: want empty, got %v", got)
194+
}
195+
}
196+
197+
// ── toBreadcrumbItems ─────────────────────────────────────────────────────────
198+
199+
func TestToBreadcrumbItems(t *testing.T) {
200+
bcs := []render.Breadcrumb{
201+
{Name: "Home", URL: "https://example.com/"},
202+
{Name: "Recipes", URL: "https://example.com/recipes/"},
203+
}
204+
items := toBreadcrumbItems(bcs)
205+
if len(items) != 2 {
206+
t.Fatalf("want 2 items, got %d", len(items))
207+
}
208+
if items[0].Name != "Home" || items[0].URL != "https://example.com/" {
209+
t.Errorf("first item: got %+v", items[0])
210+
}
211+
if items[1].Name != "Recipes" {
212+
t.Errorf("second item: got %+v", items[1])
213+
}
214+
}
215+
128216
// readSearchIndex reads and unmarshals the search-index.json from outDir.
129217
func readSearchIndex(t *testing.T, outDir string) []map[string]string {
130218
t.Helper()

internal/config/config_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,105 @@ func TestPath(t *testing.T) {
7575
t.Errorf("Path() = %q, want %q", got, want)
7676
}
7777
}
78+
79+
// ── ShardsEnabled ─────────────────────────────────────────────────────────────
80+
81+
func TestShardsEnabled_DefaultTrue(t *testing.T) {
82+
cfg := &Config{}
83+
if !cfg.ShardsEnabled() {
84+
t.Error("ShardsEnabled() with nil Shards should default to true")
85+
}
86+
}
87+
88+
func TestShardsEnabled_ExplicitFalse(t *testing.T) {
89+
f := false
90+
cfg := &Config{Shards: &f}
91+
if cfg.ShardsEnabled() {
92+
t.Error("ShardsEnabled() with Shards=false should return false")
93+
}
94+
}
95+
96+
func TestShardsEnabled_ExplicitTrue(t *testing.T) {
97+
tr := true
98+
cfg := &Config{Shards: &tr}
99+
if !cfg.ShardsEnabled() {
100+
t.Error("ShardsEnabled() with Shards=true should return true")
101+
}
102+
}
103+
104+
// ── applyEnv ──────────────────────────────────────────────────────────────────
105+
106+
func TestApplyEnv_APIKey(t *testing.T) {
107+
t.Setenv("HOME", t.TempDir())
108+
t.Setenv("SUPERMODEL_API_KEY", "env-key-123")
109+
t.Setenv("SUPERMODEL_API_BASE", "")
110+
t.Setenv("SUPERMODEL_SHARDS", "")
111+
cfg, err := Load()
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
if cfg.APIKey != "env-key-123" {
116+
t.Errorf("SUPERMODEL_API_KEY env override: got %q", cfg.APIKey)
117+
}
118+
}
119+
120+
func TestApplyEnv_APIBase(t *testing.T) {
121+
t.Setenv("HOME", t.TempDir())
122+
t.Setenv("SUPERMODEL_API_KEY", "")
123+
t.Setenv("SUPERMODEL_API_BASE", "https://custom.api.com")
124+
t.Setenv("SUPERMODEL_SHARDS", "")
125+
cfg, err := Load()
126+
if err != nil {
127+
t.Fatal(err)
128+
}
129+
if cfg.APIBase != "https://custom.api.com" {
130+
t.Errorf("SUPERMODEL_API_BASE env override: got %q", cfg.APIBase)
131+
}
132+
}
133+
134+
func TestApplyEnv_ShardsDisabled(t *testing.T) {
135+
t.Setenv("HOME", t.TempDir())
136+
t.Setenv("SUPERMODEL_API_KEY", "")
137+
t.Setenv("SUPERMODEL_API_BASE", "")
138+
t.Setenv("SUPERMODEL_SHARDS", "false")
139+
cfg, err := Load()
140+
if err != nil {
141+
t.Fatal(err)
142+
}
143+
if cfg.ShardsEnabled() {
144+
t.Error("SUPERMODEL_SHARDS=false should disable shards")
145+
}
146+
}
147+
148+
// ── applyDefaults ─────────────────────────────────────────────────────────────
149+
150+
func TestApplyDefaults_FilledFromFile(t *testing.T) {
151+
home := t.TempDir()
152+
t.Setenv("HOME", home)
153+
t.Setenv("SUPERMODEL_API_KEY", "")
154+
t.Setenv("SUPERMODEL_API_BASE", "")
155+
t.Setenv("SUPERMODEL_SHARDS", "")
156+
157+
// Write a config that has api_key but no api_base or output
158+
cfgFile := filepath.Join(home, ".supermodel", "config.yaml")
159+
if err := os.MkdirAll(filepath.Dir(cfgFile), 0700); err != nil {
160+
t.Fatal(err)
161+
}
162+
if err := os.WriteFile(cfgFile, []byte("api_key: loaded-key\n"), 0600); err != nil {
163+
t.Fatal(err)
164+
}
165+
166+
cfg, err := Load()
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
if cfg.APIKey != "loaded-key" {
171+
t.Errorf("loaded api_key: got %q", cfg.APIKey)
172+
}
173+
if cfg.APIBase != DefaultAPIBase {
174+
t.Errorf("default api_base: got %q", cfg.APIBase)
175+
}
176+
if cfg.Output != "human" {
177+
t.Errorf("default output: got %q", cfg.Output)
178+
}
179+
}

internal/focus/handler_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,80 @@ func TestRender_MarkdownTokenHint(t *testing.T) {
302302
t.Errorf("should show token hint:\n%s", buf.String())
303303
}
304304
}
305+
306+
// ── extractTypes ──────────────────────────────────────────────────────────────
307+
308+
func TestExtractTypes(t *testing.T) {
309+
g := &api.Graph{
310+
Nodes: []api.Node{
311+
{ID: "f1", Labels: []string{"File"}, Properties: map[string]any{"path": "auth/handler.go"}},
312+
{ID: "cls1", Labels: []string{"Class"}, Properties: map[string]any{"name": "AuthService", "file": "auth/handler.go"}},
313+
{ID: "iface1", Labels: []string{"Interface"}, Properties: map[string]any{"name": "Authenticator", "file": "auth/handler.go"}},
314+
},
315+
Relationships: []api.Relationship{
316+
{ID: "r1", Type: "declares_class", StartNode: "f1", EndNode: "cls1"},
317+
{ID: "r2", Type: "defines", StartNode: "f1", EndNode: "iface1"},
318+
},
319+
}
320+
nodeByID := map[string]*api.Node{}
321+
for i := range g.Nodes {
322+
nodeByID[g.Nodes[i].ID] = &g.Nodes[i]
323+
}
324+
types := extractTypes(g, "f1", nodeByID, g.Rels())
325+
if len(types) != 2 {
326+
t.Fatalf("want 2 types, got %d: %v", len(types), types)
327+
}
328+
// Class should have kind "class"
329+
var foundClass bool
330+
for _, typ := range types {
331+
if typ.Name == "AuthService" && typ.Kind == "class" {
332+
foundClass = true
333+
}
334+
}
335+
if !foundClass {
336+
t.Errorf("should have AuthService with kind='class', got %v", types)
337+
}
338+
}
339+
340+
func TestExtractTypes_OtherFileExcluded(t *testing.T) {
341+
// Relations from a different file should not appear
342+
g := &api.Graph{
343+
Nodes: []api.Node{
344+
{ID: "f1", Labels: []string{"File"}, Properties: map[string]any{"path": "a.go"}},
345+
{ID: "f2", Labels: []string{"File"}, Properties: map[string]any{"path": "b.go"}},
346+
{ID: "cls1", Labels: []string{"Class"}, Properties: map[string]any{"name": "Foo"}},
347+
},
348+
Relationships: []api.Relationship{
349+
{ID: "r1", Type: "declares_class", StartNode: "f2", EndNode: "cls1"}, // from f2, not f1
350+
},
351+
}
352+
nodeByID := map[string]*api.Node{}
353+
for i := range g.Nodes {
354+
nodeByID[g.Nodes[i].ID] = &g.Nodes[i]
355+
}
356+
types := extractTypes(g, "f1", nodeByID, g.Rels())
357+
if len(types) != 0 {
358+
t.Errorf("other file's types should not appear, got %v", types)
359+
}
360+
}
361+
362+
// ── extract with includeTypes ─────────────────────────────────────────────────
363+
364+
func TestExtract_WithTypes(t *testing.T) {
365+
g := &api.Graph{
366+
Nodes: []api.Node{
367+
{ID: "f1", Labels: []string{"File"}, Properties: map[string]any{"path": "auth.go"}},
368+
{ID: "cls1", Labels: []string{"Class"}, Properties: map[string]any{"name": "AuthService"}},
369+
},
370+
Relationships: []api.Relationship{
371+
{ID: "r1", Type: "declares_class", StartNode: "f1", EndNode: "cls1"},
372+
},
373+
}
374+
sl := extract(g, "auth.go", 1, true)
375+
if sl == nil {
376+
t.Fatal("nil slice")
377+
}
378+
if len(sl.Types) != 1 || sl.Types[0].Name != "AuthService" {
379+
t.Errorf("types: got %v", sl.Types)
380+
}
381+
}

0 commit comments

Comments
 (0)