Skip to main content

max / makenotwork

mt: surface System auto-hide rows in the mod log (LEFT JOIN) list_mod_log INNER JOINed the actor user, so every NULL-actor row silently vanished from the listing — exactly the flag-threshold auto-hide entries migration 032 made actor_id nullable to record. count_mod_log counted them, so pagination overcounted against a listing that couldn't show them. LEFT JOIN the actor and COALESCE a missing username to 'System', restoring the auto-moderation audit trail on the page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-07 15:00 UTC
Commit: 06ee0487635a6011b9ac855dacf638b1999e7544
Parent: 81e2cbc
4 files changed, +83 insertions, -56 deletions
@@ -0,0 +1,54 @@
1 + {
2 + "db_name": "PostgreSQL",
3 + "query": "SELECT ml.id,\n COALESCE(actor.username, 'System') AS \"actor_username!\",\n ml.action AS \"action: ModAction\",\n target.username AS \"target_username?\",\n ml.reason,\n ml.created_at AS \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM mod_log ml\n LEFT JOIN users actor ON actor.mnw_account_id = ml.actor_id\n LEFT JOIN users target ON target.mnw_account_id = ml.target_user\n WHERE ml.community_id = $1\n ORDER BY ml.created_at DESC\n LIMIT $2 OFFSET $3",
4 + "describe": {
5 + "columns": [
6 + {
7 + "ordinal": 0,
8 + "name": "id",
9 + "type_info": "Uuid"
10 + },
11 + {
12 + "ordinal": 1,
13 + "name": "actor_username!",
14 + "type_info": "Text"
15 + },
16 + {
17 + "ordinal": 2,
18 + "name": "action: ModAction",
19 + "type_info": "Text"
20 + },
21 + {
22 + "ordinal": 3,
23 + "name": "target_username?",
24 + "type_info": "Text"
25 + },
26 + {
27 + "ordinal": 4,
28 + "name": "reason",
29 + "type_info": "Text"
30 + },
31 + {
32 + "ordinal": 5,
33 + "name": "created_at: chrono::DateTime<chrono::Utc>",
34 + "type_info": "Timestamptz"
35 + }
36 + ],
37 + "parameters": {
38 + "Left": [
39 + "Uuid",
40 + "Int8",
41 + "Int8"
42 + ]
43 + },
44 + "nullable": [
45 + false,
46 + null,
47 + false,
48 + false,
49 + true,
50 + false
51 + ]
52 + },
53 + "hash": "5a76d12888310cce3fd0468969b65c25981690b3b9d73d55678be086a89a0ad2"
54 + }
@@ -1,54 +0,0 @@
1 - {
2 - "db_name": "PostgreSQL",
3 - "query": "SELECT ml.id,\n actor.username AS actor_username,\n ml.action AS \"action: ModAction\",\n target.username AS \"target_username?\",\n ml.reason,\n ml.created_at AS \"created_at: chrono::DateTime<chrono::Utc>\"\n FROM mod_log ml\n JOIN users actor ON actor.mnw_account_id = ml.actor_id\n LEFT JOIN users target ON target.mnw_account_id = ml.target_user\n WHERE ml.community_id = $1\n ORDER BY ml.created_at DESC\n LIMIT $2 OFFSET $3",
4 - "describe": {
5 - "columns": [
6 - {
7 - "ordinal": 0,
8 - "name": "id",
9 - "type_info": "Uuid"
10 - },
11 - {
12 - "ordinal": 1,
13 - "name": "actor_username",
14 - "type_info": "Text"
15 - },
16 - {
17 - "ordinal": 2,
18 - "name": "action: ModAction",
19 - "type_info": "Text"
20 - },
21 - {
22 - "ordinal": 3,
23 - "name": "target_username?",
24 - "type_info": "Text"
25 - },
26 - {
27 - "ordinal": 4,
28 - "name": "reason",
29 - "type_info": "Text"
30 - },
31 - {
32 - "ordinal": 5,
33 - "name": "created_at: chrono::DateTime<chrono::Utc>",
34 - "type_info": "Timestamptz"
35 - }
36 - ],
37 - "parameters": {
38 - "Left": [
39 - "Uuid",
40 - "Int8",
41 - "Int8"
42 - ]
43 - },
44 - "nullable": [
45 - false,
46 - false,
47 - false,
48 - false,
49 - true,
50 - false
51 - ]
52 - },
53 - "hash": "b5cc05e9ffeec4e09477213658945252858eb8005a61c6b3551f276c2897ec39"
54 - }
@@ -914,14 +914,20 @@ pub async fn list_mod_log(
914 914 ) -> Result<Vec<ModLogEntry>, sqlx::Error> {
915 915 sqlx::query_as!(
916 916 ModLogEntry,
917 + // LEFT JOIN + a "System" label for a NULL actor_id: migration 032 made
918 + // actor_id nullable precisely so auto-hide (flag-threshold) removals could
919 + // be recorded with no human actor. An INNER JOIN silently dropped every
920 + // such row — the auto-moderation audit trail the migration exists to keep
921 + // — while count_mod_log counted them, so pagination overcounted. COALESCE
922 + // is non-null, hence the `!` (see fuzz-2026-07-06 top DB fix).
917 923 r#"SELECT ml.id,
918 - actor.username AS actor_username,
924 + COALESCE(actor.username, 'System') AS "actor_username!",
919 925 ml.action AS "action: ModAction",
920 926 target.username AS "target_username?",
921 927 ml.reason,
922 928 ml.created_at AS "created_at: chrono::DateTime<chrono::Utc>"
923 929 FROM mod_log ml
924 - JOIN users actor ON actor.mnw_account_id = ml.actor_id
930 + LEFT JOIN users actor ON actor.mnw_account_id = ml.actor_id
925 931 LEFT JOIN users target ON target.mnw_account_id = ml.target_user
926 932 WHERE ml.community_id = $1
927 933 ORDER BY ml.created_at DESC
@@ -199,6 +199,27 @@ async fn auto_hide_logs_system_actor_not_flagger() {
199 199 .expect("auto-hide must write exactly one mod_log row");
200 200 assert_eq!(actor, None, "auto-hide must be logged as System (NULL actor_id), not the flagger");
201 201 assert_ne!(actor, Some(flagger), "the flagger must never be recorded as the auto-hide actor");
202 +
203 + // Regression (fuzz-2026-07-06 top DB fix): the System row must be VISIBLE in
204 + // the mod-log listing. list_mod_log INNER JOINed the actor, silently dropping
205 + // every NULL-actor row — so the auto-moderation ledger migration 032 exists to
206 + // keep was invisible on the page, while count_mod_log counted it (pagination
207 + // overcount). The row must now appear, labeled "System", and the two agree.
208 + let entries = mt_db::queries::list_mod_log(&h.db, comm_id, 50, 0).await.unwrap();
209 + let count = mt_db::queries::count_mod_log(&h.db, comm_id).await.unwrap();
210 + assert_eq!(
211 + count as usize,
212 + entries.len(),
213 + "count_mod_log must match the number of rows list_mod_log returns"
214 + );
215 + let system_row = entries
216 + .iter()
217 + .find(|e| e.action == mt_core::types::ModAction::AutoHidePost)
218 + .expect("the auto-hide row must be present in the mod-log listing");
219 + assert_eq!(
220 + system_row.actor_username, "System",
221 + "a NULL-actor auto-hide row must render as the System actor"
222 + );
202 223 }
203 224
204 225 #[tokio::test]