Skip to content

Fix seven protocol shape bugs, and sweep every request to find the next one - #26

Merged
Agash merged 6 commits into
masterfrom
fix/protocol-shape-audit
Aug 26, 2026
Merged

Fix seven protocol shape bugs, and sweep every request to find the next one#26
Agash merged 6 commits into
masterfrom
fix/protocol-shape-audit

Conversation

@Agash

@Agash Agash commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Seven bugs, all shipping in 0.4.1-dev1. Six were invisible until something read the payload; the seventh was in our own numeric table.

Closes #24.

Fixes

  • GetCanvasList had no stub, so it generated as List<JsonElement>, which no MessagePack formatter could build. Unreadable on that transport, fine on JSON.
  • InputVolumeMeters was read as InputStub. The meter payload carries name, uuid and levels, so it failed on the input kind it never sends. The event never fired, on either transport.
  • SceneItemListReindexed was read as SceneItemStub. The reindex event asks OBS for the basic scene item list, which is id and index only. Also never fired.
  • GetInputAudioTracks / SetInputAudioTracks use Dictionary<string, bool>, which had no formatter either. Unreadable and unsendable on MessagePack.
  • SetTBarPosition took an int for a 0.0 to 1.0 position, so only the two ends of the T-bar were reachable.
  • SetSceneItemTransform required every member, but the endpoint applies only the fields present. A partial transform could not be expressed, and a transform read back from OBS was refused.
  • Requests declaring no response payload tried to deserialize one anyway. OBS sends a payload for some of them and there is no metadata for object, so ToggleRecordPause failed a request that had succeeded.

The generator keyed array mapping on the field name alone, so payloads sharing a name shared a stub. Parent-specific cases come first now.

Diagnosis, which is what made them findable

A payload that arrives and cannot be read used to come back as null, so the caller was told OBS had returned nothing. Request paths raise ObsWebSocketSerializationException now. The receive loop stays tolerant on purpose: one unmodellable event from a newer OBS must not tear the connection down.

Guards

  • OBSWSGEN010 (unmapped array) and OBSWSGEN012 (unclassified number) are errors, not notes.
  • OBSWSGEN013: a whole-number field whose protocol restriction is written with a decimal point fails the build. That is what SetTBarPosition was.
  • OBSWSGEN014: a declared string enum with no field mapped fails the build, so a protocol refresh cannot leave one as a plain string.
  • Two tests walk the generated surface for types with no MessagePack formatter, including the bare-registered-but-list-forgotten mistake.

Both new diagnostics were verified by re-breaking the tables.

Sweeps

run-transport-tests now calls every request in the protocol and reports what it could not read or send, building the OBS state it needs rather than testing against whatever happens to be there. A log sink fails the run on any unreadable payload, since a dropped event is silent by design.

On both transports against OBS 32.2.2: 59 of 60 read requests, 68 write requests serialized, zero failures. The read count is by distinct request type and fails if fewer than 60 are accounted for, so a probe cannot go missing quietly.

Not covered, deliberately: GetLastReplayBufferReplay needs a saved replay, and starting the replay buffer to make one crashes OBS (#25). SetOutputSettings is not sent for the same reason.

284 tests on net9.0/net10.0/net11.0, 0 warnings, full forced regeneration clean.

Agash added 6 commits August 26, 2026 18:26
A payload that arrives and cannot be read came back as null, so the
caller was told OBS had returned nothing. GetCanvasList over MessagePack
reported a missing formatter as a server-side problem for every user.

The receive loop keeps the tolerant form: one unmodellable event from a
newer OBS must not tear the connection down.
Three arrays were read as the wrong stub and failed on required fields
the payload never carries, so the messages never surfaced at all:

- GetCanvasList had no stub, so MessagePack had no formatter for it.
- InputVolumeMeters was read as InputStub; the meter payload has no
  input kind.
- SceneItemListReindexed was read as SceneItemStub; the reindex event
  asks OBS for the basic list, which is id and index only.

The generator keyed the mapping on the field name alone, so payloads
sharing a name shared a stub. Parent-specific cases come first now.
GetInputAudioTracks could not be read and SetInputAudioTracks could not
be sent, on MessagePack only. Found by a new sweep in the example that
calls every read request and reports the responses it cannot
deserialize, plus a log sink that fails the run on any unreadable
payload, since a dropped event is silent by design.

Two tests now walk the generated surface for types with no formatter,
which is the shape both this and GetCanvasList had.
SetTBarPosition took an int for a 0.0 to 1.0 position, so only the two
ends of the T-bar were reachable. The protocol states the range with a
decimal point, which the generator now treats as the protocol saying the
field is fractional, and refuses to build if the table disagrees.

SetSceneItemTransform applies only the fields present, but the request
took the full transform with every member required, so a partial one
could not be expressed and a transform read back from OBS was refused.
Requests take a patch type now; responses keep the full one.

A request declaring no response payload no longer tries to read one.
OBS sends a payload for some of them and there is no metadata for
object, so ToggleRecordPause failed a request that had succeeded.

Unmapped arrays, unclassified numbers, and a declared string enum with
no field mapped are errors now rather than notes.
The read sweep ran against whatever input happened to be first, so the
audio, media and property requests were declined rather than exercised,
and a scene collection made fresh in the UI has no inputs at all and
failed the suite outright.

Both sweeps now create what they need: a scene, an audio input, a media
input and a filter, plus studio mode. Read coverage goes from 52 to 59
of 60. Volume meters seeds its input into the program scene, since OBS
meters only the inputs it considers active.

SetOutputSettings is no longer sent: writing settings to a real output
wedged the output subsystem for the rest of the session.
GetSourceFilterList had been dropped from the sweep when the filter
discovery was replaced by a fixture, and the count did not notice
because it was a running total against a hardcoded 60. It counts
distinct request types now and fails if fewer than 60 are accounted
for, so a probe cannot go missing quietly again.

The write sweep reports what it actually covers: a request OBS declines
still proves it serialized, which is the thing under test, so the total
is stable across transports even when machine state is not.
@Agash
Agash merged commit 682ae41 into master Aug 26, 2026
7 checks passed
@Agash
Agash deleted the fix/protocol-shape-audit branch August 26, 2026 17:54
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.

Deserialization failures are swallowed and returned as null

1 participant