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
Signed with PGP, not checked
Commit: 06ee0487635a6011b9ac855dacf638b1999e7544
Parent: 81e2cbc
3 files changed, +33 insertions, -6 deletions
@@ -199,6 +199,27 @@
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]
@@ -914,14 +914,20 @@
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
@@ -1,6 +1,6 @@
1 1 {
2 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",
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 4 "describe": {
5 5 "columns": [
6 6 {
@@ -10,7 +10,7 @@
10 10 },
11 11 {
12 12 "ordinal": 1,
13 - "name": "actor_username",
13 + "name": "actor_username!",
14 14 "type_info": "Text"
15 15 },
16 16 {
@@ -43,12 +43,12 @@
43 43 },
44 44 "nullable": [
45 45 false,
46 - false,
46 + null,
47 47 false,
48 48 false,
49 49 true,
50 50 false
51 51 ]
52 52 },
53 - "hash": "b5cc05e9ffeec4e09477213658945252858eb8005a61c6b3551f276c2897ec39"
53 + "hash": "5a76d12888310cce3fd0468969b65c25981690b3b9d73d55678be086a89a0ad2"
54 54 }