From 1c2514eb8701ff10a14ddbcb39790f19641e5294 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:53:39 +0000 Subject: [PATCH] fix(#2869): inject FULLSEND_OUTPUT_SCHEMA into validation script env When a harness uses validation_loop.schema, the schema path is resolved and available at h.ValidationLoop.Schema but was not injected into the validation script's environment. Extract a validationEnv helper that conditionally adds FULLSEND_OUTPUT_SCHEMA when the schema path is set, fixing validation failures for harnesses that rely on the new field instead of env.runner.FULLSEND_OUTPUT_SCHEMA. Co-Authored-By: Claude Opus 4.6 --- internal/cli/run.go | 22 ++++++++++++++++------ internal/cli/run_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/internal/cli/run.go b/internal/cli/run.go index a793273fc..ca4f5a591 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -1106,12 +1106,7 @@ func runAgent(ctx context.Context, agentName, fullsendDir, outputBase, targetRep printer.StepStart("Running validation: " + h.ValidationLoop.Script) valCmd := exec.Command(h.ValidationLoop.Script) valCmd.Dir = iterDir - valCmd.Env = append(os.Environ(), - append(envToList(h.RunnerEnv), - fmt.Sprintf("TARGET_REPO_DIR=%s", hostRepositoryDir), - fmt.Sprintf("FULLSEND_RUN_DIR=%s", runDir), - )..., - ) + valCmd.Env = append(os.Environ(), validationEnv(h, hostRepositoryDir, runDir)...) valOut, valErr := valCmd.CombinedOutput() if valErr == nil { @@ -1698,6 +1693,21 @@ func childScriptEnv(runnerEnv map[string]string, traceparent string) []string { return env } +// validationEnv builds the extra environment entries for the validation +// script. It includes RunnerEnv, TARGET_REPO_DIR, FULLSEND_RUN_DIR, and — +// when the harness specifies a validation_loop.schema — FULLSEND_OUTPUT_SCHEMA +// pointing to the host-side cached schema path. +func validationEnv(h *harness.Harness, hostRepoDir, runDir string) []string { + env := append(envToList(h.RunnerEnv), + fmt.Sprintf("TARGET_REPO_DIR=%s", hostRepoDir), + fmt.Sprintf("FULLSEND_RUN_DIR=%s", runDir), + ) + if h.ValidationLoop != nil && h.ValidationLoop.Schema != "" { + env = append(env, fmt.Sprintf("FULLSEND_OUTPUT_SCHEMA=%s", h.ValidationLoop.Schema)) + } + return env +} + func envToList(env map[string]string) []string { keys := make([]string, 0, len(env)) for k := range env { diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index bdb09d95c..f3d9cf229 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -1393,6 +1393,46 @@ func TestValidationFailMessage_TrimsOutput(t *testing.T) { assert.Equal(t, "some output", msg) } +func TestValidationEnv_IncludesSchemaWhenSet(t *testing.T) { + h := &harness.Harness{ + RunnerEnv: map[string]string{"FOO": "bar"}, + ValidationLoop: &harness.ValidationLoop{ + Script: "scripts/validate.sh", + Schema: "/tmp/test-schema.json", + }, + } + env := validationEnv(h, "/repo", "/run") + assert.Contains(t, env, "FULLSEND_OUTPUT_SCHEMA=/tmp/test-schema.json") + assert.Contains(t, env, "TARGET_REPO_DIR=/repo") + assert.Contains(t, env, "FULLSEND_RUN_DIR=/run") + assert.Contains(t, env, "FOO=bar") +} + +func TestValidationEnv_OmitsSchemaWhenEmpty(t *testing.T) { + h := &harness.Harness{ + RunnerEnv: map[string]string{"FOO": "bar"}, + ValidationLoop: &harness.ValidationLoop{ + Script: "scripts/validate.sh", + }, + } + env := validationEnv(h, "/repo", "/run") + for _, e := range env { + assert.False(t, strings.HasPrefix(e, "FULLSEND_OUTPUT_SCHEMA="), + "FULLSEND_OUTPUT_SCHEMA should not be set when Schema is empty") + } +} + +func TestValidationEnv_OmitsSchemaWhenNoValidationLoop(t *testing.T) { + h := &harness.Harness{ + RunnerEnv: map[string]string{"FOO": "bar"}, + } + env := validationEnv(h, "/repo", "/run") + for _, e := range env { + assert.False(t, strings.HasPrefix(e, "FULLSEND_OUTPUT_SCHEMA="), + "FULLSEND_OUTPUT_SCHEMA should not be set when ValidationLoop is nil") + } +} + func TestOpenTeeReader_EmptyPath(t *testing.T) { src := strings.NewReader("hello") printer := ui.New(io.Discard)