Skip to main content

tuitbot_core/storage/
action_log.rs

1//! Append-only action log for auditing and status reporting.
2//!
3//! Records every action taken by the agent with timestamps,
4//! status, and optional metadata in JSON format.
5
6use super::accounts::DEFAULT_ACCOUNT_ID;
7use super::DbPool;
8use crate::error::StorageError;
9use std::collections::HashMap;
10
11/// An entry in the action audit log.
12#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize)]
13pub struct ActionLogEntry {
14    /// Internal auto-generated ID.
15    pub id: i64,
16    /// Action type: search, reply, tweet, thread, mention_check, cleanup, auth_refresh.
17    pub action_type: String,
18    /// Status: success, failure, or skipped.
19    pub status: String,
20    /// Human-readable description.
21    pub message: Option<String>,
22    /// JSON blob for flexible extra data.
23    pub metadata: Option<String>,
24    /// ISO-8601 UTC timestamp.
25    pub created_at: String,
26}
27
28/// Insert a new action log entry for a specific account.
29///
30/// The `metadata` parameter is a pre-serialized JSON string; the caller
31/// is responsible for serialization. The `created_at` field uses the SQL default.
32pub async fn log_action_for(
33    pool: &DbPool,
34    account_id: &str,
35    action_type: &str,
36    status: &str,
37    message: Option<&str>,
38    metadata: Option<&str>,
39) -> Result<(), StorageError> {
40    sqlx::query(
41        "INSERT INTO action_log (account_id, action_type, status, message, metadata) \
42         VALUES (?, ?, ?, ?, ?)",
43    )
44    .bind(account_id)
45    .bind(action_type)
46    .bind(status)
47    .bind(message)
48    .bind(metadata)
49    .execute(pool)
50    .await
51    .map_err(|e| StorageError::Query { source: e })?;
52
53    Ok(())
54}
55
56/// Insert a new action log entry.
57///
58/// The `metadata` parameter is a pre-serialized JSON string; the caller
59/// is responsible for serialization. The `created_at` field uses the SQL default.
60pub async fn log_action(
61    pool: &DbPool,
62    action_type: &str,
63    status: &str,
64    message: Option<&str>,
65    metadata: Option<&str>,
66) -> Result<(), StorageError> {
67    log_action_for(
68        pool,
69        DEFAULT_ACCOUNT_ID,
70        action_type,
71        status,
72        message,
73        metadata,
74    )
75    .await
76}
77
78/// Fetch action log entries since a given timestamp for a specific account,
79/// optionally filtered by type.
80///
81/// Results are ordered by `created_at` ascending.
82pub async fn get_actions_since_for(
83    pool: &DbPool,
84    account_id: &str,
85    since: &str,
86    action_type: Option<&str>,
87) -> Result<Vec<ActionLogEntry>, StorageError> {
88    match action_type {
89        Some(at) => sqlx::query_as::<_, ActionLogEntry>(
90            "SELECT * FROM action_log WHERE created_at >= ? AND action_type = ? \
91                 AND account_id = ? ORDER BY created_at ASC",
92        )
93        .bind(since)
94        .bind(at)
95        .bind(account_id)
96        .fetch_all(pool)
97        .await
98        .map_err(|e| StorageError::Query { source: e }),
99        None => sqlx::query_as::<_, ActionLogEntry>(
100            "SELECT * FROM action_log WHERE created_at >= ? \
101                 AND account_id = ? ORDER BY created_at ASC",
102        )
103        .bind(since)
104        .bind(account_id)
105        .fetch_all(pool)
106        .await
107        .map_err(|e| StorageError::Query { source: e }),
108    }
109}
110
111/// Fetch action log entries since a given timestamp, optionally filtered by type.
112///
113/// Results are ordered by `created_at` ascending.
114pub async fn get_actions_since(
115    pool: &DbPool,
116    since: &str,
117    action_type: Option<&str>,
118) -> Result<Vec<ActionLogEntry>, StorageError> {
119    get_actions_since_for(pool, DEFAULT_ACCOUNT_ID, since, action_type).await
120}
121
122/// Get counts of each action type since a given timestamp for a specific account.
123///
124/// Returns a HashMap mapping action types to their counts.
125pub async fn get_action_counts_since_for(
126    pool: &DbPool,
127    account_id: &str,
128    since: &str,
129) -> Result<HashMap<String, i64>, StorageError> {
130    let rows: Vec<(String, i64)> = sqlx::query_as(
131        "SELECT action_type, COUNT(*) as count FROM action_log \
132         WHERE created_at >= ? AND account_id = ? GROUP BY action_type",
133    )
134    .bind(since)
135    .bind(account_id)
136    .fetch_all(pool)
137    .await
138    .map_err(|e| StorageError::Query { source: e })?;
139
140    Ok(rows.into_iter().collect())
141}
142
143/// Get counts of each action type since a given timestamp.
144///
145/// Returns a HashMap mapping action types to their counts.
146pub async fn get_action_counts_since(
147    pool: &DbPool,
148    since: &str,
149) -> Result<HashMap<String, i64>, StorageError> {
150    get_action_counts_since_for(pool, DEFAULT_ACCOUNT_ID, since).await
151}
152
153/// Get the most recent action log entries for a specific account, newest first.
154pub async fn get_recent_actions_for(
155    pool: &DbPool,
156    account_id: &str,
157    limit: u32,
158) -> Result<Vec<ActionLogEntry>, StorageError> {
159    sqlx::query_as::<_, ActionLogEntry>(
160        "SELECT * FROM action_log WHERE account_id = ? ORDER BY created_at DESC LIMIT ?",
161    )
162    .bind(account_id)
163    .bind(limit)
164    .fetch_all(pool)
165    .await
166    .map_err(|e| StorageError::Query { source: e })
167}
168
169/// Get the most recent action log entries, newest first.
170pub async fn get_recent_actions(
171    pool: &DbPool,
172    limit: u32,
173) -> Result<Vec<ActionLogEntry>, StorageError> {
174    get_recent_actions_for(pool, DEFAULT_ACCOUNT_ID, limit).await
175}
176
177/// Fetch paginated action log entries for a specific account with optional
178/// type and status filters.
179///
180/// Results are ordered by `created_at` descending (newest first).
181pub async fn get_actions_paginated_for(
182    pool: &DbPool,
183    account_id: &str,
184    limit: u32,
185    offset: u32,
186    action_type: Option<&str>,
187    status: Option<&str>,
188) -> Result<Vec<ActionLogEntry>, StorageError> {
189    let mut sql = String::from("SELECT * FROM action_log WHERE 1=1 AND account_id = ?");
190    if action_type.is_some() {
191        sql.push_str(" AND action_type = ?");
192    }
193    if status.is_some() {
194        sql.push_str(" AND status = ?");
195    }
196    sql.push_str(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
197
198    let mut query = sqlx::query_as::<_, ActionLogEntry>(&sql);
199    query = query.bind(account_id);
200    if let Some(at) = action_type {
201        query = query.bind(at);
202    }
203    if let Some(st) = status {
204        query = query.bind(st);
205    }
206    query = query.bind(limit).bind(offset);
207
208    query
209        .fetch_all(pool)
210        .await
211        .map_err(|e| StorageError::Query { source: e })
212}
213
214/// Fetch paginated action log entries with optional type and status filters.
215///
216/// Results are ordered by `created_at` descending (newest first).
217pub async fn get_actions_paginated(
218    pool: &DbPool,
219    limit: u32,
220    offset: u32,
221    action_type: Option<&str>,
222    status: Option<&str>,
223) -> Result<Vec<ActionLogEntry>, StorageError> {
224    get_actions_paginated_for(pool, DEFAULT_ACCOUNT_ID, limit, offset, action_type, status).await
225}
226
227/// Get total count of action log entries for a specific account with optional
228/// type and status filters.
229pub async fn get_actions_count_for(
230    pool: &DbPool,
231    account_id: &str,
232    action_type: Option<&str>,
233    status: Option<&str>,
234) -> Result<i64, StorageError> {
235    let mut sql = String::from("SELECT COUNT(*) FROM action_log WHERE 1=1 AND account_id = ?");
236    if action_type.is_some() {
237        sql.push_str(" AND action_type = ?");
238    }
239    if status.is_some() {
240        sql.push_str(" AND status = ?");
241    }
242
243    let mut query = sqlx::query_as::<_, (i64,)>(&sql);
244    query = query.bind(account_id);
245    if let Some(at) = action_type {
246        query = query.bind(at);
247    }
248    if let Some(st) = status {
249        query = query.bind(st);
250    }
251
252    let (count,) = query
253        .fetch_one(pool)
254        .await
255        .map_err(|e| StorageError::Query { source: e })?;
256    Ok(count)
257}
258
259/// Get total count of action log entries with optional type and status filters.
260pub async fn get_actions_count(
261    pool: &DbPool,
262    action_type: Option<&str>,
263    status: Option<&str>,
264) -> Result<i64, StorageError> {
265    get_actions_count_for(pool, DEFAULT_ACCOUNT_ID, action_type, status).await
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::storage::init_test_db;
272
273    #[tokio::test]
274    async fn log_and_retrieve_action() {
275        let pool = init_test_db().await.expect("init db");
276
277        log_action(&pool, "search", "success", Some("Found 10 tweets"), None)
278            .await
279            .expect("log");
280
281        let actions = get_actions_since(&pool, "2000-01-01T00:00:00Z", None)
282            .await
283            .expect("get");
284
285        assert_eq!(actions.len(), 1);
286        assert_eq!(actions[0].action_type, "search");
287        assert_eq!(actions[0].status, "success");
288        assert_eq!(actions[0].message.as_deref(), Some("Found 10 tweets"));
289    }
290
291    #[tokio::test]
292    async fn filter_by_action_type() {
293        let pool = init_test_db().await.expect("init db");
294
295        log_action(&pool, "search", "success", None, None)
296            .await
297            .expect("log");
298        log_action(&pool, "reply", "success", None, None)
299            .await
300            .expect("log");
301        log_action(&pool, "search", "failure", None, None)
302            .await
303            .expect("log");
304
305        let searches = get_actions_since(&pool, "2000-01-01T00:00:00Z", Some("search"))
306            .await
307            .expect("get");
308        assert_eq!(searches.len(), 2);
309
310        let replies = get_actions_since(&pool, "2000-01-01T00:00:00Z", Some("reply"))
311            .await
312            .expect("get");
313        assert_eq!(replies.len(), 1);
314    }
315
316    #[tokio::test]
317    async fn action_counts_aggregation() {
318        let pool = init_test_db().await.expect("init db");
319
320        log_action(&pool, "search", "success", None, None)
321            .await
322            .expect("log");
323        log_action(&pool, "search", "success", None, None)
324            .await
325            .expect("log");
326        log_action(&pool, "reply", "success", None, None)
327            .await
328            .expect("log");
329        log_action(&pool, "tweet", "failure", None, None)
330            .await
331            .expect("log");
332
333        let counts = get_action_counts_since(&pool, "2000-01-01T00:00:00Z")
334            .await
335            .expect("get counts");
336
337        assert_eq!(counts.get("search"), Some(&2));
338        assert_eq!(counts.get("reply"), Some(&1));
339        assert_eq!(counts.get("tweet"), Some(&1));
340    }
341
342    #[tokio::test]
343    async fn log_with_metadata() {
344        let pool = init_test_db().await.expect("init db");
345
346        let metadata = r#"{"tweet_id": "123", "score": 85}"#;
347        log_action(
348            &pool,
349            "reply",
350            "success",
351            Some("Replied to tweet"),
352            Some(metadata),
353        )
354        .await
355        .expect("log");
356
357        let actions = get_actions_since(&pool, "2000-01-01T00:00:00Z", Some("reply"))
358            .await
359            .expect("get");
360
361        assert_eq!(actions[0].metadata.as_deref(), Some(metadata));
362    }
363
364    #[tokio::test]
365    async fn empty_counts_returns_empty_map() {
366        let pool = init_test_db().await.expect("init db");
367
368        let counts = get_action_counts_since(&pool, "2000-01-01T00:00:00Z")
369            .await
370            .expect("get counts");
371
372        assert!(counts.is_empty());
373    }
374
375    #[tokio::test]
376    async fn paginated_actions_with_offset() {
377        let pool = init_test_db().await.expect("init db");
378
379        for i in 0..10 {
380            log_action(
381                &pool,
382                "search",
383                "success",
384                Some(&format!("Action {i}")),
385                None,
386            )
387            .await
388            .expect("log");
389        }
390
391        let page1 = get_actions_paginated(&pool, 3, 0, None, None)
392            .await
393            .expect("page 1");
394        assert_eq!(page1.len(), 3);
395
396        let page2 = get_actions_paginated(&pool, 3, 3, None, None)
397            .await
398            .expect("page 2");
399        assert_eq!(page2.len(), 3);
400
401        // Pages should not overlap
402        let ids1: Vec<i64> = page1.iter().map(|a| a.id).collect();
403        let ids2: Vec<i64> = page2.iter().map(|a| a.id).collect();
404        assert!(ids1.iter().all(|id| !ids2.contains(id)));
405    }
406
407    #[tokio::test]
408    async fn paginated_actions_with_type_filter() {
409        let pool = init_test_db().await.expect("init db");
410
411        log_action(&pool, "search", "success", None, None)
412            .await
413            .expect("log");
414        log_action(&pool, "reply", "success", None, None)
415            .await
416            .expect("log");
417        log_action(&pool, "search", "success", None, None)
418            .await
419            .expect("log");
420
421        let searches = get_actions_paginated(&pool, 10, 0, Some("search"), None)
422            .await
423            .expect("get");
424        assert_eq!(searches.len(), 2);
425
426        let count = get_actions_count(&pool, Some("search"), None)
427            .await
428            .expect("count");
429        assert_eq!(count, 2);
430    }
431
432    #[tokio::test]
433    async fn paginated_actions_with_status_filter() {
434        let pool = init_test_db().await.expect("init db");
435
436        log_action(&pool, "search", "success", None, None)
437            .await
438            .expect("log");
439        log_action(&pool, "reply", "failure", Some("Rate limited"), None)
440            .await
441            .expect("log");
442        log_action(&pool, "tweet", "failure", Some("API error"), None)
443            .await
444            .expect("log");
445
446        let failures = get_actions_paginated(&pool, 10, 0, None, Some("failure"))
447            .await
448            .expect("get");
449        assert_eq!(failures.len(), 2);
450
451        let count = get_actions_count(&pool, None, Some("failure"))
452            .await
453            .expect("count");
454        assert_eq!(count, 2);
455    }
456
457    #[tokio::test]
458    async fn paginated_actions_combined_filters() {
459        let pool = init_test_db().await.expect("init db");
460
461        log_action(&pool, "reply", "success", None, None)
462            .await
463            .expect("log");
464        log_action(&pool, "reply", "failure", None, None)
465            .await
466            .expect("log");
467        log_action(&pool, "tweet", "failure", None, None)
468            .await
469            .expect("log");
470
471        let reply_failures = get_actions_paginated(&pool, 10, 0, Some("reply"), Some("failure"))
472            .await
473            .expect("get");
474        assert_eq!(reply_failures.len(), 1);
475
476        let count = get_actions_count(&pool, Some("reply"), Some("failure"))
477            .await
478            .expect("count");
479        assert_eq!(count, 1);
480    }
481
482    #[tokio::test]
483    async fn actions_count_no_filter() {
484        let pool = init_test_db().await.expect("init db");
485
486        log_action(&pool, "search", "success", None, None)
487            .await
488            .expect("log");
489        log_action(&pool, "reply", "success", None, None)
490            .await
491            .expect("log");
492
493        let count = get_actions_count(&pool, None, None).await.expect("count");
494        assert_eq!(count, 2);
495    }
496
497    #[tokio::test]
498    async fn actions_count_empty_db() {
499        let pool = init_test_db().await.expect("init db");
500        let count = get_actions_count(&pool, None, None).await.expect("count");
501        assert_eq!(count, 0);
502    }
503
504    #[tokio::test]
505    async fn paginated_offset_beyond_data_returns_empty() {
506        let pool = init_test_db().await.expect("init db");
507
508        log_action(&pool, "search", "success", None, None)
509            .await
510            .expect("log");
511        log_action(&pool, "reply", "success", None, None)
512            .await
513            .expect("log");
514
515        let page = get_actions_paginated(&pool, 10, 100, None, None)
516            .await
517            .expect("page");
518        assert!(page.is_empty(), "offset past data should return empty");
519    }
520
521    #[tokio::test]
522    async fn get_recent_actions_returns_limited_set() {
523        let pool = init_test_db().await.expect("init db");
524
525        log_action(&pool, "search", "success", Some("first"), None)
526            .await
527            .expect("log");
528        log_action(&pool, "reply", "success", Some("second"), None)
529            .await
530            .expect("log");
531        log_action(&pool, "tweet", "success", Some("third"), None)
532            .await
533            .expect("log");
534
535        let recent = get_recent_actions(&pool, 2).await.expect("get");
536        assert_eq!(recent.len(), 2, "should respect limit");
537
538        let all = get_recent_actions(&pool, 10).await.expect("get all");
539        assert_eq!(all.len(), 3);
540    }
541
542    #[tokio::test]
543    async fn log_action_with_null_message_and_metadata() {
544        let pool = init_test_db().await.expect("init db");
545
546        log_action(&pool, "cleanup", "success", None, None)
547            .await
548            .expect("log");
549
550        let actions = get_actions_since(&pool, "2000-01-01T00:00:00Z", None)
551            .await
552            .expect("get");
553        assert_eq!(actions.len(), 1);
554        assert!(actions[0].message.is_none());
555        assert!(actions[0].metadata.is_none());
556    }
557
558    #[tokio::test]
559    async fn action_counts_since_future_returns_empty() {
560        let pool = init_test_db().await.expect("init db");
561
562        log_action(&pool, "search", "success", None, None)
563            .await
564            .expect("log");
565
566        let counts = get_action_counts_since(&pool, "2099-01-01T00:00:00Z")
567            .await
568            .expect("counts");
569        assert!(counts.is_empty());
570    }
571
572    #[tokio::test]
573    async fn paginated_type_and_status_combined_count() {
574        let pool = init_test_db().await.expect("init db");
575
576        log_action(&pool, "reply", "success", None, None)
577            .await
578            .expect("log");
579        log_action(&pool, "reply", "failure", None, None)
580            .await
581            .expect("log");
582        log_action(&pool, "reply", "success", None, None)
583            .await
584            .expect("log");
585        log_action(&pool, "tweet", "success", None, None)
586            .await
587            .expect("log");
588
589        let count = get_actions_count(&pool, Some("reply"), Some("success"))
590            .await
591            .expect("count");
592        assert_eq!(count, 2);
593
594        let page = get_actions_paginated(&pool, 10, 0, Some("reply"), Some("success"))
595            .await
596            .expect("page");
597        assert_eq!(page.len(), 2);
598    }
599
600    #[tokio::test]
601    async fn log_action_for_different_accounts() {
602        let pool = init_test_db().await.expect("init db");
603
604        log_action_for(&pool, "acct_a", "search", "success", Some("a"), None)
605            .await
606            .expect("log a");
607        log_action_for(&pool, "acct_b", "search", "success", Some("b"), None)
608            .await
609            .expect("log b");
610        log_action_for(&pool, "acct_a", "reply", "success", Some("a2"), None)
611            .await
612            .expect("log a2");
613
614        let actions_a = get_actions_since_for(&pool, "acct_a", "2000-01-01T00:00:00Z", None)
615            .await
616            .expect("get a");
617        assert_eq!(actions_a.len(), 2);
618
619        let actions_b = get_actions_since_for(&pool, "acct_b", "2000-01-01T00:00:00Z", None)
620            .await
621            .expect("get b");
622        assert_eq!(actions_b.len(), 1);
623
624        let count_a = get_actions_count_for(&pool, "acct_a", None, None)
625            .await
626            .expect("count a");
627        assert_eq!(count_a, 2);
628
629        let count_b = get_actions_count_for(&pool, "acct_b", None, None)
630            .await
631            .expect("count b");
632        assert_eq!(count_b, 1);
633    }
634
635    #[tokio::test]
636    async fn get_recent_actions_respects_limit() {
637        let pool = init_test_db().await.expect("init db");
638
639        for i in 0..5 {
640            log_action(
641                &pool,
642                "search",
643                "success",
644                Some(&format!("Action {i}")),
645                None,
646            )
647            .await
648            .expect("log");
649        }
650
651        let recent = get_recent_actions(&pool, 0).await.expect("get");
652        assert!(recent.is_empty(), "limit 0 should return empty");
653
654        let recent = get_recent_actions(&pool, 1).await.expect("get");
655        assert_eq!(recent.len(), 1);
656    }
657
658    // ================================================================
659    // Account-scoped `_for` variant isolation tests
660    // ================================================================
661
662    #[tokio::test]
663    async fn get_action_counts_since_for_account_isolation() {
664        let pool = init_test_db().await.expect("init db");
665
666        log_action_for(&pool, "acct_x", "search", "success", None, None)
667            .await
668            .expect("log x");
669        log_action_for(&pool, "acct_x", "search", "success", None, None)
670            .await
671            .expect("log x2");
672        log_action_for(&pool, "acct_x", "reply", "success", None, None)
673            .await
674            .expect("log x3");
675        log_action_for(&pool, "acct_y", "search", "success", None, None)
676            .await
677            .expect("log y");
678
679        let counts_x = get_action_counts_since_for(&pool, "acct_x", "2000-01-01T00:00:00Z")
680            .await
681            .expect("counts x");
682        assert_eq!(counts_x.get("search"), Some(&2));
683        assert_eq!(counts_x.get("reply"), Some(&1));
684
685        let counts_y = get_action_counts_since_for(&pool, "acct_y", "2000-01-01T00:00:00Z")
686            .await
687            .expect("counts y");
688        assert_eq!(counts_y.get("search"), Some(&1));
689        assert!(counts_y.get("reply").is_none());
690    }
691
692    #[tokio::test]
693    async fn get_actions_paginated_for_account_isolation() {
694        let pool = init_test_db().await.expect("init db");
695
696        for i in 0..5 {
697            log_action_for(
698                &pool,
699                "acct_p",
700                "search",
701                "success",
702                Some(&format!("P{i}")),
703                None,
704            )
705            .await
706            .expect("log p");
707        }
708        for i in 0..3 {
709            log_action_for(
710                &pool,
711                "acct_q",
712                "reply",
713                "failure",
714                Some(&format!("Q{i}")),
715                None,
716            )
717            .await
718            .expect("log q");
719        }
720
721        // Paginate acct_p
722        let page1 = get_actions_paginated_for(&pool, "acct_p", 3, 0, None, None)
723            .await
724            .expect("page1");
725        assert_eq!(page1.len(), 3);
726
727        let page2 = get_actions_paginated_for(&pool, "acct_p", 3, 3, None, None)
728            .await
729            .expect("page2");
730        assert_eq!(page2.len(), 2);
731
732        // acct_q should have its own data
733        let q_all = get_actions_paginated_for(&pool, "acct_q", 10, 0, None, None)
734            .await
735            .expect("q all");
736        assert_eq!(q_all.len(), 3);
737        assert!(q_all.iter().all(|a| a.action_type == "reply"));
738
739        // Filter by type within account
740        let q_filtered =
741            get_actions_paginated_for(&pool, "acct_q", 10, 0, Some("reply"), Some("failure"))
742                .await
743                .expect("q filtered");
744        assert_eq!(q_filtered.len(), 3);
745
746        // Cross-check: acct_p should have no reply actions
747        let p_replies = get_actions_paginated_for(&pool, "acct_p", 10, 0, Some("reply"), None)
748            .await
749            .expect("p replies");
750        assert!(p_replies.is_empty());
751    }
752
753    #[tokio::test]
754    async fn get_recent_actions_for_account_isolation() {
755        let pool = init_test_db().await.expect("init db");
756
757        log_action_for(&pool, "acct_r", "search", "success", Some("R1"), None)
758            .await
759            .expect("log r1");
760        log_action_for(&pool, "acct_r", "reply", "success", Some("R2"), None)
761            .await
762            .expect("log r2");
763        log_action_for(&pool, "acct_s", "tweet", "success", Some("S1"), None)
764            .await
765            .expect("log s1");
766
767        let recent_r = get_recent_actions_for(&pool, "acct_r", 10)
768            .await
769            .expect("recent r");
770        assert_eq!(recent_r.len(), 2);
771
772        let recent_s = get_recent_actions_for(&pool, "acct_s", 10)
773            .await
774            .expect("recent s");
775        assert_eq!(recent_s.len(), 1);
776        assert_eq!(recent_s[0].message.as_deref(), Some("S1"));
777
778        // Limit works per account
779        let recent_r1 = get_recent_actions_for(&pool, "acct_r", 1)
780            .await
781            .expect("recent r limited");
782        assert_eq!(recent_r1.len(), 1);
783    }
784}