feat(desktop): add isolated named demo builds (#6407)

🤖 I’m Larry, updating this description on Logan’s behalf.

## Summary

Build named macOS demo apps without Finder automation or collisions with
installed Buzz. `just desktop-demo-build "PR 6407 Demo"` produces a
matching app and DMG, with a fresh build identity even when the same
display name is reused.

- The headless DMG packager uses `hdiutil`; optional Finder styling is
bounded. The existing production release recipe is unchanged.
- Each demo has independent app data, keychain, nest, CLI name,
voice-model storage, repository discovery, and agent OAuth/config
storage. Reset preserves production and sibling-demo state, and retains
retry intent when credential removal or root resolution fails.
- Native links accept only the active build’s registered scheme, then
translate validated entity links into the frontend’s canonical `buzz:`
format.
- The recipe builds all six executable sidecars. Display names are
capped at 31 ASCII characters so the generated identity fits Rust’s
build-time limit.

**Open delivery requirement:** downloaded demos must run without a
Gatekeeper security override. The current recipe is ad-hoc signed and
unnotarized; it does **not** satisfy this requirement. Trusted
branch-demo signing/distribution remains blocked on establishing an
approved signing path. This PR is not being presented as complete
download-and-run delivery.

### Related issue

N/A — reported in the Buzz DMG-packaging workstream.

### Testing

At `11ce21ff97cb387ad676e7caa65b00964097d0bb`, macOS Blox passed the
Tauri workspace suite and compiled-flags gate (including the full
named-demo state; each library pass: 2,992 passed, 19 ignored), Tauri
all-target clippy, the full `buzz-agent` package suite, and frontend
lint/typecheck plus 5,733 tests. Regression coverage includes
cold-start/running entity-link handling, wrong-build rejection, OAuth
deletion failure and retry, unresolved credential roots, and
production/sibling preservation.

At the same head, an extra full named-demo/mesh-enabled run had 3,092
passing tests and one failure: a pre-existing shared-compute `auto`
versus `mesh` expectation, also reproduced on the old published head
`a77b25eca`. The ordinary and demo-state matrix above passes; this is
not an all-features-green claim. Live macOS Launch Services delivery
remains unverified.

GitHub CI completed with 30 successful checks and 9 skipped. The
exact-range security review has not run; its authorization notice
remains open. CI success does not establish trusted signing or
downloaded-app launch.

Earlier demo artifacts established matching app/DMG names, side-by-side
launch, and six non-empty executable arm64 sidecars. These screenshots
show an earlier artifact, not a new build of the final repair commit.
Signature-integrity checks are not Gatekeeper/notarization evidence.

<img width="1032" height="548" alt="Buzz PR 6407 Demo disk image
containing the matching app"
src="https://github.com/user-attachments/assets/bca0277e-db03-4308-b280-fcad55e6d601"
/>

<img width="1186" height="821" alt="Buzz PR 6407 Demo running alongside
other Buzz installations"
src="https://github.com/user-attachments/assets/b4bf4ae5-c341-4e15-8090-9d2ea7c623b6"
/>

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <loganj+sandbox-larry@squareup.com>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
This commit is contained in:
Logan Johnson
2026-08-31 21:14:19 -04:00
committed by GitHub
parent 0affe527e8
commit 4a9de1a3a1
29 changed files with 1227 additions and 98 deletions

View File

@@ -256,7 +256,18 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
echo "Both compiled states verified."
echo "=== Maximum accepted demo name reaches Rust build validation ==="
DEMO_CONFIG="$(node ../scripts/demo-build-config.mjs "$(printf 'x%.0s' {1..31})" /dev/null 1234567812345678)"
DEMO_SLUG="$(node -e 'console.log(JSON.parse(process.argv[1]).slug)' "$DEMO_CONFIG")"
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" \
BUZZ_TEST_EXPECTED_DEMO_SLUG="$DEMO_SLUG" \
cargo test compiled_demo_slug_matches_expected -- --ignored --nocapture
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" cargo test --workspace
if node ../scripts/demo-build-config.mjs "$(printf 'x%.0s' {1..32})" /dev/null 1234567812345678; then
echo "A 32-character demo name unexpectedly passed JavaScript validation" >&2
exit 1
fi
echo "Both compiled states and the accepted/rejected demo-name boundary verified."
# Build the full desktop Tauri app locally (unsigned, for testing)
# Sidecar binary list must stay in sync with _ensure-sidecar-stubs above.
@@ -277,6 +288,38 @@ desktop-release-build target="aarch64-apple-darwin":
pnpm install
cd {{desktop_dir}} && pnpm tauri build --features mesh-llm --target {{target}}
# Build an unsigned named macOS demo DMG with isolated app and runtime identities.
desktop-demo-build demo_name target="aarch64-apple-darwin":
#!/usr/bin/env bash
set -euo pipefail
TARGET={{target}}
[[ "$(uname -s)" == "Darwin" && "$TARGET" == *-apple-darwin ]] || { echo "Demo DMGs require a macOS Apple target" >&2; exit 2; }
CONFIG_PATH="$(mktemp "${TMPDIR:-/tmp}/buzz-demo-config.XXXXXX")"
trap 'rm -f "$CONFIG_PATH"' EXIT
DEMO_BUILD_ID="$(node -e 'console.log(require("node:crypto").randomBytes(8).toString("hex"))')"
DEMO_CONFIG="$(node desktop/scripts/demo-build-config.mjs {{quote(demo_name)}} "$CONFIG_PATH" "$DEMO_BUILD_ID")"
read_config() { node -e 'console.log(JSON.parse(process.argv[1])[process.argv[2]])' "$DEMO_CONFIG" "$1"; }
PRODUCT_NAME="$(read_config productName)"
DMG_VOLUME_NAME="$(read_config dmgVolumeName)"
DMG_FILE_STEM="$(read_config dmgFileStem)"
DEMO_SLUG="$(read_config slug)"
cargo build --release --target "$TARGET" \
-p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp \
-p git-credential-nostr -p buzz-cli
./scripts/bundle-sidecars.sh "$TARGET"
pnpm install
cd {{desktop_dir}}
BUZZ_BUILD_DEMO_SLUG="$DEMO_SLUG" pnpm tauri build --features mesh-llm --target "$TARGET" --bundles app --config "$CONFIG_PATH"
cd ..
VERSION="$(node -p "require('./desktop/package.json').version")"
DMG_ARCH="${TARGET%%-*}"; [[ "$DMG_ARCH" == "x86_64" ]] && DMG_ARCH=x64
APP_PATH="desktop/src-tauri/target/$TARGET/release/bundle/macos/$PRODUCT_NAME.app"
PLIST="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $PRODUCT_NAME" "$PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleName $PRODUCT_NAME" "$PLIST"
codesign --force --deep --sign - "$APP_PATH"
VOL_NAME="$DMG_VOLUME_NAME" ./desktop/scripts/package-macos-dmg.sh "$APP_PATH" "desktop/src-tauri/target/$TARGET/release/bundle/dmg/${DMG_FILE_STEM}_${VERSION}_${DMG_ARCH}.dmg"
# Run desktop checks suitable for CI / pre-push
desktop-ci: desktop-check desktop-test desktop-tauri-fmt-check desktop-build desktop-tauri-check desktop-tauri-test

View File

@@ -93,8 +93,9 @@ impl TokenSource for StaticTokenSource {
///
/// The `discovery_url` must return a JSON document with at least
/// `authorization_endpoint` and `token_endpoint` (RFC 8414). The
/// `cache_namespace` is the directory under `~/.config/buzz-agent/oauth/`
/// the token JSON lives in — separates providers' caches cleanly.
/// `cache_namespace` is the directory under the platform config directory's
/// `buzz-agent/oauth/` root where the token JSON lives — separates providers'
/// caches cleanly.
#[derive(Debug, Clone)]
pub struct PkceOAuthConfig {
pub discovery_url: String,
@@ -102,7 +103,7 @@ pub struct PkceOAuthConfig {
pub scopes: Vec<String>,
pub cache_namespace: String,
/// When `Some`, the engine writes tokens here instead of
/// `~/.config/buzz-agent/oauth/<cache_namespace>/`. Production code
/// `<platform config dir>/buzz-agent/oauth/<cache_namespace>/`. Production code
/// leaves this `None`. Integration tests use it to avoid stomping on
/// a shared `$HOME` when running in parallel.
pub cache_dir_override: Option<PathBuf>,
@@ -444,6 +445,29 @@ fn is_expired(t: &CachedToken) -> bool {
now + TOKEN_REFRESH_LEEWAY.as_secs() >= exp
}
const BUZZ_AGENT_CONFIG_DIR_ENV: &str = "BUZZ_AGENT_CONFIG_DIR";
fn oauth_cache_root_for(
config_override: Option<PathBuf>,
home_dir: Option<PathBuf>,
) -> Result<PathBuf, AgentError> {
if let Some(root) = config_override {
return Ok(root.join("buzz-agent").join("oauth"));
}
Ok(home_dir
.ok_or_else(|| AgentError::Llm("oauth cache: home directory not found".into()))?
.join(".config")
.join("buzz-agent")
.join("oauth"))
}
fn default_oauth_cache_root() -> Result<PathBuf, AgentError> {
oauth_cache_root_for(
std::env::var_os(BUZZ_AGENT_CONFIG_DIR_ENV).map(PathBuf::from),
dirs::home_dir(),
)
}
fn cache_path_for(cfg: &PkceOAuthConfig) -> Result<PathBuf, AgentError> {
let mut h = sha2::Sha256::new();
h.update(cfg.discovery_url.as_bytes());
@@ -455,12 +479,7 @@ fn cache_path_for(cfg: &PkceOAuthConfig) -> Result<PathBuf, AgentError> {
let dir = match &cfg.cache_dir_override {
Some(p) => p.join(&cfg.cache_namespace),
None => dirs::home_dir()
.ok_or_else(|| AgentError::Llm("oauth cache: home directory not found".into()))?
.join(".config")
.join("buzz-agent")
.join("oauth")
.join(&cfg.cache_namespace),
None => default_oauth_cache_root()?.join(&cfg.cache_namespace),
};
Ok(dir.join(format!("{hash}.json")))
}
@@ -862,7 +881,41 @@ mod tests {
}
#[test]
fn cache_path_uses_platform_home_directory() {
fn production_and_demo_oauth_roots_are_concrete_and_distinct() {
let home = PathBuf::from("/Users/demo");
let production = oauth_cache_root_for(None, Some(home.clone())).unwrap();
let first_demo_config = home
.join("Library/Application Support")
.join("buzz-demo-board-1234567812345678");
let second_demo_config = home
.join("Library/Application Support")
.join("buzz-demo-board-8765432187654321");
let first_demo = oauth_cache_root_for(Some(first_demo_config), Some(home.clone())).unwrap();
let second_demo = oauth_cache_root_for(Some(second_demo_config), Some(home)).unwrap();
assert_eq!(
production,
PathBuf::from("/Users/demo/.config/buzz-agent/oauth")
);
assert_eq!(
first_demo,
PathBuf::from(
"/Users/demo/Library/Application Support/buzz-demo-board-1234567812345678/buzz-agent/oauth"
)
);
assert_eq!(
second_demo,
PathBuf::from(
"/Users/demo/Library/Application Support/buzz-demo-board-8765432187654321/buzz-agent/oauth"
)
);
assert_ne!(production, first_demo);
assert_ne!(production, second_demo);
assert_ne!(first_demo, second_demo);
}
#[test]
fn cache_path_preserves_production_home_config_directory() {
let cfg = PkceOAuthConfig {
discovery_url: "https://example.com/.well-known".into(),
client_id: "abc".into(),

View File

@@ -12,7 +12,7 @@
//! This helper never opens a browser. Callers choose whether to reject, degrade,
//! or start a separate interactive authentication flow.
use std::{collections::HashSet, sync::Arc, time::Duration};
use std::{collections::HashSet, path::Path, sync::Arc, time::Duration};
use reqwest::Client;
use serde_json::Value;
@@ -133,7 +133,26 @@ pub(crate) fn is_chat_capable_endpoint(name: &str) -> bool {
/// # Panics
/// Never panics.
pub async fn discover_databricks_models(cfg: &Config) -> Result<Vec<ModelEntry>, AgentError> {
discover_databricks_models_with_token_source(cfg, build_token_source(cfg)?).await
discover_databricks_models_with_cache_dir(cfg, None).await
}
/// Discover Databricks models while storing PKCE credentials under an explicit
/// cache root. `None` preserves buzz-agent's production cache location.
pub async fn discover_databricks_models_with_cache_dir(
cfg: &Config,
cache_dir: Option<&Path>,
) -> Result<Vec<ModelEntry>, AgentError> {
let token_source = if matches!(cfg.provider, Provider::Databricks | Provider::DatabricksV2)
&& cfg.api_key.is_empty()
{
crate::auth::PkceOAuthTokenSource::new(crate::llm::databricks_pkce_config(
&cfg.base_url,
cache_dir.map(Path::to_path_buf),
))?
} else {
build_token_source(cfg)?
};
discover_databricks_models_with_token_source(cfg, token_source).await
}
async fn discover_databricks_models_with_token_source(

View File

@@ -13,7 +13,9 @@ mod permission;
pub mod types;
mod wire;
pub use catalog::{discover_databricks_models, ModelEntry};
pub use catalog::{
discover_databricks_models, discover_databricks_models_with_cache_dir, ModelEntry,
};
pub use config::Provider;
pub use types::AgentError;
@@ -161,10 +163,22 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
/// Authenticate to Databricks and store credentials under an optional explicit
/// cache root. `None` preserves buzz-agent's production cache location.
pub async fn authenticate_databricks_with_cache_dir(
host: &str,
cache_dir: Option<&std::path::Path>,
) -> Result<(), AgentError> {
auth::PkceOAuthTokenSource::new(llm::databricks_pkce_config(
host,
cache_dir.map(std::path::Path::to_path_buf),
))?
.interactive_login()
.await
}
pub async fn authenticate_databricks(host: &str) -> Result<(), AgentError> {
auth::PkceOAuthTokenSource::new(llm::databricks_pkce_config(host))?
.interactive_login()
.await
authenticate_databricks_with_cache_dir(host, None).await
}
/// `buzz-agent auth <provider>` — run the interactive auth flow for a

View File

@@ -1,4 +1,5 @@
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
@@ -2033,7 +2034,10 @@ where
)))
}
pub(crate) fn databricks_pkce_config(host: &str) -> PkceOAuthConfig {
pub(crate) fn databricks_pkce_config(
host: &str,
cache_dir_override: Option<PathBuf>,
) -> PkceOAuthConfig {
PkceOAuthConfig {
discovery_url: format!(
"{}/oidc/.well-known/oauth-authorization-server",
@@ -2045,7 +2049,7 @@ pub(crate) fn databricks_pkce_config(host: &str) -> PkceOAuthConfig {
.map(|scope| (*scope).into())
.collect(),
cache_namespace: "databricks".into(),
cache_dir_override: None,
cache_dir_override,
}
}
@@ -2070,6 +2074,7 @@ pub(crate) fn build_token_source(cfg: &Config) -> Result<Arc<dyn TokenSource>, A
}
Ok(PkceOAuthTokenSource::new(databricks_pkce_config(
&cfg.base_url,
None,
))?)
}
}

View File

@@ -45,6 +45,9 @@ const PASSTHROUGH_ENV: &[&str] = &[
"LC_ALL",
"TMPDIR",
"XDG_CONFIG_HOME",
// Explicit Buzz-owned OAuth root for named demo builds. The agent may spawn
// auth-capable child tools after clearing its ambient environment.
"BUZZ_AGENT_CONFIG_DIR",
// SSH — required for git clone/push over SSH (git@github.com:...)
"SSH_AUTH_SOCK",
"SSH_AGENT_PID",

View File

@@ -14,7 +14,7 @@
"lint": "biome lint .",
"check": "biome check . && pnpm check:px-text && pnpm check:pubkey-truncation",
"format": "biome format --write .",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test \"src/**/*.test.mjs\"",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test \"src/**/*.test.mjs\" \"scripts/*.test.mjs\"",
"preview": "vite preview",
"tauri": "node ./scripts/tauri-command.mjs",
"test:e2e": "pnpm build:e2e && playwright test",

View File

@@ -0,0 +1,94 @@
import { randomBytes } from "node:crypto";
import { writeFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
const PRODUCTION_IDENTIFIER = "xyz.block.buzz.app";
// The build ID suffix is 17 characters including its separator, and the Rust
// build contract caps the complete demo slug at 48 ASCII bytes.
const MAX_DEMO_SLUG_LENGTH = 48;
const DEMO_BUILD_ID_SUFFIX_LENGTH = 17;
const MAX_DEMO_NAME_LENGTH = MAX_DEMO_SLUG_LENGTH - DEMO_BUILD_ID_SUFFIX_LENGTH;
export const productionBuildIdentity = Object.freeze({
productName: "Buzz",
identifier: PRODUCTION_IDENTIFIER,
deepLinkScheme: "buzz",
keyringService: "buzz-desktop",
nestName: ".buzz",
cliName: "buzz",
});
export function demoBuildConfig(
rawName,
buildId = randomBytes(8).toString("hex"),
) {
if (typeof rawName !== "string") throw new Error("Demo name must be text");
const name = rawName.trim().replace(/\s+/g, " ");
if (!name) throw new Error("Demo name must not be empty");
if (name.length > MAX_DEMO_NAME_LENGTH) {
throw new Error(
`Demo name must be at most ${MAX_DEMO_NAME_LENGTH} characters`,
);
}
if (!/^[A-Za-z0-9][A-Za-z0-9 -]*$/.test(name)) {
throw new Error(
"Demo name may contain ASCII letters, numbers, spaces, and hyphens only",
);
}
if (!/^[a-f0-9]{16}$/.test(buildId)) {
throw new Error(
"Demo build ID must be sixteen lowercase hexadecimal characters",
);
}
const readableSlug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const slug = `${readableSlug}-${buildId}`;
const productName = `Buzz ${name}`;
return {
name,
slug,
productName,
dmgVolumeName: productName,
dmgFileStem: productName.replace(/ /g, "_"),
identifier: `${PRODUCTION_IDENTIFIER}.demo.${slug}`,
appDataIdentity: `${PRODUCTION_IDENTIFIER}.demo.${slug}`,
deepLinkScheme: `buzz-demo-${slug}`,
keyringService: `buzz-desktop-demo.${slug}`,
nestName: `.buzz-demo-${slug}`,
cliName: `buzz-demo-${slug}`,
tauriConfig: {
productName,
identifier: `${PRODUCTION_IDENTIFIER}.demo.${slug}`,
plugins: { "deep-link": { desktop: { schemes: [`buzz-demo-${slug}`] } } },
bundle: { targets: ["app"] },
},
};
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
const [name, outputPath, buildId] = process.argv.slice(2);
if (!outputPath) {
console.error(
"Usage: demo-build-config.mjs <demo-name> <output-config-path>",
);
process.exit(2);
}
try {
const config = demoBuildConfig(name, buildId);
writeFileSync(
outputPath,
`${JSON.stringify(config.tauriConfig, null, 2)}\n`,
);
console.log(JSON.stringify(config));
} catch (error) {
console.error(`Invalid demo build: ${error.message}`);
process.exit(1);
}
}

View File

@@ -0,0 +1,134 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
demoBuildConfig,
productionBuildIdentity,
} from "./demo-build-config.mjs";
const expected = (name, slug) => ({
name,
slug,
productName: `Buzz ${name}`,
dmgVolumeName: `Buzz ${name}`,
dmgFileStem: `Buzz_${name.replace(/ /g, "_")}`,
identifier: `xyz.block.buzz.app.demo.${slug}`,
appDataIdentity: `xyz.block.buzz.app.demo.${slug}`,
deepLinkScheme: `buzz-demo-${slug}`,
keyringService: `buzz-desktop-demo.${slug}`,
nestName: `.buzz-demo-${slug}`,
cliName: `buzz-demo-${slug}`,
tauriConfig: {
productName: `Buzz ${name}`,
identifier: `xyz.block.buzz.app.demo.${slug}`,
plugins: { "deep-link": { desktop: { schemes: [`buzz-demo-${slug}`] } } },
bundle: { targets: ["app"] },
},
});
test("production identity remains unchanged", () => {
assert.deepEqual(productionBuildIdentity, {
productName: "Buzz",
identifier: "xyz.block.buzz.app",
deepLinkScheme: "buzz",
keyringService: "buzz-desktop",
nestName: ".buzz",
cliName: "buzz",
});
});
test("two demo names produce complete, distinct identities", () => {
const board = demoBuildConfig("Workstream Board", "27a4294c27a4294c");
const interests = demoBuildConfig("Interests Demo", "deb5339adeb5339a");
assert.deepEqual(
board,
expected("Workstream Board", "workstream-board-27a4294c27a4294c"),
);
assert.deepEqual(
interests,
expected("Interests Demo", "interests-demo-deb5339adeb5339a"),
);
for (const key of [
"productName",
"dmgVolumeName",
"dmgFileStem",
"identifier",
"appDataIdentity",
"deepLinkScheme",
"keyringService",
"nestName",
"cliName",
]) {
assert.notEqual(board[key], interests[key], key);
assert.notEqual(board[key], productionBuildIdentity[key], key);
}
});
test("normalized spelling aliases retain distinct runtime identities", () => {
for (const [leftName, rightName] of [
["A B", "A-B"],
["Demo", "demo"],
["Workstream Board", "WORKSTREAM BOARD"],
]) {
const left = demoBuildConfig(leftName, "1111111111111111");
const right = demoBuildConfig(rightName, "2222222222222222");
assert.notEqual(left.slug, right.slug);
for (const key of [
"identifier",
"appDataIdentity",
"deepLinkScheme",
"keyringService",
"nestName",
"cliName",
]) {
assert.notEqual(
left[key],
right[key],
`${leftName}/${rightName}: ${key}`,
);
}
}
});
test("the same display name gets a distinct identity for each build", () => {
const first = demoBuildConfig("Demo", "1111111111111111");
const second = demoBuildConfig("Demo", "2222222222222222");
assert.equal(first.productName, second.productName);
assert.equal(first.dmgFileStem, second.dmgFileStem);
for (const key of [
"slug",
"identifier",
"appDataIdentity",
"deepLinkScheme",
"keyringService",
"nestName",
"cliName",
]) {
assert.notEqual(first[key], second[key], key);
}
});
test("whitespace normalization preserves deterministic identity", () => {
assert.deepEqual(
demoBuildConfig(" Workstream Board ", "27a4294c27a4294c"),
demoBuildConfig("Workstream Board", "27a4294c27a4294c"),
);
});
test("maximum-length name produces a Rust-valid 48-byte slug", () => {
const config = demoBuildConfig("x".repeat(31), "1234567812345678");
assert.equal(config.slug.length, 48);
assert.match(config.slug, /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/);
});
for (const name of [
"",
" ",
"Workstream/Board",
"Workstream_Board",
"équipe",
"x".repeat(32),
]) {
test(`rejects unusable name ${JSON.stringify(name)}`, () =>
assert.throws(() => demoBuildConfig(name, "1234567812345678")));
}

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# Build a drag-to-Applications DMG without requiring a GUI login session.
# Finder styling is optional; the disk image itself is always authoritative.
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <Buzz.app> <output.dmg>" >&2
exit 2
fi
app_path="$1"
out_dmg="$2"
app_name="$(basename "$app_path")"
volume_name="${VOL_NAME:-Buzz}"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
background="$script_dir/../src-tauri/icons/dmg-background.png"
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/buzz-dmg.XXXXXX")"
source_dir="$work_dir/source"
rw_dmg="$work_dir/read-write.dmg"
mount_point="$work_dir/mount"
applescript="$work_dir/style.applescript"
device=""
finish() {
local status="$?"
trap - EXIT
if [[ -n "$device" ]]; then
hdiutil detach "$device" >/dev/null 2>&1 || true
hdiutil detach -force "$device" >/dev/null 2>&1 || true
fi
rm -rf "$work_dir"
exit "$status"
}
trap finish EXIT
[[ -d "$app_path" ]] || { echo "App bundle not found: $app_path" >&2; exit 1; }
[[ -f "$background" ]] || { echo "DMG background not found: $background" >&2; exit 1; }
mkdir -p "$(dirname "$out_dmg")" "$source_dir/.background" "$mount_point"
ditto "$app_path" "$source_dir/$app_name"
ln -s /Applications "$source_dir/Applications"
cp "$background" "$source_dir/.background/background.png"
rm -f "$rw_dmg" "$out_dmg"
hdiutil create -volname "$volume_name" -srcfolder "$source_dir" \
-format UDRW -ov "$rw_dmg" >/dev/null
attach_output="$(hdiutil attach -readwrite -noverify -noautoopen -nobrowse \
-mountpoint "$mount_point" "$rw_dmg")"
device="$(printf '%s\n' "$attach_output" | awk '/^\/dev\// { print $1; exit }')"
[[ -n "$device" ]] || { echo "Failed to attach writable DMG" >&2; exit 1; }
detach() {
local attempt
for attempt in 1 2 3 4 5; do
if hdiutil detach "$device" >/dev/null 2>&1; then
device=""
return 0
fi
sleep 1
done
hdiutil detach -force "$device" >/dev/null
device=""
}
if command -v SetFile >/dev/null 2>&1; then
SetFile -a V "$mount_point/.background" || true
icon="$mount_point/$app_name/Contents/Resources/icon.icns"
if [[ -f "$icon" ]]; then
cp "$icon" "$mount_point/.VolumeIcon.icns" || true
SetFile -c icnC "$mount_point/.VolumeIcon.icns" || true
SetFile -a C "$mount_point" || true
fi
fi
cat >"$applescript" <<'APPLESCRIPT'
on run argv
set mountPath to item 1 of argv
set appName to item 2 of argv
tell application "Finder"
set rootFolder to POSIX file mountPath as alias
open rootFolder
set imageWindow to container window of rootFolder
set current view of imageWindow to icon view
set toolbar visible of imageWindow to false
set statusbar visible of imageWindow to false
set bounds of imageWindow to {200, 120, 860, 652}
set viewOptions to icon view options of imageWindow
set arrangement of viewOptions to not arranged
set icon size of viewOptions to 128
set text size of viewOptions to 14
set background picture of viewOptions to file ".background:background.png" of rootFolder
set position of item appName of rootFolder to {191, 330}
set position of item "Applications" of rootFolder to {469, 330}
set extension hidden of item appName of rootFolder to true
delay 1
close imageWindow
end tell
end run
APPLESCRIPT
style_with_finder() {
local child elapsed=0
/usr/bin/osascript "$applescript" "$mount_point" "$app_name" &
child=$!
while kill -0 "$child" 2>/dev/null; do
if (( elapsed >= 100 )); then
echo "Finder styling timed out; continuing without it" >&2
kill "$child" 2>/dev/null || true
wait "$child" 2>/dev/null || true
return 124
fi
sleep 0.1
elapsed=$((elapsed + 1))
done
wait "$child"
}
if ! style_with_finder; then
echo "Finder styling unavailable; continuing without it" >&2
fi
sync
detach
hdiutil convert "$rw_dmg" -format UDZO -imagekey zlib-level=9 \
-o "$out_dmg" >/dev/null
printf 'DMG ready: %s\n' "$out_dmg"

View File

@@ -18,8 +18,29 @@ fn main() {
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_DEMO_SLUG");
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");
if let Ok(slug) = std::env::var("BUZZ_BUILD_DEMO_SLUG") {
let valid = !slug.is_empty()
&& slug.len() <= 48
&& slug
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
&& slug
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
&& slug
.bytes()
.last()
.is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit());
if !valid {
panic!("BUZZ_BUILD_DEMO_SLUG must be a lowercase ASCII slug");
}
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_DEMO_SLUG={slug}");
}
// Explicit owner-only agent-access capability. Release packaging sets this
// presence-only marker; OSS/custom builds leave agent access configurable.
if std::env::var("BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY").is_ok() {

View File

@@ -7,7 +7,12 @@ fn dev_keyring_service(configured: Option<String>) -> String {
}
pub(crate) fn keyring_service() -> &'static str {
if cfg!(debug_assertions) {
if crate::build_identity::is_demo_build() {
static DEMO_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
DEMO_SERVICE
.get_or_init(|| crate::build_identity::keyring_service().into_owned())
.as_str()
} else if cfg!(debug_assertions) {
static DEV_SERVICE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
DEV_SERVICE
.get_or_init(|| dev_keyring_service(std::env::var("BUZZ_DEV_KEYRING_SERVICE").ok()))

View File

@@ -0,0 +1,183 @@
//! Compile-time identity for reusable named demo builds.
//!
//! Production builds leave `BUZZ_DESKTOP_BUILD_DEMO_SLUG` unset and retain all
//! existing names. The demo recipe validates one slug and `build.rs` bakes it
//! into the binary; every runtime identity is then derived from that one value.
use std::borrow::Cow;
pub(crate) fn demo_slug() -> Option<&'static str> {
option_env!("BUZZ_DESKTOP_BUILD_DEMO_SLUG")
}
pub(crate) fn is_demo_build() -> bool {
demo_slug().is_some()
}
pub(crate) const DEMO_AGENT_CONFIG_ENV: &str = "BUZZ_AGENT_CONFIG_DIR";
pub(crate) fn demo_config_home() -> Result<Option<std::path::PathBuf>, String> {
demo_config_home_for(demo_slug(), dirs::config_dir())
}
pub(crate) fn demo_agent_oauth_cache_dir() -> Result<Option<std::path::PathBuf>, String> {
Ok(demo_config_home()?.map(|dir| dir.join("buzz-agent").join("oauth")))
}
/// Keep child config caches inside this demo build's identity. In particular,
/// bundled buzz-agent OAuth tokens must not read or write production's root.
/// Refuse launch if a demo cannot resolve its root; None means production only.
pub(crate) fn apply_demo_config_home(command: &mut std::process::Command) -> Result<(), String> {
if let Some(config_home) = demo_config_home()? {
command.env(DEMO_AGENT_CONFIG_ENV, config_home);
}
Ok(())
}
fn demo_config_home_for(
demo_slug: Option<&str>,
config_dir: Option<std::path::PathBuf>,
) -> Result<Option<std::path::PathBuf>, String> {
match demo_slug {
None => Ok(None),
Some(slug) => config_dir
.map(|dir| Some(dir.join(format!("buzz-demo-{slug}"))))
.ok_or_else(|| "cannot resolve demo credential directory".to_string()),
}
}
pub(crate) fn deep_link_scheme() -> Cow<'static, str> {
demo_slug()
.map(|slug| Cow::Owned(format!("buzz-demo-{slug}")))
.unwrap_or(Cow::Borrowed("buzz"))
}
pub(crate) fn is_deep_link_for_build(value: &str) -> bool {
is_deep_link_for_scheme(value, deep_link_scheme().as_ref())
}
fn is_deep_link_for_scheme(value: &str, scheme: &str) -> bool {
value
.strip_prefix(scheme)
.is_some_and(|suffix| suffix.starts_with("://"))
}
pub(crate) fn keyring_service() -> Cow<'static, str> {
demo_slug()
.map(|slug| Cow::Owned(format!("buzz-desktop-demo.{slug}")))
.unwrap_or(Cow::Borrowed("buzz-desktop"))
}
pub(crate) fn nest_name(is_dev: bool) -> Cow<'static, str> {
nest_name_for(demo_slug(), is_dev)
}
fn nest_name_for(demo_slug: Option<&str>, is_dev: bool) -> Cow<'_, str> {
if let Some(slug) = demo_slug {
Cow::Owned(format!(".buzz-demo-{slug}"))
} else if is_dev {
Cow::Borrowed(".buzz-dev")
} else {
Cow::Borrowed(".buzz")
}
}
pub(crate) fn cli_name(is_dev: bool) -> String {
if let Some(slug) = demo_slug() {
format!("buzz-demo-{slug}")
} else if is_dev {
"buzz-dev".to_string()
} else {
"buzz".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "compiled with BUZZ_BUILD_DEMO_SLUG by the compiled-flags recipe"]
fn compiled_demo_slug_matches_expected() {
let expected = std::env::var("BUZZ_TEST_EXPECTED_DEMO_SLUG")
.expect("BUZZ_TEST_EXPECTED_DEMO_SLUG must be set");
assert_eq!(demo_slug(), Some(expected.as_str()));
}
#[test]
fn ordinary_release_defaults_remain_production_identity() {
if demo_slug().is_none() {
assert_eq!(deep_link_scheme(), "buzz");
assert_eq!(keyring_service(), "buzz-desktop");
assert_eq!(nest_name(false), ".buzz");
assert_eq!(cli_name(false), "buzz");
}
}
#[test]
fn demo_agent_config_and_oauth_roots_are_build_scoped() {
let base = std::path::PathBuf::from("/Users/demo/Library/Application Support");
assert_eq!(
demo_config_home_for(None, Some(base.clone())).unwrap(),
None
);
let first = demo_config_home_for(Some("board-1234567812345678"), Some(base.clone()))
.unwrap()
.unwrap();
let second = demo_config_home_for(Some("board-8765432187654321"), Some(base))
.unwrap()
.unwrap();
assert_eq!(
first,
std::path::PathBuf::from(
"/Users/demo/Library/Application Support/buzz-demo-board-1234567812345678"
)
);
assert_eq!(
first.join("buzz-agent/oauth"),
std::path::PathBuf::from(
"/Users/demo/Library/Application Support/buzz-demo-board-1234567812345678/buzz-agent/oauth"
)
);
assert_ne!(first, second);
}
#[test]
fn unresolved_demo_credentials_never_select_production_defaults() {
assert_eq!(demo_config_home_for(None, None).unwrap(), None);
assert_eq!(
demo_config_home_for(Some("board-1234567812345678"), None),
Err("cannot resolve demo credential directory".to_string())
);
}
#[test]
fn duplicate_instance_links_follow_the_build_scheme() {
assert!(is_deep_link_for_scheme("buzz://message?id=1", "buzz"));
assert!(!is_deep_link_for_scheme(
"buzz-demo-board-1234567812345678://message?id=1",
"buzz"
));
assert!(is_deep_link_for_scheme(
"buzz-demo-board-1234567812345678://message?id=1",
"buzz-demo-board-1234567812345678"
));
assert!(!is_deep_link_for_scheme(
"buzz://message?id=1",
"buzz-demo-board-1234567812345678"
));
}
#[test]
fn production_and_named_demo_nests_are_distinct() {
assert_eq!(nest_name_for(None, false), ".buzz");
assert_eq!(
nest_name_for(Some("workstream-board"), false),
".buzz-demo-workstream-board"
);
assert_eq!(
nest_name_for(Some("second-demo"), false),
".buzz-demo-second-demo"
);
}
}

View File

@@ -54,6 +54,8 @@ pub(super) async fn run_agent_models_command(
for (k, v) in &merged_env {
cmd.env(k, v);
}
// Demo identity is authoritative and must win over ambient/user env.
crate::build_identity::apply_demo_config_home(&mut cmd)?;
crate::managed_agents::configure_runtime_cli(&mut cmd, known_acp_runtime(&agent_command));
crate::util::configure_no_window(&mut cmd);
cmd.stdout(std::process::Stdio::piped())

View File

@@ -178,12 +178,23 @@ pub(super) async fn discover_databricks_models(
parsed_filter.clone(),
);
let redaction_env = redaction_env_with_value(env, "DATABRICKS_TOKEN", &api_key);
let oauth_cache_dir = crate::build_identity::demo_agent_oauth_cache_dir()?;
let entries = match buzz_agent_pkg::discover_databricks_models(&config).await {
let entries = match buzz_agent_pkg::discover_databricks_models_with_cache_dir(
&config,
oauth_cache_dir.as_deref(),
)
.await
{
Ok(entries) => entries,
Err(buzz_agent_pkg::AgentError::LlmAuth(_)) if should_start_interactive_auth(&api_key) => {
let _auth = AUTH_GATE.lock().await;
match buzz_agent_pkg::discover_databricks_models(&config).await {
match buzz_agent_pkg::discover_databricks_models_with_cache_dir(
&config,
oauth_cache_dir.as_deref(),
)
.await
{
// A peer sign-in under the gate already succeeded.
Ok(entries) => entries,
Err(buzz_agent_pkg::AgentError::LlmAuth(_)) => {
@@ -194,22 +205,28 @@ pub(super) async fn discover_databricks_models(
return Err(databricks_sign_in_required_error());
}
run_interactive_databricks_auth(
buzz_agent_pkg::authenticate_databricks(&host),
buzz_agent_pkg::authenticate_databricks_with_cache_dir(
&host,
oauth_cache_dir.as_deref(),
),
AUTH_FLOW_TIMEOUT,
&AUTH_COOLDOWNS,
&host,
&redaction_env,
)
.await?;
buzz_agent_pkg::discover_databricks_models(&config)
.await
.map_err(|error| {
format_redacted_error(
"Databricks model discovery failed after sign-in",
&error,
&redaction_env,
)
})?
buzz_agent_pkg::discover_databricks_models_with_cache_dir(
&config,
oauth_cache_dir.as_deref(),
)
.await
.map_err(|error| {
format_redacted_error(
"Databricks model discovery failed after sign-in",
&error,
&redaction_env,
)
})?
}
Err(error) => {
return Err(format_redacted_error(

View File

@@ -145,13 +145,26 @@ pub(crate) fn find_local_repo_dir(
}
pub(crate) fn default_repos_root_candidates() -> Vec<std::path::PathBuf> {
default_repos_root_candidates_for(
nest_dir(),
dirs::home_dir(),
crate::build_identity::is_demo_build(),
)
}
fn default_repos_root_candidates_for(
nest: Option<std::path::PathBuf>,
home: Option<std::path::PathBuf>,
is_demo_build: bool,
) -> Vec<std::path::PathBuf> {
let mut candidates = Vec::new();
candidates.extend(nest_dir().map(|path| path.join("REPOS")));
candidates.extend(
dirs::home_dir()
.map(|home| home.join(".buzz").join("REPOS"))
.filter(|path| !candidates.iter().any(|candidate| candidate == path)),
);
candidates.extend(nest.map(|path| path.join("REPOS")));
if !is_demo_build {
candidates.extend(
home.map(|home| home.join(".buzz").join("REPOS"))
.filter(|path| !candidates.iter().any(|candidate| candidate == path)),
);
}
candidates
}
@@ -190,3 +203,34 @@ pub(crate) fn canonical_repos_roots(
}
Ok(roots)
}
#[cfg(test)]
mod tests {
use super::default_repos_root_candidates_for;
use std::path::PathBuf;
#[test]
fn production_keeps_the_legacy_repo_fallback() {
let home = PathBuf::from("/Users/example");
assert_eq!(
default_repos_root_candidates_for(
Some(home.join(".buzz-dev")),
Some(home.clone()),
false,
),
vec![home.join(".buzz-dev/REPOS"), home.join(".buzz/REPOS")]
);
}
#[test]
fn named_demos_only_search_their_selected_nest() {
let home = PathBuf::from("/Users/example");
for slug in ["workstream-board", "second-demo"] {
let nest = home.join(format!(".buzz-demo-{slug}"));
assert_eq!(
default_repos_root_candidates_for(Some(nest.clone()), Some(home.clone()), true,),
vec![nest.join("REPOS")]
);
}
}
}

View File

@@ -404,6 +404,18 @@ const ENTITY_LINK_TABS: [&str; 6] = [
"channels",
];
/// Validate the build-specific transport URL, then hand the frontend its
/// canonical entity-link representation. Never broaden frontend scheme trust.
fn canonical_entity_deep_link(url: &Url, build_scheme: &str) -> Option<String> {
if url.scheme() != build_scheme {
return None;
}
parse_entity_deep_link(url)?;
let mut canonical = url.clone();
canonical.set_scheme("buzz").ok()?;
Some(canonical.into())
}
/// The canonical-form rules match `parseEntityLink`: no path segments, no
/// fragment, and no parameters beyond `owner`/`d` (plus `id` for event
/// links and the optional `tab` for coordinate links), so a future
@@ -600,7 +612,7 @@ pub(crate) fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
}
};
if url.scheme() != "buzz" {
if url.scheme() != crate::build_identity::deep_link_scheme() {
eprintln!("buzz-desktop: ignoring unsupported deep link scheme: {url_str}");
return;
}
@@ -678,17 +690,17 @@ pub(crate) fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
let _ = app.emit("deep-link-message", payload);
}
Some("repo" | "project" | "pr" | "issue") => {
// `buzz://repo|project?owner=<pubkey>&d=<dtag>` and
// `buzz://pr|issue?id=<eventId>&owner=<pubkey>&d=<dtag>` — the
// share links copied from the Projects UI. The frontend owns
// routing (`useEntityDeepLinks`), so the validated URL is
// forwarded unchanged.
if parse_entity_deep_link(&url).is_none() {
// OS routing uses this build's scheme; frontend navigation consumes
// canonical buzz:// entity links rather than transport identity.
let Some(href) = canonical_entity_deep_link(
&url,
crate::build_identity::deep_link_scheme().as_ref(),
) else {
eprintln!("buzz-desktop: malformed entity deep link: {url_str}");
return;
}
};
activate_main_window(app);
let pending = queue_entity_deep_link(app, url_str.to_owned());
let pending = queue_entity_deep_link(app, href);
let _ = app.emit("deep-link-entity", pending);
}
Some("nostr-bind") => match parse_nostr_bind_deep_link(&url) {

View File

@@ -1,10 +1,11 @@
use url::Url;
use super::{
parse_add_community_deep_link, parse_channel_deep_link, parse_entity_deep_link,
parse_join_deep_link, parse_message_deep_link, parse_nostr_bind_deep_link,
PendingCommunityDeepLink, PendingCommunityDeepLinks, PendingEntityDeepLinks,
PendingNavigationDeepLink, PendingNavigationDeepLinks, ENTITY_LINK_TABS,
canonical_entity_deep_link, parse_add_community_deep_link, parse_channel_deep_link,
parse_entity_deep_link, parse_join_deep_link, parse_message_deep_link,
parse_nostr_bind_deep_link, PendingCommunityDeepLink, PendingCommunityDeepLinks,
PendingEntityDeepLinks, PendingNavigationDeepLink, PendingNavigationDeepLinks,
ENTITY_LINK_TABS,
};
fn entity_link_golden() -> serde_json::Value {
@@ -12,6 +13,35 @@ fn entity_link_golden() -> serde_json::Value {
.expect("valid entity-links golden fixture")
}
#[test]
fn demo_entity_transport_produces_the_frontend_golden_contract() {
let golden = entity_link_golden();
let scheme = "buzz-demo-board-1234567812345678";
for canonical in golden["links"].as_object().unwrap().values() {
let canonical = canonical.as_str().unwrap();
let transport = Url::parse(&canonical.replacen("buzz:", &format!("{scheme}:"), 1)).unwrap();
let href = canonical_entity_deep_link(&transport, scheme).unwrap();
// This same fixture is parsed and routed by the frontend entity tests.
assert_eq!(href, canonical);
let queue = PendingEntityDeepLinks::default();
let pending = queue.enqueue(href);
assert_eq!(queue.first().unwrap().href, canonical);
assert!(queue.acknowledge(&pending.id));
assert!(queue.first().is_none());
assert!(canonical_entity_deep_link(&transport, "buzz").is_none());
assert!(
canonical_entity_deep_link(&transport, "buzz-demo-other-8765432187654321").is_none()
);
assert!(canonical_entity_deep_link(&Url::parse(canonical).unwrap(), scheme).is_none());
assert_eq!(
canonical_entity_deep_link(&Url::parse(canonical).unwrap(), "buzz").as_deref(),
Some(canonical)
);
}
let invalid = Url::parse(&format!("{scheme}://repo?owner=bad&d=repo")).unwrap();
assert!(canonical_entity_deep_link(&invalid, scheme).is_none());
}
#[test]
fn parse_entity_deep_link_accepts_every_share_link_shape() {
let golden = entity_link_golden();

View File

@@ -587,6 +587,10 @@ fn tts_model_slot() -> ModelSlot {
.with_expected_sizes(tts_expected_size)
}
fn models_dir(nest_dir: PathBuf) -> PathBuf {
nest_dir.join("models")
}
// ── ModelManager ──────────────────────────────────────────────────────────────
/// Manages download and location of STT/TTS model files.
@@ -594,18 +598,18 @@ fn tts_model_slot() -> ModelSlot {
/// Cheap to clone — all inner state is behind `Arc`.
#[derive(Clone)]
pub struct ModelManager {
/// `~/.buzz/models/`
/// Model storage under the selected build's nest.
models_dir: PathBuf,
stt: ModelSlot,
tts: ModelSlot,
}
impl ModelManager {
/// Create a new `ModelManager` rooted at `~/.buzz/models/`.
/// Create a new `ModelManager` rooted in the selected build's nest.
///
/// Returns `None` if the home directory cannot be resolved.
/// Returns `None` if the nest directory cannot be resolved.
pub fn new() -> Option<Self> {
let models_dir = dirs::home_dir()?.join(".buzz").join("models");
let models_dir = models_dir(crate::managed_agents::nest_dir()?);
let manager = Self {
models_dir,
stt: ModelSlot::new(STT_MODEL_DIR_NAME, STT_EXPECTED_FILES, STT_MODEL_VERSION),

View File

@@ -1,5 +1,18 @@
use super::*;
#[test]
fn voice_models_follow_the_selected_build_nest() {
let home = PathBuf::from("/Users/example");
for nest_name in [
".buzz",
".buzz-demo-workstream-board",
".buzz-demo-second-demo",
] {
let nest = home.join(nest_name);
assert_eq!(models_dir(nest.clone()), nest.join("models"));
}
}
fn create_ready_model_dir(root: &Path) -> PathBuf {
let model_dir = root.join(TTS_MODEL_DIR_NAME);
std::fs::create_dir_all(&model_dir).expect("create model dir");

View File

@@ -2,6 +2,7 @@
mod app_menu;
mod app_state;
mod archive;
mod build_identity;
mod builderlab;
mod channel_head_cache;
mod commands;
@@ -127,7 +128,7 @@ pub fn run() {
}
// Forward any deep link URLs from the duplicate launch.
for arg in &argv {
if arg.starts_with("buzz://") {
if crate::build_identity::is_deep_link_for_build(arg) {
handle_deep_link_url(app, arg);
}
}
@@ -397,7 +398,10 @@ pub fn run() {
// the now-inert ~/.sprout; the frontend dedupes the toast.
// Suppressed when a reset completed this boot: the nest was wiped and
// a fresh ~/.sprout-less state is exactly what we want.
if !reset_outcome.completed && migration::migrate_legacy_nest() {
if !crate::build_identity::is_demo_build()
&& !reset_outcome.completed
&& migration::migrate_legacy_nest()
{
let _ = app_handle.emit("legacy-nest-migrated", ());
}

View File

@@ -63,12 +63,6 @@ const CANONICAL_SKILL_DIR: &str = ".agents/skills/buzz-cli";
/// Nest directory name for production builds.
const NEST_DIR_PROD: &str = ".buzz";
/// Nest directory name for dev builds. Dev builds (those whose Tauri app-data
/// directory name starts with `"xyz.block.buzz.app.dev"`) use a separate nest
/// so that the DMG and dev-build instances don't clobber each other's
/// `.repos-dir` dotfile and `REPOS` symlink.
const NEST_DIR_DEV: &str = ".buzz-dev";
/// Process-lifetime nest directory. Initialized once at startup via
/// [`init_nest_dir`] before any call to [`nest_dir`].
///
@@ -88,8 +82,8 @@ static NEST_DIR: std::sync::OnceLock<Option<PathBuf>> = std::sync::OnceLock::new
/// when the Tauri app-data directory name starts with `"xyz.block.buzz.app.dev"`.
/// Pass `false` for production (signed DMG) builds.
pub fn init_nest_dir(is_dev: bool) {
let suffix = if is_dev { NEST_DIR_DEV } else { NEST_DIR_PROD };
let path = dirs::home_dir().map(|h| h.join(suffix));
let suffix = crate::build_identity::nest_name(is_dev);
let path = dirs::home_dir().map(|h| h.join(suffix.as_ref()));
// set() is a no-op when already initialized, which is correct: only the
// first call (at boot, before any filesystem work) should win.
let _ = NEST_DIR.set(path);
@@ -315,12 +309,8 @@ fn ensure_skill_symlinks(_root: &Path) -> Result<(), String> {
/// Dev builds (`is_dev = true`) use `"buzz-dev"` so that a running DMG and a
/// concurrent dev build each own a separate link and never clobber each other —
/// the same isolation that separates `~/.buzz` (prod) from `~/.buzz-dev` (dev).
pub fn cli_link_name(is_dev: bool) -> &'static str {
if is_dev {
"buzz-dev"
} else {
"buzz"
}
pub fn cli_link_name(is_dev: bool) -> String {
crate::build_identity::cli_name(is_dev)
}
/// Ensures `~/.local/bin/buzz` (prod) or `~/.local/bin/buzz-dev` (dev) is a

View File

@@ -7,7 +7,7 @@ fn nest_dir_is_under_home() {
// whether init_nest_dir was called before this test ran.
let name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
assert!(
name == NEST_DIR_PROD || name == NEST_DIR_DEV,
name == NEST_DIR_PROD || name == crate::build_identity::nest_name(true),
"nest_dir must end with .buzz or .buzz-dev, got {dir:?}"
);
}
@@ -23,7 +23,7 @@ fn init_nest_dir_prod_sets_buzz() {
if let Some(d) = dir {
let name = d.file_name().and_then(|n| n.to_str()).unwrap_or("");
assert!(
name == NEST_DIR_PROD || name == NEST_DIR_DEV,
name == NEST_DIR_PROD || name == crate::build_identity::nest_name(true),
"nest_dir suffix must be .buzz or .buzz-dev, got {d:?}"
);
}
@@ -357,13 +357,19 @@ fn ensure_skill_symlinks_skip_dangling_symlink() {
}
#[test]
fn cli_link_name_prod_is_buzz() {
assert_eq!(cli_link_name(false), "buzz");
fn cli_link_name_prod_follows_build_identity() {
let expected = crate::build_identity::demo_slug()
.map(|slug| format!("buzz-demo-{slug}"))
.unwrap_or_else(|| "buzz".to_string());
assert_eq!(cli_link_name(false), expected);
}
#[test]
fn cli_link_name_dev_is_buzz_dev() {
assert_eq!(cli_link_name(true), "buzz-dev");
fn cli_link_name_dev_follows_build_identity() {
let expected = crate::build_identity::demo_slug()
.map(|slug| format!("buzz-demo-{slug}"))
.unwrap_or_else(|| "buzz-dev".to_string());
assert_eq!(cli_link_name(true), expected);
}
#[cfg(unix)]
@@ -395,8 +401,8 @@ fn ensure_cli_symlink_creates_symlink_dev() {
let local_bin = tmp.path().join("local_bin");
fs::create_dir_all(&local_bin).unwrap();
// Dev link must be "buzz-dev", never "buzz".
assert_eq!(cli_link_name(true), "buzz-dev");
// Dev and demo links must never overwrite production's "buzz".
assert_ne!(cli_link_name(true), "buzz");
let link = local_bin.join(cli_link_name(true));
std::os::unix::fs::symlink(exe_parent.join("buzz"), &link).unwrap();

View File

@@ -67,6 +67,9 @@ pub(crate) const RESERVED_ENV_KEYS: &[&str] = &[
// ambient env var must not be able to forge setup mode (NotReady) on a
// Ready agent or suppress it (empty/stale payload) on a NotReady one.
"BUZZ_ACP_SETUP_PAYLOAD",
// Demo-build identity owns the child agent config root. A user override
// could silently reconnect a demo harness to production OAuth state.
"BUZZ_AGENT_CONFIG_DIR",
// Desktop ownership markers: these brand every spawned harness with the
// launching Desktop instance. A user-supplied override would let a
// definition masquerade as a different instance or fake the nonce used

View File

@@ -501,7 +501,6 @@ pub fn spawn_agent_child(
// The caller supplies the explicit canonical pair relay. This is the only
// relay this child may connect to, regardless of the record/workspace default.
let effective_relay_url = runtime_key.relay_url.clone();
// Augment PATH for DMG launches so child processes can find:
// - bundled CLI via ~/.local/bin symlink
// - nvm-managed node/npm (nvm initializes only in interactive shells)
@@ -809,6 +808,7 @@ pub fn spawn_agent_child(
for (key, value) in &descriptor.env {
command.env(key, value);
}
crate::build_identity::apply_demo_config_home(&mut command)?;
// B5: carry persisted effort; harness resolves thought_level configId at first session.
// Written AFTER descriptor.env so the canonical persisted value wins over any

View File

@@ -129,10 +129,9 @@ pub fn run_boot_migrations_after_reset(app: &tauri::AppHandle) {
}
fn run_boot_migrations_inner(app: &tauri::AppHandle, reset_completed: bool) {
// Initialize the process-lifetime nest directory before any filesystem
// operation that calls nest_dir(). The discriminator matches the existing
// pattern used by reconcile_target_dir: dev instances have an app-data-dir
// name starting with CANONICAL_DEV_IDENTIFIER.
// Initialize the process-lifetime nest directory before filesystem access
// that calls nest_dir(). The discriminator matches reconcile_target_dir:
// dev instances have an app-data-dir name starting with CANONICAL_DEV_IDENTIFIER.
let is_dev = if let Ok(data_dir) = app.path().app_data_dir() {
let dev = data_dir
.file_name()
@@ -144,18 +143,18 @@ fn run_boot_migrations_inner(app: &tauri::AppHandle, reset_completed: bool) {
false
};
// On dev builds, copy `.repos-dir` from ~/.buzz → ~/.buzz-dev BEFORE
// control returns to lib.rs where resolve_repos_at_boot() reads it. This
// ensures the dev nest boots with the correct workspace on its first launch,
// matching what the prod nest had configured. Skip-if-dest-exists so it is
// idempotent and never clobbers a value the dev nest already set explicitly.
// Uses the composed helper so gate + migration share the tested code path.
// On dev builds, copy `.repos-dir` from ~/.buzz → ~/.buzz-dev before
// resolve_repos_at_boot() reads it. Skip-if-dest-exists so it is idempotent
// and never clobbers a value the dev nest already set explicitly.
// The composed helper keeps gate + migration on the tested code path.
if let (Some(home), Some(dev_nest)) = (dirs::home_dir(), crate::managed_agents::nest_dir()) {
maybe_migrate_dev_repos_dir(is_dev, reset_completed, &home, &dev_nest);
}
migrate_legacy_app_data_dir(app);
sync_shared_agent_data(app);
if !crate::build_identity::is_demo_build() {
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.

View File

@@ -104,6 +104,11 @@ pub(crate) struct ResetContext<'a> {
pub keychain: &'a dyn ResetKeychain,
pub home_dir: Option<PathBuf>,
pub is_dev: bool,
/// Build-owned config root for demos. Production leaves this unset.
pub demo_config_dir: Option<PathBuf>,
/// Demo builds own only build-scoped state and must never delete shared
/// production or legacy agent roots.
pub is_demo: bool,
}
/// Entry point called from `lib.rs` setup (before migrations).
@@ -126,6 +131,16 @@ pub(crate) fn run_boot_reset(app_data_dir: &Path) -> ResetOutcome {
let legacy_dir = crate::migration::legacy_app_data_dir(app_data_dir);
let nest_dir = crate::managed_agents::nest_dir();
let demo_config_dir = match crate::build_identity::demo_config_home() {
Ok(dir) => dir,
Err(error) => {
eprintln!("buzz-desktop reset: {error}");
return ResetOutcome {
completed: false,
failed: true,
};
}
};
let ctx = ResetContext {
app_data_dir,
legacy_app_data_dir: legacy_dir,
@@ -133,6 +148,8 @@ pub(crate) fn run_boot_reset(app_data_dir: &Path) -> ResetOutcome {
keychain: &store,
home_dir,
is_dev,
demo_config_dir,
is_demo: crate::build_identity::is_demo_build(),
};
run_boot_reset_with_keychain(ctx)
@@ -166,6 +183,15 @@ fn rename_to_trash(src: &Path) -> Result<PathBuf, String> {
/// Core wipe logic — separated for testing.
pub(crate) fn run_boot_reset_with_keychain(ctx: ResetContext<'_>) -> ResetOutcome {
// An unknown demo credential root is not evidence of an absent root. Refuse
// before any destructive work and retain reset intent for the next boot.
if ctx.is_demo && ctx.demo_config_dir.is_none() {
eprintln!("buzz-desktop reset: cannot resolve demo credential directory");
return ResetOutcome {
completed: false,
failed: true,
};
}
let app_data_dir = ctx.app_data_dir;
// ── Step 1: rename app-data dir (atomic — sentinel survives the parent) ──
@@ -211,13 +237,34 @@ pub(crate) fn run_boot_reset_with_keychain(ctx: ResetContext<'_>) -> ResetOutcom
None
};
// ── Step 3: remove nest, ~/.sprout, ~/.config/buzz-agent, CLI symlink ────
// ── Step 3: remove build-owned nest and CLI symlink ──────────────────────
// Production and dev preserve their existing legacy/global cleanup. A demo
// never owns these shared roots, so signing out of one must leave them
// available to production and every other demo.
if let Some(ref nest) = ctx.nest_dir {
let _ = std::fs::remove_dir_all(nest);
}
// A demo owns credentials here. Failure to remove them must keep the reset
// pending, even if the app data and keychain were successfully wiped.
let demo_config_removed =
ctx.demo_config_dir
.as_ref()
.is_none_or(|path| match std::fs::remove_dir_all(path) {
Ok(()) => true,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => true,
Err(error) => {
eprintln!(
"buzz-desktop reset: remove demo config {}: {error}",
path.display()
);
false
}
});
if let Some(ref home) = ctx.home_dir {
let _ = std::fs::remove_dir_all(home.join(".sprout"));
let _ = std::fs::remove_dir_all(home.join(".config").join("buzz-agent"));
if !ctx.is_demo {
let _ = std::fs::remove_dir_all(home.join(".sprout"));
let _ = std::fs::remove_dir_all(home.join(".config").join("buzz-agent"));
}
let link_name = crate::managed_agents::cli_link_name(ctx.is_dev);
let _ = std::fs::remove_file(home.join(".local").join("bin").join(link_name));
}
@@ -273,6 +320,11 @@ pub(crate) fn run_boot_reset_with_keychain(ctx: ResetContext<'_>) -> ResetOutcom
.map(|p| !p.exists())
.unwrap_or(true);
let nest_gone = ctx.nest_dir.as_ref().map(|n| !n.exists()).unwrap_or(true);
// `exists()` treats metadata errors as absence. Only NotFound establishes
// that credentials are gone; a dangling symlink is not an absent root.
let demo_config_gone = ctx.demo_config_dir.as_ref().is_none_or(|path| {
matches!(std::fs::symlink_metadata(path), Err(error) if error.kind() == std::io::ErrorKind::NotFound)
});
let trash_app_gone = !trash_app.exists();
let trash_legacy_gone = trash_legacy.as_ref().map(|p| !p.exists()).unwrap_or(true);
let trash_webkit_gone = trash_webkit.as_ref().map(|p| !p.exists()).unwrap_or(true);
@@ -281,6 +333,8 @@ pub(crate) fn run_boot_reset_with_keychain(ctx: ResetContext<'_>) -> ResetOutcom
|| !app_data_gone
|| !legacy_gone
|| !nest_gone
|| !demo_config_removed
|| !demo_config_gone
|| !trash_app_gone
|| !trash_legacy_gone
|| !trash_webkit_gone
@@ -288,6 +342,7 @@ pub(crate) fn run_boot_reset_with_keychain(ctx: ResetContext<'_>) -> ResetOutcom
eprintln!(
"buzz-desktop reset: verification failed (keychain_wiped={keychain_ok}, \
app_data_gone={app_data_gone}, legacy_gone={legacy_gone}, nest_gone={nest_gone}, \
demo_config_removed={demo_config_removed}, demo_config_gone={demo_config_gone}, \
trash_app_gone={trash_app_gone}, trash_legacy_gone={trash_legacy_gone}, \
trash_webkit_gone={trash_webkit_gone})"
);
@@ -318,6 +373,10 @@ mod tests {
use std::cell::Cell;
use tempfile::TempDir;
mod demo {
include!("reset_demo_tests.rs");
}
// ── Fake keychain ─────────────────────────────────────────────────────────
struct FakeKeychain {
@@ -408,6 +467,8 @@ mod tests {
keychain,
home_dir: None, // skip nest/sprout/CLI ops in unit tests
is_dev,
demo_config_dir: None,
is_demo: false,
}
}
@@ -451,6 +512,8 @@ mod tests {
keychain: &kc,
home_dir: None,
is_dev: false,
demo_config_dir: None,
is_demo: false,
};
let outcome = run_boot_reset_with_keychain(ctx);
@@ -584,6 +647,8 @@ mod tests {
keychain: &kc,
home_dir: None,
is_dev: true,
demo_config_dir: None,
is_demo: false,
};
let outcome = run_boot_reset_with_keychain(ctx);
@@ -620,6 +685,8 @@ mod tests {
keychain: &kc,
home_dir: None,
is_dev: false,
demo_config_dir: None,
is_demo: false,
};
let outcome = run_boot_reset_with_keychain(ctx);
@@ -653,6 +720,8 @@ mod tests {
keychain: &kc,
home_dir: None,
is_dev: false,
demo_config_dir: None,
is_demo: false,
};
let outcome = run_boot_reset_with_keychain(ctx);
@@ -736,6 +805,8 @@ mod tests {
keychain: &kc,
home_dir: None,
is_dev: true,
demo_config_dir: None,
is_demo: false,
};
let outcome = run_boot_reset_with_keychain(ctx);
assert!(outcome.completed, "reset must complete");
@@ -830,6 +901,8 @@ mod tests {
keychain: &kc1,
home_dir: Some(tmp.path().to_path_buf()),
is_dev: false,
demo_config_dir: None,
is_demo: false,
};
let first = run_boot_reset_with_keychain(ctx1);
assert!(first.failed, "first attempt must fail");
@@ -853,6 +926,8 @@ mod tests {
keychain: &kc2,
home_dir: Some(tmp.path().to_path_buf()),
is_dev: false,
demo_config_dir: None,
is_demo: false,
};
let second = run_boot_reset_with_keychain(ctx2);
assert!(second.completed, "second attempt must complete");

View File

@@ -0,0 +1,169 @@
use super::*;
#[test]
fn test_demo_reset_preserves_shared_and_other_build_state() {
let tmp = TempDir::new().unwrap();
let home = tmp.path().join("home");
let app_data = tmp
.path()
.join("Application Support")
.join("xyz.block.buzz.app.demo.current-1234567812345678");
let demo_nest = home.join(".buzz-demo-current-1234567812345678");
let prod_nest = home.join(".buzz");
let other_demo_nest = home.join(".buzz-demo-other-8765432187654321");
let shared_sprout = home.join(".sprout");
let shared_agent = home.join(".config").join("buzz-agent");
let demo_config = home
.join("Library")
.join("Application Support")
.join("buzz-demo-current-1234567812345678");
let demo_oauth = demo_config.join("buzz-agent").join("oauth");
let other_demo_config = home
.join("Library")
.join("Application Support")
.join("buzz-demo-other-8765432187654321");
let other_demo_oauth = other_demo_config.join("buzz-agent").join("oauth");
for path in [
&app_data,
&demo_nest,
&prod_nest,
&other_demo_nest,
&shared_sprout,
&shared_agent,
&demo_oauth,
&other_demo_oauth,
] {
std::fs::create_dir_all(path).unwrap();
}
write_sentinel(&app_data).unwrap();
let kc = FakeKeychain::ok();
let ctx = ResetContext {
app_data_dir: &app_data,
legacy_app_data_dir: None,
nest_dir: Some(demo_nest.clone()),
keychain: &kc,
home_dir: Some(home),
is_dev: false,
demo_config_dir: Some(demo_config.clone()),
is_demo: true,
};
let outcome = run_boot_reset_with_keychain(ctx);
assert!(outcome.completed, "demo reset must complete");
assert!(!app_data.exists(), "demo app data must be wiped");
assert!(!demo_nest.exists(), "selected demo nest must be wiped");
assert!(
!demo_config.exists(),
"selected demo auth root must be wiped"
);
assert!(
other_demo_oauth.exists(),
"another demo's concrete auth root must survive"
);
assert!(prod_nest.exists(), "production nest must survive");
assert!(other_demo_nest.exists(), "another demo nest must survive");
assert!(shared_sprout.exists(), "shared legacy state must survive");
assert!(
shared_agent.exists(),
"shared agent auth state must survive"
);
}
#[test]
fn demo_config_delete_failure_keeps_sentinel_until_retry() {
let tmp = TempDir::new().unwrap();
let app_data = make_app_data(&tmp);
let config = tmp.path().join("demo-config");
let production = tmp.path().join("production/oauth/token.json");
let sibling = tmp.path().join("sibling/oauth/token.json");
for path in [&production, &sibling] {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "preserve").unwrap();
}
// A file at the directory path makes remove_dir_all fail on every platform,
// independent of the test user's privileges.
std::fs::write(&config, "obstruction").unwrap();
write_sentinel(&app_data).unwrap();
let kc = FakeKeychain::ok();
let run = || {
let mut ctx = make_ctx(&app_data, &kc, false);
ctx.is_demo = true;
ctx.demo_config_dir = Some(config.clone());
run_boot_reset_with_keychain(ctx)
};
let first = run();
assert!(first.failed && !first.completed);
assert!(check_sentinel(&app_data));
assert!(config.exists());
std::fs::remove_file(&config).unwrap();
let token = config.join("buzz-agent/oauth/databricks/token.json");
std::fs::create_dir_all(token.parent().unwrap()).unwrap();
std::fs::write(&token, "demo credential").unwrap();
let second = run();
assert!(second.completed && !second.failed);
assert!(!check_sentinel(&app_data));
assert!(!config.exists());
for path in [&production, &sibling] {
assert_eq!(std::fs::read_to_string(path).unwrap(), "preserve");
}
// A retry after a crash that already removed the root must also succeed.
write_sentinel(&app_data).unwrap();
assert!(run().completed);
assert!(!check_sentinel(&app_data));
}
#[cfg(unix)]
#[test]
fn demo_oauth_permission_failure_preserves_retry_intent() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let app_data = make_app_data(&tmp);
let config = tmp.path().join("demo-config");
let oauth = config.join("buzz-agent/oauth/databricks");
let token = oauth.join("token.json");
std::fs::create_dir_all(&oauth).unwrap();
std::fs::write(&token, "demo credential").unwrap();
write_sentinel(&app_data).unwrap();
let kc = FakeKeychain::ok();
let run = || {
let mut ctx = make_ctx(&app_data, &kc, false);
ctx.is_demo = true;
ctx.demo_config_dir = Some(config.clone());
run_boot_reset_with_keychain(ctx)
};
std::fs::set_permissions(&oauth, std::fs::Permissions::from_mode(0o500)).unwrap();
let first = run();
// Restore permissions before assertions so a failure never leaves test debris.
if oauth.exists() {
std::fs::set_permissions(&oauth, std::fs::Permissions::from_mode(0o700)).unwrap();
}
assert!(first.failed && !first.completed);
assert!(check_sentinel(&app_data));
assert_eq!(std::fs::read_to_string(&token).unwrap(), "demo credential");
assert!(run().completed);
assert!(!config.exists());
assert!(!check_sentinel(&app_data));
}
#[test]
fn unresolved_demo_config_keeps_reset_pending_without_deleting_state() {
let tmp = TempDir::new().unwrap();
let app_data = make_app_data(&tmp);
write_sentinel(&app_data).unwrap();
let kc = FakeKeychain::ok();
let mut ctx = make_ctx(&app_data, &kc, false);
ctx.is_demo = true;
assert!(ctx.demo_config_dir.is_none());
let outcome = run_boot_reset_with_keychain(ctx);
assert!(outcome.failed && !outcome.completed);
assert!(check_sentinel(&app_data));
assert!(
app_data.exists(),
"unresolved root must refuse before wiping"
);
}

View File

@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { afterEach, test } from "node:test";
const ipcHandlers = new Map();
@@ -23,8 +24,11 @@ globalThis.window = {
};
globalThis.__TAURI_INTERNALS__ = tauriInternals;
const { listenForNavigationDeepLinks, resetNavigationDeepLinkDrain } =
await import("@/shared/deep-link.ts");
const {
listenForEntityDeepLinks,
listenForNavigationDeepLinks,
resetNavigationDeepLinkDrain,
} = await import("@/shared/deep-link.ts");
function deferred() {
let resolve;
@@ -449,3 +453,58 @@ test("rejected navigation remains queued and is not acknowledged", async () => {
console.warn = originalWarn;
}
});
for (const delivery of ["cold-start", "running-instance"]) {
test(`canonical native demo entity payloads navigate before acknowledgement: ${delivery}`, async () => {
// Rust's canonical_entity_deep_link test proves all demo transport URLs
// produce these exact shared values, rather than passing demo schemes on.
const golden = JSON.parse(
readFileSync(
new URL("../../../test-fixtures/entity-links.json", import.meta.url),
"utf8",
),
);
const { parseEntityLink } = await import("@/shared/lib/entityLink.ts");
const pending = Object.entries(golden.links).map(([id, href]) => ({
id,
href,
}));
const queue = delivery === "cold-start" ? [...pending] : [];
const opened = [];
const acknowledged = [];
let notify;
ipcHandlers.set("plugin:event|listen", ({ handler }) => {
notify = callbacks.get(handler);
return handler;
});
ipcHandlers.set("plugin:event|unlisten", () => {});
ipcHandlers.set("take_pending_entity_deep_link", () => queue[0] ?? null);
ipcHandlers.set("acknowledge_pending_entity_deep_link", ({ id }) => {
assert.equal(queue[0]?.id, id);
assert.equal(opened.length, acknowledged.length + 1);
acknowledged.push(id);
queue.shift();
return true;
});
const unlisten = await listenForEntityDeepLinks((href) => {
const parsed = parseEntityLink(href);
assert.equal(parsed.ok, true, href);
opened.push(parsed.value);
return true;
});
await settle();
if (delivery === "running-instance") {
queue.push(...pending);
notify({ payload: pending[0] });
}
await settle();
await settle();
assert.deepEqual(
acknowledged,
pending.map(({ id }) => id),
);
assert.equal(opened.length, pending.length);
assert.equal(queue.length, 0);
unlisten();
});
}