From 48892e446556df2ef53d55d4cc6831263714a3e7 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Wed, 26 Aug 2026 17:20:51 -0400 Subject: [PATCH 1/9] feat(desktop): add build-gated Bestie experiment Co-authored-by: Codex Signed-off-by: Arjun Mahanti --- .env.example | 3 ++ .../settings/ui/ExperimentalFeaturesCard.tsx | 4 +- .../features/buildAvailability.test.mjs | 43 +++++++++++++++++++ .../src/shared/features/buildAvailability.ts | 24 +++++++++++ desktop/src/shared/features/index.ts | 2 + desktop/src/shared/features/manifest.ts | 2 + .../shared/features/resolveEnabled.test.mjs | 16 +++++++ desktop/src/shared/features/resolveEnabled.ts | 2 + .../features/runtimeBuildAvailability.ts | 21 +++++++++ desktop/src/shared/features/types.ts | 5 +++ .../src/shared/features/useFeatureEnabled.ts | 8 +++- preview-features.json | 7 +++ 12 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 desktop/src/shared/features/buildAvailability.test.mjs create mode 100644 desktop/src/shared/features/buildAvailability.ts create mode 100644 desktop/src/shared/features/runtimeBuildAvailability.ts diff --git a/.env.example b/.env.example index a6740f7a7d8..c0b913d746d 100644 --- a/.env.example +++ b/.env.example @@ -206,6 +206,9 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz # app launch while keeping the current identity and relay data. # VITE_BUZZ_FORCE_FRESH_ONBOARDING=true +# Makes the default-off Bestie experiment available in eligible builds. +# VITE_BUZZ_BESTIE=1 + # ── Subscription & filtering ───────────────────────────────────────────────── # Subscribe mode: "mentions" (default), "all", or "config" (rule-based). # BUZZ_ACP_SUBSCRIBE=mentions diff --git a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx index 50052c014fe..db08e8b7027 100644 --- a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx +++ b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx @@ -1,5 +1,5 @@ import { setAgentManagedProfiles } from "@/shared/api/tauri"; -import { desktopFeatures, useFeatureToggle } from "@/shared/features"; +import { availableDesktopFeatures, useFeatureToggle } from "@/shared/features"; import type { FeatureDefinition } from "@/shared/features"; import { Switch } from "@/shared/ui/switch"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; @@ -42,7 +42,7 @@ function FeatureRow({ feature }: { feature: FeatureDefinition }) { export function ExperimentalFeaturesCard() { // Manifest is preview-only by definition; every desktop entry is a preview // feature. - const previewFeatures = desktopFeatures; + const previewFeatures = availableDesktopFeatures; return (
diff --git a/desktop/src/shared/features/buildAvailability.test.mjs b/desktop/src/shared/features/buildAvailability.test.mjs new file mode 100644 index 00000000000..046615876c5 --- /dev/null +++ b/desktop/src/shared/features/buildAvailability.test.mjs @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + filterBuildAvailableFeatures, + isFeatureBuildAvailable, +} from "./buildAvailability.ts"; + +const publicFeature = { + id: "projects", + name: "Projects", + description: "", +}; +const bestie = { + id: "bestie", + name: "Bestie", + description: "", + requiredBuildFlag: "bestie", +}; + +describe("feature build availability", () => { + it("keeps features without a required build flag available", () => { + assert.equal( + isFeatureBuildAvailable(publicFeature, { bestie: false }), + true, + ); + }); + + it("does not allow a required feature into an ineligible build", () => { + assert.equal(isFeatureBuildAvailable(bestie, { bestie: false }), false); + }); + + it("shows a required feature when its build opts in", () => { + assert.equal(isFeatureBuildAvailable(bestie, { bestie: true }), true); + }); + + it("filters unavailable features out of the experiment picker", () => { + assert.deepEqual( + filterBuildAvailableFeatures([publicFeature, bestie], { bestie: false }), + [publicFeature], + ); + }); +}); diff --git a/desktop/src/shared/features/buildAvailability.ts b/desktop/src/shared/features/buildAvailability.ts new file mode 100644 index 00000000000..3c6b3bcb44b --- /dev/null +++ b/desktop/src/shared/features/buildAvailability.ts @@ -0,0 +1,24 @@ +import type { FeatureBuildFlag, FeatureDefinition } from "./types"; + +export type FeatureBuildFlags = Record; + +/** + * Build capabilities are deliberately separate from user preference. A local + * storage override cannot make an unavailable feature visible. + */ +export function isFeatureBuildAvailable( + feature: FeatureDefinition, + buildFlags: FeatureBuildFlags, +): boolean { + const requiredFlag = feature.requiredBuildFlag; + return requiredFlag ? buildFlags[requiredFlag] : true; +} + +export function filterBuildAvailableFeatures( + features: FeatureDefinition[], + buildFlags: FeatureBuildFlags, +): FeatureDefinition[] { + return features.filter((feature) => + isFeatureBuildAvailable(feature, buildFlags), + ); +} diff --git a/desktop/src/shared/features/index.ts b/desktop/src/shared/features/index.ts index 9f6db1b156e..f476cbd14d9 100644 --- a/desktop/src/shared/features/index.ts +++ b/desktop/src/shared/features/index.ts @@ -1,8 +1,10 @@ export { FeatureGate } from "./FeatureGate"; export { allFeatures, desktopFeatures, getFeature, manifest } from "./manifest"; +export { availableDesktopFeatures } from "./runtimeBuildAvailability"; export { getOverrides, setOverride, clearOverride } from "./store"; export type { FeatureDefinition, + FeatureBuildFlag, FeaturesManifest, FeaturePlatform, } from "./types"; diff --git a/desktop/src/shared/features/manifest.ts b/desktop/src/shared/features/manifest.ts index 1e6f48ae017..8ebdae5c06e 100644 --- a/desktop/src/shared/features/manifest.ts +++ b/desktop/src/shared/features/manifest.ts @@ -8,6 +8,7 @@ import type { FeatureDefinition, FeaturesManifest } from "./types"; // The app keeps working; gated UI stays hidden; nothing accidentally leaks. const FeaturePlatformSchema = z.enum(["desktop", "mobile"]); +const FeatureBuildFlagSchema = z.enum(["bestie"]); const FeatureDefinitionSchema = z.object({ id: z.string().min(1), @@ -15,6 +16,7 @@ const FeatureDefinitionSchema = z.object({ description: z.string(), defaultEnabled: z.boolean().optional(), platforms: z.array(FeaturePlatformSchema).optional(), + requiredBuildFlag: FeatureBuildFlagSchema.optional(), }); const FeaturesManifestSchema = z.object({ diff --git a/desktop/src/shared/features/resolveEnabled.test.mjs b/desktop/src/shared/features/resolveEnabled.test.mjs index d1639f6e4f0..dc88aa8db78 100644 --- a/desktop/src/shared/features/resolveEnabled.test.mjs +++ b/desktop/src/shared/features/resolveEnabled.test.mjs @@ -30,4 +30,20 @@ describe("resolveEnabled (preview-only)", () => { it("ignores overrides for unrelated ids", () => { assert.equal(resolveEnabled("workflows", { pulse: true }), false); }); + + it("requires both build availability and a user opt-in", () => { + const cases = [ + { buildAvailable: false, optedIn: false, expected: false }, + { buildAvailable: false, optedIn: true, expected: false }, + { buildAvailable: true, optedIn: false, expected: false }, + { buildAvailable: true, optedIn: true, expected: true }, + ]; + + for (const { buildAvailable, optedIn, expected } of cases) { + assert.equal( + resolveEnabled("bestie", { bestie: optedIn }, false, buildAvailable), + expected, + ); + } + }); }); diff --git a/desktop/src/shared/features/resolveEnabled.ts b/desktop/src/shared/features/resolveEnabled.ts index e3c544ec502..3aee2738dd6 100644 --- a/desktop/src/shared/features/resolveEnabled.ts +++ b/desktop/src/shared/features/resolveEnabled.ts @@ -14,6 +14,8 @@ export function resolveEnabled( featureId: string, overrides: Record, defaultEnabled = false, + buildAvailable = true, ): boolean { + if (!buildAvailable) return false; return overrides[featureId] ?? defaultEnabled; } diff --git a/desktop/src/shared/features/runtimeBuildAvailability.ts b/desktop/src/shared/features/runtimeBuildAvailability.ts new file mode 100644 index 00000000000..d0966f70951 --- /dev/null +++ b/desktop/src/shared/features/runtimeBuildAvailability.ts @@ -0,0 +1,21 @@ +import { + filterBuildAvailableFeatures, + isFeatureBuildAvailable, + type FeatureBuildFlags, +} from "./buildAvailability"; +import { desktopFeatures } from "./manifest"; + +const runtimeBuildFlags: FeatureBuildFlags = { + bestie: import.meta.env.VITE_BUZZ_BESTIE === "1", +}; + +export function isFeatureAvailableInThisBuild( + feature: Parameters[0], +): boolean { + return isFeatureBuildAvailable(feature, runtimeBuildFlags); +} + +export const availableDesktopFeatures = filterBuildAvailableFeatures( + desktopFeatures, + runtimeBuildFlags, +); diff --git a/desktop/src/shared/features/types.ts b/desktop/src/shared/features/types.ts index c452cd54b4a..4fdf07f2afd 100644 --- a/desktop/src/shared/features/types.ts +++ b/desktop/src/shared/features/types.ts @@ -1,6 +1,9 @@ /** Platforms a feature is available on */ export type FeaturePlatform = "desktop" | "mobile"; +/** Build-time capabilities that can make a preview available to users. */ +export type FeatureBuildFlag = "bestie"; + /** * A single feature definition from the manifest. * @@ -16,6 +19,8 @@ export interface FeatureDefinition { defaultEnabled?: boolean; /** If omitted, feature is available on all platforms */ platforms?: FeaturePlatform[]; + /** If present, the build must opt in before the user can see or enable it */ + requiredBuildFlag?: FeatureBuildFlag; } /** The root manifest schema */ diff --git a/desktop/src/shared/features/useFeatureEnabled.ts b/desktop/src/shared/features/useFeatureEnabled.ts index b0c9878d0b7..356a362ccac 100644 --- a/desktop/src/shared/features/useFeatureEnabled.ts +++ b/desktop/src/shared/features/useFeatureEnabled.ts @@ -1,6 +1,7 @@ import { useSyncExternalStore, useCallback, useEffect } from "react"; import { getFeature } from "./manifest"; import { resolveEnabled } from "./resolveEnabled"; +import { isFeatureAvailableInThisBuild } from "./runtimeBuildAvailability"; import { getOverrides, setOverride, OVERRIDES_KEY } from "./store"; type Listener = () => void; @@ -102,7 +103,12 @@ export function useFeatureEnabled(featureId: string): boolean { return true; } - return resolveEnabled(featureId, overrides, feature.defaultEnabled); + return resolveEnabled( + featureId, + overrides, + feature.defaultEnabled, + isFeatureAvailableInThisBuild(feature), + ); } /** diff --git a/preview-features.json b/preview-features.json index 388f1c39b04..79460c3cad1 100644 --- a/preview-features.json +++ b/preview-features.json @@ -30,6 +30,13 @@ "name": "Agent-managed profiles", "description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy", "platforms": ["desktop"] + }, + { + "id": "bestie", + "name": "Bestie", + "description": "Try a personal agent that is always close at hand", + "platforms": ["desktop"], + "requiredBuildFlag": "bestie" } ] } From 2acd085729d5d6f3c644fcfd13412dd2b95e48f8 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Wed, 26 Aug 2026 19:34:37 -0400 Subject: [PATCH 2/9] fix(desktop): fail closed for unavailable previews Co-authored-by: Codex Signed-off-by: Arjun Mahanti --- .../src/features/settings/ui/SettingsView.tsx | 11 +++-- .../features/buildAvailability.test.mjs | 7 ++++ desktop/src/shared/features/index.ts | 6 ++- .../shared/features/resolveEnabled.test.mjs | 42 +++++++++++++++---- desktop/src/shared/features/resolveEnabled.ts | 21 ++++++---- .../features/runtimeBuildAvailability.ts | 14 +++++++ .../src/shared/features/useFeatureEnabled.ts | 12 +----- 7 files changed, 82 insertions(+), 31 deletions(-) diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index 2f9b2c36a1a..b3bcaf75862 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -7,11 +7,11 @@ import { canManageCommunityMembers, shouldWarnMissingMembershipSnapshot, } from "@/shared/api/relayMembers"; -import { getFeature } from "@/shared/features/manifest"; import { - resolveEnabled, + getFeature, + resolveFeatureEnabledInThisBuild, useFeatureSnapshot, -} from "@/shared/features/useFeatureEnabled"; +} from "@/shared/features"; import { topChromeBackdrop } from "@/shared/layout/chromeLayout"; import { cn } from "@/shared/lib/cn"; import { @@ -137,7 +137,10 @@ export function SettingsView({ // stable and renders unconditionally (fail-open). if (s.featureGate) { const feature = getFeature(s.featureGate); - if (feature && !resolveEnabled(s.featureGate, featureState)) { + if ( + feature && + !resolveFeatureEnabledInThisBuild(feature, featureState) + ) { return false; } } diff --git a/desktop/src/shared/features/buildAvailability.test.mjs b/desktop/src/shared/features/buildAvailability.test.mjs index 046615876c5..688afe59bfc 100644 --- a/desktop/src/shared/features/buildAvailability.test.mjs +++ b/desktop/src/shared/features/buildAvailability.test.mjs @@ -40,4 +40,11 @@ describe("feature build availability", () => { [publicFeature], ); }); + + it("includes the Bestie experiment only in an eligible build", () => { + assert.deepEqual( + filterBuildAvailableFeatures([publicFeature, bestie], { bestie: true }), + [publicFeature, bestie], + ); + }); }); diff --git a/desktop/src/shared/features/index.ts b/desktop/src/shared/features/index.ts index f476cbd14d9..e53c1d1ad42 100644 --- a/desktop/src/shared/features/index.ts +++ b/desktop/src/shared/features/index.ts @@ -1,6 +1,9 @@ export { FeatureGate } from "./FeatureGate"; export { allFeatures, desktopFeatures, getFeature, manifest } from "./manifest"; -export { availableDesktopFeatures } from "./runtimeBuildAvailability"; +export { + availableDesktopFeatures, + resolveFeatureEnabledInThisBuild, +} from "./runtimeBuildAvailability"; export { getOverrides, setOverride, clearOverride } from "./store"; export type { FeatureDefinition, @@ -13,5 +16,4 @@ export { useFeatureToggle, useFeatureSnapshot, usePreviewFeatureWarning, - resolveEnabled, } from "./useFeatureEnabled"; diff --git a/desktop/src/shared/features/resolveEnabled.test.mjs b/desktop/src/shared/features/resolveEnabled.test.mjs index dc88aa8db78..bf08878a788 100644 --- a/desktop/src/shared/features/resolveEnabled.test.mjs +++ b/desktop/src/shared/features/resolveEnabled.test.mjs @@ -4,31 +4,48 @@ import { describe, it } from "node:test"; import { resolveEnabled } from "./resolveEnabled.ts"; describe("resolveEnabled (preview-only)", () => { + const workflows = { + id: "workflows", + name: "Workflows", + description: "", + }; + it("returns false by default (no override)", () => { - assert.equal(resolveEnabled("workflows", {}), false); + assert.equal(resolveEnabled(workflows, {}, true), false); }); it("returns true when user opts in", () => { - assert.equal(resolveEnabled("workflows", { workflows: true }), true); + assert.equal(resolveEnabled(workflows, { workflows: true }, true), true); }); it("uses an enabled manifest default when no override exists", () => { - assert.equal(resolveEnabled("defaultOnFeature", {}, true), true); + assert.equal( + resolveEnabled( + { ...workflows, id: "defaultOnFeature", defaultEnabled: true }, + {}, + true, + ), + true, + ); }); it("lets an explicit opt-out override an enabled default", () => { assert.equal( - resolveEnabled("defaultOnFeature", { defaultOnFeature: false }, true), + resolveEnabled( + { ...workflows, id: "defaultOnFeature", defaultEnabled: true }, + { defaultOnFeature: false }, + true, + ), false, ); }); it("returns false when user explicitly opts out", () => { - assert.equal(resolveEnabled("workflows", { workflows: false }), false); + assert.equal(resolveEnabled(workflows, { workflows: false }, true), false); }); it("ignores overrides for unrelated ids", () => { - assert.equal(resolveEnabled("workflows", { pulse: true }), false); + assert.equal(resolveEnabled(workflows, { pulse: true }, true), false); }); it("requires both build availability and a user opt-in", () => { @@ -41,9 +58,20 @@ describe("resolveEnabled (preview-only)", () => { for (const { buildAvailable, optedIn, expected } of cases) { assert.equal( - resolveEnabled("bestie", { bestie: optedIn }, false, buildAvailable), + resolveEnabled( + { ...workflows, id: "bestie" }, + { bestie: optedIn }, + buildAvailable, + ), expected, ); } }); + + it("fails closed when a JavaScript caller omits build availability", () => { + assert.equal( + resolveEnabled({ ...workflows, id: "bestie" }, { bestie: true }), + false, + ); + }); }); diff --git a/desktop/src/shared/features/resolveEnabled.ts b/desktop/src/shared/features/resolveEnabled.ts index 3aee2738dd6..e89b06544e6 100644 --- a/desktop/src/shared/features/resolveEnabled.ts +++ b/desktop/src/shared/features/resolveEnabled.ts @@ -1,21 +1,26 @@ /** * Pure resolution logic for preview-feature visibility. - * No side effects, no imports — safe to test in isolation. + * No side effects — safe to test in isolation. * * The manifest (`preview-features.json`) lists only preview features. * Anything not in the manifest is stable and resolves true elsewhere * (see `useFeatureEnabled`). Once you're inside `resolveEnabled`, the * feature IS in the manifest — preview by definition. * - * An explicit user override wins; otherwise the feature's manifest default is - * used (false when omitted). + * Build availability is mandatory. Within an eligible build, an explicit user + * override wins; otherwise the feature's manifest default is used (false when + * omitted). */ +import type { FeatureDefinition } from "./types"; + export function resolveEnabled( - featureId: string, + feature: FeatureDefinition, overrides: Record, - defaultEnabled = false, - buildAvailable = true, + buildAvailable: boolean, ): boolean { - if (!buildAvailable) return false; - return overrides[featureId] ?? defaultEnabled; + // Strict equality is intentional: JavaScript callers that omit the + // capability argument fail closed instead of recreating a build-gate side + // door outside TypeScript. + if (buildAvailable !== true) return false; + return overrides[feature.id] ?? feature.defaultEnabled ?? false; } diff --git a/desktop/src/shared/features/runtimeBuildAvailability.ts b/desktop/src/shared/features/runtimeBuildAvailability.ts index d0966f70951..76a86891150 100644 --- a/desktop/src/shared/features/runtimeBuildAvailability.ts +++ b/desktop/src/shared/features/runtimeBuildAvailability.ts @@ -4,6 +4,8 @@ import { type FeatureBuildFlags, } from "./buildAvailability"; import { desktopFeatures } from "./manifest"; +import { resolveEnabled } from "./resolveEnabled"; +import type { FeatureDefinition } from "./types"; const runtimeBuildFlags: FeatureBuildFlags = { bestie: import.meta.env.VITE_BUZZ_BESTIE === "1", @@ -15,6 +17,18 @@ export function isFeatureAvailableInThisBuild( return isFeatureBuildAvailable(feature, runtimeBuildFlags); } +/** Canonical preview resolution for the running build. */ +export function resolveFeatureEnabledInThisBuild( + feature: FeatureDefinition, + overrides: Record, +): boolean { + return resolveEnabled( + feature, + overrides, + isFeatureAvailableInThisBuild(feature), + ); +} + export const availableDesktopFeatures = filterBuildAvailableFeatures( desktopFeatures, runtimeBuildFlags, diff --git a/desktop/src/shared/features/useFeatureEnabled.ts b/desktop/src/shared/features/useFeatureEnabled.ts index 356a362ccac..0326fe51db7 100644 --- a/desktop/src/shared/features/useFeatureEnabled.ts +++ b/desktop/src/shared/features/useFeatureEnabled.ts @@ -1,7 +1,6 @@ import { useSyncExternalStore, useCallback, useEffect } from "react"; import { getFeature } from "./manifest"; -import { resolveEnabled } from "./resolveEnabled"; -import { isFeatureAvailableInThisBuild } from "./runtimeBuildAvailability"; +import { resolveFeatureEnabledInThisBuild } from "./runtimeBuildAvailability"; import { getOverrides, setOverride, OVERRIDES_KEY } from "./store"; type Listener = () => void; @@ -103,12 +102,7 @@ export function useFeatureEnabled(featureId: string): boolean { return true; } - return resolveEnabled( - featureId, - overrides, - feature.defaultEnabled, - isFeatureAvailableInThisBuild(feature), - ); + return resolveFeatureEnabledInThisBuild(feature, overrides); } /** @@ -163,5 +157,3 @@ export function usePreviewFeatureWarning(featureId: string): void { }; }, [feature, enabled]); } - -export { resolveEnabled } from "./resolveEnabled"; From 523cf49ced03cba9be43836a54d6aa5d6923cc82 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Thu, 27 Aug 2026 12:21:12 -0400 Subject: [PATCH 3/9] feat(desktop): exclude Bestie from OSS artifacts Signed-off-by: Arjun Mahanti Co-authored-by: Codex --- .env.example | 3 +- desktop/package.json | 2 +- .../build-protected-feature-artifacts.mjs | 108 ++++++++++++++++++ .../settings/ui/ExperimentalFeaturesCard.tsx | 4 +- .../src/features/settings/ui/SettingsView.tsx | 4 +- desktop/src/protectedFeatures/internal.ts | 11 ++ .../protectedFeatures.test.mjs | 19 +++ desktop/src/protectedFeatures/public.ts | 7 ++ .../features/buildAvailability.test.mjs | 50 -------- .../src/shared/features/buildAvailability.ts | 24 ---- desktop/src/shared/features/index.ts | 6 +- desktop/src/shared/features/manifest.ts | 8 +- .../shared/features/resolveEnabled.test.mjs | 56 +-------- desktop/src/shared/features/resolveEnabled.ts | 19 +-- .../features/runtimeBuildAvailability.ts | 35 ------ desktop/src/shared/features/types.ts | 5 - .../src/shared/features/useFeatureEnabled.ts | 6 +- desktop/test-loader-hooks.mjs | 6 + desktop/tsconfig.json | 1 + desktop/vite.config.ts | 107 +++++++++-------- preview-features.json | 7 -- 21 files changed, 242 insertions(+), 246 deletions(-) create mode 100644 desktop/scripts/build-protected-feature-artifacts.mjs create mode 100644 desktop/src/protectedFeatures/internal.ts create mode 100644 desktop/src/protectedFeatures/protectedFeatures.test.mjs create mode 100644 desktop/src/protectedFeatures/public.ts delete mode 100644 desktop/src/shared/features/buildAvailability.test.mjs delete mode 100644 desktop/src/shared/features/buildAvailability.ts delete mode 100644 desktop/src/shared/features/runtimeBuildAvailability.ts diff --git a/.env.example b/.env.example index c0b913d746d..e15489f8c65 100644 --- a/.env.example +++ b/.env.example @@ -206,7 +206,8 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz # app launch while keeping the current identity and relay data. # VITE_BUZZ_FORCE_FRESH_ONBOARDING=true -# Makes the default-off Bestie experiment available in eligible builds. +# Protected internal builds only: selects the module graph that contains the +# default-off Bestie experiment. Official OSS builds must leave this unset. # VITE_BUZZ_BESTIE=1 # ── Subscription & filtering ───────────────────────────────────────────────── diff --git a/desktop/package.json b/desktop/package.json index 1e93fd76a85..e810d2bc284 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && node ./scripts/build-protected-feature-artifacts.mjs", "build:e2e": "tsc && vite build --mode e2e", "typecheck": "tsc --noEmit", "check:file-sizes": "node ./scripts/check-file-sizes.mjs", diff --git a/desktop/scripts/build-protected-feature-artifacts.mjs b/desktop/scripts/build-protected-feature-artifacts.mjs new file mode 100644 index 00000000000..1fd17f6fdbb --- /dev/null +++ b/desktop/scripts/build-protected-feature-artifacts.mjs @@ -0,0 +1,108 @@ +import { spawnSync } from "node:child_process"; +import { + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const desktopRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +const selectedInternalVariant = process.env.VITE_BUZZ_BESTIE === "1"; +const scratchRoot = mkdtempSync( + path.join(tmpdir(), "buzz-protected-feature-artifacts-"), +); +const selectedOutput = path.join(desktopRoot, "dist"); +const alternateOutput = path.join(scratchRoot, "alternate"); +const viteBinary = process.platform === "win32" ? "vite.cmd" : "vite"; + +function buildVariant({ internal, output }) { + const env = { ...process.env }; + if (internal) { + env.VITE_BUZZ_BESTIE = "1"; + } else { + delete env.VITE_BUZZ_BESTIE; + } + + const result = spawnSync( + viteBinary, + ["build", "--outDir", output, "--emptyOutDir"], + { + cwd: desktopRoot, + env, + stdio: "inherit", + }, + ); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + `${internal ? "internal" : "OSS"} desktop build failed with status ${result.status}`, + ); + } +} + +function emittedText(root) { + const chunks = []; + const visit = (candidate) => { + const stat = statSync(candidate); + if (stat.isDirectory()) { + for (const child of readdirSync(candidate)) { + visit(path.join(candidate, child)); + } + return; + } + if (/\.(?:css|html|js|json)$/u.test(candidate)) { + chunks.push(readFileSync(candidate, "utf8")); + } + }; + visit(root); + return chunks.join("\n"); +} + +function assertArtifactContract({ ossOutput, internalOutput }) { + const ossText = emittedText(ossOutput); + const internalText = emittedText(internalOutput); + const protectedContent = /\bbestie\b|chief of staff|builtin:bestie/iu; + const internalManifestMarker = + "Try a personal agent that is always close at hand"; + + if (protectedContent.test(ossText)) { + throw new Error( + "Official OSS desktop artifact contains protected Bestie/Chief content", + ); + } + if (!internalText.includes(internalManifestMarker)) { + throw new Error( + "Protected internal desktop artifact is missing the Bestie manifest", + ); + } +} + +try { + // Build the unselected variant outside dist first, then leave the requested + // variant in dist for Vite/Tauri's ordinary packaging contract. + buildVariant({ + internal: !selectedInternalVariant, + output: alternateOutput, + }); + buildVariant({ + internal: selectedInternalVariant, + output: selectedOutput, + }); + + assertArtifactContract({ + ossOutput: selectedInternalVariant ? alternateOutput : selectedOutput, + internalOutput: selectedInternalVariant ? selectedOutput : alternateOutput, + }); + console.log( + `Protected feature artifact matrix passed; dist contains the ${selectedInternalVariant ? "internal" : "OSS"} variant.`, + ); +} finally { + rmSync(scratchRoot, { recursive: true, force: true }); +} diff --git a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx index db08e8b7027..50052c014fe 100644 --- a/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx +++ b/desktop/src/features/settings/ui/ExperimentalFeaturesCard.tsx @@ -1,5 +1,5 @@ import { setAgentManagedProfiles } from "@/shared/api/tauri"; -import { availableDesktopFeatures, useFeatureToggle } from "@/shared/features"; +import { desktopFeatures, useFeatureToggle } from "@/shared/features"; import type { FeatureDefinition } from "@/shared/features"; import { Switch } from "@/shared/ui/switch"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; @@ -42,7 +42,7 @@ function FeatureRow({ feature }: { feature: FeatureDefinition }) { export function ExperimentalFeaturesCard() { // Manifest is preview-only by definition; every desktop entry is a preview // feature. - const previewFeatures = availableDesktopFeatures; + const previewFeatures = desktopFeatures; return (
diff --git a/desktop/src/features/settings/ui/SettingsView.tsx b/desktop/src/features/settings/ui/SettingsView.tsx index b3bcaf75862..d242faf6189 100644 --- a/desktop/src/features/settings/ui/SettingsView.tsx +++ b/desktop/src/features/settings/ui/SettingsView.tsx @@ -9,7 +9,7 @@ import { } from "@/shared/api/relayMembers"; import { getFeature, - resolveFeatureEnabledInThisBuild, + resolveEnabled, useFeatureSnapshot, } from "@/shared/features"; import { topChromeBackdrop } from "@/shared/layout/chromeLayout"; @@ -139,7 +139,7 @@ export function SettingsView({ const feature = getFeature(s.featureGate); if ( feature && - !resolveFeatureEnabledInThisBuild(feature, featureState) + !resolveEnabled(s.featureGate, featureState, feature.defaultEnabled) ) { return false; } diff --git a/desktop/src/protectedFeatures/internal.ts b/desktop/src/protectedFeatures/internal.ts new file mode 100644 index 00000000000..7f9f6b551e8 --- /dev/null +++ b/desktop/src/protectedFeatures/internal.ts @@ -0,0 +1,11 @@ +import type { FeatureDefinition } from "@/shared/features/types"; + +/** Definitions available only in the protected internal application build. */ +export const protectedFeatureDefinitions: FeatureDefinition[] = [ + { + id: "bestie", + name: "Bestie", + description: "Try a personal agent that is always close at hand", + platforms: ["desktop"], + }, +]; diff --git a/desktop/src/protectedFeatures/protectedFeatures.test.mjs b/desktop/src/protectedFeatures/protectedFeatures.test.mjs new file mode 100644 index 00000000000..fca8cc698ad --- /dev/null +++ b/desktop/src/protectedFeatures/protectedFeatures.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { protectedFeatureDefinitions as internalDefinitions } from "./internal.ts"; +import { protectedFeatureDefinitions as publicDefinitions } from "./public.ts"; + +describe("protected feature build variants", () => { + it("keeps protected definitions out of the OSS module", () => { + assert.deepEqual(publicDefinitions, []); + }); + + it("adds Bestie only through the internal module", () => { + assert.deepEqual( + internalDefinitions.map((feature) => feature.id), + ["bestie"], + ); + assert.equal(internalDefinitions[0]?.defaultEnabled, undefined); + }); +}); diff --git a/desktop/src/protectedFeatures/public.ts b/desktop/src/protectedFeatures/public.ts new file mode 100644 index 00000000000..90c1e596242 --- /dev/null +++ b/desktop/src/protectedFeatures/public.ts @@ -0,0 +1,7 @@ +import type { FeatureDefinition } from "@/shared/features/types"; + +/** + * Protected feature definitions compiled into the official OSS application. + * Keep this module free of protected product names, metadata, and imports. + */ +export const protectedFeatureDefinitions: FeatureDefinition[] = []; diff --git a/desktop/src/shared/features/buildAvailability.test.mjs b/desktop/src/shared/features/buildAvailability.test.mjs deleted file mode 100644 index 688afe59bfc..00000000000 --- a/desktop/src/shared/features/buildAvailability.test.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { - filterBuildAvailableFeatures, - isFeatureBuildAvailable, -} from "./buildAvailability.ts"; - -const publicFeature = { - id: "projects", - name: "Projects", - description: "", -}; -const bestie = { - id: "bestie", - name: "Bestie", - description: "", - requiredBuildFlag: "bestie", -}; - -describe("feature build availability", () => { - it("keeps features without a required build flag available", () => { - assert.equal( - isFeatureBuildAvailable(publicFeature, { bestie: false }), - true, - ); - }); - - it("does not allow a required feature into an ineligible build", () => { - assert.equal(isFeatureBuildAvailable(bestie, { bestie: false }), false); - }); - - it("shows a required feature when its build opts in", () => { - assert.equal(isFeatureBuildAvailable(bestie, { bestie: true }), true); - }); - - it("filters unavailable features out of the experiment picker", () => { - assert.deepEqual( - filterBuildAvailableFeatures([publicFeature, bestie], { bestie: false }), - [publicFeature], - ); - }); - - it("includes the Bestie experiment only in an eligible build", () => { - assert.deepEqual( - filterBuildAvailableFeatures([publicFeature, bestie], { bestie: true }), - [publicFeature, bestie], - ); - }); -}); diff --git a/desktop/src/shared/features/buildAvailability.ts b/desktop/src/shared/features/buildAvailability.ts deleted file mode 100644 index 3c6b3bcb44b..00000000000 --- a/desktop/src/shared/features/buildAvailability.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { FeatureBuildFlag, FeatureDefinition } from "./types"; - -export type FeatureBuildFlags = Record; - -/** - * Build capabilities are deliberately separate from user preference. A local - * storage override cannot make an unavailable feature visible. - */ -export function isFeatureBuildAvailable( - feature: FeatureDefinition, - buildFlags: FeatureBuildFlags, -): boolean { - const requiredFlag = feature.requiredBuildFlag; - return requiredFlag ? buildFlags[requiredFlag] : true; -} - -export function filterBuildAvailableFeatures( - features: FeatureDefinition[], - buildFlags: FeatureBuildFlags, -): FeatureDefinition[] { - return features.filter((feature) => - isFeatureBuildAvailable(feature, buildFlags), - ); -} diff --git a/desktop/src/shared/features/index.ts b/desktop/src/shared/features/index.ts index e53c1d1ad42..9f6db1b156e 100644 --- a/desktop/src/shared/features/index.ts +++ b/desktop/src/shared/features/index.ts @@ -1,13 +1,8 @@ export { FeatureGate } from "./FeatureGate"; export { allFeatures, desktopFeatures, getFeature, manifest } from "./manifest"; -export { - availableDesktopFeatures, - resolveFeatureEnabledInThisBuild, -} from "./runtimeBuildAvailability"; export { getOverrides, setOverride, clearOverride } from "./store"; export type { FeatureDefinition, - FeatureBuildFlag, FeaturesManifest, FeaturePlatform, } from "./types"; @@ -16,4 +11,5 @@ export { useFeatureToggle, useFeatureSnapshot, usePreviewFeatureWarning, + resolveEnabled, } from "./useFeatureEnabled"; diff --git a/desktop/src/shared/features/manifest.ts b/desktop/src/shared/features/manifest.ts index 8ebdae5c06e..423fbc3b36b 100644 --- a/desktop/src/shared/features/manifest.ts +++ b/desktop/src/shared/features/manifest.ts @@ -1,4 +1,5 @@ import manifestJson from "@features-manifest"; +import { protectedFeatureDefinitions } from "@protected-features"; import { z } from "zod"; import type { FeatureDefinition, FeaturesManifest } from "./types"; @@ -8,7 +9,6 @@ import type { FeatureDefinition, FeaturesManifest } from "./types"; // The app keeps working; gated UI stays hidden; nothing accidentally leaks. const FeaturePlatformSchema = z.enum(["desktop", "mobile"]); -const FeatureBuildFlagSchema = z.enum(["bestie"]); const FeatureDefinitionSchema = z.object({ id: z.string().min(1), @@ -16,7 +16,6 @@ const FeatureDefinitionSchema = z.object({ description: z.string(), defaultEnabled: z.boolean().optional(), platforms: z.array(FeaturePlatformSchema).optional(), - requiredBuildFlag: FeatureBuildFlagSchema.optional(), }); const FeaturesManifestSchema = z.object({ @@ -27,7 +26,10 @@ const FeaturesManifestSchema = z.object({ const EMPTY_MANIFEST: FeaturesManifest = { version: 1, features: [] }; function loadManifest(): FeaturesManifest { - const result = FeaturesManifestSchema.safeParse(manifestJson); + const result = FeaturesManifestSchema.safeParse({ + ...manifestJson, + features: [...manifestJson.features, ...protectedFeatureDefinitions], + }); if (!result.success) { console.warn( "[FeatureFlags] preview-features.json failed schema validation; falling back to empty manifest.", diff --git a/desktop/src/shared/features/resolveEnabled.test.mjs b/desktop/src/shared/features/resolveEnabled.test.mjs index bf08878a788..d1639f6e4f0 100644 --- a/desktop/src/shared/features/resolveEnabled.test.mjs +++ b/desktop/src/shared/features/resolveEnabled.test.mjs @@ -4,74 +4,30 @@ import { describe, it } from "node:test"; import { resolveEnabled } from "./resolveEnabled.ts"; describe("resolveEnabled (preview-only)", () => { - const workflows = { - id: "workflows", - name: "Workflows", - description: "", - }; - it("returns false by default (no override)", () => { - assert.equal(resolveEnabled(workflows, {}, true), false); + assert.equal(resolveEnabled("workflows", {}), false); }); it("returns true when user opts in", () => { - assert.equal(resolveEnabled(workflows, { workflows: true }, true), true); + assert.equal(resolveEnabled("workflows", { workflows: true }), true); }); it("uses an enabled manifest default when no override exists", () => { - assert.equal( - resolveEnabled( - { ...workflows, id: "defaultOnFeature", defaultEnabled: true }, - {}, - true, - ), - true, - ); + assert.equal(resolveEnabled("defaultOnFeature", {}, true), true); }); it("lets an explicit opt-out override an enabled default", () => { assert.equal( - resolveEnabled( - { ...workflows, id: "defaultOnFeature", defaultEnabled: true }, - { defaultOnFeature: false }, - true, - ), + resolveEnabled("defaultOnFeature", { defaultOnFeature: false }, true), false, ); }); it("returns false when user explicitly opts out", () => { - assert.equal(resolveEnabled(workflows, { workflows: false }, true), false); + assert.equal(resolveEnabled("workflows", { workflows: false }), false); }); it("ignores overrides for unrelated ids", () => { - assert.equal(resolveEnabled(workflows, { pulse: true }, true), false); - }); - - it("requires both build availability and a user opt-in", () => { - const cases = [ - { buildAvailable: false, optedIn: false, expected: false }, - { buildAvailable: false, optedIn: true, expected: false }, - { buildAvailable: true, optedIn: false, expected: false }, - { buildAvailable: true, optedIn: true, expected: true }, - ]; - - for (const { buildAvailable, optedIn, expected } of cases) { - assert.equal( - resolveEnabled( - { ...workflows, id: "bestie" }, - { bestie: optedIn }, - buildAvailable, - ), - expected, - ); - } - }); - - it("fails closed when a JavaScript caller omits build availability", () => { - assert.equal( - resolveEnabled({ ...workflows, id: "bestie" }, { bestie: true }), - false, - ); + assert.equal(resolveEnabled("workflows", { pulse: true }), false); }); }); diff --git a/desktop/src/shared/features/resolveEnabled.ts b/desktop/src/shared/features/resolveEnabled.ts index e89b06544e6..e3c544ec502 100644 --- a/desktop/src/shared/features/resolveEnabled.ts +++ b/desktop/src/shared/features/resolveEnabled.ts @@ -1,26 +1,19 @@ /** * Pure resolution logic for preview-feature visibility. - * No side effects — safe to test in isolation. + * No side effects, no imports — safe to test in isolation. * * The manifest (`preview-features.json`) lists only preview features. * Anything not in the manifest is stable and resolves true elsewhere * (see `useFeatureEnabled`). Once you're inside `resolveEnabled`, the * feature IS in the manifest — preview by definition. * - * Build availability is mandatory. Within an eligible build, an explicit user - * override wins; otherwise the feature's manifest default is used (false when - * omitted). + * An explicit user override wins; otherwise the feature's manifest default is + * used (false when omitted). */ -import type { FeatureDefinition } from "./types"; - export function resolveEnabled( - feature: FeatureDefinition, + featureId: string, overrides: Record, - buildAvailable: boolean, + defaultEnabled = false, ): boolean { - // Strict equality is intentional: JavaScript callers that omit the - // capability argument fail closed instead of recreating a build-gate side - // door outside TypeScript. - if (buildAvailable !== true) return false; - return overrides[feature.id] ?? feature.defaultEnabled ?? false; + return overrides[featureId] ?? defaultEnabled; } diff --git a/desktop/src/shared/features/runtimeBuildAvailability.ts b/desktop/src/shared/features/runtimeBuildAvailability.ts deleted file mode 100644 index 76a86891150..00000000000 --- a/desktop/src/shared/features/runtimeBuildAvailability.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - filterBuildAvailableFeatures, - isFeatureBuildAvailable, - type FeatureBuildFlags, -} from "./buildAvailability"; -import { desktopFeatures } from "./manifest"; -import { resolveEnabled } from "./resolveEnabled"; -import type { FeatureDefinition } from "./types"; - -const runtimeBuildFlags: FeatureBuildFlags = { - bestie: import.meta.env.VITE_BUZZ_BESTIE === "1", -}; - -export function isFeatureAvailableInThisBuild( - feature: Parameters[0], -): boolean { - return isFeatureBuildAvailable(feature, runtimeBuildFlags); -} - -/** Canonical preview resolution for the running build. */ -export function resolveFeatureEnabledInThisBuild( - feature: FeatureDefinition, - overrides: Record, -): boolean { - return resolveEnabled( - feature, - overrides, - isFeatureAvailableInThisBuild(feature), - ); -} - -export const availableDesktopFeatures = filterBuildAvailableFeatures( - desktopFeatures, - runtimeBuildFlags, -); diff --git a/desktop/src/shared/features/types.ts b/desktop/src/shared/features/types.ts index 4fdf07f2afd..c452cd54b4a 100644 --- a/desktop/src/shared/features/types.ts +++ b/desktop/src/shared/features/types.ts @@ -1,9 +1,6 @@ /** Platforms a feature is available on */ export type FeaturePlatform = "desktop" | "mobile"; -/** Build-time capabilities that can make a preview available to users. */ -export type FeatureBuildFlag = "bestie"; - /** * A single feature definition from the manifest. * @@ -19,8 +16,6 @@ export interface FeatureDefinition { defaultEnabled?: boolean; /** If omitted, feature is available on all platforms */ platforms?: FeaturePlatform[]; - /** If present, the build must opt in before the user can see or enable it */ - requiredBuildFlag?: FeatureBuildFlag; } /** The root manifest schema */ diff --git a/desktop/src/shared/features/useFeatureEnabled.ts b/desktop/src/shared/features/useFeatureEnabled.ts index 0326fe51db7..1be1e5e30e4 100644 --- a/desktop/src/shared/features/useFeatureEnabled.ts +++ b/desktop/src/shared/features/useFeatureEnabled.ts @@ -1,6 +1,6 @@ import { useSyncExternalStore, useCallback, useEffect } from "react"; import { getFeature } from "./manifest"; -import { resolveFeatureEnabledInThisBuild } from "./runtimeBuildAvailability"; +import { resolveEnabled } from "./resolveEnabled"; import { getOverrides, setOverride, OVERRIDES_KEY } from "./store"; type Listener = () => void; @@ -102,9 +102,11 @@ export function useFeatureEnabled(featureId: string): boolean { return true; } - return resolveFeatureEnabledInThisBuild(feature, overrides); + return resolveEnabled(featureId, overrides, feature.defaultEnabled); } +export { resolveEnabled } from "./resolveEnabled"; + /** * Hook to toggle a feature override. Returns [enabled, toggle]. */ diff --git a/desktop/test-loader-hooks.mjs b/desktop/test-loader-hooks.mjs index 06c44ae2130..d473587adf3 100644 --- a/desktop/test-loader-hooks.mjs +++ b/desktop/test-loader-hooks.mjs @@ -89,6 +89,12 @@ export function resolve(specifier, context, nextResolve) { const resolved = path.join(repoRoot, "preview-features.json"); return nextResolve(toFileSpecifier(resolved), context); } + if (specifier === "@protected-features") { + const variant = + process.env.VITE_BUZZ_BESTIE === "1" ? "internal.ts" : "public.ts"; + const resolved = path.join(srcRoot, "protectedFeatures", variant); + return nextResolve(toFileSpecifier(resolved), context); + } if (specifier === "@model-capabilities-manifest") { const resolved = path.join(repoRoot, "scripts", "model-capabilities.json"); return nextResolve(toFileSpecifier(resolved), context); diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index a2a57c66efb..feb7e7590f2 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -8,6 +8,7 @@ "paths": { "@/*": ["./src/*"], "@features-manifest": ["../preview-features.json"], + "@protected-features": ["./src/protectedFeatures/public.ts"], "@model-capabilities-manifest": ["../scripts/model-capabilities.json"] }, diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index 5a5de191204..257c8382bbb 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -1,56 +1,71 @@ import path from "node:path"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ -export default defineConfig(async () => ({ - plugins: [ - tanstackRouter({ - target: "react", - routesDirectory: "./src/app/routes", - generatedRouteTree: "./src/app/routeTree.gen.ts", - virtualRouteConfig: "./src/app/routes.ts", - quoteStyle: "double", - semicolons: true, - routeTreeFileHeader: [ - "// biome-ignore-all lint: generated by TanStack Router", - ], - }), - react(), - ], - resolve: { - alias: { - "@": "/src", - "@features-manifest": path.resolve(__dirname, "../preview-features.json"), - "@model-capabilities-manifest": path.resolve( - __dirname, - "../scripts/model-capabilities.json", - ), +export default defineConfig(async ({ mode }) => { + const modeEnv = loadEnv(mode, __dirname, ""); + const protectedFeaturesEnabled = + (process.env.VITE_BUZZ_BESTIE ?? modeEnv.VITE_BUZZ_BESTIE) === "1"; + + return { + plugins: [ + tanstackRouter({ + target: "react", + routesDirectory: "./src/app/routes", + generatedRouteTree: "./src/app/routeTree.gen.ts", + virtualRouteConfig: "./src/app/routes.ts", + quoteStyle: "double", + semicolons: true, + routeTreeFileHeader: [ + "// biome-ignore-all lint: generated by TanStack Router", + ], + }), + react(), + ], + resolve: { + alias: { + "@": "/src", + "@features-manifest": path.resolve( + __dirname, + "../preview-features.json", + ), + "@protected-features": path.resolve( + __dirname, + protectedFeaturesEnabled + ? "./src/protectedFeatures/internal.ts" + : "./src/protectedFeatures/public.ts", + ), + "@model-capabilities-manifest": path.resolve( + __dirname, + "../scripts/model-capabilities.json", + ), + }, }, - }, - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent Vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: parseInt(process.env.VITE_PORT || "1420", 10), - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: parseInt(process.env.VITE_HMR_PORT || "1421", 10), - } - : undefined, - watch: { - // 3. tell Vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: parseInt(process.env.VITE_PORT || "1420", 10), + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: parseInt(process.env.VITE_HMR_PORT || "1421", 10), + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, }, - }, -})); + }; +}); diff --git a/preview-features.json b/preview-features.json index 79460c3cad1..388f1c39b04 100644 --- a/preview-features.json +++ b/preview-features.json @@ -30,13 +30,6 @@ "name": "Agent-managed profiles", "description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy", "platforms": ["desktop"] - }, - { - "id": "bestie", - "name": "Bestie", - "description": "Try a personal agent that is always close at hand", - "platforms": ["desktop"], - "requiredBuildFlag": "bestie" } ] } From ad3647e84d375033b2e7824b7178ac6e39fa49f6 Mon Sep 17 00:00:00 2001 From: Fizz Date: Fri, 28 Aug 2026 10:43:48 -0400 Subject: [PATCH 4/9] fix(desktop): launch protected builds portably On-behalf-of: mahanti Signed-off-by: Fizz Co-authored-by: Codex --- .github/workflows/ci.yml | 36 ++++++++++++++++++- .../build-protected-feature-artifacts.mjs | 13 +++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd2949492f6..69cae1b8a55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,7 +302,7 @@ jobs: name: Desktop runs-on: ubuntu-latest timeout-minutes: 5 - needs: [changes, desktop-core, desktop-smoke-e2e] + needs: [changes, desktop-core, desktop-smoke-e2e, desktop-windows-build] if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true') permissions: contents: read @@ -317,6 +317,10 @@ jobs: echo "Desktop Smoke E2E shards finished with: ${{ needs.desktop-smoke-e2e.result }}" exit 1 fi + if [ "${{ needs.desktop-windows-build.result }}" != "success" ]; then + echo "Desktop Windows Build finished with: ${{ needs.desktop-windows-build.result }}" + exit 1 + fi echo "Desktop jobs passed" desktop-e2e-relay: @@ -1008,6 +1012,36 @@ jobs: -p git-credential-nostr \ -p git-sign-nostr + desktop-windows-build: + name: Desktop Windows Build + runs-on: windows-latest + timeout-minutes: 20 + needs: [changes] + if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true' + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 24.14.1 + package-manager-cache: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + with: + version: 11.4.0 + - name: Install desktop dependencies + shell: bash + run: pnpm install --frozen-lockfile + - name: Build both protected-feature selections + shell: pwsh + run: | + Remove-Item Env:VITE_BUZZ_BESTIE -ErrorAction SilentlyContinue + pnpm -C desktop build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $env:VITE_BUZZ_BESTIE = "1" + pnpm -C desktop build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + windows-rust: name: Windows Rust (x86_64-pc-windows-msvc) runs-on: windows-latest diff --git a/desktop/scripts/build-protected-feature-artifacts.mjs b/desktop/scripts/build-protected-feature-artifacts.mjs index 1fd17f6fdbb..1561be66c87 100644 --- a/desktop/scripts/build-protected-feature-artifacts.mjs +++ b/desktop/scripts/build-protected-feature-artifacts.mjs @@ -20,7 +20,14 @@ const scratchRoot = mkdtempSync( ); const selectedOutput = path.join(desktopRoot, "dist"); const alternateOutput = path.join(scratchRoot, "alternate"); -const viteBinary = process.platform === "win32" ? "vite.cmd" : "vite"; +const vitePackageJsonPath = fileURLToPath( + import.meta.resolve("vite/package.json"), +); +const vitePackage = JSON.parse(readFileSync(vitePackageJsonPath, "utf8")); +const viteEntrypoint = path.resolve( + path.dirname(vitePackageJsonPath), + vitePackage.bin.vite, +); function buildVariant({ internal, output }) { const env = { ...process.env }; @@ -31,8 +38,8 @@ function buildVariant({ internal, output }) { } const result = spawnSync( - viteBinary, - ["build", "--outDir", output, "--emptyOutDir"], + process.execPath, + [viteEntrypoint, "build", "--outDir", output, "--emptyOutDir"], { cwd: desktopRoot, env, From 7d882a30214abb9b2cff6a1c8035deceb9f9ba02 Mon Sep 17 00:00:00 2001 From: Fizz Date: Fri, 28 Aug 2026 10:53:09 -0400 Subject: [PATCH 5/9] fix(desktop): enable Bestie in protected builds On-behalf-of: mahanti Signed-off-by: Fizz Co-authored-by: Codex --- desktop/src/protectedFeatures/internal.ts | 1 + desktop/src/protectedFeatures/protectedFeatures.test.mjs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/src/protectedFeatures/internal.ts b/desktop/src/protectedFeatures/internal.ts index 7f9f6b551e8..d5f2133af65 100644 --- a/desktop/src/protectedFeatures/internal.ts +++ b/desktop/src/protectedFeatures/internal.ts @@ -6,6 +6,7 @@ export const protectedFeatureDefinitions: FeatureDefinition[] = [ id: "bestie", name: "Bestie", description: "Try a personal agent that is always close at hand", + defaultEnabled: true, platforms: ["desktop"], }, ]; diff --git a/desktop/src/protectedFeatures/protectedFeatures.test.mjs b/desktop/src/protectedFeatures/protectedFeatures.test.mjs index fca8cc698ad..1c584d6a2f8 100644 --- a/desktop/src/protectedFeatures/protectedFeatures.test.mjs +++ b/desktop/src/protectedFeatures/protectedFeatures.test.mjs @@ -14,6 +14,6 @@ describe("protected feature build variants", () => { internalDefinitions.map((feature) => feature.id), ["bestie"], ); - assert.equal(internalDefinitions[0]?.defaultEnabled, undefined); + assert.equal(internalDefinitions[0]?.defaultEnabled, true); }); }); From 73085c8ce6985cbb4312a7b7ddd8368952a0d2ef Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Fri, 28 Aug 2026 12:19:07 -0400 Subject: [PATCH 6/9] fix(desktop): keep Bestie experiment default off Signed-off-by: Arjun Mahanti Co-authored-by: Codex --- desktop/src/protectedFeatures/internal.ts | 1 - desktop/src/protectedFeatures/protectedFeatures.test.mjs | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/desktop/src/protectedFeatures/internal.ts b/desktop/src/protectedFeatures/internal.ts index d5f2133af65..7f9f6b551e8 100644 --- a/desktop/src/protectedFeatures/internal.ts +++ b/desktop/src/protectedFeatures/internal.ts @@ -6,7 +6,6 @@ export const protectedFeatureDefinitions: FeatureDefinition[] = [ id: "bestie", name: "Bestie", description: "Try a personal agent that is always close at hand", - defaultEnabled: true, platforms: ["desktop"], }, ]; diff --git a/desktop/src/protectedFeatures/protectedFeatures.test.mjs b/desktop/src/protectedFeatures/protectedFeatures.test.mjs index 1c584d6a2f8..20a6d469faa 100644 --- a/desktop/src/protectedFeatures/protectedFeatures.test.mjs +++ b/desktop/src/protectedFeatures/protectedFeatures.test.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; +import { resolveEnabled } from "../shared/features/resolveEnabled.ts"; import { protectedFeatureDefinitions as internalDefinitions } from "./internal.ts"; import { protectedFeatureDefinitions as publicDefinitions } from "./public.ts"; @@ -9,11 +10,13 @@ describe("protected feature build variants", () => { assert.deepEqual(publicDefinitions, []); }); - it("adds Bestie only through the internal module", () => { + it("adds Bestie as a default-off experiment only through the internal module", () => { assert.deepEqual( internalDefinitions.map((feature) => feature.id), ["bestie"], ); - assert.equal(internalDefinitions[0]?.defaultEnabled, true); + const bestie = internalDefinitions[0]; + assert.ok(bestie); + assert.equal(resolveEnabled(bestie.id, {}, bestie.defaultEnabled), false); }); }); From 8b23f345d368276ab8e0d0518d0894e615a90b02 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Fri, 28 Aug 2026 16:06:52 -0400 Subject: [PATCH 7/9] fix(desktop): pin protected artifact variants Signed-off-by: Arjun Mahanti Co-authored-by: Codex --- .../build-protected-feature-artifacts.mjs | 70 ++++++++++---- .../buildProtectedFeatureArtifacts.test.mjs | 95 +++++++++++++++++++ 2 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 desktop/src/protectedFeatures/buildProtectedFeatureArtifacts.test.mjs diff --git a/desktop/scripts/build-protected-feature-artifacts.mjs b/desktop/scripts/build-protected-feature-artifacts.mjs index 1561be66c87..879cea47671 100644 --- a/desktop/scripts/build-protected-feature-artifacts.mjs +++ b/desktop/scripts/build-protected-feature-artifacts.mjs @@ -9,17 +9,12 @@ import { import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { loadEnv } from "vite"; const desktopRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "..", ); -const selectedInternalVariant = process.env.VITE_BUZZ_BESTIE === "1"; -const scratchRoot = mkdtempSync( - path.join(tmpdir(), "buzz-protected-feature-artifacts-"), -); -const selectedOutput = path.join(desktopRoot, "dist"); -const alternateOutput = path.join(scratchRoot, "alternate"); const vitePackageJsonPath = fileURLToPath( import.meta.resolve("vite/package.json"), ); @@ -30,12 +25,12 @@ const viteEntrypoint = path.resolve( ); function buildVariant({ internal, output }) { - const env = { ...process.env }; - if (internal) { - env.VITE_BUZZ_BESTIE = "1"; - } else { - delete env.VITE_BUZZ_BESTIE; - } + const env = { + ...process.env, + // Pin both children explicitly. Deleting the OSS value lets Vite reload + // `=1` from .env.local or a mode-specific env file. + VITE_BUZZ_BESTIE: internal ? "1" : "0", + }; const result = spawnSync( process.execPath, @@ -72,7 +67,7 @@ function emittedText(root) { return chunks.join("\n"); } -function assertArtifactContract({ ossOutput, internalOutput }) { +export function assertArtifactContract({ ossOutput, internalOutput }) { const ossText = emittedText(ossOutput); const internalText = emittedText(internalOutput); const protectedContent = /\bbestie\b|chief of staff|builtin:bestie/iu; @@ -91,14 +86,25 @@ function assertArtifactContract({ ossOutput, internalOutput }) { } } -try { +/** Resolve the requested output with the same precedence used by Vite config. */ +export function selectInternalVariant({ processEnv, modeEnv }) { + return (processEnv.VITE_BUZZ_BESTIE ?? modeEnv.VITE_BUZZ_BESTIE) === "1"; +} + +/** Build and inspect both graphs, leaving the requested variant in dist. */ +export function buildArtifactMatrix({ + selectedInternalVariant, + selectedOutput, + alternateOutput, + build = buildVariant, +}) { // Build the unselected variant outside dist first, then leave the requested // variant in dist for Vite/Tauri's ordinary packaging contract. - buildVariant({ + build({ internal: !selectedInternalVariant, output: alternateOutput, }); - buildVariant({ + build({ internal: selectedInternalVariant, output: selectedOutput, }); @@ -107,9 +113,37 @@ try { ossOutput: selectedInternalVariant ? alternateOutput : selectedOutput, internalOutput: selectedInternalVariant ? selectedOutput : alternateOutput, }); +} + +function main() { + const selectedInternalVariant = selectInternalVariant({ + processEnv: process.env, + modeEnv: loadEnv("production", desktopRoot, ""), + }); + const scratchRoot = mkdtempSync( + path.join(tmpdir(), "buzz-protected-feature-artifacts-"), + ); + const selectedOutput = path.join(desktopRoot, "dist"); + const alternateOutput = path.join(scratchRoot, "alternate"); + + try { + buildArtifactMatrix({ + selectedInternalVariant, + selectedOutput, + alternateOutput, + }); + } finally { + rmSync(scratchRoot, { recursive: true, force: true }); + } + console.log( `Protected feature artifact matrix passed; dist contains the ${selectedInternalVariant ? "internal" : "OSS"} variant.`, ); -} finally { - rmSync(scratchRoot, { recursive: true, force: true }); +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + main(); } diff --git a/desktop/src/protectedFeatures/buildProtectedFeatureArtifacts.test.mjs b/desktop/src/protectedFeatures/buildProtectedFeatureArtifacts.test.mjs new file mode 100644 index 00000000000..ae330a6889e --- /dev/null +++ b/desktop/src/protectedFeatures/buildProtectedFeatureArtifacts.test.mjs @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import { + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, it } from "node:test"; +import { loadEnv } from "vite"; + +import { + buildArtifactMatrix, + selectInternalVariant, +} from "../../scripts/build-protected-feature-artifacts.mjs"; + +const INTERNAL_MARKER = "Try a personal agent that is always close at hand"; + +function fakeBuilder(calls) { + return ({ internal, output }) => { + calls.push(internal); + rmSync(output, { recursive: true, force: true }); + mkdirSync(output, { recursive: true }); + writeFileSync( + path.join(output, "index.js"), + internal ? INTERNAL_MARKER : "public desktop artifact", + ); + }; +} + +describe("protected feature production artifact selection", () => { + it("honors env-file selection while process overrides retain the requested dist", () => { + const root = mkdtempSync(path.join(tmpdir(), "buzz-protected-build-test-")); + const envRoot = path.join(root, "env"); + mkdirSync(envRoot); + writeFileSync(path.join(envRoot, ".env.local"), "VITE_BUZZ_BESTIE=1\n"); + + try { + const modeEnv = loadEnv("production", envRoot, ""); + const internalOutput = path.join(root, "internal-dist"); + const internalAlternate = path.join(root, "internal-alternate"); + const internalCalls = []; + const fileSelectedInternal = selectInternalVariant({ + processEnv: {}, + modeEnv, + }); + + assert.equal(fileSelectedInternal, true); + buildArtifactMatrix({ + selectedInternalVariant: fileSelectedInternal, + selectedOutput: internalOutput, + alternateOutput: internalAlternate, + build: fakeBuilder(internalCalls), + }); + assert.deepEqual(internalCalls, [false, true]); + assert.match( + readFileSync(path.join(internalOutput, "index.js"), "utf8"), + /personal agent/u, + ); + assert.doesNotMatch( + readFileSync(path.join(internalAlternate, "index.js"), "utf8"), + /personal agent/u, + ); + + const ossOutput = path.join(root, "oss-dist"); + const ossAlternate = path.join(root, "oss-alternate"); + const ossCalls = []; + const processSelectedOss = selectInternalVariant({ + processEnv: { VITE_BUZZ_BESTIE: "0" }, + modeEnv, + }); + + assert.equal(processSelectedOss, false); + buildArtifactMatrix({ + selectedInternalVariant: processSelectedOss, + selectedOutput: ossOutput, + alternateOutput: ossAlternate, + build: fakeBuilder(ossCalls), + }); + assert.deepEqual(ossCalls, [true, false]); + assert.doesNotMatch( + readFileSync(path.join(ossOutput, "index.js"), "utf8"), + /personal agent/u, + ); + assert.match( + readFileSync(path.join(ossAlternate, "index.js"), "utf8"), + /personal agent/u, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); From 80ee86051ed0206813666aa89adc40bb23b52a09 Mon Sep 17 00:00:00 2001 From: Fizz Date: Mon, 31 Aug 2026 09:52:50 -0400 Subject: [PATCH 8/9] fix(desktop): isolate concurrent package artifacts On-behalf-of: mahanti Signed-off-by: Fizz --- desktop/package.json | 4 +- .../build-protected-feature-artifacts.mjs | 4 +- desktop/scripts/tauri-command.mjs | 59 ++++++++++++++++ .../protectedFeatures/tauriCommand.test.mjs | 67 +++++++++++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 desktop/scripts/tauri-command.mjs create mode 100644 desktop/src/protectedFeatures/tauriCommand.test.mjs diff --git a/desktop/package.json b/desktop/package.json index e810d2bc284..14db248a134 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -16,13 +16,13 @@ "format": "biome format --write .", "test": "node --import ./test-loader.mjs --experimental-strip-types --test \"src/**/*.test.mjs\"", "preview": "vite preview", - "tauri": "tauri", + "tauri": "node ./scripts/tauri-command.mjs", "test:e2e": "pnpm build:e2e && playwright test", "test:e2e:smoke": "pnpm build:e2e && playwright test --project=smoke", "test:e2e:integration": "pnpm build:e2e && playwright test --project=integration", "test:e2e:release-smoke": "pnpm build:e2e && playwright test --config=playwright.release-smoke.config.ts", "test:e2e:report": "playwright show-report", - "tauri:build": "tauri build" + "tauri:build": "node ./scripts/tauri-command.mjs build" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/desktop/scripts/build-protected-feature-artifacts.mjs b/desktop/scripts/build-protected-feature-artifacts.mjs index 879cea47671..3de4830ceeb 100644 --- a/desktop/scripts/build-protected-feature-artifacts.mjs +++ b/desktop/scripts/build-protected-feature-artifacts.mjs @@ -123,7 +123,9 @@ function main() { const scratchRoot = mkdtempSync( path.join(tmpdir(), "buzz-protected-feature-artifacts-"), ); - const selectedOutput = path.join(desktopRoot, "dist"); + const selectedOutput = process.env.BUZZ_PROTECTED_BUILD_OUTPUT + ? path.resolve(process.env.BUZZ_PROTECTED_BUILD_OUTPUT) + : path.join(desktopRoot, "dist"); const alternateOutput = path.join(scratchRoot, "alternate"); try { diff --git a/desktop/scripts/tauri-command.mjs b/desktop/scripts/tauri-command.mjs new file mode 100644 index 00000000000..d60ac007499 --- /dev/null +++ b/desktop/scripts/tauri-command.mjs @@ -0,0 +1,59 @@ +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const desktopRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +const tauriPackageJsonPath = fileURLToPath( + import.meta.resolve("@tauri-apps/cli/package.json"), +); +const tauriPackage = JSON.parse(readFileSync(tauriPackageJsonPath, "utf8")); +const defaultTauriEntrypoint = path.resolve( + path.dirname(tauriPackageJsonPath), + tauriPackage.bin.tauri, +); + +function runTauri(args, options = {}) { + const entrypoint = + process.env.BUZZ_TAURI_CLI_ENTRYPOINT ?? defaultTauriEntrypoint; + const result = spawnSync(process.execPath, [entrypoint, ...args], { + cwd: desktopRoot, + env: { ...process.env, ...options.env }, + stdio: "inherit", + }); + if (result.error) throw result.error; + return result.status ?? 1; +} + +export function runTauriCommand(args) { + if (args[0] !== "build") return runTauri(args); + + // Tauri runs beforeBuildCommand and then consumes frontendDist. Give the + // entire invocation a private directory so concurrent OSS/internal packages + // cannot replace one another's assets between those two operations. + const invocationRoot = mkdtempSync( + path.join(tmpdir(), "buzz-tauri-package-assets-"), + ); + const frontendDist = path.join(invocationRoot, "dist"); + const outputOverride = JSON.stringify({ build: { frontendDist } }); + + try { + return runTauri([...args, "--config", outputOverride], { + env: { BUZZ_PROTECTED_BUILD_OUTPUT: frontendDist }, + }); + } finally { + rmSync(invocationRoot, { recursive: true, force: true }); + } +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + process.exitCode = runTauriCommand(process.argv.slice(2)); +} diff --git a/desktop/src/protectedFeatures/tauriCommand.test.mjs b/desktop/src/protectedFeatures/tauriCommand.test.mjs new file mode 100644 index 00000000000..c002787d697 --- /dev/null +++ b/desktop/src/protectedFeatures/tauriCommand.test.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; + +const desktopRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../..", +); +const wrapper = path.join(desktopRoot, "scripts/tauri-command.mjs"); +const fakeCli = path.join(tmpdir(), `buzz-fake-tauri-${process.pid}.mjs`); + +writeFileSync( + fakeCli, + `import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +const args = process.argv.slice(2); +const configIndex = args.lastIndexOf("--config"); +const override = JSON.parse(args[configIndex + 1]); +const output = override.build.frontendDist; +mkdirSync(output, { recursive: true }); +writeFileSync(path.join(output, "variant.txt"), process.env.VITE_BUZZ_BESTIE); +await new Promise((resolve) => setTimeout(resolve, 100)); +const observed = readFileSync(path.join(output, "variant.txt"), "utf8"); +writeFileSync(process.env.BUZZ_TEST_RESULT, JSON.stringify({ output, observed })); +`, +); + +function packageVariant(variant, result) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [wrapper, "build"], { + cwd: desktopRoot, + env: { + ...process.env, + BUZZ_TAURI_CLI_ENTRYPOINT: fakeCli, + BUZZ_TEST_RESULT: result, + VITE_BUZZ_BESTIE: variant, + }, + stdio: "inherit", + }); + child.once("error", reject); + child.once("exit", (code) => + code === 0 ? resolve() : reject(new Error(`wrapper exited ${code}`)), + ); + }); +} + +test("opposite Tauri package variants own private frontend artifacts", async () => { + const resultRoot = path.join(tmpdir(), `buzz-tauri-results-${process.pid}`); + mkdirSync(resultRoot, { recursive: true }); + const ossResult = path.join(resultRoot, "oss.json"); + const internalResult = path.join(resultRoot, "internal.json"); + + await Promise.all([ + packageVariant("0", ossResult), + packageVariant("1", internalResult), + ]); + + const oss = JSON.parse(readFileSync(ossResult, "utf8")); + const internal = JSON.parse(readFileSync(internalResult, "utf8")); + assert.equal(oss.observed, "0"); + assert.equal(internal.observed, "1"); + assert.notEqual(oss.output, internal.output); +}); From 0ae4abab4533fed78ba6960949af5768b5e33d12 Mon Sep 17 00:00:00 2001 From: Fizz Date: Mon, 31 Aug 2026 15:51:46 -0400 Subject: [PATCH 9/9] fix(desktop): preserve Tauri runner arguments On-behalf-of: mahanti Signed-off-by: Fizz --- desktop/scripts/tauri-command.mjs | 6 ++- .../protectedFeatures/tauriCommand.test.mjs | 52 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/desktop/scripts/tauri-command.mjs b/desktop/scripts/tauri-command.mjs index d60ac007499..dc1d8691e96 100644 --- a/desktop/scripts/tauri-command.mjs +++ b/desktop/scripts/tauri-command.mjs @@ -43,7 +43,11 @@ export function runTauriCommand(args) { const outputOverride = JSON.stringify({ build: { frontendDist } }); try { - return runTauri([...args, "--config", outputOverride], { + const delimiterIndex = args.indexOf("--"); + const configIndex = delimiterIndex === -1 ? args.length : delimiterIndex; + const tauriArgs = [...args]; + tauriArgs.splice(configIndex, 0, "--config", outputOverride); + return runTauri(tauriArgs, { env: { BUZZ_PROTECTED_BUILD_OUTPUT: frontendDist }, }); } finally { diff --git a/desktop/src/protectedFeatures/tauriCommand.test.mjs b/desktop/src/protectedFeatures/tauriCommand.test.mjs index c002787d697..e3e1532af45 100644 --- a/desktop/src/protectedFeatures/tauriCommand.test.mjs +++ b/desktop/src/protectedFeatures/tauriCommand.test.mjs @@ -25,22 +25,29 @@ mkdirSync(output, { recursive: true }); writeFileSync(path.join(output, "variant.txt"), process.env.VITE_BUZZ_BESTIE); await new Promise((resolve) => setTimeout(resolve, 100)); const observed = readFileSync(path.join(output, "variant.txt"), "utf8"); -writeFileSync(process.env.BUZZ_TEST_RESULT, JSON.stringify({ output, observed })); +writeFileSync( + process.env.BUZZ_TEST_RESULT, + JSON.stringify({ args, output, observed }), +); `, ); -function packageVariant(variant, result) { +function packageVariant(variant, result, runnerArguments = []) { return new Promise((resolve, reject) => { - const child = spawn(process.execPath, [wrapper, "build"], { - cwd: desktopRoot, - env: { - ...process.env, - BUZZ_TAURI_CLI_ENTRYPOINT: fakeCli, - BUZZ_TEST_RESULT: result, - VITE_BUZZ_BESTIE: variant, + const child = spawn( + process.execPath, + [wrapper, "build", ...runnerArguments], + { + cwd: desktopRoot, + env: { + ...process.env, + BUZZ_TAURI_CLI_ENTRYPOINT: fakeCli, + BUZZ_TEST_RESULT: result, + VITE_BUZZ_BESTIE: variant, + }, + stdio: "inherit", }, - stdio: "inherit", - }); + ); child.once("error", reject); child.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`wrapper exited ${code}`)), @@ -65,3 +72,26 @@ test("opposite Tauri package variants own private frontend artifacts", async () assert.equal(internal.observed, "1"); assert.notEqual(oss.output, internal.output); }); + +test("private config precedes Cargo runner arguments", async () => { + const result = path.join( + tmpdir(), + `buzz-tauri-runner-arguments-${process.pid}.json`, + ); + await packageVariant("0", result, [ + "--config", + '{"bundle":{"active":false}}', + "--", + "--locked", + ]); + + const invocation = JSON.parse(readFileSync(result, "utf8")); + const delimiterIndex = invocation.args.indexOf("--"); + const privateConfigIndex = invocation.args.lastIndexOf("--config"); + assert.ok(privateConfigIndex < delimiterIndex); + assert.equal(invocation.args[delimiterIndex + 1], "--locked"); + assert.equal( + JSON.parse(invocation.args[privateConfigIndex + 1]).build.frontendDist, + invocation.output, + ); +});