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.
364 lines
9.1 KiB
Go
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)),
|
|
)
|
|
}
|