package issue import ( "reflect" "strings" "testing" ) const sample = `--- id: wire-sqlc-appclick state: open labels: [type/task, tech/sql] assignees: [naudachu] milestone: v0.2 depends: [migrate-schema] origin: gitea gitea: claude-skills/tea#42 synced: 2026-08-09T18:40:00Z --- # Wire sqlc into the appclick repo layer ## Summary Проводка sqlc. ## Spec none ## Motivation Ручной SQL расходится со схемой. ## Acceptance criteria - [x] сгенерирован код - [ ] тесты зелёные ` func TestFromTextReadsTheDomainAndCarriesTheRest(t *testing.T) { i := FromText(sample, "") if i.ID != "wire-sqlc-appclick" { t.Errorf("id = %q", i.ID) } if i.Title != "Wire sqlc into the appclick repo layer" { t.Errorf("title = %q", i.Title) } if want := []string{"type/task", "tech/sql"}; !reflect.DeepEqual(i.Labels, want) { t.Errorf("labels = %v, want %v", i.Labels, want) } if i.Type() != "task" { t.Errorf("type = %q", i.Type()) } if i.IsLocal() { t.Error("origin gitea must not read as local") } if i.Extra["gitea"] != "claude-skills/tea#42" { t.Errorf("foreign key lost: %v", i.Extra) } // The domain carries foreign keys; it must not learn to read them. if _, ok := i.Extra["labels"]; ok { t.Error("a domain key leaked into Extra") } if strings.Contains(i.Body, "# "+i.Title) { t.Error("the title heading was left in the body") } if !strings.HasPrefix(i.Body, SummarySection) { t.Errorf("body does not start at ## Summary: %q", head(i.Body)) } } func TestTextRoundTripsByteForByte(t *testing.T) { if got := FromText(sample, "").Text(); got != sample { t.Errorf("round trip changed the file:\n--- got ---\n%s\n--- want ---\n%s", got, sample) } } func TestMilestoneNoneIsTheEmptyMilestone(t *testing.T) { i := FromText("---\nid: x\nmilestone: none\n---\n# T\n\nbody\n", "") if i.Milestone != "" { t.Errorf("milestone = %q, want empty", i.Milestone) } if !strings.Contains(i.Text(), "milestone: none") { t.Error("an empty milestone must render back as none") } } func TestBareListValueIsTheSameStatementAsABracketedOne(t *testing.T) { i := FromText("---\nid: x\nlabels: type/bug\n---\n# T\n\nbody\n", "") if want := []string{"type/bug"}; !reflect.DeepEqual(i.Labels, want) { t.Errorf("labels = %v, want %v", i.Labels, want) } } func TestFileNameWinsOverTheMetadataID(t *testing.T) { // The store names the file after the slug, so a hand-edited `id:` that // disagrees with it is the one that is wrong. if got := FromText(sample, "renamed-by-hand").ID; got != "renamed-by-hand" { t.Errorf("id = %q", got) } } func TestSlugify(t *testing.T) { cases := map[string]string{ // Truncation cuts back to the last dash, so a slug never ends in half // a word — even when the limit happened to land on a boundary. "Wire sqlc into the appclick repo layer": "wire-sqlc-into-the-appclick", "Fix tea-guard crash": "fix-tea-guard-crash", " Trailing --- dashes ": "trailing-dashes", // Titles are English by format rule; anything else is dropped rather // than transliterated, and an empty result is not an id. "Крашится гвард": "issue", "": "issue", } for in, want := range cases { if got := Slugify(in, 32); got != want { t.Errorf("Slugify(%q) = %q, want %q", in, got, want) } } } func head(s string) string { if len(s) > 40 { return s[:40] } return s } func TestSectionBodyStopsAtTheNextHeading(t *testing.T) { body := "## Summary\nодин\nдва\n\n## Spec\nnone\n" if got := SectionBody(body, "## Summary"); got != "один\nдва" { t.Errorf("Summary = %q", got) } if got := SectionBody(body, SpecSection); got != "none" { t.Errorf("Spec = %q", got) } if got := SectionBody(body, "## Missing"); got != "" { t.Errorf("missing section = %q, want empty", got) } }