Files
Agent-System/internal/notification/reminder_test.go
T
weichenandCursor b6035991d4 feat(notification): add 24h stage advance reminder and confirm on advance.
Poll pending requests every 15 minutes, emit deduplicated escalation notifications, and close requests when creator confirms stage advance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

111 lines
3.5 KiB
Go

package notification_test
import (
"context"
"testing"
"time"
"chat-agent-system/internal/notification"
)
func TestPG_StageAdvanceReminder24h(t *testing.T) {
pool := openPool(t)
defer pool.Close()
notification.SetStageReminderAfterForTest(1 * time.Millisecond)
t.Cleanup(func() {
notification.SetStageReminderAfterForTest(24 * time.Hour)
})
svc := notification.NewService(pool)
ctx := context.Background()
var projectID, instanceID, stageInstID int64
code := "remind-" + time.Now().Format("150405.000")
if err := pool.QueryRow(ctx, `
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status)
VALUES ($1, 'Reminder Test', 'active', 2, 'active') RETURNING id
`, code).Scan(&projectID); err != nil {
t.Fatalf("insert project: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
})
var wfVersionID int64
if err := pool.QueryRow(ctx, `SELECT id FROM workflow_versions ORDER BY id DESC LIMIT 1`).Scan(&wfVersionID); err != nil {
t.Skip("no workflow version")
}
if err := pool.QueryRow(ctx, `
INSERT INTO project_workflow_instances (project_id, workflow_version_id, instance_no, status, config_revision, plan_hash, config_snapshot, created_by)
VALUES ($1, $2, 1, 'active', 5, 'h', '{}'::jsonb, 2) RETURNING id
`, projectID, wfVersionID).Scan(&instanceID); err != nil {
t.Fatalf("insert instance: %v", err)
}
if err := pool.QueryRow(ctx, `
INSERT INTO project_stage_instances (workflow_instance_id, stage_key, position, enabled, status)
VALUES ($1, 'biz_intake', 0, true, 'active') RETURNING id
`, instanceID).Scan(&stageInstID); err != nil {
t.Fatalf("insert stage: %v", err)
}
created, err := svc.EmitStageReadyToAdvance(ctx, 2, notification.StageReadyPayload{
ProjectID: projectID,
WorkflowInstanceID: instanceID,
StageInstanceID: stageInstID,
StageKey: "biz_intake",
StageName: "商务摸底",
ConfigRevision: 5,
RequiredDocCount: 1,
ApprovedDocCount: 1,
})
if err != nil || !created {
t.Fatalf("initial emit: created=%v err=%v", created, err)
}
_, err = pool.Exec(ctx, `
UPDATE stage_advance_requests SET requested_at = now() - interval '25 hours'
WHERE stage_instance_id = $1 AND readiness_revision = 5
`, stageInstID)
if err != nil {
t.Fatalf("backdate request: %v", err)
}
if _, err := svc.ProcessStageAdvanceReminders(ctx); err != nil {
t.Fatalf("process reminders: %v", err)
}
var remindCount int
err = pool.QueryRow(ctx, `
SELECT COUNT(*) FROM notifications
WHERE user_id = 2 AND project_id = $1 AND type = $2
`, projectID, notification.TypeStageReadyReminder24).Scan(&remindCount)
if err != nil {
t.Fatalf("count reminders: %v", err)
}
if remindCount != 1 {
t.Fatalf("expected 1 reminder notification for test project, got %d", remindCount)
}
if _, err := svc.ProcessStageAdvanceReminders(ctx); err != nil {
t.Fatalf("process reminders again: %v", err)
}
err = pool.QueryRow(ctx, `
SELECT COUNT(*) FROM notifications
WHERE user_id = 2 AND project_id = $1 AND type = $2
`, projectID, notification.TypeStageReadyReminder24).Scan(&remindCount)
if err != nil || remindCount != 1 {
t.Fatalf("expected deduped reminder count 1, got %d err=%v", remindCount, err)
}
if err := svc.ConfirmStageAdvanceRequest(ctx, stageInstID, 5, 2); err != nil {
t.Fatalf("confirm: %v", err)
}
if _, err := svc.ProcessStageAdvanceReminders(ctx); err != nil {
t.Fatalf("process after confirm: %v", err)
}
}