11import { afterEach , beforeEach , describe , test , vi } from "vitest"
22import { NodeModulesCollector } from "app-builder-lib/src/node-module-collector/nodeModulesCollector"
3+ import { LogMessageByKey } from "app-builder-lib/src/node-module-collector/moduleManager"
34import { PM } from "app-builder-lib/src/node-module-collector/packageManager"
45import * as childProcess from "child_process"
56import * as fsExtra from "fs-extra"
@@ -19,10 +20,13 @@ class TestCollector extends NodeModulesCollector<any, any> {
1920 }
2021 protected async extractProductionDependencyGraph ( ) { }
2122 protected async collectAllDependencies ( ) { }
22- // expose protected method for testing
23+ // expose protected members for testing
2324 streamCollectorCommandToFile ( command : string , args : string [ ] , cwd : string , tempOutputFile : string ) {
2425 return super . streamCollectorCommandToFile ( command , args , cwd , tempOutputFile )
2526 }
27+ get logSummary ( ) {
28+ return this . cache . logSummary
29+ }
2630}
2731
2832const BAT_PATH = "C:\\Temp\\pnpm-deadbeef.bat"
@@ -36,6 +40,7 @@ function setPlatform(p: NodeJS.Platform) {
3640
3741let collector : TestCollector
3842let closeCb : ( ( code : number ) => void ) | undefined
43+ let stderrDataCb : ( ( chunk : string ) => void ) | undefined
3944
4045async function waitForCloseCb ( ) {
4146 for ( let i = 0 ; i < 50 && closeCb === undefined ; i ++ ) {
@@ -48,9 +53,14 @@ async function waitForCloseCb() {
4853
4954beforeEach ( ( ) => {
5055 closeCb = undefined
56+ stderrDataCb = undefined
5157 const mockChild = {
5258 stdout : { pipe : vi . fn ( ) } ,
53- stderr : { on : vi . fn ( ) } ,
59+ stderr : {
60+ on : vi . fn ( ( ev : string , cb : ( chunk : string ) => void ) => {
61+ if ( ev === "data" ) stderrDataCb = cb
62+ } ) ,
63+ } ,
5464 on : vi . fn ( ( ev : string , cb : ( code : number ) => void ) => {
5565 if ( ev === "close" ) closeCb = cb
5666 } ) ,
@@ -142,6 +152,7 @@ describe.sequential("streamCollectorCommandToFile", () => {
142152 setPlatform ( "win32" )
143153 const cmd = "C:\\tools\\pnpm.exe"
144154 const p = collector . streamCollectorCommandToFile ( cmd , [ ] , "/cwd" , OUTPUT_FILE )
155+ await waitForCloseCb ( )
145156 closeCb ! ( 0 )
146157 await p
147158 expect ( vi . mocked ( childProcess . spawn ) . mock . calls [ 0 ] [ 0 ] ) . toBe ( cmd )
@@ -151,6 +162,7 @@ describe.sequential("streamCollectorCommandToFile", () => {
151162 test ( "extensionless at path without spaces: spawn receives original command" , async ( { expect } ) => {
152163 setPlatform ( "win32" )
153164 const p = collector . streamCollectorCommandToFile ( "pnpm" , [ ] , "/cwd" , OUTPUT_FILE )
165+ await waitForCloseCb ( )
154166 closeCb ! ( 0 )
155167 await p
156168 expect ( vi . mocked ( childProcess . spawn ) . mock . calls [ 0 ] [ 0 ] ) . toBe ( "pnpm" )
@@ -161,10 +173,72 @@ describe.sequential("streamCollectorCommandToFile", () => {
161173 setPlatform ( "darwin" )
162174 const cmd = "/Applications/Volta/pnpm.exe"
163175 const p = collector . streamCollectorCommandToFile ( cmd , [ ] , "/cwd" , OUTPUT_FILE )
176+ await waitForCloseCb ( )
164177 closeCb ! ( 0 )
165178 await p
166179 expect ( vi . mocked ( childProcess . spawn ) . mock . calls [ 0 ] [ 0 ] ) . toBe ( cmd )
167180 expect ( vi . mocked ( fsExtra . writeFile ) ) . not . toHaveBeenCalled ( )
168181 } )
169182 } )
183+
184+ describe ( "stderr and exit code behavior" , ( ) => {
185+ test ( "npm list exit code 1: stderr is NOT surfaced as a collector warning" , async ( { expect } ) => {
186+ const p = collector . streamCollectorCommandToFile ( "npm" , [ "list" , "--json" ] , "/cwd" , OUTPUT_FILE )
187+ await waitForCloseCb ( )
188+ stderrDataCb ?.( "npm error code ELSPROBLEMS\nnpm error invalid: canvas@npm:npm-empty-stub@1.0.1\n" )
189+ closeCb ! ( 1 )
190+ await p
191+ expect ( collector . logSummary [ LogMessageByKey . PKG_COLLECTOR_OUTPUT ] ) . toHaveLength ( 0 )
192+ } )
193+
194+ test ( "npm list exit code 0 with stderr: stderr IS surfaced as a collector warning" , async ( { expect } ) => {
195+ const p = collector . streamCollectorCommandToFile ( "npm" , [ "list" , "--json" ] , "/cwd" , OUTPUT_FILE )
196+ await waitForCloseCb ( )
197+ stderrDataCb ?.( "npm warn some unexpected warning\n" )
198+ closeCb ! ( 0 )
199+ await p
200+ const output = collector . logSummary [ LogMessageByKey . PKG_COLLECTOR_OUTPUT ]
201+ expect ( output ) . toHaveLength ( 1 )
202+ expect ( output [ 0 ] ) . toContain ( "npm warn some unexpected warning" )
203+ } )
204+
205+ test ( "non-npm command exit code 1: rejects with stderr in error message" , async ( { expect } ) => {
206+ const p = collector . streamCollectorCommandToFile ( "pnpm" , [ "list" , "--json" ] , "/cwd" , OUTPUT_FILE )
207+ await waitForCloseCb ( )
208+ stderrDataCb ?.( "pnpm error: something went wrong\n" )
209+ closeCb ! ( 1 )
210+ await expect ( p ) . rejects . toThrow ( "pnpm error: something went wrong" )
211+ } )
212+
213+ test ( "npm command (not list) exit code 1: rejects" , async ( { expect } ) => {
214+ const p = collector . streamCollectorCommandToFile ( "npm" , [ "install" ] , "/cwd" , OUTPUT_FILE )
215+ await waitForCloseCb ( )
216+ stderrDataCb ?.( "npm error: install failed\n" )
217+ closeCb ! ( 1 )
218+ await expect ( p ) . rejects . toThrow ( )
219+ } )
220+
221+ test ( "npm list with full path exit code 1: stderr suppressed (shouldIgnore applies to basename)" , async ( { expect } ) => {
222+ const p = collector . streamCollectorCommandToFile ( "/usr/local/bin/npm" , [ "list" , "--json" ] , "/cwd" , OUTPUT_FILE )
223+ await waitForCloseCb ( )
224+ stderrDataCb ?.( "npm error code ELSPROBLEMS\nnpm error invalid: some-pkg@npm:stub@1.0.0\n" )
225+ closeCb ! ( 1 )
226+ await p
227+ expect ( collector . logSummary [ LogMessageByKey . PKG_COLLECTOR_OUTPUT ] ) . toHaveLength ( 0 )
228+ } )
229+
230+ test ( "win32 npm.cmd wrapped via cmd.exe: exit code 1 still triggers shouldIgnore" , async ( { expect } ) => {
231+ // Regression: execName must be derived from the original command ("npm"), not the rewritten
232+ // cmd.exe invocation, otherwise shouldIgnore never fires and npm list exit code 1 rejects.
233+ // Use "npm.cmd" without a backslash-prefixed directory so path.basename works cross-platform
234+ // in the test environment (macOS path.basename ignores backslash separators).
235+ setPlatform ( "win32" )
236+ const p = collector . streamCollectorCommandToFile ( "npm.cmd" , [ "list" , "--json" ] , "/cwd" , OUTPUT_FILE )
237+ await waitForCloseCb ( )
238+ stderrDataCb ?.( "npm error code ELSPROBLEMS\nnpm error invalid: canvas@npm:npm-empty-stub@1.0.1\n" )
239+ closeCb ! ( 1 )
240+ await p
241+ expect ( collector . logSummary [ LogMessageByKey . PKG_COLLECTOR_OUTPUT ] ) . toHaveLength ( 0 )
242+ } )
243+ } )
170244} )
0 commit comments