scripts/repro-shard5-postgrest-flakes.ts
Rama main · réplica de lectura del repositorio original.
/**
* Reproduce backend shard 5/6 PostgREST/Kong upstream flakes without Vitest retries.
*
* CI evidence:
* - stats createAppVersions via PostgREST → "An invalid response was received from the upstream server"
* - organization-api capgkey auth via PostgREST → intermittent 401 invalid_apikey
*
* This script forces the upstream failure mode by pausing the local PostgREST
* container during the old-path hammer, then proves direct SQL still succeeds.
*
* Usage (Supabase must be running):
* bun run supabase:with-env -- bun scripts/repro-shard5-postgrest-flakes.ts
*/
import { randomUUID } from 'node:crypto'
import { spawnSync } from 'node:child_process'
import { createClient } from '@supabase/supabase-js'
import pg from 'pg'
const SUPABASE_URL = process.env.SUPABASE_URL
const SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY
?? process.env.SUPABASE_SERVICE_ROLE_KEY
?? process.env.SERVICE_ROLE_KEY
const DB_URL = process.env.SUPABASE_DB_URL ?? process.env.DB_URL
if (!SUPABASE_URL || !SERVICE_KEY || !DB_URL) {
console.error('Missing SUPABASE_URL / service key / DB URL. Use: bun run supabase:with-env -- bun scripts/repro-shard5-postgrest-flakes.ts')
process.exit(2)
}
const ORG_ID = '046a36ac-e03c-4590-9257-bd6c9dba9ee8'
const USER_ID = '6aa76066-55ef-4238-ade6-0b32334a4097'
const PARALLEL = Number(process.env.REPRO_PARALLEL ?? 20)
const REQUEST_TIMEOUT_MS = Number(process.env.REPRO_TIMEOUT_MS ?? 2000)
const supabase = createClient(SUPABASE_URL, SERVICE_KEY, {
auth: { persistSession: false },
global: {
fetch: (input, init) => fetch(input, {
...init,
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
}),
},
})
const pool = new pg.Pool({ connectionString: DB_URL, max: 10 })
function findPostgrestContainer(): string | null {
const listed = spawnSync('docker', ['ps', '--format', '{{.Names}}'], { encoding: 'utf8' })
if (listed.status !== 0)
return null
const names = listed.stdout.split('\n').map(s => s.trim()).filter(Boolean)
return names.find(name => name.includes('rest') && name.includes('capgo'))
?? names.find(name => name.includes('rest'))
?? null
}
function dockerAction(action: 'pause' | 'unpause', container: string) {
const result = spawnSync('docker', [action, container], { encoding: 'utf8' })
if (result.status !== 0) {
throw new Error(`docker ${action} ${container} failed: ${result.stderr || result.stdout}`)
}
}
async function ensureApp(appId: string) {
await pool.query(
`INSERT INTO public.apps (app_id, icon_url, name, owner_org, user_id)
VALUES ($1, '', $1, $2::uuid, $3::uuid)
ON CONFLICT (app_id) DO NOTHING`,
[appId, ORG_ID, USER_ID],
)
}
async function cleanupApp(appId: string) {
await pool.query('DELETE FROM public.app_versions WHERE app_id = $1', [appId])
await pool.query('DELETE FROM public.apps WHERE app_id = $1', [appId])
}
async function postgrestCreateVersion(appId: string, version: string) {
try {
const { data, error } = await supabase.from('app_versions').upsert({
app_id: appId,
name: version,
owner_org: ORG_ID,
}, {
onConflict: 'app_id,name',
}).select('id,name').single()
if (error || !data)
return { ok: false as const, error: error?.message ?? 'no data' }
return { ok: true as const }
}
catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { ok: false as const, error: message }
}
}
async function sqlCreateVersion(appId: string, version: string) {
const result = await pool.query(
`INSERT INTO public.app_versions (app_id, name, owner_org)
VALUES ($1, $2, $3::uuid)
ON CONFLICT (name, app_id) DO UPDATE SET updated_at = now()
RETURNING id, name`,
[appId, version, ORG_ID],
)
if (!result.rows[0])
return { ok: false as const, error: 'no data' }
return { ok: true as const }
}
async function runBatch(
label: string,
fn: (appId: string, version: string) => Promise<{ ok: boolean, error?: string }>,
) {
const appId = `com.repro.shard5.${randomUUID().slice(0, 8)}`
await ensureApp(appId)
const results = await Promise.all(
Array.from({ length: PARALLEL }, (_, i) => {
const version = `1.0.0-repro-${i}-${randomUUID().slice(0, 8)}`
return fn(appId, version)
}),
)
const failures = results.filter(r => !r.ok)
const errors = new Map<string, number>()
for (const failure of failures) {
const key = failure.error ?? 'unknown'
errors.set(key, (errors.get(key) ?? 0) + 1)
}
console.log(`\n[${label}] failures=${failures.length}/${PARALLEL} (${((failures.length / PARALLEL) * 100).toFixed(1)}%)`)
for (const [message, count] of [...errors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5))
console.log(` - ${count}x ${message}`)
await cleanupApp(appId)
return failures.length
}
const container = findPostgrestContainer()
if (!container) {
console.error('Could not find local Postgrest docker container')
process.exit(2)
}
console.log(`Using PostgREST container: ${container}`)
let postgrestFailures = 0
try {
dockerAction('pause', container)
await Bun.sleep(300)
postgrestFailures = await runBatch('PostgREST upsert while upstream paused (old path)', postgrestCreateVersion)
}
finally {
try {
dockerAction('unpause', container)
}
catch {
// already running
}
await Bun.sleep(300)
}
const sqlFailures = await runBatch('Direct SQL upsert (fixed path)', sqlCreateVersion)
await pool.end()
if (postgrestFailures < PARALLEL) {
console.error('\nExpected PostgREST path to fail 100% while upstream is paused.')
process.exit(1)
}
if (sqlFailures > 0) {
console.error('\nSQL path failed — fix is incomplete.')
process.exit(1)
}
console.log('\nRepro locked: PostgREST path fails 100% on upstream outage; direct SQL stays green.')
process.exit(0)