@@ -258,11 +258,67 @@ 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 : "/custom/path/state.json" ,
285+ pid_file_path : "/custom/path/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 ( "AGENTAPI_STATE_FILE: /custom/path/state.json" ) ;
295+ expect ( mockLog ) . toContain ( "AGENTAPI_PID_FILE: /custom/path/agentapi.pid" ) ;
296+ } ) ;
297+
298+ test ( "state-persistence-default-paths" , async ( ) => {
299+ const { id } = await setup ( ) ;
300+ await execModuleScript ( id ) ;
301+ await expectAgentAPIStarted ( id ) ;
302+ const mockLog = await readFileContainer (
303+ id ,
304+ "/home/coder/agentapi-mock.log" ,
305+ ) ;
306+ expect ( mockLog ) . toContain (
307+ `AGENTAPI_STATE_FILE: /home/coder/${ moduleDirName } /state.json` ,
308+ ) ;
309+ expect ( mockLog ) . toContain (
310+ `AGENTAPI_PID_FILE: /home/coder/${ moduleDirName } /agentapi.pid` ,
311+ ) ;
312+ expect ( mockLog ) . toContain ( "AGENTAPI_SAVE_STATE: true" ) ;
313+ expect ( mockLog ) . toContain ( "AGENTAPI_LOAD_STATE: true" ) ;
314+ } ) ;
315+
261316 describe ( "shutdown script" , async ( ) => {
262317 const setupMocks = async (
263318 containerId : string ,
264319 agentapiPreset : string ,
265320 httpCode : number = 204 ,
321+ pidFilePath : string = "" ,
266322 ) => {
267323 const agentapiMock = await loadTestFile (
268324 import . meta. dir ,
@@ -285,10 +341,11 @@ describe("agentapi", async () => {
285341 content : coderMock ,
286342 } ) ;
287343
344+ const pidFileEnv = pidFilePath ? `AGENTAPI_PID_FILE=${ pidFilePath } ` : "" ;
288345 await execContainer ( containerId , [
289346 "bash" ,
290347 "-c" ,
291- `PRESET=${ agentapiPreset } nohup node /usr/local/bin/mock-agentapi 3284 > /tmp/mock-agentapi.log 2>&1 &` ,
348+ `PRESET=${ agentapiPreset } ${ pidFileEnv } nohup node /usr/local/bin/mock-agentapi 3284 > /tmp/mock-agentapi.log 2>&1 &` ,
292349 ] ) ;
293350
294351 await execContainer ( containerId , [
@@ -303,12 +360,25 @@ describe("agentapi", async () => {
303360 const runShutdownScript = async (
304361 containerId : string ,
305362 taskId : string = "test-task" ,
363+ pidFilePath : string = "" ,
364+ enableStatePersistence : string = "true" ,
306365 ) => {
307366 const shutdownScript = await loadTestFile (
308367 import . meta. dir ,
309368 "../scripts/agentapi-shutdown.sh" ,
310369 ) ;
311370
371+ const libScript = await loadTestFile (
372+ import . meta. dir ,
373+ "../scripts/lib.sh" ,
374+ ) ;
375+
376+ await writeExecutable ( {
377+ containerId,
378+ filePath : "/tmp/agentapi-lib.sh" ,
379+ content : libScript ,
380+ } ) ;
381+
312382 await writeExecutable ( {
313383 containerId,
314384 filePath : "/tmp/shutdown.sh" ,
@@ -318,7 +388,7 @@ describe("agentapi", async () => {
318388 return await execContainer ( containerId , [
319389 "bash" ,
320390 "-c" ,
321- `ARG_TASK_ID=${ taskId } ARG_AGENTAPI_PORT=3284 CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh` ,
391+ `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` ,
322392 ] ) ;
323393 } ;
324394
@@ -409,5 +479,126 @@ describe("agentapi", async () => {
409479 "Log snapshot endpoint not supported by this Coder version" ,
410480 ) ;
411481 } ) ;
482+
483+ test ( "sends SIGUSR1 before shutdown" , async ( ) => {
484+ const { id } = await setup ( {
485+ moduleVariables : { } ,
486+ skipAgentAPIMock : true ,
487+ } ) ;
488+ const pidFile = "/tmp/agentapi-test.pid" ;
489+ await setupMocks ( id , "normal" , 204 , pidFile ) ;
490+ const result = await runShutdownScript ( id , "test-task" , pidFile , "true" ) ;
491+
492+ expect ( result . exitCode ) . toBe ( 0 ) ;
493+ expect ( result . stdout ) . toContain ( "Sending SIGUSR1 to AgentAPI" ) ;
494+
495+ const sigusr1Log = await readFileContainer ( id , "/tmp/sigusr1-received" ) ;
496+ expect ( sigusr1Log ) . toContain ( "SIGUSR1 received" ) ;
497+ } ) ;
498+
499+ test ( "handles missing PID file gracefully" , async ( ) => {
500+ const { id } = await setup ( {
501+ moduleVariables : { } ,
502+ skipAgentAPIMock : true ,
503+ } ) ;
504+ await setupMocks ( id , "normal" ) ;
505+ // Pass a non-existent PID file path
506+ const result = await runShutdownScript (
507+ id ,
508+ "test-task" ,
509+ "/tmp/nonexistent.pid" ,
510+ ) ;
511+
512+ expect ( result . exitCode ) . toBe ( 0 ) ;
513+ expect ( result . stdout ) . toContain ( "Shutdown complete" ) ;
514+ } ) ;
515+
516+ test ( "sends SIGTERM even when snapshot fails" , async ( ) => {
517+ const { id } = await setup ( {
518+ moduleVariables : { } ,
519+ skipAgentAPIMock : true ,
520+ } ) ;
521+ const pidFile = "/tmp/agentapi-test.pid" ;
522+ // HTTP 500 will cause snapshot to fail
523+ await setupMocks ( id , "normal" , 500 , pidFile ) ;
524+ const result = await runShutdownScript ( id , "test-task" , pidFile ) ;
525+
526+ expect ( result . exitCode ) . toBe ( 0 ) ;
527+ expect ( result . stdout ) . toContain (
528+ "Log snapshot capture failed, continuing shutdown" ,
529+ ) ;
530+ expect ( result . stdout ) . toContain ( "Sending SIGTERM to AgentAPI" ) ;
531+ } ) ;
532+
533+ test ( "resolves default PID path from MODULE_DIR_NAME" , async ( ) => {
534+ const { id } = await setup ( {
535+ moduleVariables : { } ,
536+ skipAgentAPIMock : true ,
537+ } ) ;
538+ // Start mock with PID file at the module_dir_name default location.
539+ const defaultPidPath = `/home/coder/${ moduleDirName } /agentapi.pid` ;
540+ await setupMocks ( id , "normal" , 204 , defaultPidPath ) ;
541+ // Don't pass pidFilePath - let shutdown script compute it from MODULE_DIR_NAME.
542+ const shutdownScript = await loadTestFile (
543+ import . meta. dir ,
544+ "../scripts/agentapi-shutdown.sh" ,
545+ ) ;
546+ const libScript = await loadTestFile (
547+ import . meta. dir ,
548+ "../scripts/lib.sh" ,
549+ ) ;
550+ await writeExecutable ( {
551+ containerId : id ,
552+ filePath : "/tmp/agentapi-lib.sh" ,
553+ content : libScript ,
554+ } ) ;
555+ await writeExecutable ( {
556+ containerId : id ,
557+ filePath : "/tmp/shutdown.sh" ,
558+ content : shutdownScript ,
559+ } ) ;
560+ const result = await execContainer ( id , [
561+ "bash" ,
562+ "-c" ,
563+ `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` ,
564+ ] ) ;
565+
566+ expect ( result . exitCode ) . toBe ( 0 ) ;
567+ expect ( result . stdout ) . toContain ( "Sending SIGUSR1 to AgentAPI" ) ;
568+ expect ( result . stdout ) . toContain ( "Sending SIGTERM to AgentAPI" ) ;
569+ } ) ;
570+
571+ test ( "skips SIGUSR1 when no PID file available" , async ( ) => {
572+ const { id } = await setup ( {
573+ moduleVariables : { } ,
574+ skipAgentAPIMock : true ,
575+ } ) ;
576+ await setupMocks ( id , "normal" , 204 ) ;
577+ // No pidFilePath and no MODULE_DIR_NAME, so no PID file can be resolved.
578+ const result = await runShutdownScript ( id , "test-task" , "" , "false" ) ;
579+
580+ expect ( result . exitCode ) . toBe ( 0 ) ;
581+ // Should not send SIGUSR1 or SIGTERM (no PID to signal).
582+ expect ( result . stdout ) . not . toContain ( "Sending SIGUSR1" ) ;
583+ expect ( result . stdout ) . not . toContain ( "Sending SIGTERM" ) ;
584+ expect ( result . stdout ) . toContain ( "Shutdown complete" ) ;
585+ } ) ;
586+
587+ test ( "skips SIGUSR1 when state persistence disabled" , async ( ) => {
588+ const { id } = await setup ( {
589+ moduleVariables : { } ,
590+ skipAgentAPIMock : true ,
591+ } ) ;
592+ const pidFile = "/tmp/agentapi-test.pid" ;
593+ await setupMocks ( id , "normal" , 204 , pidFile ) ;
594+ // PID file exists but state persistence is disabled.
595+ const result = await runShutdownScript ( id , "test-task" , pidFile , "false" ) ;
596+
597+ expect ( result . exitCode ) . toBe ( 0 ) ;
598+ // Should NOT send SIGUSR1 (persistence disabled).
599+ expect ( result . stdout ) . not . toContain ( "Sending SIGUSR1" ) ;
600+ // Should still send SIGTERM (graceful shutdown always happens).
601+ expect ( result . stdout ) . toContain ( "Sending SIGTERM to AgentAPI" ) ;
602+ } ) ;
412603 } ) ;
413604} ) ;
0 commit comments