diff --git a/packages/cli/package.json b/packages/cli/package.json index a5e6a159..d3e80d32 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.0.39", + "version": "1.0.40", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/feature-flags.test.ts b/packages/cli/src/__tests__/feature-flags.test.ts index 36890682..4efdf2b0 100644 --- a/packages/cli/src/__tests__/feature-flags.test.ts +++ b/packages/cli/src/__tests__/feature-flags.test.ts @@ -6,6 +6,7 @@ import { dirname, join } from "node:path"; import { _awaitBackgroundRefreshForTest, _resetFeatureFlagsForTest, + expandFastProvisionVariant, getFeatureFlag, initFeatureFlags, } from "../shared/feature-flags.js"; @@ -225,4 +226,28 @@ describe("feature flags", () => { expect(getFeatureFlag("unknown", "default")).toBe("default"); }); }); + + describe("expandFastProvisionVariant", () => { + // These tests pin the experiment bundle composition so that tweaking the + // list in feature-flags.ts forces an explicit test update — drifting the + // bundle silently is the failure mode we're guarding against. + + it("returns the full provisioning-speed bundle for the test variant", () => { + expect(expandFastProvisionVariant("test")).toEqual([ + "images", + "docker", + "sandbox", + ]); + }); + + it("returns an empty bundle for the control variant", () => { + expect(expandFastProvisionVariant("control")).toEqual([]); + }); + + it("returns an empty bundle for unknown variants (fail-closed)", () => { + expect(expandFastProvisionVariant("")).toEqual([]); + expect(expandFastProvisionVariant("rollout")).toEqual([]); + expect(expandFastProvisionVariant("totally-made-up")).toEqual([]); + }); + }); }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8f83c5b5..dfce5810 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -40,7 +40,7 @@ import { } from "./commands/index.js"; import { expandEqualsFlags, findUnknownFlag } from "./flags.js"; import { agentKeys, cloudKeys, getCacheAge, loadManifest } from "./manifest.js"; -import { getFeatureFlag, initFeatureFlags } from "./shared/feature-flags.js"; +import { expandFastProvisionVariant, getFeatureFlag, initFeatureFlags } from "./shared/feature-flags.js"; import { getInstallRefPath } from "./shared/paths.js"; import { asyncTryCatch, asyncTryCatchIf, isFileError, isNetworkError, tryCatch, tryCatchIf } from "./shared/result.js"; import { captureError, initTelemetry, setTelemetryContext } from "./shared/telemetry.js"; @@ -962,9 +962,14 @@ async function main(): Promise { process.exit(1); } } - // --fast implies all beta features + // --fast: explicit user opt-in to the full provisioning-speed stack. Kept + // aligned with the `fast_provision` experiment's `test` variant (images, + // docker, sandbox) so the explicit flag and the silent A/B exercise the same + // surface — plus tarball + parallel, which are speed-ups outside the + // experiment scope. If you change either bundle, update the other or the + // alignment comment in expandFastProvisionVariant(). if (process.env.SPAWN_FAST === "1") { - betaFeatures.push("tarball", "images", "parallel", "docker"); + betaFeatures.push("tarball", "images", "parallel", "docker", "sandbox"); } // fast_provision experiment: if the user did NOT pass --beta or --fast, @@ -974,11 +979,11 @@ async function main(): Promise { // - docker: Docker CE host image on Hetzner/GCP (cloud-side faster boot) // - sandbox: local agents run in a Docker container (local-side faster boot) // Exposure is captured for both variants so PostHog can compute conversion. + // Bundle composition lives in expandFastProvisionVariant() for unit testing. if (!userOptedIntoBeta) { const variant = getFeatureFlag("fast_provision", "control"); - if (variant === "test") { - betaFeatures.push("images", "docker", "sandbox"); - } + const variantStr = isString(variant) ? variant : "control"; + betaFeatures.push(...expandFastProvisionVariant(variantStr)); } if (betaFeatures.length > 0) { diff --git a/packages/cli/src/shared/feature-flags.ts b/packages/cli/src/shared/feature-flags.ts index 48435743..e665e73c 100644 --- a/packages/cli/src/shared/feature-flags.ts +++ b/packages/cli/src/shared/feature-flags.ts @@ -201,6 +201,28 @@ export function getFeatureFlag(key: string, fallback return value; } +/** + * Beta features bundled by the `fast_provision` PostHog experiment for a given + * variant. Returns an empty array for `control` or any unknown variant — the + * caller is responsible for de-duping against features the user already passed. + * + * Kept as a pure, named export so the bundle composition is testable in + * isolation from `main()` arg parsing. Keep this in sync with the `--fast` + * branch in `index.ts` — both opt the user into the same speed-ups; they only + * differ on `tarball` + `parallel`, which `--fast` adds (these aren't part of + * the experiment because they're already on by default in most paths). + */ +export function expandFastProvisionVariant(variant: string): readonly string[] { + if (variant === "test") { + return [ + "images", + "docker", + "sandbox", + ]; + } + return []; +} + /** Test-only: reset module state between tests. */ export function _resetFeatureFlagsForTest(): void { _flags = null;