diff --git a/internal/cli/mint_test.go b/internal/cli/mint_test.go index 534cd752b1..0ea16b2a06 100644 --- a/internal/cli/mint_test.go +++ b/internal/cli/mint_test.go @@ -983,6 +983,10 @@ func TestValidateMintSetupRole(t *testing.T) { require.NoError(t, err) assert.Equal(t, "coder", role) + role, err = validateMintSetupRole("e2e") + require.NoError(t, err) + assert.Equal(t, "e2e", role) + _, err = validateMintSetupRole("fix") require.Error(t, err) assert.Contains(t, err.Error(), "coder") diff --git a/internal/config/config.go b/internal/config/config.go index 6dcf4897eb..ce0c027ec2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,7 +91,7 @@ type OrgConfig struct { // ValidRoles returns the set of recognized agent roles. func ValidRoles() []string { - return []string{"fullsend", "triage", "coder", "review", "fix", "retro", "prioritize"} + return []string{"fullsend", "triage", "coder", "review", "fix", "retro", "prioritize", "e2e"} } // ValidProviders returns the set of recognized inference providers. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a9ce98b57c..71625a4bcf 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,11 +6,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/fullsend-ai/fullsend/internal/mintcore" ) func TestValidRoles(t *testing.T) { roles := ValidRoles() - assert.Len(t, roles, 7) + assert.Len(t, roles, 8) assert.Contains(t, roles, "fullsend") assert.Contains(t, roles, "triage") assert.Contains(t, roles, "coder") @@ -18,6 +20,14 @@ func TestValidRoles(t *testing.T) { assert.Contains(t, roles, "fix") assert.Contains(t, roles, "retro") assert.Contains(t, roles, "prioritize") + assert.Contains(t, roles, "e2e") +} + +func TestValidRoles_RecognizedByMintcore(t *testing.T) { + for _, role := range ValidRoles() { + assert.True(t, mintcore.HasRole(role), + "ValidRoles() contains %q but mintcore.HasRole is false — role lists may have drifted (see issue tracking consolidation)", role) + } } func TestPerRepoDefaultRoles(t *testing.T) { diff --git a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed index 9844af361c..16170ea105 100644 --- a/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed +++ b/internal/dispatch/gcf/mintsrc/mintcore/github.go.embed @@ -64,6 +64,12 @@ var canonicalRolePermissions = map[string]map[string]string{ "retro": {"actions": "read", "contents": "read", "pull_requests": "write", "issues": "write", "metadata": "read"}, "prioritize": {"contents": "read", "issues": "write", "organization_projects": "write", "metadata": "read"}, "fullsend": {"actions": "write", "actions_variables": "read", "contents": "write", "pull_requests": "write", "workflows": "write", "metadata": "read"}, + "e2e": { + "actions": "write", "actions_variables": "read", "administration": "write", + "contents": "write", "issues": "write", "members": "write", "metadata": "read", + "organization_administration": "write", "pull_requests": "write", + "secrets": "write", "workflows": "write", + }, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/forge/github/types.go b/internal/forge/github/types.go index 6d0354935c..0bf9caf1d0 100644 --- a/internal/forge/github/types.go +++ b/internal/forge/github/types.go @@ -4,16 +4,18 @@ import "fmt" // AppPermissions defines the permissions for a GitHub App. type AppPermissions struct { - Actions string `json:"actions,omitempty"` - Issues string `json:"issues,omitempty"` - PullRequests string `json:"pull_requests,omitempty"` - Checks string `json:"checks,omitempty"` - Contents string `json:"contents,omitempty"` - Variables string `json:"actions_variables,omitempty"` - Workflows string `json:"workflows,omitempty"` - Administration string `json:"administration,omitempty"` - Members string `json:"members,omitempty"` - OrganizationProjects string `json:"organization_projects,omitempty"` + Actions string `json:"actions,omitempty"` + Issues string `json:"issues,omitempty"` + PullRequests string `json:"pull_requests,omitempty"` + Checks string `json:"checks,omitempty"` + Contents string `json:"contents,omitempty"` + Variables string `json:"actions_variables,omitempty"` + Workflows string `json:"workflows,omitempty"` + Administration string `json:"administration,omitempty"` + Members string `json:"members,omitempty"` + OrganizationProjects string `json:"organization_projects,omitempty"` + OrganizationAdministration string `json:"organization_administration,omitempty"` + Secrets string `json:"secrets,omitempty"` } // HookAttributes configures the webhook for a GitHub App. @@ -139,6 +141,23 @@ func AgentAppConfig(org, role, appSet string) AppConfig { // No webhook events — triggered via workflow_dispatch from other agents. base.Events = []string{} + case "e2e": + base.Description = fmt.Sprintf("Fullsend e2e pool testing for %s", org) + base.Permissions = AppPermissions{ + Actions: "write", + Variables: "read", + Administration: "write", + Contents: "write", + Issues: "write", + Members: "write", + OrganizationAdministration: "write", + PullRequests: "write", + Secrets: "write", + Workflows: "write", + } + // Pool tests are API/mint driven; no webhook events required. + base.Events = []string{} + default: base.Description = fmt.Sprintf("Fullsend %s agent for %s", role, org) base.Permissions = AppPermissions{ diff --git a/internal/forge/github/types_test.go b/internal/forge/github/types_test.go index 097191002e..4d0a2a0bf7 100644 --- a/internal/forge/github/types_test.go +++ b/internal/forge/github/types_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/fullsend-ai/fullsend/internal/mintcore" ) func TestDefaultAgentRoles(t *testing.T) { @@ -102,6 +104,82 @@ func TestAgentAppConfig_Retro(t *testing.T) { assert.Empty(t, cfg.Events) } +func TestAgentAppConfig_E2e(t *testing.T) { + cfg := AgentAppConfig("myorg", "e2e", "fullsend-ai") + + assert.Equal(t, "fullsend-ai-e2e", cfg.Name) + assert.Equal(t, "write", cfg.Permissions.Actions) + assert.Equal(t, "read", cfg.Permissions.Variables) + assert.Equal(t, "write", cfg.Permissions.Administration) + assert.Equal(t, "write", cfg.Permissions.Contents) + assert.Equal(t, "write", cfg.Permissions.Issues) + assert.Equal(t, "write", cfg.Permissions.Members) + assert.Equal(t, "write", cfg.Permissions.OrganizationAdministration) + assert.Equal(t, "write", cfg.Permissions.PullRequests) + assert.Equal(t, "write", cfg.Permissions.Secrets) + assert.Equal(t, "write", cfg.Permissions.Workflows) + assert.Empty(t, cfg.Events) +} + +// appPermissionsAsMap converts manifest permissions to GitHub API permission names. +func appPermissionsAsMap(p AppPermissions) map[string]string { + out := make(map[string]string) + if p.Actions != "" { + out["actions"] = p.Actions + } + if p.Issues != "" { + out["issues"] = p.Issues + } + if p.PullRequests != "" { + out["pull_requests"] = p.PullRequests + } + if p.Checks != "" { + out["checks"] = p.Checks + } + if p.Contents != "" { + out["contents"] = p.Contents + } + if p.Variables != "" { + out["actions_variables"] = p.Variables + } + if p.Workflows != "" { + out["workflows"] = p.Workflows + } + if p.Administration != "" { + out["administration"] = p.Administration + } + if p.Members != "" { + out["members"] = p.Members + } + if p.OrganizationProjects != "" { + out["organization_projects"] = p.OrganizationProjects + } + if p.OrganizationAdministration != "" { + out["organization_administration"] = p.OrganizationAdministration + } + if p.Secrets != "" { + out["secrets"] = p.Secrets + } + return out +} + +func TestAgentAppConfig_E2eMatchesMintcorePermissions(t *testing.T) { + canonical := mintcore.RolePermissionsFor("e2e") + require.NotNil(t, canonical) + + manifest := appPermissionsAsMap(AgentAppConfig("myorg", "e2e", "fullsend-ai").Permissions) + + // metadata is added at mint token time; GitHub App manifests omit it explicitly. + for key, want := range canonical { + if key == "metadata" { + continue + } + got, ok := manifest[key] + assert.True(t, ok, "AgentAppConfig(e2e) missing permission %q from mintcore canonicalRolePermissions", key) + assert.Equal(t, want, got, "permission %q mismatch between AgentAppConfig and mintcore", key) + } +} + func TestAgentAppConfig_UnknownRole(t *testing.T) { cfg := AgentAppConfig("myorg", "custom-bot", "fullsend") diff --git a/internal/layers/harnesswrappers.go b/internal/layers/harnesswrappers.go index 98ad631ee9..ce82053b67 100644 --- a/internal/layers/harnesswrappers.go +++ b/internal/layers/harnesswrappers.go @@ -53,9 +53,10 @@ func (l *HarnessWrappersLayer) RequiredScopes(op Operation) []string { // harnessesForRole returns the harness filename(s) for a given agent role. // The coder role maps to both code and fix harnesses (fix reuses the coder app). // The fullsend role is the org-level app and has no harness. +// The e2e role is a pool/CI mint role and is not installed as an agent app. func harnessesForRole(role string) []string { switch role { - case "fullsend": + case "fullsend", "e2e": return nil case "coder": return []string{"code", "fix"} diff --git a/internal/mintcore/github.go b/internal/mintcore/github.go index 9844af361c..16170ea105 100644 --- a/internal/mintcore/github.go +++ b/internal/mintcore/github.go @@ -64,6 +64,12 @@ var canonicalRolePermissions = map[string]map[string]string{ "retro": {"actions": "read", "contents": "read", "pull_requests": "write", "issues": "write", "metadata": "read"}, "prioritize": {"contents": "read", "issues": "write", "organization_projects": "write", "metadata": "read"}, "fullsend": {"actions": "write", "actions_variables": "read", "contents": "write", "pull_requests": "write", "workflows": "write", "metadata": "read"}, + "e2e": { + "actions": "write", "actions_variables": "read", "administration": "write", + "contents": "write", "issues": "write", "members": "write", "metadata": "read", + "organization_administration": "write", "pull_requests": "write", + "secrets": "write", "workflows": "write", + }, } // RolePermissions returns a deep copy of the role-to-permissions map, diff --git a/internal/mintcore/github_test.go b/internal/mintcore/github_test.go index 81e79cc2fa..ce3339d486 100644 --- a/internal/mintcore/github_test.go +++ b/internal/mintcore/github_test.go @@ -104,7 +104,7 @@ func TestCreateInstallationToken_UnknownRole(t *testing.T) { } func TestRolePermissions_AllRolesPresent(t *testing.T) { - expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend"} + expectedRoles := []string{"triage", "coder", "review", "fix", "retro", "prioritize", "fullsend", "e2e"} allPerms := RolePermissions() for _, role := range expectedRoles { perms, ok := allPerms[role] @@ -115,6 +115,22 @@ func TestRolePermissions_AllRolesPresent(t *testing.T) { } } +func TestRolePermissions_E2e(t *testing.T) { + perms := RolePermissionsFor("e2e") + require.NotNil(t, perms) + assert.Equal(t, "write", perms["actions"]) + assert.Equal(t, "read", perms["actions_variables"]) + assert.Equal(t, "write", perms["administration"]) + assert.Equal(t, "write", perms["contents"]) + assert.Equal(t, "write", perms["issues"]) + assert.Equal(t, "write", perms["members"]) + assert.Equal(t, "read", perms["metadata"]) + assert.Equal(t, "write", perms["organization_administration"]) + assert.Equal(t, "write", perms["pull_requests"]) + assert.Equal(t, "write", perms["secrets"]) + assert.Equal(t, "write", perms["workflows"]) +} + func TestRolePermissions_ReturnsCopy(t *testing.T) { // Mutating the returned map must not affect the canonical definitions. perms := RolePermissions()