@@ -579,6 +579,117 @@ test("#1938: a server with no investigation key is not reported as 'no investiga
579579 }
580580} ) ;
581581
582+ // -- #6: `projects show` was advertised in help but never dispatched ---------
583+
584+ const PROJECT_DETAIL = {
585+ id : 108 ,
586+ name : "PilotLight" ,
587+ project_type : "godot" ,
588+ tasks_count : 12 ,
589+ build_runs_count : 34 ,
590+ assets_count : 56 ,
591+ pending_assets_count : 3 ,
592+ latest_build : {
593+ id : BUILD_ID ,
594+ status : "failed" ,
595+ target_type : null ,
596+ commit_hash : "p4-207" ,
597+ duration : 0.764199 ,
598+ created_at : "2026-09-18T10:55:02-04:00"
599+ }
600+ } ;
601+
602+ test ( "#6: `projects show` renders a project instead of failing as an unknown subcommand" , async ( ) => {
603+ const home = mkHome ( ) ;
604+ const { server, port, requests } = await startApiServer ( {
605+ "GET /api/v1/projects/108" : { body : PROJECT_DETAIL }
606+ } ) ;
607+ try {
608+ writeCredentials ( home , { host : `http://127.0.0.1:${ port } ` } ) ;
609+ const { stdout, stderr, success } = await runButter ( [ "projects" , "show" , "108" ] , { home } ) ;
610+
611+ assert . equal ( success , true , `expected success, got stderr: ${ stderr } ` ) ;
612+ assert . ok ( ! stderr . includes ( "Unknown subcommand" ) ) ;
613+ assert . ok ( stdout . includes ( "PROJECT 108" ) , "the project id should be printed" ) ;
614+ assert . ok ( stdout . includes ( "PilotLight" ) , "the project name should be printed" ) ;
615+ assert . ok ( stdout . includes ( "godot" ) , "the project type should be printed" ) ;
616+ assert . ok ( stdout . includes ( BUILD_ID ) , "the latest build should be printed" ) ;
617+ assert . ok ( stdout . includes ( "p4-207" ) , "the latest build's commit should be printed" ) ;
618+
619+ const req = await requests . pop ( 2000 ) ;
620+ assert . equal ( req . method , "GET" ) ;
621+ assert . equal ( req . url , "/api/v1/projects/108" ) ;
622+ } finally {
623+ await stopCaptureServer ( server ) ;
624+ rmHome ( home ) ;
625+ }
626+ } ) ;
627+
628+ test ( "#6: `projects show` says 'none' when the project has no builds" , async ( ) => {
629+ const home = mkHome ( ) ;
630+ const { server, port } = await startApiServer ( {
631+ "GET /api/v1/projects/108" : { body : { ...PROJECT_DETAIL , latest_build : null } }
632+ } ) ;
633+ try {
634+ writeCredentials ( home , { host : `http://127.0.0.1:${ port } ` } ) ;
635+ const { stdout, success } = await runButter ( [ "projects" , "show" , "108" ] , { home } ) ;
636+ assert . equal ( success , true ) ;
637+ assert . match ( stdout , / L a t e s t b u i l d : \s + ( \x1b \[ [ 0 - 9 ; ] * m ) * n o n e / ) ;
638+ } finally {
639+ await stopCaptureServer ( server ) ;
640+ rmHome ( home ) ;
641+ }
642+ } ) ;
643+
644+ test ( "#6: `projects show --json` works with the flag on either side of the id" , async ( ) => {
645+ const home = mkHome ( ) ;
646+ const { server, port } = await startApiServer ( {
647+ "GET /api/v1/projects/108" : { body : PROJECT_DETAIL }
648+ } ) ;
649+ try {
650+ writeCredentials ( home , { host : `http://127.0.0.1:${ port } ` } ) ;
651+
652+ const before = await runButter ( [ "projects" , "show" , "--json" , "108" ] , { home } ) ;
653+ assert . equal ( before . success , true , `expected success, got stderr: ${ before . stderr } ` ) ;
654+ assert . deepEqual ( JSON . parse ( before . stdout ) , PROJECT_DETAIL ) ;
655+
656+ const after = await runButter ( [ "projects" , "show" , "108" , "--json" ] , { home } ) ;
657+ assert . equal ( after . success , true , `expected success, got stderr: ${ after . stderr } ` ) ;
658+ assert . deepEqual ( JSON . parse ( after . stdout ) , PROJECT_DETAIL ) ;
659+ } finally {
660+ await stopCaptureServer ( server ) ;
661+ rmHome ( home ) ;
662+ }
663+ } ) ;
664+
665+ test ( "#6: `projects show` without an id prints usage and exits non-zero" , async ( ) => {
666+ const home = mkHome ( ) ;
667+ try {
668+ const { stderr, success } = await runButter ( [ "projects" , "show" ] , { home } ) ;
669+ assert . equal ( success , false ) ;
670+ assert . ok ( stderr . includes ( "Usage: butter projects show" ) ) ;
671+ } finally {
672+ rmHome ( home ) ;
673+ }
674+ } ) ;
675+
676+ test ( "#6: `projects show` for a missing project fails loudly" , async ( ) => {
677+ const home = mkHome ( ) ;
678+ const { server, port } = await startApiServer ( {
679+ "GET /api/v1/projects/999" : { status : 404 , body : { error : "Project not found" } }
680+ } ) ;
681+ try {
682+ writeCredentials ( home , { host : `http://127.0.0.1:${ port } ` } ) ;
683+ const { stderr, success } = await runButter ( [ "projects" , "show" , "999" ] , { home } ) ;
684+ assert . equal ( success , false ) ;
685+ assert . ok ( stderr . includes ( "Failed to show project" ) ) ;
686+ assert . ok ( stderr . includes ( "Project not found" ) , "the server's error should be surfaced" ) ;
687+ } finally {
688+ await stopCaptureServer ( server ) ;
689+ rmHome ( home ) ;
690+ }
691+ } ) ;
692+
582693// buildEnv sets BUTTERSTACK_NO_BROWSER for every test; this pins that the
583694// binary honors it.
584695// Buffers the child's full stdout, so tests can assert on the whole block.
0 commit comments