diff --git a/packages/cli/package.json b/packages/cli/package.json index b2661d538..60c6eab74 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.0.38", + "version": "1.0.41", "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 36890682d..4efdf2b03 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 2d7bdded6..7fcfabe87 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"; @@ -969,13 +969,16 @@ async function main(): Promise { // fast_provision experiment: if the user did NOT pass --beta or --fast, // bucket them on the PostHog `fast_provision` flag. The `test` variant - // turns on images by default; control behaves as before. + // turns on images + docker + sandbox by default; control behaves as before. + // - images: pre-built DO marketplace images (cloud-side faster boot) + // - 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"); - } + 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 484357435..871e62495 100644 --- a/packages/cli/src/shared/feature-flags.ts +++ b/packages/cli/src/shared/feature-flags.ts @@ -201,6 +201,26 @@ 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. This is the experiment surface only — + * unrelated to `--fast`, which is its own user-facing flag and stays as-is. + */ +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;