fix: use quartz clock in task status test (#22969)

Replace time.Since() usage with a quartz.Clock injected via RootCmd to
ensure relative time strings ("Xs ago") are deterministic.
This commit is contained in:
Zach
2026-03-12 08:33:09 -06:00
committed by GitHub
parent 2bb483b425
commit 5cb820387c
4 changed files with 45 additions and 14 deletions
+13
View File
@@ -24,6 +24,7 @@ import (
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/testutil" "github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
"github.com/coder/serpent" "github.com/coder/serpent"
) )
@@ -40,6 +41,18 @@ func New(t testing.TB, args ...string) (*serpent.Invocation, config.Root) {
return NewWithCommand(t, cmd, args...) return NewWithCommand(t, cmd, args...)
} }
// NewWithClock is like New, but injects the given clock for
// tests that are time-dependent.
func NewWithClock(t testing.TB, clk quartz.Clock, args ...string) (*serpent.Invocation, config.Root) {
var root cli.RootCmd
root.SetClock(clk)
cmd, err := root.Command(root.AGPL())
require.NoError(t, err)
return NewWithCommand(t, cmd, args...)
}
type logWriter struct { type logWriter struct {
prefix string prefix string
log slog.Logger log slog.Logger
+15
View File
@@ -39,6 +39,7 @@ import (
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/pretty" "github.com/coder/pretty"
"github.com/coder/quartz"
"github.com/coder/serpent" "github.com/coder/serpent"
) )
@@ -230,6 +231,10 @@ func (r *RootCmd) RunWithSubcommands(subcommands []*serpent.Command) {
} }
func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, error) { func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, error) {
if r.clock == nil {
r.clock = quartz.NewReal()
}
fmtLong := `Coder %s — A tool for provisioning self-hosted development environments with Terraform. fmtLong := `Coder %s — A tool for provisioning self-hosted development environments with Terraform.
` `
hiddenAgentAuth := &AgentAuth{} hiddenAgentAuth := &AgentAuth{}
@@ -548,6 +553,16 @@ type RootCmd struct {
useKeyring bool useKeyring bool
keyringServiceName string keyringServiceName string
useKeyringWithGlobalConfig bool useKeyringWithGlobalConfig bool
// clock is used for time-dependent operations. Initialized to
// quartz.NewReal() in Command() if not set via SetClock.
clock quartz.Clock
}
// SetClock sets the clock used for time-dependent operations.
// Must be called before Command() to take effect.
func (r *RootCmd) SetClock(clk quartz.Clock) {
r.clock = clk
} }
// ensureClientURL loads the client URL from the config file if it // ensureClientURL loads the client URL from the config file if it
+5 -5
View File
@@ -90,7 +90,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
return err return err
} }
tsr := toStatusRow(task) tsr := toStatusRow(task, r.clock.Now())
out, err := formatter.Format(ctx, []taskStatusRow{tsr}) out, err := formatter.Format(ctx, []taskStatusRow{tsr})
if err != nil { if err != nil {
return xerrors.Errorf("format task status: %w", err) return xerrors.Errorf("format task status: %w", err)
@@ -112,7 +112,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
} }
// Only print if something changed // Only print if something changed
newStatusRow := toStatusRow(task) newStatusRow := toStatusRow(task, r.clock.Now())
if !taskStatusRowEqual(lastStatusRow, newStatusRow) { if !taskStatusRowEqual(lastStatusRow, newStatusRow) {
out, err := formatter.Format(ctx, []taskStatusRow{newStatusRow}) out, err := formatter.Format(ctx, []taskStatusRow{newStatusRow})
if err != nil { if err != nil {
@@ -166,10 +166,10 @@ func taskStatusRowEqual(r1, r2 taskStatusRow) bool {
taskStateEqual(r1.CurrentState, r2.CurrentState) taskStateEqual(r1.CurrentState, r2.CurrentState)
} }
func toStatusRow(task codersdk.Task) taskStatusRow { func toStatusRow(task codersdk.Task, now time.Time) taskStatusRow {
tsr := taskStatusRow{ tsr := taskStatusRow{
Task: task, Task: task,
ChangedAgo: time.Since(task.UpdatedAt).Truncate(time.Second).String() + " ago", ChangedAgo: now.Sub(task.UpdatedAt).Truncate(time.Second).String() + " ago",
} }
tsr.Healthy = task.WorkspaceAgentHealth != nil && tsr.Healthy = task.WorkspaceAgentHealth != nil &&
task.WorkspaceAgentHealth.Healthy && task.WorkspaceAgentHealth.Healthy &&
@@ -178,7 +178,7 @@ func toStatusRow(task codersdk.Task) taskStatusRow {
!task.WorkspaceAgentLifecycle.ShuttingDown() !task.WorkspaceAgentLifecycle.ShuttingDown()
if task.CurrentState != nil { if task.CurrentState != nil {
tsr.ChangedAgo = time.Since(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago" tsr.ChangedAgo = now.Sub(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
} }
return tsr return tsr
} }
+12 -9
View File
@@ -19,6 +19,7 @@ import (
"github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil" "github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
) )
func Test_TaskStatus(t *testing.T) { func Test_TaskStatus(t *testing.T) {
@@ -28,12 +29,12 @@ func Test_TaskStatus(t *testing.T) {
args []string args []string
expectOutput string expectOutput string
expectError string expectError string
hf func(context.Context, time.Time) func(http.ResponseWriter, *http.Request) hf func(context.Context, quartz.Clock) func(http.ResponseWriter, *http.Request)
}{ }{
{ {
args: []string{"doesnotexist"}, args: []string{"doesnotexist"},
expectError: httpapi.ResourceNotFoundResponse.Message, expectError: httpapi.ResourceNotFoundResponse.Message,
hf: func(ctx context.Context, _ time.Time) func(w http.ResponseWriter, r *http.Request) { hf: func(ctx context.Context, _ quartz.Clock) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case "/api/v2/tasks/me/doesnotexist": case "/api/v2/tasks/me/doesnotexist":
@@ -49,7 +50,8 @@ func Test_TaskStatus(t *testing.T) {
args: []string{"exists"}, args: []string{"exists"},
expectOutput: `STATE CHANGED STATUS HEALTHY STATE MESSAGE expectOutput: `STATE CHANGED STATUS HEALTHY STATE MESSAGE
0s ago active true working Thinking furiously...`, 0s ago active true working Thinking furiously...`,
hf: func(ctx context.Context, now time.Time) func(w http.ResponseWriter, r *http.Request) { hf: func(ctx context.Context, clk quartz.Clock) func(w http.ResponseWriter, r *http.Request) {
now := clk.Now()
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case "/api/v2/tasks/me/exists": case "/api/v2/tasks/me/exists":
@@ -84,7 +86,8 @@ func Test_TaskStatus(t *testing.T) {
4s ago active true 4s ago active true
3s ago active true working Reticulating splines... 3s ago active true working Reticulating splines...
2s ago active true complete Splines reticulated successfully!`, 2s ago active true complete Splines reticulated successfully!`,
hf: func(ctx context.Context, now time.Time) func(http.ResponseWriter, *http.Request) { hf: func(ctx context.Context, clk quartz.Clock) func(http.ResponseWriter, *http.Request) {
now := clk.Now()
var calls atomic.Int64 var calls atomic.Int64
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
@@ -215,7 +218,7 @@ func Test_TaskStatus(t *testing.T) {
"created_at": "2025-08-26T12:34:56Z", "created_at": "2025-08-26T12:34:56Z",
"updated_at": "2025-08-26T12:34:56Z" "updated_at": "2025-08-26T12:34:56Z"
}`, }`,
hf: func(ctx context.Context, now time.Time) func(http.ResponseWriter, *http.Request) { hf: func(ctx context.Context, _ quartz.Clock) func(http.ResponseWriter, *http.Request) {
ts := time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC) ts := time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC)
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
@@ -252,8 +255,8 @@ func Test_TaskStatus(t *testing.T) {
var ( var (
ctx = testutil.Context(t, testutil.WaitShort) ctx = testutil.Context(t, testutil.WaitShort)
now = time.Now().UTC() // TODO: replace with quartz mClock = quartz.NewMock(t)
srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, now))) srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, mClock)))
client = codersdk.New(testutil.MustURL(t, srv.URL)) client = codersdk.New(testutil.MustURL(t, srv.URL))
sb = strings.Builder{} sb = strings.Builder{}
args = []string{"task", "status", "--watch-interval", testutil.IntervalFast.String()} args = []string{"task", "status", "--watch-interval", testutil.IntervalFast.String()}
@@ -261,10 +264,10 @@ func Test_TaskStatus(t *testing.T) {
t.Cleanup(srv.Close) t.Cleanup(srv.Close)
args = append(args, tc.args...) args = append(args, tc.args...)
inv, root := clitest.New(t, args...) inv, cfgDir := clitest.NewWithClock(t, mClock, args...)
inv.Stdout = &sb inv.Stdout = &sb
inv.Stderr = &sb inv.Stderr = &sb
clitest.SetupConfig(t, client, root) clitest.SetupConfig(t, client, cfgDir)
err := inv.WithContext(ctx).Run() err := inv.WithContext(ctx).Run()
if tc.expectError == "" { if tc.expectError == "" {
assert.NoError(t, err) assert.NoError(t, err)