package wire

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/gorilla/websocket"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/workspacefs"
)

func TestElasticRequestLinkOpenDialsWithLinkTokenAndHandlesAssignment(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, filepath.Join(t.TempDir(), ".agentteam"))
	agentDir := filepath.Join(profile.ConfigDir(), "agents", "agent-1")
	require.NoError(t, os.MkdirAll(agentDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(agentDir, "README.md"), []byte("hello"), 0o644))

	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", r.URL.Path)
		require.Empty(t, r.URL.Query().Get("key"), "Elastic Request Link must not reuse the machine API key")
		require.Equal(t, "Bearer single-use-link-token", r.Header.Get("Authorization"))
		require.Equal(t, "wsp_1", r.Header.Get(routeWorkspaceHeader))

		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()

		payload := json.RawMessage(`{"agentId":"agent-1","limit":20}`)
		require.NoError(t, conn.WriteJSON(remoteRequestAssignmentMsg{
			Type:                 remoteRequestAssignType,
			SchemaVersion:        1,
			RequestID:            "rrq_1",
			RemoteRequestID:      "rrq_1",
			EndpointID:           remoteEndpointWorkspaceList,
			EndpointVersion:      1,
			PayloadSchemaVersion: 1,
			DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
			TimeoutBudgetMs:      1000,
			CancelToken:          "cancel_1",
			Limits: remoteEndpointLimits{
				RequestBytes:  1024,
				ResponseBytes: 64 * 1024,
			},
			Payload: payload,
		}))

		var result remoteRequestResultMsg
		require.NoError(t, conn.ReadJSON(&result))
		resultCh <- result
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		apiKey:                 "machine-key",
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}

	ch.handleElasticRequestLinkOpen(ctx, mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "single-use-link-token"))

	select {
	case result := <-resultCh:
		require.True(t, result.Success)
		require.Equal(t, "ok", result.Outcome)
		require.Equal(t, "rrq_1", result.RequestID)
		require.Contains(t, string(result.Payload), "README.md")
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for remote request result")
	}
}

func TestElasticRequestLinkRejectsMachineKeyInConnectURL(t *testing.T) {
	_, err := buildElasticRequestLinkURL("https://agentteam.example.com", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect?key=machine-key")
	require.ErrorContains(t, err, "machine key")

	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              "https://agentteam.example.com",
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleElasticRequestLinkOpen(context.Background(), mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect?key=machine-key", "single-use-link-token"))
	ch.elasticLinksMu.Lock()
	defer ch.elasticLinksMu.Unlock()
	require.Empty(t, ch.elasticRequestLinks)
}

func TestElasticRequestLinkEnvelopeMustMatchGrantAllowlist(t *testing.T) {
	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()
		assignment := testRemoteAssignment("rrq_bad_endpoint")
		assignment.EndpointID = "review.summary"
		require.NoError(t, conn.WriteJSON(assignment))
		var result remoteRequestResultMsg
		require.NoError(t, conn.ReadJSON(&result))
		resultCh <- result
	}))
	t.Cleanup(server.Close)

	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{
		{EndpointID: remoteEndpointWorkspaceList, Version: 1, Handler: handleRemoteWorkspaceList},
		{EndpointID: "review.summary", Version: 1, Handler: func(context.Context, *Protocol, remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			return json.RawMessage(`{"ok":true}`), "ok", nil
		}},
	})
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	ch.handleElasticRequestLinkOpen(context.Background(), mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token"))

	select {
	case result := <-resultCh:
		require.False(t, result.Success)
		require.Equal(t, "protocol_error", result.Outcome)
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for endpoint mismatch result")
	}
}

func TestElasticRequestLinkAcceptsMultiEndpointAllowlist(t *testing.T) {
	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()
		assignment := testRemoteAssignment("rrq_review_summary")
		assignment.EndpointID = remoteEndpointReviewSummary
		require.NoError(t, conn.WriteJSON(assignment))
		var result remoteRequestResultMsg
		require.NoError(t, conn.ReadJSON(&result))
		resultCh <- result
	}))
	t.Cleanup(server.Close)

	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{
		{EndpointID: remoteEndpointWorkspaceList, Version: 1, Handler: handleRemoteWorkspaceList},
		{EndpointID: remoteEndpointReviewSummary, Version: 1, Handler: func(context.Context, *Protocol, remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			return json.RawMessage(`{"summary":{"status":"ready"}}`), "ok", nil
		}},
	})
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	open := mustElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token")
	open.Purpose = "warm"
	open.ProfileName = "user_interactive_read_review_v1"
	open.AllowedEndpoints = []daemonTransportEndpointRef{
		{EndpointID: remoteEndpointWorkspaceList, Version: 1},
		{EndpointID: remoteEndpointReviewSummary, Version: 1},
	}
	ch.handleElasticRequestLinkOpen(context.Background(), mustJSON(t, open))

	select {
	case result := <-resultCh:
		require.True(t, result.Success)
		require.Equal(t, "ok", result.Outcome)
		require.Equal(t, "rrq_review_summary", result.RequestID)
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for multi-endpoint allowlist result")
	}
}

func TestElasticRequestLinkSequentiallyReusesMultiEndpointWarmLink(t *testing.T) {
	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 2)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()

		assignments := []remoteRequestAssignmentMsg{
			testRemoteAssignment("rrq_workspace_list"),
			testRemoteAssignment("rrq_review_summary"),
		}
		assignments[1].EndpointID = remoteEndpointReviewSummary

		for _, assignment := range assignments {
			require.NoError(t, conn.WriteJSON(assignment))
			var result remoteRequestResultMsg
			require.NoError(t, conn.ReadJSON(&result))
			resultCh <- result
		}
	}))
	t.Cleanup(server.Close)

	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{
		{EndpointID: remoteEndpointWorkspaceList, Version: 1, Handler: func(context.Context, *Protocol, remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			return json.RawMessage(`{"endpoint":"workspace.list"}`), "ok", nil
		}},
		{EndpointID: remoteEndpointReviewSummary, Version: 1, Handler: func(context.Context, *Protocol, remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			return json.RawMessage(`{"endpoint":"review.summary"}`), "ok", nil
		}},
	})
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	open := mustElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token")
	open.Purpose = "warm"
	open.ProfileName = "user_interactive_read_review_v1"
	open.AllowedEndpoints = []daemonTransportEndpointRef{
		{EndpointID: remoteEndpointWorkspaceList, Version: 1},
		{EndpointID: remoteEndpointReviewSummary, Version: 1},
	}
	ch.handleElasticRequestLinkOpen(context.Background(), mustJSON(t, open))

	got := map[string]remoteRequestResultMsg{}
	for len(got) < 2 {
		select {
		case result := <-resultCh:
			got[result.RequestID] = result
		case <-time.After(2 * time.Second):
			t.Fatal("timed out waiting for sequential warm-link results")
		}
	}
	require.True(t, got["rrq_workspace_list"].Success)
	require.Equal(t, "ok", got["rrq_workspace_list"].Outcome)
	require.True(t, got["rrq_review_summary"].Success)
	require.Equal(t, "ok", got["rrq_review_summary"].Outcome)
}

func TestElasticRequestLinkOpenRejectsUnknownAllowlistEndpoint(t *testing.T) {
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              "https://agentteam.example.com",
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	open := mustElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token")
	open.AllowedEndpoints = append(open.AllowedEndpoints, daemonTransportEndpointRef{EndpointID: "workspace.delete_everything", Version: 1})
	require.ErrorContains(t, ch.validateElasticRequestLinkOpen(open), "endpoint_not_allowed")
}

func TestElasticRequestLinkOpenRejectsWarmDemandOnlyEndpoint(t *testing.T) {
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              "https://agentteam.example.com",
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	open := mustElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token")
	open.Purpose = "warm"
	open.ProfileName = "user_interactive_read_review_v1"
	open.AllowedEndpoints = []daemonTransportEndpointRef{{EndpointID: remoteEndpointWorkspaceDelete, Version: 1}}
	require.ErrorContains(t, ch.validateElasticRequestLinkOpen(open), "warm_endpoint_not_allowed")

	open.Purpose = "demand"
	require.NoError(t, ch.validateElasticRequestLinkOpen(open))
}

func TestElasticRequestLinkSingleFlightReturnsBusyForConcurrentAssignment(t *testing.T) {
	started := make(chan struct{}, 1)
	release := make(chan struct{})
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointWorkspaceList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, _ remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			started <- struct{}{}
			select {
			case <-release:
				return json.RawMessage(`{"ok":true}`), "ok", nil
			case <-ctx.Done():
				return nil, "", ctx.Err()
			}
		},
	}})

	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	results := make(chan remoteRequestResultMsg, 2)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()

		require.NoError(t, conn.WriteJSON(testRemoteAssignment("rrq_1")))
		select {
		case <-started:
		case <-time.After(time.Second):
			t.Fatal("first assignment did not start")
		}
		require.NoError(t, conn.WriteJSON(testRemoteAssignment("rrq_2")))

		for i := 0; i < 2; i++ {
			var result remoteRequestResultMsg
			require.NoError(t, conn.ReadJSON(&result))
			results <- result
			if result.RequestID == "rrq_2" {
				close(release)
			}
		}
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	ch.handleElasticRequestLinkOpen(ctx, mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token"))

	got := map[string]remoteRequestResultMsg{}
	for len(got) < 2 {
		select {
		case result := <-results:
			got[result.RequestID] = result
		case <-time.After(2 * time.Second):
			t.Fatal("timed out waiting for single-flight results")
		}
	}
	require.Equal(t, "desktop_busy", got["rrq_2"].Outcome)
	require.False(t, got["rrq_2"].Success)
	require.True(t, got["rrq_1"].Success)
	require.Equal(t, "ok", got["rrq_1"].Outcome)
}

func TestElasticRequestLinkMaxAgeBusyDrainsAfterAssignment(t *testing.T) {
	started := make(chan struct{}, 1)
	release := make(chan struct{})
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointWorkspaceList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, _ remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			started <- struct{}{}
			select {
			case <-release:
				return json.RawMessage(`{"ok":true}`), "ok", nil
			case <-ctx.Done():
				return nil, "", ctx.Err()
			}
		},
	}})

	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()

		require.NoError(t, conn.WriteJSON(testRemoteAssignment("rrq_1")))
		var result remoteRequestResultMsg
		require.NoError(t, conn.ReadJSON(&result))
		resultCh <- result
		_, _, _ = conn.ReadMessage()
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	open := mustElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token")
	open.IdleTimeoutMs = 5000
	open.MaxAgeMs = 20
	ch.handleElasticRequestLinkOpen(ctx, mustJSON(t, open))

	select {
	case <-started:
	case <-time.After(time.Second):
		t.Fatal("assignment did not start")
	}
	require.Eventually(t, func() bool {
		link := ch.elasticRequestLink("erl_1")
		if link == nil {
			return false
		}
		link.mu.Lock()
		defer link.mu.Unlock()
		return link.busy && link.draining
	}, time.Second, 10*time.Millisecond)
	close(release)

	select {
	case result := <-resultCh:
		require.True(t, result.Success)
		require.Equal(t, "ok", result.Outcome)
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for drained assignment result")
	}
	require.Eventually(t, func() bool {
		ch.elasticLinksMu.Lock()
		defer ch.elasticLinksMu.Unlock()
		return len(ch.elasticRequestLinks) == 0
	}, time.Second, 10*time.Millisecond)
}

func TestElasticRequestLinkCancelReturnsStableCanceledOutcome(t *testing.T) {
	started := make(chan struct{}, 1)
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointWorkspaceList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, _ remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			started <- struct{}{}
			<-ctx.Done()
			return nil, "", ctx.Err()
		},
	}})

	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()
		require.NoError(t, conn.WriteJSON(testRemoteAssignment("rrq_cancel")))
		var result remoteRequestResultMsg
		require.NoError(t, conn.ReadJSON(&result))
		resultCh <- result
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	ch.handleElasticRequestLinkOpen(ctx, mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token"))

	select {
	case <-started:
		ch.handleElasticRequestLinkCancel(mustJSON(t, elasticRequestLinkCancelMsg{
			Type:            elasticRequestLinkCancelType,
			LinkID:          "erl_1",
			RemoteRequestID: "rrq_cancel",
			CancelToken:     "cancel_rrq_cancel",
			Reason:          "test",
		}))
	case <-time.After(time.Second):
		t.Fatal("assignment did not start")
	}

	select {
	case result := <-resultCh:
		require.Equal(t, "rrq_cancel", result.RequestID)
		require.False(t, result.Success)
		require.Equal(t, "canceled", result.Outcome)
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for canceled result")
	}
}

func TestElasticRequestLinkCancelRequiresCurrentCancelToken(t *testing.T) {
	started := make(chan struct{}, 1)
	release := make(chan struct{})
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointWorkspaceList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, _ remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			started <- struct{}{}
			select {
			case <-release:
				return json.RawMessage(`{"ok":true}`), "ok", nil
			case <-ctx.Done():
				return nil, "", ctx.Err()
			}
		},
	}})

	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	resultCh := make(chan remoteRequestResultMsg, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()
		require.NoError(t, conn.WriteJSON(testRemoteAssignment("rrq_cancel")))
		var result remoteRequestResultMsg
		require.NoError(t, conn.ReadJSON(&result))
		resultCh <- result
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: registry,
	}
	ch.handleElasticRequestLinkOpen(ctx, mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token"))

	select {
	case <-started:
		ch.handleElasticRequestLinkCancel(mustJSON(t, elasticRequestLinkCancelMsg{
			Type:            elasticRequestLinkCancelType,
			LinkID:          "erl_1",
			RemoteRequestID: "rrq_cancel",
			CancelToken:     "wrong_cancel_token",
			Reason:          "test",
		}))
	case <-time.After(time.Second):
		t.Fatal("assignment did not start")
	}

	select {
	case result := <-resultCh:
		t.Fatalf("wrong cancel token should not finish assignment, got %+v", result)
	case <-time.After(10 * time.Millisecond):
	}
	close(release)

	select {
	case result := <-resultCh:
		require.Equal(t, "rrq_cancel", result.RequestID)
		require.True(t, result.Success)
		require.Equal(t, "ok", result.Outcome)
	case <-time.After(2 * time.Second):
		t.Fatal("timed out waiting for canceled result")
	}
}

func TestElasticRequestLinkIdleTimeoutClosesTrackedLink(t *testing.T) {
	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	connected := make(chan struct{}, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()
		connected <- struct{}{}
		_, _, _ = conn.ReadMessage()
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	open := mustElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token")
	open.IdleTimeoutMs = 20
	ch.handleElasticRequestLinkOpen(ctx, mustJSON(t, open))

	select {
	case <-connected:
	case <-time.After(time.Second):
		t.Fatal("link did not connect")
	}
	require.Eventually(t, func() bool {
		ch.elasticLinksMu.Lock()
		defer ch.elasticLinksMu.Unlock()
		return len(ch.elasticRequestLinks) == 0
	}, time.Second, 10*time.Millisecond)
}

func TestCloseElasticRequestLinksClosesTrackedLinks(t *testing.T) {
	upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
	closed := make(chan struct{}, 1)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		require.NoError(t, err)
		defer conn.Close()
		_, _, _ = conn.ReadMessage()
		closed <- struct{}{}
	}))
	t.Cleanup(server.Close)

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	ch := &Protocol{
		id:                     "agentteam-test",
		serverURL:              server.URL,
		workspaceID:            "wsp_1",
		sendCh:                 make(chan []byte, sendBufSize),
		elasticRequestLinks:    make(map[string]*elasticRequestLink),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleElasticRequestLinkOpen(ctx, mustMarshalElasticOpen(t, "erl_1", "/api/v1/workspaces/wsp_1/daemon/elastic-request-links/erl_1/connect", "token"))
	require.Eventually(t, func() bool {
		ch.elasticLinksMu.Lock()
		defer ch.elasticLinksMu.Unlock()
		return len(ch.elasticRequestLinks) == 1
	}, time.Second, 10*time.Millisecond)

	ch.closeElasticRequestLinks("session_disconnected")

	select {
	case <-closed:
	case <-time.After(time.Second):
		t.Fatal("tracked link was not closed")
	}
	ch.elasticLinksMu.Lock()
	defer ch.elasticLinksMu.Unlock()
	require.Empty(t, ch.elasticRequestLinks)
}

func TestRemoteRequestAssignmentClampsDeadlineToTimeoutBudget(t *testing.T) {
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointWorkspaceList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, _ remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			<-ctx.Done()
			return nil, "", ctx.Err()
		},
	}})
	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: registry,
	}
	assignment := testRemoteAssignment("rrq_deadline")
	assignment.DeadlineAt = time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano)
	assignment.TimeoutBudgetMs = 20
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, assignment))

	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, "deadline_exceeded", result.Outcome)
	require.Equal(t, "deadline_exceeded", result.Error)
}

func TestRemoteRequestAssignmentEnforcesRequestAndResponseLimits(t *testing.T) {
	t.Run("request payload too large", func(t *testing.T) {
		ch := &Protocol{
			id:                     "agentteam-test",
			sendCh:                 make(chan []byte, sendBufSize),
			remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
		}
		assignment := testRemoteAssignment("rrq_request_too_large")
		assignment.Payload = json.RawMessage(`{"tooLarge":true}`)
		assignment.Limits.RequestBytes = 1
		ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, assignment))

		result := nextRemoteRequestResult(t, ch)
		require.False(t, result.Success)
		require.Equal(t, "protocol_error", result.Outcome)
	})

	t.Run("response payload too large", func(t *testing.T) {
		registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
			EndpointID: remoteEndpointWorkspaceList,
			Version:    1,
			Handler: func(context.Context, *Protocol, remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
				return json.RawMessage(`{"message":"response is larger than the assignment budget"}`), "ok", nil
			},
		}})
		ch := &Protocol{
			id:                     "agentteam-test",
			sendCh:                 make(chan []byte, sendBufSize),
			remoteEndpointRegistry: registry,
		}
		assignment := testRemoteAssignment("rrq_response_too_large")
		assignment.Limits.ResponseBytes = 8
		ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, assignment))

		result := nextRemoteRequestResult(t, ch)
		require.False(t, result.Success)
		require.Equal(t, "protocol_error", result.Outcome)
	})
}

func TestSessionLinkRemoteRequestAssignmentUsesTypedAdapter(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, filepath.Join(t.TempDir(), ".agentteam"))
	agentDir := filepath.Join(profile.ConfigDir(), "agents", "agent-session")
	require.NoError(t, os.MkdirAll(agentDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(agentDir, "main.go"), []byte("package main"), 0o644))

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	payload := json.RawMessage(`{"agentId":"agent-session","limit":20}`)
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_session",
		RemoteRequestID:      "rrq_session",
		EndpointID:           remoteEndpointWorkspaceList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_session",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              payload,
	}))

	select {
	case data := <-ch.sendCh:
		var result remoteRequestResultMsg
		require.NoError(t, json.Unmarshal(data, &result))
		require.True(t, result.Success)
		require.Equal(t, "rrq_session", result.RequestID)
		require.Contains(t, string(result.Payload), "main.go")
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for Session Link remote request result")
	}
}

func TestRemoteWorkspaceEndpointAdaptersAllowTargetPathWithoutAgent(t *testing.T) {
	targetDir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(targetDir, "README.md"), []byte("target"), 0o644))

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_target_list",
		RemoteRequestID:      "rrq_target_list",
		EndpointID:           remoteEndpointWorkspaceList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_target_list",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              mustJSONRaw(t, remoteWorkspaceListPayload{TargetPath: targetDir, Limit: 20}),
	}))
	listResult := nextRemoteRequestResult(t, ch)
	require.True(t, listResult.Success)
	require.Contains(t, string(listResult.Payload), "README.md")

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_target_read",
		RemoteRequestID:      "rrq_target_read",
		EndpointID:           remoteEndpointWorkspaceRead,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_target_read",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              mustJSONRaw(t, remoteWorkspaceReadPayload{TargetPath: targetDir, Path: "README.md"}),
	}))
	readResult := nextRemoteRequestResult(t, ch)
	require.True(t, readResult.Success)
	var readResponse remoteWorkspaceReadResponse
	require.NoError(t, json.Unmarshal(readResult.Payload, &readResponse))
	require.NotNil(t, readResponse.Content)
	require.Equal(t, "target", *readResponse.Content)
}

func TestRemoteWorkspaceEndpointAdaptersReadApplyAndDelete(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, filepath.Join(t.TempDir(), ".agentteam"))
	agentDir := filepath.Join(profile.ConfigDir(), "agents", "agent-remote")
	require.NoError(t, os.MkdirAll(agentDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(agentDir, "README.md"), []byte("hello"), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(agentDir, "remove.txt"), []byte("remove"), 0o644))

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_read",
		RemoteRequestID:      "rrq_read",
		EndpointID:           remoteEndpointWorkspaceRead,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_read",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-remote","path":"README.md"}`),
	}))
	readResult := nextRemoteRequestResult(t, ch)
	require.True(t, readResult.Success)
	var readResponse remoteWorkspaceReadResponse
	require.NoError(t, json.Unmarshal(readResult.Payload, &readResponse))
	require.NotNil(t, readResponse.Content)
	require.Equal(t, "hello", *readResponse.Content)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_apply",
		RemoteRequestID:      "rrq_apply",
		EndpointID:           remoteEndpointWorkspaceApplySeedFiles,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_apply",
		Limits:               remoteEndpointLimits{RequestBytes: 64 * 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-remote","seedFiles":[{"path":"new.md","content":"seed"}]}`),
	}))
	applyResult := nextRemoteRequestResult(t, ch)
	require.True(t, applyResult.Success)
	written, err := os.ReadFile(filepath.Join(agentDir, "new.md"))
	require.NoError(t, err)
	require.Equal(t, "seed", string(written))

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_delete",
		RemoteRequestID:      "rrq_delete",
		EndpointID:           remoteEndpointWorkspaceDelete,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_delete",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-remote","path":"remove.txt"}`),
	}))
	deleteResult := nextRemoteRequestResult(t, ch)
	require.True(t, deleteResult.Success)
	_, err = os.Stat(filepath.Join(agentDir, "remove.txt"))
	require.True(t, os.IsNotExist(err))
}

func TestRemoteWorkspaceEndpointAdapterErrorsAreRedacted(t *testing.T) {
	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_missing",
		RemoteRequestID:      "rrq_missing",
		EndpointID:           remoteEndpointWorkspaceRead,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_missing",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-missing","path":"/tmp/secret-path"}`),
	}))
	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, "endpoint_error", result.Outcome)
	require.Equal(t, "endpoint_error", result.Error)
	require.NotContains(t, result.Error, "/tmp/secret-path")
}

func nextRemoteRequestResult(t *testing.T, ch *Protocol) remoteRequestResultMsg {
	t.Helper()
	select {
	case data := <-ch.sendCh:
		var result remoteRequestResultMsg
		require.NoError(t, json.Unmarshal(data, &result))
		return result
	case <-time.After(time.Second):
		t.Fatal("timed out waiting for remote request result")
		return remoteRequestResultMsg{}
	}
}

func mustMarshalElasticOpen(t *testing.T, linkID string, connectURL string, token string) []byte {
	t.Helper()
	return mustJSON(t, mustElasticOpen(t, linkID, connectURL, token))
}

func mustElasticOpen(t *testing.T, linkID string, connectURL string, token string) elasticRequestLinkOpenMsg {
	t.Helper()
	return elasticRequestLinkOpenMsg{
		Type:                    elasticRequestLinkOpenType,
		LinkID:                  linkID,
		ConnectURL:              connectURL,
		Token:                   token,
		ExpiresAt:               time.Now().Add(time.Second).UTC().Format(time.RFC3339),
		NodeHint:                "test",
		ProtocolVersion:         daemonTransportSchemaVersion,
		EndpointRegistryVersion: defaultEndpointRegistryVersion,
		AllowedEndpoints:        []daemonTransportEndpointRef{{EndpointID: remoteEndpointWorkspaceList, Version: 1}},
		IdleTimeoutMs:           5000,
		MaxAgeMs:                30000,
	}
}

func testRemoteAssignment(requestID string) remoteRequestAssignmentMsg {
	return remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            requestID,
		RemoteRequestID:      requestID,
		EndpointID:           remoteEndpointWorkspaceList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339),
		TimeoutBudgetMs:      5000,
		CancelToken:          "cancel_" + requestID,
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-1"}`),
	}
}

func mustJSON(t *testing.T, value any) []byte {
	t.Helper()
	data, err := json.Marshal(value)
	require.NoError(t, err)
	return data
}

func TestRemoteReviewEndpointAdaptersSummaryFilesDiffAndValidate(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}
	repoDir := t.TempDir()
	setupRemoteReviewTestRepo(t, repoDir)
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "tracked.txt"), []byte("changed\n"), 0o644))

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_review_summary",
		RemoteRequestID:      "rrq_review_summary",
		EndpointID:           remoteEndpointReviewSummary,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_review_summary",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 256 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewSummaryPayload{AgentID: "agent-review", TargetPath: repoDir}),
	}))
	summaryResult := nextRemoteRequestResult(t, ch)
	require.True(t, summaryResult.Success)
	var summaryResponse remoteReviewSummaryResponse
	require.NoError(t, json.Unmarshal(summaryResult.Payload, &summaryResponse))
	require.NotNil(t, summaryResponse.Summary)
	require.Equal(t, "ready", summaryResponse.Summary.Status)
	require.True(t, summaryResponse.Summary.Dirty)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_review_snapshot",
		RemoteRequestID:      "rrq_review_snapshot",
		EndpointID:           remoteEndpointReviewSnapshot,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339),
		TimeoutBudgetMs:      5000,
		CancelToken:          "cancel_review_snapshot",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 2 * 1024 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewSummaryPayload{AgentID: "agent-review", TargetPath: repoDir}),
	}))
	snapshotResult := nextRemoteRequestResult(t, ch)
	require.True(t, snapshotResult.Success)
	var snapshotResponse remoteReviewSnapshotResponse
	require.NoError(t, json.Unmarshal(snapshotResult.Payload, &snapshotResponse))
	require.NotNil(t, snapshotResponse.Snapshot)
	require.Equal(t, "ready", snapshotResponse.Snapshot.Summary.Status)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_review_files",
		RemoteRequestID:      "rrq_review_files",
		EndpointID:           remoteEndpointReviewFiles,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_review_files",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 256 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewFilesPayload{AgentID: "agent-review", TargetPath: repoDir, Scope: "working"}),
	}))
	filesResult := nextRemoteRequestResult(t, ch)
	require.True(t, filesResult.Success)
	var filesResponse remoteReviewFilesResponse
	require.NoError(t, json.Unmarshal(filesResult.Payload, &filesResponse))
	// Token is derived from filesystem stat and is non-deterministic across
	// runs; clear it before comparing against the literal expectation.
	for i := range filesResponse.Files {
		filesResponse.Files[i].Token = ""
	}
	require.Equal(t, []types.ReviewFile{{Path: "tracked.txt", ChangeType: "M", Unstaged: true, UnstagedChangeType: "M"}}, filesResponse.Files)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_review_diff",
		RemoteRequestID:      "rrq_review_diff",
		EndpointID:           remoteEndpointReviewDiff,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_review_diff",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 2 * 1024 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewDiffPayload{AgentID: "agent-review", TargetPath: repoDir, Scope: "working", Path: "tracked.txt"}),
	}))
	diffResult := nextRemoteRequestResult(t, ch)
	require.True(t, diffResult.Success)
	var diffResponse remoteReviewDiffResponse
	require.NoError(t, json.Unmarshal(diffResult.Payload, &diffResponse))
	require.NotNil(t, diffResponse.Diff)
	require.Equal(t, "ready", diffResponse.Diff.Status)
	require.Equal(t, "tracked.txt", diffResponse.Diff.Path)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_validate",
		RemoteRequestID:      "rrq_validate",
		EndpointID:           remoteEndpointOutputTargetValidate,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_validate",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              mustJSONRaw(t, remoteOutputTargetValidatePayload{AgentID: "agent-review", Kind: "git_repo", TargetPath: repoDir}),
	}))
	validateResult := nextRemoteRequestResult(t, ch)
	require.True(t, validateResult.Success)
}

func TestRemoteReviewEndpointAdapterErrorsAreRedacted(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git not available")
	}
	repoDir := t.TempDir()
	setupRemoteReviewTestRepo(t, repoDir)
	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	// This test verifies endpoint error redaction, not deadline clamping. Keep
	// the budget comfortably above git/review setup time so a loaded test run
	// cannot race the handler error with requestCtx.DeadlineExceeded.
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_review_secret",
		RemoteRequestID:      "rrq_review_secret",
		EndpointID:           remoteEndpointReviewDiff,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(10 * time.Second).UTC().Format(time.RFC3339),
		TimeoutBudgetMs:      10000,
		CancelToken:          "cancel_review_secret",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              mustJSONRaw(t, remoteReviewDiffPayload{AgentID: "agent-review", TargetPath: repoDir, Scope: "working", Path: "../secret-review-path"}),
	}))
	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, "endpoint_error", result.Outcome)
	require.Equal(t, "endpoint_error", result.Error)
	require.NotContains(t, result.Error, "secret-review-path")
}

func TestRemoteMachineSystemEndpointAdapters(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, filepath.Join(t.TempDir(), ".agentteam"))
	require.NoError(t, os.MkdirAll(filepath.Join(profile.ConfigDir(), "agents", "agent-a"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(profile.ConfigDir(), "agents", "agent-a", "README.md"), []byte("hello"), 0o644))
	dispatcher := &agentteamTestDispatcher{
		trajectoryItems: []types.TrajectoryLogItem{{
			AgentID:   "agent-a",
			Entry:     types.TrajectoryEntry{Kind: "text", Text: "hello"},
			Timestamp: 42,
		}},
	}
	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
		disp:                   dispatcher,
	}

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_scan",
		RemoteRequestID:      "rrq_machine_scan",
		EndpointID:           remoteEndpointMachineWorkspaceScan,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_scan",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 256 * 1024},
		Payload:              json.RawMessage(`{"limit":50}`),
	}))
	scanResult := nextRemoteRequestResult(t, ch)
	require.True(t, scanResult.Success)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_trajectory",
		RemoteRequestID:      "rrq_trajectory",
		EndpointID:           remoteEndpointRuntimeTrajectoryHistory,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_trajectory",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 256 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-a","limit":10}`),
	}))
	trajectoryResult := nextRemoteRequestResult(t, ch)
	require.True(t, trajectoryResult.Success)
	var trajectoryResponse remoteRuntimeTrajectoryHistoryResponse
	require.NoError(t, json.Unmarshal(trajectoryResult.Payload, &trajectoryResponse))
	require.Equal(t, dispatcher.trajectoryItems, trajectoryResponse.Items)

	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_restore",
		RemoteRequestID:      "rrq_restore",
		EndpointID:           remoteEndpointRuntimeRestoreRecoveryPoint,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_restore",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"agentId":"agent-a","runtime":"codex","strategy":"turns","recoveryRef":"ref-1"}`),
	}))
	restoreResult := nextRemoteRequestResult(t, ch)
	require.True(t, restoreResult.Success)
	require.Len(t, dispatcher.restoreCalls, 1)
	require.Equal(t, "rrq_restore", dispatcher.restoreCalls[0].RequestID)
}

func TestRemoteMachineDashboardAdapterScopesAgentsToServerAndSelection(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, filepath.Join(t.TempDir(), ".agentteam"))
	pm := profile.Default()
	require.NoError(t, pm.Servers().Add(&profile.AgentTeamServer{ID: "agentteam-test", Enabled: true, ServerURL: "https://agentteam.example", WorkspaceID: "wsp-1", MachineID: "mch-1", APIKey: "machine-secret"}))
	require.NoError(t, pm.Servers().Add(&profile.AgentTeamServer{ID: "other-server", Enabled: true, ServerURL: "https://other.example", WorkspaceID: "wsp-other", MachineID: "mch-other", APIKey: "other-secret"}))
	require.NoError(t, pm.Agents().Add(agent.Config{ID: "agent-a", Name: "Alpha", Runtime: "codex", Source: agent.NewAgentTeamSource("agentteam-test")}))
	require.NoError(t, pm.Agents().Add(agent.Config{ID: "agent-b", Name: "Beta", Runtime: "opencode", Source: agent.NewAgentTeamSource("agentteam-test")}))
	require.NoError(t, pm.Agents().Add(agent.Config{ID: "agent-foreign", Name: "Foreign", Runtime: "codex", Source: agent.NewAgentTeamSource("other-server")}))
	require.NoError(t, pm.Agents().Add(agent.Config{ID: "agent-local", Name: "Local", Runtime: "codex", Source: agent.NewLocalSource()}))

	dispatcher := &agentteamTestDispatcher{sessions: []types.SessionSnapshot{
		{ChannelID: "agentteam-test", AgentID: "agent-a", AgentConfigID: "agent-a", Status: "running", ServerAgentID: "agent-a"},
		{ChannelID: "other-server", AgentID: "agent-b", AgentConfigID: "agent-b", Status: "running", ServerAgentID: "agent-b"},
	}}
	ch := &Protocol{
		id:                     "agentteam-test",
		workspaceID:            "wsp-1",
		machineID:              "mch-1",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
		disp:                   dispatcher,
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_dashboard",
		RemoteRequestID:      "rrq_machine_dashboard",
		EndpointID:           remoteEndpointMachineDashboard,
		EndpointVersion:      remoteEndpointMachineDashboardVersion,
		PayloadSchemaVersion: remoteEndpointMachineDashboardVersion,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_dashboard",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
		Payload: mustJSONRaw(t, remoteMachineDashboardPayload{
			WorkspaceID: "wsp-1",
			MachineID:   "mch-1",
			Window:      "1d",
			AgentIDs:    []string{"agent-b", "agent-a", "agent-a", ""},
		}),
	}))

	result := nextRemoteRequestResult(t, ch)
	require.True(t, result.Success)
	require.Equal(t, "ok", result.Outcome)
	var response remoteMachineDashboardResponse
	require.NoError(t, json.Unmarshal(result.Payload, &response))
	require.Equal(t, "mch-1", response.MachineID)
	require.Equal(t, "wsp-1", response.WorkspaceID)
	require.Equal(t, "1d", response.Window)
	require.Equal(t, 2, response.AgentCount)
	require.Equal(t, 1, response.ActiveAgents)
	require.Equal(t, []remoteMachineDashboardAgent{
		{AgentID: "agent-b", Name: "Beta", Runtime: "opencode"},
		{AgentID: "agent-a", Name: "Alpha", Runtime: "codex", Active: true},
	}, response.Agents)
	// The outbound dashboard always carries the tokenSources contract.
	require.Contains(t, string(result.Payload), `"tokenSources"`)
	require.False(t, response.TokenSources.Available)
}

func TestRemoteMachineDashboardAdapterRejectsForeignSelectedAgent(t *testing.T) {
	t.Setenv(profile.ConfigDirEnvKey, filepath.Join(t.TempDir(), ".agentteam"))
	pm := profile.Default()
	require.NoError(t, pm.Servers().Add(&profile.AgentTeamServer{ID: "agentteam-test", Enabled: true, ServerURL: "https://agentteam.example", WorkspaceID: "wsp-1", MachineID: "mch-1", APIKey: "machine-secret"}))
	require.NoError(t, pm.Servers().Add(&profile.AgentTeamServer{ID: "other-server", Enabled: true, ServerURL: "https://other.example", WorkspaceID: "wsp-other", MachineID: "mch-other", APIKey: "other-secret"}))
	require.NoError(t, pm.Agents().Add(agent.Config{ID: "agent-a", Name: "Alpha", Runtime: "codex", Source: agent.NewAgentTeamSource("agentteam-test")}))
	require.NoError(t, pm.Agents().Add(agent.Config{ID: "agent-foreign", Name: "Foreign", Runtime: "codex", Source: agent.NewAgentTeamSource("other-server")}))

	ch := &Protocol{
		id:                     "agentteam-test",
		workspaceID:            "wsp-1",
		machineID:              "mch-1",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_dashboard_foreign",
		RemoteRequestID:      "rrq_machine_dashboard_foreign",
		EndpointID:           remoteEndpointMachineDashboard,
		EndpointVersion:      remoteEndpointMachineDashboardVersion,
		PayloadSchemaVersion: remoteEndpointMachineDashboardVersion,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_dashboard_foreign",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
		Payload:              mustJSONRaw(t, remoteMachineDashboardPayload{WorkspaceID: "wsp-1", MachineID: "mch-1", Window: "1d", AgentIDs: []string{"agent-foreign"}}),
	}))

	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, remoteMachineDashboardInvalidOutcome, result.Outcome)
	require.Equal(t, remoteMachineDashboardInvalidOutcome, result.Error)
}

func TestRemoteMachineWorkspaceDeleteAdapterRejectsTraversalWithoutLeakingPath(t *testing.T) {
	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_delete_secret",
		RemoteRequestID:      "rrq_machine_delete_secret",
		EndpointID:           remoteEndpointMachineWorkspaceDelete,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_delete_secret",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 64 * 1024},
		Payload:              json.RawMessage(`{"directoryName":"../secret-machine-path"}`),
	}))
	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, "endpoint_error", result.Outcome)
	require.Equal(t, "endpoint_error", result.Error)
	require.NotContains(t, result.Error, "secret-machine-path")
}

func TestRemoteMachineDirectoryListAdapterReturnsBoundedDirectoryPage(t *testing.T) {
	base := canonicalRemoteMachineDirectoryPath(t, t.TempDir())
	require.NoError(t, os.MkdirAll(filepath.Join(base, "alpha"), 0o755))
	require.NoError(t, os.MkdirAll(filepath.Join(base, "beta"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(base, "README.md"), []byte("readme"), 0o644))

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_dir_list",
		RemoteRequestID:      "rrq_machine_dir_list",
		EndpointID:           remoteEndpointMachineDirectoryList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_dir_list",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
		Payload:              mustJSONRaw(t, remoteMachineDirectoryListPayload{Path: base, Limit: 50}),
	}))

	result := nextRemoteRequestResult(t, ch)
	require.True(t, result.Success)
	require.Equal(t, "ok", result.Outcome)
	require.Contains(t, string(result.Payload), `"directories"`)
	require.Contains(t, string(result.Payload), `"alpha"`)
	require.Contains(t, string(result.Payload), `"beta"`)
	require.NotContains(t, string(result.Payload), "README.md")
	require.LessOrEqual(t, len(result.Payload), 512*1024)
}

func TestRemoteMachineDirectoryListAdapterMapsStableOutcomesWithoutPathLeak(t *testing.T) {
	tmp := canonicalRemoteMachineDirectoryPath(t, t.TempDir())
	require.NoError(t, os.MkdirAll(filepath.Join(tmp, "child"), 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(tmp, "file.txt"), []byte("x"), 0o644))

	unique := "glm2-adapter-marker-7c2e9b41"
	missingPath := filepath.Join(tmp, unique)

	cases := []struct {
		name     string
		payload  remoteMachineDirectoryListPayload
		outcome  string
		markerPO string
	}{
		{
			name:     "relative path",
			payload:  remoteMachineDirectoryListPayload{Path: "relative/path"},
			outcome:  "machine_directory_invalid",
			markerPO: "relative",
		},
		{
			name:     "missing path",
			payload:  remoteMachineDirectoryListPayload{Path: missingPath},
			outcome:  "machine_directory_invalid",
			markerPO: unique,
		},
		{
			name:     "file path",
			payload:  remoteMachineDirectoryListPayload{Path: filepath.Join(tmp, "file.txt")},
			outcome:  "machine_directory_invalid",
			markerPO: "file.txt",
		},
		{
			name:     "invalid cursor",
			payload:  remoteMachineDirectoryListPayload{Path: tmp, Cursor: "abc"},
			outcome:  "machine_directory_invalid",
			markerPO: "abc",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ch := &Protocol{
				id:                     "agentteam-test",
				sendCh:                 make(chan []byte, sendBufSize),
				remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
			}
			ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
				Type:                 remoteRequestAssignType,
				SchemaVersion:        1,
				RequestID:            "rrq_machine_dir_fail",
				RemoteRequestID:      "rrq_machine_dir_fail",
				EndpointID:           remoteEndpointMachineDirectoryList,
				EndpointVersion:      1,
				PayloadSchemaVersion: 1,
				DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
				TimeoutBudgetMs:      1000,
				CancelToken:          "cancel_machine_dir_fail",
				Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
				Payload:              mustJSONRaw(t, tc.payload),
			}))
			result := nextRemoteRequestResult(t, ch)
			require.False(t, result.Success)
			require.Equal(t, tc.outcome, result.Outcome)
			require.Equal(t, tc.outcome, result.Error)
			// Path markers must never appear in outcome, error, or payload text.
			if tc.name == "missing path" {
				require.NotContains(t, result.Outcome, unique)
				require.NotContains(t, result.Error, unique)
				require.NotContains(t, string(result.Payload), unique)
			}
		})
	}
}

func TestRemoteMachineDirectoryListAdapterMapsUnreadableOutcome(t *testing.T) {
	// Build a custom registry whose handler closure delegates to
	// runMachineDirectoryList with an injected list dependency. This proves
	// the unreadable classification survives the full assignment -> registry
	// -> executor -> result path, not just the factored core. The injected
	// dependency returns ErrMachineDirectoryUnreadable deterministically so
	// the test never depends on chmod behavior or whether tests run as root.
	tmp := canonicalRemoteMachineDirectoryPath(t, t.TempDir())
	unique := "glm2-unreadable-marker-4f8c2d63"
	path := filepath.Join(tmp, unique)

	dep := func(rawPath string, _ workspacefs.ListOptions) ([]workspacefs.MachineDirectory, workspacefs.MachineDirectoryPageInfo, error) {
		require.Equal(t, path, rawPath, "handler must forward raw path byte-exact")
		return nil, workspacefs.MachineDirectoryPageInfo{}, workspacefs.ErrMachineDirectoryUnreadable
	}
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointMachineDirectoryList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			return runMachineDirectoryList(ctx, dep, assignment)
		},
	}})

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: registry,
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_dir_unreadable",
		RemoteRequestID:      "rrq_machine_dir_unreadable",
		EndpointID:           remoteEndpointMachineDirectoryList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_dir_unreadable",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
		Payload:              mustJSONRaw(t, remoteMachineDirectoryListPayload{Path: path}),
	}))
	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, "machine_directory_unreadable", result.Outcome)
	require.Equal(t, "machine_directory_unreadable", result.Error)
	require.Empty(t, result.Payload)
	require.NotContains(t, result.Outcome, unique)
	require.NotContains(t, result.Error, unique)
	require.NotContains(t, string(result.Payload), unique)
}

func TestRemoteMachineDirectoryListAdapterCapturesNoPathInSlog(t *testing.T) {
	tmp := canonicalRemoteMachineDirectoryPath(t, t.TempDir())
	unique := "glm2-slog-marker-3e7d5a91"
	missingPath := filepath.Join(tmp, unique)

	var captured []capturedRecord
	capture := &bytes.Buffer{}
	inner := slog.NewTextHandler(capture, &slog.HandlerOptions{Level: slog.LevelDebug})
	wrapped := capturingHandler{inner: inner, captured: &captured}
	prev := slog.Default()
	slog.SetDefault(slog.New(wrapped))
	t.Cleanup(func() { slog.SetDefault(prev) })

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: defaultDesktopRemoteEndpointRegistry(),
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_dir_slog",
		RemoteRequestID:      "rrq_machine_dir_slog",
		EndpointID:           remoteEndpointMachineDirectoryList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(time.Second).UTC().Format(time.RFC3339Nano),
		TimeoutBudgetMs:      1000,
		CancelToken:          "cancel_machine_dir_slog",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
		Payload:              mustJSONRaw(t, remoteMachineDirectoryListPayload{Path: missingPath}),
	}))
	result := nextRemoteRequestResult(t, ch)
	require.False(t, result.Success)
	require.Equal(t, "machine_directory_invalid", result.Outcome)
	require.Equal(t, "machine_directory_invalid", result.Error)

	// The raw path marker must never appear in outcome, error, payload, the
	// full rendered slog text, or any captured slog record's message/attrs.
	require.NotContains(t, result.Outcome, unique)
	require.NotContains(t, result.Error, unique)
	require.NotContains(t, string(result.Payload), unique)
	rendered := capture.String()
	require.NotContains(t, rendered, unique, "PATH LEAK: raw path bytes acked in slog output:\n%s", rendered)
	for i, r := range captured {
		require.False(t, strings.Contains(r.message, unique),
			"PATH LEAK: marker found in slog record %d message: %q", i, r.message)
		require.False(t, strings.Contains(r.attrs, unique),
			"PATH LEAK: marker found in slog record %d attrs: %q", i, r.attrs)
	}
}

func TestRemoteMachineDirectoryListAdapterEnvelopeStaysWithinTransportBudget(t *testing.T) {
	// Inject a synthetic page sized to land in the (440 KiB, 448 KiB] band
	// using at most MaxListLimit rows so the page stays inside the lower-layer
	// contract. NAME_MAX=255 makes a real filesystem fixture unable to reach
	// this band with <=200 rows, so injection is required.
	//
	// Per-row JSON is `{"name":"<name>","path":"<path>"},` plus a small
	// envelope overhead. With 200 rows and a target ~444 KiB body, each row
	// must contribute ~2270 bytes; we use 1100-char names plus a matching
	// path prefix to land in band.
	const rowCount = 200
	const nameLen = 1120
	directoryName := strings.Repeat("d", nameLen)
	directories := make([]workspacefs.MachineDirectory, rowCount)
	for i := 0; i < rowCount; i++ {
		directories[i] = workspacefs.MachineDirectory{
			Name: directoryName,
			Path: fmt.Sprintf("/synthetic/transport/%s-%d", directoryName, i),
		}
	}
	dep := func(_ string, _ workspacefs.ListOptions) ([]workspacefs.MachineDirectory, workspacefs.MachineDirectoryPageInfo, error) {
		return directories, workspacefs.MachineDirectoryPageInfo{
			CurrentPath: "/synthetic/transport",
			ParentPath:  "/synthetic",
			Limit:       rowCount,
			HasMore:     false,
		}, nil
	}
	registry := newDesktopRemoteEndpointRegistry([]desktopRemoteEndpointDescriptor{{
		EndpointID: remoteEndpointMachineDirectoryList,
		Version:    1,
		Handler: func(ctx context.Context, _ *Protocol, assignment remoteRequestAssignmentMsg) (json.RawMessage, string, error) {
			return runMachineDirectoryList(ctx, dep, assignment)
		},
	}})

	ch := &Protocol{
		id:                     "agentteam-test",
		sendCh:                 make(chan []byte, sendBufSize),
		remoteEndpointRegistry: registry,
	}
	ch.handleRemoteRequestAssignment(context.Background(), nil, mustJSON(t, remoteRequestAssignmentMsg{
		Type:                 remoteRequestAssignType,
		SchemaVersion:        1,
		RequestID:            "rrq_machine_dir_budget",
		RemoteRequestID:      "rrq_machine_dir_budget",
		EndpointID:           remoteEndpointMachineDirectoryList,
		EndpointVersion:      1,
		PayloadSchemaVersion: 1,
		DeadlineAt:           time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339),
		TimeoutBudgetMs:      5000,
		CancelToken:          "cancel_machine_dir_budget",
		Limits:               remoteEndpointLimits{RequestBytes: 1024, ResponseBytes: 512 * 1024},
		Payload:              mustJSONRaw(t, remoteMachineDirectoryListPayload{Path: "/synthetic/transport", Limit: rowCount}),
	}))
	result := nextRemoteRequestResult(t, ch)
	require.True(t, result.Success)
	require.Equal(t, "ok", result.Outcome)
	// The workspacefs layer caps the page at 448 KiB; the transport cap is
	// 512 KiB. The synthetic fixture lands in the (440, 448] band so the
	// assertion exercises a real near-budget envelope.
	require.Greater(t, len(result.Payload), 440*1024, "fixture must produce a >440 KiB envelope")
	require.LessOrEqual(t, len(result.Payload), 448*1024, "fixture must respect the workspacefs page budget")
	require.LessOrEqual(t, len(result.Payload), 512*1024, "emitted payload must stay under the 512 KiB transport cap")
}

func TestMachineDirectoryListOutcomeMapperClassifiesSentinels(t *testing.T) {
	require.Equal(t, "machine_directory_invalid", machineDirectoryListOutcomeForError(workspacefs.ErrMachineDirectoryInvalid))
	require.Equal(t, "machine_directory_unreadable", machineDirectoryListOutcomeForError(workspacefs.ErrMachineDirectoryUnreadable))
	require.Equal(t, "endpoint_error", machineDirectoryListOutcomeForError(errors.New("unexpected implementation error")))
}

// canonicalRemoteMachineDirectoryPath resolves macOS /var -> /private/var so
// adapter tests compare against the same canonical native form that the
// production workspacefs layer returns.
func canonicalRemoteMachineDirectoryPath(t *testing.T, path string) string {
	t.Helper()
	resolved, err := filepath.EvalSymlinks(path)
	require.NoError(t, err)
	return resolved
}

func mustJSONRaw(t *testing.T, value any) json.RawMessage {
	t.Helper()
	return json.RawMessage(mustJSON(t, value))
}

func setupRemoteReviewTestRepo(t *testing.T, repoDir string) {
	t.Helper()
	runGitCommand(t, repoDir, "init")
	runGitCommand(t, repoDir, "checkout", "-b", "main")
	runGitCommand(t, repoDir, "config", "user.name", "AgentTeam Test")
	runGitCommand(t, repoDir, "config", "user.email", "agentteam-test@example.com")
	require.NoError(t, os.WriteFile(filepath.Join(repoDir, "tracked.txt"), []byte("base\n"), 0o644))
	runGitCommand(t, repoDir, "add", "tracked.txt")
	runGitCommand(t, repoDir, "commit", "-m", "init")
}

func runGitCommand(t *testing.T, dir string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	output, err := cmd.CombinedOutput()
	require.NoError(t, err, "git %v failed: %s", args, string(output))
}
