diff --git a/services/actions.go b/services/actions.go index 1e004fd..c64464a 100644 --- a/services/actions.go +++ b/services/actions.go @@ -29,6 +29,8 @@ const ( ActionGithubPRReader = "github-pr-reader" ActionGithubPRCommenter = "github-pr-commenter" ActionGithubPRReviewer = "github-pr-reviewer" + ActionGithubPRCreator = "github-pr-creator" + ActionGithubGetAllContent = "github-get-all-repository-content" ActionGithubREADME = "github-readme" ActionScraper = "scraper" ActionWikipedia = "wikipedia" @@ -49,12 +51,14 @@ var AvailableActions = []string{ ActionGithubIssueCloser, ActionGithubIssueSearcher, ActionGithubRepositoryGet, + ActionGithubGetAllContent, ActionGithubRepositoryCreateOrUpdate, ActionGithubIssueReader, ActionGithubIssueCommenter, ActionGithubPRReader, ActionGithubPRCommenter, ActionGithubPRReviewer, + ActionGithubPRCreator, ActionGithubREADME, ActionScraper, ActionBrowse, @@ -118,6 +122,10 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP a = actions.NewGithubPRCommenter(config) case ActionGithubPRReviewer: a = actions.NewGithubPRReviewer(config) + case ActionGithubPRCreator: + a = actions.NewGithubPRCreator(config) + case ActionGithubGetAllContent: + a = actions.NewGithubRepositoryGetAllContent(config) case ActionGithubIssueCommenter: a = actions.NewGithubIssueCommenter(config) case ActionGithubRepositoryGet: @@ -201,6 +209,11 @@ func ActionsConfigMeta() []config.FieldGroup { Label: "GitHub Repository Get Content", Fields: actions.GithubRepositoryGetContentConfigMeta(), }, + { + Name: "github-get-all-repository-content", + Label: "GitHub Get All Repository Content", + Fields: actions.GithubRepositoryGetAllContentConfigMeta(), + }, { Name: "github-repository-create-or-update-content", Label: "GitHub Repository Create/Update Content", @@ -226,6 +239,11 @@ func ActionsConfigMeta() []config.FieldGroup { Label: "GitHub PR Reviewer", Fields: actions.GithubPRReviewerConfigMeta(), }, + { + Name: "github-pr-creator", + Label: "GitHub PR Creator", + Fields: actions.GithubPRCreatorConfigMeta(), + }, { Name: "twitter-post", Label: "Twitter Post", diff --git a/services/actions/githubprcreator.go b/services/actions/githubprcreator.go new file mode 100644 index 0000000..909f0e8 --- /dev/null +++ b/services/actions/githubprcreator.go @@ -0,0 +1,315 @@ +package actions + +import ( + "context" + "fmt" + + "github.com/google/go-github/v69/github" + "github.com/mudler/LocalAGI/core/types" + "github.com/mudler/LocalAGI/pkg/config" + "github.com/sashabaranov/go-openai/jsonschema" +) + +type GithubPRCreator struct { + token, repository, owner, customActionName, defaultBranch string + client *github.Client +} + +func NewGithubPRCreator(config map[string]string) *GithubPRCreator { + client := github.NewClient(nil).WithAuthToken(config["token"]) + + return &GithubPRCreator{ + client: client, + token: config["token"], + repository: config["repository"], + owner: config["owner"], + customActionName: config["customActionName"], + defaultBranch: config["defaultBranch"], + } +} + +func (g *GithubPRCreator) createOrUpdateBranch(ctx context.Context, branchName string) error { + // Get the latest commit SHA from the default branch + ref, _, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+g.defaultBranch) + if err != nil { + return fmt.Errorf("failed to get reference: %w", err) + } + + // Try to get the branch if it exists + _, resp, err := g.client.Git.GetRef(ctx, g.owner, g.repository, "refs/heads/"+branchName) + if err != nil { + // If branch doesn't exist, create it + if resp != nil && resp.StatusCode == 404 { + newRef := &github.Reference{ + Ref: github.String("refs/heads/" + branchName), + Object: &github.GitObject{SHA: ref.Object.SHA}, + } + _, _, err = g.client.Git.CreateRef(ctx, g.owner, g.repository, newRef) + if err != nil { + return fmt.Errorf("failed to create branch: %w", err) + } + return nil + } + return fmt.Errorf("failed to check branch existence: %w", err) + } + + // Branch exists, update it to the latest commit + updateRef := &github.Reference{ + Ref: github.String("refs/heads/" + branchName), + Object: &github.GitObject{SHA: ref.Object.SHA}, + } + _, _, err = g.client.Git.UpdateRef(ctx, g.owner, g.repository, updateRef, true) + if err != nil { + return fmt.Errorf("failed to update branch: %w", err) + } + + return nil +} + +func (g *GithubPRCreator) createOrUpdateFile(ctx context.Context, branch string, filePath string, content string, message string) error { + // Get the current file content if it exists + var sha *string + fileContent, _, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, filePath, &github.RepositoryContentGetOptions{ + Ref: branch, + }) + if err == nil && fileContent != nil { + sha = fileContent.SHA + } + + // Create or update the file + _, _, err = g.client.Repositories.CreateFile(ctx, g.owner, g.repository, filePath, &github.RepositoryContentFileOptions{ + Message: &message, + Content: []byte(content), + Branch: &branch, + SHA: sha, + }) + if err != nil { + return fmt.Errorf("failed to create/update file: %w", err) + } + + return nil +} + +func (g *GithubPRCreator) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { + result := struct { + Repository string `json:"repository"` + Owner string `json:"owner"` + Branch string `json:"branch"` + Title string `json:"title"` + Body string `json:"body"` + BaseBranch string `json:"base_branch"` + Files []struct { + Path string `json:"path"` + Content string `json:"content"` + } `json:"files"` + }{} + err := params.Unmarshal(&result) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err) + } + + if g.repository != "" && g.owner != "" { + result.Repository = g.repository + result.Owner = g.owner + } + + if result.BaseBranch == "" { + result.BaseBranch = g.defaultBranch + } + + // Create or update branch + err = g.createOrUpdateBranch(ctx, result.Branch) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to create/update branch: %w", err) + } + + // Create or update files + for _, file := range result.Files { + err = g.createOrUpdateFile(ctx, result.Branch, file.Path, file.Content, fmt.Sprintf("Update %s", file.Path)) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to update file %s: %w", file.Path, err) + } + } + + // Check if PR already exists for this branch + prs, _, err := g.client.PullRequests.List(ctx, result.Owner, result.Repository, &github.PullRequestListOptions{ + State: "open", + Head: fmt.Sprintf("%s:%s", result.Owner, result.Branch), + }) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to list pull requests: %w", err) + } + + if len(prs) > 0 { + // Update existing PR + pr := prs[0] + update := &github.PullRequest{ + Title: &result.Title, + Body: &result.Body, + } + updatedPR, _, err := g.client.PullRequests.Edit(ctx, result.Owner, result.Repository, pr.GetNumber(), update) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to update pull request: %w", err) + } + return types.ActionResult{ + Result: fmt.Sprintf("Updated pull request #%d: %s", updatedPR.GetNumber(), updatedPR.GetHTMLURL()), + }, nil + } + + // Create new pull request + newPR := &github.NewPullRequest{ + Title: &result.Title, + Body: &result.Body, + Head: &result.Branch, + Base: &result.BaseBranch, + } + + createdPR, _, err := g.client.PullRequests.Create(ctx, result.Owner, result.Repository, newPR) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to create pull request: %w", err) + } + + return types.ActionResult{ + Result: fmt.Sprintf("Created pull request #%d: %s", createdPR.GetNumber(), createdPR.GetHTMLURL()), + }, nil +} + +func (g *GithubPRCreator) Definition() types.ActionDefinition { + actionName := "create_github_pr" + if g.customActionName != "" { + actionName = g.customActionName + } + description := "Create a GitHub pull request with file changes" + if g.repository != "" && g.owner != "" && g.defaultBranch != "" { + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "branch": { + Type: jsonschema.String, + Description: "The name of the new branch to create", + }, + "title": { + Type: jsonschema.String, + Description: "The title of the pull request", + }, + "body": { + Type: jsonschema.String, + Description: "The body/description of the pull request", + }, + "files": { + Type: jsonschema.Array, + Items: &jsonschema.Definition{ + Type: jsonschema.Object, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "The path of the file to create/update", + }, + "content": { + Type: jsonschema.String, + Description: "The content of the file", + }, + }, + Required: []string{"path", "content"}, + }, + Description: "Array of files to create or update", + }, + }, + Required: []string{"branch", "title", "files"}, + } + } + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "branch": { + Type: jsonschema.String, + Description: "The name of the new branch to create", + }, + "title": { + Type: jsonschema.String, + Description: "The title of the pull request", + }, + "body": { + Type: jsonschema.String, + Description: "The body/description of the pull request", + }, + "base_branch": { + Type: jsonschema.String, + Description: "The base branch to merge into (defaults to configured default branch)", + }, + "files": { + Type: jsonschema.Array, + Items: &jsonschema.Definition{ + Type: jsonschema.Object, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "The path of the file to create/update", + }, + "content": { + Type: jsonschema.String, + Description: "The content of the file", + }, + }, + Required: []string{"path", "content"}, + }, + Description: "Array of files to create or update", + }, + "repository": { + Type: jsonschema.String, + Description: "The repository to create the pull request in", + }, + "owner": { + Type: jsonschema.String, + Description: "The owner of the repository", + }, + }, + Required: []string{"branch", "title", "files", "repository", "owner"}, + } +} + +func (a *GithubPRCreator) Plannable() bool { + return true +} + +// GithubPRCreatorConfigMeta returns the metadata for GitHub PR Creator action configuration fields +func GithubPRCreatorConfigMeta() []config.Field { + return []config.Field{ + { + Name: "token", + Label: "GitHub Token", + Type: config.FieldTypeText, + Required: true, + HelpText: "GitHub API token with repository access", + }, + { + Name: "repository", + Label: "Repository", + Type: config.FieldTypeText, + Required: false, + HelpText: "GitHub repository name", + }, + { + Name: "owner", + Label: "Owner", + Type: config.FieldTypeText, + Required: false, + HelpText: "GitHub repository owner", + }, + { + Name: "customActionName", + Label: "Custom Action Name", + Type: config.FieldTypeText, + HelpText: "Custom name for this action", + }, + { + Name: "defaultBranch", + Label: "Default Branch", + Type: config.FieldTypeText, + Required: false, + HelpText: "Default branch to create PRs against (defaults to main)", + }, + } +} diff --git a/services/actions/githubprcreator_test.go b/services/actions/githubprcreator_test.go new file mode 100644 index 0000000..c96c9f6 --- /dev/null +++ b/services/actions/githubprcreator_test.go @@ -0,0 +1,118 @@ +package actions_test + +import ( + "context" + "os" + + "github.com/mudler/LocalAGI/services/actions" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GithubPRCreator", func() { + var ( + action *actions.GithubPRCreator + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + + // Check for required environment variables + token := os.Getenv("GITHUB_TOKEN") + repo := os.Getenv("TEST_REPOSITORY") + owner := os.Getenv("TEST_OWNER") + + // Skip tests if any required environment variable is missing + if token == "" || repo == "" || owner == "" { + Skip("Skipping GitHub PR creator tests: required environment variables not set") + } + + config := map[string]string{ + "token": token, + "repository": repo, + "owner": owner, + "customActionName": "test_create_pr", + "defaultBranch": "main", + } + + action = actions.NewGithubPRCreator(config) + }) + + Describe("Creating pull requests", func() { + It("should successfully create a pull request with file changes", func() { + params := map[string]interface{}{ + "branch": "test-branch", + "title": "Test PR", + "body": "This is a test pull request", + "base_branch": "main", + "files": []map[string]interface{}{ + { + "path": "test.txt", + "content": "This is a test file", + }, + }, + } + + result, err := action.Run(ctx, params) + Expect(err).NotTo(HaveOccurred()) + Expect(result.Result).To(ContainSubstring("pull request #")) + }) + + It("should handle missing required fields", func() { + params := map[string]interface{}{ + "title": "Test PR", + "body": "This is a test pull request", + } + + _, err := action.Run(ctx, params) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("Action Definition", func() { + It("should return correct action definition", func() { + def := action.Definition() + Expect(def.Name.String()).To(Equal("test_create_pr")) + Expect(def.Description).To(ContainSubstring("Create a GitHub pull request with file changes")) + Expect(def.Properties).To(HaveKey("branch")) + Expect(def.Properties).To(HaveKey("title")) + Expect(def.Properties).To(HaveKey("files")) + }) + + It("should handle custom action name", func() { + config := map[string]string{ + "token": "test-token", + "customActionName": "custom_action_name", + } + action := actions.NewGithubPRCreator(config) + def := action.Definition() + Expect(def.Name.String()).To(Equal("custom_action_name")) + }) + }) + + Describe("Configuration", func() { + It("should handle missing repository and owner in config", func() { + config := map[string]string{ + "token": "test-token", + } + action := actions.NewGithubPRCreator(config) + def := action.Definition() + Expect(def.Properties).To(HaveKey("repository")) + Expect(def.Properties).To(HaveKey("owner")) + }) + + It("should handle provided repository and owner in config", func() { + config := map[string]string{ + "token": "test-token", + "repository": "test-repo", + "defaultBranch": "main", + "owner": "test-owner", + } + action := actions.NewGithubPRCreator(config) + def := action.Definition() + Expect(def.Properties).NotTo(HaveKey("repository")) + Expect(def.Properties).NotTo(HaveKey("owner")) + }) + }) +}) diff --git a/services/actions/githubprreviewer_test.go b/services/actions/githubprreviewer_test.go new file mode 100644 index 0000000..99b294d --- /dev/null +++ b/services/actions/githubprreviewer_test.go @@ -0,0 +1,103 @@ +package actions_test + +import ( + "context" + "os" + + "github.com/mudler/LocalAGI/core/types" + "github.com/mudler/LocalAGI/services/actions" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GithubPRReviewer", func() { + var ( + reviewer *actions.GithubPRReviewer + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + + // Check for required environment variables + token := os.Getenv("GITHUB_TOKEN") + repo := os.Getenv("TEST_REPOSITORY") + owner := os.Getenv("TEST_OWNER") + prNumber := os.Getenv("TEST_PR_NUMBER") + + // Skip tests if any required environment variable is missing + if token == "" || repo == "" || owner == "" || prNumber == "" { + Skip("Skipping GitHub PR reviewer tests: required environment variables not set") + } + + config := map[string]string{ + "token": token, + "repository": repo, + "owner": owner, + "customActionName": "test_review_github_pr", + } + + reviewer = actions.NewGithubPRReviewer(config) + }) + + Describe("Reviewing a PR", func() { + It("should successfully submit a review with comments", func() { + prNumber := os.Getenv("TEST_PR_NUMBER") + Expect(prNumber).NotTo(BeEmpty()) + + params := types.ActionParams{ + "pr_number": prNumber, + "review_comment": "Test review comment from integration test", + "review_action": "COMMENT", + "comments": []map[string]interface{}{ + { + "file": "README.md", + "line": 1, + "comment": "Test line comment from integration test", + }, + }, + } + + result, err := reviewer.Run(ctx, params) + Expect(err).NotTo(HaveOccurred()) + Expect(result.Result).To(ContainSubstring("reviewed successfully")) + }) + + It("should handle invalid PR number", func() { + params := types.ActionParams{ + "pr_number": 999999, + "review_comment": "Test review comment", + "review_action": "COMMENT", + } + + result, err := reviewer.Run(ctx, params) + Expect(err).To(HaveOccurred()) + Expect(result.Result).To(ContainSubstring("not found")) + }) + + It("should handle invalid review action", func() { + prNumber := os.Getenv("TEST_PR_NUMBER") + Expect(prNumber).NotTo(BeEmpty()) + + params := types.ActionParams{ + "pr_number": prNumber, + "review_comment": "Test review comment", + "review_action": "INVALID_ACTION", + } + + _, err := reviewer.Run(ctx, params) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("Action Definition", func() { + It("should return correct action definition", func() { + def := reviewer.Definition() + Expect(def.Name).To(Equal(types.ActionDefinitionName("test_review_github_pr"))) + Expect(def.Description).To(ContainSubstring("Review a GitHub pull request")) + Expect(def.Properties).To(HaveKey("pr_number")) + Expect(def.Properties).To(HaveKey("review_action")) + Expect(def.Properties).To(HaveKey("comments")) + }) + }) +}) diff --git a/services/actions/githubrepositorygetallcontent.go b/services/actions/githubrepositorygetallcontent.go new file mode 100644 index 0000000..2cb844f --- /dev/null +++ b/services/actions/githubrepositorygetallcontent.go @@ -0,0 +1,174 @@ +package actions + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-github/v69/github" + "github.com/mudler/LocalAGI/core/types" + "github.com/mudler/LocalAGI/pkg/config" + "github.com/sashabaranov/go-openai/jsonschema" +) + +type GithubRepositoryGetAllContent struct { + token, repository, owner, customActionName string + client *github.Client +} + +func NewGithubRepositoryGetAllContent(config map[string]string) *GithubRepositoryGetAllContent { + client := github.NewClient(nil).WithAuthToken(config["token"]) + + return &GithubRepositoryGetAllContent{ + client: client, + token: config["token"], + repository: config["repository"], + owner: config["owner"], + customActionName: config["customActionName"], + } +} + +func (g *GithubRepositoryGetAllContent) getContentRecursively(ctx context.Context, path string) (string, error) { + var result strings.Builder + + // Get content at the current path + _, directoryContent, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, path, nil) + if err != nil { + return "", fmt.Errorf("error getting content at path %s: %w", path, err) + } + + // Process each item in the directory + for _, item := range directoryContent { + if item.GetType() == "dir" { + // Recursively get content for subdirectories + subContent, err := g.getContentRecursively(ctx, item.GetPath()) + if err != nil { + return "", err + } + result.WriteString(subContent) + } else if item.GetType() == "file" { + // Get file content + fileContent, _, _, err := g.client.Repositories.GetContents(ctx, g.owner, g.repository, item.GetPath(), nil) + if err != nil { + return "", fmt.Errorf("error getting file content for %s: %w", item.GetPath(), err) + } + + content, err := fileContent.GetContent() + if err != nil { + return "", fmt.Errorf("error decoding content for %s: %w", item.GetPath(), err) + } + + // Add file content to result with clear markers + result.WriteString(fmt.Sprintf("\n--- START FILE: %s ---\n", item.GetPath())) + result.WriteString(content) + result.WriteString(fmt.Sprintf("\n--- END FILE: %s ---\n", item.GetPath())) + } + } + + return result.String(), nil +} + +func (g *GithubRepositoryGetAllContent) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { + result := struct { + Repository string `json:"repository"` + Owner string `json:"owner"` + Path string `json:"path,omitempty"` + }{} + err := params.Unmarshal(&result) + if err != nil { + return types.ActionResult{}, fmt.Errorf("failed to unmarshal params: %w", err) + } + + if g.repository != "" && g.owner != "" { + result.Repository = g.repository + result.Owner = g.owner + } + + // Start from root if no path specified + if result.Path == "" { + result.Path = "." + } + + content, err := g.getContentRecursively(ctx, result.Path) + if err != nil { + return types.ActionResult{}, err + } + + return types.ActionResult{Result: content}, nil +} + +func (g *GithubRepositoryGetAllContent) Definition() types.ActionDefinition { + actionName := "get_all_github_repository_content" + if g.customActionName != "" { + actionName = g.customActionName + } + description := "Get all content of a GitHub repository recursively" + if g.repository != "" && g.owner != "" { + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "Optional path to start from (defaults to repository root)", + }, + }, + } + } + return types.ActionDefinition{ + Name: types.ActionDefinitionName(actionName), + Description: description, + Properties: map[string]jsonschema.Definition{ + "path": { + Type: jsonschema.String, + Description: "Optional path to start from (defaults to repository root)", + }, + "repository": { + Type: jsonschema.String, + Description: "The repository to get content from", + }, + "owner": { + Type: jsonschema.String, + Description: "The owner of the repository", + }, + }, + Required: []string{"repository", "owner"}, + } +} + +func (a *GithubRepositoryGetAllContent) Plannable() bool { + return true +} + +// GithubRepositoryGetAllContentConfigMeta returns the metadata for GitHub Repository Get All Content action configuration fields +func GithubRepositoryGetAllContentConfigMeta() []config.Field { + return []config.Field{ + { + Name: "token", + Label: "GitHub Token", + Type: config.FieldTypeText, + Required: true, + HelpText: "GitHub API token with repository access", + }, + { + Name: "repository", + Label: "Repository", + Type: config.FieldTypeText, + Required: false, + HelpText: "GitHub repository name", + }, + { + Name: "owner", + Label: "Owner", + Type: config.FieldTypeText, + Required: false, + HelpText: "GitHub repository owner", + }, + { + Name: "customActionName", + Label: "Custom Action Name", + Type: config.FieldTypeText, + HelpText: "Custom name for this action", + }, + } +} diff --git a/services/actions/githubrepositorygetallcontent_test.go b/services/actions/githubrepositorygetallcontent_test.go new file mode 100644 index 0000000..61effd4 --- /dev/null +++ b/services/actions/githubrepositorygetallcontent_test.go @@ -0,0 +1,114 @@ +package actions_test + +import ( + "context" + "os" + "strings" + + "github.com/mudler/LocalAGI/services/actions" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("GithubRepositoryGetAllContent", func() { + var ( + action *actions.GithubRepositoryGetAllContent + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + + // Check for required environment variables + token := os.Getenv("GITHUB_TOKEN") + repo := os.Getenv("TEST_REPOSITORY") + owner := os.Getenv("TEST_OWNER") + + // Skip tests if any required environment variable is missing + if token == "" || repo == "" || owner == "" { + Skip("Skipping GitHub repository get all content tests: required environment variables not set") + } + + config := map[string]string{ + "token": token, + "repository": repo, + "owner": owner, + "customActionName": "test_get_all_content", + } + + action = actions.NewGithubRepositoryGetAllContent(config) + }) + + Describe("Getting repository content", func() { + It("should successfully get content from root directory with proper file markers", func() { + params := map[string]interface{}{ + "path": ".", + } + + result, err := action.Run(ctx, params) + Expect(err).NotTo(HaveOccurred()) + Expect(result.Result).NotTo(BeEmpty()) + + // Verify file markers + Expect(result.Result).To(ContainSubstring("--- START FILE:")) + Expect(result.Result).To(ContainSubstring("--- END FILE:")) + + // Verify markers are properly paired + startCount := strings.Count(result.Result, "--- START FILE:") + endCount := strings.Count(result.Result, "--- END FILE:") + Expect(startCount).To(Equal(endCount), "Number of start and end markers should match") + }) + + It("should handle non-existent path", func() { + params := map[string]interface{}{ + "path": "non-existent-path", + } + + _, err := action.Run(ctx, params) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("Action Definition", func() { + It("should return correct action definition", func() { + def := action.Definition() + Expect(def.Name.String()).To(Equal("test_get_all_content")) + Expect(def.Description).To(ContainSubstring("Get all content of a GitHub repository recursively")) + Expect(def.Properties).To(HaveKey("path")) + }) + + It("should handle custom action name", func() { + config := map[string]string{ + "token": "test-token", + "customActionName": "custom_action_name", + } + action := actions.NewGithubRepositoryGetAllContent(config) + def := action.Definition() + Expect(def.Name.String()).To(Equal("custom_action_name")) + }) + }) + + Describe("Configuration", func() { + It("should handle missing repository and owner in config", func() { + config := map[string]string{ + "token": "test-token", + } + action := actions.NewGithubRepositoryGetAllContent(config) + def := action.Definition() + Expect(def.Properties).To(HaveKey("repository")) + Expect(def.Properties).To(HaveKey("owner")) + }) + + It("should handle provided repository and owner in config", func() { + config := map[string]string{ + "token": "test-token", + "repository": "test-repo", + "owner": "test-owner", + } + action := actions.NewGithubRepositoryGetAllContent(config) + def := action.Definition() + Expect(def.Properties).NotTo(HaveKey("repository")) + Expect(def.Properties).NotTo(HaveKey("owner")) + }) + }) +})