diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 74da36d65b..1c032e7d6b 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -95,6 +95,9 @@ const overrides = new Map([ // unified-agent-model 1A.1: inline test module moved to nest/tests.rs, // ratcheting 1575 -> 679 (under the 1000 default; entry kept as a ratchet). ["src-tauri/src/managed_agents/nest.rs", 679], + // keyring-dev-isolation: agent key migration added copy_agent_keys_between_stores + // and load_readonly support; file grew past 1000 default. Queued to split. + ["src-tauri/src/managed_agents/storage.rs", 1325], // harness-persona-sync: persona-runtime resolution threaded into the spawn // path here. Load-bearing feature growth; queued to split in the resolver // unify refactor followup. +26 for resolve_effective_prompt_model_provider @@ -192,7 +195,8 @@ const overrides = new Map([ // + inner fn with baked-env gate + 26 tests. Load-bearing correctness fix. // am review fix: also clear stale V1 model field on provider rewrite + // new model-clear test. Load-bearing chimera fix. - ["src-tauri/src/migration.rs", 1402], + // keyring-dev-isolation: run_boot_migrations wires agent-key migration. + ["src-tauri/src/migration.rs", 1415], // onMarkRead + isUnread prop threading (mirrors the onMarkUnread prop // already here) for the single-toggle mark-read/unread menu item — a small // overage from load-bearing per-message plumbing, not generic debt growth. @@ -213,8 +217,10 @@ const overrides = new Map([ // cross-process keychain race fix (D3): interprocess lock + BlobLockGuard + // uid-keyed lockfile path + behavioral tests add ~303 lines. Load-bearing // security fix for the lost-update race that stranded agent keys. - ["src-tauri/src/secret_store.rs", 1043], - ["src-tauri/src/app_state.rs", 1033], + ["src-tauri/src/secret_store.rs", 1110], + // keyring-dev-isolation: keyring_service() fn (7 lines) replaces the const + // to return "buzz-desktop-dev" in debug builds. Load-bearing isolation fix. + ["src-tauri/src/app_state.rs", 1042], // multi-slot splitting + no-op suppression (#1309): the ReadStateManager // class grew from ~700 lines to ~1019 with the addition of // splitContextsIntoBudgetedSlots (pure fn + 5 tests), publishSplitSlots, diff --git a/desktop/src-tauri/src/app_state.rs b/desktop/src-tauri/src/app_state.rs index dca627fe6e..6141d8a295 100644 --- a/desktop/src-tauri/src/app_state.rs +++ b/desktop/src-tauri/src/app_state.rs @@ -184,7 +184,16 @@ pub fn resolve_persisted_identity(app: &AppHandle, state: &AppState) -> Result<( /// Service name for the desktop OS keyring. Shared by the human identity key /// and managed-agent keys (each addressed by a distinct key name within it). -pub(crate) const KEYRING_SERVICE: &str = "buzz-desktop"; +/// +/// Debug builds use a distinct service name so dev and production keyring +/// entries never collide on the same machine. +pub(crate) fn keyring_service() -> &'static str { + if cfg!(debug_assertions) { + "buzz-desktop-dev" + } else { + "buzz-desktop" + } +} /// Keyring key name for the human identity nsec. const IDENTITY_KEY_NAME: &str = "identity"; @@ -238,7 +247,7 @@ fn load_or_create_identity(data_dir: &std::path::Path) -> Result { return load_file_or_generate(&legacy_path, data_dir); } - let store = crate::secret_store::SecretStore::shared(KEYRING_SERVICE); + let store = crate::secret_store::SecretStore::shared(keyring_service()); resolve_identity_with_store(store, &legacy_path, data_dir) } diff --git a/desktop/src-tauri/src/managed_agents/storage.rs b/desktop/src-tauri/src/managed_agents/storage.rs index fa5281f888..e98c2a569c 100644 --- a/desktop/src-tauri/src/managed_agents/storage.rs +++ b/desktop/src-tauri/src/managed_agents/storage.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fs::{self, File, OpenOptions}, io::{Read as _, Seek, SeekFrom, Write}, path::{Path, PathBuf}, @@ -6,7 +7,7 @@ use std::{ use tauri::{AppHandle, Manager}; -use crate::app_state::KEYRING_SERVICE; +use crate::app_state::keyring_service; use crate::managed_agents::ManagedAgentRecord; use crate::secret_store::{KeyringProbe, SecretStore}; @@ -23,7 +24,7 @@ fn agent_keyring_name(pubkey: &str) -> String { /// races on concurrent blob writes. fn agent_secret_store() -> Option<&'static SecretStore> { if cfg!(feature = "system-keyring") { - Some(SecretStore::shared(KEYRING_SERVICE)) + Some(SecretStore::shared(keyring_service())) } else { None } @@ -61,9 +62,18 @@ trait KeyStore { /// Read a key. `Ok(None)` is "no such entry" (absent); `Err` is a backend /// failure (keyring unreachable) — the caller MUST NOT collapse the two. fn load(&self, name: &str) -> Result, String>; + /// Read a key without any side effects (no legacy-key migration, no writes). + /// `Ok(None)` when the blob or the key is absent; `Err` only on backend failure. + fn load_readonly(&self, name: &str) -> Result, String>; + /// Read the entire blob as a map without any side effects. + /// `Ok(None)` when no blob exists yet; `Err` only on backend failure. + /// Callers must not call `migrate_legacy_key` — this is a read-only view. + fn load_all_readonly(&self) -> Result>, String>; /// Write `value` and read it back to confirm before the caller strips the /// inline copy. fn write_and_verify(&self, name: &str, value: &str) -> Result<(), String>; + /// Insert all entries from `entries` in a single blob mutation. + fn store_all(&self, entries: &HashMap) -> Result<(), String>; } impl KeyStore for SecretStore { @@ -73,6 +83,12 @@ impl KeyStore for SecretStore { fn load(&self, name: &str) -> Result, String> { SecretStore::load(self, name) } + fn load_readonly(&self, name: &str) -> Result, String> { + SecretStore::load_readonly(self, name) + } + fn load_all_readonly(&self) -> Result>, String> { + SecretStore::load_all_readonly(self) + } fn write_and_verify(&self, name: &str, value: &str) -> Result<(), String> { self.store(name, value)?; match self.load(name)? { @@ -80,6 +96,9 @@ impl KeyStore for SecretStore { _ => Err("keyring read-back verify failed".to_string()), } } + fn store_all(&self, entries: &HashMap) -> Result<(), String> { + SecretStore::store_all(self, entries) + } } /// Outcome of attempting to lift a record's inline key into the keyring. @@ -355,6 +374,138 @@ fn persist_agent_keys_with(store: &impl KeyStore, records: &mut [ManagedAgentRec } } } + +/// One-time migration of agent keys from the production keyring service +/// (`"buzz-desktop"`) to the dev service (`"buzz-desktop-dev"`). Only runs +/// in debug builds — release builds never touch `"buzz-desktop"` from this +/// path. +/// +/// Idempotent: skips any key that already exists in the dev service so +/// repeated boots after migration are no-ops. Leaves the production keyring +/// untouched — a dev build and a prod install can coexist without sharing +/// keys after this migration. +/// +/// Call this at boot before `hydrate_keys` runs (i.e. before +/// `load_managed_agents` is called) so agents find their keys on first boot +/// after the service-name change. +#[cfg(debug_assertions)] +pub fn migrate_agent_keys_to_dev_service(app: &tauri::AppHandle) { + if !cfg!(feature = "system-keyring") { + return; + } + + // Read the JSON store for pubkeys only — we want every instance + // record without running hydrate_keys (which would try the dev + // keyring that is empty, and log noisy "has no key" warnings). + let records = match load_agent_store(app) { + Ok(r) => r, + Err(e) => { + eprintln!("buzz-desktop: keyring-dev-migration: cannot read agent store: {e}"); + return; + } + }; + + let pubkeys: Vec = records + .into_iter() + .filter(|r| !r.pubkey.is_empty()) + .map(|r| r.pubkey) + .collect(); + + if pubkeys.is_empty() { + return; + } + + // A fresh non-singleton store for the prod service — its own empty + // cache so reads go to the OS keyring without polluting the dev + // singleton's cache. + let prod_store = crate::secret_store::SecretStore::keyring("buzz-desktop"); + let dev_store = crate::secret_store::SecretStore::shared(keyring_service()); + copy_agent_keys_between_stores(&pubkeys, &prod_store, dev_store); +} + +/// Marker key stored inside the dev blob after a successful agent-key migration. +/// Its presence means all agent keys that existed in the prod service at +/// migration time have been copied; subsequent dev boots skip the migration +/// entirely (no prod keyring access). +#[cfg(debug_assertions)] +const DEV_MIGRATION_MARKER: &str = "_dev_migration_v1"; + +/// Testable core of [`migrate_agent_keys_to_dev_service`]: copy `agent:` +/// entries from `src` to `dst` for each pubkey, then write a migration-complete +/// marker so future boots skip the entire function with zero prod-keyring access. +/// +/// On the first migration boot: +/// 1. One `dst.load_all_readonly()` — dev blob read (1 keychain prompt) +/// 2. One `src.load_all_readonly()` — prod blob read (1 keychain prompt) +/// 3. One `dst.store_all()` — dev blob write (same service as #1; macOS may +/// skip the ACL prompt if the initial grant was "Always Allow") +/// +/// On subsequent boots (marker already present): +/// 1. One `dst.load_all_readonly()` — dev blob read (1 keychain prompt) +/// Returns immediately — prod keyring is NEVER accessed. +/// +/// Idempotency: keys already present in `dst` are not overwritten (the agent +/// may have rotated their key in the dev service after initial migration). +/// New agents (pubkey not in `src`) are silently skipped — they will mint a +/// fresh key on their next onboarding run. +#[cfg(debug_assertions)] +fn copy_agent_keys_between_stores(pubkeys: &[String], src: &impl KeyStore, dst: &impl KeyStore) { + // One read of the dev blob. If the migration-complete marker is present, + // all prior agent keys are already in the dev service — skip entirely. + let dst_map: HashMap = match dst.load_all_readonly() { + Ok(Some(map)) if map.contains_key(DEV_MIGRATION_MARKER) => { + return; // already migrated: 0 prod keyring accesses + } + Ok(Some(map)) => map, + Ok(None) => HashMap::new(), + Err(e) => { + eprintln!("buzz-desktop: keyring-dev-migration: cannot read dev keyring: {e}"); + return; + } + }; + + // One read of the prod blob. + let src_map: HashMap = match src.load_all_readonly() { + Ok(Some(map)) => map, + Ok(None) => HashMap::new(), // prod has no blob yet — nothing to copy + Err(e) => { + eprintln!("buzz-desktop: keyring-dev-migration: cannot read prod keyring: {e}"); + return; + } + }; + + // Compute the set of entries to write: agent keys absent from dst, plus + // the migration-complete marker. + let mut to_write: HashMap = HashMap::new(); + let mut copied = 0usize; + for pubkey in pubkeys { + let name = agent_keyring_name(pubkey); + if dst_map.contains_key(&name) { + continue; // already in dev service — do not overwrite (idempotent) + } + if let Some(nsec) = src_map.get(&name) { + to_write.insert(name, nsec.clone()); + copied += 1; + } + // absent from src → new agent, will mint a fresh key + } + + // Always write the marker so future boots skip the prod read entirely, + // even when there were no keys to copy (empty dev environment). + to_write.insert(DEV_MIGRATION_MARKER.to_string(), "done".to_string()); + + if let Err(e) = dst.store_all(&to_write) { + eprintln!("buzz-desktop: keyring-dev-migration: cannot write to dev keyring: {e}"); + return; + } + + if copied > 0 { + eprintln!( + "buzz-desktop: keyring-dev-migration: copied {copied} agent key(s) from buzz-desktop" + ); + } +} + /// Remove an agent's key from the keyring (best-effort). Called when an agent /// is deleted so its secret does not linger in the OS store. pub fn delete_agent_key(pubkey: &str) { @@ -602,6 +753,7 @@ mod tests { fail_verify: bool, stored: RefCell>, write_count: RefCell, + read_count: RefCell, } impl FakeKeyStore { @@ -611,6 +763,7 @@ mod tests { fail_verify: false, stored: RefCell::new(HashMap::new()), write_count: RefCell::new(0), + read_count: RefCell::new(0), } } fn unreachable() -> Self { @@ -619,6 +772,7 @@ mod tests { fail_verify: false, stored: RefCell::new(HashMap::new()), write_count: RefCell::new(0), + read_count: RefCell::new(0), } } fn verify_fails() -> Self { @@ -627,6 +781,7 @@ mod tests { fail_verify: true, stored: RefCell::new(HashMap::new()), write_count: RefCell::new(0), + read_count: RefCell::new(0), } } /// Seed a key as already present in the keyring. @@ -652,8 +807,26 @@ mod tests { if !self.reachable { return Err("keyring backend unreachable".to_string()); } + *self.read_count.borrow_mut() += 1; Ok(self.stored.borrow().get(name).cloned()) } + fn load_readonly(&self, name: &str) -> Result, String> { + // The fake has no side effects, so load_readonly is identical to load. + self.load(name) + } + fn load_all_readonly(&self) -> Result>, String> { + if !self.reachable { + return Err("keyring backend unreachable".to_string()); + } + *self.read_count.borrow_mut() += 1; + let map = self.stored.borrow().clone(); + // Return None when completely empty (simulates no blob written yet). + if map.is_empty() { + Ok(None) + } else { + Ok(Some(map)) + } + } fn write_and_verify(&self, name: &str, value: &str) -> Result<(), String> { if self.fail_verify { return Err("read-back verify failed".to_string()); @@ -664,6 +837,20 @@ mod tests { .insert(name.to_string(), value.to_string()); Ok(()) } + fn store_all(&self, entries: &HashMap) -> Result<(), String> { + if !self.reachable { + return Err("keyring backend unreachable".to_string()); + } + if self.fail_verify { + return Err("read-back verify failed".to_string()); + } + *self.write_count.borrow_mut() += 1; + let mut stored = self.stored.borrow_mut(); + for (k, v) in entries { + stored.insert(k.clone(), v.clone()); + } + Ok(()) + } } fn record_with_key(nsec: &str) -> ManagedAgentRecord { @@ -942,4 +1129,192 @@ mod tests { "2026-05-27T15:16:32 INFO buzz_acp: starting" ); } + + // ── keyring-dev-migration tests ──────────────────────────────────────── + + #[test] + fn copy_agent_keys_copies_keys_present_in_src_to_dst() { + // Keys in src but not in dst must be copied in a single bulk write, + // and the migration-complete marker must be set. + let src = FakeKeyStore::reachable() + .with_key(&agent_keyring_name("agent-alpha"), "nsec1alpha") + .with_key(&agent_keyring_name("agent-beta"), "nsec1beta"); + let dst = FakeKeyStore::reachable(); + + super::copy_agent_keys_between_stores( + &["agent-alpha".to_string(), "agent-beta".to_string()], + &src, + &dst, + ); + + assert_eq!( + dst.stored + .borrow() + .get(&agent_keyring_name("agent-alpha")) + .map(String::as_str), + Some("nsec1alpha"), + "agent-alpha must be copied from src to dst" + ); + assert_eq!( + dst.stored + .borrow() + .get(&agent_keyring_name("agent-beta")) + .map(String::as_str), + Some("nsec1beta"), + "agent-beta must be copied from src to dst" + ); + assert_eq!( + dst.stored + .borrow() + .get(super::DEV_MIGRATION_MARKER) + .map(String::as_str), + Some("done"), + "migration-complete marker must be set after first migration" + ); + // Bulk write: exactly 1 store_all call. + assert_eq!( + *dst.write_count.borrow(), + 1, + "must perform exactly one bulk write" + ); + // Src accessed exactly once (bulk blob read). + assert_eq!( + *src.read_count.borrow(), + 1, + "src must be read exactly once (bulk)" + ); + } + + #[test] + fn copy_agent_keys_skips_keys_already_in_dst() { + // Idempotency: a key already present in dst must NOT be overwritten + // — the agent may have rotated their key in the dev service. + let src = + FakeKeyStore::reachable().with_key(&agent_keyring_name("agent-alpha"), "nsec1old"); + let dst = + FakeKeyStore::reachable().with_key(&agent_keyring_name("agent-alpha"), "nsec1new"); + + super::copy_agent_keys_between_stores(&["agent-alpha".to_string()], &src, &dst); + + // dst value must remain unchanged — src must not overwrite it. + assert_eq!( + dst.stored + .borrow() + .get(&agent_keyring_name("agent-alpha")) + .map(String::as_str), + Some("nsec1new"), + "key already in dst must not be overwritten by migration" + ); + // Marker must still be written even though no new keys were copied. + assert_eq!( + dst.stored + .borrow() + .get(super::DEV_MIGRATION_MARKER) + .map(String::as_str), + Some("done"), + "marker must be set even when all keys are already present" + ); + } + + #[test] + fn copy_agent_keys_skips_keys_absent_from_src() { + // A pubkey with no entry in src (new agent that will mint a fresh key) + // must be silently skipped — no agent key written to dst. + let src = FakeKeyStore::reachable(); // empty + let dst = FakeKeyStore::reachable(); + + super::copy_agent_keys_between_stores(&["new-agent".to_string()], &src, &dst); + + assert!( + dst.stored + .borrow() + .get(&agent_keyring_name("new-agent")) + .is_none(), + "absent src key must produce no agent key write to dst" + ); + // Marker must still be written. + assert_eq!( + dst.stored + .borrow() + .get(super::DEV_MIGRATION_MARKER) + .map(String::as_str), + Some("done"), + "marker must be set even when no keys were present in src" + ); + } + + #[test] + fn copy_agent_keys_skips_all_when_dst_unreachable() { + // When dst keyring is unreachable the migration must be a no-op — never + // data-loss (failing to write is fine; the agent will re-mint on next + // onboarding run). + let src = + FakeKeyStore::reachable().with_key(&agent_keyring_name("agent-alpha"), "nsec1alpha"); + let dst = FakeKeyStore::unreachable(); + + super::copy_agent_keys_between_stores(&["agent-alpha".to_string()], &src, &dst); + + // No writes attempted to an unreachable dst. + assert_eq!(*dst.write_count.borrow(), 0); + // Src must not have been accessed (failed on dst read, returned early). + assert_eq!( + *src.read_count.borrow(), + 0, + "src must not be accessed when dst is unreachable" + ); + } + + #[test] + fn copy_agent_keys_skips_entirely_when_marker_present() { + // After the first migration, the marker is in dst. Subsequent calls + // must return immediately — the prod keyring (src) must never be read. + let src = + FakeKeyStore::reachable().with_key(&agent_keyring_name("agent-alpha"), "nsec1alpha"); + let dst = FakeKeyStore::reachable() + .with_key(super::DEV_MIGRATION_MARKER, "done") + .with_key(&agent_keyring_name("agent-alpha"), "nsec1dev"); + + super::copy_agent_keys_between_stores(&["agent-alpha".to_string()], &src, &dst); + + // Src must not have been accessed at all. + assert_eq!( + *src.read_count.borrow(), + 0, + "src must not be read when migration-complete marker is present" + ); + // Dst must not have been written. + assert_eq!( + *dst.write_count.borrow(), + 0, + "dst must not be written when migration-complete marker is present" + ); + // Dev key must remain unchanged. + assert_eq!( + dst.stored + .borrow() + .get(&agent_keyring_name("agent-alpha")) + .map(String::as_str), + Some("nsec1dev"), + "dev key must not be overwritten on subsequent boots" + ); + } + + #[test] + fn copy_agent_keys_writes_marker_even_with_empty_agent_list() { + // An empty pubkey list (no agents yet) must still write the marker so + // future boots skip the prod read. + let src = FakeKeyStore::reachable(); + let dst = FakeKeyStore::reachable(); + + super::copy_agent_keys_between_stores(&[], &src, &dst); + + assert_eq!( + dst.stored + .borrow() + .get(super::DEV_MIGRATION_MARKER) + .map(String::as_str), + Some("done"), + "marker must be set even when pubkey list is empty" + ); + } } diff --git a/desktop/src-tauri/src/migration.rs b/desktop/src-tauri/src/migration.rs index fb3b033737..0f1d292d24 100644 --- a/desktop/src-tauri/src/migration.rs +++ b/desktop/src-tauri/src/migration.rs @@ -145,6 +145,16 @@ pub fn run_boot_migrations(app: &tauri::AppHandle) { migrate_legacy_app_data_dir(app); sync_shared_agent_data(app); + // Dev-build-only: copy any agent keys that exist in the production + // keyring ("buzz-desktop") into the dev service ("buzz-desktop-dev") + // so existing agents don't lose their keys after the service-name split. + // Must run after sync_shared_agent_data (JSON symlinked) and before + // any load_managed_agents call (which runs hydrate_keys against the + // dev service and would log "has no key" for un-migrated entries). + #[cfg(debug_assertions)] + if is_dev { + crate::managed_agents::migrate_agent_keys_to_dev_service(app); + } migrate_packs_to_teams(app); reconcile_persona_team_dirs(app); migrate_persona_provider_to_runtime(app); diff --git a/desktop/src-tauri/src/secret_store.rs b/desktop/src-tauri/src/secret_store.rs index ab6a4c267c..f3d4d0df8f 100644 --- a/desktop/src-tauri/src/secret_store.rs +++ b/desktop/src-tauri/src/secret_store.rs @@ -575,6 +575,69 @@ impl SecretStore { } } + /// Read the secret for `key` without any legacy-migration side effects. + /// + /// Unlike [`load`](Self::load), this method never calls + /// `migrate_legacy_key` and therefore never writes to or deletes from the + /// keyring. Use this when the caller must guarantee the store is not + /// mutated — for example, when reading from a foreign service (prod) to + /// copy values into a dev service. + /// + /// Returns `Ok(Some(value))` when the key is present in the blob, + /// `Ok(None)` when the blob is absent or the key is not in it, and `Err` + /// only when the backend is unavailable. + pub fn load_readonly(&self, key: &str) -> Result, String> { + #[cfg(feature = "system-keyring")] + { + match self.load_blob()? { + Some(map) => Ok(map.get(key).cloned()), + None => Ok(None), + } + } + #[cfg(not(feature = "system-keyring"))] + { + let _ = key; + Err("system-keyring feature disabled".to_string()) + } + } + + /// Read the entire blob without any legacy-migration side effects. + /// + /// Returns the full key→value map when a blob exists, `Ok(None)` when no + /// blob has been written yet, and `Err` only when the backend is + /// unavailable. Never calls `migrate_legacy_key`. + pub fn load_all_readonly(&self) -> Result>, String> { + #[cfg(feature = "system-keyring")] + { + self.load_blob() + } + #[cfg(not(feature = "system-keyring"))] + { + Err("system-keyring feature disabled".to_string()) + } + } + + /// Insert all entries from `entries` into the blob in a single mutation. + /// + /// Entries that already exist in the blob are overwritten; entries not + /// present in `entries` are left unchanged. If the resulting blob is + /// identical to what is already stored, no keychain write occurs. + pub fn store_all(&self, entries: &HashMap) -> Result<(), String> { + #[cfg(feature = "system-keyring")] + { + self.mutate_blob(|map| { + for (k, v) in entries { + map.insert(k.clone(), v.clone()); + } + }) + } + #[cfg(not(feature = "system-keyring"))] + { + let _ = entries; + Err("system-keyring feature disabled".to_string()) + } + } + /// On first launch after upgrading from the per-key DPK format, read the /// old DPK entry for `key`, write it into a new blob, and delete the old /// item. Returns `Ok(None)` when no old entry exists. diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index a830cfd2e3..92bf092df7 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -203,7 +203,7 @@ function OnboardingLoadingGate() { tabIndex={-1} type="button" > - Continue with Block Inc. workspace + Continue with default workspace )} diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index cfc7e428c8..e47d6f1ec6 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -595,7 +595,7 @@ test("first-run default workspace handoff gives immediate stepper feedback", asy await expect(page.getByText("Welcome to Buzz")).toBeVisible(); await page - .getByRole("button", { name: "Continue with Block Inc. workspace" }) + .getByRole("button", { name: "Continue with default workspace" }) .click(); await page.waitForTimeout(80); @@ -603,7 +603,7 @@ test("first-run default workspace handoff gives immediate stepper feedback", asy 0, ); await expect( - page.getByRole("button", { name: "Continue with Block Inc. workspace" }), + page.getByRole("button", { name: "Continue with default workspace" }), ).toBeVisible(); await expect(page.getByRole("progressbar")).toHaveAttribute( "aria-valuenow", @@ -671,7 +671,7 @@ test("welcome presents custom workspace setup as joining a workspace", async ({ await page.goto("/"); await expect( - page.getByRole("button", { name: "Continue with Block Inc. workspace" }), + page.getByRole("button", { name: "Continue with default workspace" }), ).toHaveCount(0); await page.getByRole("button", { name: "Join a workspace" }).click();