Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .chunk/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@
"role": "precheck",
"fileExt": ".go",
"timeout": 300,
"limit": 3
"limit": 3,
"remote": true
},
{
"name": "lint",
"run": "task lint",
"role": "gate",
"fileExt": ".go",
"timeout": 60,
"limit": 3
"limit": 3,
"remote": true
},
{
"name": "format",
Expand All @@ -39,8 +41,9 @@
"repo": "chunk-cli"
},
"validation": {
"sidecarImage": "ac042eab-a45f-452e-a4f5-1d56e1b026bd"
"sidecarImage": "fb982185-d7bd-483d-a0f4-7204dbac62f3"
},
"orgID": "f22b6566-597d-46d5-ba74-99ef5bb3d85c",
"environment": {
"stack": "go",
"setup": [
Expand All @@ -59,6 +62,5 @@
],
"image": "cimg/go",
"image_version": "1.26.2"
},
"orgID": "f22b6566-597d-46d5-ba74-99ef5bb3d85c"
}
}
6 changes: 6 additions & 0 deletions acceptance/build_prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestBuildPromptHappyPath(t *testing.T) {
"build-prompt",
"--org", "test-org",
"--repos", "test-repo",
"--since", "2025-01-01",
"--top", "2",
"--output", filepath.Join(workDir, "review-prompt.md"),
}, env, workDir)
Expand Down Expand Up @@ -348,6 +349,7 @@ func TestBuildPromptBotFiltering(t *testing.T) {
"build-prompt",
"--org", "test-org",
"--repos", "test-repo",
"--since", "2025-01-01",
"--top", "5",
"--output", filepath.Join(workDir, "review-prompt.md"),
}, env, workDir)
Expand Down Expand Up @@ -491,6 +493,7 @@ func TestBuildPromptTopNFiltering(t *testing.T) {
"build-prompt",
"--org", "test-org",
"--repos", "test-repo",
"--since", "2025-01-01",
"--top", "2",
"--output", filepath.Join(workDir, "review-prompt.md"),
}, env, workDir)
Expand Down Expand Up @@ -575,6 +578,7 @@ func TestBuildPromptDefaultTop(t *testing.T) {
"build-prompt",
"--org", "test-org",
"--repos", "test-repo",
"--since", "2025-01-01",
"--output", filepath.Join(workDir, "review-prompt.md"),
}, env, workDir)

Expand Down Expand Up @@ -848,6 +852,7 @@ func TestBuildPromptTopOne(t *testing.T) {
"build-prompt",
"--org", "test-org",
"--repos", "test-repo",
"--since", "2025-01-01",
"--top", "1",
"--output", filepath.Join(workDir, "review-prompt.md"),
}, env, workDir)
Expand Down Expand Up @@ -902,6 +907,7 @@ func TestBuildPromptMaxCommentsEffect(t *testing.T) {
"build-prompt",
"--org", "test-org",
"--repos", "test-repo",
"--since", "2025-01-01",
"--max-comments", "1",
"--output", filepath.Join(workDir, "review-prompt.md"),
}, env, workDir)
Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func runValidate(ctx context.Context, client *circleci.Client, rc config.Resolve
if err != nil {
return err
}
return validate.RunRemote(ctx, execFn, cfg, name, dest, statusFn, streams)
return validate.RunRemote(ctx, execFn, cfg, name, workDir, dest, statusFn, streams)
}

// Per-command remote routing: commands with Remote:true go to the sidecar,
Expand All @@ -333,7 +333,7 @@ func runValidate(ctx context.Context, client *circleci.Client, rc config.Resolve
if err != nil {
return err
}
return validate.RunRemote(ctx, execFn, cfg, name, dest, statusFn, streams)
return validate.RunRemote(ctx, execFn, cfg, name, workDir, dest, statusFn, streams)
}
statusFn(iostream.LevelInfo, fmt.Sprintf("running %s locally (not marked remote)", name))
// Named command is not marked remote; fall through to local execution.
Expand Down Expand Up @@ -503,7 +503,7 @@ func runSplitCommands(ctx context.Context, client *circleci.Client, sidecarID st
streams.ErrPrintf("warning: %v (%q); run 'chunk sidecar env build' to set up the workspace; running %s locally instead\n", wsErr, dest, commandNames(remoteCfg.Commands))
localCfg.Commands = append(remoteCfg.Commands, localCfg.Commands...)
} else {
runErr = validate.RunRemote(ctx, execFn, remoteCfg, "", dest, statusFn, streams)
runErr = validate.RunRemote(ctx, execFn, remoteCfg, "", workDir, dest, statusFn, streams)
}
}
if len(localCfg.Commands) > 0 {
Expand Down
6 changes: 6 additions & 0 deletions internal/racefixture/racefixture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package racefixture

// Sum returns the sum of a and b.
func Sum(a, b int) int {
return a + b
}
9 changes: 9 additions & 0 deletions internal/racefixture/racefixture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package racefixture

import "testing"

func TestSum(t *testing.T) {
if got := Sum(1, 2); got != 3 {
t.Fatalf("Sum(1, 2) = %d, want 3", got)
}
}
11 changes: 8 additions & 3 deletions internal/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func RunDryRun(cfg *config.ProjectConfig, name string, status iostream.StatusFun

// RunRemote runs commands on a remote sidecar via SSH.
// If name is non-empty, only the named command is run.
func RunRemote(ctx context.Context, execFn func(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error), cfg *config.ProjectConfig, name, dest string, status iostream.StatusFunc, streams iostream.Streams) error {
func RunRemote(ctx context.Context, execFn func(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error), cfg *config.ProjectConfig, name, workDir, dest string, status iostream.StatusFunc, streams iostream.Streams) error {
commands := cfg.Commands
if name != "" {
c := cfg.FindCommand(name)
Expand All @@ -118,8 +118,13 @@ func RunRemote(ctx context.Context, execFn func(ctx context.Context, script stri
commands = []config.Command{*c}
}
for _, c := range commands {
script := "cd " + shellEscape(dest) + " && " + c.Run
status(iostream.LevelInfo, fmt.Sprintf("Running %s (remote): %s", c.Name, c.Run))
// Expand template variables (e.g. {{CHANGED_PACKAGES}}) against the
// local working tree before sending the command to the sidecar; the
// synced remote tree mirrors it. Without this the literal template
// reaches the remote shell and tools like go-task fail to parse it.
run := expandCommand(workDir, c.Run)
script := "cd " + shellEscape(dest) + " && " + run
status(iostream.LevelInfo, fmt.Sprintf("Running %s (remote): %s", c.Name, run))
stdout, stderr, exitCode, err := execFn(ctx, script)
if err != nil {
return fmt.Errorf("remote %s: %w", c.Name, err)
Expand Down
38 changes: 29 additions & 9 deletions internal/validate/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ func TestRunRemote(t *testing.T) {
}}
streams, out, _ := newStreams()

assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", "/workspace", func(iostream.Level, string) {}, streams))
assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", "", "/workspace", func(iostream.Level, string) {}, streams))
assert.Assert(t, strings.Contains(out.String(), "remote output"), "got: %s", out.String())
assert.Equal(t, execCount, 2)
})
Expand All @@ -296,7 +296,7 @@ func TestRunRemote(t *testing.T) {
}}
streams, _, _ := newStreams()

err := RunRemote(context.Background(), execFn, cfg, "", "/workspace", func(iostream.Level, string) {}, streams)
err := RunRemote(context.Background(), execFn, cfg, "", "", "/workspace", func(iostream.Level, string) {}, streams)
assert.ErrorContains(t, err, "remote test failed")
})

Expand All @@ -310,7 +310,7 @@ func TestRunRemote(t *testing.T) {
}}
streams, out, _ := newStreams()

assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", "/workspace", func(iostream.Level, string) {}, streams))
assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", "", "/workspace", func(iostream.Level, string) {}, streams))
assert.Equal(t, out.Len(), 0)
})

Expand All @@ -327,7 +327,7 @@ func TestRunRemote(t *testing.T) {
}}
streams, _, _ := newStreams()

assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "test", "/workspace", func(iostream.Level, string) {}, streams))
assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "test", "", "/workspace", func(iostream.Level, string) {}, streams))
assert.Equal(t, len(capturedScripts), 1)
assert.Assert(t, strings.Contains(capturedScripts[0], "echo test"), "got: %s", capturedScripts[0])
})
Expand All @@ -342,7 +342,7 @@ func TestRunRemote(t *testing.T) {
}}
streams, _, _ := newStreams()

err := RunRemote(context.Background(), execFn, cfg, "lint", "/workspace", func(iostream.Level, string) {}, streams)
err := RunRemote(context.Background(), execFn, cfg, "lint", "", "/workspace", func(iostream.Level, string) {}, streams)
assert.ErrorContains(t, err, `"lint" not configured`)
})

Expand All @@ -358,9 +358,29 @@ func TestRunRemote(t *testing.T) {
}}
streams, _, _ := newStreams()

assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", "/custom/path", func(iostream.Level, string) {}, streams))
assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", "", "/custom/path", func(iostream.Level, string) {}, streams))
assert.Assert(t, strings.HasPrefix(capturedScript, "cd '/custom/path' &&"), "got: %s", capturedScript)
})

t.Run("expands CHANGED_PACKAGES before sending remotely", func(t *testing.T) {
var capturedScript string
execFn := func(_ context.Context, script string) (string, string, int, error) {
capturedScript = script
return "", "", 0, nil
}

cfg := &config.ProjectConfig{Commands: []config.Command{
{Name: "test-changed", Run: "task test -- {{CHANGED_PACKAGES}}"},
}}
streams, _, _ := newStreams()

// workDir is not a git repo, so expansion falls back to "./...".
// The literal template must never reach the remote shell, where
// tools like go-task would fail to parse it.
assert.NilError(t, RunRemote(context.Background(), execFn, cfg, "", t.TempDir(), "/workspace", func(iostream.Level, string) {}, streams))
assert.Assert(t, strings.Contains(capturedScript, "task test -- ./..."), "got: %s", capturedScript)
assert.Assert(t, !strings.Contains(capturedScript, "{{CHANGED_PACKAGES}}"), "template should be expanded, got: %s", capturedScript)
})
}

// TestRunRemoteSSH tests RunRemote end-to-end with a real fake SSH server,
Expand Down Expand Up @@ -404,7 +424,7 @@ func TestRunRemoteSSH(t *testing.T) {
}}
streams, out, _ := newStreams()

assert.NilError(t, RunRemote(context.Background(), execCallback(t, session), cfg, "", "/workspace/repo", func(iostream.Level, string) {}, streams))
assert.NilError(t, RunRemote(context.Background(), execCallback(t, session), cfg, "", "", "/workspace/repo", func(iostream.Level, string) {}, streams))
assert.Assert(t, strings.Contains(out.String(), "hello from remote"), "got: %s", out.String())
assert.Equal(t, len(sshSrv.Commands()), 1)
})
Expand All @@ -429,7 +449,7 @@ func TestRunRemoteSSH(t *testing.T) {
}}
streams, _, _ := newStreams()

err = RunRemote(context.Background(), execCallback(t, session), cfg, "", "/workspace/repo", func(iostream.Level, string) {}, streams)
err = RunRemote(context.Background(), execCallback(t, session), cfg, "", "", "/workspace/repo", func(iostream.Level, string) {}, streams)
assert.ErrorContains(t, err, "remote test failed")
})

Expand All @@ -454,7 +474,7 @@ func TestRunRemoteSSH(t *testing.T) {
}}
streams, _, _ := newStreams()

err = RunRemote(context.Background(), execCallback(t, session), cfg, "", "/workspace/repo", func(iostream.Level, string) {}, streams)
err = RunRemote(context.Background(), execCallback(t, session), cfg, "", "", "/workspace/repo", func(iostream.Level, string) {}, streams)
assert.ErrorContains(t, err, "remote install failed")
assert.Equal(t, len(sshSrv.Commands()), 1)
})
Expand Down