cli/pkg/cmd/agent-task/view/view_test.go
Kynan Ware 0fb10fca7d Various log rendering improvements
Updated the LogRenderer interface and implementations to accept *iostreams.IOStreams instead of *iostreams.ColorScheme, enabling access to terminal theme and width for improved markdown rendering. Refactored related code, tests, and mocks to support this change, and enhanced log rendering to better handle markdown and code output for various tool calls.
2025-09-15 10:13:55 -06:00

1005 lines
29 KiB
Go

package view
import (
"bytes"
"context"
"errors"
"io"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/agent-task/capi"
"github.com/cli/cli/v2/pkg/cmd/agent-task/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCmdList(t *testing.T) {
tests := []struct {
name string
tty bool
args string
wantOpts ViewOptions
wantBaseRepo ghrepo.Interface
wantErr string
}{
{
name: "no arg tty",
tty: true,
args: "",
wantOpts: ViewOptions{},
},
{
name: "session ID arg tty",
tty: true,
args: "00000000-0000-0000-0000-000000000000",
wantOpts: ViewOptions{
SelectorArg: "00000000-0000-0000-0000-000000000000",
SessionID: "00000000-0000-0000-0000-000000000000",
},
},
{
name: "PR agent-session URL arg tty",
tty: true,
args: "https://github.com/OWNER/REPO/pull/101/agent-sessions/00000000-0000-0000-0000-000000000000",
wantOpts: ViewOptions{
SelectorArg: "https://github.com/OWNER/REPO/pull/101/agent-sessions/00000000-0000-0000-0000-000000000000",
SessionID: "00000000-0000-0000-0000-000000000000",
},
},
{
name: "non-session ID arg tty",
tty: true,
args: "some-arg",
wantOpts: ViewOptions{
SelectorArg: "some-arg",
},
},
{
name: "session ID required if non-tty",
tty: false,
args: "some-arg",
wantErr: "session ID is required when not running interactively",
},
{
name: "repo override",
tty: true,
args: "some-arg -R OWNER/REPO",
wantBaseRepo: ghrepo.New("OWNER", "REPO"),
wantOpts: ViewOptions{
SelectorArg: "some-arg",
},
},
{
name: "with --log",
tty: true,
args: "some-arg --log",
wantOpts: ViewOptions{
SelectorArg: "some-arg",
Log: true,
},
},
{
name: "with --log and --follow",
tty: true,
args: "some-arg --log --follow",
wantOpts: ViewOptions{
SelectorArg: "some-arg",
Log: true,
Follow: true,
},
},
{
name: "--follow requires --log",
tty: true,
args: "some-arg --follow",
wantErr: "--log is required when providing --follow",
},
{
name: "web mode",
tty: true,
args: "some-arg -w",
wantOpts: ViewOptions{
SelectorArg: "some-arg",
Web: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
f := &cmdutil.Factory{
IOStreams: ios,
}
var gotOpts *ViewOptions
cmd := NewCmdView(f, func(opts *ViewOptions) error { gotOpts = opts; return nil })
argv, err := shlex.Split(tt.args)
require.NoError(t, err)
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
_, err = cmd.ExecuteC()
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantOpts.SelectorArg, gotOpts.SelectorArg)
assert.Equal(t, tt.wantOpts.SessionID, gotOpts.SessionID)
if tt.wantBaseRepo != nil {
baseRepo, err := gotOpts.BaseRepo()
require.NoError(t, err)
assert.True(t, ghrepo.IsSame(tt.wantBaseRepo, baseRepo))
}
})
}
}
func Test_viewRun(t *testing.T) {
sampleDate := time.Now().Add(-6 * time.Hour) // 6h ago
tests := []struct {
name string
tty bool
opts ViewOptions
promptStubs func(*testing.T, *prompter.MockPrompter)
capiStubs func(*testing.T, *capi.CapiClientMock)
logRendererStubs func(*testing.T, *shared.LogRendererMock)
wantOut string
wantErr error
wantStderr string
wantBrowserURL string
}{
{
name: "with session id, not found (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, _ string) (*capi.Session, error) {
return nil, capi.ErrSessionNotFound
}
},
wantStderr: "session not found\n",
wantErr: cmdutil.SilentError,
},
{
name: "with session id, api error (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, _ string) (*capi.Session, error) {
return nil, errors.New("some error")
}
},
wantErr: errors.New("some error"),
},
{
name: "with session id, success, with pr and user data (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
User: &api.GitHubUser{
Login: "octocat",
},
}, nil
}
},
wantOut: heredoc.Doc(`
Completed • fix something • OWNER/REPO#101
Started on behalf of octocat about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
View this session on GitHub:
https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
`),
},
{
// The user data should always be there, but we need to cover the code path.
name: "with session id, success, without user data (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
}, nil
}
},
wantOut: heredoc.Doc(`
Completed • fix something • OWNER/REPO#101
Started about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
View this session on GitHub:
https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
`),
},
{
// This can happen when the session is just created and a PR is not yet available for it.
name: "with session id, success, without pr data (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
User: &api.GitHubUser{
Login: "octocat",
},
}, nil
}
},
wantOut: heredoc.Doc(`
Completed
Started on behalf of octocat about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
`),
},
{
// The user data should always be there, but we need to cover the code path.
name: "with session id, success, without pr nor user data (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
}, nil
}
},
wantOut: heredoc.Doc(`
Completed
Started about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
`),
},
{
name: "with session id, not found, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, _ string) (*capi.Session, error) {
return nil, capi.ErrSessionNotFound
}
},
wantStderr: "session not found\n",
wantErr: cmdutil.SilentError,
},
{
name: "with session id, without pr data, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
// User data is irrelevant in this case
}, nil
}
},
wantBrowserURL: "https://github.com/copilot/agents",
wantStderr: "Opening https://github.com/copilot/agents in your browser.\n",
},
{
name: "with session id, with pr data, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
// User data is irrelevant in this case
}, nil
}
},
wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id",
wantStderr: "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id in your browser.\n",
},
{
name: "with pr number, api error (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "101",
Finder: prShared.NewMockFinder(
"101",
&api.PullRequest{
FullDatabaseID: "999999",
URL: "https://github.com/OWNER/REPO/pull/101",
},
ghrepo.New("OWNER", "REPO"),
),
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.ListSessionsByResourceIDFunc = func(_ context.Context, _ string, _ int64, _ int) ([]*capi.Session, error) {
return nil, errors.New("some error")
}
},
wantErr: errors.New("failed to list sessions for pull request: some error"),
},
{
name: "with pr reference, unsupported hostname (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "OWNER/REPO#101",
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.NewWithHost("OWNER", "REPO", "foo.com"), nil
},
},
wantErr: errors.New("agent tasks are not supported on this host: foo.com"),
},
{
name: "with pr reference, api error when fetching pr database ID (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "OWNER/REPO#101",
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetPullRequestDatabaseIDFunc = func(_ context.Context, _ string, _ string, _ string, _ int) (int64, string, error) {
return 0, "", errors.New("some error")
}
},
wantErr: errors.New("failed to fetch pull request: some error"),
},
{
name: "with pr reference, api error when fetching session (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "OWNER/REPO#101",
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetPullRequestDatabaseIDFunc = func(_ context.Context, _ string, _ string, _ string, _ int) (int64, string, error) {
return 999999, "some-url", nil
}
m.ListSessionsByResourceIDFunc = func(_ context.Context, _ string, _ int64, _ int) ([]*capi.Session, error) {
return nil, errors.New("some error")
}
},
wantErr: errors.New("failed to list sessions for pull request: some error"),
},
{
name: "with pr number, success, single session (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "101",
Finder: prShared.NewMockFinder(
"101",
&api.PullRequest{
FullDatabaseID: "999999",
URL: "https://github.com/OWNER/REPO/pull/101",
},
ghrepo.New("OWNER", "REPO"),
),
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
assert.Equal(t, "pull", resourceType)
assert.Equal(t, int64(999999), resourceID)
assert.Equal(t, defaultLimit, limit)
return []*capi.Session{
{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
User: &api.GitHubUser{
Login: "octocat",
},
},
}, nil
}
},
wantOut: heredoc.Doc(`
Completed • fix something • OWNER/REPO#101
Started on behalf of octocat about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
View this session on GitHub:
https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
`),
},
{
name: "with pr number, success, multiple sessions (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "101",
Finder: prShared.NewMockFinder(
"101",
&api.PullRequest{
FullDatabaseID: "999999",
URL: "https://github.com/OWNER/REPO/pull/101",
},
ghrepo.New("OWNER", "REPO"),
),
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
assert.Equal(t, "pull", resourceType)
assert.Equal(t, int64(999999), resourceID)
assert.Equal(t, defaultLimit, limit)
return []*capi.Session{
{
ID: "some-session-id",
Name: "session one",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
User: &api.GitHubUser{
Login: "octocat",
},
},
{
ID: "some-other-session-id",
Name: "session two",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
User: &api.GitHubUser{
Login: "octocat",
},
},
}, nil
}
},
promptStubs: func(t *testing.T, pm *prompter.MockPrompter) {
pm.RegisterSelect(
"Select a session",
[]string{
"✓ session one • about 6 hours ago",
"✓ session two • about 6 hours ago",
},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "✓ session one • about 6 hours ago")
},
)
},
wantOut: heredoc.Doc(`
Completed • fix something • OWNER/REPO#101
Started on behalf of octocat about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
View this session on GitHub:
https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
`),
},
{
name: "with pr reference, success, multiple sessions (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "OWNER/REPO#101",
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetPullRequestDatabaseIDFunc = func(_ context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
assert.Equal(t, "github.com", hostname)
assert.Equal(t, "OWNER", owner)
assert.Equal(t, "REPO", repo)
assert.Equal(t, 101, number)
return 999999, "https://github.com/OWNER/REPO/pull/101", nil
}
m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
assert.Equal(t, "pull", resourceType)
assert.Equal(t, int64(999999), resourceID)
assert.Equal(t, defaultLimit, limit)
return []*capi.Session{
{
ID: "some-session-id",
Name: "session one",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
User: &api.GitHubUser{
Login: "octocat",
},
},
{
ID: "some-other-session-id",
Name: "session two",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
User: &api.GitHubUser{
Login: "octocat",
},
},
}, nil
}
},
promptStubs: func(t *testing.T, pm *prompter.MockPrompter) {
pm.RegisterSelect(
"Select a session",
[]string{
"✓ session one • about 6 hours ago",
"✓ session two • about 6 hours ago",
},
func(_, _ string, opts []string) (int, error) {
return prompter.IndexFor(opts, "✓ session one • about 6 hours ago")
},
)
},
wantOut: heredoc.Doc(`
Completed • fix something • OWNER/REPO#101
Started on behalf of octocat about 6 hours ago
For detailed session logs, try:
gh agent-task view 'some-session-id' --log
View this session on GitHub:
https://github.com/OWNER/REPO/pull/101/agent-sessions/some-session-id
`),
},
{
name: "with pr number, api error, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "101",
Finder: prShared.NewMockFinder(
"101",
&api.PullRequest{
FullDatabaseID: "999999",
URL: "https://github.com/OWNER/REPO/pull/101",
},
ghrepo.New("OWNER", "REPO"),
),
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.ListSessionsByResourceIDFunc = func(_ context.Context, _ string, _ int64, _ int) ([]*capi.Session, error) {
return nil, errors.New("some error")
}
},
wantErr: errors.New("failed to list sessions for pull request: some error"),
},
{
name: "with pr number, single session, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "101",
Finder: prShared.NewMockFinder(
"101",
&api.PullRequest{
FullDatabaseID: "999999",
URL: "https://github.com/OWNER/REPO/pull/101",
},
ghrepo.New("OWNER", "REPO"),
),
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
assert.Equal(t, "pull", resourceType)
assert.Equal(t, int64(999999), resourceID)
assert.Equal(t, defaultLimit, limit)
return []*capi.Session{
{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
// User data is irrelevant in this case
},
}, nil
}
},
wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions",
wantStderr: "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions in your browser.\n",
},
{
name: "with pr number, multiple sessions, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "101",
Finder: prShared.NewMockFinder(
"101",
&api.PullRequest{
FullDatabaseID: "999999",
URL: "https://github.com/OWNER/REPO/pull/101",
},
ghrepo.New("OWNER", "REPO"),
),
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
assert.Equal(t, "pull", resourceType)
assert.Equal(t, int64(999999), resourceID)
assert.Equal(t, defaultLimit, limit)
return []*capi.Session{
{
ID: "some-session-id",
Name: "session one",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
// User data is irrelevant in this case
},
{
ID: "some-other-session-id",
Name: "session two",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
// User data is irrelevant in this case
},
}, nil
}
},
wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions",
wantStderr: "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions in your browser.\n",
},
{
name: "with pr reference, multiple sessions, web mode (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "OWNER/REPO#101",
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
},
Web: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetPullRequestDatabaseIDFunc = func(_ context.Context, hostname string, owner string, repo string, number int) (int64, string, error) {
assert.Equal(t, "github.com", hostname)
assert.Equal(t, "OWNER", owner)
assert.Equal(t, "REPO", repo)
assert.Equal(t, 101, number)
return 999999, "https://github.com/OWNER/REPO/pull/101", nil
}
m.ListSessionsByResourceIDFunc = func(_ context.Context, resourceType string, resourceID int64, limit int) ([]*capi.Session, error) {
assert.Equal(t, "pull", resourceType)
assert.Equal(t, int64(999999), resourceID)
assert.Equal(t, defaultLimit, limit)
return []*capi.Session{
{
ID: "some-session-id",
Name: "session one",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
// User data is irrelevant in this case
},
{
ID: "some-other-session-id",
Name: "session two",
State: "completed",
CreatedAt: sampleDate,
PullRequest: &api.PullRequest{
Title: "fix something",
Number: 101,
URL: "https://github.com/OWNER/REPO/pull/101",
Repository: &api.PRRepository{
NameWithOwner: "OWNER/REPO",
},
},
// User data is irrelevant in this case
},
}, nil
}
},
wantBrowserURL: "https://github.com/OWNER/REPO/pull/101/agent-sessions",
wantStderr: "Opening https://github.com/OWNER/REPO/pull/101/agent-sessions in your browser.\n",
},
{
name: "with log (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
Log: true,
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
User: &api.GitHubUser{
Login: "octocat",
},
}, nil
}
m.GetSessionLogsFunc = func(_ context.Context, id string) ([]byte, error) {
assert.Equal(t, "some-session-id", id)
return []byte("<raw-logs>"), nil
}
},
logRendererStubs: func(t *testing.T, m *shared.LogRendererMock) {
m.RenderFunc = func(raw []byte, w io.Writer, ios *iostreams.IOStreams) (bool, error) {
w.Write([]byte("(rendered:) " + string(raw) + "\n"))
return false, nil
}
},
wantOut: heredoc.Doc(`
Completed
Started on behalf of octocat about 6 hours ago
To follow session logs, try:
gh agent-task view 'some-session-id' --log --follow
(rendered:) <raw-logs>
`),
},
{
name: "with log and follow (tty)",
tty: true,
opts: ViewOptions{
SelectorArg: "some-session-id",
SessionID: "some-session-id",
Log: true,
Follow: true,
Sleep: func(_ time.Duration) {},
},
capiStubs: func(t *testing.T, m *capi.CapiClientMock) {
m.GetSessionFunc = func(_ context.Context, id string) (*capi.Session, error) {
assert.Equal(t, "some-session-id", id)
return &capi.Session{
ID: "some-session-id",
State: "completed",
CreatedAt: sampleDate,
User: &api.GitHubUser{
Login: "octocat",
},
}, nil
}
var count int
m.GetSessionLogsFunc = func(_ context.Context, id string) ([]byte, error) {
assert.Equal(t, "some-session-id", id)
count++
require.Less(t, count, 3, "too many calls to fetch logs")
if count == 1 {
return []byte("<raw-logs-one>"), nil
}
return []byte("<raw-logs-two>"), nil
}
},
logRendererStubs: func(t *testing.T, m *shared.LogRendererMock) {
m.FollowFunc = func(fetcher func() ([]byte, error), w io.Writer, ios *iostreams.IOStreams) error {
raw, err := fetcher()
require.NoError(t, err)
w.Write([]byte("(rendered:) " + string(raw) + "\n"))
raw, err = fetcher()
require.NoError(t, err)
w.Write([]byte("(rendered:) " + string(raw) + "\n"))
return nil
}
},
wantOut: heredoc.Doc(`
Completed
Started on behalf of octocat about 6 hours ago
(rendered:) <raw-logs-one>
(rendered:) <raw-logs-two>
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
capiClientMock := &capi.CapiClientMock{}
if tt.capiStubs != nil {
tt.capiStubs(t, capiClientMock)
}
prompter := prompter.NewMockPrompter(t)
if tt.promptStubs != nil {
tt.promptStubs(t, prompter)
}
logRenderer := &shared.LogRendererMock{}
if tt.logRendererStubs != nil {
tt.logRendererStubs(t, logRenderer)
}
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(tt.tty)
browser := &browser.Stub{}
opts := tt.opts
opts.IO = ios
opts.Prompter = prompter
opts.Browser = browser
opts.CapiClient = func() (capi.CapiClient, error) {
return capiClientMock, nil
}
opts.LogRenderer = func() shared.LogRenderer {
return logRenderer
}
err := viewRun(&opts)
if tt.wantErr != nil {
assert.Error(t, err)
require.EqualError(t, err, tt.wantErr.Error())
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantOut, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
assert.Equal(t, tt.wantBrowserURL, browser.BrowsedURL())
})
}
}