696 lines
18 KiB
Go
696 lines
18 KiB
Go
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"])
|
|
}
|
|
}
|