{"record":{"id":"836d0b84edce2a75","repo":"block/buzz","slug":"publish-canonical-role","errorCode":null,"errorMessage":"publish canonical role","messagePattern":"publish canonical role","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/buzz-db/src/store/channel_members.rs","lineNumber":3186,"sourceCode":"        let roster = |role: &str, timestamp| {\n            EventBuilder::new(Kind::Custom(39002), \"\")\n                .tags(vec![\n                    Tag::parse([\"d\", channel.to_string().as_str()]).expect(\"d tag\"),\n                    Tag::parse([\"p\", hex::encode(owner).as_str(), \"\", \"owner\"])\n                        .expect(\"owner p tag\"),\n                    Tag::parse([\"p\", hex::encode(member).as_str(), \"\", role])\n                        .expect(\"member p tag\"),\n                ])\n                .custom_created_at(Timestamp::from(timestamp))\n                .sign_with_keys(&relay_keys)\n                .expect(\"sign roster\")\n        };\n        let base = Timestamp::now().as_secs();\n        let fresh = roster(\"admin\", base);\n        assert!(\n            db.replace_addressable_event(community, &fresh, Some(channel))\n                .await\n                .expect(\"publish canonical role\")\n                .1\n        );\n        let stale = roster(\"member\", base + 1);\n        let error = db\n            .replace_addressable_event(community, &stale, Some(channel))\n            .await\n            .expect_err(\"desired-state fence must reject stale role\");\n        assert!(matches!(\n            error,\n            DbError::Sqlx(sqlx::Error::Database(ref db_error))\n                if db_error.code().as_deref() == Some(\"23514\")\n        ));\n        let live_id: Vec<u8> = sqlx::query_scalar(\n            \"SELECT id FROM events WHERE community_id=$1 AND channel_id=$2 \\\n             AND kind=39002 AND deleted_at IS NULL\",\n        )\n        .bind(community_uuid)\n        .bind(channel)","sourceCodeStart":3168,"sourceCodeEnd":3204,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-db/src/store/channel_members.rs#L3168-L3204","documentation":"Panic from `db.replace_addressable_event(community, &fresh, Some(channel)).await.expect(\"publish canonical role\")`. The test asserts that a fresh kind:39002 roster event (newer created_at) is accepted via last-write-wins (NIP-33 style) replacement; the expect fires when the DB call returns Err instead of the expected (result, replaced=true) tuple.","triggerScenarios":"`replace_addressable_event` errors when the addressable event fails verification, the community/channel row does not exist, the scratch database schema/migrations are stale, or a database connection error occurs (TEST_DATABASE_URL unreachable).","commonSituations":"Running integration tests without Postgres provisioned, migrations not applied to the scratch DB, or a code change to replace_addressable_event that now rejects valid roster events.","solutions":["Ensure TEST_DATABASE_URL points at a running Postgres with current migrations (just setup / just test).","Check the scratch DB setup/teardown helpers in this test file — the community and channel rows must exist before publishing the roster.","Confirm replace_addressable_event still accepts kind:39002 events with custom created_at after any LWW-related changes.","Run with RUST_BACKTRACE=1 and the sqlx query log to see the underlying DB error."],"exampleFix":"// before\nassert!(db.replace_addressable_event(community, &fresh, Some(channel)).await.expect(\"publish canonical role\").1);\n// after\nlet res = db.replace_addressable_event(community, &fresh, Some(channel)).await\n    .unwrap_or_else(|e| panic!(\"publish canonical role: {e:?}\"));\nassert!(res.1, \"expected LWW replacement of existing roster\");","handlingStrategy":"try-catch","validationCode":"// before publishing, confirm prerequisites exist\nlet community_row = sqlx::query(\"SELECT 1 FROM communities WHERE id=$1\").bind(community).fetch_optional(&pool).await.expect(\"check community\");\nassert!(community_row.is_some(), \"community row must exist before roster publish\");","typeGuard":null,"tryCatchPattern":"match db.replace_addressable_event(community, &fresh, Some(channel)).await {\n    Ok((_, true)) => {}, // replaced as expected\n    Ok((_, false)) => panic!(\"fresh roster was not treated as LWW winner\"),\n    Err(e) => panic!(\"publish canonical role failed: {e}\"),\n}","preventionTips":["Run integration tests via `just test` so Postgres and migrations are provisioned.","Create community/channel prerequisite rows in setup, not mid-test.","Log the underlying sqlx error instead of a bare expect message.","After changing LWW logic, re-run replace_addressable_event tests before the full suite."],"tags":["rust","test-panic","database","nip33"],"backgroundTag":"database-write-failed","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-30T13:49:18.474Z","contentChangedAt":"2026-08-30T13:49:18.474Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}