From b6774bf07811b0ad34793c115fa32e65651fe56e Mon Sep 17 00:00:00 2001 From: glmgbj233 <2411434344@qq.com> Date: Thu, 21 May 2026 08:11:11 +0000 Subject: [PATCH 1/4] Harden generated output handling Keep generated output behind the intended trust boundary while preserving the normal safe workflow. Add regression coverage for the unsafe flow and the expected safe behavior. --- sdk/go/agent/agent.go | 30 ++++++++++--- sdk/go/agent/agent_test.go | 91 +++++++++++++++++++++++++++----------- 2 files changed, 90 insertions(+), 31 deletions(-) diff --git a/sdk/go/agent/agent.go b/sdk/go/agent/agent.go index 7f584804..f7f98f47 100644 --- a/sdk/go/agent/agent.go +++ b/sdk/go/agent/agent.go @@ -2000,13 +2000,15 @@ func (a *Agent) AIWithTools(ctx context.Context, prompt string, config ai.ToolCa }, } + allowedTargets := make(map[string]struct{}, len(tools)) + for _, tool := range tools { + allowedTargets[normalizeToolInvocationTarget(agentToolNameToInvocationTarget(tool.Function.Name))] = struct{}{} + } + callFn := func(ctx context.Context, target string, input map[string]interface{}) (map[string]interface{}, error) { - if strings.Contains(target, ":skill:") { - parts := strings.SplitN(target, ":skill:", 2) - target = parts[0] + "." + parts[1] - } else if strings.Contains(target, ":") { - parts := strings.SplitN(target, ":", 2) - target = parts[0] + "." + parts[1] + target = normalizeToolInvocationTarget(target) + if _, ok := allowedTargets[target]; !ok { + return nil, fmt.Errorf("tool call target %q is not a discovered capability", target) } return a.Call(ctx, target, input) } @@ -2014,6 +2016,22 @@ func (a *Agent) AIWithTools(ctx context.Context, prompt string, config ai.ToolCa return a.aiClient.ExecuteToolCallLoop(ctx, messages, tools, config, callFn) } +func normalizeToolInvocationTarget(target string) string { + if strings.Contains(target, ":skill:") { + parts := strings.SplitN(target, ":skill:", 2) + return parts[0] + "." + parts[1] + } + if strings.Contains(target, ":") { + parts := strings.SplitN(target, ":", 2) + return parts[0] + "." + parts[1] + } + return target +} + +func agentToolNameToInvocationTarget(name string) string { + return strings.ReplaceAll(name, "__", ":") +} + // AIStream makes a streaming AI/LLM call. // Returns channels for streaming chunks and errors. // diff --git a/sdk/go/agent/agent_test.go b/sdk/go/agent/agent_test.go index 412dac41..79e01ded 100644 --- a/sdk/go/agent/agent_test.go +++ b/sdk/go/agent/agent_test.go @@ -935,6 +935,47 @@ func TestAIWithTools(t *testing.T) { require.Len(t, trace.Calls, 1) assert.Equal(t, "agent-1.lookup", trace.Calls[0].ToolName) }) + + t.Run("rejects model-invented tool targets before outbound execute request", func(t *testing.T) { + var executeCalls atomic.Int32 + var chatRequests atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/discovery/capabilities": + _, _ = w.Write([]byte(`{"discovered_at":"2025-01-01T00:00:00Z","total_agents":1,"total_reasoners":1,"total_skills":0,"pagination":{"limit":50,"offset":0,"has_more":false},"capabilities":[{"agent_id":"agent-1","reasoners":[{"id":"lookup","invocation_target":"agent-1.lookup","input_schema":{"type":"object"}}],"skills":[]}]}`)) + case "/chat/completions": + if chatRequests.Add(1) == 1 { + _ = json.NewEncoder(w).Encode(ai.Response{Choices: []ai.Choice{{Message: ai.Message{ToolCalls: []ai.ToolCall{{ID: "call-1", Type: "function", Function: ai.ToolCallFunction{Name: "agent-1.admin", Arguments: `{"query":"status"}`}}}}}}}) + return + } + _ = json.NewEncoder(w).Encode(ai.Response{Choices: []ai.Choice{{Message: ai.Message{Content: []ai.ContentPart{{Type: "text", Text: "blocked"}}}}}}) + case "/api/v1/execute/agent-1.lookup": + executeCalls.Add(1) + _, _ = w.Write([]byte(`{"status":"open"}`)) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + })) + defer server.Close() + + agent, err := New(Config{ + NodeID: "agent-1", + Version: "1.0.0", + AgentFieldURL: server.URL, + Logger: log.New(io.Discard, "", 0), + AIConfig: &ai.Config{APIKey: "test-key", BaseURL: server.URL, Model: "gpt-4o"}, + }) + require.NoError(t, err) + + resp, trace, err := agent.AIWithTools(context.Background(), "hello", ai.ToolCallConfig{MaxTurns: 1, MaxToolCalls: 1, PromptConfig: &ai.PromptConfig{}}) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, trace) + require.Len(t, trace.Calls, 1) + assert.Contains(t, trace.Calls[0].Error, "not a discovered capability") + assert.Equal(t, "blocked", resp.Text()) + assert.Equal(t, int32(0), executeCalls.Load()) + }) } func TestRunAndServe_ShutdownOnContextCancel(t *testing.T) { @@ -1446,28 +1487,28 @@ func TestCallLocalUnknownReasoner(t *testing.T) { } func TestCall_TargetPrefixing(t *testing.T) { - var capturedPath string - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - capturedPath = r.URL.Path - - resp := map[string]any{ - "status": "succeeded", - "result": map[string]any{"ok": true}, - } - _ = json.NewEncoder(w).Encode(resp) - })) - defer server.Close() - - agent, _ := New(Config{ - NodeID: "node-1", - Version: "1.0.0", - AgentFieldURL: server.URL, - Logger: log.New(io.Discard, "", 0), - }) - - _, err := agent.Call(context.Background(), "lookup", nil) - require.NoError(t, err) - - assert.Contains(t, capturedPath, "/execute/node-1.lookup") -} \ No newline at end of file + var capturedPath string + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedPath = r.URL.Path + + resp := map[string]any{ + "status": "succeeded", + "result": map[string]any{"ok": true}, + } + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + agent, _ := New(Config{ + NodeID: "node-1", + Version: "1.0.0", + AgentFieldURL: server.URL, + Logger: log.New(io.Discard, "", 0), + }) + + _, err := agent.Call(context.Background(), "lookup", nil) + require.NoError(t, err) + + assert.Contains(t, capturedPath, "/execute/node-1.lookup") +} From c47d33b1eb29834700771100ce509a94fff38602 Mon Sep 17 00:00:00 2001 From: glmgbj233 <2411434344@qq.com> Date: Mon, 25 May 2026 02:34:02 +0000 Subject: [PATCH 2/4] Add coverage for generated tool target handling --- sdk/go/agent/agent_test.go | 109 +++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/sdk/go/agent/agent_test.go b/sdk/go/agent/agent_test.go index 79e01ded..2090b522 100644 --- a/sdk/go/agent/agent_test.go +++ b/sdk/go/agent/agent_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" "log" "net/http" @@ -978,6 +979,36 @@ func TestAIWithTools(t *testing.T) { }) } +func TestNormalizeToolInvocationTarget(t *testing.T) { + tests := []struct { + name string + target string + want string + }{ + { + name: "skill target", + target: "agent-1:skill:lookup", + want: "agent-1.lookup", + }, + { + name: "reasoner target", + target: "agent-1:lookup", + want: "agent-1.lookup", + }, + { + name: "already normalized", + target: "agent-1.lookup", + want: "agent-1.lookup", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, normalizeToolInvocationTarget(tt.target)) + }) + } +} + func TestRunAndServe_ShutdownOnContextCancel(t *testing.T) { var shutdownCalls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1471,6 +1502,84 @@ func TestCallLocalEmitsStructuredExecutionLogs(t *testing.T) { } } +func TestCallLocalCoversCompletionAndFailureBranches(t *testing.T) { + tests := []struct { + name string + handler func(context.Context, map[string]any) (any, error) + wantEvent string + wantLevel string + wantErr string + }{ + { + name: "success", + handler: func(_ context.Context, input map[string]any) (any, error) { + return map[string]any{"echo": input["msg"]}, nil + }, + wantEvent: "call.local.complete", + wantLevel: "info", + }, + { + name: "failure", + handler: func(context.Context, map[string]any) (any, error) { + return nil, errors.New("handler failed") + }, + wantEvent: "call.local.failed", + wantLevel: "error", + wantErr: "handler failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ag, err := New(Config{ + NodeID: "node-1", + Version: "1.0.0", + Logger: log.New(io.Discard, "", 0), + }) + require.NoError(t, err) + ag.RegisterReasoner("child", tt.handler) + + parentCtx := contextWithExecution(context.Background(), ExecutionContext{ + RunID: "run-1", + ExecutionID: "exec-parent", + WorkflowID: "wf-1", + RootWorkflowID: "wf-1", + ReasonerName: "parent", + AgentNodeID: "node-1", + }) + + stdout, _, callErr := captureOutput(t, func() error { + _, err := ag.CallLocal(parentCtx, "child", map[string]any{"msg": "hi"}) + return err + }) + + if tt.wantErr == "" { + require.NoError(t, callErr) + } else { + require.Error(t, callErr) + assert.Contains(t, callErr.Error(), tt.wantErr) + } + + lines := strings.Split(strings.TrimSpace(stdout), "\n") + require.GreaterOrEqual(t, len(lines), 2) + + var seen bool + for _, line := range lines { + var entry ExecutionLogEntry + require.NoError(t, json.Unmarshal([]byte(line), &entry)) + if entry.EventType != tt.wantEvent { + continue + } + seen = true + assert.Equal(t, "child", entry.ReasonerID) + assert.Equal(t, tt.wantLevel, entry.Level) + assert.Equal(t, "sdk.runtime", entry.Source) + } + assert.True(t, seen, "expected %s log entry", tt.wantEvent) + }) + } +} + func TestCallLocalUnknownReasoner(t *testing.T) { cfg := Config{ NodeID: "node-1", From 46d108060f8c1bc416268dc1e5ad5fd9c2fa9c14 Mon Sep 17 00:00:00 2001 From: glmgbj233 <2411434344@qq.com> Date: Tue, 26 May 2026 09:20:23 +0000 Subject: [PATCH 3/4] test: avoid parallel port contention in package runner tests --- control-plane/internal/packages/runner_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/control-plane/internal/packages/runner_test.go b/control-plane/internal/packages/runner_test.go index c0bf8184..b65691d3 100644 --- a/control-plane/internal/packages/runner_test.go +++ b/control-plane/internal/packages/runner_test.go @@ -29,8 +29,6 @@ func waitForHTTPServer(t *testing.T, addr string) { } func TestAgentNodeRunnerPortEnvAndRegistry(t *testing.T) { - t.Parallel() - home := t.TempDir() runner := &AgentNodeRunner{AgentFieldHome: home} From cba922d5fe15a1e162197567b74676197c426b81 Mon Sep 17 00:00:00 2001 From: glmgbj233 <2411434344@qq.com> Date: Wed, 27 May 2026 09:19:56 +0000 Subject: [PATCH 4/4] Stabilize control-plane coverage tests --- .github/workflows/coverage.yml | 2 +- .../internal/cli/agent_helpers_test.go | 105 +++++++++++++----- .../cli/command_helpers_additional_test.go | 25 +++-- control-plane/internal/cli/commands_test.go | 18 ++- .../core/services/coverage_targeted_test.go | 16 ++- 5 files changed, 115 insertions(+), 51 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 884920fa..dc0906e5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -94,7 +94,7 @@ jobs: if: matrix.surface == 'control-plane' || matrix.surface == 'sdk-typescript' || matrix.surface == 'web-ui' uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22.12" cache: npm cache-dependency-path: | control-plane/web/client/package-lock.json diff --git a/control-plane/internal/cli/agent_helpers_test.go b/control-plane/internal/cli/agent_helpers_test.go index 32c2e4b1..536f3f27 100644 --- a/control-plane/internal/cli/agent_helpers_test.go +++ b/control-plane/internal/cli/agent_helpers_test.go @@ -3,8 +3,8 @@ package cli import ( "bytes" "encoding/json" + "io" "net/http" - "net/http/httptest" "os" "path/filepath" "testing" @@ -13,6 +13,12 @@ import ( "github.com/stretchr/testify/require" ) +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + func TestAgentHelpers(t *testing.T) { t.Run("output agent json supports pretty and compact", func(t *testing.T) { tests := []struct { @@ -41,17 +47,30 @@ func TestAgentHelpers(t *testing.T) { t.Run("agent http covers success headers and failures", func(t *testing.T) { var gotAPIKey string var gotContentType string - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotAPIKey = r.Header.Get("X-API-Key") - gotContentType = r.Header.Get("Content-Type") - require.Equal(t, "/api/test", r.URL.Path) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"ok":true}`)) - })) - defer server.Close() + var gotMethod string + var gotPath string + var gotBody []byte + + oldTransport := http.DefaultTransport + http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) { + gotMethod = req.Method + gotPath = req.URL.Path + gotAPIKey = req.Header.Get("X-API-Key") + gotContentType = req.Header.Get("Content-Type") + bodyBytes, err := io.ReadAll(req.Body) + require.NoError(t, err) + gotBody = bodyBytes + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(`{"ok":true}`))), + Request: req, + }, nil + }) + defer func() { http.DefaultTransport = oldTransport }() oldServer, oldKey, oldTimeout := serverURL, apiKey, requestTimeout - serverURL, apiKey, requestTimeout = server.URL+"/", "api-secret", 1 + serverURL, apiKey, requestTimeout = "http://agent.test", "api-secret", 1 defer func() { serverURL, apiKey, requestTimeout = oldServer, oldKey, oldTimeout }() @@ -60,8 +79,11 @@ func TestAgentHelpers(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, status) require.JSONEq(t, `{"ok":true}`, string(body)) + require.Equal(t, http.MethodPost, gotMethod) + require.Equal(t, "/api/test", gotPath) require.Equal(t, "api-secret", gotAPIKey) require.Equal(t, "application/json", gotContentType) + require.JSONEq(t, `{"name":"demo"}`, string(gotBody)) _, _, err = agentHTTP(http.MethodPost, "/api/test", map[string]func(){"bad": func() {}}) require.ErrorContains(t, err, "encode request body") @@ -71,6 +93,21 @@ func TestAgentHelpers(t *testing.T) { require.ErrorContains(t, err, "build request") }) + t.Run("agent command help path returns structured output", func(t *testing.T) { + oldServer := serverURL + serverURL = "http://example.test" + defer func() { serverURL = oldServer }() + + cmd := NewAgentCommand() + cmd.SetArgs([]string{}) + output := captureOutput(t, func() { + require.NoError(t, cmd.Execute()) + }) + require.Contains(t, output, `"ok": true`) + require.Contains(t, output, `"command": "af agent"`) + require.Contains(t, output, `"server": "http://example.test"`) + }) + t.Run("read batch input covers stdin and files", func(t *testing.T) { withStdin(t, `{"operations":[]}`, func() { data, err := readBatchInput("-") @@ -224,22 +261,29 @@ func TestAgentCommandSubcommands(t *testing.T) { } var records []requestRecord - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + oldTransport := http.DefaultTransport + http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) { var body bytes.Buffer - _, _ = body.ReadFrom(r.Body) + if req.Body != nil { + _, _ = body.ReadFrom(req.Body) + } records = append(records, requestRecord{ - Method: r.Method, - Path: r.URL.Path, - Query: r.URL.RawQuery, + Method: req.Method, + Path: req.URL.Path, + Query: req.URL.RawQuery, Body: body.String(), }) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"ok":true,"data":{"id":"demo"}}`)) - })) - defer server.Close() + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(`{"ok":true,"data":{"id":"demo"}}`))), + Request: req, + }, nil + }) + defer func() { http.DefaultTransport = oldTransport }() oldServer, oldFormat, oldTimeout := serverURL, outputFormat, requestTimeout - serverURL, outputFormat, requestTimeout = server.URL, "json", 1 + serverURL, outputFormat, requestTimeout = "http://agent.test", "json", 1 defer func() { serverURL, outputFormat, requestTimeout = oldServer, oldFormat, oldTimeout }() @@ -281,7 +325,7 @@ func TestAgentCommandSubcommands(t *testing.T) { if tt.wantBodyPart != "" { require.Contains(t, records[0].Body, tt.wantBodyPart) } - require.Contains(t, output, `"server": "`+server.URL+`"`) + require.Contains(t, output, `"server": "http://agent.test"`) }) } @@ -339,14 +383,19 @@ func TestSpinnerAndPrintHelpers(t *testing.T) { } func TestProxyToServerArrayResponse(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`[{"id":"one"}]`)) - })) - defer server.Close() + oldTransport := http.DefaultTransport + http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(`[{"id":"one"}]`))), + Request: req, + }, nil + }) + defer func() { http.DefaultTransport = oldTransport }() oldServer, oldFormat, oldTimeout := serverURL, outputFormat, requestTimeout - serverURL, outputFormat, requestTimeout = server.URL, "json", 1 + serverURL, outputFormat, requestTimeout = "http://agent.test", "json", 1 defer func() { serverURL, outputFormat, requestTimeout = oldServer, oldFormat, oldTimeout }() @@ -355,7 +404,7 @@ func TestProxyToServerArrayResponse(t *testing.T) { proxyToServer(http.MethodGet, "/array", nil) }) require.Contains(t, output, `"ok": true`) - require.Contains(t, output, `"server": "`+server.URL+`"`) + require.Contains(t, output, `"server": "http://agent.test"`) } func batchFile(t *testing.T, content string) string { diff --git a/control-plane/internal/cli/command_helpers_additional_test.go b/control-plane/internal/cli/command_helpers_additional_test.go index 621c7451..aa7b9fce 100644 --- a/control-plane/internal/cli/command_helpers_additional_test.go +++ b/control-plane/internal/cli/command_helpers_additional_test.go @@ -2,8 +2,8 @@ package cli import ( "bytes" + "io" "net/http" - "net/http/httptest" "os" "path/filepath" "testing" @@ -131,17 +131,22 @@ installed: require.NoError(t, err) require.JSONEq(t, `{"operations":[{"id":"1"}]}`, string(data)) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "application/json", r.Header.Get("Accept")) - require.Equal(t, "secret", r.Header.Get("X-API-Key")) - require.Equal(t, "/api/test", r.URL.Path) - w.WriteHeader(http.StatusCreated) - _, _ = w.Write([]byte(`{"ok":true}`)) - })) - defer server.Close() + oldTransport := http.DefaultTransport + http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) { + require.Equal(t, "application/json", req.Header.Get("Accept")) + require.Equal(t, "secret", req.Header.Get("X-API-Key")) + require.Equal(t, "/api/test", req.URL.Path) + return &http.Response{ + StatusCode: http.StatusCreated, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(`{"ok":true}`))), + Request: req, + }, nil + }) + defer func() { http.DefaultTransport = oldTransport }() oldServer, oldAPIKey, oldTimeout := serverURL, apiKey, requestTimeout - serverURL, apiKey, requestTimeout = server.URL, "secret", 1 + serverURL, apiKey, requestTimeout = "http://agent.test", "secret", 1 defer func() { serverURL, apiKey, requestTimeout = oldServer, oldAPIKey, oldTimeout }() diff --git a/control-plane/internal/cli/commands_test.go b/control-plane/internal/cli/commands_test.go index 73557449..901971c2 100644 --- a/control-plane/internal/cli/commands_test.go +++ b/control-plane/internal/cli/commands_test.go @@ -49,8 +49,8 @@ func TestStopCommand(t *testing.T) { errorMsg := err.Error() require.True(t, strings.Contains(strings.ToLower(errorMsg), "not installed") || - strings.Contains(strings.ToLower(errorMsg), "not running") || - strings.Contains(strings.ToLower(errorMsg), "not found"), + strings.Contains(strings.ToLower(errorMsg), "not running") || + strings.Contains(strings.ToLower(errorMsg), "not found"), "Expected error about agent not found/installed/running, got: %s", errorMsg) } } @@ -148,17 +148,23 @@ func TestLogsCommand(t *testing.T) { // TestInitCommand tests the init command func TestInitCommand(t *testing.T) { + wd := t.TempDir() t.Setenv("HOME", t.TempDir()) + oldWD, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(wd)) + t.Cleanup(func() { + _ = os.Chdir(oldWD) + }) resetCLIStateForTest() cmd := NewInitCommand() cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - cmd.SetArgs([]string{}) + cmd.SetArgs([]string{"demo-agent", "--non-interactive", "--language", "python", "--author", "Jane Doe", "--email", "jane@example.com"}) - err := cmd.Execute() - // May error if already initialized, but validates command structure - _ = err + err = cmd.Execute() + require.NoError(t, err) } // TestRootCommandFlags tests various root command flags diff --git a/control-plane/internal/core/services/coverage_targeted_test.go b/control-plane/internal/core/services/coverage_targeted_test.go index da41ab6e..2505bd23 100644 --- a/control-plane/internal/core/services/coverage_targeted_test.go +++ b/control-plane/internal/core/services/coverage_targeted_test.go @@ -62,6 +62,10 @@ func TestRunDevHelperProcess(t *testing.T) { } } +func runDevPythonHelperMain() string { + return "import os\nimport sys\nimport threading\nimport time\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\nport = int(os.environ['PORT'])\nmode = os.environ.get('AF_TEST_HELPER_MODE', 'serve-success')\n\nclass Handler(BaseHTTPRequestHandler):\n def log_message(self, fmt, *args):\n pass\n\n def do_GET(self):\n if self.path == '/health':\n self.send_response(200)\n self.end_headers()\n elif self.path == '/reasoners':\n self.send_response(200)\n self.end_headers()\n if mode == 'serve-capabilities-error':\n self.wfile.write(b'{\\\"reasoners\\\":[')\n else:\n self.wfile.write(b'{\\\"reasoners\\\":[{\\\"id\\\":\\\"helper-reasoner\\\"}]}')\n elif self.path == '/skills':\n self.send_response(200)\n self.end_headers()\n self.wfile.write(b'{\\\"skills\\\":[{\\\"id\\\":\\\"helper-skill\\\"}]}')\n else:\n self.send_response(404)\n self.end_headers()\n\nserver = HTTPServer(('127.0.0.1', port), Handler)\nthread = threading.Thread(target=server.serve_forever)\nthread.daemon = True\nthread.start()\ntime.sleep(10)\nserver.shutdown()\nserver.server_close()\nsys.exit(3 if mode == 'serve-exit-error' else 0)\n" +} + func TestDevServiceRunDev(t *testing.T) { // These subtests spawn a real python3 subprocess that serves HTTP on a // port in the 8001-8999 range. discoverAgentPort scans that range with a @@ -82,8 +86,8 @@ func TestDevServiceRunDev(t *testing.T) { port := findPortInAgentRange(t) t.Setenv("AF_TEST_HELPER_MODE", "serve-success") - script := "#!/bin/sh\npython3 - <<'PY'\nimport os, sys, threading, time\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nport = int(os.environ['PORT'])\nmode = os.environ.get('AF_TEST_HELPER_MODE', 'serve-success')\nclass Handler(BaseHTTPRequestHandler):\n def log_message(self, fmt, *args):\n pass\n def do_GET(self):\n if self.path == '/health':\n self.send_response(200)\n self.end_headers()\n elif self.path == '/reasoners':\n self.send_response(200)\n self.end_headers()\n if mode == 'serve-capabilities-error':\n self.wfile.write(b'{\"reasoners\":[')\n else:\n self.wfile.write(b'{\"reasoners\":[{\"id\":\"helper-reasoner\"}]}')\n elif self.path == '/skills':\n self.send_response(200)\n self.end_headers()\n self.wfile.write(b'{\"skills\":[{\"id\":\"helper-skill\"}]}')\n else:\n self.send_response(404)\n self.end_headers()\nserver = HTTPServer(('127.0.0.1', port), Handler)\nthread = threading.Thread(target=server.serve_forever)\nthread.daemon = True\nthread.start()\ntime.sleep(1.2)\nserver.shutdown()\nserver.server_close()\nsys.exit(3 if mode == 'serve-exit-error' else 0)\nPY\n" - require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte(script), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.py"), []byte(runDevPythonHelperMain()), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte("#!/bin/sh\nexec python3 \"$@\"\n"), 0o755)) service := &DefaultDevService{} err := service.runDev(dir, domain.DevOptions{Port: port}) @@ -97,8 +101,8 @@ func TestDevServiceRunDev(t *testing.T) { port := findPortInAgentRange(t) t.Setenv("AF_TEST_HELPER_MODE", "serve-exit-error") - script := "#!/bin/sh\npython3 - <<'PY'\nimport os, sys, threading, time\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nport = int(os.environ['PORT'])\nmode = os.environ.get('AF_TEST_HELPER_MODE', 'serve-success')\nclass Handler(BaseHTTPRequestHandler):\n def log_message(self, fmt, *args):\n pass\n def do_GET(self):\n if self.path == '/health':\n self.send_response(200)\n self.end_headers()\n elif self.path == '/reasoners':\n self.send_response(200)\n self.end_headers()\n if mode == 'serve-capabilities-error':\n self.wfile.write(b'{\"reasoners\":[')\n else:\n self.wfile.write(b'{\"reasoners\":[{\"id\":\"helper-reasoner\"}]}')\n elif self.path == '/skills':\n self.send_response(200)\n self.end_headers()\n self.wfile.write(b'{\"skills\":[{\"id\":\"helper-skill\"}]}')\n else:\n self.send_response(404)\n self.end_headers()\nserver = HTTPServer(('127.0.0.1', port), Handler)\nthread = threading.Thread(target=server.serve_forever)\nthread.daemon = True\nthread.start()\ntime.sleep(1.2)\nserver.shutdown()\nserver.server_close()\nsys.exit(3 if mode == 'serve-exit-error' else 0)\nPY\n" - require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte(script), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.py"), []byte(runDevPythonHelperMain()), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte("#!/bin/sh\nexec python3 \"$@\"\n"), 0o755)) service := &DefaultDevService{} err := service.runDev(dir, domain.DevOptions{Port: port}) @@ -124,8 +128,8 @@ func TestDevServiceRunDev(t *testing.T) { port := findPortInAgentRange(t) t.Setenv("AF_TEST_HELPER_MODE", "serve-capabilities-error") - script := "#!/bin/sh\npython3 - <<'PY'\nimport os, sys, threading, time\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nport = int(os.environ['PORT'])\nmode = os.environ.get('AF_TEST_HELPER_MODE', 'serve-success')\nclass Handler(BaseHTTPRequestHandler):\n def log_message(self, fmt, *args):\n pass\n def do_GET(self):\n if self.path == '/health':\n self.send_response(200)\n self.end_headers()\n elif self.path == '/reasoners':\n self.send_response(200)\n self.end_headers()\n if mode == 'serve-capabilities-error':\n self.wfile.write(b'{\"reasoners\":[')\n else:\n self.wfile.write(b'{\"reasoners\":[{\"id\":\"helper-reasoner\"}]}')\n elif self.path == '/skills':\n self.send_response(200)\n self.end_headers()\n self.wfile.write(b'{\"skills\":[{\"id\":\"helper-skill\"}]}')\n else:\n self.send_response(404)\n self.end_headers()\nserver = HTTPServer(('127.0.0.1', port), Handler)\nthread = threading.Thread(target=server.serve_forever)\nthread.daemon = True\nthread.start()\ntime.sleep(1.2)\nserver.shutdown()\nserver.server_close()\nsys.exit(3 if mode == 'serve-exit-error' else 0)\nPY\n" - require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte(script), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "main.py"), []byte(runDevPythonHelperMain()), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte("#!/bin/sh\nexec python3 \"$@\"\n"), 0o755)) service := &DefaultDevService{} err := service.runDev(dir, domain.DevOptions{Port: port})