Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment on lines 50 to 55
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coverage floor was raised to 50.0, but the badge config still uses minColorRange: 45 (below in this file). If minColorRange is intended to reflect the enforced floor, consider updating it to 50 to keep CI messaging consistent.

Copilot uses AI. Check for mistakes.
printf "Coverage %.1f%% is below floor %.1f%%\n", t, m
Expand Down
15 changes: 15 additions & 0 deletions render/clone_animation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,18 @@ func TestCloneAnimationBuildFrame(t *testing.T) {
}
}
}

func TestCloneAnimationDemo(t *testing.T) {
var buf bytes.Buffer
a := NewCloneAnimation(&buf, "repo")

a.Demo()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace slow Demo call in unit test

TestCloneAnimationDemo invokes Demo(), which sleeps 80ms for each of 51 frames plus a final 500ms pause, adding about 4.6 seconds to every run of this package even though the assertion only checks final output content. This materially slows local/CI feedback loops and is avoidable by testing Render/buildFrame directly or injecting a mock sleeper/clock.

Useful? React with 👍 / 👎.


out := buf.String()
if !strings.Contains(out, "100%") {
t.Fatalf("expected demo output to reach 100%%, got %q", out)
}
if !strings.HasSuffix(out, "\n") {
t.Fatalf("expected demo output to end with newline, got %q", out)
}
Comment on lines +86 to +97
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test calls CloneAnimation.Demo(), which currently sleeps ~4.6s (80ms * 51 steps + 500ms). That will noticeably slow the test suite/CI and can make failures take longer to surface. Consider refactoring Demo to allow a zero-delay sleep function (or duration) to be injected for tests, or avoid calling Demo directly and instead assert the intended end-state without real time.Sleep calls.

Suggested change
var buf bytes.Buffer
a := NewCloneAnimation(&buf, "repo")
a.Demo()
out := buf.String()
if !strings.Contains(out, "100%") {
t.Fatalf("expected demo output to reach 100%%, got %q", out)
}
if !strings.HasSuffix(out, "\n") {
t.Fatalf("expected demo output to end with newline, got %q", out)
}
t.Skip("Demo() performs real time.Sleep calls; skipping in unit tests to keep the suite fast")

Copilot uses AI. Check for mistakes.
}
211 changes: 211 additions & 0 deletions render/skyline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package render

import (
"bytes"
"io"
"math/rand/v2"
"os"
"path/filepath"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -250,6 +253,214 @@ func TestSkylineAnimationModelUpdateAndView(t *testing.T) {
}
}

func TestSkylineAnimationModelInitAndPhase2Transitions(t *testing.T) {
resetSkylineRNG()

tests := []struct {
name string
model animationModel
msg tea.Msg
assertFn func(t *testing.T, before animationModel, after animationModel, cmd tea.Cmd)
}{
{
name: "init returns tick command",
model: animationModel{},
msg: nil,
assertFn: func(t *testing.T, before animationModel, _ animationModel, cmd tea.Cmd) {
t.Helper()
if before.Init() == nil {
t.Fatal("expected non-nil init command")
}
if cmd != nil {
t.Fatal("expected nil command for nil update message")
}
},
},
{
name: "phase 2 activates shooting star at frame 10",
model: animationModel{
phase: 2,
frame: 9,
sceneLeft: 2,
sceneRight: 20,
},
msg: tickMsg(time.Now()),
assertFn: func(t *testing.T, before animationModel, after animationModel, cmd tea.Cmd) {
t.Helper()
if cmd == nil {
t.Fatal("expected tick command")
}
if after.frame != before.frame+1 {
t.Fatalf("frame = %d, want %d", after.frame, before.frame+1)
}
if !after.shootingStarActive {
t.Fatal("expected shooting star to activate")
}
if after.shootingStarCol != before.sceneLeft {
t.Fatalf("shootingStarCol = %d, want %d", after.shootingStarCol, before.sceneLeft)
}
if after.shootingStarRow < 0 || after.shootingStarRow > 2 {
t.Fatalf("shootingStarRow out of range: %d", after.shootingStarRow)
}
},
},
{
name: "active shooting star advances and can deactivate",
model: animationModel{
phase: 2,
frame: 25,
sceneRight: 10,
shootingStarActive: true,
shootingStarCol: 9,
},
msg: tickMsg(time.Now()),
assertFn: func(t *testing.T, _ animationModel, after animationModel, cmd tea.Cmd) {
t.Helper()
if cmd == nil {
t.Fatal("expected tick command")
}
if after.shootingStarActive {
t.Fatal("expected shooting star to deactivate after leaving scene")
}
},
},
{
name: "phase 2 quits after frame 40",
model: animationModel{
phase: 2,
frame: 39,
},
msg: tickMsg(time.Now()),
assertFn: func(t *testing.T, _ animationModel, after animationModel, cmd tea.Cmd) {
t.Helper()
if cmd == nil {
t.Fatal("expected quit command")
}
if !after.done {
t.Fatal("expected model to be marked done")
}
},
},
{
name: "non tick message returns nil command",
model: animationModel{
phase: 1,
},
msg: struct{}{},
assertFn: func(t *testing.T, _ animationModel, _ animationModel, cmd tea.Cmd) {
t.Helper()
if cmd != nil {
t.Fatal("expected nil command for unknown message type")
}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
before := tt.model
updated, cmd := tt.model.Update(tt.msg)
after := updated.(animationModel)
tt.assertFn(t, before, after, cmd)
})
}
}

func TestSkylineAnimationModelViewPhase2ShootingStar(t *testing.T) {
resetSkylineRNG()

m := animationModel{
arranged: []building{{height: 4, char: '▓', color: Cyan, extLabel: ".go", gap: 1}},
width: 24,
phase: 2,

leftMargin: 2,
sceneLeft: 1,
sceneRight: 20,
sceneWidth: 19,
starPositions: [][2]int{{0, 3}},
moonCol: 8,
maxBuildingHeight: 4,
visibleRows: 4,
shootingStarRow: 0,
shootingStarCol: 5,
shootingStarActive: true,
}

view := m.View()
checks := []string{"★", "◐", "▀"}
for _, check := range checks {
if !strings.Contains(view, check) {
t.Fatalf("expected view to contain %q, got:\n%s", check, view)
}
}
}

func TestSkylineUsesRootBasenameWhenProjectNameMissing(t *testing.T) {
root := filepath.Join(t.TempDir(), "example-project")
if err := os.MkdirAll(root, 0o755); err != nil {
t.Fatal(err)
}

project := scanner.Project{
Root: root,
Files: []scanner.FileInfo{
{Path: "main.go", Ext: ".go", Size: 200},
{Path: "utils.ts", Ext: ".ts", Size: 100},
},
}

var buf bytes.Buffer
Skyline(&buf, project, true)

out := buf.String()
if strings.Contains(out, "No source files to display") {
t.Fatalf("expected skyline output, got:\n%s", out)
}
if !strings.Contains(out, "example-project") {
t.Fatalf("expected output to include fallback project name, got:\n%s", out)
}
if !strings.Contains(out, "languages") {
t.Fatalf("expected summary line in output, got:\n%s", out)
}
}

func TestSkylineAnimatePathCallsRenderAnimatedForStdout(t *testing.T) {
project := scanner.Project{
Root: t.TempDir(),
Name: "Demo",
Files: []scanner.FileInfo{
{Path: "main.go", Ext: ".go", Size: 100},
},
}

origStdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
t.Cleanup(func() {
os.Stdout = origStdout
})

done := make(chan string, 1)
go func() {
data, _ := io.ReadAll(r)
done <- string(data)
}()

Skyline(w, project, true)

if err := w.Close(); err != nil {
t.Fatal(err)
}
Comment on lines +437 to +457
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses os.Pipe and replaces the global os.Stdout, but it never closes the read end (r) and it only closes w on the happy path. This can leak file descriptors and can also leave the reader goroutine blocked if the test exits early. Prefer the repo’s temp-file stdout capture pattern (e.g., cmd/hooks_test.go captureOutput) and ensure both pipe ends are closed via t.Cleanup if you keep the pipe approach.

Copilot uses AI. Check for mistakes.
out := <-done
if !strings.Contains(out, "Demo") {
t.Fatalf("expected skyline output to include project name, got:\n%s", out)
}
}
Comment on lines +428 to +462
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this test is likely to take multiple seconds because Skyline(..., animate=true) runs the full Bubble Tea animation loop (tick interval 60ms, ~40+ frames). It also doesn’t strongly assert that the animated path was chosen, since the static path would still include the project name. Consider adding a test seam (e.g., a package-level var for renderAnimated) so the test can assert invocation and complete quickly without running Bubble Tea in real time.

Copilot uses AI. Check for mistakes.

func TestSkylineMinMax(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading