1import { fileURLToPath } from 'url' 2import { execFile } from 'child_process' 3import { promisify } from 'util' 10const aExecFile = promisify(execFile) 12type SpawnResult = { code: number, stdout: string, stderr: string } 14const spawnEp = async (epPath: string, args: string[], opts?: {timeout?: number}): Promise<SpawnResult> => { 16 const {stdout, stderr} = await aExecFile('node', [epPath, ...args], { 17 timeout: opts?.timeout || 10000, 18 maxBuffer: 1024 * 1024, 20 return {code: 0, stdout, stderr} 22 return {code: err.code || 1, stdout: err.stdout || '', stderr: err.stderr || ''} 26type TestFnc = ReturnType<typeof createTestCollector>['test'] 28const genericCliTests = (test: TestFnc, epPath: string) => { 29 test('help-exits-0', async () => { 30 const {code, stdout} = await spawnEp(epPath, ['help']) 31 if (code !== 0) return {passed: false, msg: `exit ${code}`} 32 if (!stdout.includes('[action]')) return {passed: false, msg: 'missing [action] in help'} 33 return {passed: true, msg: 'help works'} 36 test('unknown-action-errors', async () => { 37 const {code, stderr} = await spawnEp(epPath, ['notarealaction123']) 38 if (code === 0) return {passed: false, msg: 'should exit non-zero'} 39 if (!stderr.toLowerCase().includes('unknown')) return {passed: false, msg: 'missing "unknown" in stderr'} 40 return {passed: true, msg: 'rejects unknown action'} 43 test('unknown-flag-errors', async () => { 44 const {code, stderr} = await spawnEp(epPath, ['help', '--notarealflag123']) 45 if (code === 0) return {passed: false, msg: 'should exit non-zero'} 46 if (!stderr.includes(unknownArgsPrefix)) return {passed: false, msg: `missing "${unknownArgsPrefix}" in stderr`} 47 return {passed: true, msg: 'rejects unknown flag'} 50 test('howimports-accepts-positional-arg', async () => { 51 const {stdout: helpOut} = await spawnEp(epPath, ['help']) 52 if (!helpOut.includes('howimports')) return skipTest('ep does not have howimports action') 53 const {code, stdout, stderr} = await spawnEp(epPath, ['howimports', 'pt0/deployF/constantsF.mts']) 54 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: 'howimports rejected positional arg'} 55 if (code !== 0) return {passed: false, msg: `exit ${code}: ${stderr.trim()}`} 56 if (!stdout.includes('constantsF.mts')) return {passed: false, msg: 'missing target file in output'} 57 return {passed: true, msg: 'howimports resolves import tree'} 60 test('fexec-accepts-positional-cmd', async () => { 61 const {stdout: helpOut} = await spawnEp(epPath, ['help']) 62 if (!helpOut.includes('fexec')) return skipTest('ep does not have fexec action') 63 const {stderr} = await spawnEp(epPath, ['fexec', 'echo bla']) 64 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: 'fexec rejected positional arg'} 65 return {passed: true, msg: 'fexec accepts positional cmd'} 68 test('fexec-accepts-pod-flag', async () => { 69 const {stdout: helpOut} = await spawnEp(epPath, ['help']) 70 if (!helpOut.includes('fexec')) return skipTest('ep does not have fexec action') 71 const {stderr} = await spawnEp(epPath, ['fexec', '--pod=2']) 72 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: 'fexec rejected --pod flag'} 73 return {passed: true, msg: 'fexec accepts --pod flag'} 76 test('apply-history-flag-accepted', async () => { 77 const {stderr} = await spawnEp(epPath, ['apply', '--history']) 78 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: 'apply --history rejected'} 79 return {passed: true, msg: 'apply --history accepted'} 82 test('apply-history-shortcircuits-build', async () => { 83 const {stdout, stderr} = await spawnEp(epPath, ['apply', '--history'], {timeout: 10000}) 84 const output = stdout + stderr 85 const stripped = output.replace(/\x1b\[[0-9;]*m/g, '') 86 if (stripped.includes('kanikobuild') || stripped.includes('image not found') || stripped.includes('dockerbuild')) 87 return {passed: false, msg: '--history did not short-circuit; build started'} 88 if (stripped.includes('No history') || stripped.includes('No matching')) 89 return {passed: true, msg: 'apply --history short-circuits (no history yet)'} 91 const markerCount = (stripped.match(markerRe) || []).length 92 if (markerCount !== 1) 93 return {passed: false, msg: `history printed ${markerCount} times (expected 1)`} 94 return {passed: true, msg: 'apply --history short-circuits correctly'} 98const dbCliTests = (test: TestFnc, epPath: string) => { 99 test('dbconsole-produces-output', async () => { 100 const {stdout, stderr} = await spawnEp(epPath, ['dbconsole', 'SELECT 1'], {timeout: 30000}) 101 const output = stdout + stderr 102 if (output.length === 0) return {passed: false, msg: 'SILENT EXIT - action produced no output'} 103 return {passed: true, msg: 'dbconsole produces output'} 107const getFirstStepFromHelp = async (epPath: string) => { 108 const {stdout} = await spawnEp(epPath, ['help']) 109 const availMatch = stdout.match(/Available steps: (.+)/) 110 return availMatch ? availMatch[1].split(',')[0].trim() : null 113const clusterCliTests = (test: TestFnc, epPath: string) => { 114 test('help-lists-steps', async () => { 115 const {code, stdout} = await spawnEp(epPath, ['help']) 116 if (code !== 0) return {passed: false, msg: `exit ${code}`} 117 if (!stdout.includes('Available steps:')) return {passed: false, msg: 'missing step list in help'} 118 return {passed: true, msg: 'help lists steps'} 121 test('unknown-step-errors', async () => { 122 const {code} = await spawnEp(epPath, ['info', '--steps=notarealstep123']) 123 if (code === 0) return {passed: false, msg: 'should exit non-zero'} 124 return {passed: true, msg: 'rejects unknown step'} 127 test('steps-flag-accepted', async () => { 128 const firstStep = await getFirstStepFromHelp(epPath) 129 if (!firstStep) return {passed: false, msg: 'cannot parse step names from help'} 130 const {code, stderr} = await spawnEp(epPath, ['info', `--steps=${firstStep}`]) 131 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: `--steps=${firstStep} rejected`} 132 if (code !== 0) return {passed: false, msg: `exit ${code}: ${stderr.trim()}`} 133 return {passed: true, msg: `info --steps=${firstStep} accepted`} 136 test('apply-flags-accepted-with-steps', async () => { 137 const firstStep = await getFirstStepFromHelp(epPath) 138 if (!firstStep) return {passed: false, msg: 'cannot parse step names from help'} 139 const {stderr} = await spawnEp(epPath, ['info', `--steps=${firstStep}`, '--withKaniko']) 140 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: '--withKaniko rejected'} 141 return {passed: true, msg: 'applyCli flags accepted with --steps'} 144 test('apply-steps-not-rejected-by-parseCli', async () => { 145 const firstStep = await getFirstStepFromHelp(epPath) 146 if (!firstStep) return {passed: false, msg: 'cannot parse step names from help'} 147 const {stderr} = await spawnEp(epPath, ['apply', `--steps=${firstStep}`, '--history']) 148 if (stderr.includes(unknownArgsPrefix)) return {passed: false, msg: '--steps rejected on apply --history'} 149 return {passed: true, msg: 'apply --steps --history accepted'} 153/** Cluster sync CLI tests suite - validates eptClusterSync CLI behavior */ 154export const runClusterCliTestsSuite: SuiteRunner = async ({ appCfg }) => { 155 const epPath = appCfg?.importMetaUrl ? fileURLToPath(appCfg.importMetaUrl) : getProcArgv()[1] 157 clusterCliTests(test, epPath) 161/** Generic CLI tests suite - validates CLI behavior without needing server/db */ 162export const runCliTestsSuite: SuiteRunner = async ({ appCfg }) => { 163 const epPath = appCfg?.importMetaUrl ? fileURLToPath(appCfg.importMetaUrl) : getProcArgv()[1] 165 genericCliTests(test, epPath) 169/** DB entrypoint CLI tests - includes dbconsole test */ 170export const runDbCliTestsSuite: SuiteRunner = async ({ appCfg }) => { 171 const epPath = appCfg?.importMetaUrl ? fileURLToPath(appCfg.importMetaUrl) : getProcArgv()[1] 173 genericCliTests(test, epPath) 174 dbCliTests(test, epPath)