package roadmapsync import ( "errors" "testing" ) func TestIdentityNormalizeValid(t *testing.T) { in := Identity{ RoadmapMilestonePath: " agent-roadmap/phase/p/milestones/m.md ", Provider: " plane ", Tenant: " general ", Project: " NOMAD ", WorkItemID: " NOMAD-13 ", } got, err := in.Normalize() if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Shape != ShapeMilestone { t.Errorf("shape = %q, want default milestone", got.Shape) } if got.RoadmapMilestonePath != "agent-roadmap/phase/p/milestones/m.md" { t.Errorf("path = %q", got.RoadmapMilestonePath) } if got.RoadmapItemID != "m" { t.Errorf("expected RoadmapItemID to be m, got %q", got.RoadmapItemID) } if got.Provider != "plane" || got.WorkItemID != "NOMAD-13" { t.Errorf("provider/work item not trimmed: %+v", got) } } func TestIdentityNormalizeRejectsMissingFields(t *testing.T) { cases := map[string]Identity{ "missing provider": { RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", WorkItemID: "NOMAD-13", }, "missing milestone path and item id": { Provider: "plane", WorkItemID: "NOMAD-13", }, "task without parent": { Shape: ShapeTask, RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: "plane", WorkItemID: "NOMAD-13", }, "unknown shape": { Shape: Shape("epic"), RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: "plane", WorkItemID: "NOMAD-13", }, } for name, in := range cases { t.Run(name, func(t *testing.T) { if _, err := in.Normalize(); !errors.Is(err, ErrInvalidIdentity) { t.Fatalf("err = %v, want ErrInvalidIdentity", err) } }) } } func TestIdentityMatches(t *testing.T) { base := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: "plane", WorkItemID: "NOMAD-13", } same := base same.Project = "NOMAD" // non-key field differs, still a match if !base.Matches(same) { t.Errorf("expected match ignoring non-key fields") } differentWorkItem := base differentWorkItem.WorkItemID = "NOMAD-99" if base.Matches(differentWorkItem) { t.Errorf("different work item id must not match") } invalid := Identity{Provider: "plane"} if base.Matches(invalid) { t.Errorf("invalid identity must not match") } } func TestIdentityMatchesStrict(t *testing.T) { base := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: "plane", Tenant: "general", Project: "NOMAD", WorkItemID: "NOMAD-13", } if !base.MatchesStrict(base) { t.Errorf("identical identity must strict-match") } differentTenant := base differentTenant.Tenant = "other-workspace" if base.MatchesStrict(differentTenant) { t.Errorf("different tenant must not strict-match") } differentProject := base differentProject.Project = "OTHER" if base.MatchesStrict(differentProject) { t.Errorf("different project must not strict-match") } differentWorkItem := base differentWorkItem.WorkItemID = "NOMAD-99" if base.MatchesStrict(differentWorkItem) { t.Errorf("different work item id must not strict-match") } // Matches (loose) ignores tenant/project; MatchesStrict must not. if !base.Matches(differentTenant) { t.Errorf("sanity: loose Matches should still ignore tenant") } } func TestIdentityMatchesByMilestoneIDAcrossPhaseMove(t *testing.T) { left := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p1/milestones/m.md", Provider: "plane", } right := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p2/milestones/m.md", // moved phase Provider: "plane", } if !left.Matches(right) { t.Errorf("moved path must match if MilestoneID is same") } if !left.MatchesStrict(right) { t.Errorf("moved path must strict-match if other key fields are same") } } func TestIdentityMatchesStrictAllowsProviderWorkItemAttributeChange(t *testing.T) { left := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: "plane", Tenant: "general", Project: "NOMAD", WorkItemID: "NOMAD-13", } right := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: "plane", Tenant: "general", Project: "NOMAD", WorkItemID: "NOMAD-13", // same work item id } if !left.MatchesStrict(right) { t.Errorf("same work item id must still strict-match") } } func TestIdentityNormalizeParsesSlugFromPath(t *testing.T) { in := Identity{ RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/some-slug.md", Provider: "plane", } got, err := in.Normalize() if err != nil { t.Fatalf("unexpected error: %v", err) } if got.RoadmapItemID != "some-slug" { t.Errorf("expected RoadmapItemID to be some-slug, got %q", got.RoadmapItemID) } } func TestRevisionNormalizeDefaultsDevelop(t *testing.T) { got := Revision{}.Normalize() if got.Branch != DefaultBranch { t.Errorf("branch = %q, want develop", got.Branch) } if !(Revision{Branch: "develop", Revision: "abc"}).OnDevelop() { t.Errorf("expected OnDevelop true") } if (Revision{Branch: "feature/x"}).OnDevelop() { t.Errorf("feature branch must not be OnDevelop") } }