iop/apps/edge/internal/transport/integration_test.go
toki b4124f0bd6 feat: CLI setup, edge/node transport refactor, and infrastructure updates
- 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
2026-05-20 16:37:42 +09:00

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")
}
}