fix(desktop): migrate Sprout workspace storage (#1016)

Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
Bradley Axen
2026-06-12 14:51:54 -07:00
committed by GitHub
parent 5a8cc79c6a
commit 563f684342
12 changed files with 679 additions and 15 deletions

View File

@@ -877,6 +877,7 @@ dependencies = [
"reqwest 0.13.4",
"rodio",
"rubato",
"rusqlite",
"serde",
"serde_json",
"serde_yaml",
@@ -2333,6 +2334,18 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.4.1"
@@ -3062,6 +3075,15 @@ dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "heapify"
version = "0.2.0"
@@ -4185,6 +4207,16 @@ dependencies = [
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -7275,6 +7307,20 @@ dependencies = [
"windowfunctions",
]
[[package]]
name = "rusqlite"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
"bitflags 2.11.1",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rust-ini"
version = "0.21.3"

View File

@@ -79,6 +79,7 @@ png = "0.18"
zip = "8"
sherpa-onnx = "1.12"
regex = "1"
rusqlite = "0.37"
axum = "0.8"
rodio = "0.22"
earshot = "1.0"

View File

@@ -0,0 +1,289 @@
use std::path::{Path, PathBuf};
use rusqlite::{Connection, OpenFlags};
use serde::Serialize;
const BUZZ_RELEASE_IDENTIFIER_PREFIX: &str = "xyz.block.buzz.app";
const SPROUT_RELEASE_IDENTIFIER: &str = "xyz.block.sprout.app";
const BUZZ_DEV_IDENTIFIER_PREFIX: &str = "xyz.block.buzz.app.dev";
const SPROUT_DEV_IDENTIFIER_PREFIX: &str = "xyz.block.sprout.app.dev";
const SPROUT_WORKSPACES_KEY: &str = "sprout-workspaces";
const SPROUT_ACTIVE_WORKSPACE_KEY: &str = "sprout-active-workspace-id";
const SPROUT_ONBOARDING_COMPLETE_PREFIX: &str = "sprout-onboarding-complete.v1:";
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LegacyWorkspaceStorage {
workspaces: Option<String>,
active_workspace_id: Option<String>,
onboarding_completions: Vec<LegacyOnboardingCompletion>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LegacyOnboardingCompletion {
pubkey: String,
value: String,
}
fn legacy_identifier(current_identifier: &str) -> Option<String> {
if current_identifier.starts_with(BUZZ_DEV_IDENTIFIER_PREFIX) {
Some(current_identifier.replacen(
BUZZ_DEV_IDENTIFIER_PREFIX,
SPROUT_DEV_IDENTIFIER_PREFIX,
1,
))
} else if current_identifier.starts_with(BUZZ_RELEASE_IDENTIFIER_PREFIX) {
Some(current_identifier.replacen(
BUZZ_RELEASE_IDENTIFIER_PREFIX,
SPROUT_RELEASE_IDENTIFIER,
1,
))
} else {
None
}
}
#[cfg(target_os = "macos")]
fn legacy_webkit_data_root(identifier: &str) -> Option<PathBuf> {
dirs::home_dir().map(|home| {
home.join("Library")
.join("WebKit")
.join(identifier)
.join("WebsiteData")
})
}
#[cfg(not(target_os = "macos"))]
fn legacy_webkit_data_root(_identifier: &str) -> Option<PathBuf> {
None
}
fn collect_local_storage_databases(root: &Path, databases: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_local_storage_databases(&path, databases);
} else if path.file_name().and_then(|name| name.to_str()) == Some("localstorage.sqlite3") {
databases.push(path);
}
}
}
fn decode_webkit_local_storage_value(bytes: &[u8]) -> Option<String> {
if bytes.is_empty() {
return Some(String::new());
}
if bytes.len() % 2 == 0 {
let has_utf16_ascii_shape = bytes.chunks_exact(2).any(|chunk| chunk[1] == 0);
if has_utf16_ascii_shape {
let utf16: Vec<u16> = bytes
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect();
if let Ok(value) = String::from_utf16(&utf16) {
return Some(value.trim_end_matches('\0').to_string());
}
}
}
String::from_utf8(bytes.to_vec()).ok()
}
fn read_legacy_workspace_storage_db(path: &Path) -> Result<LegacyWorkspaceStorage, String> {
let conn = Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| format!("open legacy localStorage db: {e}"))?;
let mut stmt = conn
.prepare(
"SELECT key, value FROM ItemTable \
WHERE key = ?1 OR key = ?2 OR key LIKE ?3",
)
.map_err(|e| format!("prepare legacy localStorage query: {e}"))?;
let mut rows = stmt
.query([
SPROUT_WORKSPACES_KEY,
SPROUT_ACTIVE_WORKSPACE_KEY,
&format!("{SPROUT_ONBOARDING_COMPLETE_PREFIX}%"),
])
.map_err(|e| format!("query legacy localStorage: {e}"))?;
let mut result = LegacyWorkspaceStorage::default();
while let Some(row) = rows
.next()
.map_err(|e| format!("read legacy localStorage row: {e}"))?
{
let key: String = row
.get(0)
.map_err(|e| format!("read legacy localStorage key: {e}"))?;
let value_bytes: Vec<u8> = row
.get(1)
.map_err(|e| format!("read legacy localStorage value: {e}"))?;
let Some(value) = decode_webkit_local_storage_value(&value_bytes) else {
continue;
};
if key == SPROUT_WORKSPACES_KEY {
result.workspaces = Some(value);
} else if key == SPROUT_ACTIVE_WORKSPACE_KEY {
result.active_workspace_id = Some(value);
} else if let Some(pubkey) = key.strip_prefix(SPROUT_ONBOARDING_COMPLETE_PREFIX) {
result
.onboarding_completions
.push(LegacyOnboardingCompletion {
pubkey: pubkey.to_string(),
value,
});
}
}
Ok(result)
}
fn merge_legacy_workspace_storage(
target: &mut LegacyWorkspaceStorage,
source: LegacyWorkspaceStorage,
) {
if target.workspaces.is_none() {
target.workspaces = source.workspaces;
}
if target.active_workspace_id.is_none() {
target.active_workspace_id = source.active_workspace_id;
}
target
.onboarding_completions
.extend(source.onboarding_completions);
}
/// Return workspace-scoped localStorage values from the legacy Sprout WebKit
/// data directory so the frontend can seed Buzz localStorage before first
/// render. This is separate from `migrate_legacy_app_data_dir`: Tauri app data
/// migration copies files such as `identity.key`, but WebKit localStorage lives
/// under `~/Library/WebKit/<identifier>/...` on macOS and is not included in the
/// app data directory.
#[tauri::command]
pub fn get_legacy_workspace_storage(
app: tauri::AppHandle,
) -> Result<LegacyWorkspaceStorage, String> {
let Some(identifier) = legacy_identifier(&app.config().identifier) else {
return Ok(LegacyWorkspaceStorage::default());
};
let Some(root) = legacy_webkit_data_root(&identifier) else {
return Ok(LegacyWorkspaceStorage::default());
};
if !root.exists() {
return Ok(LegacyWorkspaceStorage::default());
}
let mut databases = Vec::new();
collect_local_storage_databases(&root, &mut databases);
let mut result = LegacyWorkspaceStorage::default();
for database in databases {
match read_legacy_workspace_storage_db(&database) {
Ok(storage) => merge_legacy_workspace_storage(&mut result, storage),
Err(error) => eprintln!(
"buzz-desktop: legacy-local-storage-migration: {}: {error}",
database.display()
),
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_identifier_maps_release_identifier() {
assert_eq!(
legacy_identifier("xyz.block.buzz.app"),
Some("xyz.block.sprout.app".to_string())
);
}
#[test]
fn legacy_identifier_maps_dev_worktree_identifier() {
assert_eq!(
legacy_identifier("xyz.block.buzz.app.dev.my-branch"),
Some("xyz.block.sprout.app.dev.my-branch".to_string())
);
}
#[test]
fn decode_webkit_local_storage_value_reads_utf16le() {
let bytes: Vec<u8> = "true".encode_utf16().flat_map(u16::to_le_bytes).collect();
assert_eq!(
decode_webkit_local_storage_value(&bytes).as_deref(),
Some("true")
);
}
#[test]
fn decode_webkit_local_storage_value_reads_utf8_fallback() {
assert_eq!(
decode_webkit_local_storage_value(b"plain utf8").as_deref(),
Some("plain utf8")
);
}
#[test]
fn read_legacy_workspace_storage_db_reads_workspace_keys() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("localstorage.sqlite3");
let conn = Connection::open(&path).unwrap();
conn.execute(
"CREATE TABLE ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)",
[],
)
.unwrap();
fn utf16le(value: &str) -> Vec<u8> {
value.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
conn.execute(
"INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
(
SPROUT_WORKSPACES_KEY,
utf16le("[{\"relayUrl\":\"wss://relay.example.com\"}]"),
),
)
.unwrap();
conn.execute(
"INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
(SPROUT_ACTIVE_WORKSPACE_KEY, utf16le("workspace-1")),
)
.unwrap();
conn.execute(
"INSERT INTO ItemTable (key, value) VALUES (?1, ?2)",
(
format!("{SPROUT_ONBOARDING_COMPLETE_PREFIX}abc123"),
utf16le("true"),
),
)
.unwrap();
drop(conn);
let storage = read_legacy_workspace_storage_db(&path).unwrap();
assert_eq!(
storage.workspaces.as_deref(),
Some("[{\"relayUrl\":\"wss://relay.example.com\"}]")
);
assert_eq!(storage.active_workspace_id.as_deref(), Some("workspace-1"));
assert_eq!(storage.onboarding_completions.len(), 1);
assert_eq!(storage.onboarding_completions[0].pubkey, "abc123");
assert_eq!(storage.onboarding_completions[0].value, "true");
}
}

View File

@@ -10,6 +10,7 @@ mod engrams;
mod export_util;
mod identity;
mod identity_archive;
mod legacy_storage;
mod media;
mod media_download;
mod media_transcode;
@@ -37,6 +38,7 @@ pub use dms::*;
pub use engrams::*;
pub use identity::*;
pub use identity_archive::*;
pub use legacy_storage::*;
pub use media::*;
pub use media_download::*;
#[cfg(feature = "mesh-llm")]

View File

@@ -690,6 +690,7 @@ pub fn run() {
search_users,
get_presence,
get_default_relay_url,
get_legacy_workspace_storage,
is_shared_identity,
get_relay_ws_url,
get_relay_http_url,

View File

@@ -0,0 +1,153 @@
import assert from "node:assert/strict";
import test from "node:test";
import { applyLegacyWorkspaceStorage } from "./legacyWorkspaceStorage.ts";
function createMemoryStorage(initial = {}) {
const values = new Map(Object.entries(initial));
return {
getItem(key) {
return values.has(key) ? values.get(key) : null;
},
setItem(key, value) {
values.set(key, String(value));
},
removeItem(key) {
values.delete(key);
},
clear() {
values.clear();
},
key(index) {
return Array.from(values.keys())[index] ?? null;
},
get length() {
return values.size;
},
};
}
const legacyWorkspaces = JSON.stringify([
{
id: "legacy-workspace",
name: "Existing relay",
relayUrl: "wss://relay.example.com",
addedAt: "2026-06-12T00:00:00.000Z",
},
]);
const currentWorkspaces = JSON.stringify([
{
id: "current-workspace",
name: "Current relay",
relayUrl: "wss://current.example.com",
addedAt: "2026-06-12T00:00:00.000Z",
},
]);
const localhostWorkspaces = JSON.stringify([
{
id: "local-workspace",
name: "Local Dev",
relayUrl: "ws://localhost:3000",
addedAt: "2026-06-12T00:00:00.000Z",
},
]);
test("applyLegacyWorkspaceStorage seeds missing workspaces and active workspace", () => {
const storage = createMemoryStorage();
applyLegacyWorkspaceStorage(
{
workspaces: legacyWorkspaces,
activeWorkspaceId: "legacy-workspace",
onboardingCompletions: [],
},
storage,
);
assert.equal(storage.getItem("buzz-workspaces"), legacyWorkspaces);
assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy-workspace");
});
test("applyLegacyWorkspaceStorage preserves existing non-local Buzz workspaces", () => {
const storage = createMemoryStorage({
"buzz-workspaces": currentWorkspaces,
"buzz-active-workspace-id": "current-workspace",
});
applyLegacyWorkspaceStorage(
{
workspaces: legacyWorkspaces,
activeWorkspaceId: "legacy-workspace",
onboardingCompletions: [],
},
storage,
);
assert.equal(storage.getItem("buzz-workspaces"), currentWorkspaces);
assert.equal(
storage.getItem("buzz-active-workspace-id"),
"current-workspace",
);
});
test("applyLegacyWorkspaceStorage replaces broken localhost first-run workspace", () => {
const storage = createMemoryStorage({
"buzz-workspaces": localhostWorkspaces,
"buzz-active-workspace-id": "local-workspace",
});
applyLegacyWorkspaceStorage(
{
workspaces: legacyWorkspaces,
activeWorkspaceId: "legacy-workspace",
onboardingCompletions: [],
},
storage,
);
assert.equal(storage.getItem("buzz-workspaces"), legacyWorkspaces);
assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy-workspace");
});
test("applyLegacyWorkspaceStorage treats trailing-slash localhost as broken", () => {
const storage = createMemoryStorage({
"buzz-workspaces": JSON.stringify([
{
id: "local-workspace",
name: "Local Dev",
relayUrl: "ws://localhost:3000/",
addedAt: "2026-06-12T00:00:00.000Z",
},
]),
"buzz-active-workspace-id": "local-workspace",
});
applyLegacyWorkspaceStorage(
{
workspaces: legacyWorkspaces,
activeWorkspaceId: "legacy-workspace",
onboardingCompletions: [],
},
storage,
);
assert.equal(storage.getItem("buzz-workspaces"), legacyWorkspaces);
assert.equal(storage.getItem("buzz-active-workspace-id"), "legacy-workspace");
});
test("applyLegacyWorkspaceStorage migrates onboarding completion keys", () => {
const storage = createMemoryStorage();
applyLegacyWorkspaceStorage(
{
workspaces: null,
activeWorkspaceId: null,
onboardingCompletions: [{ pubkey: "abc123", value: "true" }],
},
storage,
);
assert.equal(storage.getItem("buzz-onboarding-complete.v1:abc123"), "true");
});

View File

@@ -0,0 +1,138 @@
import { invokeTauri } from "@/shared/api/tauri";
const BUZZ_WORKSPACES_KEY = "buzz-workspaces";
const BUZZ_ACTIVE_WORKSPACE_KEY = "buzz-active-workspace-id";
const BUZZ_ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX =
"buzz-onboarding-complete.v1:";
const LOCAL_DEV_RELAY_URLS = new Set([
"ws://localhost:3000",
"ws://127.0.0.1:3000",
]);
type LegacyWorkspaceStorageSnapshot = {
workspaces: string | null;
activeWorkspaceId: string | null;
onboardingCompletions: Array<{
pubkey: string;
value: string;
}>;
};
type StoredWorkspace = {
relayUrl?: unknown;
};
function parseWorkspaceList(raw: string | null): StoredWorkspace[] | null {
if (!raw) {
return null;
}
try {
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as StoredWorkspace[]) : null;
} catch {
return null;
}
}
function normalizeRelayUrl(relayUrl: string) {
return relayUrl.trim().replace(/\/$/, "");
}
function hasOnlyLocalDevWorkspace(raw: string | null): boolean {
const workspaces = parseWorkspaceList(raw);
return (
workspaces?.length === 1 &&
typeof workspaces[0]?.relayUrl === "string" &&
LOCAL_DEV_RELAY_URLS.has(normalizeRelayUrl(workspaces[0].relayUrl))
);
}
function hasNonLocalCurrentWorkspaces(raw: string | null): boolean {
const workspaces = parseWorkspaceList(raw);
return (
workspaces !== null &&
workspaces.length > 0 &&
!hasOnlyLocalDevWorkspace(raw)
);
}
function shouldWriteLegacyWorkspaces({
currentWorkspacesRaw,
legacyWorkspacesRaw,
}: {
currentWorkspacesRaw: string | null;
legacyWorkspacesRaw: string | null;
}) {
const legacyWorkspaces = parseWorkspaceList(legacyWorkspacesRaw);
if (!legacyWorkspaces || legacyWorkspaces.length === 0) {
return false;
}
return !hasNonLocalCurrentWorkspaces(currentWorkspacesRaw);
}
export function applyLegacyWorkspaceStorage(
legacyStorage: LegacyWorkspaceStorageSnapshot,
storage: Storage = window.localStorage,
): void {
const currentWorkspacesRaw = storage.getItem(BUZZ_WORKSPACES_KEY);
const shouldWriteWorkspaces = shouldWriteLegacyWorkspaces({
currentWorkspacesRaw,
legacyWorkspacesRaw: legacyStorage.workspaces,
});
if (shouldWriteWorkspaces && legacyStorage.workspaces) {
storage.setItem(BUZZ_WORKSPACES_KEY, legacyStorage.workspaces);
}
const currentActiveWorkspaceId = storage.getItem(BUZZ_ACTIVE_WORKSPACE_KEY);
if (
legacyStorage.activeWorkspaceId &&
(!currentActiveWorkspaceId || shouldWriteWorkspaces)
) {
storage.setItem(BUZZ_ACTIVE_WORKSPACE_KEY, legacyStorage.activeWorkspaceId);
}
for (const completion of legacyStorage.onboardingCompletions) {
const key = `${BUZZ_ONBOARDING_COMPLETION_STORAGE_KEY_PREFIX}${completion.pubkey}`;
if (storage.getItem(key) === null) {
storage.setItem(key, completion.value);
}
}
}
/**
* Seed Buzz localStorage from legacy Sprout WebKit localStorage before the app
* renders providers that read workspace state. The native command reads the old
* app identifier's WebKit SQLite database; this frontend step writes only when
* Buzz does not already have workspace state, except for the known broken
* Sprout→Buzz first-run handoff that created a single localhost workspace.
*/
export async function migrateLegacyWorkspaceStorageBeforeRender(): Promise<void> {
if (typeof window === "undefined") {
return;
}
const currentWorkspacesRaw = window.localStorage.getItem(BUZZ_WORKSPACES_KEY);
const hasCurrentActiveWorkspace = window.localStorage.getItem(
BUZZ_ACTIVE_WORKSPACE_KEY,
);
if (
currentWorkspacesRaw &&
hasCurrentActiveWorkspace &&
!hasOnlyLocalDevWorkspace(currentWorkspacesRaw)
) {
return;
}
try {
applyLegacyWorkspaceStorage(
await invokeTauri<LegacyWorkspaceStorageSnapshot>(
"get_legacy_workspace_storage",
),
);
} catch (error) {
console.warn("Failed to read legacy Sprout workspace storage.", error);
}
}

View File

@@ -31,6 +31,14 @@ type WelcomeSetupProps = {
};
const DEFAULT_WORKSPACE_HANDOFF_MIN_MS = 200;
const LOCAL_DEV_RELAY_URLS = new Set([
"ws://localhost:3000",
"ws://127.0.0.1:3000",
]);
function isLocalDevRelayUrl(relayUrl: string) {
return LOCAL_DEV_RELAY_URLS.has(relayUrl.trim().replace(/\/$/, ""));
}
function wait(ms: number) {
return new Promise<void>((resolve) => {
@@ -97,6 +105,12 @@ export function WelcomeSetup({
setError("Please enter a workspace URL.");
return;
}
if (!workspaceName && isLocalDevRelayUrl(trimmedUrl)) {
setError("Enter your relay URL to join a workspace.");
setTransitionMode("forward");
setPage("create-workspace");
return;
}
const handoffStartedAt = performance.now();
flushSync(() => {
@@ -220,20 +234,22 @@ export function WelcomeSetup({
</p>
<div className="mt-8 flex w-full flex-col gap-3">
<Button
className="h-10 w-full"
aria-disabled={isConnecting}
onClick={() => {
if (isConnecting) {
return;
}
setError(null);
void handleConnect(defaultRelayUrl);
}}
type="button"
>
Continue with Block Inc. workspace
</Button>
{isLocalDevRelayUrl(defaultRelayUrl) ? null : (
<Button
className="h-10 w-full"
aria-disabled={isConnecting}
onClick={() => {
if (isConnecting) {
return;
}
setError(null);
void handleConnect(defaultRelayUrl);
}}
type="button"
>
Continue with Block Inc. workspace
</Button>
)}
<Button
className="h-10 w-full"

View File

@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
import { App } from "@/app/App";
import "@/shared/styles/globals.css";
import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
import { migrateLegacyWorkspaceStorageBeforeRender } from "@/features/workspaces/legacyWorkspaceStorage";
import { WorkspacesProvider } from "@/features/workspaces/useWorkspaces";
import { ThemeProvider } from "@/shared/theme/ThemeProvider";
import { EmojiBurstProvider } from "@/shared/ui/EmojiBurstProvider";
@@ -78,6 +79,7 @@ async function installE2eBridgeIfConfigured() {
async function bootstrap() {
configureDevE2eBridgeFromUrl();
await installE2eBridgeIfConfigured();
await migrateLegacyWorkspaceStorageBeforeRender();
renderApp();
}

View File

@@ -6149,6 +6149,12 @@ export function maybeInstallE2eTauriMocks() {
return getRelayWsUrl(activeConfig);
case "get_default_relay_url":
return getRelayWsUrl(activeConfig);
case "get_legacy_workspace_storage":
return {
workspaces: null,
activeWorkspaceId: null,
onboardingCompletions: [],
};
case "get_relay_http_url":
return getRelayHttpUrl(activeConfig);
case "discover_acp_providers":

View File

@@ -582,7 +582,11 @@ test("first-run default workspace handoff gives immediate stepper feedback", asy
{
profileReadDelayMs: 2_000,
},
{ skipOnboardingSeed: true, skipWorkspaceSeed: true },
{
relayWsUrl: "wss://default.example.com",
skipOnboardingSeed: true,
skipWorkspaceSeed: true,
},
);
await page.goto("/");
@@ -621,6 +625,7 @@ test("first-run default workspace handoff gives immediate stepper feedback", asy
test("welcome can continue using an existing Nostr key", async ({ page }) => {
await installMockBridge(page, undefined, {
relayWsUrl: "wss://default.example.com",
skipOnboardingSeed: true,
skipWorkspaceSeed: true,
});
@@ -662,6 +667,9 @@ 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" }),
).toHaveCount(0);
await page.getByRole("button", { name: "Join a workspace" }).click();
await expect(

View File

@@ -446,6 +446,7 @@ export async function installMockBridge(
page: Page,
mock?: MockBridgeOptions,
options?: {
relayWsUrl?: string;
skipOnboardingSeed?: boolean;
skipWorkspaceSeed?: boolean;
seedPreviewFeatures?: boolean;
@@ -454,6 +455,7 @@ export async function installMockBridge(
await installBridge(page, {
mode: "mock",
mock,
relayWsUrl: options?.relayWsUrl,
skipOnboardingSeed: options?.skipOnboardingSeed,
skipWorkspaceSeed: options?.skipWorkspaceSeed,
seedPreviewFeatures: options?.seedPreviewFeatures,