Substrate changes that turn /inbox from approvals-only into the unified notification surface m asked for. - Migration 126: paliad.users.inbox_seen_at (high-watermark read cursor; pending approval_requests bypass it per design §3). - KnownProjectEventKinds gains note_created, our_side_changed, deadline_updated/deleted, deadlines_imported. New InboxProjectEventKinds curated subset (head's Q1=A lock). - InboxSystemView spans [approval_request, project_event]; defaults to past 30 days, newest first, row_action="inbox". - view_service.allowedProjectEventKinds drops *_approval_* audits when ApprovalRequest is also in spec.Sources (no double-count). - RunSpec resolves the caller's inbox_seen_at once and threads it through viewSpecBounds; runProjectEvents excludes self-authored events and rows older than the cursor when unread_only is set. Decided approval_requests follow the cursor; pending always survives. - ApprovalService.UnseenInboxCountForUser (unified badge count) + MarkInboxSeen + InboxSeenAt service methods. - GET /api/inbox/count returns the unified count; new POST /api/inbox/mark-all-seen advances the cursor (optional up_to=). Tests cover the InboxSystemView shape, the audit-dedup helper, the isApprovalAuditKind matcher, and the no-narrow-no-approvals nil path.
112 lines
3.4 KiB
Go
112 lines
3.4 KiB
Go
package services
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
// t-paliad-249 — Slice A. Ensures the *_approval_* audit kinds are
|
|
// dropped from the project_event read path when ApprovalRequest is
|
|
// also fanning out, so the same fact isn't shown twice in /inbox.
|
|
|
|
func TestAllowedProjectEventKinds_DedupsApprovalAudits(t *testing.T) {
|
|
spec := FilterSpec{
|
|
Version: SpecVersion,
|
|
Sources: []DataSource{SourceApprovalRequest, SourceProjectEvent},
|
|
Predicates: map[DataSource]Predicates{
|
|
SourceProjectEvent: {ProjectEvent: &ProjectEventPredicates{
|
|
EventTypes: []string{
|
|
"deadline_created",
|
|
"deadline_approval_requested",
|
|
"appointment_approval_approved",
|
|
"approval_decided",
|
|
"note_created",
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
got := allowedProjectEventKinds(spec)
|
|
if slices.Contains(got, "deadline_approval_requested") {
|
|
t.Errorf("must drop deadline_approval_requested, got %v", got)
|
|
}
|
|
if slices.Contains(got, "appointment_approval_approved") {
|
|
t.Errorf("must drop appointment_approval_approved, got %v", got)
|
|
}
|
|
if slices.Contains(got, "approval_decided") {
|
|
t.Errorf("must drop approval_decided, got %v", got)
|
|
}
|
|
if !slices.Contains(got, "deadline_created") {
|
|
t.Errorf("must keep deadline_created, got %v", got)
|
|
}
|
|
if !slices.Contains(got, "note_created") {
|
|
t.Errorf("must keep note_created, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAllowedProjectEventKinds_NoDedupWhenApprovalsAbsent(t *testing.T) {
|
|
spec := FilterSpec{
|
|
Version: SpecVersion,
|
|
Sources: []DataSource{SourceProjectEvent},
|
|
Predicates: map[DataSource]Predicates{
|
|
SourceProjectEvent: {ProjectEvent: &ProjectEventPredicates{
|
|
EventTypes: []string{
|
|
"deadline_created",
|
|
"deadline_approval_requested",
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
got := allowedProjectEventKinds(spec)
|
|
if !slices.Contains(got, "deadline_approval_requested") {
|
|
t.Errorf("Verlauf-style spec without approvals source should keep audit kinds, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAllowedProjectEventKinds_NilWhenNoPredicateAndNoDedup(t *testing.T) {
|
|
spec := FilterSpec{
|
|
Version: SpecVersion,
|
|
Sources: []DataSource{SourceProjectEvent},
|
|
}
|
|
if got := allowedProjectEventKinds(spec); got != nil {
|
|
t.Errorf("expected nil (all kinds), got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAllowedProjectEventKinds_FillsImplicitListWhenDedup(t *testing.T) {
|
|
// When approvals are in sources but the caller didn't explicitly
|
|
// narrow EventTypes, the helper must materialise the curated set
|
|
// minus audit duplicates so the WHERE clause filters them out.
|
|
spec := FilterSpec{
|
|
Version: SpecVersion,
|
|
Sources: []DataSource{SourceApprovalRequest, SourceProjectEvent},
|
|
}
|
|
got := allowedProjectEventKinds(spec)
|
|
if got == nil {
|
|
t.Fatal("expected explicit kind list, got nil")
|
|
}
|
|
for _, k := range got {
|
|
if isApprovalAuditKind(k) {
|
|
t.Errorf("audit kind %q leaked through dedup", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsApprovalAuditKind(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"deadline_approval_requested": true,
|
|
"appointment_approval_approved": true,
|
|
"appointment_approval_rejected": true,
|
|
"deadline_approval_changes_suggested": true,
|
|
"approval_decided": true,
|
|
"deadline_created": false,
|
|
"note_created": false,
|
|
"our_side_changed": false,
|
|
"project_archived": false,
|
|
}
|
|
for kind, want := range cases {
|
|
if got := isApprovalAuditKind(kind); got != want {
|
|
t.Errorf("isApprovalAuditKind(%q) = %v, want %v", kind, got, want)
|
|
}
|
|
}
|
|
}
|