From 0dd60a90230485e9b22702e07d0013bcf79b3b22 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Fri, 24 Apr 2026 02:57:26 +0000 Subject: [PATCH] fix(gateway): align launcher fallback contract and dispatch launch args Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: pionxe <148670367+pionxe@users.noreply.github.com> --- Makefile | 6 +-- README.md | 2 +- docs/gateway-detailed-design.md | 2 +- docs/gateway-rpc-api.md | 24 ++++++------ .../gateway/adapters/urlscheme/dispatcher.go | 14 ++++++- .../adapters/urlscheme/dispatcher_test.go | 26 ++++++++++++- internal/gateway/launcher/launcher.go | 17 +++------ internal/gateway/launcher/launcher_test.go | 37 ++++++++----------- 8 files changed, 77 insertions(+), 51 deletions(-) diff --git a/Makefile b/Makefile index c7548ef1..3b5260e6 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ install-skills: @./scripts/install_skills.sh docs-gateway: - @go run ./scripts/generate_gateway_rpc_examples + @go run -tags gatewaydocgen ./scripts/generate_gateway_rpc_examples.go docs-gateway-check: - @go run ./scripts/generate_gateway_rpc_examples - @git diff --exit-code -- docs/reference/gateway-rpc-api.md + @go run -tags gatewaydocgen ./scripts/generate_gateway_rpc_examples.go + @git diff --exit-code -- docs/generated/gateway-rpc-examples.json diff --git a/README.md b/README.md index 36c9df35..aa52a792 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ go run ./cmd/neocode url-dispatch --url "neocode://review?path=README.md" 1. `NEOCODE_GATEWAY_BIN` 显式路径 2. `PATH` 中 `neocode-gateway` -3. 回退当前可执行 `neocode gateway` +3. `PATH` 中 `neocode` 并追加子命令 `gateway` ## 安装脚本 diff --git a/docs/gateway-detailed-design.md b/docs/gateway-detailed-design.md index b8ef0a08..afd78666 100644 --- a/docs/gateway-detailed-design.md +++ b/docs/gateway-detailed-design.md @@ -73,7 +73,7 @@ stateDiagram-v2 1. `NEOCODE_GATEWAY_BIN` 显式路径 2. `PATH` 中的 `neocode-gateway` -3. 当前可执行回退 `neocode gateway` +3. `PATH` 中的 `neocode` 并追加子命令 `gateway` 约束:仅允许一次受控回退,失败后返回确定性错误。 diff --git a/docs/gateway-rpc-api.md b/docs/gateway-rpc-api.md index 8d0549ba..ccededf9 100644 --- a/docs/gateway-rpc-api.md +++ b/docs/gateway-rpc-api.md @@ -106,12 +106,12 @@ type BindStreamParams struct { { "jsonrpc": "2.0", "id": "bind-1", - "result": { - "type": "ack", - "action": "bind_stream", - "request_id": "bind-1", - "session_id": "sess-1", - "run_id": "run-1", + "result": { + "type": "ack", + "action": "bind_stream", + "request_id": "bind-1", + "session_id": "sess-1", + "run_id": "run-1", "payload": { "message": "stream binding updated", "channel": "ws" @@ -182,12 +182,12 @@ type RunParams struct { { "jsonrpc": "2.0", "id": "run-req-1", - "result": { - "type": "ack", - "action": "run", - "request_id": "run-req-1", - "session_id": "sess-1", - "run_id": "run-1", + "result": { + "type": "ack", + "action": "run", + "request_id": "run-req-1", + "session_id": "sess-1", + "run_id": "run-1", "payload": { "message": "run accepted" } diff --git a/internal/gateway/adapters/urlscheme/dispatcher.go b/internal/gateway/adapters/urlscheme/dispatcher.go index 14ec9ffb..95495a41 100644 --- a/internal/gateway/adapters/urlscheme/dispatcher.go +++ b/internal/gateway/adapters/urlscheme/dispatcher.go @@ -370,7 +370,9 @@ func (d *Dispatcher) launchGateway(ctx context.Context, listenAddress string, re LaunchMode: spec.LaunchMode, ResolvedExec: spec.Executable, }) - if err := startGatewayFn(spec); err != nil { + launchSpec := spec + launchSpec.Args = buildGatewayLaunchArgs(spec.Args, listenAddress) + if err := startGatewayFn(launchSpec); err != nil { d.emitLaunchDecisionLog(launchDecisionLogEntry{ RequestID: requestID, Method: string(protocol.MethodWakeOpenURL), @@ -415,6 +417,16 @@ func (d *Dispatcher) launchGateway(ctx context.Context, listenAddress string, re return nil } +// buildGatewayLaunchArgs 构造自动拉起参数,确保子进程监听地址与调度重拨地址一致。 +func buildGatewayLaunchArgs(baseArgs []string, listenAddress string) []string { + args := append([]string(nil), baseArgs...) + normalizedListenAddress := strings.TrimSpace(listenAddress) + if normalizedListenAddress == "" { + return args + } + return append(args, "--listen", normalizedListenAddress) +} + // waitGatewayReady 在单次回退窗口内轮询网关连通性,超时后返回确定性错误。 func (d *Dispatcher) waitGatewayReady(ctx context.Context, listenAddress string) error { nowFn := d.nowFn diff --git a/internal/gateway/adapters/urlscheme/dispatcher_test.go b/internal/gateway/adapters/urlscheme/dispatcher_test.go index 8dbbcd6e..d193a867 100644 --- a/internal/gateway/adapters/urlscheme/dispatcher_test.go +++ b/internal/gateway/adapters/urlscheme/dispatcher_test.go @@ -9,6 +9,7 @@ import ( "log" "net" "os" + "reflect" "strings" "testing" "time" @@ -1282,11 +1283,13 @@ func TestDispatcherLaunchGatewayBranches(t *testing.T) { }) t.Run("start gateway failed", func(t *testing.T) { + var capturedSpec launcher.LaunchSpec dispatcher := &Dispatcher{ resolveLaunchSpecFn: func() (launcher.LaunchSpec, error) { return launcher.LaunchSpec{LaunchMode: launcher.LaunchModePathBinary, Executable: "/tmp/neocode-gateway"}, nil }, - startGatewayFn: func(launcher.LaunchSpec) error { + startGatewayFn: func(spec launcher.LaunchSpec) error { + capturedSpec = spec return errors.New("start failed") }, } @@ -1295,6 +1298,9 @@ func TestDispatcherLaunchGatewayBranches(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "start failed") { t.Fatalf("expected start failed error, got %v", err) } + if !reflect.DeepEqual(capturedSpec.Args, []string{"--listen", "stub://gateway"}) { + t.Fatalf("launch args = %#v, want %#v", capturedSpec.Args, []string{"--listen", "stub://gateway"}) + } }) } @@ -1497,6 +1503,24 @@ func TestDispatcherEmitLaunchDecisionLogNilGuards(t *testing.T) { dispatcher.emitLaunchDecisionLog(launchDecisionLogEntry{}) } +func TestBuildGatewayLaunchArgs(t *testing.T) { + t.Run("appends listen argument when provided", func(t *testing.T) { + got := buildGatewayLaunchArgs([]string{"gateway"}, " unix:///tmp/neocode.sock ") + want := []string{"gateway", "--listen", "unix:///tmp/neocode.sock"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("buildGatewayLaunchArgs() = %#v, want %#v", got, want) + } + }) + + t.Run("keeps base args when listen is empty", func(t *testing.T) { + got := buildGatewayLaunchArgs([]string{"gateway"}, " ") + want := []string{"gateway"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("buildGatewayLaunchArgs() = %#v, want %#v", got, want) + } + }) +} + type cancelOnWriteErrorConn struct { stubDispatchConn cancel context.CancelFunc diff --git a/internal/gateway/launcher/launcher.go b/internal/gateway/launcher/launcher.go index 0f4ea54d..f29b7a89 100644 --- a/internal/gateway/launcher/launcher.go +++ b/internal/gateway/launcher/launcher.go @@ -14,7 +14,7 @@ const ( LaunchModeExplicitPath = "explicit_path" // LaunchModePathBinary 表示命中 PATH 中的 neocode-gateway。 LaunchModePathBinary = "path_neocode_gateway" - // LaunchModeFallbackSubcommand 表示回退到当前可执行的 gateway 子命令。 + // LaunchModeFallbackSubcommand 表示回退到 PATH 中 neocode 的 gateway 子命令。 LaunchModeFallbackSubcommand = "fallback_neocode_gateway_subcommand" ) @@ -31,9 +31,9 @@ type ResolveOptions struct { } // ResolveGatewayLaunchSpec 解析网关可执行发现顺序: -// 显式路径(NEOCODE_GATEWAY_BIN) > PATH(neocode-gateway) > 当前可执行 + gateway 子命令。 +// 显式路径(NEOCODE_GATEWAY_BIN) > PATH(neocode-gateway) > PATH(neocode) + gateway 子命令。 func ResolveGatewayLaunchSpec(options ResolveOptions) (LaunchSpec, error) { - return resolveGatewayLaunchSpecWithDeps(options, exec.LookPath, os.Executable) + return resolveGatewayLaunchSpecWithDeps(options, exec.LookPath) } // StartDetachedGateway 以非阻塞方式拉起网关进程并释放父进程句柄。 @@ -55,7 +55,6 @@ func StartDetachedGateway(spec LaunchSpec) error { func resolveGatewayLaunchSpecWithDeps( options ResolveOptions, lookPathFn func(string) (string, error), - executableFn func() (string, error), ) (LaunchSpec, error) { resolveByLookup := func(binary string) (string, error) { resolved, err := lookPathFn(strings.TrimSpace(binary)) @@ -84,18 +83,14 @@ func resolveGatewayLaunchSpecWithDeps( }, nil } - currentExecutable, err := executableFn() + resolvedFallbackExecutable, err := resolveByLookup("neocode") if err != nil { - return LaunchSpec{}, fmt.Errorf("resolve current executable: %w", err) - } - trimmedCurrentExecutable := strings.TrimSpace(currentExecutable) - if trimmedCurrentExecutable == "" { - return LaunchSpec{}, fmt.Errorf("resolve current executable: empty executable path") + return LaunchSpec{}, err } return LaunchSpec{ LaunchMode: LaunchModeFallbackSubcommand, - Executable: trimmedCurrentExecutable, + Executable: resolvedFallbackExecutable, Args: []string{"gateway"}, }, nil } diff --git a/internal/gateway/launcher/launcher_test.go b/internal/gateway/launcher/launcher_test.go index 6268f8c5..9738cf5e 100644 --- a/internal/gateway/launcher/launcher_test.go +++ b/internal/gateway/launcher/launcher_test.go @@ -20,9 +20,6 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) { } return "", errors.New("unexpected lookup") }, - func() (string, error) { - return "/usr/local/bin/neocode", nil - }, ) if err != nil { t.Fatalf("resolveGatewayLaunchSpecWithDeps() error = %v", err) @@ -47,9 +44,6 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) { } return "", errors.New("unexpected lookup") }, - func() (string, error) { - return "/usr/local/bin/neocode", nil - }, ) if err != nil { t.Fatalf("resolveGatewayLaunchSpecWithDeps() error = %v", err) @@ -65,14 +59,18 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) { } }) - t.Run("fallback to current executable subcommand", func(t *testing.T) { + t.Run("fallback to neocode subcommand", func(t *testing.T) { spec, err := resolveGatewayLaunchSpecWithDeps( ResolveOptions{}, - func(string) (string, error) { - return "", errors.New("not found") - }, - func() (string, error) { - return "/usr/local/bin/neocode", nil + func(binary string) (string, error) { + switch binary { + case "neocode-gateway": + return "", errors.New("not found") + case "neocode": + return "/usr/local/bin/neocode", nil + default: + return "", errors.New("unexpected lookup") + } }, ) if err != nil { @@ -95,23 +93,20 @@ func TestResolveGatewayLaunchSpecWithDeps(t *testing.T) { func(string) (string, error) { return "", errors.New("missing") }, - func() (string, error) { - return "/usr/local/bin/neocode", nil - }, ) if err == nil { t.Fatal("expected explicit lookup error") } }) - t.Run("fallback fails when current executable unavailable", func(t *testing.T) { + t.Run("fallback fails when neocode is unavailable", func(t *testing.T) { _, err := resolveGatewayLaunchSpecWithDeps( ResolveOptions{}, - func(string) (string, error) { - return "", errors.New("not found") - }, - func() (string, error) { - return "", errors.New("unavailable") + func(binary string) (string, error) { + if binary == "neocode-gateway" || binary == "neocode" { + return "", errors.New("not found") + } + return "", errors.New("unexpected lookup") }, ) if err == nil {