diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 964784e..386ba0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: run: | total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') # Current enforced coverage floor. Codex PRs raise this incrementally toward 90%. - min=45.0 + min=50.0 awk -v t="$total" -v m="$min" 'BEGIN { if (t+0 < m+0) { printf "Coverage %.1f%% is below floor %.1f%%\n", t, m diff --git a/render/depgraph_test.go b/render/depgraph_test.go index 0cabb75..7ad6296 100644 --- a/render/depgraph_test.go +++ b/render/depgraph_test.go @@ -2,6 +2,8 @@ package render import ( "bytes" + "os" + "path/filepath" "strings" "testing" @@ -103,3 +105,73 @@ func TestDepgraphRendersExternalDepsAndSummarySection(t *testing.T) { } } } + +func writeDepgraphFixture(t *testing.T, root string) { + t.Helper() + + files := map[string]string{ + "go.mod": "module example.com/demo\n\ngo 1.24.0\n", + "app/main.go": "package app\n\nimport (\n\t\"example.com/demo/core/extra1\"\n\t\"example.com/demo/core/extra2\"\n\t\"example.com/demo/core/leaf\"\n\t\"example.com/demo/core/mid\"\n\t\"example.com/demo/core/root\"\n)\n\nfunc Main() {\n\textra1.Extra1()\n\textra2.Extra2()\n\tleaf.Leaf()\n\tmid.Mid()\n\troot.Root()\n}\n", + "core/root/root.go": "package root\n\nimport \"example.com/demo/core/mid\"\n\nfunc Root() {\n\tmid.Mid()\n}\n", + "core/mid/mid.go": "package mid\n\nimport \"example.com/demo/core/leaf\"\n\nfunc Mid() {\n\tleaf.Leaf()\n}\n", + "core/leaf/leaf.go": "package leaf\n\nfunc Leaf() {}\n", + "core/extra1/extra1.go": "package extra1\n\nimport \"example.com/demo/core/leaf\"\n\nfunc Extra1() {\n\tleaf.Leaf()\n}\n", + "core/extra2/extra2.go": "package extra2\n\nimport \"example.com/demo/core/leaf\"\n\nfunc Extra2() {\n\tleaf.Leaf()\n}\n", + } + + for path, content := range files { + full := filepath.Join(root, path) + if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } +} + +func TestDepgraphRendersChainsFanoutAndHubs(t *testing.T) { + if !scanner.NewAstGrepAnalyzer().Available() { + t.Skip("ast-grep not available") + } + + root := t.TempDir() + writeDepgraphFixture(t, root) + + project := scanner.DepsProject{ + Root: root, + Files: []scanner.FileAnalysis{ + {Path: "app/main.go", Functions: []string{"Main"}}, + {Path: "core/root/root.go", Functions: []string{"Root"}}, + {Path: "core/mid/mid.go", Functions: []string{"Mid"}}, + {Path: "core/leaf/leaf.go", Functions: []string{"Leaf"}}, + {Path: "core/extra1/extra1.go", Functions: []string{"Extra1"}}, + {Path: "core/extra2/extra2.go", Functions: []string{"Extra2"}}, + }, + ExternalDeps: map[string][]string{ + "go": {"example.com/very/long/module/name/v2"}, + }, + } + + var buf bytes.Buffer + Depgraph(&buf, project) + output := buf.String() + + expectedSnippets := []string{ + "Dependency Flow", + "Go: name", + "App", + "Core", + "main ──┬──▶ core/extra1/extra1", + "└──▶ core/root/root", + "root ───▶ core/mid/mid", + "HUBS: core/leaf/leaf (4←), core/mid/mid (2←)", + "6 files · 6 functions · 9 deps", + } + + for _, snippet := range expectedSnippets { + if !strings.Contains(output, snippet) { + t.Fatalf("expected output to contain %q, got:\n%s", snippet, output) + } + } +}