From af8ac2450604cf9df6f02b30d5f7de75d8bce3d2 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Fri, 8 May 2026 22:47:02 -0700 Subject: [PATCH] feat(cli): expand fast_provision experiment to images + docker + sandbox (#3398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): expand fast_provision experiment to images + docker + sandbox Builds on the existing PostHog `fast_provision` flag (already wired via shared/feature-flags.ts). The `test` variant now bundles the full provisioning-speed stack rather than images alone: - 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) Users who explicitly pass --beta or --fast still take precedence and skip the experiment bucket. Exposure events are still captured for both variants so PostHog can compute conversion across the broader bundle. Bumps CLI to 1.0.39. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor(cli): extract fast_provision bundle helper, align --fast Addresses review feedback on the fast_provision experiment expansion: - Align `--fast` with the experiment `test` variant. Previously --fast pushed [tarball, images, parallel, docker] (no sandbox) while the silent A/B pushed [images, docker, sandbox]. Add `sandbox` to --fast so the explicit user opt-in exercises the same surface as the silent experiment, plus the speed-ups outside the experiment scope. - Extract the experiment bundle into a pure `expandFastProvisionVariant` helper in shared/feature-flags.ts so the bundle composition is testable in isolation from main() arg parsing. Drop-in replacement in index.ts; behavior unchanged for the test variant. - Add unit tests pinning the bundle: test variant -> [images, docker, sandbox], control -> [], unknown variants -> [] (fail-closed). These guard against silent drift when someone tweaks the list later. - Bump CLI 1.0.39 -> 1.0.40 per the version-on-every-cli-change rule. Verified: bunx biome check src/ clean, bun test 2188/2188 pass. Co-Authored-By: Claude Opus 4.7 (1M context) * revert(cli): leave --fast untouched, experiment owns its own bundle My previous commit (8f87330) added `sandbox` to --fast to align it with the experiment test variant. That was wrong — --fast is unrelated to the fast_provision experiment, has its own meaning, and shouldn't inherit changes from the experiment bundle. - Restore --fast to [tarball, images, parallel, docker] (its prior set). - Drop the cross-reference comments tying --fast to the experiment. - Keep expandFastProvisionVariant() and its unit tests intact — the experiment bundle still lives in feature-flags.ts as a testable pure helper, just no longer claims alignment with --fast. - Bump CLI 1.0.40 -> 1.0.41. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Claude --- packages/cli/package.json | 2 +- .../cli/src/__tests__/feature-flags.test.ts | 25 +++++++++++++++++++ packages/cli/src/index.ts | 13 ++++++---- packages/cli/src/shared/feature-flags.ts | 20 +++++++++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index b2661d53..60c6eab7 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 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 2d7bdded..7fcfabe8 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 48435743..871e6249 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;