Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions crates/buzz-db/src/runtime/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,14 +705,17 @@ mod postgres_tests {
// upstream carries 45 (0032-0034 and 0040 adopted from our PRs; 0045
// push revocation tombstones synced 2026-09-16); fork adds
// 0046_task_system (PR #6425 pending upstream),
// 0047_agent_machine_homes (AGENT-HOMES-001 PR-3), and 0049 structured
// task history. All stay additive for existing deployments.
assert_eq!(migrations.len(), 48);
// 0047_agent_machine_homes (AGENT-HOMES-001 PR-3),
// 0048_community_brand_color (REG-10; renumbered from 0037, which is
// upstream-owned relay_admin_action_lease), and 0049 structured task
// history. All stay additive for existing deployments.
assert_eq!(migrations.len(), 49);
assert_eq!(migrations[44].version, 45);
assert_eq!(migrations[45].version, 46);
assert_eq!(migrations[46].version, 47);
assert_eq!(migrations[47].version, 49);
let task_changes = migrations[47].sql.as_str();
assert_eq!(migrations[47].version, 48);
assert_eq!(migrations[48].version, 49);
let task_changes = migrations[48].sql.as_str();
assert!(task_changes.contains("ALTER TABLE task_events ADD COLUMN changes JSONB"));
assert!(task_changes.contains("ALTER COLUMN created_at SET DEFAULT clock_timestamp()"));
assert!(!migrations[44].sql.as_str().contains("ADD COLUMN changes"));
Expand Down Expand Up @@ -1456,6 +1459,21 @@ mod postgres_tests {
assert!(crate::deletion::EXPECTED_SCOPED_TABLES.contains(&"task_events"));
}

#[test]
fn community_brand_color_is_additive_and_mirrored_in_desired_state() {
let mut migrations: Vec<_> = MIGRATOR.iter().collect();
migrations.sort_by_key(|migration| migration.version);

let migration = migrations
.iter()
.find(|migration| migration.version == 48)
.expect("embedded migration 0048");
let sql = migration.sql.as_str();
assert!(sql.contains("ALTER TABLE communities ADD COLUMN brand_color TEXT"));
assert!(!migrations[0].sql.as_str().contains("brand_color"));
assert!(include_str!("../../../../schema/schema.sql").contains("brand_color TEXT"));
}

#[test]
fn migration_lint_detects_tables_missing_community_id_by_default() {
let sql = r#"
Expand Down
118 changes: 118 additions & 0 deletions crates/buzz-db/src/store/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,57 @@ impl Db {
Ok(())
}

/// Returns the community's brand color (`#rrggbb`), if set.
///
/// Set by relay admins/owners via the kind:9033 workspace-profile command
/// alongside the icon; the value is validated and length-capped at that
/// write path. Mirrors [`Self::get_community_icon`] exactly, including the
/// empty-string-is-unset filter, so the two presentation scalars cannot
/// drift apart.
#[datastore_span(name = "get_community_brand_color", system = "postgresql")]
pub async fn get_community_brand_color(
&self,
community_id: CommunityId,
) -> Result<Option<String>> {
let row = sqlx::query(
r#"
SELECT brand_color
FROM communities
WHERE id = $1
"#,
)
.bind(community_id.as_uuid())
.fetch_optional(&self.pool)
.await?;

Ok(row
.map(|row| row.try_get::<Option<String>, _>("brand_color"))
.transpose()?
.flatten()
.filter(|color| !color.is_empty()))
}

/// Sets or clears (`None`) the community's brand color.
#[datastore_span(name = "set_community_brand_color", system = "postgresql")]
pub async fn set_community_brand_color(
&self,
community_id: CommunityId,
brand_color: Option<&str>,
) -> Result<()> {
sqlx::query(
r#"
UPDATE communities
SET brand_color = $2
WHERE id = $1
"#,
)
.bind(community_id.as_uuid())
.bind(brand_color)
.execute(&self.pool)
.await?;
Ok(())
}

/// Ensure a configured community host exists and return its row.
///
/// This is the startup/config seeding path for N=1 deployments. Migrations
Expand Down Expand Up @@ -713,6 +764,8 @@ mod postgres_tests {
"lookup_community_host",
"get_community_icon",
"set_community_icon",
"get_community_brand_color",
"set_community_brand_color",
"ensure_configured_community",
"create_community_with_owner",
"archive_community_owned_by",
Expand Down Expand Up @@ -768,6 +821,7 @@ mod postgres_tests {
"create_community_with_owner_enforces_per_owner_limit",
"concurrent_same_owner_create_returns_the_winning_row_to_both_callers",
"ensure_configured_community_reports_insert_winner",
"community_brand_color_round_trips_and_clears_independently_from_icon",
"list_communities_owned_by_returns_only_owner_rows",
"communities_of_channels_present_for_existing_absent_for_missing",
];
Expand Down Expand Up @@ -1016,6 +1070,70 @@ mod postgres_tests {
assert_eq!(second.host, host);
}

#[tokio::test]
#[ignore = "requires Postgres"]
async fn community_brand_color_round_trips_and_clears_independently_from_icon() {
let db = setup_db().await;
let community = CommunityId::from_uuid(make_community(&db.pool).await);

assert_eq!(
db.get_community_brand_color(community)
.await
.expect("initial brand color"),
None
);

db.set_community_icon(community, Some("https://example.com/icon.png"))
.await
.expect("set icon");
db.set_community_brand_color(community, Some("#ff8800"))
.await
.expect("set brand color");
assert_eq!(
db.get_community_brand_color(community)
.await
.expect("stored brand color")
.as_deref(),
Some("#ff8800")
);
assert_eq!(
db.get_community_icon(community)
.await
.expect("stored icon")
.as_deref(),
Some("https://example.com/icon.png"),
"brand color writes must not disturb the icon scalar"
);

db.set_community_brand_color(community, Some(""))
.await
.expect("store empty brand color");
assert_eq!(
db.get_community_brand_color(community)
.await
.expect("empty brand color"),
None,
"empty string is treated as cleared, matching get_community_icon"
);

db.set_community_brand_color(community, None)
.await
.expect("clear brand color");
assert_eq!(
db.get_community_brand_color(community)
.await
.expect("cleared brand color"),
None
);
assert_eq!(
db.get_community_icon(community)
.await
.expect("icon after brand clears")
.as_deref(),
Some("https://example.com/icon.png")
);
}

#[tokio::test]
#[ignore = "requires Postgres"]
async fn list_communities_owned_by_returns_only_owner_rows() {
Expand Down
Loading
Loading