Skip to content

Commit 352f0bf

Browse files
fix: mirror Mind's InProcessRuntime so local == remote
Align the python agent runtime with Mind's InProcessRuntime so a healed run behaves the same locally and remotely, and add an internal test covering it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 55c7470 commit 352f0bf

3 files changed

Lines changed: 235 additions & 11 deletions

File tree

‎go.sum‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
3333
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
3434
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
3535
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
36-
github.com/codefly-dev/core v0.2.11 h1:EZZscZPW1VSIvoQzekMUH/epNgyl7AkA19I2UbciRnE=
37-
github.com/codefly-dev/core v0.2.11/go.mod h1:17JyLS7Ni2GsOXSzl5pL6I8++GFogzRMDynle4+2uEs=
3836
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
3937
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
4038
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

‎pkg/runtime/runtime.go‎

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ func (s *Runtime) Load(ctx context.Context, req *runtimev0.LoadRequest) (*runtim
6666
s.Service.SourceLocation = wd
6767
}
6868

69-
return s.Runtime.LoadResponse()
69+
resp, err := s.Runtime.LoadResponse()
70+
if resp != nil && resp.Status != nil {
71+
resp.Status.Message = pythonhelpers.AppendRuntimeEvidence(resp.Status.Message, pythonhelpers.RuntimeEvidence(s.Service.SourceLocation))
72+
}
73+
return resp, err
7074
}
7175

7276
// Init runs `uv sync` to materialize the venv. Non-fatal on failure.
@@ -150,11 +154,14 @@ func (s *Runtime) Test(ctx context.Context, req *runtimev0.TestRequest) (*runtim
150154
wool.Field("command", fspec.Command), wool.Field("output", fspec.Output))
151155
fStart := time.Now()
152156
fRun, fErr := pythonhelpers.RunFormulaStructured(ctx, s.Service.SourceLocation, fspec)
157+
evidence := pythonhelpers.RuntimeEvidenceForFormula(s.Service.SourceLocation, cmd, output, env, prov, formulaDerivedFromProject(req, s.Base.Service))
153158
if fRun == nil {
154-
return s.Runtime.TestError(fErr)
159+
return s.testErrorWithEvidence(fErr, evidence)
155160
}
156161
s.Wool.Forwardf("Tests: %s", fRun.LegacyTestSummary().SummaryLine())
157-
return fRun.ToProtoResponse("formula", req.Suite, time.Since(fStart)), fErr
162+
resp := fRun.ToProtoResponse("formula", req.Suite, time.Since(fStart))
163+
appendTestResponseEvidence(resp, evidence)
164+
return resp, fErr
158165
}
159166

160167
opts := pythonhelpers.TestOptions{
@@ -195,9 +202,11 @@ func (s *Runtime) Test(ctx context.Context, req *runtimev0.TestRequest) (*runtim
195202
}
196203

197204
if run == nil {
198-
return s.Runtime.TestError(runErr)
205+
return s.testErrorWithEvidence(runErr, pythonhelpers.RuntimeEvidence(s.Service.SourceLocation))
199206
}
200-
return run.ToProtoResponse("pytest", req.Suite, duration), runErr
207+
resp := run.ToProtoResponse("pytest", req.Suite, duration)
208+
appendTestResponseEvidence(resp, pythonhelpers.RuntimeEvidence(s.Service.SourceLocation))
209+
return resp, runErr
201210
}
202211

203212
// Lint runs ruff via the core python helper.
@@ -229,12 +238,29 @@ func (s *Runtime) Information(ctx context.Context, req *runtimev0.InformationReq
229238
// (resources.Service.Test), else the formula DERIVED from the project, else none
230239
// (ok=false → the agent's default runner). Returns the raw language-agnostic
231240
// fields; SpecFromFormula does the uv mapping.
241+
//
242+
// A per-call formula with NO command but WITH provisioning/env (a Mind
243+
// environment heal like {cwd: tests} or {with: Werkzeug<3}) is an OVERLAY: it
244+
// rides on top of the configured/derived formula instead of being dropped.
245+
// Mirrors Mind's InProcessRuntime so local == remote — before this, a healed
246+
// work_dir sent without a command was a silent no-op on the agent path.
232247
func resolveTestFormula(req *runtimev0.TestRequest, svc *resources.Service, sourceDir string) (cmd []string, output string, env, prov map[string]string, ok bool) {
233-
if f := req.GetFormula(); f != nil && len(f.GetCommand()) > 0 {
234-
return f.GetCommand(), f.GetOutput(), f.GetEnv(), f.GetProvisioning(), true
248+
f := req.GetFormula()
249+
if f != nil && len(f.GetCommand()) > 0 {
250+
return f.GetCommand(), f.GetOutput(), f.GetEnv(), enrichSuppliedProvisioning(f.GetProvisioning(), sourceDir), true
251+
}
252+
overlay := func(bcmd []string, boutput string, benv, bprov map[string]string) ([]string, string, map[string]string, map[string]string, bool) {
253+
if f != nil {
254+
if o := f.GetOutput(); o != "" {
255+
boutput = o
256+
}
257+
benv = overlayStringMap(benv, f.GetEnv())
258+
bprov = overlayStringMap(bprov, f.GetProvisioning())
259+
}
260+
return bcmd, boutput, benv, bprov, true
235261
}
236262
if svc != nil && svc.Test != nil && len(svc.Test.Command) > 0 {
237-
return svc.Test.Command, svc.Test.Output, svc.Test.Env, svc.Test.Provisioning, true
263+
return overlay(svc.Test.Command, svc.Test.Output, svc.Test.Env, enrichSuppliedProvisioning(svc.Test.Provisioning, sourceDir))
238264
}
239265
// No explicit (per-call) or configured (service.yaml) formula → DERIVE it from
240266
// the project: read its own declarations (tox/Makefile/CI/README) for the
@@ -243,12 +269,87 @@ func resolveTestFormula(req *runtimev0.TestRequest, svc *resources.Service, sour
243269
// python plugin "just run the project's tests" — no framework knowledge in Mind.
244270
if sourceDir != "" {
245271
if dcmd, doutput, denv, dprov, dok := pythonhelpers.DeriveFormula(sourceDir); dok {
246-
return dcmd, doutput, denv, dprov, true
272+
return overlay(dcmd, doutput, denv, dprov)
247273
}
248274
}
249275
return nil, "", nil, nil, false
250276
}
251277

278+
// overlayStringMap returns base with overlay's entries layered on top (overlay
279+
// wins). Used so a per-call heal (provisioning/env without a command) survives
280+
// when the command comes from config/derivation. Returns base unchanged when
281+
// the overlay is empty.
282+
func overlayStringMap(base, overlay map[string]string) map[string]string {
283+
if len(overlay) == 0 {
284+
return base
285+
}
286+
out := make(map[string]string, len(base)+len(overlay))
287+
for k, v := range base {
288+
out[k] = v
289+
}
290+
for k, v := range overlay {
291+
out[k] = v
292+
}
293+
return out
294+
}
295+
296+
// enrichSuppliedProvisioning delegates to the SHARED core helper
297+
// (pythonhelpers.EnrichSuppliedProvisioning) so the agent's Test RPC and
298+
// Mind's in-process runtime resolve identical formulas — the health probe and
299+
// the task-phase test run must see the same derived editable/python/
300+
// build-requires under any supplied keys.
301+
func enrichSuppliedProvisioning(supplied map[string]string, sourceDir string) map[string]string {
302+
return pythonhelpers.EnrichSuppliedProvisioning(supplied, sourceDir)
303+
}
304+
305+
func formulaDerivedFromProject(req *runtimev0.TestRequest, svc *resources.Service) bool {
306+
if f := req.GetFormula(); f != nil && len(f.GetCommand()) > 0 {
307+
return false
308+
}
309+
return svc == nil || svc.Test == nil || len(svc.Test.Command) == 0
310+
}
311+
312+
func (s *Runtime) testErrorWithEvidence(err error, evidence string) (*runtimev0.TestResponse, error) {
313+
if err == nil {
314+
msg := pythonhelpers.AppendRuntimeEvidence("test runtime errored before tests could execute", evidence)
315+
return &runtimev0.TestResponse{
316+
Status: &runtimev0.TestStatus{State: runtimev0.TestStatus_ERROR, Message: msg},
317+
Result: &runtimev0.TestRunResult{State: runtimev0.TestRunResult_ERRORED, Message: msg},
318+
Counts: &runtimev0.TestCounts{},
319+
}, nil
320+
}
321+
resp, testErr := s.Runtime.TestError(err)
322+
appendTestResponseErrorEvidence(resp, evidence)
323+
return resp, testErr
324+
}
325+
326+
func appendTestResponseEvidence(resp *runtimev0.TestResponse, evidence string) {
327+
if resp == nil || resp.GetResult().GetState() != runtimev0.TestRunResult_ERRORED {
328+
return
329+
}
330+
appendTestResponseErrorEvidence(resp, evidence)
331+
}
332+
333+
func appendTestResponseErrorEvidence(resp *runtimev0.TestResponse, evidence string) {
334+
if resp == nil {
335+
return
336+
}
337+
if resp.Counts == nil {
338+
resp.Counts = &runtimev0.TestCounts{}
339+
}
340+
if resp.Status != nil {
341+
resp.Status.Message = pythonhelpers.AppendRuntimeEvidence(resp.Status.Message, evidence)
342+
}
343+
if resp.Result != nil {
344+
resp.Result.Message = pythonhelpers.AppendRuntimeEvidence(resp.Result.Message, evidence)
345+
} else {
346+
resp.Result = &runtimev0.TestRunResult{
347+
State: runtimev0.TestRunResult_ERRORED,
348+
Message: pythonhelpers.AppendRuntimeEvidence("", evidence),
349+
}
350+
}
351+
}
352+
252353
func boolToLintState(ok bool) runtimev0.LintStatus_Status {
253354
if ok {
254355
return runtimev0.LintStatus_SUCCESS
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package runtime
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
runtimev0 "github.com/codefly-dev/core/generated/go/codefly/services/runtime/v0"
9+
"github.com/codefly-dev/core/resources"
10+
)
11+
12+
// TestEnrichSuppliedProvisioning locks the supplied-formula environment
13+
// contract: the caller owns WHAT to run, the plugin derives the uv
14+
// environment around it (editable install, interpreter pin), and explicitly
15+
// supplied keys always win. This is the regression test for the SWE-bench
16+
// easy-tier failure where django's supplied "python runtests.py" formula ran
17+
// without --with-editable . and env-blocked with "Django module not found".
18+
func TestEnrichSuppliedProvisioning(t *testing.T) {
19+
dir := t.TempDir()
20+
if err := os.WriteFile(filepath.Join(dir, "setup.py"), []byte("from setuptools import setup\nsetup(name='pkg')\n"), 0o644); err != nil {
21+
t.Fatal(err)
22+
}
23+
if err := os.WriteFile(filepath.Join(dir, ".python-version"), []byte("3.9\n"), 0o644); err != nil {
24+
t.Fatal(err)
25+
}
26+
27+
t.Run("empty bag gains derived environment", func(t *testing.T) {
28+
got := enrichSuppliedProvisioning(nil, dir)
29+
if got["editable"] != "true" {
30+
t.Fatalf("editable = %q, want true (project package must be importable)", got["editable"])
31+
}
32+
if got["no_project"] != "true" {
33+
t.Fatalf("no_project = %q, want true", got["no_project"])
34+
}
35+
if got["python"] != "3.9" {
36+
t.Fatalf("python = %q, want 3.9 from .python-version", got["python"])
37+
}
38+
})
39+
40+
t.Run("supplied keys win over derived", func(t *testing.T) {
41+
got := enrichSuppliedProvisioning(map[string]string{"editable": "false", "python": "3.11"}, dir)
42+
if got["editable"] != "false" {
43+
t.Fatalf("editable = %q, want supplied false to win", got["editable"])
44+
}
45+
if got["python"] != "3.11" {
46+
t.Fatalf("python = %q, want supplied 3.11 to win", got["python"])
47+
}
48+
if got["no_project"] != "true" {
49+
t.Fatalf("no_project = %q, want derived true to fill the gap", got["no_project"])
50+
}
51+
})
52+
53+
t.Run("no source dir passes supplied through", func(t *testing.T) {
54+
supplied := map[string]string{"editable": "false"}
55+
got := enrichSuppliedProvisioning(supplied, "")
56+
if len(got) != 1 || got["editable"] != "false" {
57+
t.Fatalf("got %v, want supplied bag unchanged", got)
58+
}
59+
})
60+
}
61+
62+
// A per-call formula with NO command but WITH provisioning/env — a Mind
63+
// environment heal such as {cwd: tests} or {with: Werkzeug<3} — must OVERLAY
64+
// the configured/derived formula instead of being dropped. This locks the
65+
// remote-agent path against the django-trace defect where a healed work_dir
66+
// sent without a command was a silent no-op (mirrors Mind's InProcessRuntime).
67+
func TestResolveTestFormulaOverlaysCommandlessHealOntoConfiguredFormula(t *testing.T) {
68+
svc := &resources.Service{
69+
Test: &resources.TestFormula{
70+
Command: []string{"python", "runtests.py"},
71+
Output: "unittest-text",
72+
Provisioning: map[string]string{"python": "3.9"},
73+
Env: map[string]string{"KEEP": "1"},
74+
},
75+
}
76+
req := &runtimev0.TestRequest{Formula: &runtimev0.TestFormula{
77+
Provisioning: map[string]string{"cwd": "tests", "with": "Werkzeug<3"},
78+
Env: map[string]string{"PYTHONPATH": "."},
79+
}}
80+
81+
cmd, output, env, prov, ok := resolveTestFormula(req, svc, "")
82+
if !ok {
83+
t.Fatal("resolveTestFormula returned ok=false")
84+
}
85+
if len(cmd) != 2 || cmd[0] != "python" || cmd[1] != "runtests.py" {
86+
t.Fatalf("command = %v, want the configured command", cmd)
87+
}
88+
if output != "unittest-text" {
89+
t.Fatalf("output = %q", output)
90+
}
91+
if prov["cwd"] != "tests" || prov["with"] != "Werkzeug<3" {
92+
t.Fatalf("healed provisioning must overlay the configured formula, got %v", prov)
93+
}
94+
if prov["python"] != "3.9" {
95+
t.Fatalf("configured provisioning must survive the overlay, got %v", prov)
96+
}
97+
if env["PYTHONPATH"] != "." || env["KEEP"] != "1" {
98+
t.Fatalf("env overlay wrong: %v", env)
99+
}
100+
}
101+
102+
func TestResolveTestFormulaOverlaysCommandlessHealOntoDerivedFormula(t *testing.T) {
103+
dir := t.TempDir()
104+
toxIni := "[tox]\nenvlist = py\n\n[testenv]\ncommands =\n python -m unittest -v test_sample {posargs}\n"
105+
if err := os.WriteFile(filepath.Join(dir, "tox.ini"), []byte(toxIni), 0o644); err != nil {
106+
t.Fatal(err)
107+
}
108+
req := &runtimev0.TestRequest{Formula: &runtimev0.TestFormula{
109+
Provisioning: map[string]string{"cwd": "tests"},
110+
}}
111+
112+
cmd, _, _, prov, ok := resolveTestFormula(req, nil, dir)
113+
if !ok {
114+
t.Fatal("resolveTestFormula returned ok=false (derivation should succeed from tox.ini)")
115+
}
116+
if len(cmd) == 0 || cmd[0] != "python" {
117+
t.Fatalf("command = %v, want the tox-derived command", cmd)
118+
}
119+
if prov["cwd"] != "tests" {
120+
t.Fatalf("healed cwd must overlay the DERIVED formula, got %v", prov)
121+
}
122+
if prov["no_project"] != "true" {
123+
t.Fatalf("derived provisioning must survive the overlay, got %v", prov)
124+
}
125+
}

0 commit comments

Comments
 (0)