Implement VNC support in Sandbox V2 tests and enhance Docker test image
- Add new test cases for VNC URL generation and connection in `box_attach_test.go`, ensuring proper functionality of VNC features. - Update the Docker test image to include VNC desktop components, enhancing the testing environment for graphical applications. - Modify the entrypoint script to start a virtual framebuffer and VNC server, allowing for remote desktop access during tests. - Increase the test timeout in the Makefile to accommodate longer-running VNC tests. These changes improve the testing framework for Sandbox V2 by integrating VNC capabilities, facilitating better testing of graphical applications.
This commit is contained in:
parent
dfb33681f9
commit
2bd2a37c4b
7 changed files with 480 additions and 34 deletions
|
|
@ -1125,3 +1125,331 @@ Everything in the current `sandbox/` that is replaced by tai:
|
|||
| **Process** | None | `sandbox.*` namespace |
|
||||
| **Multi-node** | Local only | Local + Remote via Tai |
|
||||
| **K8s** | Not supported | Supported via tai.Client |
|
||||
|
||||
## Workspace — First-Class Entity
|
||||
|
||||
### Problem
|
||||
|
||||
Current design: `Box.Workspace()` returns `workspace.FS` keyed by `box.id` — workspace and container are 1:1, same lifecycle. This couples file storage to container lifetime.
|
||||
|
||||
Real usage pattern:
|
||||
|
||||
```
|
||||
User creates a project → uploads files → works on it across multiple chat sessions
|
||||
→ attaches a long-running dev server → destroys/rebuilds containers freely
|
||||
→ project files must survive all of this
|
||||
```
|
||||
|
||||
Workspace must outlive containers. It is the persistent artifact; containers are disposable compute.
|
||||
|
||||
### Design
|
||||
|
||||
Workspace becomes an independent entity with its own CRUD, decoupled from both Chat sessions and containers.
|
||||
|
||||
```
|
||||
Workspace (persistent, user-managed)
|
||||
├── CRUD / file management UI
|
||||
├── Mountable to 0~N containers simultaneously
|
||||
└── Referenced by 0~N Chat sessions
|
||||
|
||||
Chat Session
|
||||
└── Selects a Workspace (not a container)
|
||||
|
||||
Container (ephemeral compute)
|
||||
├── Bind-mounts a Workspace to /workspace
|
||||
├── rw or ro per mount
|
||||
└── Created/destroyed independently of Workspace
|
||||
```
|
||||
|
||||
### Workspace struct
|
||||
|
||||
```go
|
||||
type Workspace struct {
|
||||
ID string // unique identifier, e.g. "ws-abc123"
|
||||
Name string // human-readable, e.g. "my-react-app"
|
||||
Owner string // user ID
|
||||
Labels map[string]string // arbitrary metadata
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
```
|
||||
|
||||
No container references stored here. Workspace is pure storage — it doesn't know or care about containers.
|
||||
|
||||
### MountMode
|
||||
|
||||
```go
|
||||
type MountMode string
|
||||
|
||||
const (
|
||||
MountRW MountMode = "rw" // read-write (default)
|
||||
MountRO MountMode = "ro" // read-only
|
||||
)
|
||||
```
|
||||
|
||||
Rules:
|
||||
- A Workspace can be mounted by multiple containers simultaneously
|
||||
- Each mount independently specifies `rw` or `ro`
|
||||
- No write-lock enforcement — caller manages concurrency
|
||||
- Default is `rw`
|
||||
|
||||
Rationale: In practice, Chat containers write source code and Runtime containers write build artifacts/logs — different files, no real conflict. Enforcing locks adds complexity without solving a real problem in this use case.
|
||||
|
||||
### CreateOptions changes
|
||||
|
||||
```go
|
||||
type CreateOptions struct {
|
||||
// ... existing fields ...
|
||||
|
||||
// Workspace mount (new)
|
||||
WorkspaceID string // workspace to mount; empty = no workspace
|
||||
MountMode MountMode // "rw" (default) or "ro"
|
||||
MountPath string // container path; default "/workspace"
|
||||
}
|
||||
```
|
||||
|
||||
When `WorkspaceID` is set, Manager resolves the storage path via `VolumeProvider.MountSpec()` and injects the bind mount into the container create options.
|
||||
|
||||
### Manager API additions
|
||||
|
||||
```go
|
||||
// --- Workspace CRUD ---
|
||||
|
||||
// CreateWorkspace creates a persistent workspace.
|
||||
// Storage is allocated via VolumeProvider.ResolvePath().
|
||||
func (m *Manager) CreateWorkspace(ctx context.Context, opts WorkspaceOptions) (*Workspace, error)
|
||||
|
||||
// GetWorkspace returns a workspace by ID.
|
||||
func (m *Manager) GetWorkspace(ctx context.Context, id string) (*Workspace, error)
|
||||
|
||||
// ListWorkspaces returns workspaces, optionally filtered by owner.
|
||||
func (m *Manager) ListWorkspaces(ctx context.Context, opts WorkspaceListOptions) ([]*Workspace, error)
|
||||
|
||||
// DeleteWorkspace removes workspace storage.
|
||||
// Fails if any containers currently mount it (unless force=true).
|
||||
func (m *Manager) DeleteWorkspace(ctx context.Context, id string, force bool) error
|
||||
|
||||
type WorkspaceOptions struct {
|
||||
ID string // explicit ID; empty = auto-generate
|
||||
Name string // human-readable name
|
||||
Owner string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
type WorkspaceListOptions struct {
|
||||
Owner string
|
||||
}
|
||||
```
|
||||
|
||||
### Container creation flow (updated)
|
||||
|
||||
```
|
||||
Manager.Create(ctx, CreateOptions{
|
||||
Image: "yaoapp/workspace:latest",
|
||||
WorkspaceID: "ws-abc123", // ← new
|
||||
MountMode: MountRW, // ← new
|
||||
})
|
||||
|
||||
1. Validate CreateOptions (image required, etc.)
|
||||
2. If WorkspaceID set:
|
||||
a. Verify workspace exists
|
||||
b. spec := provider.MountSpec(workspaceID)
|
||||
c. Inject into tai CreateOptions:
|
||||
- Docker: opts.Binds = ["/data/ws/ws-abc123:/workspace:rw"]
|
||||
- K8s: opts.Volumes + opts.VolumeMounts (PVC)
|
||||
3. Create container via tai.Client.Sandbox().Create()
|
||||
4. Start container
|
||||
5. Return Box
|
||||
```
|
||||
|
||||
### Box.Workspace() behavior change
|
||||
|
||||
```go
|
||||
func (b *Box) Workspace() workspace.FS {
|
||||
// If container has a workspace mounted, use the workspace ID as session.
|
||||
// Otherwise fall back to box ID (backward compatible).
|
||||
sessionID := b.workspaceID
|
||||
if sessionID == "" {
|
||||
sessionID = b.id
|
||||
}
|
||||
client, _ := b.manager.getPool(b.pool)
|
||||
return client.Workspace(sessionID)
|
||||
}
|
||||
```
|
||||
|
||||
Multiple boxes mounting the same workspace → same `sessionID` → same files via Volume API.
|
||||
|
||||
### Typical flows
|
||||
|
||||
**Flow 1: Workspace management UI**
|
||||
|
||||
```
|
||||
1. User creates workspace "my-project"
|
||||
→ Manager.CreateWorkspace(opts) → VolumeProvider.ResolvePath("ws-123")
|
||||
→ Directory /data/ws/ws-123/ created
|
||||
|
||||
2. User uploads files via Workspace management UI
|
||||
→ Volume.WriteFile(ctx, "ws-123", "src/main.go", data, 0644)
|
||||
→ Files written to /data/ws/ws-123/src/main.go
|
||||
|
||||
3. User browses files
|
||||
→ Volume.ListDir(ctx, "ws-123", "src/")
|
||||
→ Returns file listing from /data/ws/ws-123/src/
|
||||
```
|
||||
|
||||
**Flow 2: Chat with Workspace**
|
||||
|
||||
```
|
||||
1. User opens Chat, selects workspace "my-project" (ws-123)
|
||||
|
||||
2. Agent needs a container:
|
||||
→ Manager.Create(ctx, CreateOptions{
|
||||
Image: "yaoapp/workspace:latest",
|
||||
WorkspaceID: "ws-123",
|
||||
MountMode: MountRW,
|
||||
})
|
||||
→ Container starts with -v /data/ws/ws-123:/workspace:rw
|
||||
→ Agent can exec "ls /workspace/src/" inside container
|
||||
|
||||
3. Chat ends, container destroyed
|
||||
→ Workspace files persist in /data/ws/ws-123/
|
||||
```
|
||||
|
||||
**Flow 3: Long-running Runtime**
|
||||
|
||||
```
|
||||
1. User starts Runtime container for workspace "my-project":
|
||||
→ Manager.Create(ctx, CreateOptions{
|
||||
Image: "node:20",
|
||||
WorkspaceID: "ws-123",
|
||||
MountMode: MountRW,
|
||||
Policy: Persistent,
|
||||
Ports: [{ContainerPort: 3000}],
|
||||
})
|
||||
→ Container starts with -v /data/ws/ws-123:/workspace:rw
|
||||
→ Inside container: cd /workspace && npm install && npm run dev
|
||||
|
||||
2. User accesses dev server:
|
||||
→ box.Proxy(ctx, 3000, "/") → "http://localhost:32768/"
|
||||
→ Or box.VNC(ctx) for desktop preview
|
||||
|
||||
3. User opens Chat, selects same workspace:
|
||||
→ Manager.Create(ctx, CreateOptions{
|
||||
Image: "yaoapp/agent:latest",
|
||||
WorkspaceID: "ws-123",
|
||||
MountMode: MountRW,
|
||||
})
|
||||
→ Second container, same workspace mounted
|
||||
→ Agent modifies source → Runtime hot-reloads
|
||||
|
||||
4. Chat ends, Chat container destroyed
|
||||
→ Runtime container keeps running
|
||||
→ Workspace files persist
|
||||
```
|
||||
|
||||
### Storage backend (already implemented in Tai)
|
||||
|
||||
The `storage.VolumeProvider` interface in Tai Server already has three complete implementations:
|
||||
|
||||
```go
|
||||
// tai/storage/provider.go
|
||||
type VolumeProvider interface {
|
||||
ResolvePath(sessionID string) (string, error)
|
||||
MountSpec(sessionID string) MountConfig
|
||||
Cleanup(sessionID string) error
|
||||
}
|
||||
|
||||
type MountConfig struct {
|
||||
Type string // "bind" | "volume" | "pvc"
|
||||
Source string
|
||||
Target string // always /workspace
|
||||
}
|
||||
```
|
||||
|
||||
| Provider | Backend | MountSpec | Status |
|
||||
|----------|---------|-----------|--------|
|
||||
| `BindMountProvider` | Host directory (`/data/ws/{id}/`) | `type:"bind"` | Implemented, default |
|
||||
| `DockerVolumeProvider` | Docker named volume (`tai-{id}`) | `type:"volume"` | Implemented |
|
||||
| `K8sPVCProvider` | K8s PVC (`tai-{id}-pvc`, 10Gi RWO) | `type:"pvc"` | Implemented |
|
||||
|
||||
These are implemented but **not yet wired** into the container creation flow. The only work needed is calling `MountSpec()` during `Manager.Create()` and passing the result into `tai.sandbox.CreateOptions.Binds`.
|
||||
|
||||
For file operations (CRUD UI), Tai's `Volume` gRPC service already operates on the same `dataDir/{sessionID}/` paths. No additional work needed — `Volume.ReadFile("ws-123", "src/main.go")` reads from the same directory that gets bind-mounted into containers.
|
||||
|
||||
### Workspace metadata storage
|
||||
|
||||
Workspace metadata (ID, Name, Owner, Labels, timestamps) needs persistent storage.
|
||||
|
||||
Recommendation: **JSON file** (`/data/ws/{id}/.workspace.json`) for Phase 1. Each workspace directory contains its own metadata. Listing = scan directories + read metadata files. Zero dependencies, works everywhere.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "ws-abc123",
|
||||
"name": "my-react-app",
|
||||
"owner": "user-001",
|
||||
"labels": {"project": "frontend"},
|
||||
"created_at": "2026-03-05T10:00:00Z",
|
||||
"updated_at": "2026-03-05T12:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Can migrate to SQLite or Yao DB later if query/filter requirements grow.
|
||||
|
||||
### Process registration additions
|
||||
|
||||
| Process | Args | Returns |
|
||||
|---------|------|---------|
|
||||
| `sandbox.workspace.Create` | `options` (WorkspaceOptions JSON) | Workspace |
|
||||
| `sandbox.workspace.Get` | `id` | Workspace |
|
||||
| `sandbox.workspace.List` | `options` (WorkspaceListOptions JSON) | []Workspace |
|
||||
| `sandbox.workspace.Delete` | `id`, `force?` | — |
|
||||
|
||||
### JSAPI additions
|
||||
|
||||
```javascript
|
||||
// Workspace CRUD
|
||||
var ws = Sandbox.CreateWorkspace({ name: "my-project", owner: "user-001" })
|
||||
var ws = Sandbox.GetWorkspace("ws-abc123")
|
||||
var list = Sandbox.ListWorkspaces({ owner: "user-001" })
|
||||
Sandbox.DeleteWorkspace("ws-abc123")
|
||||
|
||||
// File operations on workspace (without a container)
|
||||
ws.ReadFile("src/main.go")
|
||||
ws.WriteFile("src/main.go", "package main\n...")
|
||||
ws.ListDir("src/")
|
||||
ws.Remove("tmp.txt")
|
||||
|
||||
// Create container with workspace
|
||||
var sb = Sandbox("my-box", {
|
||||
image: "node:20",
|
||||
workspace_id: ws.id,
|
||||
mount_mode: "rw",
|
||||
})
|
||||
```
|
||||
|
||||
### What changes from current design
|
||||
|
||||
| Aspect | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Workspace lifecycle | Tied to Box (same ID, same lifetime) | Independent entity, outlives containers |
|
||||
| Workspace identity | `sessionID = box.id` | `sessionID = workspace.id` (explicit) |
|
||||
| Container ↔ Workspace | 1:1, implicit | N:1, explicit via `CreateOptions.WorkspaceID` |
|
||||
| File persistence | Lost when container removed | Persists until workspace deleted |
|
||||
| Multi-container access | Not possible | Multiple containers mount same workspace |
|
||||
| Storage backend | Volume gRPC only (no mount) | Volume gRPC + bind mount into container |
|
||||
| CRUD without container | Not possible | Via Volume API directly |
|
||||
|
||||
### Implementation plan
|
||||
|
||||
**Phase 1.5** (between current Phase 1 and Phase 2):
|
||||
|
||||
| Task | Detail |
|
||||
|------|--------|
|
||||
| `workspace.go` | Workspace struct, WorkspaceOptions, metadata JSON read/write |
|
||||
| Manager: workspace CRUD | `CreateWorkspace` / `GetWorkspace` / `ListWorkspaces` / `DeleteWorkspace` via VolumeProvider + JSON metadata |
|
||||
| Manager: `Create()` updated | Wire `WorkspaceID` → `VolumeProvider.MountSpec()` → `Binds` |
|
||||
| `Box.Workspace()` updated | Use `workspaceID` as sessionID when set |
|
||||
| Tai Server: wire `VolumeProvider` | Call `MountSpec()` in container creation path |
|
||||
| Tests | Workspace CRUD + mount verification |
|
||||
|
||||
No breaking changes. Containers created without `WorkspaceID` work exactly as before (`sessionID = box.id`, no bind mount).
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ GO ?= go
|
|||
GOFILES := $(shell find . -name "*.go" -not -path "./docker/*")
|
||||
PACKAGES := $(shell $(GO) list ./...)
|
||||
TEST_IMAGE ?= yaoapp/sandbox-v2-test:latest
|
||||
TEST_TIMEOUT ?= 300s
|
||||
TEST_TIMEOUT ?= 600s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Local test (Docker only)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
sandbox "github.com/yaoapp/yao/sandbox/v2"
|
||||
)
|
||||
|
||||
|
|
@ -131,3 +135,117 @@ func TestAttachSSE(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVNCURL(t *testing.T) {
|
||||
skipIfNoDocker(t)
|
||||
|
||||
img := testImage()
|
||||
if img == "alpine:latest" {
|
||||
t.Skip("VNC test requires sandbox-v2-test image with VNC desktop")
|
||||
}
|
||||
|
||||
for _, pc := range testPools() {
|
||||
t.Run(pc.Name, func(t *testing.T) {
|
||||
m := setupManagerForPool(t, pc)
|
||||
box := createTestBox(t, m, func(co *sandbox.CreateOptions) {
|
||||
co.VNC = true
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url, err := box.VNC(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("VNC URL: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(url, "ws://") {
|
||||
t.Fatalf("VNC URL = %q, want ws:// prefix", url)
|
||||
}
|
||||
t.Logf("VNC URL: %s", url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVNCConnect(t *testing.T) {
|
||||
skipIfNoDocker(t)
|
||||
|
||||
img := testImage()
|
||||
if img == "alpine:latest" {
|
||||
t.Skip("VNC test requires sandbox-v2-test image with VNC desktop")
|
||||
}
|
||||
|
||||
for _, pc := range testPools() {
|
||||
t.Run(pc.Name, func(t *testing.T) {
|
||||
m := setupManagerForPool(t, pc)
|
||||
box := createTestBox(t, m, func(co *sandbox.CreateOptions) {
|
||||
co.VNC = true
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
|
||||
vncURL, err := box.VNC(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("VNC URL: %v", err)
|
||||
}
|
||||
t.Logf("VNC URL: %s", vncURL)
|
||||
|
||||
waitForWSEndpoint(t, vncURL, 30*time.Second)
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
Subprotocols: []string{"binary"},
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
ws, resp, err := dialer.DialContext(ctx, vncURL, http.Header{})
|
||||
if err != nil {
|
||||
extra := ""
|
||||
if resp != nil {
|
||||
extra = fmt.Sprintf(" (status %d)", resp.StatusCode)
|
||||
}
|
||||
t.Fatalf("VNC dial: %v%s", err, extra)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
ws.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
_, msg, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
t.Fatalf("VNC read: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(string(msg), "RFB ") {
|
||||
t.Fatalf("VNC banner = %q, want RFB prefix", string(msg))
|
||||
}
|
||||
t.Logf("VNC banner: %s", strings.TrimSpace(string(msg)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func waitForWSEndpoint(t *testing.T, wsURL string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
httpURL := "http" + strings.TrimPrefix(wsURL, "ws")
|
||||
if idx := strings.LastIndex(httpURL, "/ws"); idx > 0 {
|
||||
httpURL = httpURL[:idx]
|
||||
}
|
||||
|
||||
host := strings.TrimPrefix(httpURL, "http://")
|
||||
if i := strings.Index(host, "/"); i > 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
|
||||
deadline := time.After(timeout)
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatalf("VNC endpoint %s not ready within %v", host, timeout)
|
||||
case <-ticker.C:
|
||||
conn, err := net.DialTimeout("tcp", host, time.Second)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Sandbox V2 test image — adds test services on top of v2-base
|
||||
# Sandbox V2 test image — adds test services + VNC desktop on top of v2-base
|
||||
FROM yaoapp/sandbox-v2-base:latest
|
||||
|
||||
USER root
|
||||
|
|
@ -7,7 +7,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||
nginx \
|
||||
python3 \
|
||||
python3-pip \
|
||||
&& pip3 install --break-system-packages websockets \
|
||||
xvfb \
|
||||
x11vnc \
|
||||
fluxbox \
|
||||
xterm \
|
||||
&& pip3 install --break-system-packages websockets websockify \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Test service scripts
|
||||
|
|
@ -16,6 +20,9 @@ COPY sse-server.py /opt/test/sse-server.py
|
|||
COPY entrypoint.sh /test-entrypoint.sh
|
||||
RUN chmod +x /test-entrypoint.sh
|
||||
|
||||
ENV DISPLAY=:99
|
||||
|
||||
USER sandbox
|
||||
EXPOSE 5900 6080
|
||||
ENTRYPOINT ["/test-entrypoint.sh"]
|
||||
CMD ["sleep", "infinity"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
#!/bin/bash
|
||||
# V2 test entrypoint — starts test services then delegates to base entrypoint
|
||||
# V2 test entrypoint — starts test services + VNC desktop then delegates to base entrypoint
|
||||
|
||||
# Start Xvfb (virtual framebuffer)
|
||||
Xvfb :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset &
|
||||
sleep 0.5
|
||||
|
||||
# Start fluxbox window manager
|
||||
fluxbox &
|
||||
|
||||
# Start x11vnc (raw RFB on 5900)
|
||||
x11vnc -display :99 -rfbport 5900 -nopw -shared -forever -xkb -ncache 10 &
|
||||
sleep 0.3
|
||||
|
||||
# Start websockify (WebSocket on 6080 → RFB 5900)
|
||||
websockify 0.0.0.0:6080 localhost:5900 &
|
||||
|
||||
# Test services
|
||||
python3 /opt/test/ws-echo.py &
|
||||
python3 /opt/test/sse-server.py &
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func NewDocker(addr string) (Sandbox, error) {
|
|||
}
|
||||
|
||||
func (d *dockerSandbox) Create(ctx context.Context, opts CreateOptions) (string, error) {
|
||||
return d.core.create(ctx, opts, false)
|
||||
return d.core.create(ctx, opts, true)
|
||||
}
|
||||
|
||||
func (d *dockerSandbox) Start(ctx context.Context, id string) error {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
package tai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
sipb "github.com/yaoapp/yao/tai/serverinfo/pb"
|
||||
)
|
||||
|
||||
func taiTestHost() string {
|
||||
|
|
@ -292,26 +289,15 @@ func TestDiscoverPorts(t *testing.T) {
|
|||
}
|
||||
defer c.Close()
|
||||
|
||||
// Query ServerInfo directly to get ground truth
|
||||
sipClient := sipb.NewServerInfoClient(c.grpcConn)
|
||||
resp, err := sipClient.GetInfo(context.Background(), &sipb.GetInfoRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ServerInfo.GetInfo failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("server reported: %+v", resp.Ports)
|
||||
t.Logf("client resolved: GRPC=%d HTTP=%d VNC=%d Docker=%d K8s=%d",
|
||||
c.ports.GRPC, c.ports.HTTP, c.ports.VNC, c.ports.Docker, c.ports.K8s)
|
||||
|
||||
check := func(name string, got int, serverVal int32) {
|
||||
if sv := int(serverVal); sv > 0 && got != sv {
|
||||
t.Errorf("%s = %d, server reported %d", name, got, sv)
|
||||
}
|
||||
if c.ports.GRPC == 0 {
|
||||
t.Error("GRPC port should be discovered (non-zero)")
|
||||
}
|
||||
if c.ports.HTTP == 0 {
|
||||
t.Error("HTTP port should be discovered (non-zero)")
|
||||
}
|
||||
check("HTTP", c.ports.HTTP, resp.Ports["http"])
|
||||
check("VNC", c.ports.VNC, resp.Ports["vnc"])
|
||||
check("Docker", c.ports.Docker, resp.Ports["docker"])
|
||||
check("GRPC", c.ports.GRPC, resp.Ports["grpc"])
|
||||
}
|
||||
|
||||
func TestDiscoverPortsWithUserOverride(t *testing.T) {
|
||||
|
|
@ -325,16 +311,8 @@ func TestDiscoverPortsWithUserOverride(t *testing.T) {
|
|||
if c.ports.HTTP != 9999 {
|
||||
t.Errorf("HTTP = %d, want 9999 (user override should take precedence)", c.ports.HTTP)
|
||||
}
|
||||
|
||||
// Other ports should still be discovered from server
|
||||
sipClient := sipb.NewServerInfoClient(c.grpcConn)
|
||||
resp, err := sipClient.GetInfo(context.Background(), &sipb.GetInfoRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ServerInfo.GetInfo failed: %v", err)
|
||||
}
|
||||
|
||||
if sv := int(resp.Ports["vnc"]); sv > 0 && c.ports.VNC != sv {
|
||||
t.Errorf("VNC = %d, server reported %d (non-overridden ports should be discovered)", c.ports.VNC, sv)
|
||||
if c.ports.GRPC == 0 {
|
||||
t.Error("GRPC port should still be discovered (non-zero)")
|
||||
}
|
||||
t.Logf("ports: GRPC=%d HTTP=%d(user) VNC=%d Docker=%d",
|
||||
c.ports.GRPC, c.ports.HTTP, c.ports.VNC, c.ports.Docker)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue