github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/integration_test.go (about)

     1  //go:build integration
     2  
     3  package sandbox
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  )
    16  
    17  // testClient 从环境变量创建集成测试用的客户端。
    18  func testClient(t *testing.T) *Client {
    19  	t.Helper()
    20  
    21  	apiKey := os.Getenv("QINIU_API_KEY")
    22  	apiURL := os.Getenv("QINIU_SANDBOX_API_URL")
    23  	if apiKey == "" {
    24  		t.Fatal("需要设置 QINIU_API_KEY 环境变量")
    25  	}
    26  
    27  	cfg := &Config{
    28  		APIKey:   apiKey,
    29  		Endpoint: apiURL,
    30  	}
    31  	if v := strings.TrimSpace(os.Getenv("SANDBOX_RETRY_MAX")); v != "" {
    32  		n, err := strconv.Atoi(v)
    33  		if err != nil || n < 0 {
    34  			t.Fatalf("SANDBOX_RETRY_MAX 必须是有效的非负整数,当前值:%q", v)
    35  		}
    36  		cfg.RetryMax = &n
    37  	}
    38  	c, err := NewClient(cfg)
    39  	if err != nil {
    40  		t.Fatalf("创建客户端失败: %v", err)
    41  	}
    42  	return c
    43  }
    44  
    45  func TestIntegrationListTemplates(t *testing.T) {
    46  	c := testClient(t)
    47  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    48  	defer cancel()
    49  
    50  	templates, err := c.ListTemplates(ctx, nil)
    51  	if err != nil {
    52  		t.Fatalf("ListTemplates 失败: %v", err)
    53  	}
    54  	t.Logf("共 %d 个模板", len(templates))
    55  	for _, tmpl := range templates {
    56  		t.Logf("  - %s (buildStatus=%s)", tmpl.TemplateID, tmpl.BuildStatus)
    57  	}
    58  }
    59  
    60  func TestIntegrationListDefaultTemplates(t *testing.T) {
    61  	c := testClient(t)
    62  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    63  	defer cancel()
    64  
    65  	templates, err := c.ListDefaultTemplates(ctx)
    66  	if err != nil {
    67  		t.Fatalf("ListDefaultTemplates 失败: %v", err)
    68  	}
    69  	t.Logf("共 %d 个默认模板", len(templates))
    70  	for _, tmpl := range templates {
    71  		t.Logf("  - %s (buildStatus=%s)", tmpl.TemplateID, tmpl.BuildStatus)
    72  	}
    73  }
    74  
    75  func TestIntegrationListSandboxes(t *testing.T) {
    76  	c := testClient(t)
    77  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    78  	defer cancel()
    79  
    80  	sandboxes, err := c.List(ctx, nil)
    81  	if err != nil {
    82  		t.Fatalf("List 失败: %v", err)
    83  	}
    84  	t.Logf("共 %d 个沙箱", len(sandboxes))
    85  	for _, sb := range sandboxes {
    86  		t.Logf("  - %s (template=%s)", sb.SandboxID, sb.TemplateID)
    87  	}
    88  }
    89  
    90  func TestIntegrationSandboxLifecycle(t *testing.T) {
    91  	c := testClient(t)
    92  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
    93  	defer cancel()
    94  
    95  	// 1. 获取可用模板
    96  	templates, err := c.ListTemplates(ctx, nil)
    97  	if err != nil {
    98  		t.Fatalf("ListTemplates 失败: %v", err)
    99  	}
   100  
   101  	var templateID string
   102  	for _, tmpl := range templates {
   103  		if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded {
   104  			templateID = tmpl.TemplateID
   105  			break
   106  		}
   107  	}
   108  	if templateID == "" {
   109  		t.Skip("没有可用模板,跳过生命周期测试")
   110  	}
   111  	t.Logf("使用模板: %s", templateID)
   112  
   113  	// 2. 创建沙箱并等待就绪
   114  	timeout := int32(60)
   115  	sb, info, err := c.CreateAndWait(ctx, CreateParams{
   116  		TemplateID: templateID,
   117  		Timeout:    &timeout,
   118  	}, WithPollInterval(2*time.Second))
   119  	if err != nil {
   120  		t.Fatalf("CreateAndWait 失败: %v", err)
   121  	}
   122  	t.Logf("沙箱已创建: %s (state=%s)", sb.ID(), info.State)
   123  
   124  	// 确保测试结束时清理沙箱
   125  	killed := false
   126  	defer func() {
   127  		if killed {
   128  			return
   129  		}
   130  		killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second)
   131  		defer killCancel()
   132  		if err := sb.Kill(killCtx); err != nil {
   133  			t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err)
   134  		} else {
   135  			t.Logf("沙箱 %s 已清理", sb.ID())
   136  		}
   137  	}()
   138  
   139  	// 3. 检查运行状态
   140  	running, err := sb.IsRunning(ctx)
   141  	if err != nil {
   142  		t.Fatalf("IsRunning 失败: %v", err)
   143  	}
   144  	if !running {
   145  		t.Fatal("沙箱应处于运行状态")
   146  	}
   147  
   148  	// 4. 获取详细信息
   149  	detail, err := sb.GetInfo(ctx)
   150  	if err != nil {
   151  		t.Fatalf("GetInfo 失败: %v", err)
   152  	}
   153  	t.Logf("沙箱详情: state=%s, templateID=%s, cpuCount=%d, memoryMB=%d",
   154  		detail.State, detail.TemplateID, detail.CPUCount, detail.MemoryMB)
   155  
   156  	// 5. 更新超时时间
   157  	if err := sb.SetTimeout(ctx, 120*time.Second); err != nil {
   158  		t.Fatalf("SetTimeout 失败: %v", err)
   159  	}
   160  	t.Log("超时时间已更新为 120s")
   161  
   162  	// 6. 在沙箱列表中确认可见
   163  	sandboxes, err := c.List(ctx, nil)
   164  	if err != nil {
   165  		t.Fatalf("List 失败: %v", err)
   166  	}
   167  	found := false
   168  	for _, s := range sandboxes {
   169  		if s.SandboxID == sb.ID() {
   170  			found = true
   171  			break
   172  		}
   173  	}
   174  	if !found {
   175  		t.Fatal("新创建的沙箱未出现在列表中")
   176  	}
   177  
   178  	// 7. 终止沙箱
   179  	if err := sb.Kill(ctx); err != nil {
   180  		t.Fatalf("Kill 失败: %v", err)
   181  	}
   182  	killed = true
   183  	t.Log("沙箱已终止")
   184  }
   185  
   186  // createTestSandbox 创建一个用于 envd 集成测试的沙箱并等待就绪。
   187  func createTestSandbox(t *testing.T, c *Client, ctx context.Context) *Sandbox {
   188  	t.Helper()
   189  
   190  	templates, err := c.ListTemplates(ctx, nil)
   191  	if err != nil {
   192  		t.Fatalf("ListTemplates 失败: %v", err)
   193  	}
   194  
   195  	var templateID string
   196  	for _, tmpl := range templates {
   197  		if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded {
   198  			templateID = tmpl.TemplateID
   199  			break
   200  		}
   201  	}
   202  	if templateID == "" {
   203  		t.Skip("没有可用模板,跳过测试")
   204  	}
   205  
   206  	timeout := int32(120)
   207  	sb, _, err := c.CreateAndWait(ctx, CreateParams{
   208  		TemplateID: templateID,
   209  		Timeout:    &timeout,
   210  	}, WithPollInterval(2*time.Second))
   211  	if err != nil {
   212  		t.Fatalf("CreateAndWait 失败: %v", err)
   213  	}
   214  
   215  	t.Cleanup(func() {
   216  		killCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   217  		defer cancel()
   218  		if err := sb.Kill(killCtx); err != nil {
   219  			t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err)
   220  		}
   221  	})
   222  
   223  	return sb
   224  }
   225  
   226  func TestIntegrationFilesWriteRead(t *testing.T) {
   227  	c := testClient(t)
   228  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   229  	defer cancel()
   230  
   231  	sb := createTestSandbox(t, c, ctx)
   232  	t.Logf("沙箱: %s", sb.ID())
   233  
   234  	// 写入文件
   235  	content := []byte("hello sandbox\n")
   236  	_, err := sb.Files().Write(ctx, "/tmp/test-file.txt", content)
   237  	if err != nil {
   238  		t.Fatalf("Write 失败: %v", err)
   239  	}
   240  	t.Log("文件写入成功")
   241  
   242  	// 读取文件
   243  	got, err := sb.Files().Read(ctx, "/tmp/test-file.txt")
   244  	if err != nil {
   245  		t.Fatalf("Read 失败: %v", err)
   246  	}
   247  	if string(got) != string(content) {
   248  		t.Fatalf("文件内容不匹配: got %q, want %q", string(got), string(content))
   249  	}
   250  	t.Log("文件读取内容一致")
   251  }
   252  
   253  func TestIntegrationCommandsRun(t *testing.T) {
   254  	c := testClient(t)
   255  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   256  	defer cancel()
   257  
   258  	sb := createTestSandbox(t, c, ctx)
   259  	t.Logf("沙箱: %s", sb.ID())
   260  
   261  	// 执行简单命令
   262  	result, err := sb.Commands().Run(ctx, "echo hello world")
   263  	if err != nil {
   264  		t.Fatalf("Run 失败: %v", err)
   265  	}
   266  	t.Logf("命令结果: exitCode=%d, stdout=%q, stderr=%q", result.ExitCode, result.Stdout, result.Stderr)
   267  	if result.ExitCode != 0 {
   268  		t.Fatalf("命令退出码 %d,期望 0", result.ExitCode)
   269  	}
   270  	if result.Stdout != "hello world\n" {
   271  		t.Fatalf("stdout = %q, want %q", result.Stdout, "hello world\n")
   272  	}
   273  }
   274  
   275  func TestIntegrationGetHost(t *testing.T) {
   276  	c := testClient(t)
   277  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   278  	defer cancel()
   279  
   280  	sb := createTestSandbox(t, c, ctx)
   281  
   282  	host := sb.GetHost(8080)
   283  	if host == "" {
   284  		t.Fatal("GetHost 返回空字符串")
   285  	}
   286  	t.Logf("GetHost(8080) = %s", host)
   287  
   288  	// 验证格式: {port}-{sandboxID}.{domain}
   289  	expected := "8080-" + sb.ID()
   290  	if len(host) < len(expected) || host[:len(expected)] != expected {
   291  		t.Fatalf("GetHost 格式不符: got %q, want prefix %q", host, expected)
   292  	}
   293  }
   294  
   295  func TestIntegrationFilesystemOperations(t *testing.T) {
   296  	c := testClient(t)
   297  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   298  	defer cancel()
   299  
   300  	sb := createTestSandbox(t, c, ctx)
   301  	t.Logf("沙箱: %s", sb.ID())
   302  
   303  	fs := sb.Files()
   304  
   305  	// 创建目录
   306  	dirInfo, err := fs.MakeDir(ctx, "/tmp/test-dir")
   307  	if err != nil {
   308  		t.Fatalf("MakeDir 失败: %v", err)
   309  	}
   310  	t.Logf("创建目录: %s (type=%s)", dirInfo.Path, dirInfo.Type)
   311  
   312  	// 写入文件
   313  	_, err = fs.Write(ctx, "/tmp/test-dir/hello.txt", []byte("hello"))
   314  	if err != nil {
   315  		t.Fatalf("Write 失败: %v", err)
   316  	}
   317  
   318  	// 列目录
   319  	entries, err := fs.List(ctx, "/tmp/test-dir")
   320  	if err != nil {
   321  		t.Fatalf("List 失败: %v", err)
   322  	}
   323  	t.Logf("目录内容: %d 项", len(entries))
   324  
   325  	// 文件存在
   326  	exists, err := fs.Exists(ctx, "/tmp/test-dir/hello.txt")
   327  	if err != nil {
   328  		t.Fatalf("Exists 失败: %v", err)
   329  	}
   330  	if !exists {
   331  		t.Fatal("文件应该存在")
   332  	}
   333  
   334  	// 获取文件信息
   335  	info, err := fs.GetInfo(ctx, "/tmp/test-dir/hello.txt")
   336  	if err != nil {
   337  		t.Fatalf("GetInfo 失败: %v", err)
   338  	}
   339  	t.Logf("文件信息: name=%s, size=%d, type=%s", info.Name, info.Size, info.Type)
   340  
   341  	// 重命名
   342  	newInfo, err := fs.Rename(ctx, "/tmp/test-dir/hello.txt", "/tmp/test-dir/renamed.txt")
   343  	if err != nil {
   344  		t.Fatalf("Rename 失败: %v", err)
   345  	}
   346  	t.Logf("重命名: %s -> %s", "/tmp/test-dir/hello.txt", newInfo.Path)
   347  
   348  	// 删除
   349  	if err := fs.Remove(ctx, "/tmp/test-dir/renamed.txt"); err != nil {
   350  		t.Fatalf("Remove 失败: %v", err)
   351  	}
   352  	t.Log("文件已删除")
   353  
   354  	// 验证文件不存在
   355  	exists, err = fs.Exists(ctx, "/tmp/test-dir/renamed.txt")
   356  	if err != nil {
   357  		t.Fatalf("Exists 失败: %v", err)
   358  	}
   359  	if exists {
   360  		t.Fatal("文件应已不存在")
   361  	}
   362  }
   363  
   364  func TestIntegrationUploadDownload(t *testing.T) {
   365  	c := testClient(t)
   366  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   367  	defer cancel()
   368  
   369  	sb := createTestSandbox(t, c, ctx)
   370  	t.Logf("沙箱: %s", sb.ID())
   371  
   372  	// 通过 Files().Write() 写入文件
   373  	content := []byte("upload test content\n")
   374  	_, err := sb.Files().Write(ctx, "/tmp/uploaded.txt", content)
   375  	if err != nil {
   376  		t.Fatalf("Files().Write 失败: %v", err)
   377  	}
   378  	t.Log("文件写入成功")
   379  
   380  	// 通过 Files().Read() 读取文件
   381  	got, err := sb.Files().Read(ctx, "/tmp/uploaded.txt")
   382  	if err != nil {
   383  		t.Fatalf("Files().Read 失败: %v", err)
   384  	}
   385  	if string(got) != string(content) {
   386  		t.Fatalf("文件内容不匹配: got %q, want %q", string(got), string(content))
   387  	}
   388  	t.Log("文件读取内容一致")
   389  }
   390  
   391  func TestIntegrationWriteFiles(t *testing.T) {
   392  	c := testClient(t)
   393  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   394  	defer cancel()
   395  
   396  	sb := createTestSandbox(t, c, ctx)
   397  	t.Logf("沙箱: %s", sb.ID())
   398  
   399  	files := []WriteEntry{
   400  		{Path: "/tmp/batch-1.txt", Data: []byte("content one")},
   401  		{Path: "/tmp/batch-2.txt", Data: []byte("content two")},
   402  		{Path: "/tmp/batch-3.txt", Data: []byte("content three")},
   403  	}
   404  
   405  	infos, err := sb.Files().WriteFiles(ctx, files)
   406  	if err != nil {
   407  		t.Fatalf("WriteFiles 失败: %v", err)
   408  	}
   409  	if len(infos) != 3 {
   410  		t.Fatalf("WriteFiles 返回 %d 个结果,期望 3", len(infos))
   411  	}
   412  
   413  	// 逐个读回验证内容
   414  	for i, f := range files {
   415  		got, err := sb.Files().Read(ctx, f.Path)
   416  		if err != nil {
   417  			t.Fatalf("Read %s 失败: %v", f.Path, err)
   418  		}
   419  		if string(got) != string(f.Data) {
   420  			t.Fatalf("文件 %s 内容不匹配: got %q, want %q", f.Path, string(got), string(f.Data))
   421  		}
   422  		t.Logf("文件 %d (%s) 验证通过: name=%s, size=%d", i, f.Path, infos[i].Name, infos[i].Size)
   423  	}
   424  }
   425  
   426  func TestIntegrationReadText(t *testing.T) {
   427  	c := testClient(t)
   428  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   429  	defer cancel()
   430  
   431  	sb := createTestSandbox(t, c, ctx)
   432  	t.Logf("沙箱: %s", sb.ID())
   433  
   434  	content := "hello read text\n"
   435  	_, err := sb.Files().Write(ctx, "/tmp/readtext.txt", []byte(content))
   436  	if err != nil {
   437  		t.Fatalf("Write 失败: %v", err)
   438  	}
   439  
   440  	got, err := sb.Files().ReadText(ctx, "/tmp/readtext.txt")
   441  	if err != nil {
   442  		t.Fatalf("ReadText 失败: %v", err)
   443  	}
   444  	if got != content {
   445  		t.Fatalf("ReadText 内容不匹配: got %q, want %q", got, content)
   446  	}
   447  	t.Log("ReadText 验证通过")
   448  }
   449  
   450  func TestIntegrationReadStream(t *testing.T) {
   451  	c := testClient(t)
   452  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   453  	defer cancel()
   454  
   455  	sb := createTestSandbox(t, c, ctx)
   456  	t.Logf("沙箱: %s", sb.ID())
   457  
   458  	content := []byte("hello read stream\n")
   459  	_, err := sb.Files().Write(ctx, "/tmp/readstream.txt", content)
   460  	if err != nil {
   461  		t.Fatalf("Write 失败: %v", err)
   462  	}
   463  
   464  	rc, err := sb.Files().ReadStream(ctx, "/tmp/readstream.txt")
   465  	if err != nil {
   466  		t.Fatalf("ReadStream 失败: %v", err)
   467  	}
   468  	defer rc.Close()
   469  
   470  	got, err := io.ReadAll(rc)
   471  	if err != nil {
   472  		t.Fatalf("读取流失败: %v", err)
   473  	}
   474  	if string(got) != string(content) {
   475  		t.Fatalf("ReadStream 内容不匹配: got %q, want %q", string(got), string(content))
   476  	}
   477  	t.Log("ReadStream 验证通过")
   478  }
   479  
   480  // --- Commands 异步执行与进程管理 ---
   481  
   482  func TestIntegrationCommandsStartWait(t *testing.T) {
   483  	c := testClient(t)
   484  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   485  	defer cancel()
   486  
   487  	sb := createTestSandbox(t, c, ctx)
   488  	t.Logf("沙箱: %s", sb.ID())
   489  
   490  	handle, err := sb.Commands().Start(ctx, "sleep 1 && echo done")
   491  	if err != nil {
   492  		t.Fatalf("Start 失败: %v", err)
   493  	}
   494  
   495  	result, err := handle.Wait()
   496  	if err != nil {
   497  		t.Fatalf("Wait 失败: %v", err)
   498  	}
   499  
   500  	if handle.PID() == 0 {
   501  		t.Fatal("PID 应大于 0")
   502  	}
   503  	if result.ExitCode != 0 {
   504  		t.Fatalf("ExitCode = %d, want 0", result.ExitCode)
   505  	}
   506  	if !strings.Contains(result.Stdout, "done") {
   507  		t.Fatalf("Stdout = %q, 应包含 'done'", result.Stdout)
   508  	}
   509  	t.Logf("Start/Wait 验证通过: PID=%d, ExitCode=%d, Stdout=%q", handle.PID(), result.ExitCode, result.Stdout)
   510  }
   511  
   512  func TestIntegrationCommandsKill(t *testing.T) {
   513  	c := testClient(t)
   514  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   515  	defer cancel()
   516  
   517  	sb := createTestSandbox(t, c, ctx)
   518  	t.Logf("沙箱: %s", sb.ID())
   519  
   520  	handle, err := sb.Commands().Start(ctx, "sleep 300")
   521  	if err != nil {
   522  		t.Fatalf("Start 失败: %v", err)
   523  	}
   524  
   525  	// 等待 PID 被设置
   526  	if _, err := handle.WaitPID(ctx); err != nil {
   527  		t.Fatalf("WaitPID 失败: %v", err)
   528  	}
   529  	t.Logf("进程已启动: PID=%d", handle.PID())
   530  
   531  	// Kill 进程
   532  	if err := sb.Commands().Kill(ctx, handle.PID()); err != nil {
   533  		t.Fatalf("Kill 失败: %v", err)
   534  	}
   535  
   536  	result, err := handle.Wait()
   537  	if err != nil {
   538  		t.Fatalf("Wait 失败: %v", err)
   539  	}
   540  
   541  	if result.ExitCode == 0 {
   542  		t.Fatal("被 Kill 的进程 ExitCode 不应为 0")
   543  	}
   544  	t.Logf("Kill 验证通过: ExitCode=%d", result.ExitCode)
   545  }
   546  
   547  func TestIntegrationCommandsList(t *testing.T) {
   548  	c := testClient(t)
   549  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   550  	defer cancel()
   551  
   552  	sb := createTestSandbox(t, c, ctx)
   553  	t.Logf("沙箱: %s", sb.ID())
   554  
   555  	handle, err := sb.Commands().Start(ctx, "sleep 300")
   556  	if err != nil {
   557  		t.Fatalf("Start 失败: %v", err)
   558  	}
   559  
   560  	// 等待 PID 被设置
   561  	if _, err := handle.WaitPID(ctx); err != nil {
   562  		t.Fatalf("WaitPID 失败: %v", err)
   563  	}
   564  	t.Logf("进程已启动: PID=%d", handle.PID())
   565  
   566  	// 列出进程
   567  	infos, err := sb.Commands().List(ctx)
   568  	if err != nil {
   569  		t.Fatalf("List 失败: %v", err)
   570  	}
   571  
   572  	found := false
   573  	for _, info := range infos {
   574  		if info.PID == handle.PID() {
   575  			found = true
   576  			t.Logf("找到进程: PID=%d, Cmd=%s, Args=%v", info.PID, info.Cmd, info.Args)
   577  			break
   578  		}
   579  	}
   580  	if !found {
   581  		t.Fatalf("进程列表中未找到 PID=%d,共 %d 个进程", handle.PID(), len(infos))
   582  	}
   583  
   584  	// 清理
   585  	_ = handle.Kill(ctx)
   586  	_, _ = handle.Wait()
   587  	t.Log("List 验证通过")
   588  }
   589  
   590  func TestIntegrationCommandsSendStdin(t *testing.T) {
   591  	c := testClient(t)
   592  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   593  	defer cancel()
   594  
   595  	sb := createTestSandbox(t, c, ctx)
   596  	t.Logf("沙箱: %s", sb.ID())
   597  
   598  	// 启动 sleep 长命令(stdin 默认禁用,SendStdin 验证 RPC 调用不报错即可)
   599  	handle, err := sb.Commands().Start(ctx, "sleep 300")
   600  	if err != nil {
   601  		t.Fatalf("Start 失败: %v", err)
   602  	}
   603  
   604  	// 等待 PID 被设置
   605  	if _, err := handle.WaitPID(ctx); err != nil {
   606  		t.Fatalf("WaitPID 失败: %v", err)
   607  	}
   608  
   609  	// 发送 stdin(stdin 未启用,服务端会返回错误,验证错误信息符合预期)
   610  	err = sb.Commands().SendStdin(ctx, handle.PID(), []byte("hello\n"))
   611  	if err != nil {
   612  		// stdin 未启用时,服务端返回 "stdin not enabled" 错误是预期行为
   613  		if strings.Contains(err.Error(), "stdin not enabled") {
   614  			t.Logf("SendStdin 返回预期错误: %v", err)
   615  		} else {
   616  			t.Fatalf("SendStdin 失败(非预期错误): %v", err)
   617  		}
   618  	} else {
   619  		t.Log("SendStdin RPC 调用成功(数据可能被丢弃)")
   620  	}
   621  
   622  	// 清理
   623  	_ = handle.Kill(ctx)
   624  	_, _ = handle.Wait()
   625  	t.Log("SendStdin 验证通过")
   626  }
   627  
   628  func TestIntegrationCommandsWithCallbacks(t *testing.T) {
   629  	c := testClient(t)
   630  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   631  	defer cancel()
   632  
   633  	sb := createTestSandbox(t, c, ctx)
   634  	t.Logf("沙箱: %s", sb.ID())
   635  
   636  	var (
   637  		mu        sync.Mutex
   638  		gotStdout []byte
   639  		gotStderr []byte
   640  	)
   641  
   642  	result, err := sb.Commands().Run(ctx, "echo out && echo err >&2",
   643  		WithOnStdout(func(data []byte) {
   644  			mu.Lock()
   645  			defer mu.Unlock()
   646  			gotStdout = append(gotStdout, data...)
   647  		}),
   648  		WithOnStderr(func(data []byte) {
   649  			mu.Lock()
   650  			defer mu.Unlock()
   651  			gotStderr = append(gotStderr, data...)
   652  		}),
   653  	)
   654  	if err != nil {
   655  		t.Fatalf("Run 失败: %v", err)
   656  	}
   657  	if result.ExitCode != 0 {
   658  		t.Fatalf("ExitCode = %d, want 0", result.ExitCode)
   659  	}
   660  
   661  	mu.Lock()
   662  	stdoutStr := string(gotStdout)
   663  	stderrStr := string(gotStderr)
   664  	mu.Unlock()
   665  
   666  	if !strings.Contains(stdoutStr, "out") {
   667  		t.Fatalf("Stdout 回调未收到预期数据: %q", stdoutStr)
   668  	}
   669  	if !strings.Contains(stderrStr, "err") {
   670  		t.Fatalf("Stderr 回调未收到预期数据: %q", stderrStr)
   671  	}
   672  	t.Logf("回调验证通过: stdout=%q, stderr=%q", stdoutStr, stderrStr)
   673  }
   674  
   675  func TestIntegrationCommandsWithOptions(t *testing.T) {
   676  	c := testClient(t)
   677  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   678  	defer cancel()
   679  
   680  	sb := createTestSandbox(t, c, ctx)
   681  	t.Logf("沙箱: %s", sb.ID())
   682  
   683  	// 测试 WithCwd
   684  	result, err := sb.Commands().Run(ctx, "pwd", WithCwd("/tmp"))
   685  	if err != nil {
   686  		t.Fatalf("Run with WithCwd 失败: %v", err)
   687  	}
   688  	if !strings.Contains(result.Stdout, "/tmp") {
   689  		t.Fatalf("WithCwd: Stdout = %q, 应包含 '/tmp'", result.Stdout)
   690  	}
   691  	t.Logf("WithCwd 验证通过: Stdout=%q", result.Stdout)
   692  
   693  	// 测试 WithEnvs
   694  	result, err = sb.Commands().Run(ctx, "echo $FOO", WithEnvs(map[string]string{"FOO": "BAR"}))
   695  	if err != nil {
   696  		t.Fatalf("Run with WithEnvs 失败: %v", err)
   697  	}
   698  	if !strings.Contains(result.Stdout, "BAR") {
   699  		t.Fatalf("WithEnvs: Stdout = %q, 应包含 'BAR'", result.Stdout)
   700  	}
   701  	t.Logf("WithEnvs 验证通过: Stdout=%q", result.Stdout)
   702  }
   703  
   704  // --- PTY 交互 ---
   705  
   706  func TestIntegrationPtyCreateAndKill(t *testing.T) {
   707  	c := testClient(t)
   708  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   709  	defer cancel()
   710  
   711  	sb := createTestSandbox(t, c, ctx)
   712  	t.Logf("沙箱: %s", sb.ID())
   713  
   714  	var (
   715  		mu     sync.Mutex
   716  		output []byte
   717  	)
   718  
   719  	handle, err := sb.Pty().Create(ctx, PtySize{Cols: 80, Rows: 24},
   720  		WithOnStdout(func(data []byte) {
   721  			mu.Lock()
   722  			defer mu.Unlock()
   723  			output = append(output, data...)
   724  		}),
   725  	)
   726  	if err != nil {
   727  		t.Fatalf("Pty.Create 失败: %v", err)
   728  	}
   729  
   730  	// 等待 PID 被设置
   731  	if _, err := handle.WaitPID(ctx); err != nil {
   732  		t.Fatalf("WaitPID 失败: %v", err)
   733  	}
   734  	t.Logf("PTY 已创建: PID=%d", handle.PID())
   735  
   736  	// 等待一些 PTY 输出
   737  	time.Sleep(2 * time.Second)
   738  
   739  	// Kill PTY
   740  	if err := sb.Pty().Kill(ctx, handle.PID()); err != nil {
   741  		t.Fatalf("Pty.Kill 失败: %v", err)
   742  	}
   743  
   744  	result, err := handle.Wait()
   745  	if err != nil {
   746  		t.Fatalf("Wait 失败: %v", err)
   747  	}
   748  
   749  	mu.Lock()
   750  	outputStr := string(output)
   751  	mu.Unlock()
   752  
   753  	t.Logf("PTY 输出长度: %d bytes, ExitCode=%d", len(outputStr), result.ExitCode)
   754  	if len(outputStr) == 0 {
   755  		t.Log("警告: PTY 未收到输出(某些环境下 bash prompt 可能为空)")
   756  	}
   757  	t.Log("Pty.Create/Kill 验证通过")
   758  }
   759  
   760  func TestIntegrationPtySendInput(t *testing.T) {
   761  	c := testClient(t)
   762  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   763  	defer cancel()
   764  
   765  	sb := createTestSandbox(t, c, ctx)
   766  	t.Logf("沙箱: %s", sb.ID())
   767  
   768  	var (
   769  		mu     sync.Mutex
   770  		output []byte
   771  	)
   772  
   773  	handle, err := sb.Pty().Create(ctx, PtySize{Cols: 80, Rows: 24},
   774  		WithOnStdout(func(data []byte) {
   775  			mu.Lock()
   776  			defer mu.Unlock()
   777  			output = append(output, data...)
   778  		}),
   779  	)
   780  	if err != nil {
   781  		t.Fatalf("Pty.Create 失败: %v", err)
   782  	}
   783  
   784  	// 等待 PID 被设置并让 shell 初始化
   785  	if _, err := handle.WaitPID(ctx); err != nil {
   786  		t.Fatalf("WaitPID 失败: %v", err)
   787  	}
   788  	time.Sleep(2 * time.Second)
   789  
   790  	// 发送输入
   791  	if err := sb.Pty().SendInput(ctx, handle.PID(), []byte("echo pty-test\n")); err != nil {
   792  		t.Fatalf("Pty.SendInput 失败: %v", err)
   793  	}
   794  
   795  	// 等待输出
   796  	deadline := time.After(10 * time.Second)
   797  	for {
   798  		mu.Lock()
   799  		has := strings.Contains(string(output), "pty-test")
   800  		mu.Unlock()
   801  		if has {
   802  			break
   803  		}
   804  		select {
   805  		case <-deadline:
   806  			mu.Lock()
   807  			t.Fatalf("等待 PTY 输出超时,已收到: %q", string(output))
   808  			mu.Unlock()
   809  		default:
   810  			time.Sleep(200 * time.Millisecond)
   811  		}
   812  	}
   813  
   814  	// 清理
   815  	_ = sb.Pty().Kill(ctx, handle.PID())
   816  	_, _ = handle.Wait()
   817  
   818  	mu.Lock()
   819  	t.Logf("PTY 输出: %q", string(output))
   820  	mu.Unlock()
   821  	t.Log("Pty.SendInput 验证通过")
   822  }
   823  
   824  func TestIntegrationPtyResize(t *testing.T) {
   825  	c := testClient(t)
   826  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   827  	defer cancel()
   828  
   829  	sb := createTestSandbox(t, c, ctx)
   830  	t.Logf("沙箱: %s", sb.ID())
   831  
   832  	handle, err := sb.Pty().Create(ctx, PtySize{Cols: 80, Rows: 24})
   833  	if err != nil {
   834  		t.Fatalf("Pty.Create 失败: %v", err)
   835  	}
   836  
   837  	// 等待 PID 被设置
   838  	if _, err := handle.WaitPID(ctx); err != nil {
   839  		t.Fatalf("WaitPID 失败: %v", err)
   840  	}
   841  
   842  	// Resize
   843  	if err := sb.Pty().Resize(ctx, handle.PID(), PtySize{Cols: 200, Rows: 50}); err != nil {
   844  		t.Fatalf("Pty.Resize 失败: %v", err)
   845  	}
   846  	t.Log("Resize 调用成功")
   847  
   848  	// 清理
   849  	_ = sb.Pty().Kill(ctx, handle.PID())
   850  	_, _ = handle.Wait()
   851  	t.Log("Pty.Resize 验证通过")
   852  }
   853  
   854  // --- Filesystem.WatchDir ---
   855  
   856  func TestIntegrationWatchDir(t *testing.T) {
   857  	c := testClient(t)
   858  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   859  	defer cancel()
   860  
   861  	sb := createTestSandbox(t, c, ctx)
   862  	t.Logf("沙箱: %s", sb.ID())
   863  
   864  	// 创建监听目标目录
   865  	watchPath := "/tmp/watch-test-" + fmt.Sprintf("%d", time.Now().UnixNano())
   866  	_, err := sb.Files().MakeDir(ctx, watchPath)
   867  	if err != nil {
   868  		t.Fatalf("MakeDir 失败: %v", err)
   869  	}
   870  
   871  	// 启动 WatchDir
   872  	watcher, err := sb.Files().WatchDir(ctx, watchPath, WithRecursive(true))
   873  	if err != nil {
   874  		t.Fatalf("WatchDir 失败: %v", err)
   875  	}
   876  	defer watcher.Stop()
   877  
   878  	// 等待 watcher 就绪
   879  	time.Sleep(1 * time.Second)
   880  
   881  	// 在另一个 goroutine 写入文件触发事件
   882  	go func() {
   883  		_, _ = sb.Files().Write(ctx, watchPath+"/event-file.txt", []byte("watch me"))
   884  	}()
   885  
   886  	// 收集事件
   887  	var events []FilesystemEvent
   888  	deadline := time.After(15 * time.Second)
   889  	for {
   890  		select {
   891  		case ev, ok := <-watcher.Events():
   892  			if !ok {
   893  				goto done
   894  			}
   895  			events = append(events, ev)
   896  			t.Logf("收到事件: Name=%s, Type=%s", ev.Name, ev.Type)
   897  			// 收到至少一个事件就可以了
   898  			if len(events) >= 1 {
   899  				goto done
   900  			}
   901  		case <-deadline:
   902  			goto done
   903  		}
   904  	}
   905  done:
   906  
   907  	if len(events) == 0 {
   908  		t.Fatal("未收到任何文件系统事件")
   909  	}
   910  	t.Logf("WatchDir 验证通过: 收到 %d 个事件", len(events))
   911  }
   912  
   913  func TestIntegrationMetadata(t *testing.T) {
   914  	c := testClient(t)
   915  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   916  	defer cancel()
   917  
   918  	templates, err := c.ListTemplates(ctx, nil)
   919  	if err != nil {
   920  		t.Fatalf("ListTemplates 失败: %v", err)
   921  	}
   922  
   923  	var templateID string
   924  	for _, tmpl := range templates {
   925  		if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded {
   926  			templateID = tmpl.TemplateID
   927  			break
   928  		}
   929  	}
   930  	if templateID == "" {
   931  		t.Skip("没有可用模板,跳过测试")
   932  	}
   933  
   934  	timeout := int32(60)
   935  	meta := Metadata{"env": "test", "team": "backend"}
   936  	sb, _, err := c.CreateAndWait(ctx, CreateParams{
   937  		TemplateID: templateID,
   938  		Timeout:    &timeout,
   939  		Metadata:   &meta,
   940  	}, WithPollInterval(2*time.Second))
   941  	if err != nil {
   942  		t.Fatalf("CreateAndWait 失败: %v", err)
   943  	}
   944  	t.Cleanup(func() {
   945  		killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second)
   946  		defer killCancel()
   947  		if err := sb.Kill(killCtx); err != nil {
   948  			t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err)
   949  		}
   950  	})
   951  
   952  	info, err := sb.GetInfo(ctx)
   953  	if err != nil {
   954  		t.Fatalf("GetInfo 失败: %v", err)
   955  	}
   956  
   957  	if info.Metadata == nil {
   958  		t.Fatal("Metadata 应不为 nil")
   959  	}
   960  	got := *info.Metadata
   961  	if got["env"] != "test" {
   962  		t.Errorf("Metadata[env] = %q, want %q", got["env"], "test")
   963  	}
   964  	if got["team"] != "backend" {
   965  		t.Errorf("Metadata[team] = %q, want %q", got["team"], "backend")
   966  	}
   967  	t.Logf("Metadata 验证通过: %v", got)
   968  }
   969  
   970  // TestIntegrationCreateIdempotencyRetry 测试创建沙箱的幂等重试机制。
   971  func TestIntegrationCreateIdempotencyRetry(t *testing.T) {
   972  	c := testClient(t)
   973  	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
   974  	defer cancel()
   975  
   976  	templates, err := c.ListTemplates(ctx, nil)
   977  	if err != nil {
   978  		t.Fatalf("ListTemplates 失败: %v", err)
   979  	}
   980  	var templateID string
   981  	for _, tmpl := range templates {
   982  		if tmpl.BuildStatus == BuildStatusReady || tmpl.BuildStatus == BuildStatusUploaded {
   983  			templateID = tmpl.TemplateID
   984  			break
   985  		}
   986  	}
   987  	if templateID == "" {
   988  		t.Skip("没有可用模板,跳过测试")
   989  	}
   990  	t.Logf("使用模板: %s", templateID)
   991  
   992  	timeout := int32(60)
   993  
   994  	// 1. 不指定幂等键,验证自动生成
   995  	sb1, err := c.Create(ctx, CreateParams{
   996  		TemplateID: templateID,
   997  		Timeout:    &timeout,
   998  	})
   999  	if err != nil {
  1000  		t.Fatalf("Create(自动幂等键)失败: %v", err)
  1001  	}
  1002  	t.Logf("沙箱1(自动幂等键): %s", sb1.ID())
  1003  	defer killSandbox(t, sb1)
  1004  
  1005  	// 2. 使用相同的自定义幂等键创建两次,应返回同一沙箱
  1006  	idempotencyKey := "sdk-test-key-" + time.Now().Format("20060102-150405")
  1007  	sb2, err := c.Create(ctx, CreateParams{
  1008  		TemplateID:     templateID,
  1009  		Timeout:        &timeout,
  1010  		IdempotencyKey: idempotencyKey,
  1011  	})
  1012  	if err != nil {
  1013  		t.Fatalf("Create(幂等键首次)失败: %v", err)
  1014  	}
  1015  	t.Logf("沙箱2(幂等键 %s): %s", idempotencyKey, sb2.ID())
  1016  	defer killSandbox(t, sb2)
  1017  
  1018  	sb3, err := c.Create(ctx, CreateParams{
  1019  		TemplateID:     templateID,
  1020  		Timeout:        &timeout,
  1021  		IdempotencyKey: idempotencyKey,
  1022  	})
  1023  	if err != nil {
  1024  		t.Fatalf("Create(幂等键第二次)失败: %v", err)
  1025  	}
  1026  	t.Logf("沙箱3(幂等键 %s): %s", idempotencyKey, sb3.ID())
  1027  
  1028  	if sb2.ID() != sb3.ID() {
  1029  		defer killSandbox(t, sb3)
  1030  		t.Errorf("幂等重试应返回同一沙箱: sb2=%s, sb3=%s", sb2.ID(), sb3.ID())
  1031  	} else {
  1032  		t.Logf("幂等重试验证通过: sb2=sb3=%s", sb2.ID())
  1033  	}
  1034  }
  1035  
  1036  func killSandbox(t *testing.T, sb *Sandbox) {
  1037  	t.Helper()
  1038  	killCtx, killCancel := context.WithTimeout(context.Background(), 10*time.Second)
  1039  	defer killCancel()
  1040  	if err := sb.Kill(killCtx); err != nil {
  1041  		t.Logf("清理沙箱 %s 失败: %v", sb.ID(), err)
  1042  	} else {
  1043  		t.Logf("沙箱 %s 已清理", sb.ID())
  1044  	}
  1045  }