Skip to content

feat(conformance): fail a case whose server/time does not answer the client/time it received - #143

Merged
chrisuthe merged 4 commits into
Sendspin:mainfrom
chrisuthe:chrisuthe/task/assert-the-client-time-to-server-time-exchange
Oct 7, 2026
Merged

chrisuthe merged 4 commits into
Sendspin:mainfrom
chrisuthe:chrisuthe/task/assert-the-client-time-to-server-time-exchange

Conversation

@chrisuthe

@chrisuthe chrisuthe commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Adds a verdict on the client/time → server/time exchange. Refs #23, and covers only the exchange shape: the convergence half of that issue is not externally assertable and is recorded as such below, so this does not close it.

What is judged

A new server-summary field, time_exchange, holds each client/time the server received and each server/time it sent, in order, as they went on the wire, with an {"type": "other-sent"} entry where the server began sending any other text message. time_exchange_violation in protocol.py fails a case when:

  • a server/time omits client_transmitted, server_received or server_transmitted, or carries one that is not an integer;
  • its client_transmitted is not the value of a client/time the server had received and not yet answered (the spec defines the field as the timestamp "received in the client/time message");
  • its server_received is later than its server_transmitted. The spec does not state this as a rule; it follows from both being readings of the server's one monotonic clock, taken in that order;
  • a client/time was left unanswered and the record shows the server went on without it: it answered one it received later, or began sending another text message after receiving it and then received a further client/time. No interval is measured. The spec states this response declaratively ("Once received, the server responds with a server/time", messaging.md:279) with no MUST or SHOULD, and the failure reason says so.

A client/time whose own client_transmitted is not an integer fails the case naming the client. A null, absent or unreadable time_exchange fails as a harness gap. An empty list (the client sent no client/time) gives nothing to judge.

What is not judged, and why

  • Timestamp values, absolute or epoch-relative. The spec says they are "not necessarily based on epoch time".
  • Filter convergence. It is client state no message reports; judging it would need the client to self-report.
  • Whether server_transmitted was stamped late enough. The spec requires it, but judging it needs the instant the frame left, which no summary carries.
  • The last client/time received, and any client/time nothing was sent after. The spec gives no bound on the response, a connection can close with one still unread, and a server shutting down may send what it had queued and close. So a server interrupted by shutdown is not named.

Known limits of the unanswered rule, also stated in adapters/README.md:

  • False negative, deliberate. A server that answers nothing passes where the client sent one client/time, where the server sent no other text after the first, or where its adapter records no other-sent entries. No evidence distinguishes those from a server that had no opportunity to answer, so this verdict is not complete coverage of "every client/time is answered".
  • Out-of-order replies, the one remaining inferential failure. client/time 1, client/time 2, server/time 2 with no server/time 1 fails, though the spec does not define reply ordering. The record shows the server kept reading and sending, not that it had handled the first message, and wire order alone cannot tell a reply still pending from a message ignored. It fires on nothing in the matrix today.

Evidence

  • aiosendspin: the existing ControlMessageRecorder is extended, with no second recorder. Received text is JSON-parsed (a test covers client\/time). An unencrypted legacy connection bypasses the tapped transport and reports null.
  • sendspin-go: the server adapter answers client/time in its own code, so its time_exchange records a reply the adapter wrote, at the point it was handed to the library's send queue (there is no write hook), and records no other-sent entries. Every sendspin-go server case already fails earlier on the missing server/activate.

scenario_revision

Clock sync is core messaging on every connection, whatever its roles, so the verdict runs on every case and all 13 scenarios move up by one from main at 935b5ae (5,5,4,5,5,4,4,4,5,4,4,5,3 → 6,6,5,6,6,5,5,5,6,5,5,6,4).

Expected cell movement

None. A full local matrix (macOS, 192 cases) against the live baseline: the same 32 cells pass, 0 regress, 0 newly pass. Across the 68 server summaries that reached ok, every client/time was answered and the verdict reported nothing; 51 of the 54 aiosendspin ones carry other-sent entries. Because every revision changes, the regression check will print the whole matrix as exempt for this one release.

Verification

  • python -m unittest discover -s tests: 331 pass (31 new in tests/test_time_exchange.py).
  • python scripts/run_all.py: report renders; read the server-initiated-pcm aiosendspin → SendspinKit case page, which shows the recorded exchange in the server summary.
  • Go adapter built and run locally. Not run: the sendspin-jvm client (no Java runtime on this machine); no client adapter is changed here.

…client/time it received

Record each client/time the server received and each server/time it sent in
a new time_exchange summary field, from the existing ControlMessageRecorder
and the sendspin-go server adapter, and judge it on every case: the three
server/time fields are integers, client_transmitted echoes an unanswered
client/time, server_received is not later than server_transmitted, and no
client/time was skipped or all left unanswered.

The verdict applies to every scenario, so every scenario_revision moves up
by one.

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.

🔵 Needs a closer look

The documented shutdown-related false positives require human review of the verdict’s acceptance criteria.

1 open finding
What changed in this PR

Adds clock-exchange checks across the conformance matrix, covering the message-exchange portion of #23 without judging client filter convergence.

Changes:

  • Records ordered time exchanges in Python and Go server summaries.
  • Validates timestamps, echoed requests, and unanswered requests.
  • Updates contracts, tests, and all 13 scenario revisions.
File Description
tests/​test_undeclared_formats.py Updates summary fixtures.
tests/​test_time_exchange.py Adds verdict and recorder coverage.
tests/​test_stream_start_gate.py Updates summary fixtures.
tests/​test_pcm_comparison.py Updates summary fixtures.
tests/​test_group_update_violation.py Updates summary fixtures.
tests/​test_format_priority.py Updates summary fixtures.
tests/​test_format_preference.py Updates summary fixtures.
tests/​test_first_metadata_state.py Updates summary fixtures.
tests/​test_encoded_audio_source.py Updates summary fixtures.
tests/​test_controller_state.py Updates summary fixtures.
tests/​test_chunk_framing.py Updates fixtures and revision expectation.
tests/​test_activation_violation.py Updates summary fixtures.
src/​conformance/​scenarios.py Bumps every scenario revision.
src/​conformance/​runner.py Applies the verdict across scenarios.
src/​conformance/​protocol.py Implements time-exchange validation.
src/​conformance/​adapters/​aiosendspin_server.py Includes exchanges in server summaries.
src/​conformance/​adapters/​_aiosendspin_protocol_evidence.py Captures transported time messages.
AGENTS.md Documents the summary and revision contracts.
adapters/​sendspin-go/​server/​main.go Records requests and queued replies.
adapters/​README.md Documents evidence requirements and limitations.

🧠 Review effort: Balanced


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

Comment thread src/conformance/protocol.py Outdated
…er kept sending

Drop the clause that failed a server for answering no client/time at all: with
none answered every one trails the last server/time, which the same verdict
leaves unjudged, so it named a server whose only client/time arrived as the
connection closed.

An unanswered client/time now fails a case only on recorded evidence that the
server went on without it: it answered one received later, or began sending
another text message after receiving it. The recorder marks those sends in
time_exchange as other-sent entries. No interval is measured.
…t requiring it

RC1 describes the server answering a client/time and attaches no MUST or
SHOULD to it. The unanswered reason now names that kind of statement. The
check itself is unchanged.
A server shutting down may send what it had queued and close without
answering the client/time it read last. Another message sent after a
client/time now counts against the server only when a further client/time
was received as well.
@chrisuthe
chrisuthe marked this pull request as ready for review October 7, 2026 16:27
@chrisuthe
chrisuthe merged commit 4c9c79a into Sendspin:main Oct 7, 2026
2 checks passed
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