package agentnode import ( "context" "net" "os" "testing" "time" "aigateway.local/core/internal/platform/config" "aigateway.local/core/internal/platform/database" ) func TestAgentNodePostgreSQLLifecycle(t *testing.T) { databaseURL := os.Getenv("AGENT_NODE_TEST_DATABASE_URL") if databaseURL == "" { t.Skip("AGENT_NODE_TEST_DATABASE_URL is not set") } ctx := context.Background() pool, err := database.Open(ctx, config.Database{URL: databaseURL, MaxConns: 4, MinConns: 0}) if err != nil { t.Fatal(err) } defer pool.Close() _, _ = pool.Exec(ctx, `DELETE FROM gateway.agent_nodes WHERE code='node-integration'`) defer pool.Exec(ctx, `DELETE FROM gateway.agent_nodes WHERE code='node-integration'`) store := NewStore(pool) node, token, err := store.Create(ctx, CreateInput{Code: "node-integration", Name: "Integration Node", PoolType: "private", PoolCode: "test", Enabled: true}, "") if err != nil { t.Fatal(err) } if token == "" || node.Status != "pending" || node.TokenPrefix == "" { t.Fatalf("created node=%+v token=%q", node, token) } updated, err := store.Update(ctx, UpdateInput{ID: node.ID, Name: "Integration Node v2", Description: "test", Endpoint: "https://node.invalid", NodeType: "executor", PoolType: "public", PoolCode: "shared", Enabled: true}) if err != nil || updated.Name != "Integration Node v2" || updated.PoolType != "public" { t.Fatalf("updated node=%+v err=%v", updated, err) } online, err := store.Heartbeat(ctx, node.Code, token, net.ParseIP("192.0.2.10"), HeartbeatInput{Version: "0.10.0-node", Capabilities: map[string]any{"tool_exec": true}, Metadata: map[string]any{"region": "test"}}) if err != nil || online.Status != "online" || online.Version != "0.10.0-node" || online.LastHeartbeatIP != "192.0.2.10" { t.Fatalf("heartbeat node=%+v err=%v", online, err) } preview, err := store.PreviewRoute(ctx, RoutePreviewInput{PoolType: "public", PoolCode: "shared", RequestKey: "integration-request", RequiredCapabilities: []string{"tool_exec"}}) if err != nil || preview.Reason != "selected_online_node" || preview.Selected == nil || preview.Selected.ID != online.ID || len(preview.Candidates) != 1 { t.Fatalf("route preview=%+v err=%v", preview, err) } rotated, newToken, err := store.RotateToken(ctx, node.ID) if err != nil || newToken == token || rotated.TokenPrefix == node.TokenPrefix { t.Fatalf("rotated node=%+v token=%q err=%v", rotated, newToken, err) } if _, err = store.Heartbeat(ctx, node.Code, token, nil, HeartbeatInput{}); err != ErrInvalidToken { t.Fatalf("old token err=%v", err) } if _, err = store.Heartbeat(ctx, node.Code, newToken, nil, HeartbeatInput{}); err != nil { t.Fatal(err) } items, err := store.List(ctx) if err != nil || len(items) == 0 || items[0].UpdatedAt.Before(time.Now().UTC().Add(-time.Minute)) { t.Fatalf("items=%+v err=%v", items, err) } }