Skip to content

Commit 25dd71a

Browse files
greynewellclaude
andcommitted
test: expand coverage across 10 packages with new unit tests
- compact: nextShortName now at 100% (two-char overflow, skip existing, skip builtins) - deadcode: new zip_test.go covers isGitRepo, isWorktreeClean, walkZip, createZip (0% → 65%) - mcp: new zip_test.go covers same zip helpers (26% → 41%) - graph2md: pure function tests for getStr, getNum, mermaidID, generateSlug (100% each); Run-based tests for Class/Type/Domain/Subdomain/Directory nodes (40% → 64%) - output: GenerateRSSFeeds with disabled/main/default-path/category-feed cases (74% → 87%) - render: BuildFuncMap smoke test; sliceHelper with all 3 slice types + passthrough; shareimage functions svgEscape, truncate, renderBarsSVG, all Generate*ShareSVG variants (45% → 70%) - build: toTemplateHTML, writeShareSVG, maybeWriteShareSVG with enabled/disabled (6% → 7.5%) - auth: Logout_AlreadyLoggedOut branch (17% → 19%) - setup: detectClaude with ~/.claude present (23% → 25%) - analyze: TestIsWorktreeClean_NonGitDir added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fcb508d commit 25dd71a

11 files changed

Lines changed: 933 additions & 9 deletions

File tree

internal/analyze/zip_test.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ import (
88
"testing"
99
)
1010

11-
func TestIsGitRepo_WithDotGit(t *testing.T) {
12-
dir := t.TempDir()
13-
// Simulate .git via git init
14-
if err := os.MkdirAll(filepath.Join(dir, ".git"), 0750); err != nil {
15-
t.Fatal(err)
16-
}
17-
// isGitRepo uses `git rev-parse --git-dir` which needs an actual git repo;
18-
// fall back to checking directory creation only — the factory version
19-
// (os.Stat) is simpler, but here we just ensure non-git dir returns false.
11+
func TestIsGitRepo_NonGitDir(t *testing.T) {
12+
// isGitRepo uses `git rev-parse --git-dir`; an empty temp dir is not a git repo.
2013
if isGitRepo(t.TempDir()) {
2114
t.Error("empty temp dir should not be a git repo")
2215
}
2316
}
2417

18+
// ── isWorktreeClean ───────────────────────────────────────────────────────────
19+
20+
func TestIsWorktreeClean_NonGitDir(t *testing.T) {
21+
// git status on a non-repo exits non-zero → returns false
22+
if isWorktreeClean(t.TempDir()) {
23+
t.Error("non-git dir should not be considered clean")
24+
}
25+
}
26+
2527
func TestWalkZip_IncludesFiles(t *testing.T) {
2628
src := t.TempDir()
2729
if err := os.WriteFile(filepath.Join(src, "main.go"), []byte("package main"), 0600); err != nil {

internal/archdocs/graph2md/graph2md_test.go

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,312 @@ func buildGraphJSON(t *testing.T, nodes []Node, rels []Relationship) string {
6262
return f.Name()
6363
}
6464

65+
// ── getStr ────────────────────────────────────────────────────────────────────
66+
67+
func TestGetStr(t *testing.T) {
68+
m := map[string]interface{}{"name": "foo", "num": 42, "empty": ""}
69+
if got := getStr(m, "name"); got != "foo" {
70+
t.Errorf("got %q, want %q", got, "foo")
71+
}
72+
if got := getStr(m, "num"); got != "" {
73+
t.Errorf("non-string: got %q, want empty", got)
74+
}
75+
if got := getStr(m, "missing"); got != "" {
76+
t.Errorf("missing key: got %q, want empty", got)
77+
}
78+
if got := getStr(m, "empty"); got != "" {
79+
t.Errorf("empty string: got %q, want empty", got)
80+
}
81+
}
82+
83+
// ── getNum ────────────────────────────────────────────────────────────────────
84+
85+
func TestGetNum(t *testing.T) {
86+
m := map[string]interface{}{"f64": float64(7), "i": 9, "str": "x"}
87+
if got := getNum(m, "f64"); got != 7 {
88+
t.Errorf("float64: got %d, want 7", got)
89+
}
90+
if got := getNum(m, "i"); got != 9 {
91+
t.Errorf("int: got %d, want 9", got)
92+
}
93+
if got := getNum(m, "str"); got != 0 {
94+
t.Errorf("wrong type: got %d, want 0", got)
95+
}
96+
if got := getNum(m, "missing"); got != 0 {
97+
t.Errorf("missing key: got %d, want 0", got)
98+
}
99+
}
100+
101+
// ── mermaidID ─────────────────────────────────────────────────────────────────
102+
103+
func TestMermaidID(t *testing.T) {
104+
cases := []struct{ in, want string }{
105+
{"fn:src/foo.go:bar", "fn_src_foo_go_bar"},
106+
{"hello_world", "hello_world"},
107+
{"ABC123", "ABC123"},
108+
{"", "node"},
109+
{"---", "___"},
110+
}
111+
for _, tc := range cases {
112+
got := mermaidID(tc.in)
113+
if got != tc.want {
114+
t.Errorf("mermaidID(%q) = %q, want %q", tc.in, got, tc.want)
115+
}
116+
}
117+
}
118+
119+
// ── generateSlug ─────────────────────────────────────────────────────────────
120+
121+
func TestGenerateSlug_File(t *testing.T) {
122+
n := Node{Properties: map[string]interface{}{"path": "src/main.go"}}
123+
got := generateSlug(n, "File")
124+
if !strings.HasPrefix(got, "file-") {
125+
t.Errorf("File slug: got %q, want prefix 'file-'", got)
126+
}
127+
// empty path → empty slug
128+
n2 := Node{Properties: map[string]interface{}{}}
129+
if got2 := generateSlug(n2, "File"); got2 != "" {
130+
t.Errorf("empty path File slug: got %q, want empty", got2)
131+
}
132+
}
133+
134+
func TestGenerateSlug_Function(t *testing.T) {
135+
n := Node{Properties: map[string]interface{}{"name": "run", "filePath": "internal/api/handler.go"}}
136+
got := generateSlug(n, "Function")
137+
if !strings.HasPrefix(got, "fn-") {
138+
t.Errorf("Function slug with path: got %q, want prefix 'fn-'", got)
139+
}
140+
n2 := Node{Properties: map[string]interface{}{"name": "run"}}
141+
got2 := generateSlug(n2, "Function")
142+
if !strings.HasPrefix(got2, "fn-") {
143+
t.Errorf("Function slug without path: got %q, want prefix 'fn-'", got2)
144+
}
145+
n3 := Node{Properties: map[string]interface{}{}}
146+
if got3 := generateSlug(n3, "Function"); got3 != "" {
147+
t.Errorf("empty name: got %q, want empty", got3)
148+
}
149+
}
150+
151+
func TestGenerateSlug_ClassTypeLabels(t *testing.T) {
152+
for _, label := range []string{"Class", "Type"} {
153+
prefix := strings.ToLower(label) + "-"
154+
n := Node{Properties: map[string]interface{}{"name": "MyEntity", "filePath": "src/foo.go"}}
155+
got := generateSlug(n, label)
156+
if !strings.HasPrefix(got, prefix) {
157+
t.Errorf("%s slug: got %q, want prefix %q", label, got, prefix)
158+
}
159+
n2 := Node{Properties: map[string]interface{}{"name": "MyEntity"}}
160+
got2 := generateSlug(n2, label)
161+
if !strings.HasPrefix(got2, prefix) {
162+
t.Errorf("%s slug without path: got %q, want prefix %q", label, got2, prefix)
163+
}
164+
n3 := Node{Properties: map[string]interface{}{}}
165+
if got3 := generateSlug(n3, label); got3 != "" {
166+
t.Errorf("%s empty name: got %q, want empty", label, got3)
167+
}
168+
}
169+
}
170+
171+
func TestGenerateSlug_DomainSubdomain(t *testing.T) {
172+
dn := Node{Properties: map[string]interface{}{"name": "auth"}}
173+
if got := generateSlug(dn, "Domain"); !strings.HasPrefix(got, "domain-") {
174+
t.Errorf("Domain: got %q, want prefix 'domain-'", got)
175+
}
176+
sn := Node{Properties: map[string]interface{}{"name": "users"}}
177+
if got := generateSlug(sn, "Subdomain"); !strings.HasPrefix(got, "subdomain-") {
178+
t.Errorf("Subdomain: got %q, want prefix 'subdomain-'", got)
179+
}
180+
empty := Node{Properties: map[string]interface{}{}}
181+
if got := generateSlug(empty, "Domain"); got != "" {
182+
t.Errorf("Domain empty name: got %q, want empty", got)
183+
}
184+
if got := generateSlug(empty, "Subdomain"); got != "" {
185+
t.Errorf("Subdomain empty name: got %q, want empty", got)
186+
}
187+
}
188+
189+
func TestGenerateSlug_Directory(t *testing.T) {
190+
n := Node{Properties: map[string]interface{}{"path": "internal/api"}}
191+
if got := generateSlug(n, "Directory"); !strings.HasPrefix(got, "dir-") {
192+
t.Errorf("Directory: got %q, want prefix 'dir-'", got)
193+
}
194+
// path containing /app/repo-root/ → empty
195+
n2 := Node{Properties: map[string]interface{}{"path": "/app/repo-root/internal"}}
196+
if got := generateSlug(n2, "Directory"); got != "" {
197+
t.Errorf("repo-root path: got %q, want empty", got)
198+
}
199+
// empty path → empty
200+
n3 := Node{Properties: map[string]interface{}{}}
201+
if got := generateSlug(n3, "Directory"); got != "" {
202+
t.Errorf("empty path: got %q, want empty", got)
203+
}
204+
}
205+
206+
func TestGenerateSlug_Unknown(t *testing.T) {
207+
n := Node{Properties: map[string]interface{}{"name": "foo"}}
208+
if got := generateSlug(n, "Unknown"); got != "" {
209+
t.Errorf("unknown label: got %q, want empty", got)
210+
}
211+
}
212+
213+
// ── node-type rendering ───────────────────────────────────────────────────────
214+
215+
// TestRunClassNode verifies that a Class node generates a markdown file
216+
// containing class-specific frontmatter fields.
217+
func TestRunClassNode(t *testing.T) {
218+
nodes := []Node{
219+
{
220+
ID: "class:src/auth.go:UserAuth",
221+
Labels: []string{"Class"},
222+
Properties: map[string]interface{}{
223+
"name": "UserAuth",
224+
"filePath": "src/auth.go",
225+
"startLine": float64(10),
226+
"endLine": float64(50),
227+
"language": "go",
228+
},
229+
},
230+
}
231+
graphFile := buildGraphJSON(t, nodes, nil)
232+
outDir := t.TempDir()
233+
if err := Run(graphFile, outDir, "myrepo", "https://github.com/example/myrepo", 0); err != nil {
234+
t.Fatalf("Run: %v", err)
235+
}
236+
entries, _ := os.ReadDir(outDir)
237+
if len(entries) != 1 {
238+
t.Fatalf("expected 1 output file, got %d", len(entries))
239+
}
240+
content, err := os.ReadFile(filepath.Join(outDir, entries[0].Name()))
241+
if err != nil {
242+
t.Fatal(err)
243+
}
244+
body := string(content)
245+
for _, want := range []string{`node_type: "Class"`, `class_name: "UserAuth"`, `language: "go"`, `start_line: 10`, `end_line: 50`} {
246+
if !strings.Contains(body, want) {
247+
t.Errorf("missing %q in class output:\n%s", want, body)
248+
}
249+
}
250+
}
251+
252+
// TestRunTypeNode verifies that a Type node generates type-specific frontmatter.
253+
func TestRunTypeNode(t *testing.T) {
254+
nodes := []Node{
255+
{
256+
ID: "type:src/types.go:UserID",
257+
Labels: []string{"Type"},
258+
Properties: map[string]interface{}{
259+
"name": "UserID",
260+
"filePath": "src/types.go",
261+
},
262+
},
263+
}
264+
graphFile := buildGraphJSON(t, nodes, nil)
265+
outDir := t.TempDir()
266+
if err := Run(graphFile, outDir, "myrepo", "", 0); err != nil {
267+
t.Fatalf("Run: %v", err)
268+
}
269+
entries, _ := os.ReadDir(outDir)
270+
if len(entries) != 1 {
271+
t.Fatalf("expected 1 output file, got %d", len(entries))
272+
}
273+
content, _ := os.ReadFile(filepath.Join(outDir, entries[0].Name()))
274+
body := string(content)
275+
for _, want := range []string{`node_type: "Type"`, `type_name: "UserID"`} {
276+
if !strings.Contains(body, want) {
277+
t.Errorf("missing %q in type output:\n%s", want, body)
278+
}
279+
}
280+
}
281+
282+
// TestRunDomainNode verifies that a Domain node generates domain-specific frontmatter.
283+
func TestRunDomainNode(t *testing.T) {
284+
nodes := []Node{
285+
{
286+
ID: "domain:auth",
287+
Labels: []string{"Domain"},
288+
Properties: map[string]interface{}{
289+
"name": "auth",
290+
"description": "Authentication domain",
291+
},
292+
},
293+
}
294+
graphFile := buildGraphJSON(t, nodes, nil)
295+
outDir := t.TempDir()
296+
if err := Run(graphFile, outDir, "myrepo", "", 0); err != nil {
297+
t.Fatalf("Run: %v", err)
298+
}
299+
entries, _ := os.ReadDir(outDir)
300+
if len(entries) != 1 {
301+
t.Fatalf("expected 1 output file, got %d", len(entries))
302+
}
303+
content, _ := os.ReadFile(filepath.Join(outDir, entries[0].Name()))
304+
body := string(content)
305+
for _, want := range []string{`node_type: "Domain"`, `domain: "auth"`, `summary: "Authentication domain"`} {
306+
if !strings.Contains(body, want) {
307+
t.Errorf("missing %q in domain output:\n%s", want, body)
308+
}
309+
}
310+
}
311+
312+
// TestRunSubdomainNode verifies that a Subdomain node generates subdomain frontmatter.
313+
func TestRunSubdomainNode(t *testing.T) {
314+
nodes := []Node{
315+
{
316+
ID: "subdomain:users",
317+
Labels: []string{"Subdomain"},
318+
Properties: map[string]interface{}{
319+
"name": "users",
320+
},
321+
},
322+
}
323+
graphFile := buildGraphJSON(t, nodes, nil)
324+
outDir := t.TempDir()
325+
if err := Run(graphFile, outDir, "myrepo", "", 0); err != nil {
326+
t.Fatalf("Run: %v", err)
327+
}
328+
entries, _ := os.ReadDir(outDir)
329+
if len(entries) != 1 {
330+
t.Fatalf("expected 1 output file, got %d", len(entries))
331+
}
332+
content, _ := os.ReadFile(filepath.Join(outDir, entries[0].Name()))
333+
body := string(content)
334+
for _, want := range []string{`node_type: "Subdomain"`, `subdomain: "users"`} {
335+
if !strings.Contains(body, want) {
336+
t.Errorf("missing %q in subdomain output:\n%s", want, body)
337+
}
338+
}
339+
}
340+
341+
// TestRunDirectoryNode verifies that a Directory node generates directory frontmatter.
342+
func TestRunDirectoryNode(t *testing.T) {
343+
nodes := []Node{
344+
{
345+
ID: "dir:internal/api",
346+
Labels: []string{"Directory"},
347+
Properties: map[string]interface{}{
348+
"name": "api",
349+
"path": "internal/api",
350+
},
351+
},
352+
}
353+
graphFile := buildGraphJSON(t, nodes, nil)
354+
outDir := t.TempDir()
355+
if err := Run(graphFile, outDir, "myrepo", "", 0); err != nil {
356+
t.Fatalf("Run: %v", err)
357+
}
358+
entries, _ := os.ReadDir(outDir)
359+
if len(entries) != 1 {
360+
t.Fatalf("expected 1 output file, got %d", len(entries))
361+
}
362+
content, _ := os.ReadFile(filepath.Join(outDir, entries[0].Name()))
363+
body := string(content)
364+
for _, want := range []string{`node_type: "Directory"`} {
365+
if !strings.Contains(body, want) {
366+
t.Errorf("missing %q in directory output:\n%s", want, body)
367+
}
368+
}
369+
}
370+
65371
// TestSlugCollisionResolution verifies that when two nodes produce the same
66372
// base slug, the second gets a "-2" suffix, AND that a third node which
67373
// naturally produces that same "-2" slug does not silently collide with it.

0 commit comments

Comments
 (0)