cli/pkg/cmd/agent-task/capi/job_test.go
Kynan Ware 78b958f9ae
fix(agent-task): resolve Copilot API URL dynamically (#12956)
* fix(agent-task): resolve Copilot API URL dynamically

Query viewer.copilotEndpoints.api to get the correct Copilot API URL
for the user's host instead of hardcoding api.githubcopilot.com. This
fixes 401 errors for ghe.com tenancy users whose Copilot API lives at
a different endpoint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 18:14:02 +00:00

427 lines
12 KiB
Go

package capi
import (
"context"
"net/http"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetJobRequiresRepoAndJobID(t *testing.T) {
client := &CAPIClient{}
_, err := client.GetJob(context.Background(), "", "", "only-job-id")
assert.EqualError(t, err, "owner, repo, and jobID are required")
_, err = client.GetJob(context.Background(), "", "only-repo", "")
assert.EqualError(t, err, "owner, repo, and jobID are required")
_, err = client.GetJob(context.Background(), "only-owner", "", "")
assert.EqualError(t, err, "owner, repo, and jobID are required")
_, err = client.GetJob(context.Background(), "", "", "")
assert.EqualError(t, err, "owner, repo, and jobID are required")
}
func TestGetJob(t *testing.T) {
sampleDateString := "2025-08-29T00:00:00Z"
sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
require.NoError(t, err)
tests := []struct {
name string
httpStubs func(*testing.T, *httpmock.Registry)
wantErr string
wantOut *Job
}{
{
name: "job without PR",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(200, heredoc.Docf(`
{
"job_id": "job123",
"session_id": "sess1",
"problem_statement": "Do the thing",
"event_type": "foo",
"content_filter_mode": "foo",
"status": "foo",
"result": "foo",
"actor": {
"id": 1,
"login": "octocat"
},
"created_at": "%[1]s",
"updated_at": "%[1]s"
}`,
sampleDateString,
)),
)
},
wantOut: &Job{
ID: "job123",
SessionID: "sess1",
ProblemStatement: "Do the thing",
EventType: "foo",
ContentFilterMode: "foo",
Status: "foo",
Result: "foo",
Actor: &JobActor{
ID: 1,
Login: "octocat",
},
CreatedAt: sampleDate,
UpdatedAt: sampleDate,
},
},
{
name: "job with PR",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(200, heredoc.Docf(`
{
"job_id": "job123",
"session_id": "sess1",
"problem_statement": "Do the thing",
"event_type": "foo",
"content_filter_mode": "foo",
"status": "foo",
"result": "foo",
"actor": {
"id": 1,
"login": "octocat"
},
"created_at": "%[1]s",
"updated_at": "%[1]s",
"pull_request": {
"id": 101,
"number": 42
}
}`,
sampleDateString,
)),
)
},
wantOut: &Job{
ID: "job123",
SessionID: "sess1",
ProblemStatement: "Do the thing",
EventType: "foo",
ContentFilterMode: "foo",
Status: "foo",
Result: "foo",
Actor: &JobActor{
ID: 1,
Login: "octocat",
},
CreatedAt: sampleDate,
UpdatedAt: sampleDate,
PullRequest: &JobPullRequest{
ID: 101,
Number: 42,
},
},
},
{
name: "job not found",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(404, `{}`),
)
},
wantErr: "failed to get job: 404 Not Found",
},
{
name: "API error",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(500, `{}`),
)
},
wantErr: "failed to get job: 500 Internal Server Error",
},
{
name: "invalid JSON response",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/swe/v1/jobs/OWNER/REPO/job123"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(200, ``),
)
},
wantErr: "failed to decode get job response: EOF",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(t, reg)
}
defer reg.Verify(t)
httpClient := &http.Client{Transport: reg}
capiClient := NewCAPIClient(httpClient, "", "github.com", "https://api.githubcopilot.com")
job, err := capiClient.GetJob(context.Background(), "OWNER", "REPO", "job123")
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
require.Nil(t, job)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantOut, job)
})
}
}
func TestCreateJobRequiresRepoAndProblemStatement(t *testing.T) {
client := &CAPIClient{}
_, err := client.CreateJob(context.Background(), "", "only-repo", "", "", "")
assert.EqualError(t, err, "owner and repo are required")
_, err = client.CreateJob(context.Background(), "only-owner", "", "", "", "")
assert.EqualError(t, err, "owner and repo are required")
_, err = client.CreateJob(context.Background(), "", "", "", "", "")
assert.EqualError(t, err, "owner and repo are required")
_, err = client.CreateJob(context.Background(), "owner", "repo", "", "", "")
assert.EqualError(t, err, "problem statement is required")
}
func TestCreateJob(t *testing.T) {
sampleDateString := "2025-08-29T00:00:00Z"
sampleDate, err := time.Parse(time.RFC3339, sampleDateString)
require.NoError(t, err)
tests := []struct {
name string
baseBranch string
customAgent string
httpStubs func(*testing.T, *httpmock.Registry)
wantErr string
wantOut *Job
}{
{
name: "success",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.RESTPayload(201,
heredoc.Docf(`
{
"job_id": "job123",
"session_id": "sess1",
"problem_statement": "Do the thing",
"event_type": "foo",
"content_filter_mode": "foo",
"status": "foo",
"result": "foo",
"actor": {
"id": 1,
"login": "octocat"
},
"created_at": "%[1]s",
"updated_at": "%[1]s"
}
`, sampleDateString),
func(payload map[string]interface{}) {
assert.Equal(t, "Do the thing", payload["problem_statement"])
assert.Equal(t, "gh_cli", payload["event_type"])
},
),
)
},
wantOut: &Job{
ID: "job123",
SessionID: "sess1",
ProblemStatement: "Do the thing",
EventType: "foo",
ContentFilterMode: "foo",
Status: "foo",
Result: "foo",
Actor: &JobActor{
ID: 1,
Login: "octocat",
},
CreatedAt: sampleDate,
UpdatedAt: sampleDate,
},
},
{
name: "success with base branch",
baseBranch: "some-branch",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.RESTPayload(201,
heredoc.Docf(`
{
"job_id": "job123",
"session_id": "sess1",
"problem_statement": "Do the thing",
"event_type": "foo",
"content_filter_mode": "foo",
"status": "foo",
"result": "foo",
"actor": {
"id": 1,
"login": "octocat"
},
"created_at": "%[1]s",
"updated_at": "%[1]s"
}
`, sampleDateString),
func(payload map[string]interface{}) {
assert.Equal(t, "Do the thing", payload["problem_statement"])
assert.Equal(t, "gh_cli", payload["event_type"])
assert.Equal(t, "refs/heads/some-branch", payload["pull_request"].(map[string]interface{})["base_ref"])
},
),
)
},
wantOut: &Job{
ID: "job123",
SessionID: "sess1",
ProblemStatement: "Do the thing",
EventType: "foo",
ContentFilterMode: "foo",
Status: "foo",
Result: "foo",
Actor: &JobActor{
ID: 1,
Login: "octocat",
},
CreatedAt: sampleDate,
UpdatedAt: sampleDate,
},
},
{
name: "Success with custom agent",
customAgent: "my-custom-agent",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.RESTPayload(201,
heredoc.Docf(`
{
"job_id": "job123",
"session_id": "sess1",
"problem_statement": "Do the thing",
"custom_agent": "my-custom-agent",
"event_type": "foo",
"content_filter_mode": "foo",
"status": "foo",
"result": "foo",
"actor": {
"id": 1,
"login": "octocat"
},
"created_at": "%[1]s",
"updated_at": "%[1]s"
}
`, sampleDateString),
func(payload map[string]interface{}) {
assert.Equal(t, "Do the thing", payload["problem_statement"])
assert.Equal(t, "gh_cli", payload["event_type"])
assert.Equal(t, "my-custom-agent", payload["custom_agent"])
},
),
)
},
wantOut: &Job{
ID: "job123",
SessionID: "sess1",
ProblemStatement: "Do the thing",
CustomAgent: "my-custom-agent",
EventType: "foo",
ContentFilterMode: "foo",
Status: "foo",
Result: "foo",
Actor: &JobActor{
ID: 1,
Login: "octocat",
},
CreatedAt: sampleDate,
UpdatedAt: sampleDate,
},
},
{
name: "API error, included in response body",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(500, heredoc.Doc(`{
"error": {
"message": "some error"
}
}`)),
)
},
wantErr: "failed to create job: 500 Internal Server Error: some error",
},
{
name: "API error",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(500, `{}`),
)
},
wantErr: "failed to create job: 500 Internal Server Error: ",
},
{
name: "invalid JSON response, non-HTTP 200",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(401, `Unauthorized`),
)
},
wantErr: "failed to create job: 401 Unauthorized",
},
{
name: "invalid JSON response, HTTP 200",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("POST", "agents/swe/v1/jobs/OWNER/REPO"), "api.githubcopilot.com"),
httpmock.StatusStringResponse(200, ``),
)
},
wantErr: "failed to decode create job response: EOF",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(t, reg)
}
defer reg.Verify(t)
httpClient := &http.Client{Transport: reg}
capiClient := NewCAPIClient(httpClient, "", "github.com", "https://api.githubcopilot.com")
job, err := capiClient.CreateJob(context.Background(), "OWNER", "REPO", "Do the thing", tt.baseBranch, tt.customAgent)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
require.Nil(t, job)
return
}
require.NoError(t, err)
require.Equal(t, tt.wantOut, job)
})
}
}