Ctrl K
ComunidadGuías…

tests/app-onboarding-v3.db.test.ts

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

Archivo original ↗Preguntar a KiBot
import { randomUUID } from 'node:crypto'
import { env } from 'node:process'
import { createClient } from '@supabase/supabase-js'
import { describe, expect, it } from 'vitest'
import { APP_ONBOARDING_OTA_V1_STEP_IDS, APP_ONBOARDING_V3_STEP_IDS, parseAppOnboarding } from '../supabase/functions/_backend/utils/appOnboarding.ts'

const admin = createClient(env.SUPABASE_URL!, env.SUPABASE_SERVICE_KEY!, { auth: { persistSession: false, autoRefreshToken: false } })

describe('persisted app checklists', () => {
  it.concurrent.each([
    ['ota', 'ota', null, true, 4],
    ['ota', 'builder', 'B', false, 4],
    // Every non-Builder app gets the seven-step OTA checklist, whatever the org intent.
    ['both', 'ota', 'A', true, 4],
    ['builder', 'ota', 'A', true, 4],
    ['unknown', 'ota', 'A', true, 4],
  ] as const)('assigns org intent %s, creator intent %s, saved branch %s, creator-owned org %s to version %s', async (orgIntent, creatorIntent, branch, ownOrg, expectedVersion) => {
    const email = `onboarding-v3-${randomUUID()}@example.com`
    const created = await admin.auth.admin.createUser({ email, password: 'v3test-password', email_confirm: true })
    expect(created.error).toBeNull()
    const userId = created.data.user!.id
    let otherUserId: string | undefined
    const appIds: string[] = []
    const orgIds: string[] = []
    try {
      expect((await admin.from('users').upsert({ id: userId, email, onboarding: {
        intent: creatorIntent,
        abtests: branch ? { ota_todo_list_v3: { branch, assigned_at: new Date().toISOString() } } : {},
      } })).error).toBeNull()
      if (!ownOrg) {
        const other = await admin.auth.admin.createUser({ email: `onboarding-v3-other-${randomUUID()}@example.com`, password: 'v3test-password', email_confirm: true })
        expect(other.error).toBeNull()
        otherUserId = other.data.user!.id
        expect((await admin.from('users').upsert({ id: otherUserId, email: other.data.user!.email })).error).toBeNull()
      }
      // Both apps keep their insert-time versions when intent and assignments change.
      for (let n = 0; n < 2; n++) {
        const org = await admin.from('orgs').insert({ created_by: otherUserId ?? userId, name: `Checklist v3 ${randomUUID()}`, management_email: email, onboarding: { intent: orgIntent } }).select('id').single()
        expect(org.error).toBeNull()
        orgIds.push(org.data!.id)
        const appId = `com.onboarding.v3.${randomUUID()}`
        appIds.push(appId)
        const app = await admin.from('apps').insert({ app_id: appId, owner_org: org.data!.id, name: 'Checklist v3', icon_url: '', onboarding: { created_by_user_id: userId, setup: { todo_list_version: 2, steps: {} } } }).select('onboarding').single()
        expect(app.error).toBeNull()
        expect(parseAppOnboarding(app.data!.onboarding).todo_list_version).toBe(expectedVersion)
        if (expectedVersion === 4) {
          const setup = (app.data!.onboarding as any).setup
          expect(setup.ota_todo_list_version).toBe('1')
          expect(setup.paths).toEqual(['ota'])
          expect(setup.selected_path).toBe('ota')
          expect(Object.keys(setup.steps)).toEqual(['ota'])
          expect(Object.keys(setup.steps.ota).sort()).toEqual([...APP_ONBOARDING_OTA_V1_STEP_IDS].sort())
          expect(Object.values(setup.steps.ota)).toEqual(Array.from({ length: 7 }, () => ({ status: 'pending' })))
        }
      }
      expect((await admin.from('users').update({ onboarding: { intent: 'builder', abtests: {} } }).eq('id', userId)).error).toBeNull()
      expect((await admin.from('orgs').update({ onboarding: { intent: orgIntent === 'ota' ? 'builder' : 'ota' } }).in('id', orgIds)).error).toBeNull()
      expect((await admin.from('apps').update({ name: 'Renamed' }).in('app_id', appIds)).error).toBeNull()
      const apps = await admin.from('apps').select('onboarding').in('app_id', appIds)
      expect(apps.error).toBeNull()
      expect(apps.data!.map(app => parseAppOnboarding(app.onboarding).todo_list_version)).toEqual([expectedVersion, expectedVersion])
    }
    finally {
      await admin.from('apps').delete().in('app_id', appIds)
      await admin.from('orgs').delete().in('id', orgIds)
      await admin.from('users').delete().eq('id', userId)
      await admin.auth.admin.deleteUser(userId)
      if (otherUserId) {
        await admin.from('users').delete().eq('id', otherUserId)
        await admin.auth.admin.deleteUser(otherUserId)
      }
    }
  })

  it.concurrent('assigns OTA v4 from the organization even without a creator ledger', async () => {
    const email = `onboarding-org-${randomUUID()}@example.com`
    const orgId = randomUUID()
    const appId = `com.onboarding.org.${randomUUID()}`
    let userId: string | undefined
    try {
      const created = await admin.auth.admin.createUser({ email, password: 'v3test-password', email_confirm: true })
      expect(created.error).toBeNull()
      userId = created.data.user!.id
      expect((await admin.from('users').upsert({ id: userId, email, onboarding: { intent: 'builder' } })).error).toBeNull()
      expect((await admin.from('orgs').insert({ id: orgId, created_by: userId, name: 'OTA organization checklist test', management_email: email, onboarding: { intent: 'ota' } })).error).toBeNull()
      const app = await admin.from('apps').insert({ app_id: appId, owner_org: orgId, name: 'OTA organization checklist test', icon_url: '' }).select('onboarding').single()
      expect(app.error).toBeNull()
      expect((app.data!.onboarding as any).setup.todo_list_version).toBe(4)
      expect((app.data!.onboarding as any).created_by_user_id).toBeUndefined()
    }
    finally {
      await admin.from('apps').delete().eq('app_id', appId)
      await admin.from('orgs').delete().eq('id', orgId)
      if (userId) {
        await admin.from('users').delete().eq('id', userId)
        await admin.auth.admin.deleteUser(userId)
      }
    }
  })

  it.concurrent('maps legacy v1/v2 step statuses onto the seven OTA steps', async () => {
    const v2 = await admin.rpc('new_ota_onboarding_steps_v1', { p_legacy_steps: {
      login_cli_mcp: { status: 'done', at: '2026-09-01T00:00:00.000Z' },
      add_channel: { status: 'skipped' },
      add_encryption: { status: 'done' },
      upload_bundle: { status: 'unknown' },
    } })
    expect(v2.error).toBeNull()
    expect(Object.keys(v2.data).sort()).toEqual([...APP_ONBOARDING_OTA_V1_STEP_IDS].sort())
    expect(v2.data.login_cli_mcp).toEqual({ status: 'done', at: '2026-09-01T00:00:00.000Z' })
    expect(v2.data.add_channel).toEqual({ status: 'skipped' })
    expect(v2.data.upload_bundle).toEqual({ status: 'pending' })
    expect(v2.data.add_encryption).toBeUndefined()
    const v1 = await admin.rpc('new_ota_onboarding_steps_v1', { p_legacy_steps: { add_app: { status: 'done' } } })
    expect(v1.error).toBeNull()
    expect(v1.data.login_cli_mcp).toEqual({ status: 'done' })
    const empty = await admin.rpc('new_ota_onboarding_steps_v1', {})
    expect(empty.error).toBeNull()
    expect(Object.values(empty.data)).toEqual(Array.from({ length: 7 }, () => ({ status: 'pending' })))
  })

  it.concurrent('sql merge requires all seven v3 milestones and preserves legacy v2 completion', async () => {
    const current = { setup: { todo_list_version: 3, steps: {} }, features: { ota: { stage: 'local_only' } } }
    const premature = await admin.rpc('merge_app_onboarding_setup', { p_existing: current, p_patch: { outcome: 'completed', steps: { completion: { status: 'done' } } } })
    expect(premature.error).toBeNull()
    expect(parseAppOnboarding(premature.data)).toMatchObject({ todo_list_version: 3, outcome: 'in_progress', steps: {} })
    const completed = await admin.rpc('merge_app_onboarding_setup', { p_existing: current, p_patch: { steps: Object.fromEntries(APP_ONBOARDING_V3_STEP_IDS.map(id => [id, { status: 'done' }])) } })
    expect(completed.error).toBeNull()
    expect(parseAppOnboarding(completed.data).outcome).toBe('completed')
    expect(completed.data.features).toEqual(current.features)
    const control = await admin.rpc('merge_app_onboarding_setup', { p_existing: { setup: { todo_list_version: 2 } }, p_patch: { outcome: 'completed' } })
    expect(control.error).toBeNull()
    expect(parseAppOnboarding(control.data).outcome).toBe('completed')
  })

  it.concurrent('sql merge keeps all v4 OTA steps and preserves future path data', async () => {
    const current = { setup: { todo_list_version: 4, ota_todo_list_version: '1', paths: ['ota'], selected_path: 'ota', steps: { ota: Object.fromEntries(APP_ONBOARDING_OTA_V1_STEP_IDS.map(id => [id, { status: 'pending' }])), builder: { placeholder: { status: 'pending' } } } }, features: { ota: { stage: 'local_only' } } }
    const premature = await admin.rpc('merge_app_onboarding_setup', { p_existing: current, p_patch: { outcome: 'completed', steps: { ota: { add_code: { status: 'done' } } } } })
    expect(premature.error).toBeNull()
    expect(premature.data.setup.outcome).toBe('in_progress')
    expect(premature.data.setup.ota_todo_list_version).toBe('1')
    expect(premature.data.setup.steps.ota.add_code.status).toBe('done')
    expect(premature.data.setup.steps.ota.add_channel).toEqual({ status: 'pending' })
    expect(premature.data.setup.steps.builder).toEqual(current.setup.steps.builder)
    expect(premature.data.features).toEqual(current.features)
    const completed = await admin.rpc('merge_app_onboarding_setup', { p_existing: current, p_patch: { steps: Object.fromEntries(APP_ONBOARDING_OTA_V1_STEP_IDS.map(id => [id, { status: 'done' }])) } })
    expect(completed.error).toBeNull()
    expect(parseAppOnboarding(completed.data).outcome).toBe('completed')
  })

  it.concurrent('sql merge leaves unknown and numeric v4 OTA versions untouched', async () => {
    for (const version of ['2', 1]) {
      const current = { setup: { todo_list_version: 4, ota_todo_list_version: version, steps: { ota: { add_code: { status: 'pending' } } } } }
      const result = await admin.rpc('merge_app_onboarding_setup', { p_existing: current, p_patch: { steps: { add_code: { status: 'done' } }, outcome: 'completed' } })
      expect(result.error).toBeNull()
      expect(result.data.setup.ota_todo_list_version).toBe(version)
      expect(result.data.setup.steps.ota.add_code).toEqual({ status: 'pending' })
      expect(result.data.setup.paths).toBeUndefined()
      expect(result.data.setup.outcome).toBe('in_progress')
    }
    const builderOnly = { setup: { todo_list_version: 4, outcome: 'completed', paths: ['builder'], selected_path: 'builder', steps: { builder: { configure_signing: { status: 'done' } } } } }
    const result = await admin.rpc('merge_app_onboarding_setup', { p_existing: builderOnly, p_patch: { steps: { add_code: { status: 'done' } } } })
    expect(result.error).toBeNull()
    expect(result.data.setup.outcome).toBe('completed')
    expect(result.data.setup.paths).toEqual(['builder'])
    expect(result.data.setup.steps).toEqual(builderOnly.setup.steps)
  })
})