feat(02-01): add Unix socket server, poll loop, and stop handler
- Daemon struct with Start/Stop/Wait lifecycle - Unix socket server handling list/label/stop actions - Poll loop scanning /proc every 5s - Stale socket cleanup before listen - Connection dispatch with JSON encoding - Tests with -race: StartStop, ListOverSocket, LabelOverSocket, UnknownAction
This commit is contained in:
152
daemon_test.go
152
daemon_test.go
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -173,7 +175,151 @@ func TestRegistryUpdateTimestamp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder to verify file exists
|
||||
func init() {
|
||||
_ = os.TempDir()
|
||||
// sendRequest dials the daemon socket, sends a Request, and decodes the Response.
|
||||
func sendRequest(t *testing.T, sockPath string, req Request) Response {
|
||||
t.Helper()
|
||||
conn, err := net.Dial("unix", sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
|
||||
var resp Response
|
||||
if err := json.NewDecoder(conn).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// newTestDaemon creates a daemon using temp dirs (no real /proc or claude dir).
|
||||
func newTestDaemon(t *testing.T) *Daemon {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
sockPath := filepath.Join(dir, "vmux.sock")
|
||||
procDir := filepath.Join(dir, "proc") // empty, no processes
|
||||
claudeDir := filepath.Join(dir, "claude")
|
||||
labelsPath := filepath.Join(dir, "labels.json")
|
||||
|
||||
os.MkdirAll(procDir, 0o755)
|
||||
os.MkdirAll(claudeDir, 0o755)
|
||||
|
||||
labels, err := NewLabelStore(labelsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("labels: %v", err)
|
||||
}
|
||||
|
||||
return NewDaemon(sockPath, procDir, claudeDir, labels)
|
||||
}
|
||||
|
||||
func TestDaemonStartStop(t *testing.T) {
|
||||
d := newTestDaemon(t)
|
||||
|
||||
if err := d.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
|
||||
// Send stop via socket
|
||||
resp := sendRequest(t, d.sockPath, Request{Action: "stop"})
|
||||
if !resp.OK {
|
||||
t.Errorf("stop resp.OK = false, error = %q", resp.Error)
|
||||
}
|
||||
|
||||
// Daemon should stop within a short time
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
d.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// ok
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("daemon did not stop within 2s")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonListOverSocket(t *testing.T) {
|
||||
d := newTestDaemon(t)
|
||||
|
||||
if err := d.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer d.Stop()
|
||||
|
||||
// Populate registry after Start (initial scan clears unknown sessions)
|
||||
d.registry.Update(SessionInfo{
|
||||
PID: 42,
|
||||
SessionID: "test-sess",
|
||||
State: "Working",
|
||||
Cwd: "/tmp/test",
|
||||
})
|
||||
|
||||
resp := sendRequest(t, d.sockPath, Request{Action: "list"})
|
||||
if !resp.OK {
|
||||
t.Fatalf("list resp.OK = false, error = %q", resp.Error)
|
||||
}
|
||||
if len(resp.Sessions) != 1 {
|
||||
t.Fatalf("sessions len = %d, want 1", len(resp.Sessions))
|
||||
}
|
||||
if resp.Sessions[0].SessionID != "test-sess" {
|
||||
t.Errorf("session_id = %q, want %q", resp.Sessions[0].SessionID, "test-sess")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonLabelOverSocket(t *testing.T) {
|
||||
d := newTestDaemon(t)
|
||||
|
||||
if err := d.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer d.Stop()
|
||||
|
||||
// Populate registry after Start (initial scan clears unknown sessions)
|
||||
d.registry.Update(SessionInfo{
|
||||
PID: 42,
|
||||
SessionID: "test-sess",
|
||||
State: "Working",
|
||||
Cwd: "/tmp/test",
|
||||
})
|
||||
|
||||
// Set label
|
||||
args, _ := json.Marshal(LabelArgs{SessionID: "test-sess", Label: "review MR"})
|
||||
resp := sendRequest(t, d.sockPath, Request{Action: "label", Args: args})
|
||||
if !resp.OK {
|
||||
t.Fatalf("label resp.OK = false, error = %q", resp.Error)
|
||||
}
|
||||
|
||||
// Verify label appears in list
|
||||
resp = sendRequest(t, d.sockPath, Request{Action: "list"})
|
||||
if !resp.OK {
|
||||
t.Fatalf("list resp.OK = false, error = %q", resp.Error)
|
||||
}
|
||||
if len(resp.Sessions) != 1 {
|
||||
t.Fatalf("sessions len = %d, want 1", len(resp.Sessions))
|
||||
}
|
||||
if resp.Sessions[0].Label != "review MR" {
|
||||
t.Errorf("label = %q, want %q", resp.Sessions[0].Label, "review MR")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonUnknownAction(t *testing.T) {
|
||||
d := newTestDaemon(t)
|
||||
|
||||
if err := d.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer d.Stop()
|
||||
|
||||
resp := sendRequest(t, d.sockPath, Request{Action: "bogus"})
|
||||
if resp.OK {
|
||||
t.Error("expected OK=false for unknown action")
|
||||
}
|
||||
if resp.Error == "" {
|
||||
t.Error("expected error message for unknown action")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user