@@ -212,6 +212,66 @@ describe("main", () => {
212212 }
213213 } ) ;
214214
215+ it ( "prints only the recent tail for logs command" , async ( ) => {
216+ const logDir = mkdtempSync ( join ( tmpdir ( ) , "cs-cli-logs-tail-" ) ) ;
217+ const outFile = join ( logDir , "server.out.log" ) ;
218+ const errFile = join ( logDir , "server.err.log" ) ;
219+ const outLines = Array . from ( { length : 45 } , ( _ , index ) => `out line ${ index + 1 } ` ) ;
220+ const errLines = [ "err line 1" , "err line 2" ] ;
221+ writeFileSync ( outFile , `${ outLines . join ( "\n" ) } \n` , "utf-8" ) ;
222+ writeFileSync ( errFile , `${ errLines . join ( "\n" ) } \n` , "utf-8" ) ;
223+ getServerStatus . mockResolvedValue ( {
224+ status : "running" ,
225+ pid : 424242 ,
226+ host : "127.0.0.1" ,
227+ port : 4187 ,
228+ restartCount : 2 ,
229+ outFile,
230+ errFile,
231+ startedAt : 1000 ,
232+ } ) ;
233+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } ) ;
234+
235+ try {
236+ await main ( [ "logs" ] ) ;
237+ expect ( logSpy ) . toHaveBeenCalledWith (
238+ `${ outLines . slice ( - 40 ) . join ( "\n" ) } \n${ errLines . join ( "\n" ) } `
239+ ) ;
240+ } finally {
241+ if ( existsSync ( logDir ) ) {
242+ rmSync ( logDir , { recursive : true , force : true } ) ;
243+ }
244+ }
245+ } ) ;
246+
247+ it ( "prints only the requested error log tail for logs command" , async ( ) => {
248+ const logDir = mkdtempSync ( join ( tmpdir ( ) , "cs-cli-logs-errors-" ) ) ;
249+ const outFile = join ( logDir , "server.out.log" ) ;
250+ const errFile = join ( logDir , "server.err.log" ) ;
251+ writeFileSync ( outFile , "out line 1\nout line 2\n" , "utf-8" ) ;
252+ writeFileSync ( errFile , "err line 1\nerr line 2\n" , "utf-8" ) ;
253+ getServerStatus . mockResolvedValue ( {
254+ status : "running" ,
255+ pid : 424242 ,
256+ host : "127.0.0.1" ,
257+ port : 4187 ,
258+ restartCount : 2 ,
259+ outFile,
260+ errFile,
261+ startedAt : 1000 ,
262+ } ) ;
263+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } ) ;
264+
265+ try {
266+ await main ( [ "logs" , "--errors-only" , "--tail" , "1" ] ) ;
267+ expect ( logSpy ) . toHaveBeenCalledWith ( "err line 2" ) ;
268+ } finally {
269+ if ( existsSync ( logDir ) ) {
270+ rmSync ( logDir , { recursive : true , force : true } ) ;
271+ }
272+ }
273+ } ) ;
274+
215275 it ( "prints stop output for stop command" , async ( ) => {
216276 stopRunningServer . mockResolvedValue ( true ) ;
217277 const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } ) ;
@@ -383,6 +443,18 @@ describe("main", () => {
383443 expect ( openBrowser ) . toHaveBeenCalledWith ( "http://127.0.0.1:4190" ) ;
384444 } ) ;
385445
446+ it ( "rethrows background startup failures for open and does not open the browser" , async ( ) => {
447+ const startupError = new Error (
448+ "Coder Studio failed to start in background: the managed process entered the errored state.\n\nRecent error log excerpt (/tmp/server.err.log):\nError: listen EADDRINUSE: address already in use 127.0.0.1:4187\n\nRun `coder-studio logs` for details or `coder-studio serve --foreground` for interactive debugging."
449+ ) ;
450+ startManagedServer . mockRejectedValueOnce ( startupError ) ;
451+
452+ await expect ( main ( [ "open" ] ) ) . rejects . toThrow (
453+ "Error: listen EADDRINUSE: address already in use 127.0.0.1:4187"
454+ ) ;
455+ expect ( openBrowser ) . not . toHaveBeenCalled ( ) ;
456+ } ) ;
457+
386458 it ( "does not restart in non-interactive mode and prints a clear message" , async ( ) => {
387459 getServerStatus . mockResolvedValue ( {
388460 status : "running" ,
@@ -512,6 +584,20 @@ describe("parseArgs", () => {
512584 } ) ;
513585 } ) ;
514586
587+ it ( "parses logs command with tail count" , ( ) => {
588+ expect ( parseArgs ( [ "logs" , "--tail" , "100" ] ) ) . toEqual ( {
589+ command : "logs" ,
590+ tail : 100 ,
591+ } ) ;
592+ } ) ;
593+
594+ it ( "parses logs command with errors-only flag" , ( ) => {
595+ expect ( parseArgs ( [ "logs" , "--errors-only" ] ) ) . toEqual ( {
596+ command : "logs" ,
597+ errorsOnly : true ,
598+ } ) ;
599+ } ) ;
600+
515601 it ( "parses open command" , ( ) => {
516602 expect ( parseArgs ( [ "open" ] ) ) . toEqual ( {
517603 command : "open" ,
@@ -633,6 +719,26 @@ describe("parseArgs", () => {
633719 expect ( ( ) => parseArgs ( [ "logs" , "--port" , "4186" ] ) ) . toThrow ( "Unknown option: --port" ) ;
634720 } ) ;
635721
722+ it ( "rejects logs tail with a non-numeric value" , ( ) => {
723+ expect ( ( ) => parseArgs ( [ "logs" , "--tail" , "nope" ] ) ) . toThrow ( "Invalid tail number" ) ;
724+ } ) ;
725+
726+ it ( "rejects logs tail with zero" , ( ) => {
727+ expect ( ( ) => parseArgs ( [ "logs" , "--tail" , "0" ] ) ) . toThrow ( "Invalid tail number" ) ;
728+ } ) ;
729+
730+ it ( "rejects logs tail with a negative value" , ( ) => {
731+ expect ( ( ) => parseArgs ( [ "logs" , "--tail" , "-1" ] ) ) . toThrow ( "Invalid tail number" ) ;
732+ } ) ;
733+
734+ it ( "rejects logs tail with a decimal value" , ( ) => {
735+ expect ( ( ) => parseArgs ( [ "logs" , "--tail" , "1.5" ] ) ) . toThrow ( "Invalid tail number" ) ;
736+ } ) ;
737+
738+ it ( "rejects logs tail with trailing garbage" , ( ) => {
739+ expect ( ( ) => parseArgs ( [ "logs" , "--tail" , "10junk" ] ) ) . toThrow ( "Invalid tail number" ) ;
740+ } ) ;
741+
636742 it ( "rejects stop-time data-dir overrides" , ( ) => {
637743 expect ( ( ) => parseArgs ( [ "stop" , "--data-dir" , "/tmp/cs-data" ] ) ) . toThrow (
638744 "Unknown option: --data-dir"
0 commit comments