新增运行策略 agentMaxSessions(默认 30,范围 1-64),保存后同时下发到内嵌进程闸门与独立 yingce-agent 的 /v1/limit,降低上限不会中断已开始的对话。系统性能页展示 Agent 服务健康、进行中/排队数量,并可直接修改上限。
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
package yingceagent
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentruntime "infinite-canvas/backend/internal/agent/runtime"
|
|
)
|
|
|
|
func TestRemoteRunCallsBridgeAndRejectsBadToken(t *testing.T) {
|
|
if _, err := exec.LookPath("node"); err != nil {
|
|
t.Skip("node is not installed")
|
|
}
|
|
_, source, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("locate test file")
|
|
}
|
|
serverPath := filepath.Clean(filepath.Join(filepath.Dir(source), "../../../../yingce-agent/server.mjs"))
|
|
runtimePath := filepath.Join(t.TempDir(), "agent-runtime.mjs")
|
|
script := `
|
|
let raw = "";
|
|
for await (const chunk of process.stdin) raw += chunk;
|
|
const request = JSON.parse(raw);
|
|
const response = await fetch(new URL("/tool", request.bridgeURL), {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: "Bearer " + request.bridgeToken },
|
|
body: JSON.stringify({ name: "lookup" }),
|
|
});
|
|
if (!response.ok) {
|
|
process.stdout.write(JSON.stringify({ event: "runtime_error", message: "bridge " + response.status }) + "\n");
|
|
process.exitCode = 1;
|
|
} else {
|
|
process.stdout.write(JSON.stringify({ event: "settled" }) + "\n");
|
|
}
|
|
`
|
|
if err := os.WriteFile(runtimePath, []byte(script), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
token := strings.Repeat("ab", 16)
|
|
cmd := exec.Command("node", serverPath)
|
|
cmd.Env = append(os.Environ(),
|
|
"YINGCE_AGENT_TOKEN="+token,
|
|
"YINGCE_AGENT_RUNTIME="+runtimePath,
|
|
"PORT=0",
|
|
"MAX_CONCURRENT_SESSIONS=2",
|
|
)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
})
|
|
reader := bufio.NewReader(stdout)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(line, "YINGCE_AGENT_READY ") {
|
|
t.Fatalf("server did not report its port: %q", line)
|
|
}
|
|
endpoint := "http://127.0.0.1:" + strings.TrimSpace(strings.TrimPrefix(line, "YINGCE_AGENT_READY "))
|
|
|
|
health, err := http.Get(endpoint + "/healthz")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer health.Body.Close()
|
|
if health.StatusCode != http.StatusOK {
|
|
t.Fatalf("health status %d", health.StatusCode)
|
|
}
|
|
|
|
unauthorized, err := http.Post(endpoint+"/v1/runs", "application/json", strings.NewReader(`{}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unauthorized.Body.Close()
|
|
if unauthorized.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("missing token status %d", unauthorized.StatusCode)
|
|
}
|
|
|
|
called := false
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
err = Run(ctx, endpoint, token, "127.0.0.1", agentruntime.ProcessRequest{
|
|
Model: map[string]any{"id": "m"},
|
|
}, agentruntime.Bridge{
|
|
Model: func(context.Context, map[string]json.RawMessage) (any, error) {
|
|
return map[string]any{"ok": true}, nil
|
|
},
|
|
Tool: func(context.Context, map[string]json.RawMessage) (any, error) {
|
|
called = true
|
|
return map[string]any{"content": "ok"}, nil
|
|
},
|
|
Event: func(context.Context, map[string]json.RawMessage) (any, error) {
|
|
return map[string]any{"ok": true}, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !called {
|
|
t.Fatal("remote runtime did not call the Go tool bridge")
|
|
}
|
|
if err := SetLimit(ctx, endpoint, token, 16); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, err := Inspect(ctx, endpoint)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status.Limit != 16 {
|
|
t.Fatalf("limit = %d, want 16", status.Limit)
|
|
}
|
|
}
|