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.000000") if err := pool.QueryRow(ctx, ` INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status) VALUES ($1, 'Reminder Test ' || $2, 'active', 2, 'active') RETURNING id `, code, 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) } }