Skip to content

refactor(test): extract pollUntil/pollForLength to test/lib/polling (F3)#6

Closed
tesdal wants to merge 1 commit intophase-ab-basefrom
audit-f3-polling-helpers
Closed

refactor(test): extract pollUntil/pollForLength to test/lib/polling (F3)#6
tesdal wants to merge 1 commit intophase-ab-basefrom
audit-f3-polling-helpers

Conversation

@tesdal
Copy link
Copy Markdown
Owner

@tesdal tesdal commented Apr 23, 2026

Summary

Audit finding F3 from 2026-04-22 diamond audit (codex-5.3): run-events.test.ts had two bespoke helpers (waitForQuestionCount, waitForPermissionCount) plus an inline 100-iteration retry loop for replies.length === 1. Task 3 extracts a generic pollUntil + pollForLength into test/lib/polling.ts so the convention is consistent and future test files reuse the same helper.

Changes

  • New: packages/opencode/test/lib/polling.ts — exports pollUntil<A>(probe, opts?) and pollForLength<A>(probe, count, opts?). Defaults: 100 iterations × 10ms interval. Both delegate to one loop; pollForLength is a thin specialization.
  • packages/opencode/test/cli/run-events.test.ts: local waitForQuestionCount / waitForPermissionCount definitions removed; all call sites replaced with pollForLength(() => question.list(), N) (and permission). Inline replies.length === 1 retry loop consolidated via pollUntil.

Verification

  • bun typecheck — clean
  • bun test test/cli/run-events.test.ts — 12/12 pass (plan said ~14; file has 12 it.live tests; no tests dropped)
  • bun test test/session/subagent-hang-regression.test.ts — 2/2 pass (sanity)

Diamond review status

  • Spec compliance (codex-5.3): ✅ 10/10 APPROVE.
  • Code quality (Opus): APPROVE WITH NITS. Four minor nits — one actionable (redundant Omit<PollOptions, \"label\"> & { label?: string }PollOptions) was fixed and amended into the commit. Three judgement-call nits (A | undefined vs Option<A> probe contract, Array.from({ length }) allocation, Effect.sync vs Effect.succeed) left as-is — match existing test-file conventions.

Context

Part of 14-task audit remediation plan. Base phase-ab-base is 65677213c (F2 merge on local/integration-v2). Draft PR opened for Copilot review only — merge target is local integration branch.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors test polling/retry logic by extracting shared polling helpers into a reusable test utility, and updates run-events CLI tests to use the new helpers for consistent retry behavior.

Changes:

  • Added pollUntil and pollForLength helpers in test/lib/polling.ts with standardized polling defaults.
  • Replaced bespoke waitForQuestionCount / waitForPermissionCount loops in run-events.test.ts with pollForLength.
  • Consolidated an inline “wait until replies length is 1” loop via pollUntil.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packages/opencode/test/lib/polling.ts Introduces shared polling helpers used to standardize test synchronization loops.
packages/opencode/test/cli/run-events.test.ts Migrates local polling helpers and an inline retry loop to the new shared polling utilities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/opencode/test/lib/polling.ts Outdated
const interval = `${opts.intervalMillis ?? 10} millis` as const
const label = opts.label ?? "pollUntil predicate"
return Effect.gen(function* () {
for (const _ of Array.from({ length: iterations })) {
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.from({ length: iterations }) allocates an array on every call just to drive the loop. Since this helper is intended to be reused across the suite, prefer a simple counted loop (for (let i = 0; i < iterations; i++)) to avoid the extra allocation and make it obvious that iterations must be a non-negative integer.

Suggested change
for (const _ of Array.from({ length: iterations })) {
for (let i = 0; i < iterations; i++) {

Copilot uses AI. Check for mistakes.
const iterations = opts.iterations ?? 100
const interval = `${opts.intervalMillis ?? 10} millis` as const
const label = opts.label ?? "pollUntil predicate"
return Effect.gen(function* () {
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iterations is used as an array length (Array.from({ length: iterations })). If a caller passes a negative or non-integer value, this will throw a RangeError rather than failing through the Effect error channel. Consider validating/sanitizing opts.iterations (e.g. clamp to >= 0 and Math.floor) and failing with a clear error when invalid.

Suggested change
return Effect.gen(function* () {
return Effect.gen(function* () {
if (!Number.isInteger(iterations) || iterations < 0) {
return yield* Effect.fail(
new Error(
`invalid iterations for ${label}: expected a non-negative integer, got ${String(iterations)}`,
),
)
}

Copilot uses AI. Check for mistakes.
Comment thread packages/opencode/test/lib/polling.ts Outdated
Comment on lines +13 to +16
export function pollUntil<A>(
probe: () => Effect.Effect<A | undefined>,
opts: PollOptions = {},
): Effect.Effect<A, Error> {
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pollUntil/pollForLength are typed as Effect.Effect<…> with omitted error/environment parameters, which effectively restricts probes to E = never and R = never. That makes the helper less reusable than intended and forces callers to wrap/cast if a probe can fail or requires an environment. Consider generalizing the signatures to accept probe: () => Effect.Effect<A | undefined, E, R> and return Effect.Effect<A, E | Error, R> (and similarly for pollForLength).

Copilot uses AI. Check for mistakes.
Comment thread packages/opencode/test/lib/polling.ts Outdated
Comment on lines +13 to +23
export function pollUntil<A>(
probe: () => Effect.Effect<A | undefined>,
opts: PollOptions = {},
): Effect.Effect<A, Error> {
const iterations = opts.iterations ?? 100
const interval = `${opts.intervalMillis ?? 10} millis` as const
const label = opts.label ?? "pollUntil predicate"
return Effect.gen(function* () {
for (const _ of Array.from({ length: iterations })) {
const value = yield* probe()
if (value !== undefined) return value
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pollUntil uses undefined as the sentinel for “keep polling”, but the generic A is unconstrained. If a caller ever legitimately expects A to include undefined, the function can never succeed even when the probe has reached a valid state. Consider constraining A to exclude undefined (e.g. A extends {} | null) or changing the contract to return an explicit Option<A> (or { done: boolean; value?: A }) from the probe.

Copilot uses AI. Check for mistakes.
@tesdal tesdal force-pushed the audit-f3-polling-helpers branch from dff2221 to 2cefabe Compare April 23, 2026 23:19
@tesdal
Copy link
Copy Markdown
Owner Author

tesdal commented Apr 23, 2026

All 4 design comments addressed in force-pushed commit 2cefabe:

  1. L21 counted loop — replaced Array.from({ length }) with for (let i = 0; i < iterations; i++).
  2. L20 iterations validationNumber.isInteger(iterations) && iterations >= 0 check, fails through the Effect error channel with a clear message.
  3. L16 generic E/R — signatures now <A, E, R>(probe: () => Effect.Effect<Option.Option<A>, E, R>) => Effect.Effect<A, E | Error, R>. Probes can fail and require environment.
  4. L23 sentinel — contract now Option<A> instead of A | undefined. Call sites updated.

Verification: bun typecheck clean, 12/12 tests pass. Requesting re-review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/opencode/test/lib/polling.ts Outdated
Comment on lines +40 to +46
return pollUntil(
() =>
Effect.gen(function* () {
const list = yield* probe()
return list.length === count ? Option.some(list) : Option.none()
}),
{ ...opts, label: opts.label ?? `list length=${count}` },
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pollForLength accepts count: number but doesn't validate it. If count is negative or non-integer, the condition can never be satisfied and the helper will always time out, which makes failures harder to diagnose. Consider failing fast with a clear error when count is not a non-negative integer (and include it in the timeout/validation message).

Suggested change
return pollUntil(
() =>
Effect.gen(function* () {
const list = yield* probe()
return list.length === count ? Option.some(list) : Option.none()
}),
{ ...opts, label: opts.label ?? `list length=${count}` },
const label = opts.label ?? `list length=${count}`
if (!Number.isInteger(count) || count < 0) {
return Effect.fail(
new Error(`invalid count for ${label}: expected a non-negative integer, got ${String(count)}`),
)
}
return pollUntil(
() =>
Effect.gen(function* () {
const list = yield* probe()
return list.length === count ? Option.some(list) : Option.none()
}),
{ ...opts, label },

Copilot uses AI. Check for mistakes.
Removes bespoke waitForQuestionCount / waitForPermissionCount helpers
from run-events.test.ts. Future tests that need to poll for a service
list length can use the shared pollForLength helper instead of writing
yet another local 100-iteration loop.

Addresses audit finding F3 (codex-5.3 diamond review, 2026-04-22).
@tesdal
Copy link
Copy Markdown
Owner Author

tesdal commented Apr 24, 2026

Second iteration addresses the new comment: pollForLength now validates count identically to pollUntil's iterations check. Clean typecheck, 12/12 pass.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tesdal
Copy link
Copy Markdown
Owner Author

tesdal commented Apr 24, 2026

Three Copilot review rounds. 4 initial design comments + 1 follow-up all addressed. Final review: no new comments. Diamond status: codex-5.3 spec 10/10 APPROVE, Opus quality APPROVE WITH NITS (all fixed), Copilot clean. Merging --no-ff into local/integration-v2. PR closed as review-only.

@tesdal tesdal closed this Apr 24, 2026
tesdal added a commit that referenced this pull request Apr 24, 2026
Diamond: codex-5.3 spec 10/10 APPROVE, Opus quality APPROVE WITH NITS (fixed), Copilot clean after 3 review rounds (5 comments across rounds, all addressed).
Closes audit finding F3. Copilot review PR #6 (closed as review-only).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants