Appearance
Prove your error paths
A good integration is refused when it should be, and reacts well when it is. Run these from a test harness, not your production code path: you're sending bad messages on purpose.
- A. The published SOM negative cases: your validator and the bus agree.
- B. Snapshot-sequence cases: your story state handling.
- C. Policy cases: your configuration and grants.
A. The published negative cases
The SOM repository publishes 20 messages every SOM 1.0 implementation must refuse (examples/negative/), and 18 positive examples it must accept. The bus refuses all 20 and accepts all 18. Send each negative case through the Playground or your workspace's validate route (Token service and workspace API), and check the rule:
| Case | Why it must fail | Status · rule |
|---|---|---|
envelope-extensions-not-namespaced | extensions keys must be com.<vendor>.… | 400 · envelope.additionalProperties |
envelope-message-id-not-uuid | message_id must be a UUID | 400 · envelope.format.uuid |
envelope-missing-correlation-id | correlation_id is required | 400 · envelope.required |
envelope-signature-not-allowed | No signature in the 1.0 envelope | 400 · envelope.false_schema |
envelope-som-version-0-3-2 | Only 1.0.0 is SOM 1.0 | 400 · som_version.pre_1_0 (source conformance) |
envelope-source-not-allowed | source was renamed originating_system | 400 · envelope.false_schema |
envelope-system-type-unknown | system_type is a closed list | 400 · envelope.enum |
envelope-timestamp-not-date-time | timestamp must be RFC 3339 | 400 · envelope.format.date-time |
envelope-topic-not-som | topic must start with som. | 400 · envelope.pattern |
envelope-unknown-property | No unknown envelope fields | 400 · envelope.additionalProperties |
story-context-missing-story-id | story_id is required | 400 · story-context.required |
story-context-ai-enrichments | ai_enrichments was removed | 400 · story-context.false_schema |
story-context-asset-status-finalizing | FINALIZING was withdrawn at 1.0 | 400 · story-context.enum |
story-context-asset-voice-count | voice_count was withdrawn | 400 · story-context.additionalProperties |
story-context-lifecycle-phase-live | LIVE comes from the telling, never the story | 400 · story-context.enum |
link-gate-status-unknown | Not a compliance gate status | 400 · link-event.enum |
telling-transform-id | transform_id was withdrawn | 400 · telling-event.additionalProperties |
telling-transform-missing-type | Every transform needs transform_type | 400 · telling-event.required |
delivery-neither-source-nor-locator | A delivery needs a source or a locator | 400 · delivery-media-available.anyOf and .required |
audit-flat-actor | actor is an object, not a flat id | 400 · system-audit.required and .additionalProperties |
- Each file holds its case under
message, next tomust_fail_because. Send only what's undermessage. - In the
envelope-*cases that's a complete message. In the rest it's a bare payload: wrap it in a valid envelope with the matchingmessage_type, or you'll only see envelope errors. - A message can break several rules at once, and the bus reports them all. Assert on the rule named here, not on the count.
Then try to make your product produce each of them through its normal UI or API: an unknown system_type in its settings, a free-text timestamp, a withdrawn enum value. It should never get as far as sending one.
B. Snapshot-sequence cases
Start a fresh story, publish snapshots 1 to 3 of the hurricane run, then send one of these. A refused snapshot leaves the story as it was, so you can try the refused cases one after another. An accepted one moves the story on: run each accepted case on a fresh story of its own.
| Send | Expect |
|---|---|
Snapshot 2 again, with a new message_id | 409 · sequence_number.not_increasing |
Snapshot 4 with sequence_number: 3 | 409 · sequence_number.not_increasing |
Snapshot 4 with updated_at before snapshot 3's | 409 · updated_at.backwards |
Snapshot 4 without editorial_source | 409 · snapshot.members_missing |
Snapshot 4 without the asset-presser-feed asset | 409 · snapshot.assets_dropped |
Snapshot 4 with that asset's asset_type changed to AUDIO | 409 · asset.type_changed |
Snapshot 4 with sequence_number: 6 | 202, with sequence_number.gap under tolerated |
Snapshot 4 with a different correlation_id | 202, with correlation_id.changed under tolerated |
A KILLED snapshot, then an ACTIVE one | 202, then 409 · story_type.left_terminal |
Then do it through your product: restore its database from an earlier backup and edit the story again. Does it notice it's behind and resynchronise, or keep sending stale snapshots?
C. Policy cases
| Send | Expect |
|---|---|
No Authorization header | 401 · producer.unauthenticated |
| A token from the other environment, or with one character changed | 401 · producer.unauthenticated |
A valid message to another workspace's {t} | 403 · tenant.mismatch |
A valid message as a system_id your app doesn't have | 403 · producer.system_id_not_allowed |
| A message type outside your grants | 403 · producer.message_type_not_allowed |
| A message over 240,000 bytes | 413 · message.too_large |
| A body that isn't JSON | 400 · message.not_json |
The same message_id with different content | 409 · message_id.reused |
Checkpoint
- [ ] All 20 published negative cases are refused with the rules above.
- [ ] My product can't be made to produce any of them through its normal UI or API.
- [ ] Every sequence case behaves as listed, and my product handles each
409. - [ ] Every policy case behaves as listed.
Next: Consume messages, or the readiness checklist if you only produce.