Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2815,3 +2815,21 @@ Campaign analytics shows `Skipped by frequency cap`; recipient output includes
This implementation keeps the published SDK pin unchanged. Release waits for a
matching SDK prerelease and accepted API contract; source support does not imply
package publication or beta availability.

## Channel update metadata

Use the existing event stream to read `newsletter.update` observations as JSON lines:

```sh
polymorfa event stream --owner project --project "$PROJECT_ID" --type newsletter.update --json
```

`live_update` messages contain reported counts and positions, with optional message
references. Missing counts are not zero. Preserve every typed or untyped view
value and unknown milestone type. Position-only observations do not provide
message action authority. The payload contains no message body.

`channel message list --json` and `channel message updates --json` preserve a
reported `forwards` value, including zero. Event lists remain metadata-only;
`event view --include-payload --json` returns exact retained bytes only when the
API authorizes them. These commands do not request additional retention.
29 changes: 29 additions & 0 deletions contracts/newsletter-observations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"repository": "polymorfa/polymorfa",
"sourceCommit": "d60f08042a5e3f36e53a7b378be7b7e710e80e9f",
"sourcePath": "apps/api/docs/openapi.json",
"sourceSha256": "9253f7fcf7b6e802411cc2f9b3b28bbf7c1e45e90c2ebee7fc461f5627ca9a32",
"polymorfaVersion": "2026-09-22",
"sdkSourceCommit": "d7977377f97b9fa12b8837ba02bc30c6fb512733",
"localCandidateSdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651",
"published": false,
"productionDependencyPinUnchanged": "0.1.0-dev.20261004024706",
"scope": "Existing channel JSON and event view/stream retain optional newsletter metadata; no new commands. Event list stays metadata-only.",
"releaseGate": "Matching accepted and published SDK required before a real CLI dependency-pin update; local artifact testing is not registry publication.",
"qualification": {
"cliSource": "131b02f1813e383140b26ef31bd7e8726367ebb0",
"cliArtifactSha256": "027e0af085db135ee295aa42d92d0f7e9d87cee4cad00cdf7f28c7d626c366fa",
"sdkArtifactSha256": "fc2b0ba01891c38956055e1719c726e73630d8fa8f778f6a6f1a69018d54d651",
"checks": [
"channel list/update forwards zero",
"metadata-only event list",
"exact retained event bytes",
"three native SSE events to CLI NDJSON"
],
"dependencyRebind": "Final SDK Rust README fix changes no TypeScript package bytes; the packed SHA-256 matches the artifact used by this physical CLI test.",
"fullCliTestsPassed": 1091,
"productionAuditVulnerabilities": 0,
"zod": "4.6.7",
"liveAcceptance": false
}
}
168 changes: 168 additions & 0 deletions test/commands/newsletter-observations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {readFileSync} from 'node:fs'
import {runCommand} from '@oclif/test'
import {afterEach, describe, expect, test} from 'vitest'
import {
startHttpServer,
startHttpSequenceServer,
withEnvironment,
type TestHttpServer,
} from '../helpers/http-server.js'
interface FixtureEnvelope {
readonly id: string
readonly session: string
readonly timestamp: string
readonly event: string
readonly payload: {
readonly id: string
readonly action: string
readonly muted?: boolean
readonly messages?: readonly {
readonly position: number
readonly forwards?: number
readonly reactionCounts?: Readonly<Record<string, number>>
}[]
readonly milestone?: {readonly type: string; readonly value: number}
}
}
const fixture = JSON.parse(
readFileSync(
new URL(
'../fixtures/newsletter-observations/delivery.json',
import.meta.url,
),
'utf8',
),
) as {readonly events: readonly FixtureEnvelope[]}
const channel = JSON.parse(
readFileSync(
new URL(
'../fixtures/newsletter-observations/channel-message.json',
import.meta.url,
),
'utf8',
),
) as Record<string, unknown>
const PROJECT = '11111111-1111-4111-8111-111111111111'
const metadata = (index: number, includePayload: boolean) => ({
id: fixture.events[index]!.id,
organizationId: 'org_1',
projectId: PROJECT,
type: 'newsletter.update',
source: 'runtime',
environment: 'development',
createdAt: fixture.events[index]!.timestamp,
payloadAvailability: includePayload ? 'available' : 'not_retained',
payload: includePayload
? {
encoding: 'base64',
contentType: 'application/json',
data: Buffer.from(JSON.stringify(fixture.events[index])).toString(
'base64',
),
}
: null,
replayableUntil: null,
metadataExpiresAt: '2026-11-27T00:00:00.000Z',
})
describe('newsletter observation consumers', () => {
let server: TestHttpServer | undefined
afterEach(async () => {
await server?.close()
server = undefined
})
const environment = <T>(run: () => Promise<T>) =>
withEnvironment(
{
POLYMORFA_API_KEY:
'pmfa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
POLYMORFA_BASE_URL: server!.baseUrl,
POLYMORFA_CLIENT_TOKEN: undefined,
POLYMORFA_PROJECT_TOKEN: undefined,
},
run,
)
test.each(['list', 'updates'])(
'retains reported forward zero in channel message %s JSON',
async (command) => {
server = await startHttpServer({body: {success: true, data: [channel]}})
const result = await environment(() =>
runCommand(
`channel message ${command} ${fixture.events[0]!.payload.id} --session number_1 --json`,
),
)
expect(result.error).toBeUndefined()
expect((JSON.parse(result.stdout) as {data: unknown}).data).toEqual([
channel,
])
},
)
test('keeps event list metadata separate from explicitly requested retained payload bytes', async () => {
const listed = metadata(0, false),
detail = metadata(0, true)
server = await startHttpSequenceServer([
{body: {data: [listed], page: {hasMore: false, nextCursor: null}}},
{body: {data: detail}},
])
const list = await environment(() =>
runCommand(
`event list --owner project --project ${PROJECT} --type newsletter.update --json`,
),
)
expect(list.error).toBeUndefined()
expect((JSON.parse(list.stdout) as {data: unknown}).data).toEqual([listed])
const view = await environment(() =>
runCommand(
`event view ${detail.id} --owner project --project ${PROJECT} --include-payload --json`,
),
)
expect(view.error).toBeUndefined()
const bytes = (JSON.parse(view.stdout) as {data: {payload: {data: string}}})
.data.payload.data
expect(JSON.parse(Buffer.from(bytes, 'base64').toString())).toEqual(
fixture.events[0],
)
expect(server.requests[1]?.url).toContain('includePayload=true')
})
test('preserves native live, milestone and legacy payloads through real SDK SSE into NDJSON', async () => {
const ready = {
v: 1,
type: 'ready',
streamId: 's',
sequence: 1,
heartbeatIntervalMs: 15000,
}
const frames = fixture.events
.map(
(_: unknown, index: number) =>
`id: c${index}\nevent: event\ndata: ${JSON.stringify({v: 1, type: 'event', streamId: 's', sequence: index + 2, sentAt: fixture.events[index]!.timestamp, cursor: `c${index}`, event: metadata(index, true)})}\n\n`,
)
.join('')
server = await startHttpSequenceServer([
{
headers: {'content-type': 'text/event-stream'},
rawBody: `event: ready\ndata: ${JSON.stringify(ready)}\n\n${frames}`,
},
{status: 204},
])
const result = await environment(() =>
runCommand(
`event stream --owner project --project ${PROJECT} --type newsletter.update --max-events 3 --json`,
),
)
expect(result.error).toBeUndefined()
const lines = result.stdout
.trim()
.split('\n')
.map((line) => JSON.parse(line) as {webhook: FixtureEnvelope})
expect(lines).toHaveLength(3)
expect(lines.map((line) => line.webhook)).toEqual(fixture.events)
expect(lines[0]?.webhook.payload.messages?.[0]?.forwards).toBe(0)
expect(lines[0]?.webhook.payload.messages?.[0]?.reactionCounts).toEqual({})
expect(lines[0]?.webhook.payload.messages?.[1]).toEqual({position: 124})
expect(lines[1]?.webhook.payload.milestone).toMatchObject({
type: 'FUTURE_MILESTONE',
value: 0,
})
expect(lines[2]?.webhook.payload.muted).toBe(false)
})
})
16 changes: 16 additions & 0 deletions test/fixtures/newsletter-observations/channel-message.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"position": 123,
"id": "9007199254740994",
"whatsapp_ids": {
"linked_devices": "native-observed"
},
"whatsapp_id": "native-observed",
"conversation": {
"id": "9007199254740993"
},
"type": "text",
"timestamp": "2023-11-14T22:13:21Z",
"views": 11,
"forwards": 0,
"reactionCounts": {}
}
70 changes: 70 additions & 0 deletions test/fixtures/newsletter-observations/delivery.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"secret": "synthetic-newsletter-secret",
"batchId": "0198a2cb-0000-7000-8000-000000000081",
"events": [
{
"id": "newsletter-event-0",
"session": "channel-number",
"timestamp": "2026-10-27T00:00:00.000Z",
"event": "newsletter.update",
"payload": {
"id": "9007199254740993",
"action": "live_update",
"sourceTimestamp": "2023-11-14T22:13:20Z",
"messages": [
{
"position": 123,
"message": {
"id": "9007199254740994",
"whatsapp_ids": {
"linked_devices": "native-observed"
}
},
"timestamp": "2023-11-14T22:13:21Z",
"viewCounts": [
{
"count": 0
},
{
"type": "views",
"count": 11
}
],
"forwards": 0,
"reactionCounts": {}
},
{
"position": 124
}
]
}
},
{
"id": "newsletter-event-1",
"session": "channel-number",
"timestamp": "2026-10-27T00:00:00.000Z",
"event": "newsletter.update",
"payload": {
"id": "9007199254740993",
"action": "milestone",
"milestone": {
"position": 123,
"type": "FUTURE_MILESTONE",
"value": 0,
"reactionCode": "👍"
}
}
},
{
"id": "legacy-mute",
"session": "channel-number",
"timestamp": "2026-10-27T00:00:00.000Z",
"event": "newsletter.update",
"payload": {
"id": "9007199254740993",
"action": "mute",
"muted": false
}
}
]
}
Loading