package main import ( "context" "testing" "time" "git.cin.su/m/Assistant/contracts/authority" commitcontract "git.cin.su/m/Assistant/contracts/commit" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/platform/ids" ) type memoryDueRepo struct { item continuity.ProspectiveItem attention continuity.AttentionItem } func (r *memoryDueRepo) DueArmed(context.Context, int64, int) ([]continuity.ProspectiveItem, error) { if r.item.State == continuity.ProspectiveArmed { return []continuity.ProspectiveItem{r.item}, nil } return nil, nil } func (r *memoryDueRepo) ClaimDueReminder(_ context.Context, id continuity.ProspectiveItemID, revision continuity.ProspectiveRevision, attentionID continuity.AttentionItemID, now int64) (continuity.ProspectiveItem, continuity.AttentionItem, error) { if r.item.ID != id || r.item.Revision != revision || r.item.State != continuity.ProspectiveArmed { return continuity.ProspectiveItem{}, continuity.AttentionItem{}, nil } r.item.State = continuity.ProspectiveDelivered r.item.Revision += 3 r.attention = continuity.AttentionItem{ID: attentionID, OwnerPrincipalID: r.item.OwnerPrincipalID, ScopeID: r.item.ScopeID, SourceKind: continuity.AttentionSourceProspectiveReminder, SourceProspectiveID: r.item.ID, Level: continuity.AttentionDue, State: continuity.AttentionPending, Summary: r.item.Summary, DueAtUnixMillis: r.item.TriggerAtUnixMillis, CreatedAtUnixMillis: now, Revision: 1} return r.item, r.attention, nil } func (r *memoryDueRepo) PendingAttention(context.Context, int) ([]continuity.AttentionItem, error) { if r.attention.State == continuity.AttentionPending { return []continuity.AttentionItem{r.attention}, nil } return nil, nil } func (r *memoryDueRepo) MarkAttentionDelivering(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision) (continuity.AttentionItem, error) { if r.attention.ID == id && r.attention.Revision == revision { r.attention.State = continuity.AttentionDelivering r.attention.Revision++ } return r.attention, nil } func (r *memoryDueRepo) MarkAttentionDelivered(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string, deliveredAt int64) (continuity.AttentionItem, error) { if r.attention.ID == id && r.attention.Revision == revision { r.attention.State = continuity.AttentionDelivered r.attention.Revision++ r.attention.ActionIntentID = intentID r.attention.AuthorizedActionID = authorizedID r.attention.ActionAttemptID = attemptID r.attention.DeliveryOutcome = outcome r.attention.DeliveredAtUnixMillis = deliveredAt } return r.attention, nil } func (r *memoryDueRepo) MarkAttentionFailed(_ context.Context, id continuity.AttentionItemID, revision continuity.AttentionRevision, intentID continuity.CommittedActionIntentID, authorizedID continuity.CommittedAuthorizedActionID, attemptID continuity.CommittedActionAttemptID, outcome string) (continuity.AttentionItem, error) { if r.attention.ID == id && r.attention.Revision == revision { r.attention.State = continuity.AttentionFailed r.attention.Revision++ r.attention.ActionIntentID = intentID r.attention.AuthorizedActionID = authorizedID r.attention.ActionAttemptID = attemptID r.attention.DeliveryOutcome = outcome } return r.attention, nil } func (r *memoryDueRepo) Load(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) { return r.item, nil } type fakeDueNotifier struct{ calls int } func (n *fakeDueNotifier) NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error) { n.calls++ return dueNotificationResponse{ActionIntentID: "intent", AuthorizedActionID: "authorized", ActionAttemptID: "attempt", State: commitcontract.ActionSucceeded}, nil } type unknownDueNotifier struct{ calls int } func (n *unknownDueNotifier) NotifyDue(context.Context, continuity.ProspectiveItem) (dueNotificationResponse, error) { n.calls++ return dueNotificationResponse{ActionIntentID: "intent", AuthorizedActionID: "authorized", ActionAttemptID: "attempt", State: commitcontract.ActionUnknownOutcome}, nil } func TestDueReminderProducesAttentionAndNotificationOnce(t *testing.T) { repo := &memoryDueRepo{item: continuity.ProspectiveItem{ID: "prospective", OwnerPrincipalID: identity.PrincipalID("owner"), ScopeID: authority.ScopeID("scope"), Summary: "проверить уведомление", TriggerAtUnixMillis: 10, State: continuity.ProspectiveArmed, Revision: 1}} notifier := &fakeDueNotifier{} now := time.UnixMilli(20) worker := dueReminderWorker{repository: repo, commit: notifier, now: func() time.Time { return now }, newID: func() (ids.ID, error) { return ids.ID("attention"), nil }, limit: 10} worker.process(context.Background()) worker.process(context.Background()) if repo.item.State != continuity.ProspectiveDelivered || repo.attention.State != continuity.AttentionDelivered { t.Fatalf("unexpected states: prospective=%s attention=%s", repo.item.State, repo.attention.State) } if notifier.calls != 1 { t.Fatalf("expected one notification call, got %d", notifier.calls) } if repo.attention.ActionIntentID == "" || repo.attention.DeliveredAtUnixMillis == 0 { t.Fatal("delivery evidence was not persisted") } } func TestDueReminderUnknownOutcomeDoesNotMarkAttentionDelivered(t *testing.T) { repo := &memoryDueRepo{item: continuity.ProspectiveItem{ID: "prospective", OwnerPrincipalID: identity.PrincipalID("owner"), ScopeID: authority.ScopeID("scope"), Summary: "проверить уведомление", TriggerAtUnixMillis: 10, State: continuity.ProspectiveArmed, Revision: 1}} notifier := &unknownDueNotifier{} now := time.UnixMilli(20) worker := dueReminderWorker{repository: repo, commit: notifier, now: func() time.Time { return now }, newID: func() (ids.ID, error) { return ids.ID("attention"), nil }, limit: 10} worker.process(context.Background()) if repo.attention.State != continuity.AttentionFailed { t.Fatalf("unknown outcome must not be delivered, got %s", repo.attention.State) } if repo.attention.DeliveryOutcome != string(commitcontract.ActionUnknownOutcome) { t.Fatalf("missing unknown outcome evidence: %#v", repo.attention) } }