Skip to content

objects/uts: observe transient connection states via record-and-verify; pin fresh-channel pipeline readiness - #521

Open
sacOO7 wants to merge 1 commit into
mainfrom
uts/objects-faults-record-and-verify
Open

sacOO7 wants to merge 1 commit into
mainfrom
uts/objects-faults-record-and-verify

Conversation

@sacOO7

@sacOO7 sacOO7 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Three CI failures in ably-java's UTS-derived tests traced back to the same class of defect in the spec pseudocode: steps whose faithful rendering is racy on an asynchronous SDK.

  1. Awaiting a transient state after the stimulus. Both disconnect scenarios in uts/objects/integration/proxy/objects_faults.md did AWAIT_STATE client.connection.state == DISCONNECTED after the disconnect stimulus. DISCONNECTED after a drop from CONNECTED is transient at microsecond scale — per RTN15a the client library queues the reconnect before the DISCONNECTED emit — and AWAIT_STATE is level-triggered from its call point, so the transition can fire and be superseded before any waiter registers. One missed window is unrecoverable (the state never recurs in these scenarios). The corpus already forbids exactly this shape (writing-test-specs.md, Verifying Transient States (Record-and-Verify Pattern), incl. the "May miss it!" example and common mistake Make Crypto a static class consistently across all the SDKs #15) — these two sites predate-checked as non-conforming since creation. All three SDK renderings (java, js, cocoa) inherited the ordering literally; ably-java hit it in CI.
  2. Missing pipeline-readiness step on a fresh channel. The RTO17-RTO18 "initial attach" scenario in uts/objects/unit/realtime_object.md had no process_pending_events() between channels.get() and attach(). An SDK whose objects message pipeline initializes asynchronously (ably-java: the incoming-message collector subscribes on a scheduled coroutine) can lose the OBJECT_SYNC delivered by a synchronous mock to a not-yet-subscribed pipeline — reproduced deterministically in ably-java under a 2-core constraint.

CI evidence (ably-java): the proxy-tier failure in run 33166346294, the same transient-DISCONNECTED shape in run 33174366038 and run 34841083986, and the RTO17/RTO18 pipeline race in run 34874555278.

Changes

uts/objects/integration/proxy/objects_faults.md

Both scenarios move to the corpus's record-and-verify pattern, with translator-proof comments (the register-before-stimulus ordering is stated as load-bearing — "reordering breaks the test"):

  • RTO5a2/RTO17 (mid-sync disconnect, observation-only): a state_changes recording registered before the stimulus; a poll_until(state_changes CONTAINS DISCONNECTED) gate after attach() (the drop lands only after the sync frame round-trips, so an immediate sticky wait would no-op and race); the final wait targets the sticky CONNECTED; the observation is asserted with CONTAINS_IN_ORDER [DISCONNECTED, CONNECTING, CONNECTED] (annotated as a subsequence match, since the pre-connect recording captures leading initial-connect states).
  • RTO7/RTO8 (trigger_action disconnect, wait is load-bearing): recording before trigger_action; the mid-test gate polls the recorded list (not live state) before client A publishes; the "while B is disconnected" best-effort caveat is stated honestly so translators don't add a brittle "assert still disconnected" step.

uts/objects/unit/realtime_object.md

The RTO17-RTO18 "initial attach" scenario gains process_pending_events() after channels.get() (with the standard per-site pointer comment), so the objects message pipeline is subscribed before attach() — the same per-site convention the corpus already uses for dispatch-ordering yields.

uts/docs/writing-derived-tests.md

Two mapping-table additions so translators hit the rules at derivation time:

  • The AWAIT_STATE row now carries the transient-state caveat: never await DISCONNECTED/SUSPENDED post-stimulus — use record-and-verify.
  • A new recording lists row: state_changes = [] / events = [] are appended from SDK callback threads while poll_until/final asserts read them from the test thread — multithreaded SDKs must render them as thread-safe lists.

Verification

The corrected shapes are rendered 1:1 in ably-java by ably-pubsub-java PR #1244: the previously flaking tests pass 10–25× consecutive stress runs (the RTO17/RTO18 scenario 2500 iterations under a CI-like 2-core constraint, where the old shape failed at iteration 1), and the full PR CI is green including the proxy tier against the real sandbox.

Related and follow-up

  • Companion SDK PR: test(uts): Fix flaky tests - transient-DISCONNECTED CI flake — record-and-verify instead of awaitState ably-pubsub-java#1244 (renders these blocks; also fixes non-spec-derived smoke tests with the same pattern).
  • Follow-up (not in this PR): seven realtime proxy-tier spec sites share the same post-stimulus AWAIT_STATE ... DISCONNECTED shape (channel_faults.md, connection_resume.md ×4, heartbeat.md, presence_reentry.md); connection_open_failures.md is the one safe site (drop from CONNECTING under a persistent suppress rule). Those have shipped renderings in multiple SDKs, so their sweep is left to a separate PR.

…y; pin fresh-channel pipeline readiness

Three derived-test fixes surfaced by real CI failures in ably-java, all of
the same class: pseudocode steps whose faithful rendering is racy on an
asynchronous SDK.

- objects_faults.md: both scenarios awaited DISCONNECTED with AWAIT_STATE
  *after* the disconnect stimulus. DISCONNECTED after a drop from CONNECTED
  is transient at microsecond scale (RTN15a queues the reconnect before the
  DISCONNECTED emit), and AWAIT_STATE is level-triggered — the transition
  can fire and be superseded before the waiter registers, which the corpus
  itself already forbids (writing-test-specs.md, "Verifying Transient
  States"). Replace both with the record-and-verify pattern: recording
  registered before the stimulus, a poll_until gate on the recorded list
  where the wait is load-bearing, a sticky AWAIT_STATE CONNECTED for the
  final wait, and a CONTAINS_IN_ORDER assert for the observation-only
  scenario. Comments state the register-before-stimulus ordering as
  load-bearing so a translator who reorders can see the breakage.

- realtime_object.md (RTO17-RTO18, "initial attach"): the fresh-channel
  scenario had no process_pending_events() between channels.get() and
  attach(), so an SDK whose objects message pipeline initializes
  asynchronously can drop the OBJECT_SYNC delivered by a synchronous mock.
  Add the step, matching the existing per-site convention.

- writing-derived-tests.md: two mapping-table additions — a transient-state
  caveat on the AWAIT_STATE row (never await post-stimulus; use
  record-and-verify) and a recording-lists row (appended from SDK callback
  threads, read by poll_until from the test thread: multithreaded SDKs must
  render them as thread-safe lists).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Updates UTS guidance and scenarios to avoid asynchronous state and pipeline-readiness races.

Changes:

  • Records transient connection states before disconnect stimuli.
  • Flushes pending pipeline initialization before initial attach.
  • Documents transient-state and thread-safe recording-list mappings.

Reviewed changes

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

File Description
uts/objects/integration/proxy/objects_faults.md Adopts record-and-verify disconnect handling.
uts/objects/unit/realtime_object.md Adds a pipeline-readiness yield.
uts/docs/writing-derived-tests.md Adds translation guidance for transient states and recording lists.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +225 to +226
// A publishes while B is down. Best-effort: RTN15a may reconnect/re-sync B before this
// round-trips (then it tests plain delivery, not RTO7/RTO8); the final poll tolerates both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants