package roadmapsync import ( "testing" "github.com/nomadcode/nomadcode-core/internal/workitem" ) func reconcileIdentity() Identity { return Identity{ Shape: ShapeMilestone, RoadmapMilestonePath: "agent-roadmap/phase/p/milestones/m.md", Provider: workitem.ProviderID("plane"), Tenant: "general", Project: "NOMAD", WorkItemID: "NOMAD-13", } } func TestReconcile_SamePlaneTicketReusesIdentity(t *testing.T) { id := reconcileIdentity() res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: id}, CompletedSteps: map[Step]bool{ StepDevelopMatched: true, }, }) if res.Action != ActionResume { t.Fatalf("action: got %q, want resume (%s)", res.Action, res.Reason) } if !res.IdentityKept { t.Error("expected identity reuse for the same Plane ticket") } if res.NextStep != StepOriginalCommentPreserved { t.Errorf("next step: got %q, want original_comment_preserved", res.NextStep) } } func TestReconcile_SameMilestonePathReusesIdentity(t *testing.T) { id := reconcileIdentity() // Existing row found via the Milestone path: same path + provider work item. res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: id}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume || !res.IdentityKept { t.Fatalf("expected resume with identity kept, got %q kept=%v (%s)", res.Action, res.IdentityKept, res.Reason) } if res.NextStep != StepDevelopMatched { t.Errorf("fresh ledger should resume at develop_matched, got %q", res.NextStep) } } func TestReconcile_ProviderRevisionMismatchConflicts(t *testing.T) { id := reconcileIdentity() res := ReconcileCreationCycle(ReconcileInput{ Identity: id, ProviderRevision: "rev-2", Existing: ExistingIdentity{ Found: true, Identity: id, ProviderRevision: "rev-1", }, }) if res.Action != ActionConflict { t.Fatalf("action: got %q, want conflict (%s)", res.Action, res.Reason) } if !res.IdentityKept { t.Error("a conflict on a matched identity should still report the identity as kept") } } func TestReconcile_RoadmapRevisionMismatchConflicts(t *testing.T) { id := reconcileIdentity() res := ReconcileCreationCycle(ReconcileInput{ Identity: id, RoadmapRevision: "abc", Existing: ExistingIdentity{ Found: true, Identity: id, RoadmapRevision: "def", }, }) if res.Action != ActionConflict { t.Fatalf("action: got %q, want conflict (%s)", res.Action, res.Reason) } } func TestReconcile_NomadCodeActorIsNotNewTrigger(t *testing.T) { id := reconcileIdentity() res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Actor: "nomadcode", SelfActor: "nomadcode", Existing: ExistingIdentity{Found: true, Identity: id}, }) if res.Action != ActionSkipSelfMutation { t.Fatalf("action: got %q, want skip_self_mutation (%s)", res.Action, res.Reason) } } func TestReconcile_DifferentTenantIsAllowedAsSafeAttributeUpdate(t *testing.T) { id := reconcileIdentity() existing := id existing.Tenant = "other-workspace" res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: existing}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume { t.Fatalf("tenant mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason) } if !res.IdentityKept { t.Error("expected identity to be kept") } } func TestReconcile_DifferentProjectIsAllowedAsSafeAttributeUpdate(t *testing.T) { id := reconcileIdentity() existing := id existing.Project = "OTHER" res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: existing}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume { t.Fatalf("project mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason) } if !res.IdentityKept { t.Error("expected identity to be kept") } } func TestReconcile_DifferentWorkItemIDIsAllowedAsSafeAttributeUpdate(t *testing.T) { id := reconcileIdentity() existing := id existing.WorkItemID = "NOMAD-OTHER" res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: existing}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume { t.Fatalf("work item id mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason) } } func TestReconcile_DifferentPathIsAllowedAsSafeAttributeUpdate(t *testing.T) { id := reconcileIdentity() existing := id existing.RoadmapMilestonePath = "agent-roadmap/phase/other/milestones/m.md" res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: existing}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume { t.Fatalf("path mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason) } } func TestReconcile_AllStepsCompleteReturnsComplete(t *testing.T) { id := reconcileIdentity() res := ReconcileCreationCycle(ReconcileInput{ Identity: id, Existing: ExistingIdentity{Found: true, Identity: id}, CompletedSteps: map[Step]bool{ StepDevelopMatched: true, StepOriginalCommentPreserved: true, StepPlaneBodyUpdated: true, StepPlaneTodoMoved: true, }, }) if res.Action != ActionComplete { t.Fatalf("action: got %q, want complete (%s)", res.Action, res.Reason) } } func TestReconcile_InvalidIdentityConflicts(t *testing.T) { res := ReconcileCreationCycle(ReconcileInput{ Identity: Identity{Provider: workitem.ProviderID("plane")}, // missing path + work item id }) if res.Action != ActionConflict { t.Fatalf("action: got %q, want conflict (%s)", res.Action, res.Reason) } } func TestReconcile_FreshCycleNoExistingResumesAtFirstStep(t *testing.T) { res := ReconcileCreationCycle(ReconcileInput{ Identity: reconcileIdentity(), Existing: ExistingIdentity{Found: false}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume || res.NextStep != StepDevelopMatched { t.Fatalf("fresh cycle should resume at develop_matched, got %q step=%q", res.Action, res.NextStep) } if res.IdentityKept { t.Error("no existing identity should report IdentityKept=false") } } func TestReconcile_FirstCycleRevisionUnknownDoesNotConflict(t *testing.T) { id := reconcileIdentity() // Existing identity has no recorded revision yet; a trigger that carries one // must not be blocked. res := ReconcileCreationCycle(ReconcileInput{ Identity: id, ProviderRevision: "rev-1", Existing: ExistingIdentity{Found: true, Identity: id, ProviderRevision: ""}, CompletedSteps: map[Step]bool{}, }) if res.Action != ActionResume { t.Fatalf("unknown persisted revision should not conflict, got %q (%s)", res.Action, res.Reason) } }