mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
feat(desktop): add protected-build Bestie experiment (#6902)
## Summary Introduces a protected-build boundary for the default-off Bestie experiment without adding any Bestie product surface. - Official OSS builds select an empty protected-feature module and emit no Bestie/Chief metadata or implementation content. - Protected internal builds select a separate module graph containing the Bestie experiment definition. - Within an internal build, Bestie remains disabled until the user opts in under Settings → Experiments. - The production build runs an artifact matrix and fails if OSS output contains protected content or internal output lacks the Bestie manifest. ## Build contract | Build variant | User opt-in | Result | | --- | --- | --- | | Official OSS | Any/forged | Bestie absent from the compiled artifact | | Protected internal | Off | Bestie available but disabled | | Protected internal | On | Bestie enabled | The companion protected-release change is squareup/buzz-releases#91. It sets `VITE_BUZZ_BESTIE=1`, requires that exact value, forwards it into the signed macOS build, and asserts the contract in release validation. ## Why this is separate This gives later Bestie PRs one build-selected import seam. Protected implementations must be reachable only from the internal module so they never enter the official OSS module graph. ## Non-goals - No Bestie persona or provisioning - No sidebar, app-chrome, or message-toolbar UI - No entitlement or secrecy claim: the source is public; this boundary controls official Block artifacts ## Verification - Exact commit `523cf49ced03cba9be43836a54d6aa5d6923cc82` - Full `just ci`: 5,673 Desktop tests, 2,773 Tauri tests, 1,860 mobile tests, Rust/Tauri/web/mobile static checks and builds - OSS production artifact: scanner confirms no `Bestie`, `Chief of Staff`, or `builtin:bestie` content - Internal production artifact: scanner confirms the protected Bestie manifest is emitted - Both build orders verified; `dist` retains the requested variant for Vite/Tauri packaging --------- Signed-off-by: Arjun Mahanti <arjun@squareup.com> Signed-off-by: Fizz <fizz@buzz.local> Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Fizz <fizz@buzz.local> Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -274,6 +274,10 @@ 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
|
||||
|
||||
# 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 ─────────────────────────────────────────────────
|
||||
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
|
||||
# BUZZ_ACP_SUBSCRIBE=mentions
|
||||
|
||||
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@@ -304,7 +304,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
|
||||
@@ -319,6 +319,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:
|
||||
@@ -1133,6 +1137,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
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
151
desktop/scripts/build-protected-feature-artifacts.mjs
Normal file
151
desktop/scripts/build-protected-feature-artifacts.mjs
Normal file
@@ -0,0 +1,151 @@
|
||||
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";
|
||||
import { loadEnv } from "vite";
|
||||
|
||||
const desktopRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
);
|
||||
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,
|
||||
// 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,
|
||||
[viteEntrypoint, "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");
|
||||
}
|
||||
|
||||
export 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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.
|
||||
build({
|
||||
internal: !selectedInternalVariant,
|
||||
output: alternateOutput,
|
||||
});
|
||||
build({
|
||||
internal: selectedInternalVariant,
|
||||
output: selectedOutput,
|
||||
});
|
||||
|
||||
assertArtifactContract({
|
||||
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 = 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 {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
main();
|
||||
}
|
||||
63
desktop/scripts/tauri-command.mjs
Normal file
63
desktop/scripts/tauri-command.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
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 {
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
canManageCommunityMembers,
|
||||
shouldWarnMissingMembershipSnapshot,
|
||||
} from "@/shared/api/relayMembers";
|
||||
import { getFeature } from "@/shared/features/manifest";
|
||||
import {
|
||||
getFeature,
|
||||
resolveEnabled,
|
||||
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 &&
|
||||
!resolveEnabled(s.featureGate, featureState, feature.defaultEnabled)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
11
desktop/src/protectedFeatures/internal.ts
Normal file
11
desktop/src/protectedFeatures/internal.ts
Normal file
@@ -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"],
|
||||
},
|
||||
];
|
||||
22
desktop/src/protectedFeatures/protectedFeatures.test.mjs
Normal file
22
desktop/src/protectedFeatures/protectedFeatures.test.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
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";
|
||||
|
||||
describe("protected feature build variants", () => {
|
||||
it("keeps protected definitions out of the OSS module", () => {
|
||||
assert.deepEqual(publicDefinitions, []);
|
||||
});
|
||||
|
||||
it("adds Bestie as a default-off experiment only through the internal module", () => {
|
||||
assert.deepEqual(
|
||||
internalDefinitions.map((feature) => feature.id),
|
||||
["bestie"],
|
||||
);
|
||||
const bestie = internalDefinitions[0];
|
||||
assert.ok(bestie);
|
||||
assert.equal(resolveEnabled(bestie.id, {}, bestie.defaultEnabled), false);
|
||||
});
|
||||
});
|
||||
7
desktop/src/protectedFeatures/public.ts
Normal file
7
desktop/src/protectedFeatures/public.ts
Normal file
@@ -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[] = [];
|
||||
97
desktop/src/protectedFeatures/tauriCommand.test.mjs
Normal file
97
desktop/src/protectedFeatures/tauriCommand.test.mjs
Normal file
@@ -0,0 +1,97 @@
|
||||
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({ args, output, observed }),
|
||||
);
|
||||
`,
|
||||
);
|
||||
|
||||
function packageVariant(variant, result, runnerArguments = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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",
|
||||
},
|
||||
);
|
||||
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);
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import manifestJson from "@features-manifest";
|
||||
import { protectedFeatureDefinitions } from "@protected-features";
|
||||
import { z } from "zod";
|
||||
import type { FeatureDefinition, FeaturesManifest } from "./types";
|
||||
|
||||
@@ -25,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.",
|
||||
|
||||
@@ -105,6 +105,8 @@ export function useFeatureEnabled(featureId: string): boolean {
|
||||
return resolveEnabled(featureId, overrides, feature.defaultEnabled);
|
||||
}
|
||||
|
||||
export { resolveEnabled } from "./resolveEnabled";
|
||||
|
||||
/**
|
||||
* Hook to toggle a feature override. Returns [enabled, toggle].
|
||||
*/
|
||||
@@ -157,5 +159,3 @@ export function usePreviewFeatureWarning(featureId: string): void {
|
||||
};
|
||||
}, [feature, enabled]);
|
||||
}
|
||||
|
||||
export { resolveEnabled } from "./resolveEnabled";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@features-manifest": ["../preview-features.json"],
|
||||
"@protected-features": ["./src/protectedFeatures/public.ts"],
|
||||
"@model-capabilities-manifest": ["../scripts/model-capabilities.json"]
|
||||
},
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
// 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/**"],
|
||||
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/**"],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user