package node_test

import (
	"context"
	"errors"
	"fmt"
	"io"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"go.uber.org/zap"

	"iop/apps/node/internal/node"
	"iop/apps/node/internal/runtime"
	"iop/apps/node/internal/store"
	"iop/apps/node/internal/transport"
	iop "iop/proto/gen/iop"
)

// --- test doubles ---

type countingAdapter struct {
	executeCalls int32
	lastSpec     runtime.ExecutionSpec
}

func (a *countingAdapter) Name() string { return "test" }
func (a *countingAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "test", Targets: []string{"v1"}, MaxConcurrency: 1}, nil
}
func (a *countingAdapter) Execute(_ context.Context, spec runtime.ExecutionSpec, _ runtime.EventSink) error {
	atomic.AddInt32(&a.executeCalls, 1)
	a.lastSpec = spec
	return nil
}

// failingAdapter returns a fixed error from Execute.
type failingAdapter struct{ err error }

func (a *failingAdapter) Name() string { return "failing" }
func (a *failingAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "failing"}, nil
}
func (a *failingAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	return a.err
}

// blockingAdapter blocks until its context is cancelled, then returns ErrRunCancelled.
type blockingAdapter struct {
	started chan struct{}
	done    chan struct{}
}

func newBlockingAdapter() *blockingAdapter {
	return &blockingAdapter{started: make(chan struct{}), done: make(chan struct{})}
}

func (a *blockingAdapter) Name() string { return "blocking" }
func (a *blockingAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "blocking"}, nil
}
func (a *blockingAdapter) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	close(a.started)
	<-ctx.Done()
	close(a.done)
	return runtime.ErrRunCancelled
}

// terminatingAdapter implements SessionTerminator.
type terminatingAdapter struct {
	terminateCalls int32
	lastTarget     string
	lastSessionID  string
}

func (a *terminatingAdapter) Name() string { return "terminating" }
func (a *terminatingAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "terminating"}, nil
}
func (a *terminatingAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	return nil
}
func (a *terminatingAdapter) TerminateSession(_ context.Context, target, sessionID string) error {
	atomic.AddInt32(&a.terminateCalls, 1)
	a.lastTarget = target
	a.lastSessionID = sessionID
	return nil
}

type instanceKeyAdapter struct {
	instanceKey string
}

func (a *instanceKeyAdapter) Name() string { return "ollama" }
func (a *instanceKeyAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{
		AdapterName:    "ollama",
		InstanceKey:    a.instanceKey,
		Targets:        []string{"llama3"},
		MaxConcurrency: 4,
	}, nil
}
func (a *instanceKeyAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	return nil
}

type commandAdapter struct {
	lastReq runtime.CommandRequest
}

func (a *commandAdapter) Name() string { return "command" }
func (a *commandAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "command", Targets: []string{"v2", "v1"}, MaxConcurrency: 3}, nil
}
func (a *commandAdapter) Execute(_ context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	return nil
}
func (a *commandAdapter) HandleCommand(ctx context.Context, req runtime.CommandRequest) (runtime.CommandResponse, error) {
	a.lastReq = req
	switch req.Type {
	case runtime.CommandTypeUsageStatus:
		return runtime.CommandResponse{
			RequestID: req.RequestID,
			Type:      req.Type,
			Adapter:   req.Adapter,
			Target:    req.Target,
			SessionID: req.SessionID,
			UsageStatus: &runtime.AgentUsageStatus{
				RawOutput: "success",
			},
		}, nil
	default:
		return runtime.CommandResponse{}, errors.New("command not supported by adapter")
	}
}

type fixedRouter struct {
	adapterName  string
	adapter      runtime.Adapter
	adapters     map[string]runtime.Adapter
	lookupErrors map[string]error // optional per-adapter errors for LookupAdapter
}

func (r *fixedRouter) Resolve(_ context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, error) {
	return runtime.ExecutionSpec{
		RunID:       req.RunID,
		Adapter:     r.adapterName,
		Target:      req.Target,
		SessionID:   req.SessionID,
		SessionMode: req.SessionMode,
		Background:  req.Background,
		Workspace:   req.Workspace,
		Policy:      req.Policy,
		Input:       req.Input,
		TimeoutSec:  req.TimeoutSec,
		Metadata:    req.Metadata,
	}, nil
}

func (r *fixedRouter) ResolveAdapter(ctx context.Context, req runtime.RunRequest) (runtime.ExecutionSpec, runtime.Adapter, error) {
	spec, err := r.Resolve(ctx, req)
	if err != nil {
		return runtime.ExecutionSpec{}, nil, err
	}
	a, ok := r.adapters[spec.Adapter]
	if !ok {
		return runtime.ExecutionSpec{}, nil, fmt.Errorf("router: adapter %q not found", spec.Adapter)
	}
	return spec, a, nil
}

func (r *fixedRouter) LookupAdapter(adapterName string) (runtime.Adapter, error) {
	if r.lookupErrors != nil {
		if err, ok := r.lookupErrors[adapterName]; ok {
			return nil, err
		}
	}
	a, ok := r.adapters[adapterName]
	if !ok {
		return nil, fmt.Errorf("adapter %q not found", adapterName)
	}
	return a, nil
}

func (r *fixedRouter) GetAdapter(adapterName string) (runtime.Adapter, bool) {
	a, ok := r.adapters[adapterName]
	return a, ok
}

type errorRouter struct{ err error }

func (r *errorRouter) Resolve(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, error) {
	return runtime.ExecutionSpec{}, r.err
}

func (r *errorRouter) ResolveAdapter(_ context.Context, _ runtime.RunRequest) (runtime.ExecutionSpec, runtime.Adapter, error) {
	return runtime.ExecutionSpec{}, nil, r.err
}

func (r *errorRouter) LookupAdapter(_ string) (runtime.Adapter, error) {
	return nil, r.err
}

func (r *errorRouter) GetAdapter(_ string) (runtime.Adapter, bool) {
	return nil, false
}

func makeNode(t *testing.T, rtr runtime.Router) (*node.Node, *store.Store) {
	t.Helper()
	return makeNodeWithConcurrency(t, rtr, 0)
}

func makeNodeWithConcurrency(t *testing.T, rtr runtime.Router, concurrency int) (*node.Node, *store.Store) {
	t.Helper()
	st, err := store.New(":memory:", zap.NewNop())
	if err != nil {
		t.Fatalf("store: %v", err)
	}
	t.Cleanup(func() { _ = st.Close() })
	return node.New("test-node", rtr, st, concurrency, io.Discard, zap.NewNop()), st
}

// --- existing tests (updated) ---

func TestOnRunRequest_RouterError(t *testing.T) {
	n, _ := makeNode(t, &errorRouter{err: errors.New("boom")})

	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{RunId: "run-1"})
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "node: resolve:") {
		t.Fatalf("expected resolve prefix, got %v", err)
	}
}

func TestOnRunRequest_AdapterNotFound(t *testing.T) {
	router := &fixedRouter{adapterName: "missing", adapters: make(map[string]runtime.Adapter)}
	n, _ := makeNode(t, router)

	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{RunId: "run-1"})
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "not found") {
		t.Fatalf("expected adapter lookup error, got %v", err)
	}
}

func TestOnRunRequest_Success(t *testing.T) {
	adapter := &countingAdapter{}
	router := &fixedRouter{adapterName: "test", adapters: make(map[string]runtime.Adapter)}
	router.adapters["test"] = adapter
	n, st := makeNode(t, router)

	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId:     "run-1",
		Adapter:   "test",
		Target:    "v1",
		Workspace: "/config/workspace/iop",
	})
	if err != nil {
		t.Fatalf("run request: %v", err)
	}
	if got := atomic.LoadInt32(&adapter.executeCalls); got != 1 {
		t.Fatalf("expected 1 execute call, got %d", got)
	}
	if adapter.lastSpec.Workspace != "/config/workspace/iop" {
		t.Fatalf("adapter workspace: got %q", adapter.lastSpec.Workspace)
	}

	run, err := st.GetRun(context.Background(), "run-1")
	if err != nil {
		t.Fatalf("get run: %v", err)
	}
	if run == nil {
		t.Fatal("expected persisted run record")
	}
	if run.Status != "completed" {
		t.Fatalf("expected completed status, got %q", run.Status)
	}
}

// --- new tests ---

func TestOnRunRequest_ForegroundAdapterErrorReturned(t *testing.T) {
	adapter := &failingAdapter{err: errors.New("adapter boom")}
	router := &fixedRouter{adapterName: "failing", adapters: make(map[string]runtime.Adapter)}
	router.adapters["failing"] = adapter
	n, st := makeNode(t, router)

	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId:   "run-fail",
		Adapter: "failing",
		Target:  "v1",
	})
	if err == nil {
		t.Fatal("expected error from OnRunRequest")
	}
	if !strings.Contains(err.Error(), "adapter boom") {
		t.Fatalf("expected adapter boom in error, got %v", err)
	}

	run, err := st.GetRun(context.Background(), "run-fail")
	if err != nil {
		t.Fatalf("get run: %v", err)
	}
	if run == nil {
		t.Fatal("expected persisted run record")
	}
	if run.Status != "failed" {
		t.Fatalf("expected failed status, got %q", run.Status)
	}
	if !strings.Contains(run.Error, "adapter boom") {
		t.Fatalf("expected adapter boom in stored error, got %q", run.Error)
	}
}

func TestOnRunRequest_ForegroundCancelReturnedAndStored(t *testing.T) {
	adapter := &failingAdapter{err: runtime.ErrRunCancelled}
	router := &fixedRouter{adapterName: "failing", adapters: make(map[string]runtime.Adapter)}
	router.adapters["failing"] = adapter
	n, st := makeNode(t, router)

	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId:   "run-cancel-fg",
		Adapter: "failing",
		Target:  "v1",
	})
	if !errors.Is(err, runtime.ErrRunCancelled) {
		t.Fatalf("expected ErrRunCancelled, got %v", err)
	}
	run, err := st.GetRun(context.Background(), "run-cancel-fg")
	if err != nil {
		t.Fatalf("get run: %v", err)
	}
	if run == nil || run.Status != "cancelled" {
		t.Fatalf("expected cancelled status, got %+v", run)
	}
}

func TestOnRunRequest_BackgroundReturnsBeforeAdapterCompletes(t *testing.T) {
	ba := newBlockingAdapter()
	router := &fixedRouter{adapterName: "blocking", adapters: make(map[string]runtime.Adapter)}
	router.adapters["blocking"] = ba
	n, st := makeNode(t, router)

	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId:      "run-bg",
		Adapter:    "blocking",
		Target:     "v1",
		Background: true,
	})
	if err != nil {
		t.Fatalf("OnRunRequest: %v", err)
	}

	// OnRunRequest should return before adapter finishes.
	select {
	case <-ba.started:
	case <-time.After(2 * time.Second):
		t.Fatal("adapter never started")
	}

	// Cancel via OnCancel so the blocking adapter finishes.
	if err := n.OnCancel(context.Background(), &transport.Session{}, &iop.CancelRequest{RunId: "run-bg"}); err != nil {
		t.Fatalf("OnCancel: %v", err)
	}

	select {
	case <-ba.done:
	case <-time.After(2 * time.Second):
		t.Fatal("adapter did not finish after cancel")
	}

	// Wait briefly for completeRun goroutine to update the store.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		run, err := st.GetRun(context.Background(), "run-bg")
		if err != nil {
			t.Fatalf("get run: %v", err)
		}
		if run != nil && run.Status == "cancelled" {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	run, _ := st.GetRun(context.Background(), "run-bg")
	t.Fatalf("expected cancelled status, got %q", run.Status)
}

func TestOnCancel_CancelsRunViaRunManager(t *testing.T) {
	ba := newBlockingAdapter()
	router := &fixedRouter{adapterName: "blocking", adapters: make(map[string]runtime.Adapter)}
	router.adapters["blocking"] = ba
	n, _ := makeNode(t, router)

	go func() {
		_ = n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId:   "run-cancel",
			Adapter: "blocking",
			Target:  "v1",
		})
	}()

	<-ba.started

	if err := n.OnCancel(context.Background(), &transport.Session{}, &iop.CancelRequest{RunId: "run-cancel"}); err != nil {
		t.Fatalf("OnCancel: %v", err)
	}

	select {
	case <-ba.done:
	case <-time.After(2 * time.Second):
		t.Fatal("adapter did not finish after cancel")
	}
}

func TestOnCancel_TerminatesAdapterSession(t *testing.T) {
	ta := &terminatingAdapter{}
	router := &fixedRouter{adapterName: "terminating", adapters: make(map[string]runtime.Adapter)}
	router.adapters["terminating"] = ta
	n, _ := makeNode(t, router)

	err := n.OnCancel(context.Background(), &transport.Session{}, &iop.CancelRequest{
		RunId:     "",
		Adapter:   "terminating",
		Target:    "codex",
		SessionId: "session-a",
		Action:    iop.CancelAction_CANCEL_ACTION_TERMINATE_SESSION,
	})
	if err != nil {
		t.Fatalf("OnCancel terminate: %v", err)
	}
	if atomic.LoadInt32(&ta.terminateCalls) != 1 {
		t.Fatal("expected TerminateSession to be called once")
	}
	if ta.lastTarget != "codex" || ta.lastSessionID != "session-a" {
		t.Fatalf("unexpected terminate args: target=%q session=%q", ta.lastTarget, ta.lastSessionID)
	}
}

func TestOnCommandRequest_Success(t *testing.T) {
	ca := &commandAdapter{}
	router := &fixedRouter{adapterName: "command", adapters: make(map[string]runtime.Adapter)}
	router.adapters["command"] = ca
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-1",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
		Adapter:   "command",
		Target:    "codex",
		SessionId: "default",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if resp.GetError() != "" {
		t.Fatalf("expected no error, got %q", resp.GetError())
	}
	if resp.GetUsageStatus().GetRawOutput() != "success" {
		t.Fatalf("expected success raw output, got %q", resp.GetUsageStatus().GetRawOutput())
	}
	if ca.lastReq.Type != runtime.CommandTypeUsageStatus || ca.lastReq.Target != "codex" {
		t.Fatalf("unexpected last req: %+v", ca.lastReq)
	}
}

func TestOnCommandRequest_MissingAdapter(t *testing.T) {
	router := &fixedRouter{adapterName: "missing", adapters: make(map[string]runtime.Adapter)}
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-1",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
		Adapter:   "missing",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if !strings.Contains(resp.GetError(), "not found") {
		t.Fatalf("expected not found error, got %q", resp.GetError())
	}
}

func TestOnCommandRequest_NotSupported(t *testing.T) {
	ta := &terminatingAdapter{}
	router := &fixedRouter{adapterName: "terminating", adapters: make(map[string]runtime.Adapter)}
	router.adapters["terminating"] = ta
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-1",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
		Adapter:   "terminating",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if !strings.Contains(resp.GetError(), "does not support commands") {
		t.Fatalf("expected not supported error, got %q", resp.GetError())
	}
}

func TestOnCommandRequest_UnspecifiedRejected(t *testing.T) {
	ca := &commandAdapter{}
	router := &fixedRouter{adapterName: "command", adapters: make(map[string]runtime.Adapter)}
	router.adapters["command"] = ca
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-1",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_UNSPECIFIED,
		Adapter:   "command",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if !strings.Contains(resp.GetError(), "unsupported command type") {
		t.Fatalf("expected unsupported command type error, got %q", resp.GetError())
	}
	// Adapter must not be invoked for unsupported command types.
	if ca.lastReq.RequestID != "" {
		t.Fatalf("expected adapter to be skipped, got lastReq %+v", ca.lastReq)
	}
}

func TestOnCommandRequest_Capabilities(t *testing.T) {
	ca := &commandAdapter{}
	router := &fixedRouter{adapterName: "command", adapters: make(map[string]runtime.Adapter)}
	router.adapters["command"] = ca
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-cap",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES,
		Adapter:   "command",
		Target:    "codex",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if resp.GetError() != "" {
		t.Fatalf("expected no error, got %q", resp.GetError())
	}
	if resp.GetType() != iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES {
		t.Fatalf("type: got %v want CAPABILITIES", resp.GetType())
	}
	if got := resp.GetResult()["targets"]; got != "v1,v2" {
		t.Fatalf("result[targets]: got %q want %q", got, "v1,v2")
	}
	if got := resp.GetResult()["adapter"]; got != "command" {
		t.Fatalf("result[adapter]: got %q want %q", got, "command")
	}
	if got := resp.GetResult()["max_concurrency"]; got != "3" {
		t.Fatalf("result[max_concurrency]: got %q want %q", got, "3")
	}
	// Capabilities must not go through the adapter CommandHandler.
	if ca.lastReq.RequestID != "" {
		t.Fatalf("expected CommandHandler to be skipped, got %+v", ca.lastReq)
	}
}

// TestOnCommandRequest_CapabilitiesWithoutCommandHandler verifies CAPABILITIES
// succeeds for an adapter that does not implement runtime.CommandHandler.
func TestOnCommandRequest_CapabilitiesWithoutCommandHandler(t *testing.T) {
	ta := &terminatingAdapter{} // no CommandHandler
	router := &fixedRouter{adapterName: "terminating", adapters: make(map[string]runtime.Adapter)}
	router.adapters["terminating"] = ta
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-cap",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES,
		Adapter:   "terminating",
		Target:    "codex",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if resp.GetError() != "" {
		t.Fatalf("expected no error, got %q", resp.GetError())
	}
	if got := resp.GetResult()["adapter"]; got != "terminating" {
		t.Fatalf("result[adapter]: got %q want %q", got, "terminating")
	}
}

func TestOnCommandRequest_TransportStatus(t *testing.T) {
	ta := &terminatingAdapter{} // adapter not required for transport_status
	router := &fixedRouter{adapterName: "terminating", adapters: make(map[string]runtime.Adapter)}
	router.adapters["terminating"] = ta
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-tx",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS,
		Adapter:   "terminating",
		Target:    "codex",
		SessionId: "sess-a",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if resp.GetError() != "" {
		t.Fatalf("expected no error, got %q", resp.GetError())
	}
	if got := resp.GetResult()["node_id"]; got != "test-node" {
		t.Fatalf("result[node_id]: got %q want %q", got, "test-node")
	}
	if got := resp.GetResult()["adapter"]; got != "terminating" {
		t.Fatalf("result[adapter]: got %q want %q", got, "terminating")
	}
	if got := resp.GetResult()["session_id"]; got != "sess-a" {
		t.Fatalf("result[session_id]: got %q want %q", got, "sess-a")
	}
	if _, ok := resp.GetResult()["connected"]; !ok {
		t.Fatalf("result missing connected key: %+v", resp.GetResult())
	}
	if got, ok := resp.GetResult()["state"]; !ok || (got != "connected" && got != "disconnected") {
		t.Fatalf("result[state]: got %q, want \"connected\" or \"disconnected\"", got)
	}
}

func TestOnCommandRequest_AdapterError(t *testing.T) {
	ca := &commandAdapter{}
	router := &fixedRouter{adapterName: "command", adapters: make(map[string]runtime.Adapter)}
	router.adapters["command"] = ca
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-1",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_SESSION_LIST,
		Adapter:   "command",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if !strings.Contains(resp.GetError(), "command not supported by adapter") {
		t.Fatalf("expected adapter unsupported error, got %q", resp.GetError())
	}
}

func TestOnCommandRequest_TransportStatusDefaultsSessionID(t *testing.T) {
	ta := &terminatingAdapter{}
	router := &fixedRouter{adapterName: "terminating", adapters: make(map[string]runtime.Adapter)}
	router.adapters["terminating"] = ta
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-ts-default",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_TRANSPORT_STATUS,
		Adapter:   "terminating",
		Target:    "codex",
		SessionId: "",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if resp.GetError() != "" {
		t.Fatalf("expected no error, got %q", resp.GetError())
	}
	if got := resp.GetSessionId(); got != "default" {
		t.Fatalf("response SessionId: got %q want %q", got, "default")
	}
	if got := resp.GetResult()["session_id"]; got != "default" {
		t.Fatalf("result[session_id]: got %q want %q", got, "default")
	}
	if got, ok := resp.GetResult()["state"]; !ok || (got != "connected" && got != "disconnected") {
		t.Fatalf("result[state]: got %q, want \"connected\" or \"disconnected\"", got)
	}
}

func TestOnCommandRequest_ErrorResponsesPreserveEnvelope(t *testing.T) {
	cases := []struct {
		name    string
		req     *iop.NodeCommandRequest
		adapter runtime.Adapter
		wantErr string
	}{
		{
			name: "missing adapter",
			req: &iop.NodeCommandRequest{
				RequestId: "req-missing",
				Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
				Adapter:   "missing",
				Target:    "tgt",
				SessionId: "sess-x",
			},
			adapter: nil,
			wantErr: "not found",
		},
		{
			name: "unsupported command type",
			req: &iop.NodeCommandRequest{
				RequestId: "req-unspec",
				Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_UNSPECIFIED,
				Adapter:   "terminating",
				Target:    "tgt",
				SessionId: "sess-y",
			},
			adapter: &terminatingAdapter{},
			wantErr: "unsupported command type",
		},
		{
			name: "adapter does not support commands",
			req: &iop.NodeCommandRequest{
				RequestId: "req-nosupport",
				Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
				Adapter:   "terminating",
				Target:    "tgt",
				SessionId: "sess-z",
			},
			adapter: &terminatingAdapter{},
			wantErr: "does not support commands",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			adapters := make(map[string]runtime.Adapter)
			if tc.adapter != nil {
				adapters[tc.req.Adapter] = tc.adapter
			}
			router := &fixedRouter{adapterName: tc.req.Adapter, adapters: adapters}
			n, _ := makeNode(t, router)

			resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, tc.req)
			if err != nil {
				t.Fatalf("OnCommandRequest: %v", err)
			}
			if !strings.Contains(resp.GetError(), tc.wantErr) {
				t.Fatalf("error: got %q want contains %q", resp.GetError(), tc.wantErr)
			}
			if got := resp.GetRequestId(); got != tc.req.RequestId {
				t.Fatalf("RequestId: got %q want %q", got, tc.req.RequestId)
			}
			if got := resp.GetType(); got != tc.req.Type {
				t.Fatalf("Type: got %v want %v", got, tc.req.Type)
			}
			if got := resp.GetAdapter(); got != tc.req.Adapter {
				t.Fatalf("Adapter: got %q want %q", got, tc.req.Adapter)
			}
			if got := resp.GetTarget(); got != tc.req.Target {
				t.Fatalf("Target: got %q want %q", got, tc.req.Target)
			}
			if got := resp.GetSessionId(); got != tc.req.SessionId {
				t.Fatalf("SessionId: got %q want %q", got, tc.req.SessionId)
			}
		})
	}
}

// --- LookupAdapter / ambiguous error propagation tests ---

func TestOnCancel_TerminateSession_AmbiguousAdapterError(t *testing.T) {
	ambigErr := fmt.Errorf("adapter \"ollama\" is ambiguous: matches instance keys [ollama@local ollama@dgx]; use an instance key")
	router := &fixedRouter{
		adapterName:  "ollama",
		adapters:     make(map[string]runtime.Adapter),
		lookupErrors: map[string]error{"ollama": ambigErr},
	}
	n, _ := makeNode(t, router)

	err := n.OnCancel(context.Background(), &transport.Session{}, &iop.CancelRequest{
		Adapter:   "ollama",
		Target:    "llama3",
		SessionId: "sess-1",
		Action:    iop.CancelAction_CANCEL_ACTION_TERMINATE_SESSION,
	})
	if err == nil {
		t.Fatal("expected error for ambiguous adapter lookup")
	}
	if !strings.Contains(err.Error(), "ambiguous") {
		t.Errorf("expected 'ambiguous' in error, got %v", err)
	}
	if !strings.Contains(err.Error(), "instance key") {
		t.Errorf("expected 'instance key' guidance in error, got %v", err)
	}
}

func TestOnCancel_TerminateSession_ExactInstanceKey(t *testing.T) {
	ta := &terminatingAdapter{}
	router := &fixedRouter{
		adapterName: "ollama@local",
		adapters:    map[string]runtime.Adapter{"ollama@local": ta},
	}
	n, _ := makeNode(t, router)

	err := n.OnCancel(context.Background(), &transport.Session{}, &iop.CancelRequest{
		Adapter:   "ollama@local",
		Target:    "llama3",
		SessionId: "sess-a",
		Action:    iop.CancelAction_CANCEL_ACTION_TERMINATE_SESSION,
	})
	if err != nil {
		t.Fatalf("OnCancel with exact instance key: %v", err)
	}
	if atomic.LoadInt32(&ta.terminateCalls) != 1 {
		t.Fatal("expected TerminateSession to be called once")
	}
	if ta.lastTarget != "llama3" || ta.lastSessionID != "sess-a" {
		t.Errorf("terminate args: target=%q session=%q", ta.lastTarget, ta.lastSessionID)
	}
}

func TestOnCommandRequest_Capabilities_AmbiguousAdapter(t *testing.T) {
	ambigErr := fmt.Errorf("adapter \"cli\" is ambiguous: matches instance keys [cli@claude cli@codex]; use an instance key")
	router := &fixedRouter{
		adapterName:  "cli",
		adapters:     make(map[string]runtime.Adapter),
		lookupErrors: map[string]error{"cli": ambigErr},
	}
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-ambig-cap",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES,
		Adapter:   "cli",
		Target:    "codex",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if !strings.Contains(resp.GetError(), "ambiguous") {
		t.Errorf("expected 'ambiguous' in response error, got %q", resp.GetError())
	}
	if !strings.Contains(resp.GetError(), "instance key") {
		t.Errorf("expected 'instance key' guidance in response error, got %q", resp.GetError())
	}
}

func TestOnCommandRequest_AdapterDispatch_AmbiguousAdapter(t *testing.T) {
	ambigErr := fmt.Errorf("adapter \"cli\" is ambiguous: matches instance keys [cli@claude cli@codex]; use an instance key")
	router := &fixedRouter{
		adapterName:  "cli",
		adapters:     make(map[string]runtime.Adapter),
		lookupErrors: map[string]error{"cli": ambigErr},
	}
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-ambig-cmd",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_USAGE_STATUS,
		Adapter:   "cli",
		Target:    "codex",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if !strings.Contains(resp.GetError(), "ambiguous") {
		t.Errorf("expected 'ambiguous' in response error, got %q", resp.GetError())
	}
}

func TestOnCommandRequest_MultiAdapterCapabilities(t *testing.T) {
	ika1 := &instanceKeyAdapter{instanceKey: "ollama@local"}
	ika2 := &instanceKeyAdapter{instanceKey: "ollama@dgx"}
	router := &fixedRouter{
		adapters: map[string]runtime.Adapter{
			"ollama@local": ika1,
			"ollama@dgx":   ika2,
		},
	}
	n, _ := makeNode(t, router)

	cases := []struct {
		adapter     string
		wantInstKey string
	}{
		{"ollama@local", "ollama@local"},
		{"ollama@dgx", "ollama@dgx"},
	}
	instKeys := make([]string, 0, len(cases))
	for _, tc := range cases {
		resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
			RequestId: "req-multi-" + tc.adapter,
			Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES,
			Adapter:   tc.adapter,
			Target:    "llama3",
		})
		if err != nil {
			t.Fatalf("CAPABILITIES %s: %v", tc.adapter, err)
		}
		if resp.GetError() != "" {
			t.Fatalf("CAPABILITIES %s: unexpected error %q", tc.adapter, resp.GetError())
		}
		if got := resp.GetResult()["instance_key"]; got != tc.wantInstKey {
			t.Errorf("CAPABILITIES %s: instance_key got %q want %q", tc.adapter, got, tc.wantInstKey)
		}
		if resp.GetAdapter() != tc.adapter {
			t.Errorf("CAPABILITIES %s: response adapter got %q want %q", tc.adapter, resp.GetAdapter(), tc.adapter)
		}
		instKeys = append(instKeys, resp.GetResult()["instance_key"])
	}
	if instKeys[0] == instKeys[1] {
		t.Errorf("expected distinct instance_key for distinct adapters, both got %q", instKeys[0])
	}
}

func TestOnCommandRequest_Capabilities_ExactInstanceKey(t *testing.T) {
	ika := &instanceKeyAdapter{instanceKey: "ollama@local"}
	router := &fixedRouter{
		adapterName: "ollama@local",
		adapters:    map[string]runtime.Adapter{"ollama@local": ika},
	}
	n, _ := makeNode(t, router)

	resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{
		RequestId: "req-exact-cap",
		Type:      iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES,
		Adapter:   "ollama@local",
		Target:    "llama3",
	})
	if err != nil {
		t.Fatalf("OnCommandRequest: %v", err)
	}
	if resp.GetError() != "" {
		t.Fatalf("expected no error for exact instance key, got %q", resp.GetError())
	}
	if got := resp.GetResult()["adapter"]; got != "ollama" {
		t.Errorf("result[adapter]: got %q want ollama", got)
	}
	if got := resp.GetResult()["instance_key"]; got != "ollama@local" {
		t.Errorf("result[instance_key]: got %q want ollama@local", got)
	}
}

// --- concurrency gate tests ---

// slowAdapter blocks until released, exposing a start channel so tests can
// synchronize. Execute returns nil on normal completion.
type slowAdapter struct {
	started chan struct{}
	release chan struct{}
}

func newSlowAdapter() *slowAdapter {
	return &slowAdapter{started: make(chan struct{}, 1), release: make(chan struct{})}
}

func (a *slowAdapter) Name() string { return "slow" }
func (a *slowAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "slow", MaxConcurrency: 1}, nil
}

// slowAdapterUnlimited is like slowAdapter but reports MaxConcurrency=0 (unlimited).
type slowAdapterUnlimited struct {
	started chan struct{}
	release chan struct{}
}

func newSlowAdapterUnlimited() *slowAdapterUnlimited {
	return &slowAdapterUnlimited{started: make(chan struct{}, 1), release: make(chan struct{})}
}

func (a *slowAdapterUnlimited) Name() string { return "slow" }
func (a *slowAdapterUnlimited) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "slow", MaxConcurrency: 0}, nil
}
func (a *slowAdapterUnlimited) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	select {
	case a.started <- struct{}{}:
	default:
	}
	select {
	case <-a.release:
	case <-ctx.Done():
		return runtime.ErrRunCancelled
	}
	return nil
}

// slowHighCapAdapter is like slowAdapter but reports MaxConcurrency=4,
// used to verify that the global limit overrides a looser adapter cap.
type slowHighCapAdapter struct {
	started chan struct{}
	release chan struct{}
}

func newSlowHighCapAdapter() *slowHighCapAdapter {
	return &slowHighCapAdapter{started: make(chan struct{}, 1), release: make(chan struct{})}
}

func (a *slowHighCapAdapter) Name() string { return "slow-hicap" }
func (a *slowHighCapAdapter) Capabilities(_ context.Context) (runtime.Capabilities, error) {
	return runtime.Capabilities{AdapterName: "slow-hicap", MaxConcurrency: 4}, nil
}
func (a *slowHighCapAdapter) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	select {
	case a.started <- struct{}{}:
	default:
	}
	select {
	case <-a.release:
	case <-ctx.Done():
		return runtime.ErrRunCancelled
	}
	return nil
}

func (a *slowAdapter) Execute(ctx context.Context, _ runtime.ExecutionSpec, _ runtime.EventSink) error {
	select {
	case a.started <- struct{}{}:
	default:
	}
	select {
	case <-a.release:
	case <-ctx.Done():
		return runtime.ErrRunCancelled
	}
	return nil
}

// TestConcurrencyLimit_RejectSecondForeground verifies that a second foreground
// run is rejected immediately when the global limit is 1 and one run is active.
func TestConcurrencyLimit_RejectSecondForeground(t *testing.T) {
	sa := newSlowAdapter()
	router := &fixedRouter{adapterName: "slow", adapters: map[string]runtime.Adapter{"slow": sa}}
	n, _ := makeNodeWithConcurrency(t, router, 1)

	errc := make(chan error, 1)
	go func() {
		errc <- n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-fg-1", Adapter: "slow", Target: "v1",
		})
	}()

	// Wait until the first run has started executing.
	select {
	case <-sa.started:
	case <-time.After(2 * time.Second):
		t.Fatal("first run never started")
	}

	// Second run must be rejected.
	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-fg-2", Adapter: "slow", Target: "v1",
	})
	if !errors.Is(err, node.ErrConcurrencyLimitExceeded) {
		t.Fatalf("expected ErrConcurrencyLimitExceeded, got %v", err)
	}

	// Release the first run and drain it.
	close(sa.release)
	if err := <-errc; err != nil {
		t.Fatalf("first run: %v", err)
	}
}

// TestConcurrencyLimit_RejectBackgroundRun verifies that a background run is
// also subject to the global concurrency gate.
func TestConcurrencyLimit_RejectBackgroundRun(t *testing.T) {
	sa := newSlowAdapter()
	router := &fixedRouter{adapterName: "slow", adapters: map[string]runtime.Adapter{"slow": sa}}
	n, _ := makeNodeWithConcurrency(t, router, 1)

	errc := make(chan error, 1)
	go func() {
		errc <- n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-bg-hold", Adapter: "slow", Target: "v1",
		})
	}()

	select {
	case <-sa.started:
	case <-time.After(2 * time.Second):
		t.Fatal("hold run never started")
	}

	// Background run must also be rejected.
	err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-bg-2", Adapter: "slow", Target: "v1", Background: true,
	})
	if !errors.Is(err, node.ErrConcurrencyLimitExceeded) {
		t.Fatalf("expected ErrConcurrencyLimitExceeded for background run, got %v", err)
	}

	close(sa.release)
	if err := <-errc; err != nil {
		t.Fatalf("hold run: %v", err)
	}
}

// TestConcurrencyLimit_PermitReleasedAfterCompletion verifies that after the
// first run completes, a subsequent run can acquire the permit.
func TestConcurrencyLimit_PermitReleasedAfterCompletion(t *testing.T) {
	sa := newSlowAdapter()
	router := &fixedRouter{adapterName: "slow", adapters: map[string]runtime.Adapter{"slow": sa}}
	n, _ := makeNodeWithConcurrency(t, router, 1)

	// First run: start, wait, release.
	errc := make(chan error, 1)
	go func() {
		errc <- n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-seq-1", Adapter: "slow", Target: "v1",
		})
	}()
	select {
	case <-sa.started:
	case <-time.After(2 * time.Second):
		t.Fatal("seq-1 never started")
	}
	close(sa.release)
	if err := <-errc; err != nil {
		t.Fatalf("seq-1: %v", err)
	}

	// Second run must succeed now that the permit is free.
	sa2 := newSlowAdapter()
	router.adapters["slow"] = sa2
	close(sa2.release) // let it complete immediately
	if err := n.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-seq-2", Adapter: "slow", Target: "v1",
	}); err != nil {
		t.Fatalf("seq-2 should succeed after permit released, got %v", err)
	}
}

// TestConcurrencyLimit_Unlimited verifies that global=0 + adapter cap=0 imposes no cap.
func TestConcurrencyLimit_Unlimited(t *testing.T) {
	const count = 5
	shared := newSlowAdapterUnlimited() // MaxConcurrency=0 (unlimited)
	router := &fixedRouter{adapterName: "slow", adapters: map[string]runtime.Adapter{"slow": shared}}
	nd, _ := makeNodeWithConcurrency(t, router, 0) // global unlimited

	errs := make(chan error, count)
	for i := 0; i < count; i++ {
		runID := fmt.Sprintf("run-unlimited-%d", i)
		go func(id string) {
			errs <- nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
				RunId: id, Adapter: "slow", Target: "v1",
			})
		}(runID)
	}

	// Release all.
	close(shared.release)

	// All must complete without ErrConcurrencyLimitExceeded.
	for i := 0; i < count; i++ {
		if err := <-errs; errors.Is(err, node.ErrConcurrencyLimitExceeded) {
			t.Fatalf("unexpected concurrency rejection with limit=0: %v", err)
		}
	}
}

// TestConcurrencyLimit_AdapterCapFallback verifies that when global=0 and adapter
// cap=1, the adapter cap is used as the effective limit.
func TestConcurrencyLimit_AdapterCapFallback(t *testing.T) {
	sa := newSlowAdapter() // MaxConcurrency=1
	router := &fixedRouter{adapterName: "slow", adapters: map[string]runtime.Adapter{"slow": sa}}
	nd, _ := makeNodeWithConcurrency(t, router, 0) // global=0 → use adapter cap

	errc := make(chan error, 1)
	go func() {
		errc <- nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-cap-1", Adapter: "slow", Target: "v1",
		})
	}()

	select {
	case <-sa.started:
	case <-time.After(2 * time.Second):
		t.Fatal("run-cap-1 never started")
	}

	// Second run must be rejected by adapter cap.
	err := nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-cap-2", Adapter: "slow", Target: "v1",
	})
	if !errors.Is(err, node.ErrConcurrencyLimitExceeded) {
		t.Fatalf("expected ErrConcurrencyLimitExceeded from adapter cap, got %v", err)
	}

	close(sa.release)
	if err := <-errc; err != nil {
		t.Fatalf("run-cap-1: %v", err)
	}
}

// TestConcurrencyLimit_GlobalBeatsAdapterCap verifies that when both global and
// adapter cap are > 0 and global < adapter cap, the global (stricter) limit applies.
func TestConcurrencyLimit_GlobalBeatsAdapterCap(t *testing.T) {
	// slowHighCapAdapter reports MaxConcurrency=4; global=1 → effective=1.
	sa := newSlowHighCapAdapter()
	router := &fixedRouter{adapterName: "slow-hicap", adapters: map[string]runtime.Adapter{"slow-hicap": sa}}
	nd, _ := makeNodeWithConcurrency(t, router, 1) // global=1, adapter cap=4 → effective=1

	errc := make(chan error, 1)
	go func() {
		errc <- nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-g1-1", Adapter: "slow-hicap", Target: "v1",
		})
	}()

	select {
	case <-sa.started:
	case <-time.After(2 * time.Second):
		t.Fatal("run-g1-1 never started")
	}

	// Second run must be rejected by global limit=1 even though adapter cap=4.
	err := nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-g1-2", Adapter: "slow-hicap", Target: "v1",
	})
	if !errors.Is(err, node.ErrConcurrencyLimitExceeded) {
		t.Fatalf("expected ErrConcurrencyLimitExceeded (global=1 beats adapter cap=4), got %v", err)
	}

	close(sa.release)
	if err := <-errc; err != nil {
		t.Fatalf("run-g1-1: %v", err)
	}
}

// TestConcurrencyLimit_GlobalBeatsAdapterCap_DifferentAdapters verifies that
// global=1 rejects a run on adapter B when adapter A already holds the global
// permit, even though both adapter caps are > 1.
func TestConcurrencyLimit_GlobalBeatsAdapterCap_DifferentAdapters(t *testing.T) {
	saA := newSlowHighCapAdapter() // adapter "slow-hicap-a", MaxConcurrency=4
	saB := newSlowHighCapAdapter() // adapter "slow-hicap-b", MaxConcurrency=4

	router := &fixedRouter{
		adapterName: "slow-hicap-a",
		adapters: map[string]runtime.Adapter{
			"slow-hicap-a": saA,
			"slow-hicap-b": saB,
		},
	}
	nd, _ := makeNodeWithConcurrency(t, router, 1) // global=1

	// First run on adapter A: hold the global permit.
	errc := make(chan error, 1)
	go func() {
		errc <- nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-xa-1", Adapter: "slow-hicap-a", Target: "v1",
		})
	}()
	select {
	case <-saA.started:
	case <-time.After(2 * time.Second):
		t.Fatal("run-xa-1 never started")
	}

	// Second run on adapter B must be rejected because global permit is taken.
	router.adapterName = "slow-hicap-b"
	err := nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-xb-1", Adapter: "slow-hicap-b", Target: "v1",
	})
	if !errors.Is(err, node.ErrConcurrencyLimitExceeded) {
		t.Fatalf("expected ErrConcurrencyLimitExceeded for cross-adapter second run, got %v", err)
	}

	close(saA.release)
	if err := <-errc; err != nil {
		t.Fatalf("run-xa-1: %v", err)
	}
}

// TestConcurrencyLimit_RejectStoreAndEvent verifies that a rejected run is stored
// with terminal status "rejected" and an error message is recorded.
func TestConcurrencyLimit_RejectStoreAndEvent(t *testing.T) {
	sa := newSlowAdapter()
	router := &fixedRouter{adapterName: "slow", adapters: map[string]runtime.Adapter{"slow": sa}}
	nd, st := makeNodeWithConcurrency(t, router, 1)

	errc := make(chan error, 1)
	go func() {
		errc <- nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
			RunId: "run-hold", Adapter: "slow", Target: "v1",
		})
	}()
	select {
	case <-sa.started:
	case <-time.After(2 * time.Second):
		t.Fatal("run-hold never started")
	}

	// Second run must be rejected.
	err := nd.OnRunRequest(context.Background(), &transport.Session{}, &iop.RunRequest{
		RunId: "run-rejected", Adapter: "slow", Target: "v1",
	})
	if !errors.Is(err, node.ErrConcurrencyLimitExceeded) {
		t.Fatalf("expected ErrConcurrencyLimitExceeded, got %v", err)
	}

	// Rejected run must be stored with status "rejected".
	run, err := st.GetRun(context.Background(), "run-rejected")
	if err != nil {
		t.Fatalf("GetRun: %v", err)
	}
	if run == nil {
		t.Fatal("expected rejected run to be stored")
	}
	if run.Status != "rejected" {
		t.Fatalf("expected status %q, got %q", "rejected", run.Status)
	}
	if run.Error == "" {
		t.Fatal("expected non-empty error message for rejected run")
	}

	close(sa.release)
	if err := <-errc; err != nil {
		t.Fatalf("run-hold: %v", err)
	}
}
