fix(cli): use quartz clock in waitForTaskIdle for immediate first poll (#25648)

waitForTaskIdle used time.NewTicker(5s) which delays the first poll
by 5 seconds. Debugger tracing proved the failure mechanism: on slow
CI (Windows), the first poll at 5s sees "working" (idle patch has not
landed due to goroutine scheduling), needs poll #2 at 10s, but the
25s context expires before it fires.

Two changes:

1. Use r.clock.NewTicker (quartz) with time.Nanosecond initial
   interval and Reset(5s) for immediate first poll. Tests inject a
   mock clock via clitest.NewWithClock for deterministic control.
2. Rewrite WaitsForWorkingAppState test with quartz traps
   (NewTicker + TickerReset) for deterministic synchronization
   instead of racing goroutines. Fix PausedDuringWaitForReady
   sync point.

Closes DEVEX-381
This commit is contained in:
Mathias Fredriksson
2026-05-25 19:14:29 +03:00
committed by GitHub
parent 8652ef3e3b
commit 7958ad6d04
2 changed files with 32 additions and 7 deletions
+26 -4
View File
@@ -21,6 +21,7 @@ import (
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func Test_TaskSend(t *testing.T) {
@@ -255,10 +256,10 @@ func Test_TaskSend(t *testing.T) {
w := clitest.StartWithWaiter(t, inv)
// Wait for the command to enter the build-watching phase
// of waitForTaskReady.
pty.ExpectMatchContext(ctx, "Queued")
// of waitForTaskIdle.
pty.ExpectMatchContext(ctx, "Waiting for task to become idle")
// Pause the task while waitForTaskReady is polling. Since
// Pause the task while waitForTaskIdle is polling. Since
// no agent is connected, the task stays initializing until
// we pause it, at which point the status becomes paused.
pauseTask(ctx, t, setup.userClient, setup.task)
@@ -284,14 +285,32 @@ func Test_TaskSend(t *testing.T) {
Message: "busy",
}))
// Set up mock clock and traps before starting the command.
mClock := quartz.NewMock(t)
tickTrap := mClock.Trap().NewTicker("task_send", "poll")
resetTrap := mClock.Trap().TickerReset("task_send", "poll")
// When: We send input while the app is working.
inv, root := clitest.New(t, "task", "send", setup.task.Name, "some task input")
inv, root := clitest.NewWithClock(t, mClock, "task", "send", setup.task.Name, "some task input")
clitest.SetupConfig(t, setup.userClient, root)
ctx := testutil.Context(t, testutil.WaitLong)
inv = inv.WithContext(ctx)
w := clitest.StartWithWaiter(t, inv)
// Wait for ticker creation and release it.
tickCall := tickTrap.MustWait(ctx)
tickCall.MustRelease(ctx)
tickTrap.Close()
// Fire the immediate first poll (time.Nanosecond initial interval).
mClock.Advance(time.Nanosecond).MustWait(ctx)
// Wait for Reset (confirms first poll completed and saw "working").
resetCall := resetTrap.MustWait(ctx)
resetCall.MustRelease(ctx)
resetTrap.Close()
// Transition the app back to idle so waitForTaskIdle proceeds.
require.NoError(t, agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{
AppSlug: "task-sidebar",
@@ -299,6 +318,9 @@ func Test_TaskSend(t *testing.T) {
Message: "ready",
}))
// Fire second poll at the regular 5s interval.
mClock.Advance(5 * time.Second).MustWait(ctx)
// Then: The command should complete successfully.
require.NoError(t, w.Wait())
})