package main import ( "path/filepath" "strings" "testing" ) // testdataDir returns the path to the testdata directory. func testdataDir() string { return "testdata" } // TestParseIssue_BasicFields verifies that ParseIssue correctly reads frontmatter. func TestParseIssue_BasicFields(t *testing.T) { path := filepath.Join(testdataDir(), "issues", "0099-sample.md") iss, err := ParseIssue(path) if err != nil { t.Fatalf("ParseIssue: %v", err) } if iss.ID != "0099" { t.Errorf("ID: got %q, want %q", iss.ID, "0099") } if iss.Title != "datahub app (launcher central para todas las apps)" { t.Errorf("Title: got %q", iss.Title) } if iss.Status != "pendiente" { t.Errorf("Status: got %q, want %q", iss.Status, "pendiente") } if iss.Priority != "alta" { t.Errorf("Priority: got %q, want %q", iss.Priority, "alta") } if iss.Type != "feature" { t.Errorf("Type: got %q, want %q", iss.Type, "feature") } if len(iss.Domain) != 1 || iss.Domain[0] != "apps-infra" { t.Errorf("Domain: got %v, want [apps-infra]", iss.Domain) } if iss.Path != path { t.Errorf("Path: got %q, want %q", iss.Path, path) } if iss.Body == "" { t.Error("Body should not be empty") } } // TestParseIssue_AcceptancePct verifies checkbox counting in ## Acceptance. func TestParseIssue_AcceptancePct(t *testing.T) { path := filepath.Join(testdataDir(), "issues", "0099-sample.md") iss, err := ParseIssue(path) if err != nil { t.Fatalf("ParseIssue: %v", err) } // In 0099-sample.md: 2 checked, 2 unchecked = 50% if iss.AcceptancePct != 50 { t.Errorf("AcceptancePct: got %d, want 50", iss.AcceptancePct) } } // TestParseIssue_DoDPct verifies checkbox counting in ## Definition of Done. func TestParseIssue_DoDPct(t *testing.T) { path := filepath.Join(testdataDir(), "issues", "0099-sample.md") iss, err := ParseIssue(path) if err != nil { t.Fatalf("ParseIssue: %v", err) } // In 0099-sample.md DoD: 1 checked, 3 unchecked = 25% if iss.DoDPct != 25 { t.Errorf("DoDPct: got %d, want 25", iss.DoDPct) } } // TestParseIssue_EmptyBody handles a file with no body. func TestParseIssue_EmptyBody(t *testing.T) { path := filepath.Join(testdataDir(), "issues", "0051-missing-dep.md") iss, err := ParseIssue(path) if err != nil { t.Fatalf("ParseIssue: %v", err) } if iss.ID != "0051" { t.Errorf("ID: got %q, want %q", iss.ID, "0051") } // No checkboxes — both pcts should be 0 if iss.AcceptancePct != 0 { t.Errorf("AcceptancePct: got %d, want 0", iss.AcceptancePct) } } // TestDepsResolved_AllCompletado verifies DepsResolved=true when all deps are completado. func TestDepsResolved_AllCompletado(t *testing.T) { issues := []Issue{ {ID: "0001", Status: "completado"}, {ID: "0050", Status: "pendiente", Depends: []string{"0001"}}, } ComputeDepsResolved(issues) if !issues[1].DepsResolved { t.Error("DepsResolved should be true when all deps are completado") } } // TestDepsResolved_DepNotCompletado verifies DepsResolved=false when dep not completado. func TestDepsResolved_DepNotCompletado(t *testing.T) { issues := []Issue{ {ID: "0001", Status: "pendiente"}, // not completado {ID: "0050", Status: "pendiente", Depends: []string{"0001"}}, } ComputeDepsResolved(issues) if issues[1].DepsResolved { t.Error("DepsResolved should be false when dep is not completado") } } // TestDepsResolved_DepMissing verifies DepsResolved=false when dep is not in list. func TestDepsResolved_DepMissing(t *testing.T) { issues := []Issue{ {ID: "0051", Status: "pendiente", Depends: []string{"9999"}}, } ComputeDepsResolved(issues) if issues[0].DepsResolved { t.Error("DepsResolved should be false when dep is missing from issue list") } } // TestDepsResolved_NoDeps verifies DepsResolved=true when there are no deps. func TestDepsResolved_NoDeps(t *testing.T) { issues := []Issue{ {ID: "0099", Status: "pendiente", Depends: []string{}}, } ComputeDepsResolved(issues) if !issues[0].DepsResolved { t.Error("DepsResolved should be true when there are no deps") } } // TestLoadAllIssues_SkipsSkippable verifies that README.md and template.md are skipped. func TestLoadAllIssues_SkipsSkippable(t *testing.T) { issues, err := loadIssuesFromTestdata() if err != nil { t.Fatalf("load: %v", err) } for _, iss := range issues { base := filepath.Base(iss.Path) lower := strings.ToLower(base) if strings.Contains(lower, "readme") || strings.Contains(lower, "template") { t.Errorf("should have skipped %s", base) } } } // TestLoadAllIssues_CountsCorrect verifies we load the expected number of fixtures. func TestLoadAllIssues_CountsCorrect(t *testing.T) { issues, err := loadIssuesFromTestdata() if err != nil { t.Fatalf("load: %v", err) } // We have 4 fixture issues: 0099, 0001, 0050, 0051 if len(issues) != 4 { t.Errorf("expected 4 issues, got %d", len(issues)) } } // TestParseFlow_BasicFields verifies that ParseFlow reads frontmatter correctly. func TestParseFlow_BasicFields(t *testing.T) { path := filepath.Join(testdataDir(), "flows", "0001-sample.md") fl, err := ParseFlow(path) if err != nil { t.Fatalf("ParseFlow: %v", err) } if fl.ID != "0001" { t.Errorf("ID: got %q, want %q", fl.ID, "0001") } if fl.Name != "hn-top-stories" { t.Errorf("Name: got %q, want %q", fl.Name, "hn-top-stories") } if fl.Status != "pending" { t.Errorf("Status: got %q, want %q", fl.Status, "pending") } if fl.Risk != "low" { t.Errorf("Risk: got %q, want %q", fl.Risk, "low") } if len(fl.Apps) != 2 { t.Errorf("Apps: got %d apps, want 2", len(fl.Apps)) } } // TestParseFlow_AcceptancePct verifies checkbox counting in flow Acceptance. func TestParseFlow_AcceptancePct(t *testing.T) { path := filepath.Join(testdataDir(), "flows", "0001-sample.md") fl, err := ParseFlow(path) if err != nil { t.Fatalf("ParseFlow: %v", err) } // 1 checked, 2 unchecked = 33% if fl.AcceptancePct != 33 { t.Errorf("AcceptancePct: got %d, want 33", fl.AcceptancePct) } } // TestParseFlow_UserFacingPct verifies user-facing checkbox counting. func TestParseFlow_UserFacingPct(t *testing.T) { path := filepath.Join(testdataDir(), "flows", "0001-sample.md") fl, err := ParseFlow(path) if err != nil { t.Fatalf("ParseFlow: %v", err) } // 0 checked, 2 unchecked = 0% if fl.UserFacingPct != 0 { t.Errorf("UserFacingPct: got %d, want 0", fl.UserFacingPct) } } // TestNormalizeID pads short numeric IDs. func TestNormalizeID(t *testing.T) { tests := []struct { input string want string }{ {"99", "0099"}, {"0099", "0099"}, {"1", "0001"}, {"0001", "0001"}, {"0088a", "0088a"}, // non-numeric, no pad {"101", "0101"}, } for _, tt := range tests { got := normalizeID(tt.input) if got != tt.want { t.Errorf("normalizeID(%q) = %q, want %q", tt.input, got, tt.want) } } } // loadIssuesFromTestdata is a helper that loads from testdata/ instead of dev/issues/. func loadIssuesFromTestdata() ([]Issue, error) { dir := filepath.Join(testdataDir(), "issues") entries, err := filepath.Glob(filepath.Join(dir, "*.md")) if err != nil { return nil, err } var issues []Issue for _, path := range entries { name := filepath.Base(path) if isSkippable(name) { continue } iss, err := ParseIssue(path) if err != nil { return nil, err } issues = append(issues, iss) } return issues, nil }