🌳
pt0/deployF/testsF/throwawayDeployTestAI.mts
13type ThrowawayDeployTestOpts = {
14 syncWithAction: (action: string) => Promise<void>
15 cluster_name: string
16 podName?: string // optional if using verifyFn
17 pvcNames?: string[] // if provided, waits for PV cleanup after delpvcs (prevents flaky "pvc not found")
18 suiteName: string
19 ep?: string
20 includeDelpvcs?: boolean
21 includeCleanup?: boolean
22 verifyFn?: () => Promise<{passed: boolean, msg: string}> // custom verification (replaces default podHealthy check)
23 setupFn?: () => Promise<{passed: boolean, msg: string}> // optional setup step (runs after delete/delpvcs, before apply) - e.g., configure SSH
26const throwawayPtenv = 'testprod' // throwaway tests are always deployed-only
28const includesRecreateSuite = () => {
29 const suites = cliArg('--suite')
30 if (!suites) return true
31 const list = suites.split(',')
32 return list.includes('recreate') || list.includes('all')
35export const runThrowawayDeployTest = async ({syncWithAction, cluster_name, podName, pvcNames, suiteName, ep, includeDelpvcs, includeCleanup, verifyFn, setupFn}: ThrowawayDeployTestOpts) => {
36 // Handle --history flag
37 if (cliFlag('--history')) {
39 return { passed: true, results: [] }
40 }
42 // Check memoization (skip if already passed at this SHA)
43 // Simple SHA-match memo (throwaway tests don't have Next.js import tree)
44 if (ep && !cliFlag('--no-memo')) {
45 const epRel = toPtRelPath(ep)
46 const currentSha = getGitShaFull()
47 const last = readLedger().findLast(r =>
48 r.ep === epRel && r.action === 'runtests' && r.ptenv === throwawayPtenv && r.success
49 )
50 if (last?.gitSha === currentSha) {
51 printSkipMsg(throwawayPtenv, `unchanged at ${currentSha}`, last)
52 return { passed: true, results: [] }
53 }
54 }
56 console.log(chalkCyan(`[${suiteName}]`))
57 const startTime = Date.now()
59 const {tests, test} = createTestCollector()
61 const doRecreate = includesRecreateSuite()
62 const skipRecreate = {passed: true, skipped: true, msg: 'add --suite=recreate to run'} as const
63 const skipCleanup = {passed: true, skipped: true, msg: 'skipped'} as const
65 test('delete', async () => {
66 if (!doRecreate) return skipRecreate
67 await syncWithAction('delete')
68 return {passed: true, msg: 'delete completed'}
69 })
71 if (includeDelpvcs) {
72 test('delpvcs', async () => {
73 if (!doRecreate) return skipRecreate
74 // Get PV names before deleting PVCs (needed to wait for cleanup)
75 const pvNames = pvcNames ? await Promise.all(pvcNames.map(pvcName => getPvNameForPvc({pvcName, cluster_name}))) : []
76 await syncWithAction('delpvcs')
77 // Wait for PV cleanup to prevent race condition on re-create
78 await Promise.all(pvNames.filter(Boolean).map(pvName => waitForPvCleanup({pvName: pvName!, cluster_name})))
79 return {passed: true, msg: 'delpvcs completed'}
80 })
81 }
83 if (setupFn) {
84 test('setup', setupFn)
85 }
87 test('apply', async () => {
88 const doApply = async () => {
89 await syncWithAction('apply')
90 return {passed: true, msg: 'apply completed'}
91 }
92 try {
93 return await doApply()
94 } catch (err: any) {
95 // Auto-recover from disk full in throwaway tests
96 // CrashLoopBackOff on init container usually means disk full for codeserv
97 const isCrashLoop = err.uniqDebugH?.reason === 'CrashLoopBackOff' && err.uniqDebugH?.containerType === 'init container'
98 if (!isCrashLoop) throw err
99 console.log('[throwaway-pvcs-full-delpvcs] Init container CrashLoopBackOff, deleting PVCs and retrying...')
100 await syncWithAction('delete')
101 await syncWithAction('delpvcs')
102 if (pvcNames) {
103 const pvNames = await Promise.all(pvcNames.map(pvcName => getPvNameForPvc({pvcName, cluster_name})))
104 await Promise.all(pvNames.filter(Boolean).map(pvName => waitForPvCleanup({pvName: pvName!, cluster_name})))
105 }
106 return await doApply()
107 }
108 })
110 test('verify', async () => {
111 if (verifyFn) return verifyFn()
112 const result = await checkPodsHealthy({cluster_name, name: podName!})
113 return result.healthy
114 ? {passed: true, msg: `${podName}: pods healthy`}
115 : {passed: false, msg: result.message}
116 })
118 if (includeCleanup) {
119 test('cleanupDelete', async () => {
120 if (!doRecreate) return skipCleanup
121 await syncWithAction('delete')
122 return {passed: true, msg: 'cleanup delete completed'}
123 })
124 if (includeDelpvcs) {
125 test('cleanupDelpvcs', async () => {
126 if (!doRecreate) return skipCleanup
127 await syncWithAction('delpvcs')
128 return {passed: true, msg: 'cleanup delpvcs completed'}
129 })
130 }
131 }
133 const result = await quietModeCtx.run({quiet: true}, () => runTestsWithProgress({tests, suiteName, failFast: true}))
135 if (ep) recordSuiteResult({ep, ptenv: throwawayPtenv, suiteName, passed: result.passed, message: result.message, startTime})
137 if (!result.passed) process.exitCode = 1
138 return result