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>
4 files changed,
+83 insertions,
-56 deletions
|
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 |
|
- |
{
|
| 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 |
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 |
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]
|