mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
feat(desktop): add build-gated Bestie experiment
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<section className="min-w-0" data-testid="settings-experimental">
|
||||
|
||||
43
desktop/src/shared/features/buildAvailability.test.mjs
Normal file
43
desktop/src/shared/features/buildAvailability.test.mjs
Normal file
@@ -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],
|
||||
);
|
||||
});
|
||||
});
|
||||
24
desktop/src/shared/features/buildAvailability.ts
Normal file
24
desktop/src/shared/features/buildAvailability.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { FeatureBuildFlag, FeatureDefinition } from "./types";
|
||||
|
||||
export type FeatureBuildFlags = Record<FeatureBuildFlag, boolean>;
|
||||
|
||||
/**
|
||||
* 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),
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ export function resolveEnabled(
|
||||
featureId: string,
|
||||
overrides: Record<string, boolean>,
|
||||
defaultEnabled = false,
|
||||
buildAvailable = true,
|
||||
): boolean {
|
||||
if (!buildAvailable) return false;
|
||||
return overrides[featureId] ?? defaultEnabled;
|
||||
}
|
||||
|
||||
21
desktop/src/shared/features/runtimeBuildAvailability.ts
Normal file
21
desktop/src/shared/features/runtimeBuildAvailability.ts
Normal file
@@ -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<typeof isFeatureBuildAvailable>[0],
|
||||
): boolean {
|
||||
return isFeatureBuildAvailable(feature, runtimeBuildFlags);
|
||||
}
|
||||
|
||||
export const availableDesktopFeatures = filterBuildAvailableFeatures(
|
||||
desktopFeatures,
|
||||
runtimeBuildFlags,
|
||||
);
|
||||
@@ -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 */
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user