- Add CLI core setup for edge and node services - Refactor edge transport layer (server, integration tests) - Refactor node transport layer (parser, session, heartbeat, client) - Add main_test.go files for edge and node commands - Add input package for edge service - Add go.work and go.work.sum for workspace support - Update configs, docs, and project rules
196 lines
5.8 KiB
Go
196 lines
5.8 KiB
Go
package transport_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
toki "git.toki-labs.com/toki/proto-socket/go"
|
|
|
|
edgenode "iop/apps/edge/internal/node"
|
|
"iop/apps/edge/internal/transport"
|
|
"iop/packages/config"
|
|
eventpkg "iop/packages/events"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func getFreePort(t *testing.T) string {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
addr := l.Addr().String()
|
|
l.Close()
|
|
return addr
|
|
}
|
|
|
|
func waitForRegistryEntry(ctx context.Context, registry *edgenode.Registry, nodeID string) (*edgenode.NodeEntry, bool) {
|
|
ticker := time.NewTicker(25 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
if entry, ok := registry.Get(nodeID); ok {
|
|
return entry, true
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, false
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEdgeServerIntegration(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
logger := zap.NewNop()
|
|
listenAddr := getFreePort(t)
|
|
registry := edgenode.NewRegistry()
|
|
nodeStore, err := edgenode.LoadFromConfig([]config.NodeDefinition{
|
|
{
|
|
ID: "node-test-01",
|
|
Alias: "test-node",
|
|
Token: "test-token",
|
|
Adapters: config.AdaptersConf{
|
|
Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://localhost:11434"},
|
|
},
|
|
Runtime: config.RuntimeConf{Concurrency: 2, WorkspaceRoot: "/tmp/iop/test"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("load node store: %v", err)
|
|
}
|
|
|
|
// 1. Edge 서버 구동
|
|
server, err := transport.NewServer(listenAddr, registry, nodeStore, logger)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server: %v", err)
|
|
}
|
|
nodeEventCh := make(chan *iop.EdgeNodeEvent, 4)
|
|
server.SetNodeEventHandler(func(event *iop.EdgeNodeEvent) {
|
|
nodeEventCh <- event
|
|
})
|
|
if err := server.Start(ctx); err != nil {
|
|
t.Fatalf("failed to start server: %v", err)
|
|
}
|
|
defer server.Stop()
|
|
|
|
// 2. Mock Node 클라이언트 접속
|
|
host, portStr, _ := net.SplitHostPort(listenAddr)
|
|
port := 0
|
|
fmt.Sscanf(portStr, "%d", &port)
|
|
|
|
// Node parser map (mock)
|
|
nodeParser := toki.ParserMap{
|
|
toki.TypeNameOf(&iop.RegisterResponse{}): func(b []byte) (proto.Message, error) {
|
|
m := &iop.RegisterResponse{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
toki.TypeNameOf(&iop.RunRequest{}): func(b []byte) (proto.Message, error) {
|
|
m := &iop.RunRequest{}
|
|
return m, proto.Unmarshal(b, m)
|
|
},
|
|
}
|
|
|
|
client, err := toki.DialTcp(ctx, host, port, 30, 10, nodeParser)
|
|
if err != nil {
|
|
t.Fatalf("failed to dial edge server: %v", err)
|
|
}
|
|
defer client.Close()
|
|
|
|
// 3. Node가 RegisterRequest를 보내고 RegisterResponse를 받는다
|
|
resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse](
|
|
&client.Communicator,
|
|
&iop.RegisterRequest{Token: "test-token"},
|
|
2*time.Second,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("register request: %v", err)
|
|
}
|
|
if !resp.GetAccepted() {
|
|
t.Fatalf("expected accepted register response, got reason %q", resp.GetReason())
|
|
}
|
|
rec, ok := nodeStore.FindByToken("test-token")
|
|
if !ok {
|
|
t.Fatal("expected record for test-token in store")
|
|
}
|
|
wantNodeID := rec.ID
|
|
if wantNodeID != "node-test-01" {
|
|
t.Fatalf("expected stored id to match explicit config id, got %q", wantNodeID)
|
|
}
|
|
if resp.GetNodeId() != wantNodeID {
|
|
t.Fatalf("expected node id %q, got %q", wantNodeID, resp.GetNodeId())
|
|
}
|
|
if resp.GetAlias() != "test-node" {
|
|
t.Fatalf("expected alias %q, got %q", "test-node", resp.GetAlias())
|
|
}
|
|
if resp.GetConfig().GetRuntime().GetConcurrency() != 2 {
|
|
t.Fatalf("expected runtime concurrency 2, got %d", resp.GetConfig().GetRuntime().GetConcurrency())
|
|
}
|
|
if len(resp.GetConfig().GetAdapters()) != 2 {
|
|
t.Fatalf("expected mock and ollama adapters, got %d", len(resp.GetConfig().GetAdapters()))
|
|
}
|
|
select {
|
|
case event := <-nodeEventCh:
|
|
if event.GetType() != eventpkg.TypeNodeConnected {
|
|
t.Fatalf("event type: got %q want %q", event.GetType(), eventpkg.TypeNodeConnected)
|
|
}
|
|
if event.GetNodeId() != wantNodeID || event.GetAlias() != "test-node" || event.GetReason() != eventpkg.ReasonRegistered {
|
|
t.Fatalf("unexpected connected event: %+v", event)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for node connected event")
|
|
}
|
|
|
|
// 4. Registry 등록 여부 확인
|
|
entry, ok := waitForRegistryEntry(ctx, registry, wantNodeID)
|
|
if !ok {
|
|
t.Fatal("node was not registered in edge registry within timeout")
|
|
}
|
|
|
|
if got := registry.Count(); got != 1 {
|
|
t.Fatalf("expected registry count 1, got %d", got)
|
|
}
|
|
if entry == nil {
|
|
t.Fatal("expected registry entry, got nil")
|
|
}
|
|
if got := entry.NodeID; got != wantNodeID {
|
|
t.Fatalf("expected node id %q, got %q", wantNodeID, got)
|
|
}
|
|
if got := entry.Alias; got != "test-node" {
|
|
t.Fatalf("expected alias %q, got %q", "test-node", got)
|
|
}
|
|
|
|
// 5. Alias로 Resolve 확인
|
|
if e, err := registry.Resolve("test-node"); err != nil || e.NodeID != wantNodeID {
|
|
t.Fatalf("failed to resolve by alias %q: %v", "test-node", err)
|
|
}
|
|
|
|
if err := client.Close(); err != nil {
|
|
t.Fatalf("close client: %v", err)
|
|
}
|
|
select {
|
|
case event := <-nodeEventCh:
|
|
if event.GetType() != eventpkg.TypeNodeDisconnected {
|
|
t.Fatalf("event type: got %q want %q", event.GetType(), eventpkg.TypeNodeDisconnected)
|
|
}
|
|
if event.GetNodeId() != wantNodeID || event.GetAlias() != "test-node" || event.GetReason() != eventpkg.ReasonTransportClosed {
|
|
t.Fatalf("unexpected disconnected event: %+v", event)
|
|
}
|
|
if event.GetMetadata()[eventpkg.MetadataTransportCloseReason] != toki.DisconnectReasonRemoteClosed {
|
|
t.Fatalf("transport close reason: got %q want %q", event.GetMetadata()[eventpkg.MetadataTransportCloseReason], toki.DisconnectReasonRemoteClosed)
|
|
}
|
|
if event.GetMetadata()[eventpkg.MetadataTransportCloseError] == "" {
|
|
t.Fatalf("expected transport close error metadata, got %+v", event.GetMetadata())
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timeout waiting for node disconnected event")
|
|
}
|
|
}
|