Files
ttstd-infinite-canvas/backend/internal/agent/runtime/runtime_test.go
ddcat666 cb146d27a1 feat(deploy): 画布 Agent - 独立 yingce-agent 容器并修复运行中断
生产会话不再因 stdout 管道提前关闭被误判失败。设置 YINGCE_AGENT_URL 后只走独立容器,失败不回退到内嵌进程。
2026-09-29 21:41:36 +08:00

84 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtime
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
// fakeRuntime 用一个假的 agent-runtime.mjs 替换真实运行时,只验证 Go 侧的进程与输出处理。
func fakeRuntime(t *testing.T, script string) {
t.Helper()
if _, err := exec.LookPath("node"); err != nil {
t.Skip("node is not installed")
}
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "agent-runtime.mjs"), []byte(script), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CANVAS_PI_RUNTIME_DIR", dir)
}
func noopBridge() Bridge {
handler := func(context.Context, map[string]json.RawMessage) (any, error) { return map[string]any{"ok": true}, nil }
return Bridge{Model: handler, Tool: handler, Event: handler}
}
// 回归:Wait 早于读完 stdout 时,大量输出后正常退出会被判成
// "read |0: file already closed"。
func TestRunDrainsStdoutBeforeWait(t *testing.T) {
fakeRuntime(t, `
const line = JSON.stringify({ event: "progress", pad: "x".repeat(2048) }) + "\n";
for (let i = 0; i < 2000; i++) process.stdout.write(line);
process.stdout.write(JSON.stringify({ event: "settled" }) + "\n");
`)
for i := 0; i < 5; i++ {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := Run(ctx, ProcessRequest{Model: map[string]any{"id": "m"}}, noopBridge())
cancel()
if err != nil {
t.Fatalf("attempt %d: normal exit reported as failure: %v", i, err)
}
}
}
// runtime_error 之后仍要读到 EOF,并把该错误作为运行结果返回。
func TestRunReportsRuntimeErrorAfterDrainingOutput(t *testing.T) {
fakeRuntime(t, `
process.stdout.write(JSON.stringify({ event: "runtime_error", message: "boom" }) + "\n");
const line = JSON.stringify({ event: "progress", pad: "x".repeat(2048) }) + "\n";
for (let i = 0; i < 500; i++) process.stdout.write(line);
process.exitCode = 1;
`)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := Run(ctx, ProcessRequest{Model: map[string]any{"id": "m"}}, noopBridge())
if err == nil || err.Error() != "boom" {
t.Fatalf("want runtime_error message, got %v", err)
}
}
// 子进程只能拿到白名单环境变量,拿不到数据库连接等密钥。
func TestRunDoesNotLeakBackendEnvironment(t *testing.T) {
t.Setenv("DATABASE_URL", "postgres://secret")
fakeRuntime(t, `
if (process.env.DATABASE_URL) {
process.stdout.write(JSON.stringify({ event: "runtime_error", message: "leaked" }) + "\n");
process.exitCode = 1;
}
`)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := Run(ctx, ProcessRequest{Model: map[string]any{"id": "m"}}, noopBridge()); err != nil {
if strings.Contains(err.Error(), "leaked") {
t.Fatal("backend environment leaked into the Agent runtime")
}
t.Fatal(err)
}
}