Skip to content

feat(media): request media reupload when the CDN blob expired - #242

Merged
vinikjkkj merged 2 commits into
masterfrom
feat/media-retry
Aug 13, 2026
Merged

feat(media): request media reupload when the CDN blob expired#242
vinikjkkj merged 2 commits into
masterfrom
feat/media-retry

Conversation

@vinikjkkj

@vinikjkkj vinikjkkj commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Adds client.message.requestMediaReupload(), the round-trip WhatsApp Web runs when a media download answers 404/410 - typically old media surfaced by a history sync. A server-error receipt carrying an encrypted ServerErrorReceipt goes out, and the sender's primary device answers with a mediaretry notification holding a fresh directPath.

  • @media/crypto/media-retry seals and opens both payloads: HKDF-SHA-256 over the media key with the WhatsApp Media Retry Notification context, AES-256-GCM bound to the stanza id as associated data.
  • @message/primitives/media-retry owns the round-trip. A plain node query cannot serve here: the ack and the notification carry the same stanza id, so a query would resolve on the ack and drop the answer. Requests are keyed by message id, deduplicated, bounded, and expire.
  • The mediaretry notification is parsed and settled through the existing incoming pipeline; the ack it already sent is unchanged.
  • getNodeBytesContent joins the transport helpers, replacing three copies of the same optional-node-bytes expression.

On success only the directPath changes: the media key, hashes, and length of the original message stay valid. not_found is a normal answer, meaning the sender no longer holds the file.

Validated against the live server end to end, including an image whose CDN blob had expired 77 days earlier: the reupload returned a new directPath and the download verified against the original keys.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added media re-upload support for recoverable message media.
    • Added public media retry request and result types.
    • Added handling for incoming media-retry notifications, including encrypted payloads.
    • Added configurable retry timeouts and limits for pending requests.
    • Added request validation, duplicate-request handling, and result tracking.
  • Bug Fixes

    • Improved extraction of binary node content across message and verification flows.
    • Added safer handling for invalid, malformed, or unmatched media-retry notifications.

Adds `client.message.requestMediaReupload()`, the round-trip WhatsApp Web
runs when a media download answers 404/410 - typically old media surfaced
by a history sync. A `server-error` receipt carrying an encrypted
`ServerErrorReceipt` goes out, and the sender's primary device answers
with a `mediaretry` notification holding a fresh `directPath`.

- `@media/crypto/media-retry` seals and opens both payloads: HKDF-SHA-256
  over the media key with the `WhatsApp Media Retry Notification` context,
  AES-256-GCM bound to the stanza id as associated data.
- `@message/primitives/media-retry` owns the round-trip. A plain node
  query cannot serve here: the ack and the notification carry the same
  stanza id, so a query would resolve on the ack and drop the answer.
  Requests are keyed by message id, deduplicated, bounded, and expire.
- The `mediaretry` notification is parsed and settled through the
  existing incoming pipeline; the ack it already sent is unchanged.
- `getNodeBytesContent` joins the transport helpers, replacing three
  copies of the same optional-node-bytes expression.

On success only the `directPath` changes: the media key, hashes, and
length of the original message stay valid. `not_found` is a normal
answer, meaning the sender no longer holds the file.

Validated against the live server end to end, including an image whose
CDN blob had expired 77 days earlier: the reupload returned a new
`directPath` and the download verified against the original keys.
@github-actions github-actions Bot added the feat New feature label Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds encrypted media retry requests and notification handling. The change includes request tracking, timeout and eviction controls, public APIs, client wiring, protocol defaults, and binary node content extraction.

Changes

Media retry flow

Layer / File(s) Summary
Media retry cryptography
src/media/crypto/media-retry.ts, src/media/crypto/__tests__/media-crypto.test.ts
Adds HKDF-derived AES-GCM encryption and decryption with media-key, IV, and stanza-ID validation.
Request lifecycle and result handling
src/message/primitives/media-retry.ts, src/message/primitives/__tests__/media-retry.test.ts, src/protocol/defaults.ts, src/protocol/notification.ts
Adds retry request contracts, notification parsing, encrypted receipt creation, pending-request tracking, deduplication, timeouts, eviction, and result settlement.
Client API and notification integration
src/client/WaClientFactory.ts, src/client/coordinators/WaMessageCoordinator.ts, src/client/coordinators/WaIncomingNodeCoordinator.ts, src/client/events/incoming.ts, src/index.ts, src/client/__tests__/*, src/client/coordinators/__tests__/*
Adds public retry types, coordinator overloads, requester construction, notification delegation, acknowledgements, and test wiring.
Binary node content extraction
src/transport/node/helpers.ts, src/transport/index.ts, src/client/coordinators/WaMobileCoordinator.ts, src/client/events/business.ts
Adds getNodeBytesContent and uses it for binary node parsing.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: 🔵 Low · up to c56d7

The new media reupload flow is mergeable with owner follow-up to ensure validation failures are surfaced as rejected promises; otherwise callers could fail to detect an unsuccessful reupload request.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WaMessageCoordinator
  participant WaMediaRetryRequester
  participant IncomingNotificationHandler
  participant Transport

  Client->>WaMessageCoordinator: requestMediaReupload(event or request)
  WaMessageCoordinator->>WaMediaRetryRequester: validated retry request
  WaMediaRetryRequester->>Transport: send encrypted server-error receipt
  Transport-->>IncomingNotificationHandler: mediaretry notification
  IncomingNotificationHandler->>WaMediaRetryRequester: handleNotification(node)
  WaMediaRetryRequester-->>WaMessageCoordinator: resolve WaMediaRetryResult
  WaMessageCoordinator-->>Client: return retry result
Loading

Possibly related PRs

  • vinikjkkj/zapo#5: Rewires dependencies and incoming-node runtime handlers in the same client components.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: requesting media reupload when the CDN blob has expired.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/media-retry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/client/coordinators/__tests__/message-coordinator.test.ts (1)

34-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the mediaRetry parameter to the requester contract.

unknown plus as never removes shape checking from every call site. If WaMediaRetryRequester.request or handleNotification changes, the test doubles keep compiling. Use Partial<WaMediaRetryRequester> so the compiler catches contract drift.

♻️ Proposed refactor
+import type { WaMediaRetryRequester } from '`@message/primitives/media-retry`'
+
 function createCoordinator(
     peerDataOperation: PeerDataOperationRequester,
-    mediaRetry: unknown = {}
+    mediaRetry: Partial<WaMediaRetryRequester> = {}
 ): WaMessageCoordinator {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/coordinators/__tests__/message-coordinator.test.ts` around lines
34 - 41, Update the createCoordinator helper’s mediaRetry parameter to use
Partial<WaMediaRetryRequester> instead of unknown, and pass it without casting
to never; preserve the default empty object while ensuring test doubles are
checked against the requester contract, including request and
handleNotification.
src/client/coordinators/WaMessageCoordinator.ts (1)

858-875: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make the implementation async so validation failures reject instead of throwing synchronously.

The overloads declare Promise<WaMediaRetryResult>, but the implementation is not async and throws synchronously at Lines 867, 870, and 874. A caller that uses .catch() without await receives a synchronous exception. Other public methods on this class return rejections for the same class of failure. Marking the implementation async keeps the documented @throws list uniform.

♻️ Proposed refactor
-    public requestMediaReupload(
+    public async requestMediaReupload(
         source: WaIncomingMessageEvent | WaMediaRetryRequest,
         options?: { readonly timeoutMs?: number }
     ): Promise<WaMediaRetryResult> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/coordinators/WaMessageCoordinator.ts` around lines 858 - 875, Mark
the public requestMediaReupload implementation as async so validation errors
from the key, newsletter, and media-payload checks reject the declared Promise
instead of throwing synchronously. Preserve the existing overloads and return
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/media/crypto/media-retry.ts`:
- Around line 22-29: Update the JSDoc for WaMediaRetryDecryptInput to mark
mediaKey with `@sensitive` and document that it must be encrypted at rest and must
not be passed to JSON.stringify() or console.log(). Keep the existing field
declarations and stanzaId documentation unchanged.

In `@src/message/primitives/media-retry.ts`:
- Around line 190-208: The in-flight deduplication guarantee is broken because
createMediaRetryRequester.request checks pending before awaiting
buildRequestNode; update src/message/primitives/media-retry.ts lines 190-208 so
trackPending occurs before the asynchronous build, or re-checks pending
afterward, ensuring duplicate messageId calls join the first promise. In
src/client/coordinators/WaMessageCoordinator.ts lines 811-857, retain the
documented joining behavior only if enforced by request; otherwise reword that
sentence to match the actual behavior.

In `@src/transport/node/helpers.ts`:
- Around line 164-170: Update getNodeBytesContent to return undefined when
node.content is an empty Uint8Array, preserving the existing behavior for
missing, non-byte, and non-empty content. Add tests covering the empty-byte
contract and ensure empty enc_p values are rejected by
parseMediaRetryNotification.

---

Nitpick comments:
In `@src/client/coordinators/__tests__/message-coordinator.test.ts`:
- Around line 34-41: Update the createCoordinator helper’s mediaRetry parameter
to use Partial<WaMediaRetryRequester> instead of unknown, and pass it without
casting to never; preserve the default empty object while ensuring test doubles
are checked against the requester contract, including request and
handleNotification.

In `@src/client/coordinators/WaMessageCoordinator.ts`:
- Around line 858-875: Mark the public requestMediaReupload implementation as
async so validation errors from the key, newsletter, and media-payload checks
reject the declared Promise instead of throwing synchronously. Preserve the
existing overloads and return behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1fbbbd6-6b0b-4939-ae42-afc8d8694554

📥 Commits

Reviewing files that changed from the base of the PR and between 269fd96 and 30d13cd.

📒 Files selected for processing (19)
  • src/client/WaClientFactory.ts
  • src/client/__tests__/incoming.test.ts
  • src/client/coordinators/WaIncomingNodeCoordinator.ts
  • src/client/coordinators/WaMessageCoordinator.ts
  • src/client/coordinators/WaMobileCoordinator.ts
  • src/client/coordinators/__tests__/coordinators.test.ts
  • src/client/coordinators/__tests__/message-coordinator-upload.test.ts
  • src/client/coordinators/__tests__/message-coordinator.test.ts
  • src/client/events/business.ts
  • src/client/events/incoming.ts
  • src/index.ts
  • src/media/crypto/__tests__/media-crypto.test.ts
  • src/media/crypto/media-retry.ts
  • src/message/primitives/__tests__/media-retry.test.ts
  • src/message/primitives/media-retry.ts
  • src/protocol/defaults.ts
  • src/protocol/notification.ts
  • src/transport/index.ts
  • src/transport/node/helpers.ts

Comment on lines +22 to +29
/** Input for {@link decryptMediaRetryNotification}. */
export interface WaMediaRetryDecryptInput {
readonly mediaKey: Uint8Array
readonly ciphertext: Uint8Array
readonly iv: Uint8Array
/** Stanza id of the original message; doubles as the AES-GCM associated data. */
readonly stanzaId: string
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Document mediaKey as sensitive data.

WaMediaRetryDecryptInput.mediaKey contains cryptographic secret material. Add @sensitive mediaKey. Document encryption at rest. Document that callers must not use JSON.stringify() or console.log() with this value.

Proposed fix
 /**
  * Input for {`@link` decryptMediaRetryNotification}.
+ *
+ * `@sensitive` mediaKey
+ * The caller must encrypt `mediaKey` at rest.
+ * Do not pass `mediaKey` to `JSON.stringify()` or `console.log()`.
  */
 export interface WaMediaRetryDecryptInput {

As per coding guidelines, “Types containing private keys, secrets, or auth tokens must have @sensitive JSDoc listing sensitive fields and document encryption-at-rest and no JSON.stringify or console.log.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Input for {@link decryptMediaRetryNotification}. */
export interface WaMediaRetryDecryptInput {
readonly mediaKey: Uint8Array
readonly ciphertext: Uint8Array
readonly iv: Uint8Array
/** Stanza id of the original message; doubles as the AES-GCM associated data. */
readonly stanzaId: string
}
/** Input for {@link decryptMediaRetryNotification}. */
export interface WaMediaRetryDecryptInput {
readonly mediaKey: Uint8Array
readonly ciphertext: Uint8Array
readonly iv: Uint8Array
/** Stanza id of the original message; doubles as the AES-GCM associated data. */
readonly stanzaId: string
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/media/crypto/media-retry.ts` around lines 22 - 29, Update the JSDoc for
WaMediaRetryDecryptInput to mark mediaKey with `@sensitive` and document that it
must be encrypted at rest and must not be passed to JSON.stringify() or
console.log(). Keep the existing field declarations and stanzaId documentation
unchanged.

Source: Coding guidelines

Comment on lines +190 to +208
request: async (input) => {
if (!input.messageId) {
throw new Error('media reupload request requires a message id')
}
if (!input.chatJid) {
throw new Error('media reupload request requires a chat jid')
}
const inFlight = pending.get(input.messageId)
if (inFlight) {
return inFlight.promise
}

const node = await buildRequestNode(input)
const entry = trackPending(input)
sendNode(node).catch((error: unknown) => {
rejectPending(input.messageId, toError(error))
})
return entry.promise
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The in-flight deduplication guarantee does not hold, and the public JSDoc documents it as if it does. createMediaRetryRequester.request reads pending before it awaits buildRequestNode, so two concurrent calls for the same messageId both create pending entries. The second entry replaces the first under the same key, and the first caller rejects with a timeout after timeoutMs.

  • src/message/primitives/media-retry.ts#L190-L208: call trackPending(input) before the async node build, or re-check pending after the await, so a concurrent duplicate joins the first promise.
  • src/client/coordinators/WaMessageCoordinator.ts#L811-L857: keep the "A second call for a message already in flight joins the first request" sentence only after the requester enforces it; otherwise reword it.
📍 Affects 2 files
  • src/message/primitives/media-retry.ts#L190-L208 (this comment)
  • src/client/coordinators/WaMessageCoordinator.ts#L811-L857
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/message/primitives/media-retry.ts` around lines 190 - 208, The in-flight
deduplication guarantee is broken because createMediaRetryRequester.request
checks pending before awaiting buildRequestNode; update
src/message/primitives/media-retry.ts lines 190-208 so trackPending occurs
before the asynchronous build, or re-checks pending afterward, ensuring
duplicate messageId calls join the first promise. In
src/client/coordinators/WaMessageCoordinator.ts lines 811-857, retain the
documented joining behavior only if enforced by request; otherwise reword that
sentence to match the actual behavior.

Comment thread src/transport/node/helpers.ts

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 19 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/media/crypto/media-retry.ts">

<violation number="1" location="src/media/crypto/media-retry.ts:23">
P3: WaMediaRetryDecryptInput.mediaKey holds raw cryptographic key material. Document it with an @sensitive tag noting that callers must encrypt it at rest and must never pass it to JSON.stringify() or console.log().</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/message/primitives/media-retry.ts
Comment thread src/transport/node/helpers.ts
}

/** Input for {@link decryptMediaRetryNotification}. */
export interface WaMediaRetryDecryptInput {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: WaMediaRetryDecryptInput.mediaKey holds raw cryptographic key material. Document it with an @sensitive tag noting that callers must encrypt it at rest and must never pass it to JSON.stringify() or console.log().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/media/crypto/media-retry.ts, line 23:

<comment>WaMediaRetryDecryptInput.mediaKey holds raw cryptographic key material. Document it with an @sensitive tag noting that callers must encrypt it at rest and must never pass it to JSON.stringify() or console.log().</comment>

<file context>
@@ -0,0 +1,84 @@
+}
+
+/** Input for {@link decryptMediaRetryNotification}. */
+export interface WaMediaRetryDecryptInput {
+    readonly mediaKey: Uint8Array
+    readonly ciphertext: Uint8Array
</file context>

…quest

Two concurrent `requestMediaReupload` calls for the same message both
passed the in-flight check, because that check ran before the awaited
node build. The second entry then replaced the first under the same key,
so one caller never settled and a second `server-error` receipt went out.
The entry is now tracked before the build.

The existing dedup test only exercised the sequential path - it waited
for the first receipt before calling again - so it passed throughout. The
new concurrency test fails against the old code.

Also from the PR review:

- `parseMediaRetryNotification` rejects an empty `<enc_p>` instead of
  handing a zero-length ciphertext to AES-GCM
- `getNodeBytesContent` documents that empty content comes back as a
  zero-length view, mirroring the empty string `getNodeTextContent`
  returns, rather than claiming `undefined`
- `WaMediaRetryDecryptInput.mediaKey` carries the same do-not-log note
  the rest of the codebase puts on media keys

Adds the coverage `requestMediaReupload` was missing: deriving a request
from an incoming message event, and the newsletter / no-media guards.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/coordinators/__tests__/message-coordinator.test.ts`:
- Around line 167-193: Update the requestMediaReupload test to be async and
replace both assert.throws calls with await assert.rejects, preserving the
newsletter and no-downloadable-media error assertions. Ensure
requestMediaReupload propagates event-validation failures by rejecting its
returned Promise rather than throwing synchronously.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e4229f9-91c7-46fd-aa33-a9427275ff5c

📥 Commits

Reviewing files that changed from the base of the PR and between 30d13cd and c56d736.

📒 Files selected for processing (5)
  • src/client/coordinators/__tests__/message-coordinator.test.ts
  • src/media/crypto/media-retry.ts
  • src/message/primitives/__tests__/media-retry.test.ts
  • src/message/primitives/media-retry.ts
  • src/transport/node/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/transport/node/helpers.ts
  • src/message/primitives/tests/media-retry.test.ts
  • src/media/crypto/media-retry.ts
  • src/message/primitives/media-retry.ts

Comment on lines +167 to +193
test('requestMediaReupload rejects newsletters and messages without media', () => {
const coordinator = createCoordinator(createFakePdo().requester)
const base = {
rawNode: { tag: 'message', attrs: {} },
offline: false,
message: { imageMessage: { directPath: '/v/x', mediaKey: new Uint8Array(32) } }
}
const key = {
remoteJid: '123@newsletter',
id: 'MSG1',
fromMe: false,
isGroup: false,
isBroadcast: false,
isNewsletter: true,
senderDevice: 0
}

assert.throws(() => coordinator.requestMediaReupload({ ...base, key } as never), /newsletter/)
assert.throws(
() =>
coordinator.requestMediaReupload({
...base,
key: { ...key, isNewsletter: false },
message: { conversation: 'hi' }
} as never),
/no downloadable media/
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- test file ---'
sed -n '1,230p' src/client/coordinators/__tests__/message-coordinator.test.ts

printf '%s\n' '--- implementation and references ---'
rg -n -C 8 'requestMediaReupload' src

Repository: vinikjkkj/zapo

Length of output: 19299


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

test_path = Path('src/client/coordinators/__tests__/message-coordinator.test.ts')
for i, line in enumerate(test_path.read_text().splitlines(), 1):
    if 150 <= i <= 200:
        print(f'{i}: {line}')
PY

printf '%s\n' '--- declaration and implementation locations ---'
rg -n -C 15 'requestMediaReupload' src/client

Repository: vinikjkkj/zapo

Length of output: 19010


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const assert = require('node:assert/strict')

function requestMediaReupload(source) {
    if (source.key.isNewsletter) {
        throw new Error('requestMediaReupload is not supported on newsletter messages')
    }
    if (!source.message.imageMessage) {
        throw new Error('message has no downloadable media')
    }
    return Promise.resolve({ result: 'success' })
}

const event = {
    key: { isNewsletter: true },
    message: { imageMessage: { mediaKey: new Uint8Array(32) } }
}

let synchronousError = null
try {
    requestMediaReupload(event)
} catch (error) {
    synchronousError = error
}

assert.match(synchronousError?.message ?? '', /newsletter/)
assert.throws(() => requestMediaReupload(event), /newsletter/)
await assert.rejects(
    Promise.resolve().then(() => requestMediaReupload(event)),
    /newsletter/
)
console.log('validation throws synchronously; wrapping the call in a Promise converts it to rejection')
JS

Repository: vinikjkkj/zapo

Length of output: 989


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const assert = require('node:assert/strict')

;(async () => {
    function requestMediaReupload(source) {
        if (source.key.isNewsletter) {
            throw new Error('requestMediaReupload is not supported on newsletter messages')
        }
        if (!source.message.imageMessage) {
            throw new Error('message has no downloadable media')
        }
        return Promise.resolve({ result: 'success' })
    }

    const event = {
        key: { isNewsletter: true },
        message: { imageMessage: { mediaKey: new Uint8Array(32) } }
    }

    let synchronousError = null
    try {
        requestMediaReupload(event)
    } catch (error) {
        synchronousError = error
    }

    assert.match(synchronousError?.message ?? '', /newsletter/)
    assert.throws(() => requestMediaReupload(event), /newsletter/)
    await assert.rejects(
        Promise.resolve().then(() => requestMediaReupload(event)),
        /newsletter/
    )
    console.log('validation throws synchronously; wrapping the call in a Promise converts it to rejection')
})()
JS

Repository: vinikjkkj/zapo

Length of output: 241


Assert Promise rejection for requestMediaReupload.

Make the test async and use assert.rejects. Make event-validation failures reject the returned promise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/coordinators/__tests__/message-coordinator.test.ts` around lines
167 - 193, Update the requestMediaReupload test to be async and replace both
assert.throws calls with await assert.rejects, preserving the newsletter and
no-downloadable-media error assertions. Ensure requestMediaReupload propagates
event-validation failures by rejecting its returned Promise rather than throwing
synchronously.

@vinikjkkj
vinikjkkj merged commit b13dfaf into master Aug 13, 2026
26 checks passed
@vinikjkkj
vinikjkkj deleted the feat/media-retry branch August 13, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant