Skip to content

Commit 7d51bf1

Browse files
committed
test(cli): add more specified tests
1 parent 47cc161 commit 7d51bf1

3 files changed

Lines changed: 196 additions & 10 deletions

File tree

cmd/rune/mcpserver_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,107 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
6+
"net/http"
7+
"net/http/httptest"
58
"os"
69
"path/filepath"
10+
"strings"
11+
"syscall"
712
"testing"
813
"time"
14+
15+
"github.com/CryptoLabInc/rune-cli/internal/bootstrap"
916
)
1017

18+
func TestRunMCPServer_InstallErrorFailFast(t *testing.T) {
19+
saved := manifestURL
20+
manifestURL = ""
21+
defer func() { manifestURL = saved }()
22+
23+
dir := t.TempDir()
24+
t.Setenv("RUNE_HOME", filepath.Join(dir, "rune"))
25+
t.Setenv("RUNED_HOME", filepath.Join(dir, "runed"))
26+
27+
// Fail-fast error: unsupported manifest version
28+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29+
w.Header().Set("Content-Type", "application/json")
30+
_, _ = w.Write([]byte(`{"version": 999}`))
31+
}))
32+
t.Cleanup(srv.Close)
33+
t.Setenv("RUNE_MANIFEST", srv.URL)
34+
35+
var stderr bytes.Buffer
36+
start := time.Now()
37+
code := runMCPServer(context.Background(), nil, &stderr)
38+
elapsed := time.Since(start)
39+
40+
if code != 1 {
41+
t.Errorf("exit = %d, want 1", code)
42+
}
43+
if elapsed >= 5*time.Second {
44+
t.Errorf("took %s; fail fast install errors must not poll the %s self-heal budget", elapsed, mcpSelfhealBudget)
45+
}
46+
if !strings.Contains(stderr.String(), "cannot install rune-mcp") {
47+
t.Errorf("stderr missing fail-fast message: %q", stderr.String())
48+
}
49+
if strings.Contains(stderr.String(), "another session") {
50+
t.Errorf("fail-fast errors must not be diagnosed as a concurrent install: %q", stderr.String())
51+
}
52+
}
53+
54+
//--- Lock tests ---//
55+
56+
func TestRunMCPServer_TryLockDuringInstall(t *testing.T) {
57+
saved := manifestURL
58+
manifestURL = ""
59+
defer func() { manifestURL = saved }()
60+
t.Setenv("RUNE_MANIFEST", "")
61+
62+
dir := t.TempDir()
63+
t.Setenv("RUNE_HOME", filepath.Join(dir, "rune"))
64+
t.Setenv("RUNED_HOME", filepath.Join(dir, "runed"))
65+
66+
paths, err := bootstrap.Resolve()
67+
if err != nil {
68+
t.Fatalf("Resolve: %v", err)
69+
}
70+
if err := paths.EnsureDirs(); err != nil {
71+
t.Fatalf("EnsureDirs: %v", err)
72+
}
73+
74+
f, err := os.OpenFile(paths.InstallLock, os.O_CREATE|os.O_RDWR, 0o600)
75+
if err != nil {
76+
t.Fatalf("open lock: %v", err)
77+
}
78+
defer f.Close()
79+
80+
// Hold lock
81+
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
82+
t.Fatalf("flock: %v", err)
83+
}
84+
defer func() { _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) }()
85+
86+
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond) // short timeout for test
87+
defer cancel()
88+
89+
var stderr bytes.Buffer
90+
code := runMCPServer(ctx, nil, &stderr)
91+
92+
if code != 1 {
93+
t.Errorf("exit = %d, want 1", code)
94+
}
95+
if !strings.Contains(stderr.String(), "another session") {
96+
t.Errorf("lock failure should route to the concurrent-install wait path: %q", stderr.String())
97+
}
98+
if strings.Contains(stderr.String(), "cannot install rune-mcp") {
99+
t.Errorf("lock failure must be a retriable error: %q", stderr.String())
100+
}
101+
}
102+
103+
//--- waitForFile tests ---//
104+
11105
func TestWaitForFile_AlreadyPresent(t *testing.T) {
12106
p := filepath.Join(t.TempDir(), "rune-mcp")
13107
if err := os.WriteFile(p, []byte("x"), 0o755); err != nil {

internal/bootstrap/install_test.go

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http/httptest"
99
"os"
1010
"path/filepath"
11+
"strings"
1112
"testing"
1213
)
1314

@@ -224,15 +225,8 @@ func tarGz(t *testing.T, name string, body []byte) []byte {
224225
return b
225226
}
226227

227-
func TestInstall_TarballExtract(t *testing.T) {
228-
setRealms(t)
229-
paths, err := Resolve()
230-
if err != nil {
231-
t.Fatalf("Resolve: %v", err)
232-
}
233-
234-
runedTar := tarGz(t, filepath.Base(paths.RunedBinary), []byte("RUNED"))
235-
mcpTar := tarGz(t, filepath.Base(paths.RuneMCPBinary), []byte("RUNE-MCP"))
228+
func tarballManifestServer(t *testing.T, runedTar, mcpTar []byte) string {
229+
t.Helper()
236230

237231
var srv *httptest.Server
238232
mux := http.NewServeMux()
@@ -258,7 +252,20 @@ func TestInstall_TarballExtract(t *testing.T) {
258252
srv = httptest.NewServer(mux)
259253
t.Cleanup(srv.Close)
260254

261-
r, err := Install(context.Background(), InstallOptions{ManifestURL: srv.URL + "/manifest.json"})
255+
return srv.URL + "/manifest.json"
256+
}
257+
258+
func TestInstall_TarballExtract(t *testing.T) {
259+
setRealms(t)
260+
paths, err := Resolve()
261+
if err != nil {
262+
t.Fatalf("Resolve: %v", err)
263+
}
264+
265+
runedTar := tarGz(t, filepath.Base(paths.RunedBinary), []byte("RUNED"))
266+
mcpTar := tarGz(t, filepath.Base(paths.RuneMCPBinary), []byte("RUNE-MCP"))
267+
268+
r, err := Install(context.Background(), InstallOptions{ManifestURL: tarballManifestServer(t, runedTar, mcpTar)})
262269
if err != nil {
263270
t.Fatalf("Install (tar.gz): %v", err)
264271
}
@@ -277,3 +284,59 @@ func TestInstall_TarballExtract(t *testing.T) {
277284
}
278285
}
279286
}
287+
288+
func TestInstall_TarballMissingExpectedFile(t *testing.T) {
289+
setRealms(t)
290+
paths, err := Resolve()
291+
if err != nil {
292+
t.Fatalf("Resolve: %v", err)
293+
}
294+
295+
runedTar := tarGz(t, filepath.Base(paths.RunedBinary), []byte("RUNED"))
296+
mcpTar := tarGz(t, "not-rune-mcp", []byte("WRONG-NAME")) // valid archive but wrong entry
297+
298+
r, err := Install(context.Background(), InstallOptions{ManifestURL: tarballManifestServer(t, runedTar, mcpTar)})
299+
if err == nil {
300+
t.Fatal("expected error when the tarball lacks the expected file")
301+
}
302+
if !strings.Contains(err.Error(), "did not have expected file") {
303+
t.Errorf("err = %v, want 'did not have expected file'", err)
304+
}
305+
if r.Status != "partial" {
306+
t.Errorf("Status=%q, want partial", r.Status)
307+
}
308+
if r.Failed[StepRuneMCP] == "" {
309+
t.Errorf("Failed missing %s: %+v", StepRuneMCP, r.Failed)
310+
}
311+
if fileExists(paths.RuneMCPBinary) {
312+
t.Errorf("%s should not exist when the tarball lacks it", paths.RuneMCPBinary)
313+
}
314+
}
315+
316+
func TestInstall_CorruptTarball(t *testing.T) {
317+
setRealms(t)
318+
paths, err := Resolve()
319+
if err != nil {
320+
t.Fatalf("Resolve: %v", err)
321+
}
322+
323+
runedTar := tarGz(t, filepath.Base(paths.RunedBinary), []byte("RUNED"))
324+
corrupt := []byte("this is not a gzip stream") // SHA and size are matched but broken tarball
325+
326+
r, err := Install(context.Background(), InstallOptions{ManifestURL: tarballManifestServer(t, runedTar, corrupt)})
327+
if err == nil {
328+
t.Fatal("expected extract error for a corrupt gzip body")
329+
}
330+
if !strings.Contains(err.Error(), "extract") {
331+
t.Errorf("err = %v, want an extract failure", err)
332+
}
333+
if r.Status != "partial" {
334+
t.Errorf("Status=%q, want partial", r.Status)
335+
}
336+
if r.Failed[StepRuneMCP] == "" {
337+
t.Errorf("Failed missing %s: %+v", StepRuneMCP, r.Failed)
338+
}
339+
if fileExists(paths.RuneMCPBinary) {
340+
t.Errorf("%s should not exist after a failed extract", paths.RuneMCPBinary)
341+
}
342+
}

internal/supervisor/supervisor_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,32 @@ func TestWatcher_EscalateSIGKILL(t *testing.T) {
190190
t.Fatal("runWatcher hung - SIGKILL escalation did not fire")
191191
}
192192
}
193+
194+
func TestWatcher_ForwardSignalToChild(t *testing.T) {
195+
t.Setenv(fakeRunedEnv, "sleep")
196+
cfg := testWatcherConfig(t)
197+
cfg.ShutdownGrace = 2 * time.Second
198+
199+
done := make(chan error, 1)
200+
go func() { done <- runWatcher(context.Background(), cfg) }()
201+
202+
time.Sleep(400 * time.Millisecond)
203+
204+
start := time.Now()
205+
if err := syscall.Kill(os.Getpid(), syscall.SIGTERM); err != nil {
206+
t.Fatalf("send SIGTERM: %v", err)
207+
}
208+
209+
select {
210+
case err := <-done:
211+
if err != nil {
212+
t.Errorf("runWatcher = %v; want nil when SIGTERM is forwarded and the child exit 0", err)
213+
}
214+
215+
if elapsed := time.Since(start); elapsed >= cfg.ShutdownGrace {
216+
t.Errorf("took %s (>= grace %s); child should exit by forwarded SIGTERM", elapsed, cfg.ShutdownGrace)
217+
}
218+
case <-time.After(5 * time.Second):
219+
t.Fatal("runWatcher did not return after SIGTERM - signal not forwarded")
220+
}
221+
}

0 commit comments

Comments
 (0)