Ctrl K
ComunidadGuías…

cli/schedule_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli_test

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"sort"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbfake"
	"github.com/coder/coder/v2/coderd/schedule/cron"
	"github.com/coder/coder/v2/coderd/util/tz"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/coder/v2/testutil/expecter"
)

// setupTestSchedule creates 4 workspaces:
// 1. a-owner-ws1: owned by owner, has both autostart and autostop enabled.
// 2. b-owner-ws2: owned by owner, has only autostart enabled.
// 3. c-member-ws3: owned by member, has only autostop enabled.
// 4. d-member-ws4: owned by member, has neither autostart nor autostop enabled.
// It returns the owner and member clients, the database, and the workspaces.
// The workspaces are returned in the same order as they are created.
func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberClient *codersdk.Client, db database.Store, ws []codersdk.Workspace) {
	t.Helper()

	ownerClient, db = coderdtest.NewWithDatabase(t, nil)
	owner := coderdtest.CreateFirstUser(t, ownerClient)
	memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
		r.Username = "testuser2" // ensure deterministic ordering
	})
	_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:              "a-owner",
		OwnerID:           owner.UserID,
		OrganizationID:    owner.OrganizationID,
		AutostartSchedule: sql.NullString{String: sched.String(), Valid: true},
		Ttl:               sql.NullInt64{Int64: 8 * time.Hour.Nanoseconds(), Valid: true},
	}).WithAgent().Do()

	_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:              "b-owner",
		OwnerID:           owner.UserID,
		OrganizationID:    owner.OrganizationID,
		AutostartSchedule: sql.NullString{String: sched.String(), Valid: true},
	}).WithAgent().Do()
	_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:           "c-member",
		OwnerID:        memberUser.ID,
		OrganizationID: owner.OrganizationID,
		Ttl:            sql.NullInt64{Int64: 8 * time.Hour.Nanoseconds(), Valid: true},
	}).WithAgent().Do()
	_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:           "d-member",
		OwnerID:        memberUser.ID,
		OrganizationID: owner.OrganizationID,
	}).WithAgent().Do()

	// Need this for LatestBuild.Deadline
	resp, err := ownerClient.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
	require.NoError(t, err)
	require.Len(t, resp.Workspaces, 4)
	// Ensure same order as in CLI output
	ws = resp.Workspaces
	sort.Slice(ws, func(i, j int) bool {
		a := ws[i].OwnerName + "/" + ws[i].Name
		b := ws[j].OwnerName + "/" + ws[j].Name
		return a < b
	})

	return ownerClient, memberClient, db, ws
}

//nolint:paralleltest // t.Setenv
func TestScheduleShow(t *testing.T) {
	// Given
	// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
	t.Setenv("TZ", "Asia/Kolkata")
	loc, err := tz.TimezoneIANA()
	require.NoError(t, err)
	require.Equal(t, "Asia/Kolkata", loc.String())
	sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
	require.NoError(t, err, "invalid schedule")
	ownerClient, memberClient, _, ws := setupTestSchedule(t, sched)
	now := time.Now()

	t.Run("OwnerNoArgs", func(t *testing.T) {
		// When: owner specifies no args
		inv, root := clitest.New(t, "schedule", "show")
		//nolint:gocritic // Testing that owner user sees all
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: they should see their own workspaces.
		// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
		stdout.ExpectMatch(ctx, ws[0].OwnerName+"/"+ws[0].Name)
		stdout.ExpectMatch(ctx, sched.Humanize())
		stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
		stdout.ExpectMatch(ctx, "8h")
		stdout.ExpectMatch(ctx, ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
		// 2nd workspace: b-owner-ws2 has only autostart enabled.
		stdout.ExpectMatch(ctx, ws[1].OwnerName+"/"+ws[1].Name)
		stdout.ExpectMatch(ctx, sched.Humanize())
		stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
	})

	t.Run("OwnerAll", func(t *testing.T) {
		// When: owner lists all workspaces
		inv, root := clitest.New(t, "schedule", "show", "--all")
		//nolint:gocritic // Testing that owner user sees all
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: they should see all workspaces
		// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
		stdout.ExpectMatch(ctx, ws[0].OwnerName+"/"+ws[0].Name)
		stdout.ExpectMatch(ctx, sched.Humanize())
		stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
		stdout.ExpectMatch(ctx, "8h")
		stdout.ExpectMatch(ctx, ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
		// 2nd workspace: b-owner-ws2 has only autostart enabled.
		stdout.ExpectMatch(ctx, ws[1].OwnerName+"/"+ws[1].Name)
		stdout.ExpectMatch(ctx, sched.Humanize())
		stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
		// 3rd workspace: c-member-ws3 has only autostop enabled.
		stdout.ExpectMatch(ctx, ws[2].OwnerName+"/"+ws[2].Name)
		stdout.ExpectMatch(ctx, "8h")
		stdout.ExpectMatch(ctx, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
		// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
		stdout.ExpectMatch(ctx, ws[3].OwnerName+"/"+ws[3].Name)
	})

	t.Run("OwnerSearchByName", func(t *testing.T) {
		// When: owner specifies a search query
		inv, root := clitest.New(t, "schedule", "show", "--search", "name:"+ws[1].Name)
		//nolint:gocritic // Testing that owner user sees all
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: they should see workspaces matching that query
		// 2nd workspace: b-owner-ws2 has only autostart enabled.
		stdout.ExpectMatch(ctx, ws[1].OwnerName+"/"+ws[1].Name)
		stdout.ExpectMatch(ctx, sched.Humanize())
		stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
	})

	t.Run("OwnerOneArg", func(t *testing.T) {
		// When: owner asks for a specific workspace by name
		inv, root := clitest.New(t, "schedule", "show", ws[2].OwnerName+"/"+ws[2].Name)
		//nolint:gocritic // Testing that owner user sees all
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: they should see that workspace
		// 3rd workspace: c-member-ws3 has only autostop enabled.
		stdout.ExpectMatch(ctx, ws[2].OwnerName+"/"+ws[2].Name)
		stdout.ExpectMatch(ctx, "8h")
		stdout.ExpectMatch(ctx, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
	})

	t.Run("MemberNoArgs", func(t *testing.T) {
		// When: a member specifies no args
		inv, root := clitest.New(t, "schedule", "show")
		clitest.SetupConfig(t, memberClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: they should see their own workspaces
		// 1st workspace: c-member-ws3 has only autostop enabled.
		stdout.ExpectMatch(ctx, ws[2].OwnerName+"/"+ws[2].Name)
		stdout.ExpectMatch(ctx, "8h")
		stdout.ExpectMatch(ctx, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
		// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
		stdout.ExpectMatch(ctx, ws[3].OwnerName+"/"+ws[3].Name)
	})

	t.Run("MemberAll", func(t *testing.T) {
		// When: a member lists all workspaces
		inv, root := clitest.New(t, "schedule", "show", "--all")
		clitest.SetupConfig(t, memberClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		errC := make(chan error)
		go func() {
			errC <- inv.WithContext(ctx).Run()
		}()
		require.NoError(t, <-errC)

		// Then: they should only see their own
		// 1st workspace: c-member-ws3 has only autostop enabled.
		stdout.ExpectMatch(ctx, ws[2].OwnerName+"/"+ws[2].Name)
		stdout.ExpectMatch(ctx, "8h")
		stdout.ExpectMatch(ctx, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
		// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
		stdout.ExpectMatch(ctx, ws[3].OwnerName+"/"+ws[3].Name)
	})

	t.Run("JSON", func(t *testing.T) {
		// When: owner lists all workspaces in JSON format
		inv, root := clitest.New(t, "schedule", "show", "--all", "--output", "json")
		var buf bytes.Buffer
		inv.Stdout = &buf
		clitest.SetupConfig(t, ownerClient, root)
		ctx := testutil.Context(t, testutil.WaitShort)
		errC := make(chan error)
		go func() {
			errC <- inv.WithContext(ctx).Run()
		}()
		assert.NoError(t, <-errC)

		// Then: they should see all workspace schedules in JSON format
		var parsed []map[string]string
		require.NoError(t, json.Unmarshal(buf.Bytes(), &parsed))
		require.Len(t, parsed, 4)
		// Ensure same order as in CLI output
		sort.Slice(parsed, func(i, j int) bool {
			a := parsed[i]["workspace"]
			b := parsed[j]["workspace"]
			return a < b
		})
		// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
		assert.Equal(t, ws[0].OwnerName+"/"+ws[0].Name, parsed[0]["workspace"])
		assert.Equal(t, sched.Humanize(), parsed[0]["starts_at"])
		assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[0]["starts_next"])
		assert.Equal(t, "8h", parsed[0]["stops_after"])
		assert.Equal(t, ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[0]["stops_next"])
		// 2nd workspace: b-owner-ws2 has only autostart enabled.
		assert.Equal(t, ws[1].OwnerName+"/"+ws[1].Name, parsed[1]["workspace"])
		assert.Equal(t, sched.Humanize(), parsed[1]["starts_at"])
		assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[1]["starts_next"])
		assert.Empty(t, parsed[1]["stops_after"])
		assert.Empty(t, parsed[1]["stops_next"])
		// 3rd workspace: c-member-ws3 has only autostop enabled.
		assert.Equal(t, ws[2].OwnerName+"/"+ws[2].Name, parsed[2]["workspace"])
		assert.Empty(t, parsed[2]["starts_at"])
		assert.Empty(t, parsed[2]["starts_next"])
		assert.Equal(t, "8h", parsed[2]["stops_after"])
		assert.Equal(t, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[2]["stops_next"])
		// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
		assert.Equal(t, ws[3].OwnerName+"/"+ws[3].Name, parsed[3]["workspace"])
		assert.Empty(t, parsed[3]["starts_at"])
		assert.Empty(t, parsed[3]["starts_next"])
		assert.Empty(t, parsed[3]["stops_after"])
	})
}

//nolint:paralleltest // t.Setenv
func TestScheduleModify(t *testing.T) {
	// Given
	// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
	t.Setenv("TZ", "Asia/Kolkata")
	loc, err := tz.TimezoneIANA()
	require.NoError(t, err)
	require.Equal(t, "Asia/Kolkata", loc.String())
	sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
	require.NoError(t, err, "invalid schedule")
	ownerClient, _, _, ws := setupTestSchedule(t, sched)
	now := time.Now()

	t.Run("SetStart", func(t *testing.T) {
		// When: we set the start schedule
		inv, root := clitest.New(t,
			"schedule", "start", ws[3].OwnerName+"/"+ws[3].Name, "7:30AM", "Mon-Fri", "Europe/Dublin",
		)
		//nolint:gocritic // this workspace is not owned by the same user
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: the updated schedule should be shown
		stdout.ExpectMatch(ctx, ws[3].OwnerName+"/"+ws[3].Name)
		stdout.ExpectMatch(ctx, sched.Humanize())
		stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
	})

	t.Run("SetStop", func(t *testing.T) {
		// When: we set the stop schedule
		inv, root := clitest.New(t,
			"schedule", "stop", ws[2].OwnerName+"/"+ws[2].Name, "8h30m",
		)
		//nolint:gocritic // this workspace is not owned by the same user
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: the updated schedule should be shown
		stdout.ExpectMatch(ctx, ws[2].OwnerName+"/"+ws[2].Name)
		stdout.ExpectMatch(ctx, "8h30m")
		stdout.ExpectMatch(ctx, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
	})

	t.Run("UnsetStart", func(t *testing.T) {
		// When: we unset the start schedule
		inv, root := clitest.New(t,
			"schedule", "start", ws[1].OwnerName+"/"+ws[1].Name, "manual",
		)
		//nolint:gocritic // this workspace is owned by owner
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: the updated schedule should be shown
		stdout.ExpectMatch(ctx, ws[1].OwnerName+"/"+ws[1].Name)
	})

	t.Run("UnsetStop", func(t *testing.T) {
		// When: we unset the stop schedule
		inv, root := clitest.New(t,
			"schedule", "stop", ws[0].OwnerName+"/"+ws[0].Name, "manual",
		)
		//nolint:gocritic // this workspace is owned by owner
		clitest.SetupConfig(t, ownerClient, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: the updated schedule should be shown
		stdout.ExpectMatch(ctx, ws[0].OwnerName+"/"+ws[0].Name)
	})

	t.Run("SetStart_JSON", func(t *testing.T) {
		inv, root := clitest.New(t,
			"schedule", "start", ws[3].OwnerName+"/"+ws[3].Name, "7:30AM", "Mon-Fri", "Europe/Dublin", "-o", "json",
		)
		//nolint:gocritic // this workspace is not owned by the same user
		clitest.SetupConfig(t, ownerClient, root)
		var buf bytes.Buffer
		inv.Stdout = &buf
		require.NoError(t, inv.Run())

		var rows []map[string]any
		require.NoError(t, json.Unmarshal(buf.Bytes(), &rows), "unmarshal JSON output")
		require.Len(t, rows, 1)
		require.Equal(t, ws[3].OwnerName+"/"+ws[3].Name, rows[0]["workspace"])
		require.Equal(t, sched.Humanize(), rows[0]["starts_at"])
		require.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), rows[0]["starts_next"])
	})
}

//nolint:paralleltest // t.Setenv
func TestScheduleOverride(t *testing.T) {
	tests := []struct {
		command string
	}{
		{command: "extend"},
		// test for backwards compatibility
		{command: "override-stop"},
	}

	for _, tt := range tests {
		t.Run(tt.command, func(t *testing.T) {
			// Given
			// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
			t.Setenv("TZ", "Asia/Kolkata")
			loc, err := tz.TimezoneIANA()
			require.NoError(t, err)
			require.Equal(t, "Asia/Kolkata", loc.String())
			sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
			require.NoError(t, err, "invalid schedule")
			ownerClient, _, _, ws := setupTestSchedule(t, sched)
			now := time.Now()

			// When: we override the stop schedule
			inv, root := clitest.New(t,
				"schedule", tt.command, ws[0].OwnerName+"/"+ws[0].Name, "10h",
			)

			clitest.SetupConfig(t, ownerClient, root)
			stdout := expecter.NewAttachedToInvocation(t, inv)
			ctx := testutil.Context(t, testutil.WaitShort)
			require.NoError(t, inv.Run())

			// Fetch the workspace to get the actual deadline set by the
			// server. Computing our own expected deadline from a separately
			// captured time.Now() is racy: the CLI command calls time.Now()
			// internally, and with the Asia/Kolkata +05:30 offset the hour
			// boundary falls at :30 UTC minutes. A small delay between our
			// time.Now() and the command's is enough to land in different
			// hours.
			updated, err := ownerClient.Workspace(context.Background(), ws[0].ID)
			require.NoError(t, err)
			require.False(t, updated.LatestBuild.Deadline.IsZero(), "deadline should be set after extend")
			require.WithinDuration(t, now.Add(10*time.Hour), updated.LatestBuild.Deadline.Time, 5*time.Minute)
			expectedDeadline := updated.LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339)

			// Then: the updated schedule should be shown
			stdout.ExpectMatch(ctx, ws[0].OwnerName+"/"+ws[0].Name)
			stdout.ExpectMatch(ctx, sched.Humanize())
			stdout.ExpectMatch(ctx, sched.Next(now).In(loc).Format(time.RFC3339))
			stdout.ExpectMatch(ctx, "8h")
			stdout.ExpectMatch(ctx, expectedDeadline)
		})
	}
}

// TestScheduleShow_ExactMatch ensures that `schedule show` resolves the
// given workspace argument exactly, rather than falling back to a name
// filter query that can match substrings or other users' workspaces.
func TestScheduleShow_ExactMatch(t *testing.T) {
	t.Parallel()

	ownerClient, db := coderdtest.NewWithDatabase(t, nil)
	owner := coderdtest.CreateFirstUser(t, ownerClient)
	memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) {
		r.Username = "testuser2"
	})

	// Owner has a workspace named "ui", member has a workspace named "ui-2"
	// (a superset match) plus a workspace also literally named "ui".
	ownerWS := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:           "ui",
		OwnerID:        owner.UserID,
		OrganizationID: owner.OrganizationID,
	}).WithAgent().Do()
	_ = dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:           "ui-2",
		OwnerID:        memberUser.ID,
		OrganizationID: owner.OrganizationID,
	}).WithAgent().Do()
	memberWS := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
		Name:           "ui",
		OwnerID:        memberUser.ID,
		OrganizationID: owner.OrganizationID,
	}).WithAgent().Do()

	ownerWorkspace, err := ownerClient.Workspace(context.Background(), ownerWS.Workspace.ID)
	require.NoError(t, err)
	memberWorkspace, err := ownerClient.Workspace(context.Background(), memberWS.Workspace.ID)
	require.NoError(t, err)

	t.Run("OwnerSlashName", func(t *testing.T) {
		t.Parallel()
		inv, root := clitest.New(t, "schedule", "show", memberWorkspace.OwnerName+"/"+memberWorkspace.Name)
		//nolint:gocritic // Testing owner access to another user's workspace by owner/name
		clitest.SetupConfig(t, ownerClient, root)
		var buf bytes.Buffer
		inv.Stdout = &buf
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.WithContext(ctx).Run())

		output := buf.String()
		require.Contains(t, output, memberWorkspace.OwnerName+"/"+memberWorkspace.Name)
		require.NotContains(t, output, ownerWorkspace.OwnerName+"/"+ownerWorkspace.Name)
	})

	t.Run("NameSubstringMatch", func(t *testing.T) {
		t.Parallel()
		inv, root := clitest.New(t, "schedule", "show", "ui")
		clitest.SetupConfig(t, memberClient, root)
		var buf bytes.Buffer
		inv.Stdout = &buf
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.WithContext(ctx).Run())

		output := buf.String()
		require.Contains(t, output, memberWorkspace.OwnerName+"/ui")
		require.NotContains(t, output, "ui-2")
	})
}

//nolint:paralleltest // t.Setenv
func TestScheduleStart_TemplateAutostartRequirement(t *testing.T) {
	t.Setenv("TZ", "UTC")
	loc, err := tz.TimezoneIANA()
	require.NoError(t, err)
	require.Equal(t, "UTC", loc.String())

	client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	user := coderdtest.CreateFirstUser(t, client)

	version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
	template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)

	// Update template to have autostart requirement
	// Note: In AGPL, this will be ignored and all days will be allowed (enterprise feature).
	template, err = client.UpdateTemplateMeta(context.Background(), template.ID, codersdk.UpdateTemplateMeta{
		AutostartRequirement: &codersdk.TemplateAutostartRequirement{
			DaysOfWeek: []string{"monday", "wednesday", "friday"},
		},
	})
	require.NoError(t, err)

	// Verify the template - in AGPL, AutostartRequirement will have all days (enterprise feature)
	template, err = client.Template(context.Background(), template.ID)
	require.NoError(t, err)
	require.NotEmpty(t, template.AutostartRequirement.DaysOfWeek, "template should have autostart requirement days")

	workspace := coderdtest.CreateWorkspace(t, client, template.ID)
	coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

	t.Run("ShowsWarning", func(t *testing.T) {
		// When: user sets autostart schedule
		inv, root := clitest.New(t,
			"schedule", "start", workspace.Name, "9:30AM", "Mon-Fri",
		)
		clitest.SetupConfig(t, client, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		ctx := testutil.Context(t, testutil.WaitShort)
		require.NoError(t, inv.Run())

		// Then: warning should be shown
		// In AGPL, this will show all days (enterprise feature defaults to all days allowed)
		stdout.ExpectMatch(ctx, "Warning")
		stdout.ExpectMatch(ctx, "may only autostart")
	})

	t.Run("NoWarningWhenManual", func(t *testing.T) {
		// When: user sets manual schedule
		inv, root := clitest.New(t,
			"schedule", "start", workspace.Name, "manual",
		)
		clitest.SetupConfig(t, client, root)

		var stderrBuf bytes.Buffer
		inv.Stderr = &stderrBuf

		require.NoError(t, inv.Run())

		// Then: no warning should be shown on stderr
		stderrOutput := stderrBuf.String()
		require.NotContains(t, stderrOutput, "Warning")
	})
}