Revert "Merge pull request #239 from manaflow-ai/issue-151-ssh-remote-port-proxying"
This reverts commit78e4bd32ba, reversing changes made tocf75da8f8a.
This commit is contained in:
parent
78e4bd32ba
commit
f7cbbad434
60 changed files with 1250 additions and 17140 deletions
|
|
@ -1,696 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
original := os.Stdout
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe stdout: %v", err)
|
||||
}
|
||||
os.Stdout = writer
|
||||
defer func() {
|
||||
os.Stdout = original
|
||||
}()
|
||||
|
||||
fn()
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("close stdout writer: %v", err)
|
||||
}
|
||||
output, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("read stdout: %v", err)
|
||||
}
|
||||
if err := reader.Close(); err != nil {
|
||||
t.Fatalf("close stdout reader: %v", err)
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// startMockSocket creates a Unix socket that accepts one connection,
|
||||
// reads a line, and responds with the given canned response.
|
||||
func startMockSocket(t *testing.T, response string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
sockPath := filepath.Join(dir, "cmux.sock")
|
||||
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
_ = n // consume request
|
||||
conn.Write([]byte(response + "\n"))
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return sockPath
|
||||
}
|
||||
|
||||
// startMockV2Socket creates a Unix socket that echoes the received request's method
|
||||
// back as a successful JSON-RPC response with the method name in the result.
|
||||
func startMockV2Socket(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
sockPath := filepath.Join(dir, "cmux.sock")
|
||||
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
if n > 0 {
|
||||
var req map[string]any
|
||||
if err := json.Unmarshal(buf[:n], &req); err == nil {
|
||||
resp := map[string]any{
|
||||
"id": req["id"],
|
||||
"ok": true,
|
||||
"result": map[string]any{"method": req["method"], "params": req["params"]},
|
||||
}
|
||||
payload, _ := json.Marshal(resp)
|
||||
conn.Write(append(payload, '\n'))
|
||||
} else {
|
||||
conn.Write([]byte(`{"ok":false,"error":{"code":"parse","message":"bad json"}}` + "\n"))
|
||||
}
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return sockPath
|
||||
}
|
||||
|
||||
func startMockV2TCPSocketWithResult(t *testing.T, result any) string {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen on TCP: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
var req map[string]any
|
||||
if err := json.Unmarshal(buf[:n], &req); err != nil {
|
||||
_, _ = conn.Write([]byte(`{"ok":false,"error":{"code":"parse","message":"bad json"}}` + "\n"))
|
||||
return
|
||||
}
|
||||
resp := map[string]any{
|
||||
"id": req["id"],
|
||||
"ok": true,
|
||||
"result": result,
|
||||
}
|
||||
payload, _ := json.Marshal(resp)
|
||||
_, _ = conn.Write(append(payload, '\n'))
|
||||
}(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
// startMockTCPSocket creates a TCP listener that responds with a canned response.
|
||||
func startMockTCPSocket(t *testing.T, response string) string {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen on TCP: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
_ = n
|
||||
conn.Write([]byte(response + "\n"))
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
func startMockAuthenticatedTCPSocket(t *testing.T, relayID, relayToken, response string) string {
|
||||
t.Helper()
|
||||
relayTokenBytes := mustHex(t, relayToken)
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen on TCP: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
nonce := "testnonce"
|
||||
challenge, _ := json.Marshal(map[string]any{
|
||||
"protocol": "cmux-relay-auth",
|
||||
"version": 1,
|
||||
"relay_id": relayID,
|
||||
"nonce": nonce,
|
||||
})
|
||||
_, _ = conn.Write(append(challenge, '\n'))
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var authResp map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &authResp); err != nil {
|
||||
_, _ = conn.Write([]byte(`{"ok":false}` + "\n"))
|
||||
return
|
||||
}
|
||||
macHex, _ := authResp["mac"].(string)
|
||||
receivedMAC, err := hex.DecodeString(macHex)
|
||||
if err != nil {
|
||||
_, _ = conn.Write([]byte(`{"ok":false}` + "\n"))
|
||||
return
|
||||
}
|
||||
|
||||
h := hmac.New(sha256.New, relayTokenBytes)
|
||||
_, _ = io.WriteString(h, fmt.Sprintf("relay_id=%s\nnonce=%s\nversion=%d", relayID, nonce, 1))
|
||||
expectedMAC := h.Sum(nil)
|
||||
if !hmac.Equal(receivedMAC, expectedMAC) {
|
||||
_, _ = conn.Write([]byte(`{"ok":false}` + "\n"))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = conn.Write([]byte(`{"ok":true}` + "\n"))
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
_, _ = conn.Write([]byte(response))
|
||||
if n > 0 && !strings.HasSuffix(response, "\n") {
|
||||
_, _ = conn.Write([]byte("\n"))
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
return ln.Addr().String()
|
||||
}
|
||||
|
||||
func mustHex(t *testing.T, value string) []byte {
|
||||
t.Helper()
|
||||
data, err := hex.DecodeString(value)
|
||||
if err != nil {
|
||||
t.Fatalf("decode hex: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestDialTCPRetrySuccess(t *testing.T) {
|
||||
// Get a free port, then close the listener so connection is refused initially.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
addr := ln.Addr().String()
|
||||
ln.Close()
|
||||
|
||||
// Start a listener after a delay so the retry logic finds it.
|
||||
go func() {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
ln2, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer ln2.Close()
|
||||
conn, err := ln2.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
conn, err := dialTCPRetry(addr, 3*time.Second, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dialTCPRetry should succeed after retry, got: %v", err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestDialTCPRetryTimeout(t *testing.T) {
|
||||
// Get a free port and close it — nothing will ever listen.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
addr := ln.Addr().String()
|
||||
ln.Close()
|
||||
|
||||
start := time.Now()
|
||||
_, err = dialTCPRetry(addr, 600*time.Millisecond, nil)
|
||||
elapsed := time.Since(start)
|
||||
if err == nil {
|
||||
t.Fatal("dialTCPRetry should fail when nothing is listening")
|
||||
}
|
||||
if elapsed < 500*time.Millisecond {
|
||||
t.Fatalf("should have retried for ~600ms, only took %v", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIPingV1(t *testing.T) {
|
||||
sockPath := startMockSocket(t, "pong")
|
||||
code := runCLI([]string{"--socket", sockPath, "ping"})
|
||||
if code != 0 {
|
||||
t.Fatalf("ping should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIPingV1OverTCP(t *testing.T) {
|
||||
addr := startMockTCPSocket(t, "pong")
|
||||
code := runCLI([]string{"--socket", addr, "ping"})
|
||||
if code != 0 {
|
||||
t.Fatalf("ping over TCP should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIPingV1OverAuthenticatedTCPWithEnv(t *testing.T) {
|
||||
relayID := "relay-1"
|
||||
relayToken := strings.Repeat("a1", 32)
|
||||
addr := startMockAuthenticatedTCPSocket(t, relayID, relayToken, "pong")
|
||||
t.Setenv("CMUX_RELAY_ID", relayID)
|
||||
t.Setenv("CMUX_RELAY_TOKEN", relayToken)
|
||||
|
||||
code := runCLI([]string{"--socket", addr, "ping"})
|
||||
if code != 0 {
|
||||
t.Fatalf("ping over authenticated TCP should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIPingV1OverAuthenticatedTCPWithRelayFile(t *testing.T) {
|
||||
relayID := "relay-2"
|
||||
relayToken := strings.Repeat("b2", 32)
|
||||
addr := startMockAuthenticatedTCPSocket(t, relayID, relayToken, "pong")
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("split host port: %v", err)
|
||||
}
|
||||
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("CMUX_RELAY_ID", "")
|
||||
t.Setenv("CMUX_RELAY_TOKEN", "")
|
||||
relayDir := filepath.Join(home, ".cmux", "relay")
|
||||
if err := os.MkdirAll(relayDir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir relay dir: %v", err)
|
||||
}
|
||||
authPayload, _ := json.Marshal(relayAuthState{RelayID: relayID, RelayToken: relayToken})
|
||||
if err := os.WriteFile(filepath.Join(relayDir, port+".auth"), authPayload, 0o600); err != nil {
|
||||
t.Fatalf("write auth file: %v", err)
|
||||
}
|
||||
|
||||
code := runCLI([]string{"--socket", addr, "ping"})
|
||||
if code != 0 {
|
||||
t.Fatalf("ping over authenticated TCP file relay should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialSocketDetection(t *testing.T) {
|
||||
// Unix socket paths should attempt Unix dial
|
||||
for _, path := range []string{"/tmp/cmux-nonexistent-test-99999.sock", "/var/run/cmux-nonexistent.sock"} {
|
||||
conn, err := dialSocket(path, nil)
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
// We expect a connection error (not found), not a panic
|
||||
if err == nil {
|
||||
t.Fatalf("dialSocket(%q) should fail for non-existent path", path)
|
||||
}
|
||||
}
|
||||
|
||||
// TCP addresses should attempt TCP dial
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
go func() {
|
||||
conn, _ := ln.Accept()
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := dialSocket(ln.Addr().String(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dialSocket(%q) should succeed for TCP: %v", ln.Addr().String(), err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestCLINewWindowV1(t *testing.T) {
|
||||
sockPath := startMockSocket(t, "OK window_id=abc123")
|
||||
code := runCLI([]string{"--socket", sockPath, "new-window"})
|
||||
if code != 0 {
|
||||
t.Fatalf("new-window should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocketRoundTripReadsFullMultilineV1Response(t *testing.T) {
|
||||
addr := startMockTCPSocket(t, "window:alpha\nwindow:beta\nwindow:gamma")
|
||||
resp, err := socketRoundTrip(addr, "list_windows", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("socketRoundTrip should succeed, got error: %v", err)
|
||||
}
|
||||
want := "window:alpha\nwindow:beta\nwindow:gamma"
|
||||
if resp != want {
|
||||
t.Fatalf("socketRoundTrip truncated v1 response: got %q want %q", resp, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLICloseWindowV1(t *testing.T) {
|
||||
// Verify that the flag value is appended to the v1 command
|
||||
dir := t.TempDir()
|
||||
sockPath := filepath.Join(dir, "cmux.sock")
|
||||
|
||||
var received string
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
received = strings.TrimSpace(string(buf[:n]))
|
||||
conn.Write([]byte("OK\n"))
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
code := runCLI([]string{"--socket", sockPath, "close-window", "--window", "win-42"})
|
||||
if code != 0 {
|
||||
t.Fatalf("close-window should return 0, got %d", code)
|
||||
}
|
||||
if received != "close_window win-42" {
|
||||
t.Fatalf("expected 'close_window win-42', got %q", received)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIListWorkspacesV2(t *testing.T) {
|
||||
sockPath := startMockV2Socket(t)
|
||||
code := runCLI([]string{"--socket", sockPath, "--json", "list-workspaces"})
|
||||
if code != 0 {
|
||||
t.Fatalf("list-workspaces should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIListWorkspacesV2DefaultOutputShowsResult(t *testing.T) {
|
||||
sockPath := startMockV2TCPSocketWithResult(t, map[string]any{"method": "workspace.list", "params": map[string]any{}})
|
||||
output := captureStdout(t, func() {
|
||||
code := runCLI([]string{"--socket", sockPath, "list-workspaces"})
|
||||
if code != 0 {
|
||||
t.Fatalf("list-workspaces should return 0, got %d", code)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(output, "\"method\": \"workspace.list\"") {
|
||||
t.Fatalf("expected default output to include result payload, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLINotifyDefaultOutputPrintsOKForEmptyResult(t *testing.T) {
|
||||
sockPath := startMockV2TCPSocketWithResult(t, map[string]any{})
|
||||
output := captureStdout(t, func() {
|
||||
code := runCLI([]string{"--socket", sockPath, "notify", "--body", "hi"})
|
||||
if code != 0 {
|
||||
t.Fatalf("notify should return 0, got %d", code)
|
||||
}
|
||||
})
|
||||
if strings.TrimSpace(output) != "OK" {
|
||||
t.Fatalf("expected empty-result command to print OK, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIRPCPassthrough(t *testing.T) {
|
||||
sockPath := startMockV2Socket(t)
|
||||
code := runCLI([]string{"--socket", sockPath, "rpc", "system.capabilities"})
|
||||
if code != 0 {
|
||||
t.Fatalf("rpc should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIRPCWithParams(t *testing.T) {
|
||||
sockPath := startMockV2Socket(t)
|
||||
code := runCLI([]string{"--socket", sockPath, "rpc", "workspace.create", `{"title":"test"}`})
|
||||
if code != 0 {
|
||||
t.Fatalf("rpc with params should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIUnknownCommand(t *testing.T) {
|
||||
code := runCLI([]string{"--socket", "/dev/null", "does-not-exist"})
|
||||
if code != 2 {
|
||||
t.Fatalf("unknown command should return 2, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLINoSocket(t *testing.T) {
|
||||
// Without CMUX_SOCKET_PATH set, should fail
|
||||
os.Unsetenv("CMUX_SOCKET_PATH")
|
||||
code := runCLI([]string{"ping"})
|
||||
if code != 1 {
|
||||
t.Fatalf("missing socket should return 1, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLISocketEnvVar(t *testing.T) {
|
||||
sockPath := startMockSocket(t, "pong")
|
||||
os.Setenv("CMUX_SOCKET_PATH", sockPath)
|
||||
defer os.Unsetenv("CMUX_SOCKET_PATH")
|
||||
|
||||
code := runCLI([]string{"ping"})
|
||||
if code != 0 {
|
||||
t.Fatalf("ping with env socket should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIV2FlagMapping(t *testing.T) {
|
||||
// Verify that --workspace gets mapped to workspace_id in params
|
||||
dir := t.TempDir()
|
||||
sockPath := filepath.Join(dir, "cmux.sock")
|
||||
|
||||
var receivedParams map[string]any
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
var req map[string]any
|
||||
json.Unmarshal(buf[:n], &req)
|
||||
receivedParams, _ = req["params"].(map[string]any)
|
||||
resp := map[string]any{"id": req["id"], "ok": true, "result": map[string]any{}}
|
||||
payload, _ := json.Marshal(resp)
|
||||
conn.Write(append(payload, '\n'))
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
code := runCLI([]string{"--socket", sockPath, "--json", "close-workspace", "--workspace", "ws-abc"})
|
||||
if code != 0 {
|
||||
t.Fatalf("close-workspace should return 0, got %d", code)
|
||||
}
|
||||
if receivedParams["workspace_id"] != "ws-abc" {
|
||||
t.Fatalf("expected workspace_id=ws-abc, got %v", receivedParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusyboxArgv0Detection(t *testing.T) {
|
||||
// Verify that when argv[0] base is "cmux", we enter CLI mode
|
||||
base := filepath.Base("cmux")
|
||||
if base != "cmux" {
|
||||
t.Fatalf("expected base 'cmux', got %q", base)
|
||||
}
|
||||
base2 := filepath.Base("/home/user/.cmux/bin/cmux")
|
||||
if base2 != "cmux" {
|
||||
t.Fatalf("expected base 'cmux', got %q", base2)
|
||||
}
|
||||
base3 := filepath.Base("cmuxd-remote")
|
||||
if base3 == "cmux" {
|
||||
t.Fatalf("cmuxd-remote should not match cmux")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIBrowserSubcommand(t *testing.T) {
|
||||
sockPath := startMockV2Socket(t)
|
||||
code := runCLI([]string{"--socket", sockPath, "--json", "browser", "open", "--url", "https://example.com"})
|
||||
if code != 0 {
|
||||
t.Fatalf("browser open should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLINoArgs(t *testing.T) {
|
||||
code := runCLI([]string{})
|
||||
if code != 2 {
|
||||
t.Fatalf("no args should return 2, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIHelpFlag(t *testing.T) {
|
||||
code := runCLI([]string{"--help"})
|
||||
if code != 0 {
|
||||
t.Fatalf("--help should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIHelpCommand(t *testing.T) {
|
||||
code := runCLI([]string{"help"})
|
||||
if code != 0 {
|
||||
t.Fatalf("help should return 0, got %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagToParamKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
input, expected string
|
||||
}{
|
||||
{"workspace", "workspace_id"},
|
||||
{"surface", "surface_id"},
|
||||
{"panel", "panel_id"},
|
||||
{"pane", "pane_id"},
|
||||
{"window", "window_id"},
|
||||
{"command", "initial_command"},
|
||||
{"name", "title"},
|
||||
{"working-directory", "working_directory"},
|
||||
{"title", "title"},
|
||||
{"url", "url"},
|
||||
{"direction", "direction"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := flagToParamKey(tc.input)
|
||||
if got != tc.expected {
|
||||
t.Errorf("flagToParamKey(%q) = %q, want %q", tc.input, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFlags(t *testing.T) {
|
||||
args := []string{"positional-cmd", "--workspace", "ws-1", "--surface", "sf-2", "--unknown", "val"}
|
||||
result := parseFlags(args, []string{"workspace", "surface"})
|
||||
if result.flags["workspace"] != "ws-1" {
|
||||
t.Errorf("expected workspace=ws-1, got %q", result.flags["workspace"])
|
||||
}
|
||||
if result.flags["surface"] != "sf-2" {
|
||||
t.Errorf("expected surface=sf-2, got %q", result.flags["surface"])
|
||||
}
|
||||
if _, ok := result.flags["unknown"]; ok {
|
||||
t.Errorf("unknown flag should not be parsed")
|
||||
}
|
||||
if len(result.positional) == 0 || result.positional[0] != "positional-cmd" {
|
||||
t.Errorf("expected first positional=positional-cmd, got %v", result.positional)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIEnvVarDefaults(t *testing.T) {
|
||||
// Test that CMUX_WORKSPACE_ID and CMUX_SURFACE_ID are used as defaults
|
||||
dir := t.TempDir()
|
||||
sockPath := filepath.Join(dir, "cmux.sock")
|
||||
|
||||
var receivedParams map[string]any
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := conn.Read(buf)
|
||||
var req map[string]any
|
||||
json.Unmarshal(buf[:n], &req)
|
||||
receivedParams, _ = req["params"].(map[string]any)
|
||||
resp := map[string]any{"id": req["id"], "ok": true, "result": map[string]any{}}
|
||||
payload, _ := json.Marshal(resp)
|
||||
conn.Write(append(payload, '\n'))
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
os.Setenv("CMUX_WORKSPACE_ID", "env-ws-id")
|
||||
os.Setenv("CMUX_SURFACE_ID", "env-sf-id")
|
||||
defer os.Unsetenv("CMUX_WORKSPACE_ID")
|
||||
defer os.Unsetenv("CMUX_SURFACE_ID")
|
||||
|
||||
code := runCLI([]string{"--socket", sockPath, "--json", "close-surface"})
|
||||
if code != 0 {
|
||||
t.Fatalf("close-surface should return 0, got %d", code)
|
||||
}
|
||||
if receivedParams["workspace_id"] != "env-ws-id" {
|
||||
t.Errorf("expected workspace_id from env, got %v", receivedParams["workspace_id"])
|
||||
}
|
||||
if receivedParams["surface_id"] != "env-sf-id" {
|
||||
t.Errorf("expected surface_id from env, got %v", receivedParams["surface_id"])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue