Go httptest for a Sume poll loop: assert next_poll_after_seconds
A 29-line Go test: httptest answers in_progress with next_poll_after_seconds 30 twice, then completed, and asserts the loop slept 30 seconds twice.

To test a Sume poll loop in Go without waiting, start an httptest.Server that replies with a scripted sequence, pass the loop a sleep function, and assert on the durations it was given. The 29-line file below returns in_progress with next_poll_after_seconds: 30 twice, then completed, and checks that the loop slept 30 seconds twice and returned completed. go test ran it in under half a second with the standard library only.
The point is the rule it protects. Sume's status payload can ask for a longer wait than your default. The TypeScript SDK treats its 2-second interval as a floor and lets next_poll_after_seconds win when it is larger; a Go client should do the same.
The poll rule
The sample stops on completed and failed to stay short. Add the canceled spelling for the route you use.
| Fact | Value |
|---|---|
| Poll target | GET /v1/jobs/{id}/status, or the polling URL for /v1/videos |
| Stop on | completed, failed, or canceled (cancelled on /v1/videos) |
| Wait | next_poll_after_seconds when present, never below your floor |
| SDK default interval | 2 seconds, as a floor |
| Typical video job | From 30 seconds to several minutes |
The test file
Save it as poll_test.go in a module with go 1.22 or later, because it uses the built-in max. Poll takes the URL and a sleep function. The handler counts reads and switches to completed on the third. The assertion checks the result, the number of waits (two), and the first wait (30 seconds).
package poll
import ("encoding/json"; "fmt"; "net/http"; "net/http/httptest"; "testing"; "time")
func Poll(url string, sleep func(time.Duration)) (string, error) {
for i := 0; i < 40; i++ {
res, err := http.Get(url)
if err != nil { return "", err }
var s struct{ Status string; Next float64 `json:"next_poll_after_seconds"` }
json.NewDecoder(res.Body).Decode(&s)
res.Body.Close()
if s.Status == "completed" || s.Status == "failed" { return s.Status, nil }
sleep(time.Duration(max(s.Next, 2) * float64(time.Second)))
}
return "", fmt.Errorf("deadline")
}
func TestPollHonorsNextPollAfter(t *testing.T) {
reads := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reads++
if reads < 3 { fmt.Fprint(w, `{"status":"in_progress","next_poll_after_seconds":30}`); return }
fmt.Fprint(w, `{"status":"completed"}`)
}))
defer srv.Close()
var waits []time.Duration
got, err := Poll(srv.URL, func(d time.Duration) { waits = append(waits, d) })
if err != nil || got != "completed" || len(waits) != 2 || waits[0] != 30*time.Second {
t.Fatal(got, err, waits)
}
}What it protects
- A refactor that goes back to a fixed 2-second sleep fails the first assertion.
- A loop with no deadline would show up as a test that never ends; the 40-iteration cap in
Pollis the guard. - A missing field is read as 0, and the floor of 2 seconds applies.
Cost of getting this wrong
A video job can take minutes. At a 2-second interval, a 12-minute job costs 360 status reads; at 30 seconds it costs 24. Multiplied across a batch, the difference is the request rate you present to the API. Reads have their own rate budget, which is larger than the write budget, but jitter and the server's own hint keep a fleet of clients from moving in step.
Extending the test
Add a second test for the deadline: a handler that never returns a terminal status, and an assertion that Poll returns an error after 40 reads. Pass a sleep function that does nothing, so it runs at once.
Add a third for a failed job. The loop returns failed, and the caller reads the error object from the full job record. Keep the decoding of that object out of Poll, so the loop does one thing.
Run go test -count=1 ./... in CI so that the test cache does not hide a change in the handler.
The test also documents the design for the next reader. A new teammate who sees the assertion on 30 seconds learns that the wait comes from the server, not from a constant in the code.
Sources
Related posts
More in Developers
- Go: net/http client for a 30-second Wan 3.0 clip, 30 lines
A 30-line Go program using only the standard library: submit wan-3.0 for 30 seconds, poll the job, write clip.mp4. Reserve and per-second rate included.
- Go net/http: a Sume video submit retry loop with one Idempotency-Key
A 30-line Go function using net/http that retries a Sume video submit on 429 and 503, reads Retry-After, and reuses one Idempotency-Key. Run on a stub.
- Google made Omni Flash its default video model: pin it or sume/auto?
Google's docs now say to use Gemini Omni Flash as the default video model. On Sume you can pin gemini-omni-flash-1.1 or send sume/auto. What each one fixes.
- Google says Imagen is shut down: test your Imagen call on Sume now
Google's Imagen page says Imagen models are shut down. Sume lists Imagen 4 Fast and Ultra separately. One catalog check and one request tell you if yours works.
Written by Sume