Ctrl K
ComunidadGuías…

tests/bundle-compatibility-recovery.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from 'vitest'
import { BUNDLE_INCOMPATIBLE_EVENT, buildBundleCompatibilityBentoEvent, bundleIncompatibleEmailOutcome, isBreakingChangeGatedByChannelStrategy, isCliTrueTag } from '../supabase/functions/_backend/utils/bundle_compatibility_recovery.ts'

const base = {
  event: BUNDLE_INCOMPATIBLE_EVENT,
  orgId: 'org-1',
  appId: 'com.demo.app',
  channelOverwritten: true,
  channel: 'production',
  source: 'upload',
  versionNewId: '101',
  versionNewName: '1.0.1',
  versionOldId: '100',
  versionOldName: '1.0.0',
  orgName: 'Demo Org',
  appName: 'Demo',
  // 'none' never gates, so the base case is the crash-warning event.
  disableAutoUpdate: 'none',
  minUpdateVersion: null,
}

describe('buildBundleCompatibilityBentoEvent', () => {
  it.concurrent('exposes the trigger event name', () => {
    expect(BUNDLE_INCOMPATIBLE_EVENT).toBe('Bundle Incompatible')
  })

  it.concurrent('builds a full payload for an incompatible upload that went live', () => {
    const r = buildBundleCompatibilityBentoEvent(base)
    expect(r).toBeDefined()
    expect(r!.event).toBe('bundle_incompatible')
    expect(r!.preferenceKey).toBe('bundle_incompatible')
    // Permanent per-version dedupe (no reopening cron window), so retries of the
    // same incompatible version don't re-email org admins.
    expect(r!.once).toBe(true)
    expect(r!.cron).toBeUndefined()
    expect(r!.uniqId).toBe('bundle_incompatible:com.demo.app:production:1.0.1')
    expect(r!.data).toMatchObject({
      disable_auto_update: 'none',
      gated: false,
      min_update_version: '',
      org_id: 'org-1',
      org_name: 'Demo Org',
      app_id: 'com.demo.app',
      app_name: 'Demo',
      channel: 'production',
      source: 'upload',
      version_new_id: '101',
      version_new_name: '1.0.1',
      version_old_id: '100',
      version_old_name: '1.0.0',
    })
  })

  // Email gate: only an incompatible upload that overwrote the channel's live
  // version produces a payload. PostHog still records every incompatible upload
  // upstream — this only controls the org-member email.
  it.concurrent('returns undefined when the channel was not overwritten', () => {
    expect(buildBundleCompatibilityBentoEvent({ ...base, channelOverwritten: false })).toBeUndefined()
  })

  it.concurrent('returns undefined when channel_overwritten is missing', () => {
    expect(buildBundleCompatibilityBentoEvent({ ...base, channelOverwritten: undefined })).toBeUndefined()
  })

  it.concurrent('returns undefined when the caller accepted the incompatibility', () => {
    expect(buildBundleCompatibilityBentoEvent({ ...base, incompatibilityAccepted: true })).toBeUndefined()
  })

  it.concurrent('parses CLI boolean tags as true or the string true', () => {
    expect(isCliTrueTag(true)).toBe(true)
    expect(isCliTrueTag('true')).toBe(true)
    expect(isCliTrueTag(false)).toBe(false)
    expect(isCliTrueTag('false')).toBe(false)
    expect(isCliTrueTag(undefined)).toBe(false)
  })

  it.concurrent('maps accepted incompatibility to the skipped_accepted email outcome', () => {
    expect(bundleIncompatibleEmailOutcome(true, false)).toBe('skipped_accepted')
    expect(bundleIncompatibleEmailOutcome(true, true)).toBe('skipped_accepted')
    expect(bundleIncompatibleEmailOutcome(false, true)).toBe('sent_expected')
    expect(bundleIncompatibleEmailOutcome(false, false)).toBe('sent')
  })

  it.concurrent('falls back to the old version in uniqId when the new version is absent', () => {
    const r = buildBundleCompatibilityBentoEvent({ ...base, versionNewId: undefined, versionNewName: undefined })
    expect(r).toBeDefined()
    expect(r!.uniqId).toBe('bundle_incompatible:com.demo.app:production:1.0.0')
    expect(r!.data.version_new_id).toBe('')
    expect(r!.data.version_new_name).toBe('')
    expect(r!.data.version_old_name).toBe('1.0.0')
  })

  it.concurrent('returns undefined for other event names', () => {
    expect(buildBundleCompatibilityBentoEvent({ ...base, event: 'Bundle Upload Compatibility Checked' })).toBeUndefined()
  })

  it.concurrent('returns undefined when org or app id is missing', () => {
    expect(buildBundleCompatibilityBentoEvent({ ...base, orgId: undefined })).toBeUndefined()
    expect(buildBundleCompatibilityBentoEvent({ ...base, appId: undefined })).toBeUndefined()
  })

  it.concurrent('defaults missing fields to safe empties', () => {
    const r = buildBundleCompatibilityBentoEvent({
      ...base,
      source: undefined,
      channel: undefined,
      orgName: undefined,
      appName: undefined,
      versionNewId: undefined,
      versionNewName: undefined,
      versionOldId: undefined,
      versionOldName: undefined,
    })
    expect(r).toBeDefined()
    expect(r!.data.source).toBe('unknown')
    expect(r!.data.channel).toBe('')
    expect(r!.data.org_name).toBe('')
    expect(r!.data.app_name).toBe('')
    expect(r!.data.version_new_id).toBe('')
    expect(r!.data.version_old_id).toBe('')
    // No version names left to key off; uniqId trails with empty segments.
    expect(r!.uniqId).toBe('bundle_incompatible:com.demo.app::')
  })
})

// The channel's update strategy decides which of the two Bento signals fires:
// when the version bump keeps the bundle off devices still running the previous
// (incompatible) native build, the breaking change was done correctly.
describe('isBreakingChangeGatedByChannelStrategy', () => {
  it.concurrent.each([
    // strategy, old, new, gated
    ['major', '1.10.35', '2.0.0', true],
    ['major', '1.10.35', '1.11.0', false],
    ['major', '1.10.35', '1.10.36', false],
    ['minor', '1.10.35', '1.11.0', true],
    ['minor', '1.10.35', '2.0.0', true],
    ['minor', '1.10.35', '1.10.36', false],
    ['patch', '1.10.35', '1.10.36', true],
    ['patch', '1.10.35', '1.11.0', true],
    ['patch', '1.10.35', '1.10.35', false],
    ['none', '1.10.35', '2.0.0', false],
    ['unknown_strategy', '1.10.35', '2.0.0', false],
  ])('%s: %s -> %s gated=%s', (strategy, versionOldName, versionNewName, expected) => {
    expect(isBreakingChangeGatedByChannelStrategy({ strategy, versionOldName, versionNewName })).toBe(expected)
  })

  // `version_number` gates on the new bundle's min_update_version vs the device
  // native version, so a minimum that is not above the previous version still
  // reaches those devices.
  it.concurrent.each([
    ['1.11.0', true],
    ['1.10.36', true],
    ['1.10.35', false],
    ['1.10.0', false],
    [null, false],
    ['not-semver', false],
  ])('version_number with min_update_version %s gated=%s', (minUpdateVersion, expected) => {
    expect(isBreakingChangeGatedByChannelStrategy({
      strategy: 'version_number',
      versionOldName: '1.10.35',
      versionNewName: '1.11.0',
      minUpdateVersion,
    })).toBe(expected)
  })

  // Fail closed: without two parseable versions we cannot prove the strategy
  // gates delivery, so the crash warning wins.
  it.concurrent('is not gated when a version name is unparseable or missing', () => {
    expect(isBreakingChangeGatedByChannelStrategy({ strategy: 'minor', versionOldName: 'builtin', versionNewName: '1.11.0' })).toBe(false)
    expect(isBreakingChangeGatedByChannelStrategy({ strategy: 'minor', versionOldName: '1.10.35', versionNewName: 'nightly' })).toBe(false)
    expect(isBreakingChangeGatedByChannelStrategy({ strategy: 'minor', versionOldName: undefined, versionNewName: '1.11.0' })).toBe(false)
    expect(isBreakingChangeGatedByChannelStrategy({ strategy: 'minor', versionOldName: '1.10.35', versionNewName: undefined })).toBe(false)
    expect(isBreakingChangeGatedByChannelStrategy({ strategy: null, versionOldName: '1.10.35', versionNewName: '2.0.0' })).toBe(false)
    expect(isBreakingChangeGatedByChannelStrategy({ strategy: 'version_number', versionOldName: 'builtin', versionNewName: '1.11.0', minUpdateVersion: '1.11.0' })).toBe(false)
  })
})

describe('buildBundleCompatibilityBentoEvent event split', () => {
  const incident = { ...base, versionOldName: '1.10.35', versionNewName: '1.11.0', channel: 'staging' }

  it.concurrent.each([
    ['minor', null, 'gated'],
    ['patch', null, 'gated'],
    ['version_number', '1.11.0', 'gated'],
    ['version_number', '1.10.0', 'warning'],
    ['version_number', null, 'warning'],
    ['major', null, 'warning'],
    ['none', null, 'warning'],
  ])('%s strategy (min %s) on a minor bump emits the %s event', (strategy, minUpdateVersion, expectation) => {
    const r = buildBundleCompatibilityBentoEvent({ ...incident, disableAutoUpdate: strategy, minUpdateVersion })
    expect(r).toBeDefined()
    if (expectation === 'gated') {
      expect(r!.event).toBe('bundle_incompatible_expected')
      expect(r!.preferenceKey).toBe('bundle_incompatible_expected')
      expect(r!.uniqId).toBe('bundle_incompatible_expected:com.demo.app:staging:1.11.0')
      expect(r!.data).toMatchObject({ disable_auto_update: strategy, gated: true, min_update_version: minUpdateVersion ?? '' })
    }
    else {
      expect(r!.event).toBe('bundle_incompatible')
      expect(r!.preferenceKey).toBe('bundle_incompatible')
      expect(r!.uniqId).toBe('bundle_incompatible:com.demo.app:staging:1.11.0')
      expect(r!.data).toMatchObject({ disable_auto_update: strategy, gated: false })
    }
  })

  it.concurrent('emits the expected event for a patch bump on the patch strategy', () => {
    const r = buildBundleCompatibilityBentoEvent({ ...base, versionOldName: '1.10.35', versionNewName: '1.10.36', disableAutoUpdate: 'patch' })
    expect(r!.event).toBe('bundle_incompatible_expected')
  })

  it.concurrent('emits the warning event for a patch bump on the minor strategy', () => {
    const r = buildBundleCompatibilityBentoEvent({ ...base, versionOldName: '1.10.35', versionNewName: '1.10.36', disableAutoUpdate: 'minor' })
    expect(r!.event).toBe('bundle_incompatible')
  })

  it.concurrent('emits the expected event for a major bump on the major strategy', () => {
    const r = buildBundleCompatibilityBentoEvent({ ...base, versionOldName: '1.10.35', versionNewName: '2.0.0', disableAutoUpdate: 'major' })
    expect(r!.event).toBe('bundle_incompatible_expected')
  })

  it.concurrent('emits the warning event when versions are unparseable even on a gating strategy', () => {
    const r = buildBundleCompatibilityBentoEvent({ ...base, versionOldName: 'builtin', versionNewName: 'nightly', disableAutoUpdate: 'minor' })
    expect(r!.event).toBe('bundle_incompatible')
  })

  it.concurrent('skips both emails when the caller accepted the incompatibility', () => {
    expect(buildBundleCompatibilityBentoEvent({
      ...incident,
      disableAutoUpdate: 'minor',
      incompatibilityAccepted: true,
    })).toBeUndefined()
  })

  it.concurrent('returns undefined for a gating strategy when the channel was not overwritten', () => {
    expect(buildBundleCompatibilityBentoEvent({ ...incident, disableAutoUpdate: 'minor', channelOverwritten: false })).toBeUndefined()
  })
})