- Consolidate DialRemote/DialTunnel common logic into buildResources + dialEnv interface - Remove strict capability check that prevented ConnResources creation for host-exec-only nodes - Merge gRPC-discovered capabilities with registration-declared capabilities in DialTunnel - Replace requireKubeConfig hard-fail with graceful skip when kubeconfig is absent - Introduce tai/types package for shared Ports/Capabilities/SystemInfo/AuthInfo/NodeMeta - Add tai/conn.go (ConnResources) and tai/dial.go (DialRemote/DialTunnel/DialLocal) - Rename tai/sandbox → tai/runtime for clarity - Update sandbox/v2, workspace, agent/sandbox/v2 test utilities for build-tag isolation Made-with: Cursor
470 lines
12 KiB
Go
470 lines
12 KiB
Go
package jsapi_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
v8runtime "github.com/yaoapp/gou/runtime/v8"
|
|
"github.com/yaoapp/yao/config"
|
|
sandbox "github.com/yaoapp/yao/sandbox/v2"
|
|
"github.com/yaoapp/yao/tai"
|
|
"github.com/yaoapp/yao/tai/registry"
|
|
"github.com/yaoapp/yao/test"
|
|
|
|
_ "github.com/yaoapp/yao/sandbox/v2/jsapi"
|
|
)
|
|
|
|
type testMode struct {
|
|
Name string
|
|
Addr string
|
|
TaiID string
|
|
}
|
|
|
|
func testModes() []testMode {
|
|
modes := []testMode{{Name: "local", Addr: "local"}}
|
|
if addr := os.Getenv("SANDBOX_TEST_REMOTE_ADDR"); addr != "" {
|
|
modes = append(modes, testMode{Name: "remote", Addr: addr})
|
|
}
|
|
return modes
|
|
}
|
|
|
|
func testImage() string {
|
|
if img := os.Getenv("SANDBOX_TEST_IMAGE"); img != "" {
|
|
return img
|
|
}
|
|
return "alpine:latest"
|
|
}
|
|
|
|
func setupSandbox(t *testing.T, m *testMode) {
|
|
t.Helper()
|
|
test.Prepare(t, config.Conf)
|
|
|
|
reg := registry.Global()
|
|
if reg == nil {
|
|
registry.Init(nil)
|
|
reg = registry.Global()
|
|
}
|
|
|
|
taiID, _ := registerForTest(t, m.Addr)
|
|
m.TaiID = taiID
|
|
|
|
sandbox.Init()
|
|
mgr := sandbox.M()
|
|
t.Cleanup(func() { mgr.Close() })
|
|
}
|
|
|
|
func registerForTest(t testing.TB, addr string, dialOps ...tai.DialOption) (string, *tai.ConnResources) {
|
|
t.Helper()
|
|
if registry.Global() == nil {
|
|
registry.Init(nil)
|
|
}
|
|
res, err := dialForTest(addr, dialOps...)
|
|
if err != nil {
|
|
t.Fatalf("dialForTest(%s): %v", addr, err)
|
|
}
|
|
taiID := taiIDFromAddr(addr)
|
|
reg := registry.Global()
|
|
reg.Register(®istry.TaiNode{TaiID: taiID, Mode: modeForAddr(addr)})
|
|
reg.SetResources(taiID, res)
|
|
t.Cleanup(func() { res.Close() })
|
|
return taiID, res
|
|
}
|
|
|
|
func dialForTest(addr string, dialOps ...tai.DialOption) (*tai.ConnResources, error) {
|
|
if addr == "local" || addr == "" {
|
|
return tai.DialLocal("", "", nil)
|
|
}
|
|
host, grpcPort := parseHostPort(addr)
|
|
ports := tai.Ports{GRPC: grpcPort}
|
|
return tai.DialRemote(host, ports, dialOps...)
|
|
}
|
|
|
|
func taiIDFromAddr(addr string) string {
|
|
if addr == "local" || addr == "" {
|
|
return "local"
|
|
}
|
|
addr = strings.TrimPrefix(addr, "tai://")
|
|
parts := strings.SplitN(addr, ":", 2)
|
|
return parts[0]
|
|
}
|
|
|
|
func modeForAddr(addr string) string {
|
|
if addr == "local" || addr == "" {
|
|
return "local"
|
|
}
|
|
return "direct"
|
|
}
|
|
|
|
func parseHostPort(addr string) (string, int) {
|
|
addr = strings.TrimPrefix(addr, "tai://")
|
|
parts := strings.SplitN(addr, ":", 2)
|
|
h := parts[0]
|
|
if len(parts) == 2 {
|
|
if p, err := strconv.Atoi(parts[1]); err == nil {
|
|
return h, p
|
|
}
|
|
}
|
|
return h, 19100
|
|
}
|
|
|
|
func runJS(t *testing.T, source string) interface{} {
|
|
t.Helper()
|
|
res, err := v8runtime.Call(v8runtime.CallOptions{
|
|
Sid: "test",
|
|
Timeout: 60 * time.Second,
|
|
}, source)
|
|
if err != nil {
|
|
t.Fatalf("JS error: %v", err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func runJSExpectError(t *testing.T, source string) string {
|
|
t.Helper()
|
|
_, err := v8runtime.Call(v8runtime.CallOptions{
|
|
Sid: "test",
|
|
Timeout: 30 * time.Second,
|
|
}, source)
|
|
if err == nil {
|
|
t.Fatal("expected JS error, got nil")
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
func skipIfNoDocker(t *testing.T) {
|
|
t.Helper()
|
|
addr := os.Getenv("SANDBOX_TEST_LOCAL_ADDR")
|
|
if addr == "" {
|
|
addr = "local"
|
|
}
|
|
_ = addr
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sandbox.Create / sandbox.Get / sandbox.Delete
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCreate(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestCreate() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
if (pc.kind !== "box") throw new Error("kind=" + pc.kind);
|
|
if (!pc.id) throw new Error("no id");
|
|
var id = pc.id;
|
|
sandbox.Delete(id);
|
|
return id;
|
|
}`, img, m.TaiID))
|
|
if res == nil || res == "" {
|
|
t.Error("expected box id")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGet(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestGet() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var id = pc.id;
|
|
var got = sandbox.Get(id);
|
|
if (!got) throw new Error("Get returned null");
|
|
if (got.kind !== "box") throw new Error("kind=" + got.kind);
|
|
sandbox.Delete(id);
|
|
return id;
|
|
}`, img, m.TaiID))
|
|
if res == nil || res == "" {
|
|
t.Error("expected box id")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNotFound(t *testing.T) {
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
res := runJS(t, `function TestGetNotFound() {
|
|
var got = sandbox.Get("sb-nonexistent-id");
|
|
return got === null ? "null" : "found";
|
|
}`)
|
|
if res != "null" {
|
|
t.Errorf("expected null, got %v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestDelete() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var id = pc.id;
|
|
sandbox.Delete(id);
|
|
var got = sandbox.Get(id);
|
|
return got === null ? "deleted" : "still exists";
|
|
}`, img, m.TaiID))
|
|
if res != "deleted" {
|
|
t.Errorf("expected deleted, got %v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sandbox.List
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestList(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestList() {
|
|
var a = sandbox.Create({ image: "%s", owner: "list-user", node_id: "%s" });
|
|
var b = sandbox.Create({ image: "%s", owner: "list-user", node_id: "%s" });
|
|
var list = sandbox.List({ owner: "list-user" });
|
|
var count = list.length;
|
|
sandbox.Delete(a.id);
|
|
sandbox.Delete(b.id);
|
|
return count;
|
|
}`, img, m.TaiID, img, m.TaiID))
|
|
n := toInt(res)
|
|
if n < 2 {
|
|
t.Errorf("expected >= 2, got %d", n)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.Exec
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestExec(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestExec() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var r = pc.Exec(["echo", "hello-jsapi"]);
|
|
sandbox.Delete(pc.id);
|
|
return r.stdout;
|
|
}`, img, m.TaiID))
|
|
s := fmt.Sprintf("%v", res)
|
|
if !strings.Contains(s, "hello-jsapi") {
|
|
t.Errorf("stdout = %q, want contain 'hello-jsapi'", s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecWithOptions(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestExecWithOptions() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var r = pc.Exec(["pwd"], { workdir: "/tmp" });
|
|
sandbox.Delete(pc.id);
|
|
return r.stdout;
|
|
}`, img, m.TaiID))
|
|
s := fmt.Sprintf("%v", res)
|
|
if !strings.Contains(s, "/tmp") {
|
|
t.Errorf("stdout = %q, want contain '/tmp'", s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.Stream
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestStream(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestStream() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var chunks = [];
|
|
var exitCode = -1;
|
|
pc.Stream(["echo", "streaming"], function(type, data) {
|
|
if (type === "stdout") chunks.push(data);
|
|
if (type === "exit") exitCode = data;
|
|
});
|
|
sandbox.Delete(pc.id);
|
|
return chunks.join("").trim() + "|" + exitCode;
|
|
}`, img, m.TaiID))
|
|
s := fmt.Sprintf("%v", res)
|
|
if !strings.Contains(s, "streaming|0") {
|
|
t.Errorf("result = %q, want contain 'streaming|0'", s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.ComputerInfo
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestComputerInfo(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestComputerInfo() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var info = pc.ComputerInfo();
|
|
sandbox.Delete(pc.id);
|
|
return info.kind;
|
|
}`, img, m.TaiID))
|
|
if res != "box" {
|
|
t.Errorf("kind = %q, want 'box'", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.Info (box-only)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestBoxInfo(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestBoxInfo() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var info = pc.Info();
|
|
sandbox.Delete(pc.id);
|
|
return info.id ? "ok" : "no-id";
|
|
}`, img, m.TaiID))
|
|
if res != "ok" {
|
|
t.Errorf("expected ok, got %v", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Box-only method on host → error
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHostBoxMethodsThrow(t *testing.T) {
|
|
if os.Getenv("SANDBOX_TEST_REMOTE_ADDR") == "" {
|
|
t.Skip("no remote host configured")
|
|
}
|
|
for _, m := range testModes() {
|
|
if m.Name == "local" {
|
|
continue
|
|
}
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
errMsg := runJSExpectError(t, fmt.Sprintf(`function TestHostBoxMethodsThrow() {
|
|
var host = sandbox.Host("%s");
|
|
host.Info();
|
|
}`, m.TaiID))
|
|
if !strings.Contains(errMsg, "not supported") {
|
|
t.Errorf("expected 'not supported' error, got: %s", errMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computer.kind property
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestComputerKind(t *testing.T) {
|
|
skipIfNoDocker(t)
|
|
for _, m := range testModes() {
|
|
t.Run(m.Name, func(t *testing.T) {
|
|
setupSandbox(t, &m)
|
|
img := testImage()
|
|
res := runJS(t, fmt.Sprintf(`function TestComputerKind() {
|
|
var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
|
|
var k = pc.kind;
|
|
sandbox.Delete(pc.id);
|
|
return k;
|
|
}`, img, m.TaiID))
|
|
if res != "box" {
|
|
t.Errorf("kind = %q, want 'box'", res)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sandbox.Nodes (requires registry)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestNodes(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
registry.Init(nil)
|
|
res := runJS(t, `function TestNodes() {
|
|
var nodes = sandbox.Nodes();
|
|
return Array.isArray(nodes) ? "array" : typeof nodes;
|
|
}`)
|
|
if res != "array" {
|
|
t.Errorf("expected array, got %v", res)
|
|
}
|
|
}
|
|
|
|
func TestGetNodeNotFound(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
registry.Init(nil)
|
|
res := runJS(t, `function TestGetNodeNotFound() {
|
|
var node = sandbox.GetNode("tai-nonexistent");
|
|
return node === null ? "null" : "found";
|
|
}`)
|
|
if res != "null" {
|
|
t.Errorf("expected null, got %v", res)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func toInt(v interface{}) int {
|
|
switch n := v.(type) {
|
|
case int:
|
|
return n
|
|
case int32:
|
|
return int(n)
|
|
case int64:
|
|
return int(n)
|
|
case float64:
|
|
return int(n)
|
|
case float32:
|
|
return int(n)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|