Files
ttstd-infinite-canvas/backend/internal/agent/runtime/runtime_test.go
T
ddcat666 4957f8bc22 feat(admin): Agent 并发 - 后台可调同时对话上限并展示 Agent 服务状态
新增运行策略 agentMaxSessions(默认 30,范围 1-64),保存后同时下发到内嵌进程闸门与独立 yingce-agent 的 /v1/limit,降低上限不会中断已开始的对话。系统性能页展示 Agent 服务健康、进行中/排队数量,并可直接修改上限。
2026-09-30 03:37:54 +08:00

103 lines
3.4 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 TestSetProcessLimitDoesNotStopActiveWork(t *testing.T) {
t.Cleanup(func() { SetProcessLimit(defaultAgentProcessLimit) })
release, err := acquireAgentProcess(context.Background())
if err != nil {
t.Fatal(err)
}
active, limit := SetProcessLimit(1)
if active != 1 || limit != 1 {
t.Fatalf("active=%d limit=%d, want active 1 and limit 1", active, limit)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
defer cancel()
if _, err := acquireAgentProcess(ctx); err == nil {
t.Fatal("lowered limit still admitted another process")
}
release()
SetProcessLimit(defaultAgentProcessLimit)
}
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)
}
}