Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ cd neo-code
go run ./cmd/neocode
```

Gateway 独立进程(Step 1 骨架):

```bash
go run ./cmd/neocode-gateway
```

设置 API Key 示例(按你使用的 provider 选择):

```bash
Expand Down
87 changes: 87 additions & 0 deletions cmd/neocode-gateway/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

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

"neo-code/internal/gateway"
)

const (
defaultLogLevel = "info"
)

var errHelpRequested = errors.New("help requested")

// main 负责启动 Gateway 独立进程,并在收到系统信号时优雅退出。
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "neocode-gateway: %v\n", err)
os.Exit(1)
}
}

// run 解析启动参数并驱动网关服务生命周期。
func run() error {
listenAddress, logLevel, err := parseFlags()
if err != nil {
if errors.Is(err, errHelpRequested) {
return nil
}
return err
}

logger := log.New(os.Stderr, "neocode-gateway: ", log.LstdFlags)
logger.Printf("starting gateway (log-level=%s)", logLevel)

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

server, err := gateway.NewServer(gateway.ServerOptions{
ListenAddress: listenAddress,
Logger: logger,
})
if err != nil {
return err
}
defer func() {
_ = server.Close(context.Background())
}()

logger.Printf("gateway listen address: %s", server.ListenAddress())
return server.Serve(ctx, nil)
}

// parseFlags 解析命令行参数并执行基础校验。
func parseFlags() (listenAddress string, logLevel string, err error) {
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
fs.SetOutput(os.Stdout)

var listen string
var level string
fs.StringVar(&listen, "listen", "", "gateway listen address (optional override)")
fs.StringVar(&level, "log-level", defaultLogLevel, "gateway log level: debug|info|warn|error")

if parseErr := fs.Parse(os.Args[1:]); parseErr != nil {
if errors.Is(parseErr, flag.ErrHelp) {
return "", "", errHelpRequested
}
return "", "", parseErr
}

normalizedLevel := strings.ToLower(strings.TrimSpace(level))
switch normalizedLevel {
case "debug", "info", "warn", "error":
default:
return "", "", fmt.Errorf("invalid --log-level %q: must be debug|info|warn|error", level)
}

return strings.TrimSpace(listen), normalizedLevel, nil
}
89 changes: 89 additions & 0 deletions cmd/neocode-gateway/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"errors"
"flag"
"os"
"strings"
"testing"
)

func TestParseFlagsValid(t *testing.T) {
withArgs(t, []string{"neocode-gateway", "--listen", " /tmp/gateway.sock ", "--log-level", " WARN "}, func() {
listen, level, err := parseFlags()
if err != nil {
t.Fatalf("parse flags: %v", err)
}
if listen != "/tmp/gateway.sock" {
t.Fatalf("listen = %q, want %q", listen, "/tmp/gateway.sock")
}
if level != "warn" {
t.Fatalf("log level = %q, want %q", level, "warn")
}
})
}

func TestParseFlagsHelp(t *testing.T) {
withArgs(t, []string{"neocode-gateway", "--help"}, func() {
_, _, err := parseFlags()
if !errors.Is(err, errHelpRequested) {
t.Fatalf("parse flags error = %v, want %v", err, errHelpRequested)
}
})
}

func TestParseFlagsInvalidLogLevel(t *testing.T) {
withArgs(t, []string{"neocode-gateway", "--log-level", "trace"}, func() {
_, _, err := parseFlags()
if err == nil {
t.Fatal("expected invalid log level error")
}
if !strings.Contains(err.Error(), "invalid --log-level") {
t.Fatalf("error = %v, want contains %q", err, "invalid --log-level")
}
})
}

func TestParseFlagsUnknownFlag(t *testing.T) {
withArgs(t, []string{"neocode-gateway", "--unknown"}, func() {
_, _, err := parseFlags()
if err == nil {
t.Fatal("expected parse error")
}
if errors.Is(err, flag.ErrHelp) {
t.Fatalf("error = %v, should not be help error", err)
}
})
}

func TestRunHelp(t *testing.T) {
withArgs(t, []string{"neocode-gateway", "--help"}, func() {
if err := run(); err != nil {
t.Fatalf("run help: %v", err)
}
})
}

func TestRunInvalidLogLevel(t *testing.T) {
withArgs(t, []string{"neocode-gateway", "--log-level", "trace"}, func() {
err := run()
if err == nil {
t.Fatal("expected run error")
}
if !strings.Contains(err.Error(), "invalid --log-level") {
t.Fatalf("error = %v, want contains %q", err, "invalid --log-level")
}
})
}

func withArgs(t *testing.T, args []string, fn func()) {
t.Helper()

originalArgs := os.Args
os.Args = args
defer func() {
os.Args = originalArgs
}()

fn()
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
)

require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
Expand Down
Loading
Loading