Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions cmd/neocode-gateway/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"syscall"

"neo-code/internal/gateway"
"neo-code/internal/gateway/adapters"
)

// main 负责启动独立 Gateway 进程并监听本地 IPC 请求。
func main() {
transportMode := flag.String("transport", "auto", "IPC transport mode: auto|npipe|uds")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a new runnable binary and flags (neocode-gateway, --transport, --endpoint) but does not update user-facing docs. Per repo rules, please add README/docs entries for startup command and platform-specific default endpoints.

endpoint := flag.String("endpoint", "", "IPC endpoint path (socket path or named pipe)")
flag.Parse()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

bootstrap, err := gateway.NewBootstrap(gateway.BootstrapConfig{
Transport: *transportMode,
Endpoint: *endpoint,
}, adapters.NewCoreMock())
if err != nil {
fmt.Fprintf(os.Stderr, "neocode-gateway: %v\n", err)
os.Exit(1)
}
defer func() {
_ = bootstrap.Close()
}()

fmt.Fprintf(os.Stdout, "neocode-gateway listening on %s\n", bootstrap.Endpoint())
if err := bootstrap.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
fmt.Fprintf(os.Stderr, "neocode-gateway: %v\n", err)
os.Exit(1)
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce h1:TqjP/BTDrwN7zP9xyXVuLsMBXYMt6LLYi55PlrIcq8U=
github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:ifHPsLndGGzvgzcaXUvzmt6LxKT4pJ+uzEhtnMt+f7A=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
36 changes: 36 additions & 0 deletions internal/gateway/adapters/core_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package adapters

import "context"

const defaultPongMessage = "Pong"

// CoreClient 定义网关访问核心能力的最小端口接口。
type CoreClient interface {
Ping(ctx context.Context) (string, error)
}

// CoreMock 是 CoreClient 的最小可运行 mock 实现。
type CoreMock struct {
pong string
}

// NewCoreMock 创建默认返回 Pong 的核心 mock。
func NewCoreMock() *CoreMock {
return &CoreMock{pong: defaultPongMessage}
}

// NewCoreMockWithPong 创建可自定义 Pong 文案的核心 mock。
func NewCoreMockWithPong(pong string) *CoreMock {
if pong == "" {
pong = defaultPongMessage
}
return &CoreMock{pong: pong}
}

// Ping 返回 mock 端固定的 pong 结果。
func (m *CoreMock) Ping(_ context.Context) (string, error) {
if m == nil {
return defaultPongMessage, nil
}
return m.pong, nil
}
39 changes: 39 additions & 0 deletions internal/gateway/adapters/core_mock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package adapters

import (
"context"
"testing"
)

func TestCoreMockPing(t *testing.T) {
mock := NewCoreMock()
pong, err := mock.Ping(context.Background())
if err != nil {
t.Fatalf("Ping() error = %v", err)
}
if pong != "Pong" {
t.Fatalf("Ping() = %s, want Pong", pong)
}
}

func TestCoreMockPingCustomMessage(t *testing.T) {
mock := NewCoreMockWithPong("Custom")
pong, err := mock.Ping(context.Background())
if err != nil {
t.Fatalf("Ping() error = %v", err)
}
if pong != "Custom" {
t.Fatalf("Ping() = %s, want Custom", pong)
}
}

func TestCoreMockPingNilReceiver(t *testing.T) {
var mock *CoreMock
pong, err := mock.Ping(context.Background())
if err != nil {
t.Fatalf("Ping() error = %v", err)
}
if pong != "Pong" {
t.Fatalf("Ping() = %s, want Pong", pong)
}
}
69 changes: 69 additions & 0 deletions internal/gateway/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package gateway

import (
"context"
"errors"
"fmt"
"strings"

"neo-code/internal/gateway/adapters"
"neo-code/internal/gateway/handlers"
"neo-code/internal/gateway/protocol"
"neo-code/internal/gateway/transport"
)

// BootstrapConfig 描述网关进程启动时的最小配置。
type BootstrapConfig struct {
Transport string
Endpoint string
}

// Bootstrap 负责装配并驱动 Gateway IPC 服务器。
type Bootstrap struct {
server *transport.Server
}

// NewBootstrap 根据配置与 Core 端口装配可运行的网关实例。
func NewBootstrap(cfg BootstrapConfig, core adapters.CoreClient) (*Bootstrap, error) {
if core == nil {
core = adapters.NewCoreMock()
}

router := protocol.NewRouter()
pingHandler := handlers.NewPingHandler(core)
router.Register(protocol.MethodCorePing, pingHandler.Handle)

server, err := transport.NewServer(transport.Config{
Mode: transport.Mode(strings.TrimSpace(cfg.Transport)),
Endpoint: strings.TrimSpace(cfg.Endpoint),
}, router.HandleRaw)
if err != nil {
return nil, fmt.Errorf("build gateway server: %w", err)
}

return &Bootstrap{server: server}, nil
}

// Endpoint 返回网关当前监听地址。
func (b *Bootstrap) Endpoint() string {
if b == nil || b.server == nil {
return ""
}
return b.server.Endpoint()
}

// Run 启动网关服务并在上下文取消后退出。
func (b *Bootstrap) Run(ctx context.Context) error {
if b == nil || b.server == nil {
return errors.New("gateway bootstrap is not initialized")
}
return b.server.Serve(ctx)
}

// Close 主动关闭网关服务监听器。
func (b *Bootstrap) Close() error {
if b == nil || b.server == nil {
return nil
}
return b.server.Close()
}
87 changes: 87 additions & 0 deletions internal/gateway/bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package gateway

import (
"context"
"encoding/json"
"net"
"path/filepath"
"runtime"
"testing"
"time"
)

func TestBootstrapPingPongOverIPC(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uds integration test runs on non-windows")
}

socketPath := filepath.Join(t.TempDir(), "gateway.sock")
bootstrap, err := NewBootstrap(BootstrapConfig{
Transport: "uds",
Endpoint: socketPath,
}, nil)
if err != nil {
t.Fatalf("NewBootstrap() error = %v", err)
}
defer func() {
_ = bootstrap.Close()
}()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

runDone := make(chan error, 1)
go func() {
runDone <- bootstrap.Run(ctx)
}()

conn := waitForGatewayDial(t, bootstrap.Endpoint())
defer conn.Close()

encoder := json.NewEncoder(conn)
decoder := json.NewDecoder(conn)
if err := encoder.Encode(map[string]any{
"jsonrpc": "2.0",
"id": "1",
"method": "core.ping",
}); err != nil {
t.Fatalf("Encode() error = %v", err)
}

var response map[string]any
if err := decoder.Decode(&response); err != nil {
t.Fatalf("Decode() error = %v", err)
}

if response["error"] != nil {
t.Fatalf("response error = %v, want nil", response["error"])
}
if got := response["result"]; got != "Pong" {
t.Fatalf("response result = %v, want Pong", got)
}

cancel()
select {
case err := <-runDone:
if err != nil {
t.Fatalf("Run() error = %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Run() did not exit after cancel")
}
}

func waitForGatewayDial(t *testing.T, endpoint string) net.Conn {
t.Helper()

deadline := time.Now().Add(2 * time.Second)
for time.Now().Before(deadline) {
conn, err := net.Dial("unix", endpoint)
if err == nil {
return conn
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("dial gateway endpoint %s timeout", endpoint)
return nil
}
32 changes: 32 additions & 0 deletions internal/gateway/handlers/ping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package handlers

import (
"context"

"neo-code/internal/gateway/adapters"
"neo-code/internal/gateway/protocol"
)

// PingHandler 负责处理 core.ping 请求并返回基础连通性结果。
type PingHandler struct {
core adapters.CoreClient
}

// NewPingHandler 创建 ping 请求处理器。
func NewPingHandler(core adapters.CoreClient) *PingHandler {
return &PingHandler{core: core}
}

// Handle 执行 ping 请求并把结果映射为 JSON-RPC 响应。
func (h *PingHandler) Handle(ctx context.Context, req protocol.Request) protocol.Response {
if h == nil || h.core == nil {
return protocol.NewErrorResponse(req.ID, protocol.ErrorCodeInternalError, "core client unavailable")
}

pong, err := h.core.Ping(ctx)
if err != nil {
return protocol.NewErrorResponse(req.ID, protocol.ErrorCodeInternalError, "core ping failed")
}

return protocol.NewSuccessResponse(req.ID, pong)
}
57 changes: 57 additions & 0 deletions internal/gateway/handlers/ping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package handlers

import (
"context"
"errors"
"testing"

"neo-code/internal/gateway/protocol"
)

type coreClientStub struct {
pong string
err error
}

func (s coreClientStub) Ping(_ context.Context) (string, error) {
if s.err != nil {
return "", s.err
}
return s.pong, nil
}

func TestPingHandlerHandleSuccess(t *testing.T) {
handler := NewPingHandler(coreClientStub{pong: "Pong"})
response := handler.Handle(context.Background(), protocol.Request{ID: []byte(`"1"`)})

if response.Error != nil {
t.Fatalf("Handle() error = %+v, want nil", response.Error)
}
if response.Result != "Pong" {
t.Fatalf("Handle() result = %v, want Pong", response.Result)
}
}

func TestPingHandlerHandleCoreError(t *testing.T) {
handler := NewPingHandler(coreClientStub{err: errors.New("boom")})
response := handler.Handle(context.Background(), protocol.Request{ID: []byte(`"1"`)})

if response.Error == nil {
t.Fatal("Handle() error = nil, want internal error")
}
if response.Error.Code != protocol.ErrorCodeInternalError {
t.Fatalf("Handle() error code = %d, want %d", response.Error.Code, protocol.ErrorCodeInternalError)
}
}

func TestPingHandlerHandleNilCore(t *testing.T) {
handler := NewPingHandler(nil)
response := handler.Handle(context.Background(), protocol.Request{})

if response.Error == nil {
t.Fatal("Handle() error = nil, want internal error")
}
if response.Error.Code != protocol.ErrorCodeInternalError {
t.Fatalf("Handle() error code = %d, want %d", response.Error.Code, protocol.ErrorCodeInternalError)
}
}
Loading
Loading