@@ -232,6 +232,48 @@ test('mutating results carry a stable untranslated nextStep in JSON', () => {
232232 assert . match ( en . stderr , / n e x t / i)
233233} )
234234
235+ function runTty ( args , cwd = process . cwd ( ) , env = { } ) {
236+ const url = String ( new URL ( '../cli.mjs' , import . meta. url ) )
237+ const script = `process.stdout.isTTY = true; process.argv = [process.execPath, ${ JSON . stringify ( CLI ) } , ${ args . map ( a => JSON . stringify ( String ( a ) ) ) . join ( ', ' ) } ]; await import(${ JSON . stringify ( url ) } )`
238+ return spawnSync ( process . execPath , [ '--input-type=module' , '-e' , script ] , { cwd, encoding : 'utf8' , env : { ...process . env , ...env } } )
239+ }
240+
241+ test ( 'jsonEnabled routes the JSON surface by environment and explicit flags' , async ( ) => {
242+ const { jsonEnabled } = await import ( '../lib/output.mjs' )
243+ const tty = process . stdout . isTTY , env = process . env . SHADOW_DEV_JSON
244+ try {
245+ process . stdout . isTTY = false
246+ assert . equal ( jsonEnabled ( { } ) , true , 'pipe default emits JSON' )
247+ assert . equal ( jsonEnabled ( { json : true } ) , true )
248+ process . stdout . isTTY = true
249+ assert . equal ( jsonEnabled ( { } ) , false , 'TTY default suppresses JSON' )
250+ assert . equal ( jsonEnabled ( { json : true } ) , true , '--json forces JSON on TTY' )
251+ process . stdout . isTTY = undefined
252+ process . env . SHADOW_DEV_JSON = '1'
253+ assert . equal ( jsonEnabled ( { } ) , true , 'env override forces JSON' )
254+ } finally {
255+ process . stdout . isTTY = tty
256+ if ( env === undefined ) delete process . env . SHADOW_DEV_JSON ; else process . env . SHADOW_DEV_JSON = env
257+ }
258+ } )
259+
260+ test ( 'TTY suppresses stdout JSON; --json and env restore it; planHash surfaces on stderr' , ( ) => {
261+ const plain = runTty ( [ '--help' ] )
262+ assert . equal ( plain . status , 0 , plain . stderr )
263+ assert . equal ( plain . stdout . trim ( ) , '' , 'interactive help must not print JSON' )
264+ assert . match ( plain . stderr , / s h a d o w - d e v 命 令 一 览 / )
265+ const forced = runTty ( [ '--help' , '--json' ] )
266+ assert . equal ( JSON . parse ( forced . stdout ) . command , 'help' )
267+ const root = fixture ( )
268+ assert . equal ( JSON . parse ( runTty ( [ 'repo' , 'inspect' ] , root , { SHADOW_DEV_JSON : '1' } ) . stdout ) . command , 'repo.inspect' )
269+ const planned = runTty ( [ 'branch' , 'plan' , '--name' , 'sample' ] , root )
270+ assert . equal ( planned . stdout . trim ( ) , '' )
271+ assert . match ( planned . stderr , / p l a n H a s h : [ 0 - 9 a - f ] { 64 } / )
272+ const bogus = runTty ( [ 'bogus' ] , root )
273+ assert . equal ( bogus . status , 1 )
274+ assert . equal ( bogus . stdout . trim ( ) , '' , 'suppressed errors still exit nonzero without printing' )
275+ } )
276+
235277test ( 'help defaults to a compact summary; --full adds the structured catalog' , ( ) => {
236278 const overview = run ( [ 'help' , '--lang' , 'zh' ] )
237279 const data = JSON . parse ( overview . stdout ) . data
0 commit comments