Skip to content
Merged
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 {
Comment on lines 50 to 54
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Coverage floor was bumped to 50.0, but the coverage badge step still uses minColorRange: 45 later in this workflow. Consider updating the badge range to match the new enforced floor to avoid confusing badge colors vs. CI enforcement.

Copilot uses AI. Check for mistakes.
if (t+0 < m+0) {
printf "Coverage %.1f%% is below floor %.1f%%\n", t, m
Expand Down
144 changes: 144 additions & 0 deletions render/skyline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package render
import (
"bytes"
"math/rand/v2"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
Expand All @@ -12,10 +14,16 @@ import (
tea "github.com/charmbracelet/bubbletea"
)

var skylineANSIPattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)

func resetSkylineRNG() {
rng = rand.New(rand.NewPCG(42, 0))
}

func stripSkylineANSI(s string) string {
return skylineANSIPattern.ReplaceAllString(s, "")
}

func TestSkylineFilterCodeFiles(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -209,6 +217,26 @@ func TestSkylineRenderStaticIncludesTitleAndStats(t *testing.T) {
}
}

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

root := t.TempDir()
project := scanner.Project{
Root: root,
Files: []scanner.FileInfo{
{Path: "src/main.go", Ext: ".go", Size: 256},
},
}

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

out := stripSkylineANSI(buf.String())
if !strings.Contains(out, "─── "+filepath.Base(root)+" ───") {
t.Fatalf("expected skyline title to use root basename, got:\n%s", out)
}
}

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

Expand Down Expand Up @@ -250,6 +278,122 @@ func TestSkylineAnimationModelUpdateAndView(t *testing.T) {
}
}

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

m := animationModel{
arranged: []building{{height: 3, char: '▓', color: Cyan, extLabel: ".go", gap: 1}},
width: 20,
leftMargin: 2,
sceneLeft: 1,
sceneRight: 12,
sceneWidth: 11,
maxBuildingHeight: 3,
phase: 1,
visibleRows: 5,
}

if cmd := m.Init(); cmd == nil {
t.Fatal("expected Init to return a tick command")
}

updated, cmd := m.Update(tickMsg(time.Now()))
if cmd == nil {
t.Fatal("expected tick command during rising phase")
}

m1 := updated.(animationModel)
if m1.phase != 2 {
t.Fatalf("expected phase transition to 2, got %d", m1.phase)
}
if m1.frame != 0 {
t.Fatalf("expected frame reset after phase transition, got %d", m1.frame)
}

m1.frame = 39
updated, cmd = m1.Update(tickMsg(time.Now()))
if cmd == nil {
t.Fatal("expected quit command when animation completes")
}

m2 := updated.(animationModel)
if !m2.done {
t.Fatal("expected animation model to be marked done")
}
}

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

m := animationModel{
arranged: []building{{height: 4, char: '▓', color: Cyan, extLabel: ".go", gap: 1}},
width: 20,
leftMargin: 2,
sceneLeft: 3,
sceneRight: 10,
sceneWidth: 7,
maxBuildingHeight: 4,
phase: 2,
frame: 9,
shootingStarActive: false,
}

updated, cmd := m.Update(tickMsg(time.Now()))
if cmd == nil {
t.Fatal("expected tick command in twinkling phase")
}

m1 := updated.(animationModel)
if !m1.shootingStarActive {
t.Fatal("expected shooting star to activate on frame 10")
}
if m1.shootingStarCol != m.sceneLeft {
t.Fatalf("expected shooting star to start at scene left %d, got %d", m.sceneLeft, m1.shootingStarCol)
}

m1.shootingStarCol = m1.sceneRight + 1
updated, cmd = m1.Update(tickMsg(time.Now()))
if cmd == nil {
t.Fatal("expected tick command when advancing active shooting star")
}

m2 := updated.(animationModel)
if m2.shootingStarActive {
t.Fatal("expected shooting star to deactivate after leaving the scene")
}
}

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

m := animationModel{
arranged: []building{
{height: 4, char: '▓', color: Cyan, extLabel: ".go", gap: 1},
{height: 4, char: '▒', color: Yellow, extLabel: "A-1", gap: 1},
},
width: 24,
leftMargin: 2,
sceneLeft: 1,
sceneRight: 20,
sceneWidth: 19,
starPositions: [][2]int{{0, 2}},
moonCol: 12,
maxBuildingHeight: 4,
phase: 2,
visibleRows: 6,
shootingStarActive: true,
shootingStarRow: 0,
shootingStarCol: 4,
}

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

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