Inbound, identity and slots
The three rules that hand the connector something empty and expect it to cope — an empty JSON request, an envelope with nothing in it, and a well-formed slot list.
inbound.tolerates_empty_request
Checks with POST /conformance, body {}, content type application/json: InboundGateway::authenticate() returns a boolean, decode() returns an iterable, immediateResponse() returns null or a Response. None may throw.
Fails with the gateway: authenticate() did not return a boolean, decode() did not return an iterable, or the guard's threw … when a method assumed a header or a key that an empty request lacks.
Why the core's inbound gate calls these three on every request before your route runs; a gateway that throws on an unexpected body turns a platform's health check, a misrouted call or a probe into a 500 the platform retries forever.
Fix treat a missing header as "not authenticated" (return false), a body without events as "no events" (return an empty iterable), and a request that needs no special answer as null.
identity.tolerates_empty_envelope
Checks IdentityResolver::resolveInbound() given new InboundEnvelope($key, null, 'conformance', 'unknown', [], now) returns null or an IdentitySummary, nothing else, and does not throw.
Fails with resolveInbound() returned something other than null or an IdentitySummary, or the guard's threw ….
Why platforms send events with no actor (a channel post, a system notice, an event kind added after your connector shipped), and the resolver is the first thing a handler asks.
Fix read the payload defensively: no actor, no throw, return null.
identity.answers_reachability
Checks IdentityResolver::deliveryTarget() given an InstallationRef with a zero UUID and no storage ref, an empty CredentialBag and an IdentityRef for an account the connector has never seen (conformance-nobody) returns null or a Recipient, nothing else, and does not throw.
Fails with deliveryTarget() returned something other than null or a Recipient, or the guard's threw ….
Why the core asks before it sends a member anything, so an account the installation cannot reach is passed over for one it can; a resolver that throws on an unknown account would stop every member notice on the project.
Fix answer from what you already hold (your own row for the account under that installation), never by calling the platform, and return null for an account you do not know; when the platform cannot tell you in advance, return a Recipient and let the send report the refusal.
slots.well_formed
Checks UiSlots::slots(): every entry is a SlotContribution, no slot is filled twice, and every contribution has a non-empty placeholder.
Fails with slots: install is filled twice, resource_badge ships no placeholder.
Why a slot is rendered on lazy core pages, and a contribution without a placeholder breaks skeleton parity for the whole page; a slot filled twice is undefined.
Fix one contribution per slot, each naming a placeholder view. The SlotContribution constructor already refuses a contribution with both a view and a component, or with neither. An empty list passes: a connector that fills nothing is a good connector.
Empty is a valid input everywhere
These three rules share a theme the rest of your connector should follow: the core will hand you less than you expect (an event you have never seen, a request that is not from the platform, a settings form with no installation), and a connector answers "nothing here" rather than throwing.
How is this guide?
Rendering and failures
The three rules that exercise the TextRenderer and the FailureClassifier with real inputs — plain text, the canonical sample, an arbitrary throwable and an arbitrary value.
Capability rules
The seven rules that run only when a port is bound — resource kinds for access control, commands and relay modes against the manifest, recovery vocabulary and readiness, the portal button, native provider keys, the migrator's reports.