84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
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)
|
||
}
|
||
}
|