4e01b64bbd
- task domain model + exhaustive §4.4 status machine tests (single source of truth) - per-customer polling workers reconciled every 30s from the customers collection; panic-safe runs; manual sync-now trigger; status provider for the admin panel - §5.3 upsert keyed on (system, key, customerId): refreshes content while status=imported, records upstream_changed timeline + notification after - attachments cached into GridFS via connector-authorized downloads - orphaned tickets flagged with timeline + consultant notification, never deleted; idempotent across polls - consultant identity from users.extra.ticketingIdentities per system - notifications store (insert/list/unread-count/mark-read) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
package domain
|
|
|
|
import "testing"
|
|
|
|
// TestTransitionMatrixExhaustive enumerates every (from, to) pair and pins
|
|
// the §4.4 machine exactly: the allowed set below is the complete truth.
|
|
func TestTransitionMatrixExhaustive(t *testing.T) {
|
|
allowed := map[TaskStatus]map[TaskStatus]bool{
|
|
StatusImported: {StatusAtomizing: true, StatusArchived: true},
|
|
StatusAtomizing: {StatusAtomized: true, StatusImported: true, StatusArchived: true},
|
|
StatusAtomized: {StatusPublished: true, StatusAtomizing: true, StatusArchived: true},
|
|
StatusPublished: {StatusClaimRequested: true, StatusAssigned: true, StatusArchived: true},
|
|
StatusClaimRequested: {StatusAssigned: true, StatusPublished: true, StatusArchived: true},
|
|
StatusAssigned: {StatusInProgress: true, StatusInReview: true, StatusPublished: true, StatusArchived: true},
|
|
StatusInProgress: {StatusInReview: true, StatusPublished: true, StatusArchived: true},
|
|
StatusInReview: {StatusApproved: true, StatusChangesRequested: true, StatusAssigned: true, StatusArchived: true},
|
|
StatusChangesRequested: {StatusInProgress: true, StatusArchived: true},
|
|
StatusApproved: {StatusArchived: true},
|
|
StatusArchived: {},
|
|
}
|
|
|
|
for _, from := range AllStatuses {
|
|
for _, to := range AllStatuses {
|
|
want := allowed[from][to]
|
|
if got := CanTransition(from, to); got != want {
|
|
t.Errorf("CanTransition(%s, %s) = %v, want %v", from, to, got, want)
|
|
}
|
|
err := Transition(from, to)
|
|
if want && err != nil {
|
|
t.Errorf("Transition(%s, %s) unexpected error %v", from, to, err)
|
|
}
|
|
if !want && err == nil {
|
|
t.Errorf("Transition(%s, %s) should error", from, to)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTransitionRejectsUnknownStatuses(t *testing.T) {
|
|
if CanTransition("bogus", StatusArchived) {
|
|
t.Error("unknown from-status must be rejected")
|
|
}
|
|
if CanTransition(StatusImported, "bogus") {
|
|
t.Error("unknown to-status must be rejected")
|
|
}
|
|
if CanTransition(StatusImported, StatusImported) {
|
|
t.Error("self-transition must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestStatusValid(t *testing.T) {
|
|
for _, s := range AllStatuses {
|
|
if !s.Valid() {
|
|
t.Errorf("%s should be valid", s)
|
|
}
|
|
}
|
|
if TaskStatus("nope").Valid() {
|
|
t.Error("nope should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestComputeBounty(t *testing.T) {
|
|
tests := []struct {
|
|
coeff, budget, want float64
|
|
}{
|
|
{0.25, 1000, 250},
|
|
{0.2, 1000, 200},
|
|
{1.0 / 3.0, 1000, 333.33},
|
|
{0.333, 100, 33.3},
|
|
{0, 1000, 0},
|
|
{1, 0, 0},
|
|
{0.01, 99.99, 1.0}, // 0.9999 rounds to 1.00
|
|
{0.005, 100, 0.5},
|
|
{2.0, 500, 1000}, // extension coefficients may exceed 1 (§5.1)
|
|
}
|
|
for _, tt := range tests {
|
|
if got := ComputeBounty(tt.coeff, tt.budget); got != tt.want {
|
|
t.Errorf("ComputeBounty(%v, %v) = %v, want %v", tt.coeff, tt.budget, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTaskHelpers(t *testing.T) {
|
|
task := &Task{
|
|
Assignee: &Assignee{Kind: "human", UserID: "dev1"},
|
|
ClaimRequests: []ClaimRequest{{DeveloperID: "dev2"}},
|
|
}
|
|
if !task.IsAssignedTo("dev1") || task.IsAssignedTo("dev2") {
|
|
t.Error("IsAssignedTo wrong")
|
|
}
|
|
if !task.HasClaimFrom("dev2") || task.HasClaimFrom("dev1") {
|
|
t.Error("HasClaimFrom wrong")
|
|
}
|
|
aiTask := &Task{Assignee: &Assignee{Kind: "ai", JobID: "wp_1"}}
|
|
if aiTask.IsAssignedTo("dev1") {
|
|
t.Error("ai assignee must not match human checks")
|
|
}
|
|
}
|