diff --git a/bin/run b/bin/run index 6d90684..1708d35 100755 --- a/bin/run +++ b/bin/run @@ -19,6 +19,15 @@ governing permissions and limitations under the License. process.stderr.setMaxListeners(30) process.stdout.setMaxListeners(30) +// `@oclif/core` v4 exposes `handle` and `flush` as named exports on their +// respective subpath modules. Passing the module object (instead of the +// function) to `Promise#catch` / `Promise#then` silently drops the +// handler: errors escape to Node's default uncaught-exception printer +// instead of oclif's renderer, and `flush()` is never invoked. +// Destructure the functions explicitly so both paths run as intended. +const { handle } = require('@oclif/core/handle') +const { flush } = require('@oclif/core/flush') + require('../src/').run() - .then(() => require('@oclif/core/flush')) - .catch(require('@oclif/core/handle')) + .then(flush) + .catch(handle) diff --git a/e2e/e2e.js b/e2e/e2e.js index a50c2fe..c25b77d 100644 --- a/e2e/e2e.js +++ b/e2e/e2e.js @@ -10,6 +10,7 @@ governing permissions and limitations under the License. */ const execa = require('execa') +const path = require('path') const fs = jest.requireActual('fs') const util = require('util') const fse = { @@ -17,8 +18,37 @@ const fse = { rm: util.promisify(fs.rm) } +const BIN_RUN = path.resolve(__dirname, '../bin/run') + jest.setTimeout(120000) +test('errors render through oclif, not Node\'s uncaught-exception printer', async () => { + // Spawns the real bin/run with an argv guaranteed to trigger a flag + // parse error from `@oclif/core`. When the bin's `.catch` handler is + // wired correctly, oclif's renderer produces a clean single-line + // `Error: …` message. When the handler is dropped (regression + // tracked in adobe/aio-cli#829 / ACNA-4659), the rejection escapes + // to Node and the output instead contains a source frame, a caret, + // a stack trace, and a `Node.js vXX` footer. Asserting on the + // absence of those Node-specific markers detects the regression + // without coupling to oclif's exact prefix. + const result = await execa('node', [BIN_RUN, 'app', 'use', '--no-such-flag'], + { reject: false }) + + // Non-zero exit on a parse error is part of the contract. + expect(result.exitCode).not.toBe(0) + + // The user-facing message must be present so the customer knows what + // went wrong; the framework renders it on stderr. + expect(result.stderr).toMatch(/Nonexistent flag.*--no-such-flag/) + + // None of these substrings should appear: each is a fingerprint of + // Node's default unhandled-rejection / uncaught-exception output. + expect(result.stderr).not.toMatch(/at processTicksAndRejections/) + expect(result.stderr).not.toMatch(/^Node\.js v\d+/m) + expect(result.stderr).not.toMatch(/^\s+\^\s*$/m) +}) + test('cli init test', async () => { const testFolder = 'e2e_test_run'