|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bufio" |
4 | 5 | "context" |
| 6 | + "io" |
5 | 7 | "os" |
6 | 8 | "os/exec" |
7 | 9 | "path/filepath" |
8 | 10 | "runtime" |
9 | 11 | "strings" |
| 12 | + "sync" |
| 13 | + "syscall" |
10 | 14 | "testing" |
| 15 | + "time" |
11 | 16 |
|
12 | 17 | "github.com/BackendStack21/odek" |
13 | 18 | "github.com/BackendStack21/odek/internal/config" |
| 19 | + "github.com/BackendStack21/odek/internal/mcp" |
14 | 20 | "github.com/BackendStack21/odek/internal/mcpclient" |
15 | 21 | ) |
16 | 22 |
|
@@ -44,6 +50,76 @@ func skipIfNoMCPE2E(t *testing.T) { |
44 | 50 | } |
45 | 51 | } |
46 | 52 |
|
| 53 | +type idleSignalReader struct { |
| 54 | + reader io.Reader |
| 55 | + once sync.Once |
| 56 | +} |
| 57 | + |
| 58 | +func (r *idleSignalReader) Read(p []byte) (int, error) { |
| 59 | + r.once.Do(func() { |
| 60 | + _, _ = io.WriteString(os.Stdout, "MCP_SIGNAL_READY\n") |
| 61 | + }) |
| 62 | + return r.reader.Read(p) |
| 63 | +} |
| 64 | + |
| 65 | +func TestMCPServerProcess_SIGTERMExitsWhileIdle(t *testing.T) { |
| 66 | + if os.Getenv("MCP_SIGNAL_HELPER") == "1" { |
| 67 | + server := mcp.NewServer("test", nil, &idleSignalReader{reader: os.Stdin}, io.Discard) |
| 68 | + _ = server.Run(context.Background()) |
| 69 | + os.Exit(0) |
| 70 | + } |
| 71 | + |
| 72 | + cmd := exec.Command(os.Args[0], "-test.run=^TestMCPServerProcess_SIGTERMExitsWhileIdle$") |
| 73 | + cmd.Env = append(os.Environ(), "MCP_SIGNAL_HELPER=1") |
| 74 | + stdin, err := cmd.StdinPipe() |
| 75 | + if err != nil { |
| 76 | + t.Fatalf("stdin pipe: %v", err) |
| 77 | + } |
| 78 | + defer stdin.Close() |
| 79 | + stdout, err := cmd.StdoutPipe() |
| 80 | + if err != nil { |
| 81 | + t.Fatalf("stdout pipe: %v", err) |
| 82 | + } |
| 83 | + if err := cmd.Start(); err != nil { |
| 84 | + t.Fatalf("start helper: %v", err) |
| 85 | + } |
| 86 | + |
| 87 | + started := make(chan bool, 1) |
| 88 | + go func() { |
| 89 | + scanner := bufio.NewScanner(stdout) |
| 90 | + for scanner.Scan() { |
| 91 | + if scanner.Text() == "MCP_SIGNAL_READY" { |
| 92 | + started <- true |
| 93 | + return |
| 94 | + } |
| 95 | + } |
| 96 | + started <- false |
| 97 | + }() |
| 98 | + select { |
| 99 | + case ok := <-started: |
| 100 | + if !ok { |
| 101 | + t.Fatal("helper exited before entering idle MCP read") |
| 102 | + } |
| 103 | + case <-time.After(2 * time.Second): |
| 104 | + _ = cmd.Process.Kill() |
| 105 | + t.Fatal("helper did not start MCP server") |
| 106 | + } |
| 107 | + |
| 108 | + if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { |
| 109 | + t.Fatalf("signal helper: %v", err) |
| 110 | + } |
| 111 | + done := make(chan error, 1) |
| 112 | + go func() { done <- cmd.Wait() }() |
| 113 | + select { |
| 114 | + case <-done: |
| 115 | + // Termination by SIGTERM is expected; only bounded exit matters. |
| 116 | + case <-time.After(time.Second): |
| 117 | + _ = cmd.Process.Kill() |
| 118 | + <-done |
| 119 | + t.Fatal("idle MCP server did not exit after SIGTERM") |
| 120 | + } |
| 121 | +} |
| 122 | + |
47 | 123 | func TestMCPClientE2E_DiscoverTools(t *testing.T) { |
48 | 124 | skipIfNoMCPE2E(t) |
49 | 125 |
|
|
0 commit comments