🌳
pt0/deployF/testsF/cliTestsSuiteAI.mts
1import { fileURLToPath } from 'url'
2import { execFile } from 'child_process'
3import { promisify } from 'util'
6import type { SuiteRunner } from './runTestsCoreAI.mts'
10const aExecFile = promisify(execFile)
12type SpawnResult = { code: number, stdout: string, stderr: string }
14const spawnEp = async (epPath: string, args: string[], opts?: {timeout?: number}): Promise<SpawnResult> => {
15 try {
16 const {stdout, stderr} = await aExecFile('node', [epPath, ...args], {
17 timeout: opts?.timeout || 10000,
18 maxBuffer: 1024 * 1024,
19 })
20 return {code: 0, stdout, stderr}
21 } catch (err: any) {
22 return {code: err.code || 1, stdout: err.stdout || '', stderr: err.stderr || ''}
23 }
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'}
34 })
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'}
41 })
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'}
48 })
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'}
58 })
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'}
66 })
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'}
74 })
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'}
80 })
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)'}
90 const markerRe = new RegExp(`\\b${historyHeaderMarker}\\b`, 'g')
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'}
95 })
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'}
104 })
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'}
119 })
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'}
125 })
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`}
134 })
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'}
142 })
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'}
150 })
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]
156 const {tests, test} = createTestCollector()
157 clusterCliTests(test, epPath)
158 return runTestsWithProgress({tests, suiteName: 'clitests', failFast: true})
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]
164 const {tests, test} = createTestCollector()
165 genericCliTests(test, epPath)
166 return runTestsWithProgress({tests, suiteName: 'clitests', failFast: true})
169/** DB entrypoint CLI tests - includes dbconsole test */
170export const runDbCliTestsSuite: SuiteRunner = async ({ appCfg }) => {
171 const epPath = appCfg?.importMetaUrl ? fileURLToPath(appCfg.importMetaUrl) : getProcArgv()[1]
172 const {tests, test} = createTestCollector()
173 genericCliTests(test, epPath)
174 dbCliTests(test, epPath)
175 return runTestsWithProgress({tests, suiteName: 'clitests', failFast: true})