cli/pkg/cmd/agent-task/list/list_test.go
Kynan Ware 7b71b5f21b Refactor agent-task list command client initialization
Moves CAPI client initialization to a deferred function in ListOptions, simplifying command setup and improving testability. Updates tests to use the new client initialization pattern and adds more comprehensive test cases for session listing.
2025-08-29 14:09:45 -06:00

364 lines
9.1 KiB
Go

package list
import (
"net/http"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
)
func TestNewCmdList(t *testing.T) {
tests := []struct {
name string
cli string
wantOpts ListOptions
}{
{
name: "no arguments",
wantOpts: ListOptions{
Limit: defaultLimit,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{}
var gotOpts *ListOptions
cmd := NewCmdList(f, func(opts *ListOptions) error {
gotOpts = opts
return nil
})
cmd.ExecuteC()
assert.Equal(t, tt.wantOpts.Limit, gotOpts.Limit)
})
}
}
func Test_listRun(t *testing.T) {
sixHours, _ := time.ParseDuration("6h")
sixHoursAgo := time.Now().Add(-sixHours)
createdAt := sixHoursAgo.Format(time.RFC3339)
tests := []struct {
name string
tty bool
stubs func(*httpmock.Registry)
wantOut string
}{
{
name: "no sessions",
tty: true,
stubs: func(reg *httpmock.Registry) { registerEmptySessionsMock(reg) },
wantOut: "no agent tasks found\n",
},
{
name: "single session (tty)",
tty: true,
stubs: func(reg *httpmock.Registry) { registerSingleSessionMock(reg, createdAt) },
wantOut: "SESSION ID PULL REQUEST REPO SESSION STATE CREATED\n" +
"sess1 #42 OWNER/REPO completed about 6 hours ago\n",
},
{
name: "single session (nontty)",
tty: false,
stubs: func(reg *httpmock.Registry) { registerSingleSessionMock(reg, createdAt) },
wantOut: "sess1\t#42\tOWNER/REPO\tcompleted\t" + createdAt + "\n", // header omitted for non-tty
},
{
name: "many sessions (tty)",
tty: true,
stubs: func(reg *httpmock.Registry) { registerManySessionsMock(reg, createdAt) },
wantOut: "SESSION ID PULL REQUEST REPO SESSION STATE CREATED\n" +
"s1 #101 OWNER/REPO completed about 6 hours ago\n" +
"s2 #102 OWNER/REPO failed about 6 hours ago\n" +
"s3 #103 OWNER/REPO in_progress about 6 hours ago\n" +
"s4 #104 OWNER/REPO queued about 6 hours ago\n" +
"s5 #105 OWNER/REPO canceled about 6 hours ago\n" +
"s6 #106 OWNER/REPO mystery about 6 hours ago\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
tt.stubs(reg)
cfg := config.NewBlankConfig()
cfg.Set("github.com", "oauth_token", "OTOKEN")
authCfg := cfg.Authentication()
ios, _, stdout, _ := iostreams.Test()
ios.SetStdoutTTY(tt.tty)
httpClient := &http.Client{Transport: reg}
capiClient := capi.NewCAPIClient(httpClient, authCfg)
opts := &ListOptions{
IO: ios,
Config: func() (gh.Config, error) { return cfg, nil },
Limit: 30,
CapiClient: func() (*capi.CAPIClient, error) { return capiClient, nil },
}
err := listRun(opts)
assert.NoError(t, err)
got := stdout.String()
if tt.wantOut == "" && tt.name == "single session (tty)" {
t.Logf("Captured output for single session (tty):\n%s", got)
t.Fatalf("fill in wantOut with the above output and re-run tests")
}
assert.Equal(t, tt.wantOut, got)
reg.Verify(t)
})
}
}
// registerEmptySessionsMock registers a single empty page of sessions
func registerEmptySessionsMock(reg *httpmock.Registry) {
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/sessions"), "api.githubcopilot.com"),
httpmock.StringResponse(heredoc.Doc(`{
"sessions": []
}`)),
)
}
// registerSingleSessionMock registers two REST pages (one with a session, one empty) and GraphQL hydration for that session's PR
func registerSingleSessionMock(reg *httpmock.Registry, createdAt string) {
// First page with one session
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/sessions"), "api.githubcopilot.com"),
httpmock.StringResponse(heredoc.Docf(`{
"sessions": [
{
"id": "sess1",
"name": "Build artifacts",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "completed",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2000,
"created_at": "%[1]s"
}
]
}`, createdAt)),
)
// Second page empty to terminate pagination
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/sessions"), "api.githubcopilot.com"),
httpmock.StringResponse(`{"sessions": []}`),
)
// GraphQL hydration
reg.Register(
httpmock.GraphQL(`query FetchPRs`),
httpmock.StringResponse(heredoc.Docf(`{
"data": {
"nodes": [
{
"id": "PR_node",
"fullDatabaseId": "2000",
"number": 42,
"title": "Improve docs",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/42",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
}
]
}
}`, createdAt)),
)
}
// registerManySessionsMock registers multiple sessions covering various states
// States covered: completed, failed, in_progress, queued, canceled, (unknown -> treated as muted)
func registerManySessionsMock(reg *httpmock.Registry, createdAt string) {
// First page returns six sessions
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/sessions"), "api.githubcopilot.com"),
httpmock.StringResponse(heredoc.Docf(`{
"sessions": [
{
"id": "s1",
"name": "A",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "completed",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2000,
"created_at": "%[1]s"
},
{
"id": "s2",
"name": "B",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "failed",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2001,
"created_at": "%[1]s"
},
{
"id": "s3",
"name": "C",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "in_progress",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2002,
"created_at": "%[1]s"
},
{
"id": "s4",
"name": "D",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "queued",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2003,
"created_at": "%[1]s"
},
{
"id": "s5",
"name": "E",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "canceled",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2004,
"created_at": "%[1]s"
},
{
"id": "s6",
"name": "F",
"user_id": 1,
"agent_id": 2,
"logs": "",
"state": "mystery",
"owner_id": 10,
"repo_id": 1000,
"resource_type": "pull",
"resource_id": 2005,
"created_at": "%[1]s"
}
]
}`, createdAt)),
)
// Second page empty
reg.Register(
httpmock.WithHost(httpmock.REST("GET", "agents/sessions"), "api.githubcopilot.com"),
httpmock.StringResponse(`{"sessions": []}`),
)
// GraphQL hydration for 6 PRs
reg.Register(
httpmock.GraphQL(`query FetchPRs`),
httpmock.StringResponse(heredoc.Docf(`{
"data": {
"nodes": [
{
"id": "PR_node1",
"fullDatabaseId": "2000",
"number": 101,
"title": "PR 101",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/101",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
},
{
"id": "PR_node2",
"fullDatabaseId": "2001",
"number": 102,
"title": "PR 102",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/102",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
},
{
"id": "PR_node3",
"fullDatabaseId": "2002",
"number": 103,
"title": "PR 103",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/103",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
},
{
"id": "PR_node4",
"fullDatabaseId": "2003",
"number": 104,
"title": "PR 104",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/104",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
},
{
"id": "PR_node5",
"fullDatabaseId": "2004",
"number": 105,
"title": "PR 105",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/105",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
},
{
"id": "PR_node6",
"fullDatabaseId": "2005",
"number": 106,
"title": "PR 106",
"state": "OPEN",
"url": "https://github.com/OWNER/REPO/pull/106",
"body": "",
"createdAt": "%[1]s",
"updatedAt": "%[1]s",
"repository": { "nameWithOwner": "OWNER/REPO" }
}
]
}
}`, createdAt)),
)
}