Skip to content

Implement end-to-end tests for TC token - #437

Merged
jlucaso1 merged 4 commits into
mainfrom
test/tc-token
Mar 25, 2026
Merged

Implement end-to-end tests for TC token#437
jlucaso1 merged 4 commits into
mainfrom
test/tc-token

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Tests
    • Added comprehensive e2e tests and test helpers for token receipt, delivery, reply flows, multi-device sync, persistence across reconnects, pruning of expired tokens, and first-message error handling to restricted accounts.
  • Bug Fixes / Behavior
    • Token notifications skip redundant storage writes and only trigger presence re-subscribe when a token was actually stored; re-subscribe runs conditionally only for active subscriptions, reducing unnecessary operations and log noise.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@jlucaso1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 8 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 00f39cc2-dd87-43e6-80e9-88c54fa06bf7

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4aaf5 and 8637ffb.

📒 Files selected for processing (1)
  • src/handlers/notification.rs
📝 Walkthrough

Walkthrough

Added two TestClient async helpers for tcToken handling, a new end-to-end tcToken test suite (seven tests), and changes to the privacy-token notification flow to deduplicate identical tokens and conditionally re-subscribe presence when a token is stored.

Changes

Cohort / File(s) Summary
TestClient tcToken Helpers
tests/e2e/src/lib.rs
Added pub async fn tc_token_key(&self) -> anyhow::Result<String> to compute the tcToken storage key (prefers sender LID user, falls back to phone-number JID user and errors if missing) and pub async fn wait_for_tc_token(&self, jid_key: &str, timeout_secs: u64) -> anyhow::Result<TcTokenEntry> to poll the tcToken index until an entry appears or timeout. Imports TcTokenEntry.
tcToken E2E Tests
tests/e2e/tests/privacy_tokens.rs
New e2e test module with seven #[tokio::test] cases covering tcToken receipt/storage, issue_tokens delivery and index update, reply population, first-message 463 behavior to restricted contacts, multi-device delivery, persistence across reconnect, and expiry-based pruning. Adds helper unique_push_name.
Notification handler
src/handlers/notification.rs
Refactored handle_privacy_token_notification to extract from_jid once, prefer sender_lid attribute for resolution, skip backend writes when incoming token bytes equal stored bytes, set token_stored only on successful writes, and conditionally call presence re-subscribe when tokens were stored and from_jid exists.
Presence re-subscribe
src/features/presence.rs
Added pub(crate) async fn re_subscribe_when_active(&self, jid: &Jid) -> Result<(), anyhow::Error> and updated client resubscribe flow to delegate conditional subscribe logic to this method.

Sequence Diagram(s)

sequenceDiagram
    participant Notifier as Notification
    participant Handler as PrivacyTokenHandler
    participant Storage as TcTokenStorage
    participant Presence as PresenceService
    Notifier->>Handler: deliver privacy-token notification (includes from)
    Handler->>Handler: extract from_jid, resolve sender_lid (prefer attr)
    Handler->>Storage: get_tc_token(jid_key)
    alt existing token and bytes == incoming
        Handler->>Handler: skip write (dedupe)
    else write needed
        Handler->>Storage: put_tc_token(jid_key, token_entry)
        Storage-->>Handler: OK
        Handler->>Handler: set token_stored = true
    end
    alt token_stored and Presence.subscription_exists(from_jid)
        Handler->>Presence: re_subscribe_when_active(from_jid)
        Presence-->>Handler: subscribe result (log on failure)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
Tokens tumble, tiny and bright,
Hopping through tests in moonlit night.
Stored or skipped, we nudge with care,
Presence whispers “subscribe” — we’re there.
A carrot-coded privacy delight.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing end-to-end tests for the TC token feature, which is the primary focus of the PR (312 lines added to privacy_tokens.rs test file).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/tc-token

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 and usage tips.

@github-actions

github-actions Bot commented Mar 25, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchtest/tc-token
Testbedubuntu-latest

🚨 2 Alerts

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()Instructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
47.13 x 1e3
(+10.44%)Baseline: 42.67 x 1e3
44.81 x 1e3
(105.18%)

reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()Instructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
2.23 x 1e3
(+5.08%)Baseline: 2.12 x 1e3
2.23 x 1e3
(100.08%)

Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,197.00
(-6.14%)Baseline: 6,602.48
6,932.60
(89.39%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-29.51%)Baseline: 743,813.78
781,004.47
(67.13%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-6.35%)Baseline: 22,282.08
23,396.18
(89.19%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,202.00
(-18.61%)Baseline: 120,654.45
126,687.17
(77.52%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,230.00
(-11.48%)Baseline: 110,966.73
116,515.07
(84.31%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.12%)Baseline: 533,611.54
560,292.11
(95.12%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,870.00
(-5.39%)Baseline: 16,773.87
17,612.56
(90.11%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,715,201.00
(-9.09%)Baseline: 16,186,738.03
16,996,074.94
(86.58%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,358.00
(-23.00%)Baseline: 153,702.58
161,387.71
(73.34%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.12%)Baseline: 535,031.02
561,782.57
(95.12%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,919.00
(-4.82%)Baseline: 18,825.52
19,766.80
(90.65%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,066,347.00
(-24.10%)Baseline: 36,980,189.76
38,829,199.24
(72.28%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.12%)Baseline: 534,050.54
560,753.06
(95.12%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,843.00
(-8.22%)Baseline: 17,261.16
18,124.22
(87.41%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,716,627.00
(-9.09%)Baseline: 16,187,812.99
16,997,203.64
(86.58%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
107,945.00
(-14.85%)Baseline: 126,773.69
133,112.37
(81.09%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,302.00
(-11.47%)Baseline: 111,038.73
116,590.67
(84.31%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-5.91%)Baseline: 96,683.48
101,517.66
(89.61%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-3.81%)Baseline: 7,670.21
8,053.72
(91.61%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-2.06%)Baseline: 92,917.12
97,562.98
(93.28%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.42%)Baseline: 7,370.29
7,738.80
(95.63%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-1.76%)Baseline: 108,702.12
114,137.23
(93.56%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.35%)Baseline: 8,882.29
9,326.40
(95.57%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-8.79%)Baseline: 46,035.88
48,337.67
(86.87%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-4.93%)Baseline: 2,857.94
3,000.84
(90.54%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+2.79%)Baseline: 541,002.93
568,053.07
(97.89%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.34%)Baseline: 773.63
812.31
(94.91%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,626,306.00
(-0.28%)Baseline: 27,704,164.75
29,089,372.99
(94.97%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,544,446.00
(-0.07%)Baseline: 5,548,248.27
5,825,660.68
(95.17%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
175,119.00
(-1.46%)Baseline: 177,721.18
186,607.24
(93.84%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
175,768.00
(-1.54%)Baseline: 178,513.32
187,438.98
(93.77%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,302,182.00
(+0.12%)Baseline: 17,280,912.74
18,144,958.38
(95.36%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
298,417.00
(+0.62%)Baseline: 296,564.81
311,393.05
(95.83%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,595,770.00
(-0.01%)Baseline: 12,596,450.01
13,226,272.52
(95.23%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
719,597.00
(+0.43%)Baseline: 716,523.79
752,349.98
(95.65%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
🚨 view alert (🔔)
47,129.00
(+10.44%)Baseline: 42,672.52
44,806.14
(105.18%)

libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,753.06
16,339,840.71
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,371,271.00
(-2.22%)Baseline: 5,493,323.81
5,767,990.00
(93.12%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
311,806.00
(-64.55%)Baseline: 879,559.90
923,537.90
(33.76%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,547.00
(+0.20%)Baseline: 2,824,771.78
2,966,010.37
(95.43%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,452,844.00
(-0.60%)Baseline: 3,473,712.99
3,647,398.64
(94.67%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
126,147,648.00
(+0.64%)Baseline: 125,345,174.99
131,612,433.74
(95.85%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
12,002.00
(+1.70%)Baseline: 11,801.18
12,391.24
(96.86%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,930.00
(+2.77%)Baseline: 3,824.06
4,015.27
(97.88%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,073.00
(-0.89%)Baseline: 87,852.24
92,244.86
(94.39%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,022.00
(-1.10%)Baseline: 79,897.55
83,892.43
(94.19%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
50,375.00
(-1.19%)Baseline: 50,983.44
53,532.61
(94.10%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,956.00
(+3.56%)Baseline: 5,751.07
6,038.62
(98.63%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
🚨 view alert (🔔)
2,227.00
(+5.08%)Baseline: 2,119.30
2,225.27
(100.08%)

reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.02%)Baseline: 21,916.68
23,012.51
(95.25%)
🐰 View full continuous benchmarking report in Bencher

@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 the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/tests/privacy_tokens.rs`:
- Around line 288-297: The fresh token is written at the exact cutoff which can
race with a later call to tc_token_expiration_cutoff() inside prune_expired(),
causing flakiness; update the test to set the TcTokenEntry for fresh_key with a
small positive buffer beyond cutoff (e.g., cutoff + a small Duration) for both
token_timestamp and sender_timestamp so the token is safely newer than any
cutoff recomputed by prune_expired(); locate the put_tc_token call that writes
TcTokenEntry and adjust the timestamps accordingly.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2bc4cad-a345-446b-ab80-ef5cae58cab6

📥 Commits

Reviewing files that changed from the base of the PR and between 6a37102 and e133d4b.

📒 Files selected for processing (2)
  • tests/e2e/src/lib.rs
  • tests/e2e/tests/privacy_tokens.rs

Comment on lines +288 to +297
backend
.put_tc_token(
&fresh_key,
&TcTokenEntry {
token: vec![0x02],
token_timestamp: cutoff,
sender_timestamp: Some(cutoff),
},
)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential test flakiness due to boundary-value race condition.

The fresh_key token is stored with token_timestamp: cutoff (the exact boundary). However, prune_expired() internally calls tc_token_expiration_cutoff() again at line 299. If any time passes between line 274 and line 299, the new cutoff will be slightly higher, causing the "fresh" token to also be pruned since cutoff < new_cutoff.

Add a safe buffer to the fresh token timestamp:

Proposed fix
     backend
         .put_tc_token(
             &fresh_key,
             &TcTokenEntry {
                 token: vec![0x02],
-                token_timestamp: cutoff,
-                sender_timestamp: Some(cutoff),
+                token_timestamp: cutoff + 60, // 60s buffer avoids race with dynamic cutoff
+                sender_timestamp: Some(cutoff + 60),
             },
         )
         .await?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/tests/privacy_tokens.rs` around lines 288 - 297, The fresh token is
written at the exact cutoff which can race with a later call to
tc_token_expiration_cutoff() inside prune_expired(), causing flakiness; update
the test to set the TcTokenEntry for fresh_key with a small positive buffer
beyond cutoff (e.g., cutoff + a small Duration) for both token_timestamp and
sender_timestamp so the token is safely newer than any cutoff recomputed by
prune_expired(); locate the put_tc_token call that writes TcTokenEntry and
adjust the timestamps accordingly.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e133d4bfd5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +293 to +294
token_timestamp: cutoff,
sender_timestamp: Some(cutoff),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a safety margin for the "fresh" token timestamp

This test is time-flaky because prune_expired() computes a new cutoff internally, but the fixture marks the "fresh" row at exactly the old cutoff value. If wall-clock seconds advance between let cutoff = tc_token_expiration_cutoff() and prune_expired(), the new cutoff becomes greater and this row is legitimately pruned, making assert_eq!(deleted, 1) intermittently fail. Use a clear margin (for example cutoff + N) or prune with the same captured cutoff to keep the boundary deterministic.

Useful? React with 👍 / 👎.

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 25, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 53s —— View job


Code Review: TC Token E2E Tests

  • Read changed files and gather context
  • Review tests/e2e/src/lib.rs helper methods
  • Review tests/e2e/tests/privacy_tokens.rs test suite
  • Post review findings

Overall this is a well-structured test suite with good coverage across 7 scenarios. A few findings:

Issue: Flaky boundary in test_prune_expired_tc_tokens_removes_only_stale_entries

tests/e2e/tests/privacy_tokens.rs:293-294 — Both CodeRabbit and Codex flagged this and I can confirm it's a real issue after reading the implementation.

prune_expired() at src/features/tctoken.rs:66 calls tc_token_expiration_cutoff() again, which recomputes unix_now() - TC_TOKEN_TOTAL_DURATION. The fresh token is stored with token_timestamp: cutoff (the exact boundary value captured earlier at line 274). If even 1 second elapses between the two tc_token_expiration_cutoff() calls, the new cutoff shifts forward and the "fresh" token satisfies token_timestamp < new_cutoff, causing it to be pruned. The retain logic in in_memory.rs:483 keeps entries where token_timestamp >= cutoff_timestamp.

Fix: Add a buffer (e.g. cutoff + 60) to the fresh token's timestamps:

&TcTokenEntry {
    token: vec![0x02],
    token_timestamp: cutoff + 60,
    sender_timestamp: Some(cutoff + 60),
},

Fix this →

Nit: tc_token_key() — consider documenting the LID-first fallback

tests/e2e/src/lib.rs:182-190 — The helper's doc comment says "stores under the sender's LID when available, otherwise falls back to the phone-number user part." This matches the implementation, but if the production code's key-derivation logic ever diverges from this helper, tests would silently pass with the wrong key. Consider adding a brief note or // SYNC: marker pointing to the production code that computes the same key, so maintainers know to keep them in sync.

Observation: assert_no_event with matches! and client_a ownership

tests/e2e/tests/privacy_tokens.rs:163-169assert_no_event takes &mut self, which means client_a is mutably borrowed for the 5-second timeout window. This is fine here since nothing else touches client_a concurrently, but worth noting that this blocks any parallel assertions on client_a during that window.

Looks good

  • Test names are descriptive and cover the key scenarios (store, issue, reply, 463 nack, multi-device, reconnect persistence, expiry pruning).
  • The wait_for_tc_token polling helper with configurable timeout is a clean pattern consistent with existing helpers like reconnect_and_wait.
  • No real PII in test data — uses UUID-based names and synthetic keys.
  • The unique_push_name helper prevents test identity collisions.

Verdict: One actionable fix needed (the timestamp flakiness), rest looks good to merge.


…dler

Match two WhatsApp Web behaviors in handle_privacy_token_notification:

- Skip DB write when incoming token bytes are identical to stored ones
  (WAWebSetTcTokenChatAction: arrayBuffersEqualUNSAFE check)
- Re-subscribe presence after storing a new token for contacts with
  active subscriptions (WAWebHandlePrivacyTokensNotification:
  PresenceCollection.reSubscribeWhenActive)

@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 the current code and only fix it if needed.

Inline comments:
In `@src/handlers/notification.rs`:
- Around line 662-665: The handler currently does a non-atomic contains() check
on client.presence_subscriptions then calls client.presence().subscribe(from),
which races with unsubscribe() and can resurrect a removed subscription; replace
this open-coded check with a presence-layer API (e.g.,
presence::re_subscribe_when_active or a similarly named method on
client.presence()) that atomically checks whether the JID is still considered
active and only performs subscribe when appropriate; update the handler to call
that new method (passing from) instead of using
client.presence_subscriptions.lock().await.contains(from) +
client.presence().subscribe(from), and implement the atomic logic inside
src/features/presence.rs (refer to the patterns around lines shown in
presence.rs for guidance).
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0a735cf-a334-43b6-8afd-b514766d717f

📥 Commits

Reviewing files that changed from the base of the PR and between e133d4b and 4d8a851.

📒 Files selected for processing (1)
  • src/handlers/notification.rs

Comment thread src/handlers/notification.rs Outdated
…t resurrection

Extracts the check-then-subscribe pattern into a dedicated method that
sends the stanza without re-inserting into the tracking set. Fixes a
TOCTOU race in both the tc token handler and reconnect re-subscribe path.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d4aaf5a81

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/handlers/notification.rs Outdated
Comment on lines +609 to +611
// Skip if token bytes are identical
if existing.token == received.token {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update timestamp even when token bytes match

The new existing.token == received.token early return drops notifications that carry the same token bytes but a newer t value, so token_timestamp never advances in storage. That timestamp is later used by is_tc_token_expired and prune_expired, which means a refreshed token can be treated as stale and eventually pruned even though the server just re-sent it. This creates avoidable token expiry/regression for restricted-contact flows whenever WhatsApp reissues identical token bytes with a newer timestamp.

Useful? React with 👍 / 👎.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/handlers/notification.rs (1)

631-651: ⚠️ Potential issue | 🟠 Major

token_stored can be a false positive across backends.

At Line 635 and Line 650, token_stored is set on put_tc_token(...).await success, but the store contract does not distinguish “persisted” vs “no-op/skipped.” That makes the Line 660 re-subscribe condition inaccurate for no-op implementations and can trigger unnecessary subscribe traffic.

Possible mitigation in this handler
                 if let Err(e) = backend.put_tc_token(&sender_lid, &entry).await {
                     warn!(target: "Client/TcToken", "Failed to update tc_token for {}: {e}", sender_lid);
                 } else {
                     debug!(target: "Client/TcToken", "Updated tc_token for {} (t={})", sender_lid, received.timestamp);
-                    token_stored = true;
+                    if let Ok(Some(persisted)) = backend.get_tc_token(&sender_lid).await
+                        && persisted.token == entry.token
+                        && persisted.token_timestamp == entry.token_timestamp
+                    {
+                        token_stored = true;
+                    }
                 }

Longer-term, the cleaner fix is returning a typed write outcome from put_tc_token so callers can gate behavior without read-back.

Also applies to: 659-663

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 631 - 651, The handler sets
token_stored to true whenever backend.put_tc_token(...).await returns Ok, but
put_tc_token currently doesn't distinguish persisted vs no-op/skipped so
token_stored can be a false positive and cause unnecessary re-subscribe; change
the implementation and/or call site so that put_tc_token returns an explicit
outcome (e.g., enum WriteOutcome { Persisted, Noop, Error }) or at minimum a
boolean indicating whether a write actually persisted, then in this handler (the
block creating TcTokenEntry and the branch handling Ok(Some(_)) / Ok(None)) only
set token_stored = true when the returned outcome == Persisted (or boolean
true); keep warn()/debug() logging but gate the re-subscribe condition on the
persisted outcome rather than any Ok from backend.put_tc_token.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/handlers/notification.rs`:
- Around line 631-651: The handler sets token_stored to true whenever
backend.put_tc_token(...).await returns Ok, but put_tc_token currently doesn't
distinguish persisted vs no-op/skipped so token_stored can be a false positive
and cause unnecessary re-subscribe; change the implementation and/or call site
so that put_tc_token returns an explicit outcome (e.g., enum WriteOutcome {
Persisted, Noop, Error }) or at minimum a boolean indicating whether a write
actually persisted, then in this handler (the block creating TcTokenEntry and
the branch handling Ok(Some(_)) / Ok(None)) only set token_stored = true when
the returned outcome == Persisted (or boolean true); keep warn()/debug() logging
but gate the re-subscribe condition on the persisted outcome rather than any Ok
from backend.put_tc_token.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 438cd738-005a-45f5-8d07-e9f698ddfe9d

📥 Commits

Reviewing files that changed from the base of the PR and between 4d8a851 and 3d4aaf5.

📒 Files selected for processing (2)
  • src/features/presence.rs
  • src/handlers/notification.rs

When a notification carries the same token bytes but a newer timestamp,
update token_timestamp in storage to prevent premature pruning by
prune_expired.
@jlucaso1
jlucaso1 merged commit d07d904 into main Mar 25, 2026
8 checks passed
@jlucaso1
jlucaso1 deleted the test/tc-token branch March 25, 2026 21:21

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8637ffbccd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +617 to +620
if let Err(e) = backend.put_tc_token(&sender_lid, &refreshed).await {
warn!(target: "Client/TcToken", "Failed to refresh tc_token timestamp for {}: {e}", sender_lid);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat timestamp refresh as a stored tcToken update

When existing.token == received.token and the incoming timestamp is newer, this branch persists the refreshed entry but does not mark token_stored = true, so the follow-up re_subscribe_when_active path is skipped. That leaves active presence subscriptions unchanged even though token validity was extended, which can keep restricted-contact presence flows stuck on stale/no-token subscription state until a later reconnect or manual re-subscribe.

Useful? React with 👍 / 👎.

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.

1 participant