@@ -258,11 +258,71 @@ describe("agentapi", async () => {
258258 expect ( agentApiStartLog ) . toContain ( "AGENTAPI_ALLOWED_HOSTS: *" ) ;
259259 } ) ;
260260
261+ test ( "state-persistence-disabled" , async ( ) => {
262+ const { id } = await setup ( {
263+ moduleVariables : {
264+ enable_state_persistence : "false" ,
265+ } ,
266+ } ) ;
267+ await execModuleScript ( id ) ;
268+ await expectAgentAPIStarted ( id ) ;
269+ const mockLog = await readFileContainer (
270+ id ,
271+ "/home/coder/agentapi-mock.log" ,
272+ ) ;
273+ // PID file should always be exported
274+ expect ( mockLog ) . toContain ( "AGENTAPI_PID_FILE:" ) ;
275+ // State vars should NOT be present when disabled
276+ expect ( mockLog ) . not . toContain ( "AGENTAPI_STATE_FILE:" ) ;
277+ expect ( mockLog ) . not . toContain ( "AGENTAPI_SAVE_STATE:" ) ;
278+ expect ( mockLog ) . not . toContain ( "AGENTAPI_LOAD_STATE:" ) ;
279+ } ) ;
280+
281+ test ( "state-persistence-custom-paths" , async ( ) => {
282+ const { id } = await setup ( {
283+ moduleVariables : {
284+ state_file_path : "/home/coder/custom/state.json" ,
285+ pid_file_path : "/home/coder/custom/agentapi.pid" ,
286+ } ,
287+ } ) ;
288+ await execModuleScript ( id ) ;
289+ await expectAgentAPIStarted ( id ) ;
290+ const mockLog = await readFileContainer (
291+ id ,
292+ "/home/coder/agentapi-mock.log" ,
293+ ) ;
294+ expect ( mockLog ) . toContain (
295+ "AGENTAPI_STATE_FILE: /home/coder/custom/state.json" ,
296+ ) ;
297+ expect ( mockLog ) . toContain (
298+ "AGENTAPI_PID_FILE: /home/coder/custom/agentapi.pid" ,
299+ ) ;
300+ } ) ;
301+
302+ test ( "state-persistence-default-paths" , async ( ) => {
303+ const { id } = await setup ( ) ;
304+ await execModuleScript ( id ) ;
305+ await expectAgentAPIStarted ( id ) ;
306+ const mockLog = await readFileContainer (
307+ id ,
308+ "/home/coder/agentapi-mock.log" ,
309+ ) ;
310+ expect ( mockLog ) . toContain (
311+ `AGENTAPI_STATE_FILE: /home/coder/${ moduleDirName } /state.json` ,
312+ ) ;
313+ expect ( mockLog ) . toContain (
314+ `AGENTAPI_PID_FILE: /home/coder/${ moduleDirName } /agentapi.pid` ,
315+ ) ;
316+ expect ( mockLog ) . toContain ( "AGENTAPI_SAVE_STATE: true" ) ;
317+ expect ( mockLog ) . toContain ( "AGENTAPI_LOAD_STATE: true" ) ;
318+ } ) ;
319+
261320 describe ( "shutdown script" , async ( ) => {
262321 const setupMocks = async (
263322 containerId : string ,
264323 agentapiPreset : string ,
265324 httpCode : number = 204 ,
325+ pidFilePath : string = "" ,
266326 ) => {
267327 const agentapiMock = await loadTestFile (
268328 import . meta. dir ,
@@ -285,10 +345,11 @@ describe("agentapi", async () => {
285345 content : coderMock ,
286346 } ) ;
287347
348+ const pidFileEnv = pidFilePath ? `AGENTAPI_PID_FILE=${ pidFilePath } ` : "" ;
288349 await execContainer ( containerId , [
289350 "bash" ,
290351 "-c" ,
291- `PRESET=${ agentapiPreset } nohup node /usr/local/bin/mock-agentapi 3284 > /tmp/mock-agentapi.log 2>&1 &` ,
352+ `PRESET=${ agentapiPreset } ${ pidFileEnv } nohup node /usr/local/bin/mock-agentapi 3284 > /tmp/mock-agentapi.log 2>&1 &` ,
292353 ] ) ;
293354
294355 await execContainer ( containerId , [
@@ -303,12 +364,25 @@ describe("agentapi", async () => {
303364 const runShutdownScript = async (
304365 containerId : string ,
305366 taskId : string = "test-task" ,
367+ pidFilePath : string = "" ,
368+ enableStatePersistence : string = "true" ,
306369 ) => {
307370 const shutdownScript = await loadTestFile (
308371 import . meta. dir ,
309372 "../scripts/agentapi-shutdown.sh" ,
310373 ) ;
311374
375+ const libScript = await loadTestFile (
376+ import . meta. dir ,
377+ "../scripts/lib.sh" ,
378+ ) ;
379+
380+ await writeExecutable ( {
381+ containerId,
382+ filePath : "/tmp/agentapi-lib.sh" ,
383+ content : libScript ,
384+ } ) ;
385+
312386 await writeExecutable ( {
313387 containerId,
314388 filePath : "/tmp/shutdown.sh" ,
@@ -318,7 +392,7 @@ describe("agentapi", async () => {
318392 return await execContainer ( containerId , [
319393 "bash" ,
320394 "-c" ,
321- `ARG_TASK_ID=${ taskId } ARG_AGENTAPI_PORT=3284 CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh` ,
395+ `ARG_TASK_ID=${ taskId } ARG_AGENTAPI_PORT=3284 ARG_PID_FILE_PATH= ${ pidFilePath } ARG_ENABLE_STATE_PERSISTENCE= ${ enableStatePersistence } CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh` ,
322396 ] ) ;
323397 } ;
324398
@@ -409,5 +483,126 @@ describe("agentapi", async () => {
409483 "Log snapshot endpoint not supported by this Coder version" ,
410484 ) ;
411485 } ) ;
486+
487+ test ( "sends SIGUSR1 before shutdown" , async ( ) => {
488+ const { id } = await setup ( {
489+ moduleVariables : { } ,
490+ skipAgentAPIMock : true ,
491+ } ) ;
492+ const pidFile = "/tmp/agentapi-test.pid" ;
493+ await setupMocks ( id , "normal" , 204 , pidFile ) ;
494+ const result = await runShutdownScript ( id , "test-task" , pidFile , "true" ) ;
495+
496+ expect ( result . exitCode ) . toBe ( 0 ) ;
497+ expect ( result . stdout ) . toContain ( "Sending SIGUSR1 to AgentAPI" ) ;
498+
499+ const sigusr1Log = await readFileContainer ( id , "/tmp/sigusr1-received" ) ;
500+ expect ( sigusr1Log ) . toContain ( "SIGUSR1 received" ) ;
501+ } ) ;
502+
503+ test ( "handles missing PID file gracefully" , async ( ) => {
504+ const { id } = await setup ( {
505+ moduleVariables : { } ,
506+ skipAgentAPIMock : true ,
507+ } ) ;
508+ await setupMocks ( id , "normal" ) ;
509+ // Pass a non-existent PID file path
510+ const result = await runShutdownScript (
511+ id ,
512+ "test-task" ,
513+ "/tmp/nonexistent.pid" ,
514+ ) ;
515+
516+ expect ( result . exitCode ) . toBe ( 0 ) ;
517+ expect ( result . stdout ) . toContain ( "Shutdown complete" ) ;
518+ } ) ;
519+
520+ test ( "sends SIGTERM even when snapshot fails" , async ( ) => {
521+ const { id } = await setup ( {
522+ moduleVariables : { } ,
523+ skipAgentAPIMock : true ,
524+ } ) ;
525+ const pidFile = "/tmp/agentapi-test.pid" ;
526+ // HTTP 500 will cause snapshot to fail
527+ await setupMocks ( id , "normal" , 500 , pidFile ) ;
528+ const result = await runShutdownScript ( id , "test-task" , pidFile ) ;
529+
530+ expect ( result . exitCode ) . toBe ( 0 ) ;
531+ expect ( result . stdout ) . toContain (
532+ "Log snapshot capture failed, continuing shutdown" ,
533+ ) ;
534+ expect ( result . stdout ) . toContain ( "Sending SIGTERM to AgentAPI" ) ;
535+ } ) ;
536+
537+ test ( "resolves default PID path from MODULE_DIR_NAME" , async ( ) => {
538+ const { id } = await setup ( {
539+ moduleVariables : { } ,
540+ skipAgentAPIMock : true ,
541+ } ) ;
542+ // Start mock with PID file at the module_dir_name default location.
543+ const defaultPidPath = `/home/coder/${ moduleDirName } /agentapi.pid` ;
544+ await setupMocks ( id , "normal" , 204 , defaultPidPath ) ;
545+ // Don't pass pidFilePath - let shutdown script compute it from MODULE_DIR_NAME.
546+ const shutdownScript = await loadTestFile (
547+ import . meta. dir ,
548+ "../scripts/agentapi-shutdown.sh" ,
549+ ) ;
550+ const libScript = await loadTestFile (
551+ import . meta. dir ,
552+ "../scripts/lib.sh" ,
553+ ) ;
554+ await writeExecutable ( {
555+ containerId : id ,
556+ filePath : "/tmp/agentapi-lib.sh" ,
557+ content : libScript ,
558+ } ) ;
559+ await writeExecutable ( {
560+ containerId : id ,
561+ filePath : "/tmp/shutdown.sh" ,
562+ content : shutdownScript ,
563+ } ) ;
564+ const result = await execContainer ( id , [
565+ "bash" ,
566+ "-c" ,
567+ `ARG_TASK_ID=test-task ARG_AGENTAPI_PORT=3284 ARG_MODULE_DIR_NAME=${ moduleDirName } ARG_ENABLE_STATE_PERSISTENCE=true CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh` ,
568+ ] ) ;
569+
570+ expect ( result . exitCode ) . toBe ( 0 ) ;
571+ expect ( result . stdout ) . toContain ( "Sending SIGUSR1 to AgentAPI" ) ;
572+ expect ( result . stdout ) . toContain ( "Sending SIGTERM to AgentAPI" ) ;
573+ } ) ;
574+
575+ test ( "skips SIGUSR1 when no PID file available" , async ( ) => {
576+ const { id } = await setup ( {
577+ moduleVariables : { } ,
578+ skipAgentAPIMock : true ,
579+ } ) ;
580+ await setupMocks ( id , "normal" , 204 ) ;
581+ // No pidFilePath and no MODULE_DIR_NAME, so no PID file can be resolved.
582+ const result = await runShutdownScript ( id , "test-task" , "" , "false" ) ;
583+
584+ expect ( result . exitCode ) . toBe ( 0 ) ;
585+ // Should not send SIGUSR1 or SIGTERM (no PID to signal).
586+ expect ( result . stdout ) . not . toContain ( "Sending SIGUSR1" ) ;
587+ expect ( result . stdout ) . not . toContain ( "Sending SIGTERM" ) ;
588+ expect ( result . stdout ) . toContain ( "Shutdown complete" ) ;
589+ } ) ;
590+
591+ test ( "skips SIGUSR1 when state persistence disabled" , async ( ) => {
592+ const { id } = await setup ( {
593+ moduleVariables : { } ,
594+ skipAgentAPIMock : true ,
595+ } ) ;
596+ const pidFile = "/tmp/agentapi-test.pid" ;
597+ await setupMocks ( id , "normal" , 204 , pidFile ) ;
598+ // PID file exists but state persistence is disabled.
599+ const result = await runShutdownScript ( id , "test-task" , pidFile , "false" ) ;
600+
601+ expect ( result . exitCode ) . toBe ( 0 ) ;
602+ // Should NOT send SIGUSR1 (persistence disabled).
603+ expect ( result . stdout ) . not . toContain ( "Sending SIGUSR1" ) ;
604+ // Should still send SIGTERM (graceful shutdown always happens).
605+ expect ( result . stdout ) . toContain ( "Sending SIGTERM to AgentAPI" ) ;
606+ } ) ;
412607 } ) ;
413608} ) ;
0 commit comments