diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 1a67827..fd9437f 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -544,6 +544,9 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques } s.cloneWorkspaceDeps(ctx, workspaceDir, repoDir, input.Org) + if err := s.runWorkspaceLanguagePrep(ctx, workspaceDir, repoDir); err != nil { + return nil, PrepOutput{}, err + } docsDir := core.JoinPath(workspaceDir, ".core", "reference", "docs") if !fs.IsDir(docsDir) { @@ -696,6 +699,39 @@ func (s *PrepSubsystem) buildPrompt(ctx context.Context, input PrepInput, branch return b.String(), memories, consumers } +// runWorkspaceLanguagePrep installs repository dependencies before the agent starts. +// +// _ = s.runWorkspaceLanguagePrep(ctx, "/srv/.core/workspace/core/go-io/task-42", "/srv/Code/core/go-io") +func (s *PrepSubsystem) runWorkspaceLanguagePrep(ctx context.Context, workspaceDir, repoDir string) error { + process := s.Core().Process() + + if fs.IsFile(core.JoinPath(repoDir, "go.mod")) { + if result := process.RunIn(ctx, repoDir, "go", "mod", "download"); !result.OK { + return core.E("prepWorkspace", "go mod download failed", nil) + } + } + + if fs.IsFile(core.JoinPath(repoDir, "go.mod")) && (fs.IsFile(core.JoinPath(workspaceDir, "go.work")) || fs.IsFile(core.JoinPath(repoDir, "go.work"))) { + if result := process.RunIn(ctx, repoDir, "go", "work", "sync"); !result.OK { + return core.E("prepWorkspace", "go work sync failed", nil) + } + } + + if fs.IsFile(core.JoinPath(repoDir, "composer.json")) { + if result := process.RunIn(ctx, repoDir, "composer", "install"); !result.OK { + return core.E("prepWorkspace", "composer install failed", nil) + } + } + + if fs.IsFile(core.JoinPath(repoDir, "package.json")) { + if result := process.RunIn(ctx, repoDir, "npm", "install"); !result.OK { + return core.E("prepWorkspace", "npm install failed", nil) + } + } + + return nil +} + func (s *PrepSubsystem) getIssueBody(ctx context.Context, org, repo string, issue int) string { idx := core.Sprintf("%d", issue) iss, err := s.forge.Issues.Get(ctx, forge.Params{"owner": org, "repo": repo, "index": idx}) diff --git a/pkg/agentic/prep_extra_test.go b/pkg/agentic/prep_extra_test.go index eacdc47..0e5d7df 100644 --- a/pkg/agentic/prep_extra_test.go +++ b/pkg/agentic/prep_extra_test.go @@ -6,12 +6,14 @@ import ( "context" "net/http" "net/http/httptest" + "os" "testing" "time" core "dappco.re/go/core" "dappco.re/go/core/forge" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // --- Shutdown --- @@ -115,6 +117,122 @@ func TestPrep_FindConsumersList_Bad_NoGoWork(t *testing.T) { assert.Empty(t, list) } +func writeFakeLanguageCommand(t *testing.T, dir, name, logPath string, exitCode int) { + t.Helper() + + script := core.Concat( + "#!/bin/sh\n", + "printf '%s %s\\n' '", + name, + "' \"$*\" >> '", + logPath, + "'\n", + "exit ", + core.Sprint(exitCode), + "\n", + ) + require.True(t, fs.WriteMode(core.JoinPath(dir, name), script, 0755).OK) +} + +// --- runWorkspaceLanguagePrep --- + +func TestPrep_RunWorkspaceLanguagePrep_Good_Polyglot(t *testing.T) { + root := t.TempDir() + binDir := core.JoinPath(root, "bin") + require.True(t, fs.EnsureDir(binDir).OK) + + logPath := core.JoinPath(root, "commands.log") + writeFakeLanguageCommand(t, binDir, "go", logPath, 0) + writeFakeLanguageCommand(t, binDir, "composer", logPath, 0) + writeFakeLanguageCommand(t, binDir, "npm", logPath, 0) + + oldPath := os.Getenv("PATH") + t.Setenv("PATH", core.Concat(binDir, ":", oldPath)) + + workspaceDir := core.JoinPath(root, "workspace") + repoDir := core.JoinPath(root, "repo") + require.True(t, fs.EnsureDir(workspaceDir).OK) + require.True(t, fs.EnsureDir(repoDir).OK) + require.True(t, fs.Write(core.JoinPath(repoDir, "go.mod"), "module example.com/test\n").OK) + require.True(t, fs.Write(core.JoinPath(repoDir, "composer.json"), "{}").OK) + require.True(t, fs.Write(core.JoinPath(repoDir, "package.json"), "{}").OK) + require.True(t, fs.Write(core.JoinPath(workspaceDir, "go.work"), "go 1.22\n").OK) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + err := s.runWorkspaceLanguagePrep(context.Background(), workspaceDir, repoDir) + require.NoError(t, err) + + logResult := fs.Read(logPath) + require.True(t, logResult.OK) + assert.Equal(t, "go mod download\ngo work sync\ncomposer install\nnpm install\n", logResult.Value.(string)) +} + +func TestPrep_RunWorkspaceLanguagePrep_Bad_NoLanguageManifests(t *testing.T) { + root := t.TempDir() + binDir := core.JoinPath(root, "bin") + require.True(t, fs.EnsureDir(binDir).OK) + + logPath := core.JoinPath(root, "commands.log") + writeFakeLanguageCommand(t, binDir, "go", logPath, 0) + writeFakeLanguageCommand(t, binDir, "composer", logPath, 0) + writeFakeLanguageCommand(t, binDir, "npm", logPath, 0) + + oldPath := os.Getenv("PATH") + t.Setenv("PATH", core.Concat(binDir, ":", oldPath)) + + workspaceDir := core.JoinPath(root, "workspace") + repoDir := core.JoinPath(root, "repo") + require.True(t, fs.EnsureDir(workspaceDir).OK) + require.True(t, fs.EnsureDir(repoDir).OK) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + err := s.runWorkspaceLanguagePrep(context.Background(), workspaceDir, repoDir) + require.NoError(t, err) + + logResult := fs.Read(logPath) + assert.False(t, logResult.OK) +} + +func TestPrep_RunWorkspaceLanguagePrep_Ugly_CommandFailure(t *testing.T) { + root := t.TempDir() + binDir := core.JoinPath(root, "bin") + require.True(t, fs.EnsureDir(binDir).OK) + + logPath := core.JoinPath(root, "commands.log") + writeFakeLanguageCommand(t, binDir, "go", logPath, 0) + writeFakeLanguageCommand(t, binDir, "composer", logPath, 1) + writeFakeLanguageCommand(t, binDir, "npm", logPath, 0) + + oldPath := os.Getenv("PATH") + t.Setenv("PATH", core.Concat(binDir, ":", oldPath)) + + workspaceDir := core.JoinPath(root, "workspace") + repoDir := core.JoinPath(root, "repo") + require.True(t, fs.EnsureDir(workspaceDir).OK) + require.True(t, fs.EnsureDir(repoDir).OK) + require.True(t, fs.Write(core.JoinPath(repoDir, "composer.json"), "{}").OK) + + s := &PrepSubsystem{ + ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + err := s.runWorkspaceLanguagePrep(context.Background(), workspaceDir, repoDir) + require.Error(t, err) + assert.Contains(t, err.Error(), "composer install failed") +} + // --- pullWikiContent --- func TestPrep_PullWikiContent_Good_WithPages(t *testing.T) {