From de9d14a2bd5d84e8f2ef00269198d089ddae7081 Mon Sep 17 00:00:00 2001 From: Talha Amjad Date: Fri, 30 Jan 2026 08:46:37 +0500 Subject: [PATCH] Fix failing deploy test suite --- package-lock.json | 2 +- package.json | 2 +- src/cli/args.ts | 9 +--- src/cli/messages.ts | 17 ++++++-- src/index.ts | 9 ++-- src/startup.ts | 6 ++- src/test/cli.integration.spec.ts | 32 ++++++++++----- src/test/cli.ts | 70 ++++++++++++++++++++++++-------- 8 files changed, 101 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02928e6..3525bf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8941,4 +8941,4 @@ "integrity": "sha512-UzIwO92D0dSFwIRyyqAfRXICITLjF0IP8tRbEK/un7adirMssWZx8xF/1hZNE7t61knWZ+lhEuUvxlu2MO8qqA==" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4b011ed..32c5ec4 100644 --- a/package.json +++ b/package.json @@ -128,4 +128,4 @@ "prettier": "^2.1.2", "typescript": "^4.3.2" } -} \ No newline at end of file +} diff --git a/src/cli/args.ts b/src/cli/args.ts index f6059e0..6092596 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -151,13 +151,8 @@ const args = ((): CLIArgs & { _unknown: Array } => { parsedArgs['dev'] = true; } - // Initialize default working directory - if (parsedArgs['workdir'] === '') { - parsedArgs['workdir'] = process.cwd(); - } - - // Initialize default project name - if (parsedArgs['projectName'] === '') { + // Initialize default project name only when workdir was provided + if (parsedArgs['projectName'] === '' && parsedArgs['workdir'] !== '') { parsedArgs['projectName'] = basename(parsedArgs['workdir']); } diff --git a/src/cli/messages.ts b/src/cli/messages.ts index 8019717..f5d80b0 100644 --- a/src/cli/messages.ts +++ b/src/cli/messages.ts @@ -19,13 +19,24 @@ export const error = (message: string, exitCode = 1): never => { return process.exit(exitCode); }; export const apiError = (err: ProtocolError): never => { + const response = err.response as + | { status?: number; data?: unknown } + | undefined; + const status = + response && typeof response.status === 'number' + ? response.status + : undefined; + const data = response?.data; + const statusStr = status !== undefined ? String(status) : ''; + const dataStr = + data !== undefined && data !== null + ? String(data) + : String(err.message ?? ''); // eslint-disable-next-line no-console console.error( chalk.redBright.bold('X') + chalk.redBright( - ` Server responded with error code: ${ - err.response?.status || '' - } ${err.response?.data as string}` + ` Server responded with error code: ${statusStr} ${dataStr}`.trim() ) ); return process.exit(1); diff --git a/src/index.ts b/src/index.ts index 3ab86b4..7c20395 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,10 +39,11 @@ void (async () => { if (args['logout']) return logout(); const config = await startup(args['confDir']); - const api: APIInterface = API( - config.token as string, - args['dev'] ? config.devURL : config.baseURL - ); + const baseURL = + process.env.TEST_DEPLOY_LOCAL === 'true' || args['dev'] + ? config.devURL + : config.baseURL; + const api: APIInterface = API(config.token as string, baseURL); await validateToken(api); diff --git a/src/startup.ts b/src/startup.ts index 593a5f8..755a62f 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -9,7 +9,7 @@ import { auth } from './auth'; import args from './cli/args'; -import { Config, defaultPath, load } from './config'; +import { Config, defaultPath, load, save } from './config'; const devToken = 'local'; // Use some random token in order to proceed @@ -17,5 +17,9 @@ export const startup = async (confDir: string | undefined): Promise => { const config = await load(confDir || defaultPath); const token = args['dev'] ? devToken : await auth(config); + if (args['dev'] && token === devToken) { + await save({ token }, confDir || defaultPath); + } + return Object.assign(config, { token }); }; diff --git a/src/test/cli.integration.spec.ts b/src/test/cli.integration.spec.ts index 0ff2847..0fc0fce 100644 --- a/src/test/cli.integration.spec.ts +++ b/src/test/cli.integration.spec.ts @@ -12,11 +12,21 @@ import { runCLI } from './cli'; +// Strip ANSI escape codes so assertions match plain text (CLI uses chalk) +const ansiCsiRe = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'); +const stripAnsi = (s: string | unknown): string => + String(s).replace(ansiCsiRe, ''); + describe('Integration CLI (Deploy)', function () { this.timeout(2000000); const url = 'https://github.com/metacall/examples'; - const addRepoSuffix = 'metacall-examples'; + // FaaS uses repositoryName(url) = last path segment, so "examples" not "metacall-examples" + const addRepoSuffix = + url + .split('/') + .pop() + ?.replace(/\.git$/, '') ?? 'metacall-examples'; const workDirSuffix = 'time-app-web'; const filePath = join( @@ -132,7 +142,7 @@ describe('Integration CLI (Deploy)', function () { [keys.enter, 'n', keys.enter, keys.kill] ).promise; - ok(String(result).includes('i Deploying...\n')); + ok(stripAnsi(result).includes('i Deploying...\n')); strictEqual(await deployed(addRepoSuffix), true); return result; @@ -168,7 +178,7 @@ describe('Integration CLI (Deploy)', function () { const result = await runCLI(['--delete'], [keys.enter, keys.enter]) .promise; - ok(String(result).includes('i Deploy Delete Succeed\n')); + ok(stripAnsi(result).includes('i Deploy Delete Succeed\n')); strictEqual(await deleted(addRepoSuffix), true); @@ -182,7 +192,7 @@ describe('Integration CLI (Deploy)', function () { [keys.enter, 'n', keys.enter, keys.kill] ).promise; - ok(String(result).includes(`i Deploying ${filePath}...\n`)); + ok(stripAnsi(result).includes(`i Deploying ${filePath}...\n`)); strictEqual(await deployed(workDirSuffix), true); return result; @@ -193,7 +203,7 @@ describe('Integration CLI (Deploy)', function () { const result = await runCLI(['--delete'], [keys.enter, keys.enter]) .promise; - ok(String(result).includes('i Deploy Delete Succeed\n')); + ok(stripAnsi(result).includes('i Deploy Delete Succeed\n')); strictEqual(await deleted(workDirSuffix), true); @@ -214,7 +224,7 @@ describe('Integration CLI (Deploy)', function () { ] ).promise; - ok(String(result).includes('i Deploying...\n')); + ok(stripAnsi(result).includes('i Deploying...\n')); strictEqual(await deployed(addRepoSuffix), true); return result; @@ -225,7 +235,7 @@ describe('Integration CLI (Deploy)', function () { const result = await runCLI(['--delete'], [keys.enter, keys.enter]) .promise; - ok(String(result).includes('i Deploy Delete Succeed\n')); + ok(stripAnsi(result).includes('i Deploy Delete Succeed\n')); strictEqual(await deleted(workDirSuffix), true); @@ -247,7 +257,7 @@ describe('Integration CLI (Deploy)', function () { [keys.enter, keys.kill] ).promise; - ok(String(result).includes(`i Deploying ${projectPath}...\n`)); + ok(stripAnsi(result).includes(`i Deploying ${projectPath}...\n`)); strictEqual(await deployed('env'), true); return result; @@ -258,7 +268,7 @@ describe('Integration CLI (Deploy)', function () { const result = await runCLI(['--delete'], [keys.enter, keys.enter]) .promise; - ok(String(result).includes('i Deploy Delete Succeed\n')); + ok(stripAnsi(result).includes('i Deploy Delete Succeed\n')); strictEqual(await deleted('env'), true); @@ -276,7 +286,7 @@ describe('Integration CLI (Deploy)', function () { [keys.enter, 'n', keys.enter, keys.kill] ).promise; - ok(String(result).includes(`i Deploying ${filePath}...\n`)); + ok(stripAnsi(result).includes(`i Deploying ${filePath}...\n`)); strictEqual(await deployed(workDirSuffix), true); @@ -326,7 +336,7 @@ describe('Integration CLI (Deploy)', function () { const result = await runCLI(['--delete'], [keys.enter, keys.enter]) .promise; - ok(String(result).includes('i Deploy Delete Succeed\n')); + ok(stripAnsi(result).includes('i Deploy Delete Succeed\n')); strictEqual(await deleted(workDirSuffix), true); diff --git a/src/test/cli.ts b/src/test/cli.ts index 7b11b37..bea4f72 100644 --- a/src/test/cli.ts +++ b/src/test/cli.ts @@ -88,31 +88,53 @@ export const runWithInput = ( }, 3000); }; + const stderrChunks: Buffer[] = []; + child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk)); + + const isRealError = (stderrText: string): boolean => { + const trimmed = stderrText.trim(); + if (!trimmed) return false; + // Ignore Node deprecation warnings (e.g. util.isArray, DEP0044) + if (/DeprecationWarning|DEP\d+|util\.isArray/i.test(trimmed)) + return false; + if (/\(node:\d+\)\s*\[DEP/.test(trimmed)) return false; + // Real errors: CLI error prefix or HTTP/request errors + return ( + trimmed.startsWith('X ') || + trimmed.startsWith('! ') || + trimmed.startsWith('Error:') || + /Request failed|status code \d{3}/.test(trimmed) + ); + }; + return { promise: new Promise((resolve, reject) => { - child.stderr?.once('data', err => { - child.stdin?.end(); - - if (childTimeout) { - clearTimeout(childTimeout); - inputs = []; - } - reject(String(err)); - }); - child.on('error', reject); loop(inputs); child.stdout?.pipe( concat(result => { - if (killTimeout) { - clearTimeout(killTimeout); + if (killTimeout) clearTimeout(killTimeout); + if (childTimeout) clearTimeout(childTimeout); + const stderrText = Buffer.concat(stderrChunks).toString(); + if (isRealError(stderrText)) { + child.stdin?.end(); + reject(stderrText); + } else { + resolve(result.toString()); } - - resolve(result.toString()); }) ); + + // If process exits before stdout ends, settle with buffered stderr + child.on('close', (code, signal) => { + if (childTimeout) clearTimeout(childTimeout); + if (code !== 0 && signal !== 'SIGTERM') { + const stderrText = Buffer.concat(stderrChunks).toString(); + if (isRealError(stderrText)) reject(stderrText); + } + }); }), child }; @@ -130,7 +152,11 @@ export const keys = Object.freeze({ export const deployed = async (suffix: string): Promise => { const config = await startup(args['confDir']); - const api = API(config.token as string, config.baseURL); + const baseURL = + process.env.TEST_DEPLOY_LOCAL === 'true' + ? config.devURL + : config.baseURL; + const api = API(config.token as string, baseURL); const sleep = (ms: number): Promise> => new Promise(resolve => setTimeout(resolve, ms)); @@ -162,7 +188,11 @@ export const deployed = async (suffix: string): Promise => { export const deleted = async (suffix: string): Promise => { const config = await startup(args['confDir']); - const api = API(config.token as string, config.baseURL); + const baseURL = + process.env.TEST_DEPLOY_LOCAL === 'true' + ? config.devURL + : config.baseURL; + const api = API(config.token as string, baseURL); const sleep = (ms: number): Promise> => new Promise(resolve => setTimeout(resolve, ms)); @@ -198,10 +228,14 @@ export const generateRandomString = (length: number): string => { }; export const runCLI = (args: string[], inputs: string[]) => { - if (process.env.TEST_DEPLOY_LOCAL === 'true') { + const useLocal = process.env.TEST_DEPLOY_LOCAL === 'true'; + if (useLocal) { args.push('--dev'); } - return runWithInput('dist/index.js', args, inputs); + const env: Record = useLocal + ? { TEST_DEPLOY_LOCAL: 'true' } + : {}; + return runWithInput('dist/index.js', args, inputs, env); }; export const clearCache = async (): Promise => {