@@ -8,7 +8,7 @@ import { fileExists } from '@app/core/utils/files/file-exists.js';
88import { NodemonService } from '@app/unraid-api/cli/nodemon.service.js' ;
99
1010vi . mock ( 'node:fs' , ( ) => ( {
11- createWriteStream : vi . fn ( ( ) => ( { pipe : vi . fn ( ) } ) ) ,
11+ createWriteStream : vi . fn ( ( ) => ( { pipe : vi . fn ( ) , close : vi . fn ( ) } ) ) ,
1212} ) ) ;
1313vi . mock ( 'node:fs/promises' ) ;
1414vi . mock ( 'execa' , ( ) => ( { execa : vi . fn ( ) } ) ) ;
@@ -57,8 +57,21 @@ describe('NodemonService', () => {
5757 expect ( mockMkdir ) . toHaveBeenCalledWith ( '/var/run/unraid-api' , { recursive : true } ) ;
5858 } ) ;
5959
60+ it ( 'throws error when directory creation fails' , async ( ) => {
61+ const service = new NodemonService ( logger ) ;
62+ const error = new Error ( 'Permission denied' ) ;
63+ mockMkdir . mockRejectedValue ( error ) ;
64+
65+ await expect ( service . ensureNodemonDependencies ( ) ) . rejects . toThrow ( 'Permission denied' ) ;
66+ expect ( mockMkdir ) . toHaveBeenCalledWith ( '/var/log/unraid-api' , { recursive : true } ) ;
67+ } ) ;
68+
6069 it ( 'starts nodemon and writes pid file' , async ( ) => {
6170 const service = new NodemonService ( logger ) ;
71+ const logStream = { pipe : vi . fn ( ) , close : vi . fn ( ) } ;
72+ vi . mocked ( createWriteStream ) . mockReturnValue (
73+ logStream as unknown as ReturnType < typeof createWriteStream >
74+ ) ;
6275 const stdout = { pipe : vi . fn ( ) } ;
6376 const stderr = { pipe : vi . fn ( ) } ;
6477 const unref = vi . fn ( ) ;
@@ -87,6 +100,60 @@ describe('NodemonService', () => {
87100 expect ( unref ) . toHaveBeenCalled ( ) ;
88101 expect ( mockWriteFile ) . toHaveBeenCalledWith ( '/var/run/unraid-api/nodemon.pid' , '123' ) ;
89102 expect ( logger . info ) . toHaveBeenCalledWith ( 'Started nodemon (pid 123)' ) ;
103+ expect ( logStream . close ) . not . toHaveBeenCalled ( ) ;
104+ } ) ;
105+
106+ it ( 'throws error and aborts start when directory creation fails' , async ( ) => {
107+ const service = new NodemonService ( logger ) ;
108+ const error = new Error ( 'Permission denied' ) ;
109+ mockMkdir . mockRejectedValue ( error ) ;
110+
111+ await expect ( service . start ( ) ) . rejects . toThrow ( 'Permission denied' ) ;
112+ expect ( logger . error ) . toHaveBeenCalledWith (
113+ 'Failed to ensure nodemon dependencies: Permission denied'
114+ ) ;
115+ expect ( execa ) . not . toHaveBeenCalled ( ) ;
116+ } ) ;
117+
118+ it ( 'throws error and closes logStream when execa fails' , async ( ) => {
119+ const service = new NodemonService ( logger ) ;
120+ const logStream = { pipe : vi . fn ( ) , close : vi . fn ( ) } ;
121+ vi . mocked ( createWriteStream ) . mockReturnValue (
122+ logStream as unknown as ReturnType < typeof createWriteStream >
123+ ) ;
124+ const error = new Error ( 'Command not found' ) ;
125+ vi . mocked ( execa ) . mockImplementation ( ( ) => {
126+ throw error ;
127+ } ) ;
128+
129+ await expect ( service . start ( ) ) . rejects . toThrow ( 'Failed to start nodemon: Command not found' ) ;
130+ expect ( logStream . close ) . toHaveBeenCalled ( ) ;
131+ expect ( mockWriteFile ) . not . toHaveBeenCalled ( ) ;
132+ expect ( logger . info ) . not . toHaveBeenCalled ( ) ;
133+ } ) ;
134+
135+ it ( 'throws error and closes logStream when pid is missing' , async ( ) => {
136+ const service = new NodemonService ( logger ) ;
137+ const logStream = { pipe : vi . fn ( ) , close : vi . fn ( ) } ;
138+ vi . mocked ( createWriteStream ) . mockReturnValue (
139+ logStream as unknown as ReturnType < typeof createWriteStream >
140+ ) ;
141+ const stdout = { pipe : vi . fn ( ) } ;
142+ const stderr = { pipe : vi . fn ( ) } ;
143+ const unref = vi . fn ( ) ;
144+ vi . mocked ( execa ) . mockReturnValue ( {
145+ pid : undefined ,
146+ stdout,
147+ stderr,
148+ unref,
149+ } as unknown as ReturnType < typeof execa > ) ;
150+
151+ await expect ( service . start ( ) ) . rejects . toThrow (
152+ 'Failed to start nodemon: process spawned but no PID was assigned'
153+ ) ;
154+ expect ( logStream . close ) . toHaveBeenCalled ( ) ;
155+ expect ( mockWriteFile ) . not . toHaveBeenCalled ( ) ;
156+ expect ( logger . info ) . not . toHaveBeenCalled ( ) ;
90157 } ) ;
91158
92159 it ( 'returns not running when pid file is missing' , async ( ) => {
@@ -98,4 +165,44 @@ describe('NodemonService', () => {
98165 expect ( result ) . toBe ( false ) ;
99166 expect ( logger . info ) . toHaveBeenCalledWith ( 'unraid-api is not running (no pid file).' ) ;
100167 } ) ;
168+
169+ it ( 'logs stdout when tail succeeds' , async ( ) => {
170+ const service = new NodemonService ( logger ) ;
171+ vi . mocked ( execa ) . mockResolvedValue ( {
172+ stdout : 'log line 1\nlog line 2' ,
173+ } as unknown as Awaited < ReturnType < typeof execa > > ) ;
174+
175+ const result = await service . logs ( 50 ) ;
176+
177+ expect ( execa ) . toHaveBeenCalledWith ( 'tail' , [ '-n' , '50' , '/var/log/graphql-api.log' ] ) ;
178+ expect ( logger . log ) . toHaveBeenCalledWith ( 'log line 1\nlog line 2' ) ;
179+ expect ( result ) . toBe ( 'log line 1\nlog line 2' ) ;
180+ } ) ;
181+
182+ it ( 'handles ENOENT error when log file is missing' , async ( ) => {
183+ const service = new NodemonService ( logger ) ;
184+ const error = new Error ( 'ENOENT: no such file or directory' ) ;
185+ ( error as Error & { code ?: string } ) . code = 'ENOENT' ;
186+ vi . mocked ( execa ) . mockRejectedValue ( error ) ;
187+
188+ const result = await service . logs ( ) ;
189+
190+ expect ( logger . error ) . toHaveBeenCalledWith (
191+ 'Log file not found: /var/log/graphql-api.log (ENOENT: no such file or directory)'
192+ ) ;
193+ expect ( result ) . toBe ( '' ) ;
194+ } ) ;
195+
196+ it ( 'handles non-zero exit error from tail' , async ( ) => {
197+ const service = new NodemonService ( logger ) ;
198+ const error = new Error ( 'Command failed with exit code 1' ) ;
199+ vi . mocked ( execa ) . mockRejectedValue ( error ) ;
200+
201+ const result = await service . logs ( 100 ) ;
202+
203+ expect ( logger . error ) . toHaveBeenCalledWith (
204+ 'Failed to read logs from /var/log/graphql-api.log: Command failed with exit code 1'
205+ ) ;
206+ expect ( result ) . toBe ( '' ) ;
207+ } ) ;
101208} ) ;
0 commit comments