Add remote daemon distribution regressions

This commit is contained in:
Lawrence Chen 2026-03-12 05:04:35 -07:00
parent b6f0e3a3f6
commit 76cfe01fa2
6 changed files with 276 additions and 4 deletions

View file

@ -28,6 +28,27 @@ jobs:
- name: Validate GhosttyKit checksum verification
run: ./tests/test_ci_ghosttykit_checksum_verification.sh
- name: Validate release asset guard
run: node scripts/release_asset_guard.test.js
remote-daemon-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: daemon/remote/go.mod
- name: Run remote daemon tests
working-directory: daemon/remote
run: go test ./...
- name: Validate remote daemon release assets
run: ./tests/test_remote_daemon_release_assets.sh
web-typecheck:
runs-on: ubuntu-latest
defaults:

View file

@ -659,6 +659,48 @@ final class WindowTransparencyDecisionTests: XCTestCase {
}
}
final class WorkspaceRemoteDaemonManifestTests: XCTestCase {
func testParsesEmbeddedRemoteDaemonManifestJSON() throws {
let manifestJSON = """
{
"schemaVersion": 1,
"appVersion": "0.62.0",
"releaseTag": "v0.62.0",
"releaseURL": "https://github.com/manaflow-ai/cmux/releases/tag/v0.62.0",
"checksumsAssetName": "cmuxd-remote-checksums.txt",
"checksumsURL": "https://github.com/manaflow-ai/cmux/releases/download/v0.62.0/cmuxd-remote-checksums.txt",
"entries": [
{
"goOS": "linux",
"goArch": "amd64",
"assetName": "cmuxd-remote-linux-amd64",
"downloadURL": "https://github.com/manaflow-ai/cmux/releases/download/v0.62.0/cmuxd-remote-linux-amd64",
"sha256": "abc123"
}
]
}
"""
let manifest = Workspace.remoteDaemonManifest(from: [
Workspace.remoteDaemonManifestInfoKey: manifestJSON,
])
XCTAssertEqual(manifest?.releaseTag, "v0.62.0")
XCTAssertEqual(manifest?.entry(goOS: "linux", goArch: "amd64")?.assetName, "cmuxd-remote-linux-amd64")
}
func testRemoteDaemonCachePathIsVersionedByPlatform() throws {
let url = try Workspace.remoteDaemonCachedBinaryURL(
version: "0.62.0",
goOS: "linux",
goArch: "arm64"
)
XCTAssertTrue(url.path.contains("/Application Support/cmux/remote-daemons/0.62.0/linux-arm64/"))
XCTAssertEqual(url.lastPathComponent, "cmuxd-remote")
}
}
final class WindowBackgroundSelectionGateTests: XCTestCase {
func testShouldApplyWindowBackgroundUsesOwningWindowSelectionWhenAvailable() {
let tabId = UUID()

View file

@ -1,7 +1,13 @@
package main
import (
"bufio"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
@ -108,6 +114,80 @@ func startMockTCPSocket(t *testing.T, response string) string {
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")
@ -175,6 +255,47 @@ func TestCLIPingV1OverTCP(t *testing.T) {
}
}
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"} {

View file

@ -247,6 +247,25 @@ func TestProxyStreamRoundTrip(t *testing.T) {
}
}
func TestGetIntParamRejectsFractionalFloat64(t *testing.T) {
params := map[string]any{
"port": 80.9,
"timeout_ms": 100.0,
}
if _, ok := getIntParam(params, "port"); ok {
t.Fatalf("fractional float64 should be rejected")
}
timeout, ok := getIntParam(params, "timeout_ms")
if !ok {
t.Fatalf("integral float64 should be accepted")
}
if timeout != 100 {
t.Fatalf("timeout_ms = %d, want 100", timeout)
}
}
func TestRunStdioOversizedFrameContinuesServing(t *testing.T) {
oversized := `{"id":1,"method":"ping","params":{"blob":"` + strings.Repeat("a", maxRPCFrameBytes) + `"}}`
input := strings.NewReader(oversized + "\n" + `{"id":2,"method":"ping","params":{}}` + "\n")

View file

@ -11,7 +11,7 @@ const {
test("marks guard as complete and skips build/upload when all immutable assets already exist", () => {
const result = evaluateReleaseAssetGuard({
existingAssetNames: ["cmux-macos.dmg", "appcast.xml", "notes.txt"],
existingAssetNames: [...IMMUTABLE_RELEASE_ASSETS, "notes.txt"],
});
assert.deepEqual(result.conflicts, IMMUTABLE_RELEASE_ASSETS);
@ -36,12 +36,16 @@ test("marks guard as clear when immutable assets are not present", () => {
});
test("marks guard as partial when only some immutable assets exist", () => {
const partialAssets = ["appcast.xml", "cmuxd-remote-manifest.json"];
const result = evaluateReleaseAssetGuard({
existingAssetNames: ["appcast.xml"],
existingAssetNames: partialAssets,
});
assert.deepEqual(result.conflicts, ["appcast.xml"]);
assert.deepEqual(result.missingImmutableAssets, ["cmux-macos.dmg"]);
assert.deepEqual(result.conflicts, partialAssets);
assert.deepEqual(
result.missingImmutableAssets,
IMMUTABLE_RELEASE_ASSETS.filter((assetName) => !partialAssets.includes(assetName)),
);
assert.equal(result.guardState, RELEASE_ASSET_GUARD_STATE.PARTIAL);
assert.equal(result.hasPartialConflict, true);
assert.equal(result.shouldSkipBuildAndUpload, false);

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
OUTPUT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cmux-remote-assets-test.XXXXXX")"
trap 'rm -rf "$OUTPUT_DIR"' EXIT
"$ROOT_DIR/scripts/build_remote_daemon_release_assets.sh" \
--version "0.62.0-test" \
--release-tag "v0.62.0-test" \
--repo "manaflow-ai/cmux" \
--output-dir "$OUTPUT_DIR" >/dev/null
for asset in \
cmuxd-remote-darwin-arm64 \
cmuxd-remote-darwin-amd64 \
cmuxd-remote-linux-arm64 \
cmuxd-remote-linux-amd64 \
cmuxd-remote-checksums.txt \
cmuxd-remote-manifest.json
do
if [[ ! -f "$OUTPUT_DIR/$asset" ]]; then
echo "FAIL: missing asset $asset" >&2
exit 1
fi
done
python3 - <<'PY' "$OUTPUT_DIR/cmuxd-remote-manifest.json" "$OUTPUT_DIR/cmuxd-remote-checksums.txt"
import json
import sys
from pathlib import Path
manifest_path = Path(sys.argv[1])
checksums_path = Path(sys.argv[2])
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
expected_targets = {
("darwin", "arm64"),
("darwin", "amd64"),
("linux", "arm64"),
("linux", "amd64"),
}
actual_targets = {(entry["goOS"], entry["goArch"]) for entry in manifest["entries"]}
if actual_targets != expected_targets:
raise SystemExit(f"FAIL: manifest targets {sorted(actual_targets)} != {sorted(expected_targets)}")
if manifest["appVersion"] != "0.62.0-test":
raise SystemExit(f"FAIL: unexpected appVersion {manifest['appVersion']}")
if manifest["releaseTag"] != "v0.62.0-test":
raise SystemExit(f"FAIL: unexpected releaseTag {manifest['releaseTag']}")
if not manifest["checksumsURL"].endswith("/cmuxd-remote-checksums.txt"):
raise SystemExit(f"FAIL: unexpected checksumsURL {manifest['checksumsURL']}")
checksum_lines = [line for line in checksums_path.read_text(encoding="utf-8").splitlines() if line.strip()]
if len(checksum_lines) != 4:
raise SystemExit(f"FAIL: expected 4 checksum lines, got {len(checksum_lines)}")
for entry in manifest["entries"]:
if not entry["downloadURL"].endswith("/" + entry["assetName"]):
raise SystemExit(f"FAIL: downloadURL mismatch for {entry['assetName']}")
if len(entry["sha256"]) != 64:
raise SystemExit(f"FAIL: invalid sha256 for {entry['assetName']}")
print("PASS: remote daemon release assets include all targets and manifest entries")
PY