Skip to content

Commit b719dac

Browse files
committed
feat: add tests
1 parent 18fb1e4 commit b719dac

3 files changed

Lines changed: 601 additions & 0 deletions

File tree

cmd/server/server_test.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package server
22

33
import (
44
"fmt"
5+
"io"
6+
"log/slog"
57
"os"
68
"strings"
79
"testing"
@@ -477,6 +479,218 @@ func TestServerCmd_AllowedHosts(t *testing.T) {
477479
}
478480
}
479481

482+
func TestServerCmd_StatePersistenceFlags(t *testing.T) {
483+
// NOTE: These tests use --exit flag to test flag parsing and defaults.
484+
// Runtime validation that happens in runServer (e.g., "--load-state requires --state-file")
485+
// would call os.Exit(1) which terminates the test process, so those validations
486+
// are tested through integration/E2E tests instead.
487+
488+
t.Run("state-file with defaults", func(t *testing.T) {
489+
isolateViper(t)
490+
491+
serverCmd := CreateServerCmd()
492+
setupCommandOutput(t, serverCmd)
493+
serverCmd.SetArgs([]string{"--state-file", "/tmp/state.json", "--exit", "dummy-command"})
494+
err := serverCmd.Execute()
495+
require.NoError(t, err)
496+
497+
assert.Equal(t, "/tmp/state.json", viper.GetString(StateFile))
498+
// load-state and save-state default to true when state-file is set (validated in runServer)
499+
})
500+
501+
t.Run("state-file with explicit load-state=false", func(t *testing.T) {
502+
isolateViper(t)
503+
504+
serverCmd := CreateServerCmd()
505+
setupCommandOutput(t, serverCmd)
506+
serverCmd.SetArgs([]string{"--state-file", "/tmp/state.json", "--load-state=false", "--exit", "dummy-command"})
507+
err := serverCmd.Execute()
508+
require.NoError(t, err)
509+
510+
assert.Equal(t, "/tmp/state.json", viper.GetString(StateFile))
511+
assert.Equal(t, false, viper.GetBool(LoadState))
512+
})
513+
514+
t.Run("state-file with explicit save-state=false", func(t *testing.T) {
515+
isolateViper(t)
516+
517+
serverCmd := CreateServerCmd()
518+
setupCommandOutput(t, serverCmd)
519+
serverCmd.SetArgs([]string{"--state-file", "/tmp/state.json", "--save-state=false", "--exit", "dummy-command"})
520+
err := serverCmd.Execute()
521+
require.NoError(t, err)
522+
523+
assert.Equal(t, "/tmp/state.json", viper.GetString(StateFile))
524+
assert.Equal(t, false, viper.GetBool(SaveState))
525+
})
526+
527+
t.Run("state-file with explicit load-state=true and save-state=true", func(t *testing.T) {
528+
isolateViper(t)
529+
530+
serverCmd := CreateServerCmd()
531+
setupCommandOutput(t, serverCmd)
532+
serverCmd.SetArgs([]string{
533+
"--state-file", "/tmp/state.json",
534+
"--load-state=true",
535+
"--save-state=true",
536+
"--exit", "dummy-command",
537+
})
538+
err := serverCmd.Execute()
539+
require.NoError(t, err)
540+
541+
assert.Equal(t, "/tmp/state.json", viper.GetString(StateFile))
542+
assert.Equal(t, true, viper.GetBool(LoadState))
543+
assert.Equal(t, true, viper.GetBool(SaveState))
544+
})
545+
546+
t.Run("load-state flag can be parsed", func(t *testing.T) {
547+
isolateViper(t)
548+
549+
serverCmd := CreateServerCmd()
550+
setupCommandOutput(t, serverCmd)
551+
serverCmd.SetArgs([]string{"--load-state", "--exit", "dummy-command"})
552+
err := serverCmd.Execute()
553+
require.NoError(t, err)
554+
555+
// Flag is parsed correctly (validation happens in runServer)
556+
assert.Equal(t, true, viper.GetBool(LoadState))
557+
})
558+
559+
t.Run("save-state flag can be parsed", func(t *testing.T) {
560+
isolateViper(t)
561+
562+
serverCmd := CreateServerCmd()
563+
setupCommandOutput(t, serverCmd)
564+
serverCmd.SetArgs([]string{"--save-state", "--exit", "dummy-command"})
565+
err := serverCmd.Execute()
566+
require.NoError(t, err)
567+
568+
// Flag is parsed correctly (validation happens in runServer)
569+
assert.Equal(t, true, viper.GetBool(SaveState))
570+
})
571+
572+
t.Run("pid-file can be set independently", func(t *testing.T) {
573+
isolateViper(t)
574+
575+
serverCmd := CreateServerCmd()
576+
setupCommandOutput(t, serverCmd)
577+
serverCmd.SetArgs([]string{"--pid-file", "/tmp/server.pid", "--exit", "dummy-command"})
578+
err := serverCmd.Execute()
579+
require.NoError(t, err)
580+
581+
assert.Equal(t, "/tmp/server.pid", viper.GetString(PidFile))
582+
})
583+
584+
t.Run("state-file and pid-file can be set together", func(t *testing.T) {
585+
isolateViper(t)
586+
587+
serverCmd := CreateServerCmd()
588+
setupCommandOutput(t, serverCmd)
589+
serverCmd.SetArgs([]string{
590+
"--state-file", "/tmp/state.json",
591+
"--pid-file", "/tmp/server.pid",
592+
"--exit", "dummy-command",
593+
})
594+
err := serverCmd.Execute()
595+
require.NoError(t, err)
596+
597+
assert.Equal(t, "/tmp/state.json", viper.GetString(StateFile))
598+
assert.Equal(t, "/tmp/server.pid", viper.GetString(PidFile))
599+
})
600+
}
601+
602+
func TestPIDFileOperations(t *testing.T) {
603+
discardLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
604+
605+
t.Run("writePIDFile creates file with process ID", func(t *testing.T) {
606+
tmpDir := t.TempDir()
607+
pidFile := tmpDir + "/test.pid"
608+
609+
err := writePIDFile(pidFile, discardLogger)
610+
require.NoError(t, err)
611+
612+
// Verify file exists
613+
_, err = os.Stat(pidFile)
614+
require.NoError(t, err)
615+
616+
// Verify content contains current PID
617+
data, err := os.ReadFile(pidFile)
618+
require.NoError(t, err)
619+
620+
expectedPID := fmt.Sprintf("%d\n", os.Getpid())
621+
assert.Equal(t, expectedPID, string(data))
622+
})
623+
624+
t.Run("writePIDFile creates directory if not exists", func(t *testing.T) {
625+
tmpDir := t.TempDir()
626+
pidFile := tmpDir + "/nested/deep/test.pid"
627+
628+
err := writePIDFile(pidFile, discardLogger)
629+
require.NoError(t, err)
630+
631+
// Verify file exists
632+
_, err = os.Stat(pidFile)
633+
require.NoError(t, err)
634+
635+
// Verify directory was created
636+
_, err = os.Stat(tmpDir + "/nested/deep")
637+
require.NoError(t, err)
638+
})
639+
640+
t.Run("writePIDFile overwrites existing file", func(t *testing.T) {
641+
tmpDir := t.TempDir()
642+
pidFile := tmpDir + "/test.pid"
643+
644+
// Write initial PID file
645+
err := os.WriteFile(pidFile, []byte("12345\n"), 0o644)
646+
require.NoError(t, err)
647+
648+
// Overwrite with current PID
649+
err = writePIDFile(pidFile, discardLogger)
650+
require.NoError(t, err)
651+
652+
// Verify content is updated
653+
data, err := os.ReadFile(pidFile)
654+
require.NoError(t, err)
655+
656+
expectedPID := fmt.Sprintf("%d\n", os.Getpid())
657+
assert.Equal(t, expectedPID, string(data))
658+
})
659+
660+
t.Run("cleanupPIDFile removes file", func(t *testing.T) {
661+
tmpDir := t.TempDir()
662+
pidFile := tmpDir + "/test.pid"
663+
664+
// Create PID file
665+
err := os.WriteFile(pidFile, []byte("12345\n"), 0o644)
666+
require.NoError(t, err)
667+
668+
// Cleanup
669+
cleanupPIDFile(pidFile, discardLogger)
670+
671+
// Verify file is removed
672+
_, err = os.Stat(pidFile)
673+
assert.True(t, os.IsNotExist(err))
674+
})
675+
676+
t.Run("cleanupPIDFile handles non-existent file", func(t *testing.T) {
677+
tmpDir := t.TempDir()
678+
pidFile := tmpDir + "/nonexistent.pid"
679+
680+
// Should not panic or error
681+
cleanupPIDFile(pidFile, discardLogger)
682+
})
683+
684+
t.Run("cleanupPIDFile handles directory removal error gracefully", func(t *testing.T) {
685+
// Create a file in a protected directory (this is system-dependent)
686+
// Just verify it doesn't panic when it can't remove the file
687+
pidFile := "/this/should/not/exist/test.pid"
688+
689+
// Should not panic
690+
cleanupPIDFile(pidFile, discardLogger)
691+
})
692+
}
693+
480694
func TestServerCmd_AllowedOrigins(t *testing.T) {
481695
tests := []struct {
482696
name string

lib/httpapi/server_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"path/filepath"
1414
"strings"
1515
"testing"
16+
"time"
1617

1718
"github.com/coder/agentapi/lib/httpapi"
1819
"github.com/coder/agentapi/lib/logctx"
@@ -956,3 +957,36 @@ func TestServer_UploadFiles_Errors(t *testing.T) {
956957
require.Contains(t, string(body), "file size exceeds 10MB limit")
957958
})
958959
}
960+
961+
func TestServer_Stop_Idempotency(t *testing.T) {
962+
t.Parallel()
963+
ctx := logctx.WithLogger(context.Background(), slog.New(slog.NewTextHandler(os.Stdout, nil)))
964+
965+
srv, err := httpapi.NewServer(ctx, httpapi.ServerConfig{
966+
AgentType: msgfmt.AgentTypeClaude,
967+
Process: nil,
968+
Port: 0,
969+
ChatBasePath: "/chat",
970+
AllowedHosts: []string{"*"},
971+
AllowedOrigins: []string{"*"},
972+
})
973+
require.NoError(t, err)
974+
975+
// First call to Stop should succeed
976+
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
977+
defer cancel()
978+
err = srv.Stop(stopCtx)
979+
require.NoError(t, err)
980+
981+
// Second call to Stop should also succeed (no-op)
982+
stopCtx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
983+
defer cancel2()
984+
err = srv.Stop(stopCtx2)
985+
require.NoError(t, err)
986+
987+
// Third call to Stop should also succeed (no-op)
988+
stopCtx3, cancel3 := context.WithTimeout(context.Background(), 5*time.Second)
989+
defer cancel3()
990+
err = srv.Stop(stopCtx3)
991+
require.NoError(t, err)
992+
}

0 commit comments

Comments
 (0)