mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
chore(updater): delete the rollout-flag module nothing reads (#30211)
The nudge request parsed and stored a rollout block, but no build ever read a flag from it. Drops the module, its call site, its tests, and the e2e env that fed it.
This commit is contained in:
@@ -5991,7 +5991,6 @@
|
||||
"src/main/updater.quit-and-install.test.ts": 4994,
|
||||
"src/main/updater.startup-scheduling.test.ts": 3378,
|
||||
"src/main/updater/mac-browser-sidecar-recovery.test.ts": 81,
|
||||
"src/main/updater/rollout-flags.test.ts": 285,
|
||||
"src/main/updater/serve-supervisor-install-park.test.ts": 151,
|
||||
"src/main/updater/shipit-leftover-recovery.test.ts": 103,
|
||||
"src/main/usage-worktree-canonicalizer.test.ts": 32,
|
||||
|
||||
@@ -9,12 +9,10 @@ vi.mock('electron', () => ({
|
||||
}))
|
||||
|
||||
import { fetchNudge, versionMatchesRange, shouldApplyNudge } from './updater-nudge'
|
||||
import { isRolloutFlagActive, resetRolloutConfigForTests } from './updater/rollout-flags'
|
||||
|
||||
describe('updater-nudge', () => {
|
||||
beforeEach(() => {
|
||||
netFetchMock.mockReset()
|
||||
resetRolloutConfigForTests()
|
||||
})
|
||||
|
||||
describe('fetchNudge', () => {
|
||||
@@ -128,40 +126,6 @@ describe('updater-nudge', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('rollout block on the same request', () => {
|
||||
const install = { appVersion: '1.5.0', installId: 'install-a' }
|
||||
const killSwitch = { version: 1, flags: { 'pinned-relay-default': { state: 'on' } } }
|
||||
|
||||
it('records the block without changing the nudge it rides on', async () => {
|
||||
netFetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ id: 'campaign-1', minVersion: '1.0.0', rollout: killSwitch })
|
||||
})
|
||||
|
||||
await expect(fetchNudge()).resolves.toEqual({ id: 'campaign-1', minVersion: '1.0.0' })
|
||||
expect(isRolloutFlagActive('pinned-relay-default', install)).toBe(true)
|
||||
expect(netFetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reads a block even when there is no nudge campaign', async () => {
|
||||
netFetchMock.mockResolvedValue({ ok: true, json: async () => ({ rollout: killSwitch }) })
|
||||
|
||||
await expect(fetchNudge()).resolves.toBeNull()
|
||||
expect(isRolloutFlagActive('pinned-relay-default', install)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the last block when a later request fails', async () => {
|
||||
netFetchMock.mockResolvedValueOnce({ ok: true, json: async () => ({ rollout: killSwitch }) })
|
||||
await fetchNudge()
|
||||
netFetchMock.mockResolvedValueOnce({ ok: false })
|
||||
await fetchNudge()
|
||||
netFetchMock.mockRejectedValueOnce(new Error('network down'))
|
||||
await fetchNudge()
|
||||
|
||||
expect(isRolloutFlagActive('pinned-relay-default', install)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('versionMatchesRange', () => {
|
||||
it('bounded range match', () => {
|
||||
expect(versionMatchesRange('1.1.5', { minVersion: '1.1.0', maxVersion: '1.1.19' })).toBe(true)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { net } from 'electron'
|
||||
import { compareVersions, isValidVersion } from './updater-fallback'
|
||||
import { parseRolloutConfig, recordRolloutConfig } from './updater/rollout-flags'
|
||||
|
||||
export type NudgeConfig = {
|
||||
id: string
|
||||
@@ -18,8 +17,6 @@ export async function fetchNudge(): Promise<NudgeConfig | null> {
|
||||
}
|
||||
|
||||
const json: unknown = await res.json()
|
||||
// Why here: the rollout block rides on this same request; older builds ignore the key.
|
||||
recordRolloutConfig(parseRolloutConfig(json))
|
||||
if (!json || typeof json !== 'object' || Array.isArray(json)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup'
|
||||
import {
|
||||
BAKED_ROLLOUT_FLAGS,
|
||||
ROLLOUT_FLAG_NAMES,
|
||||
isRolloutFlagActive,
|
||||
parseRolloutConfig,
|
||||
readE2ERolloutConfigOverride,
|
||||
recordRolloutConfig,
|
||||
resetRolloutConfigForTests,
|
||||
resolveRolloutFlag,
|
||||
rolloutBucket,
|
||||
type RolloutConfig
|
||||
} from './rollout-flags'
|
||||
|
||||
const install = { appVersion: '1.5.0', installId: 'install-a' }
|
||||
|
||||
function campaign(flags: Record<string, unknown>, version: unknown = 1): unknown {
|
||||
return { id: 'campaign-1', minVersion: '1.0.0', rollout: { version, flags } }
|
||||
}
|
||||
|
||||
describe('rollout flags', () => {
|
||||
beforeEach(() => {
|
||||
resetRolloutConfigForTests()
|
||||
})
|
||||
|
||||
it('ships every flip inactive', () => {
|
||||
expect(ROLLOUT_FLAG_NAMES.every((name) => BAKED_ROLLOUT_FLAGS[name] === false)).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves to the baked value when the block is absent, invalid or a newer version', () => {
|
||||
for (const payload of [
|
||||
null,
|
||||
'nudge',
|
||||
[],
|
||||
{ id: 'campaign-1', minVersion: '1.0.0' },
|
||||
{ rollout: 'on' },
|
||||
{ rollout: { version: 1 } },
|
||||
campaign({ 'pinned-relay-default': { state: 'on' } }, 2)
|
||||
]) {
|
||||
const config = parseRolloutConfig(payload)
|
||||
expect(config).toBeNull()
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...install, config })).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('drops only the entries it cannot read, and ignores flags it does not know', () => {
|
||||
const config = parseRolloutConfig(
|
||||
campaign({
|
||||
'pinned-relay-default': { state: 'on', percent: 100, futureField: true },
|
||||
'managed-servers-visible': { state: 'sideways' },
|
||||
'serve-on-orcad-default': { state: 'on', percent: 140 },
|
||||
'legacy-relay-dir-sweep': { state: 'on', minVersion: '2.0.0', maxVersion: '1.0.0' },
|
||||
'flag-from-a-newer-build': { state: 'on' }
|
||||
})
|
||||
)
|
||||
expect(config).toEqual({ 'pinned-relay-default': { state: 'on', percent: 100 } })
|
||||
})
|
||||
|
||||
it('keeps the kill switch on and lets "default" defer to the build', () => {
|
||||
const config: RolloutConfig = {
|
||||
'pinned-relay-default': { state: 'off' },
|
||||
'managed-servers-visible': { state: 'default' }
|
||||
}
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...install, config })).toBe(false)
|
||||
expect(resolveRolloutFlag('managed-servers-visible', { ...install, config })).toBe(false)
|
||||
expect(resolveRolloutFlag('serve-on-orcad-default', { ...install, config })).toBe(false)
|
||||
})
|
||||
|
||||
it('applies an entry only inside its version range', () => {
|
||||
const config: RolloutConfig = {
|
||||
'pinned-relay-default': { state: 'on', minVersion: '1.5.0', maxVersion: '1.5.9' }
|
||||
}
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...install, config })).toBe(true)
|
||||
for (const appVersion of ['1.4.9', '1.6.0', 'not-a-version']) {
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...install, appVersion, config })).toBe(
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('admits about the requested share of installs, stably per install and flag', () => {
|
||||
const config: RolloutConfig = { 'pinned-relay-default': { state: 'on', percent: 25 } }
|
||||
const ids = Array.from({ length: 4000 }, (_, index) => `install-${index}`)
|
||||
const admitted = ids.filter((installId) =>
|
||||
resolveRolloutFlag('pinned-relay-default', { ...install, installId, config })
|
||||
)
|
||||
expect(admitted.length / ids.length).toBeGreaterThan(0.22)
|
||||
expect(admitted.length / ids.length).toBeLessThan(0.28)
|
||||
expect(rolloutBucket('install-a', 'pinned-relay-default')).toBe(
|
||||
rolloutBucket('install-a', 'pinned-relay-default')
|
||||
)
|
||||
// Why: each flip draws its own cohort, so one 25% step doesn't pick the same users every time.
|
||||
const sameForOtherFlag = ids.filter(
|
||||
(installId) =>
|
||||
rolloutBucket(installId, 'pinned-relay-default') ===
|
||||
rolloutBucket(installId, 'managed-servers-visible')
|
||||
)
|
||||
expect(sameForOtherFlag.length).toBeLessThan(ids.length / 10)
|
||||
})
|
||||
|
||||
it('keeps an install it cannot bucket on the baked value, except at 0% or 100%', () => {
|
||||
const partial: RolloutConfig = { 'pinned-relay-default': { state: 'on', percent: 50 } }
|
||||
const full: RolloutConfig = { 'pinned-relay-default': { state: 'on' } }
|
||||
const none: RolloutConfig = { 'pinned-relay-default': { state: 'on', percent: 0 } }
|
||||
const noId = { appVersion: '1.5.0', installId: null }
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...noId, config: partial })).toBe(false)
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...noId, config: full })).toBe(true)
|
||||
expect(resolveRolloutFlag('pinned-relay-default', { ...install, config: none })).toBe(false)
|
||||
})
|
||||
|
||||
it('reads the last recorded block until the next one replaces it', () => {
|
||||
expect(isRolloutFlagActive('pinned-relay-default', install)).toBe(false)
|
||||
recordRolloutConfig(parseRolloutConfig(campaign({ 'pinned-relay-default': { state: 'on' } })))
|
||||
expect(isRolloutFlagActive('pinned-relay-default', install)).toBe(true)
|
||||
recordRolloutConfig(parseRolloutConfig({ id: 'campaign-2', minVersion: '1.0.0' }))
|
||||
expect(isRolloutFlagActive('pinned-relay-default', install)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('e2e rollout override', () => {
|
||||
const dirs: string[] = []
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
for (const dir of dirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function flagsFile(flags: unknown): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'rollout-e2e-'))
|
||||
dirs.push(dir)
|
||||
const file = join(dir, 'flags.json')
|
||||
writeFileSync(file, JSON.stringify(flags))
|
||||
return file
|
||||
}
|
||||
|
||||
it('reads the flags file only in an unpackaged e2e launch', () => {
|
||||
const file = flagsFile({ 'legacy-relay-dir-sweep': { state: 'on' } })
|
||||
const env = { ORCA_E2E_USER_DATA_DIR: '/e2e', ORCA_E2E_ROLLOUT_FLAGS_FILE: file }
|
||||
expect(readE2ERolloutConfigOverride(env, false)).toEqual({
|
||||
'legacy-relay-dir-sweep': { state: 'on' }
|
||||
})
|
||||
expect(readE2ERolloutConfigOverride({ ORCA_E2E_ROLLOUT_FLAGS_FILE: file }, false)).toBeNull()
|
||||
})
|
||||
|
||||
it('never reads the flags file in a packaged release', () => {
|
||||
const file = flagsFile({ 'legacy-relay-dir-sweep': { state: 'on' } })
|
||||
const env = { ORCA_E2E_USER_DATA_DIR: '/e2e', ORCA_E2E_ROLLOUT_FLAGS_FILE: file }
|
||||
expect(readE2ERolloutConfigOverride(env, true)).toBeNull()
|
||||
vi.stubEnv('ORCA_E2E_USER_DATA_DIR', env.ORCA_E2E_USER_DATA_DIR)
|
||||
vi.stubEnv('ORCA_E2E_ROLLOUT_FLAGS_FILE', file)
|
||||
const context = { appVersion: '1.4.218', installId: null }
|
||||
installFakeAppEnvironment({ isPackaged: () => true })
|
||||
expect(isRolloutFlagActive('legacy-relay-dir-sweep', context)).toBe(false)
|
||||
installFakeAppEnvironment({ isPackaged: () => false })
|
||||
expect(isRolloutFlagActive('legacy-relay-dir-sweep', context)).toBe(true)
|
||||
})
|
||||
|
||||
it('treats a missing or unreadable file as no override', () => {
|
||||
const env = { ORCA_E2E_USER_DATA_DIR: '/e2e' }
|
||||
expect(readE2ERolloutConfigOverride(env, false)).toBeNull()
|
||||
expect(
|
||||
readE2ERolloutConfigOverride(
|
||||
{ ...env, ORCA_E2E_ROLLOUT_FLAGS_FILE: '/missing/flags.json' },
|
||||
false
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Server-side kill switches and percent cohorts for the Node runtime rollout flips.
|
||||
*
|
||||
* The block rides on the update-campaign payload Orca already polls (`nudge.json`); older builds
|
||||
* ignore it. Every flag resolves to its baked-in value when the block is absent, invalid, out of
|
||||
* the flag's version range, or was never received, so the payload can only move what it names.
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { z } from 'zod'
|
||||
import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment'
|
||||
import { compareVersions, isValidVersion } from '../updater-fallback'
|
||||
|
||||
export const ROLLOUT_FLAG_NAMES = [
|
||||
'pinned-relay-default',
|
||||
'managed-servers-visible',
|
||||
'serve-on-orcad-default',
|
||||
'legacy-npm-rung-retired',
|
||||
'legacy-relay-dir-sweep',
|
||||
'windows-relay-wmi-fallback-retired'
|
||||
] as const
|
||||
export type RolloutFlagName = (typeof ROLLOUT_FLAG_NAMES)[number]
|
||||
|
||||
/** Each flip's shipped behaviour: none of them is active in this build. */
|
||||
export const BAKED_ROLLOUT_FLAGS: Readonly<Record<RolloutFlagName, boolean>> = {
|
||||
'pinned-relay-default': false,
|
||||
'managed-servers-visible': false,
|
||||
'serve-on-orcad-default': false,
|
||||
'legacy-npm-rung-retired': false,
|
||||
'legacy-relay-dir-sweep': false,
|
||||
'windows-relay-wmi-fallback-retired': false
|
||||
}
|
||||
|
||||
const versionString = z.string().refine(isValidVersion)
|
||||
|
||||
// Why not strict: a newer server may add fields; an entry this build can't read falls back alone.
|
||||
const rolloutFlagEntrySchema = z
|
||||
.object({
|
||||
// `off` is the kill switch; `on` admits `percent` of installs; `default` defers to the build.
|
||||
state: z.enum(['default', 'off', 'on']),
|
||||
percent: z.number().min(0).max(100).optional(),
|
||||
minVersion: versionString.optional(),
|
||||
maxVersion: versionString.optional()
|
||||
})
|
||||
.refine(
|
||||
(entry) =>
|
||||
entry.minVersion === undefined ||
|
||||
entry.maxVersion === undefined ||
|
||||
compareVersions(entry.minVersion, entry.maxVersion) <= 0
|
||||
)
|
||||
|
||||
export type RolloutFlagEntry = z.infer<typeof rolloutFlagEntrySchema>
|
||||
export type RolloutConfig = Partial<Record<RolloutFlagName, RolloutFlagEntry>>
|
||||
|
||||
const campaignRolloutSchema = z.object({
|
||||
rollout: z.object({
|
||||
version: z.literal(1),
|
||||
flags: z.record(z.string(), z.unknown())
|
||||
})
|
||||
})
|
||||
|
||||
/** Reads the optional `rollout` block of a campaign payload; anything unreadable is null. */
|
||||
export function parseRolloutConfig(payload: unknown): RolloutConfig | null {
|
||||
const campaign = campaignRolloutSchema.safeParse(payload)
|
||||
if (!campaign.success) {
|
||||
return null
|
||||
}
|
||||
const config: RolloutConfig = {}
|
||||
for (const name of ROLLOUT_FLAG_NAMES) {
|
||||
const parsed = rolloutFlagEntrySchema.safeParse(campaign.data.rollout.flags[name])
|
||||
if (parsed.success) {
|
||||
config[name] = parsed.data
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
/** Stable per install and flag in [0, 100); salting by flag keeps the flips' cohorts independent. */
|
||||
export function rolloutBucket(installId: string, name: RolloutFlagName): number {
|
||||
const digest = createHash('sha256').update(`${name}:${installId}`).digest()
|
||||
return (digest.readUInt32BE(0) % 10_000) / 100
|
||||
}
|
||||
|
||||
function inVersionRange(version: string, entry: RolloutFlagEntry): boolean {
|
||||
return (
|
||||
(entry.minVersion === undefined || compareVersions(version, entry.minVersion) >= 0) &&
|
||||
(entry.maxVersion === undefined || compareVersions(version, entry.maxVersion) <= 0)
|
||||
)
|
||||
}
|
||||
|
||||
export type RolloutFlagContext = {
|
||||
config: RolloutConfig | null
|
||||
appVersion: string
|
||||
installId: string | null
|
||||
}
|
||||
|
||||
export function resolveRolloutFlag(name: RolloutFlagName, context: RolloutFlagContext): boolean {
|
||||
const baked = BAKED_ROLLOUT_FLAGS[name]
|
||||
const entry = context.config?.[name]
|
||||
if (!entry || entry.state === 'default') {
|
||||
return baked
|
||||
}
|
||||
if (!isValidVersion(context.appVersion) || !inVersionRange(context.appVersion, entry)) {
|
||||
return baked
|
||||
}
|
||||
if (entry.state === 'off') {
|
||||
return false
|
||||
}
|
||||
const percent = entry.percent ?? 100
|
||||
if (percent >= 100) {
|
||||
return true
|
||||
}
|
||||
// Why baked without an id: an unbucketable install must not join every cohort at once.
|
||||
if (!context.installId) {
|
||||
return baked
|
||||
}
|
||||
return rolloutBucket(context.installId, name) < percent
|
||||
}
|
||||
|
||||
let lastRolloutConfig: RolloutConfig | null = null
|
||||
|
||||
/**
|
||||
* Called with each successfully read campaign payload's block. A failed fetch keeps the last
|
||||
* block, so a transient network error can never lift a kill switch mid-session.
|
||||
*/
|
||||
export function recordRolloutConfig(config: RolloutConfig | null): void {
|
||||
lastRolloutConfig = config
|
||||
}
|
||||
|
||||
export function isRolloutFlagActive(
|
||||
name: RolloutFlagName,
|
||||
context: Omit<RolloutFlagContext, 'config'>
|
||||
): boolean {
|
||||
return resolveRolloutFlag(name, {
|
||||
...context,
|
||||
config: readE2ERolloutConfigOverride() ?? lastRolloutConfig
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* E2E launches only: a `flags` block read from `ORCA_E2E_ROLLOUT_FLAGS_FILE` on every check, so a
|
||||
* spec can flip a flag between two connects without the campaign fetch or a relaunch.
|
||||
*/
|
||||
export function readE2ERolloutConfigOverride(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
// Why fail closed: a packaged release must never take rollout flags from an env-named path.
|
||||
isPackaged: boolean = !hasAppEnvironment() || getAppEnvironment().isPackaged()
|
||||
): RolloutConfig | null {
|
||||
const file = env.ORCA_E2E_ROLLOUT_FLAGS_FILE
|
||||
if (isPackaged || !env.ORCA_E2E_USER_DATA_DIR || !file) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return parseRolloutConfig({
|
||||
rollout: { version: 1, flags: JSON.parse(readFileSync(file, 'utf8')) }
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function resetRolloutConfigForTests(): void {
|
||||
lastRolloutConfig = null
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
* Host: `ORCA_E2E_ORCAD_CONVERT_HOST`, a Windows host-cell descriptor. Template:
|
||||
* `ORCA_E2E_ORCAD_CONVERT_TEMPLATE`, built for that host's target.
|
||||
*/
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, rmSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
@@ -38,7 +38,6 @@ import type {
|
||||
const HOST = process.env[ORCAD_CONVERT_HOST_ENV]
|
||||
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
|
||||
const SCRATCH = path.join(os.tmpdir(), `orca-orcad-cli-matrix-${process.pid}`)
|
||||
const FLAGS_FILE = path.join(SCRATCH, 'rollout-flags.json')
|
||||
const MANAGED_TIMEOUT_MS = 8 * 60_000
|
||||
|
||||
type HostRow = {
|
||||
@@ -59,13 +58,11 @@ function skipUnlessWindowsHost(): void {
|
||||
function resetScratch(): void {
|
||||
rmSync(SCRATCH, { recursive: true, force: true })
|
||||
mkdirSync(SCRATCH, { recursive: true })
|
||||
writeFileSync(FLAGS_FILE, '{}')
|
||||
}
|
||||
|
||||
function launchEnv(): Record<string, string> {
|
||||
return {
|
||||
ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE!,
|
||||
ORCA_E2E_ROLLOUT_FLAGS_FILE: FLAGS_FILE
|
||||
ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,7 +561,7 @@ test('@orcad-cli-convert a seeded relay-era profile converts its Windows host, k
|
||||
const upgraded = await session.launch({ onStderr: sshStderr })
|
||||
app = upgraded.app
|
||||
await waitForSessionReady(upgraded.page)
|
||||
await convertThenRetire(upgraded.page, session.userDataDir, seeded, FLAGS_FILE)
|
||||
await convertThenRetire(upgraded.page, session.userDataDir, seeded)
|
||||
|
||||
const environmentId = await waitForManaged(upgraded.page, seeded.targetId)
|
||||
expect(await environmentIds(session.userDataDir)).toContain(environmentId)
|
||||
|
||||
Reference in New Issue
Block a user