218 lines
7 KiB
Go
218 lines
7 KiB
Go
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_DifferentWorkItemIDIsConflict(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 != ActionConflict {
|
|
t.Fatalf("work item id mismatch must be a conflict, got %q (%s)", res.Action, res.Reason)
|
|
}
|
|
if !res.IdentityKept {
|
|
t.Error("a conflict on an existing identity should report identity as kept")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|