Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2c1bded
chore: support sharding/parallel runs, namespace scopes via `COMPLEME…
gamesguru Aug 18, 2026
ae35784
tests: validate COMPLEMENT_CRYPTO_NAMESPACE before use
gamesguru Aug 18, 2026
379f122
Update tests/namespace_test.go
gamesguru Aug 18, 2026
5aac0ba
fix(justfile): rebuild-rust-sdk injects real crypto feature via cargo…
gamesguru Aug 19, 2026
250527b
test(crypto): retry fallback-key claim to tolerate async SDK upload
gamesguru Aug 20, 2026
9277df7
Address PR review comments
gamesguru Aug 20, 2026
bef10d1
test: wait for spoofed event before checking state
gamesguru Aug 30, 2026
579b68b
fix: run JS SDK rebuild through Corepack
gamesguru Aug 30, 2026
3d5e330
chore: update `package.json` / `yarn.lock`
gamesguru Aug 30, 2026
aff1078
fix: handle empty sliding-sync timelines
gamesguru Aug 30, 2026
bb75ab8
fix: find JS events across room timelines
gamesguru Aug 30, 2026
1a5e807
wip
gamesguru Aug 31, 2026
d5df01c
fix spoof / mitm tests
gamesguru Aug 31, 2026
c6e4488
fix: retry fallback-key claim until the claimed key is the fallback key
gamesguru Aug 31, 2026
501dc55
fix: adapt Rust FFI wrapper to matrix-rust-sdk 26.08.25 (Element X's …
gamesguru Aug 31, 2026
0db37b0
fix: widen mustClaimFallbackKey retry window to 30s
gamesguru Aug 31, 2026
a34b8ea
wip
gamesguru Sep 1, 2026
8ba47e3
fix: make TestFallbackKeyIsUsedIfOneTimeKeysRunOut and TestChangingDe…
gamesguru Sep 1, 2026
39ba8df
fix(rust): subscribe to a room before consuming its timeline
gamesguru Sep 2, 2026
f9b94c5
fix: parallelize TestToDeviceMessagesAreBatched setup via raw key upload
gamesguru Sep 2, 2026
ca6fd7a
fixup! fix: parallelize TestToDeviceMessagesAreBatched setup via raw …
gamesguru Sep 2, 2026
8ddb69c
wip
gamesguru Sep 2, 2026
3fbe40d
wip try this
gamesguru Sep 2, 2026
41e8982
unskip flaky tests (run them all)
gamesguru Sep 2, 2026
f4f49ee
fix: give TestToDeviceMessagesAreProcessedInOrder's catch-up wait mor…
gamesguru Sep 2, 2026
b620acb
fixup! fix: give TestToDeviceMessagesAreProcessedInOrder's catch-up w…
gamesguru Sep 2, 2026
a745236
fix: restart Alice's sync loop after 504 storm in TestToDeviceMessage…
gamesguru Sep 3, 2026
99635e6
fix: bump backpaginated-event timeout from 1s to 5s in TestOnRejoinBo…
gamesguru Sep 3, 2026
fe850a5
fix: TestUnprocessedToDeviceMessagesArentLostOnRestart/rust two fixes
gamesguru Sep 3, 2026
1e23e41
fix: switch TestUnprocessedToDeviceMessagesArentLostOnRestart/rust to…
gamesguru Sep 3, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ __pycache__
/tests/chromedp/
/tests/rust_storage/
_temp_rust_sdk
# Locally-built cmd/rpc binary (COMPLEMENT_CRYPTO_RPC_BINARY target) - machine/toolchain-specific.
/rpc
2 changes: 1 addition & 1 deletion cmd/rpc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func main() {
go httpServer.Serve(listener)

// Wait for `Server.Close` to be called, then shut down the HTTP server.
<- srvDoneChannel
<-srvDoneChannel
fmt.Println("Server starting clean shutdown...")
err = httpServer.Shutdown(context.Background())
if err != nil {
Expand Down
13 changes: 9 additions & 4 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ type Client interface {
// SendMessage sends the given text as an encrypted/unencrypted message in the room, depending
// if the room is encrypted or not. Returns the event ID of the sent event, so MUST BLOCK until the event has been sent.
// If the event cannot be sent, returns an error.
SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error)
//
// An optional timeout may be passed to override the implementation's default wait-for-local-echo
// timeout (only the first value is used). This exists for stress tests with unusually large
// fan-out (e.g. sending to a room with 100+ recipients, where session establishment can
// legitimately take longer than the default) - ordinary call sites should omit it.
SendMessage(t ct.TestLike, roomID, text string, timeout ...time.Duration) (eventID string, err error)
// Wait until an event is seen in the given room. The checker functions can be custom or you can use
// a pre-defined one like api.CheckEventHasMembership, api.CheckEventHasBody, or api.CheckEventHasEventID.
WaitUntilEventInRoom(t ct.TestLike, roomID string, checker func(e Event) bool) Waiter
Expand Down Expand Up @@ -228,10 +233,10 @@ func (c *LoggedClient) IsRoomEncrypted(t ct.TestLike, roomID string) (bool, erro
return c.Client.IsRoomEncrypted(t, roomID)
}

func (c *LoggedClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
func (c *LoggedClient) SendMessage(t ct.TestLike, roomID, text string, timeout ...time.Duration) (eventID string, err error) {
t.Helper()
c.Logf(t, "%s SendMessage %s => %s", c.logPrefix(), roomID, text)
eventID, err = c.Client.SendMessage(t, roomID, text)
eventID, err = c.Client.SendMessage(t, roomID, text, timeout...)
c.Logf(t, "%s SendMessage %s => %s %s", c.logPrefix(), roomID, eventID, err)
return
}
Expand All @@ -254,7 +259,7 @@ func (c *LoggedClient) Backpaginate(t ct.TestLike, roomID string, count int) err
result = fmt.Sprintf("ERROR: %v", err)
}
c.Logf(t, "%s Backpaginate %d %s => %s", c.logPrefix(), count, roomID, result)

return err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/js/js-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"buffer": "^6.0.3",
"matrix-js-sdk": "^41.0.0",
"matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk#develop",

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: The matrix-js-sdk dependency points at a moving branch (#develop) instead of a pinned commit. While yarn.lock currently pins commit aa1aeed63b5ef0327f0442d63e31efca58df46b0, any yarn add/yarn install that regenerates the lockfile will silently resolve the latest develop commit, so the SDK tested by this repo can drift under CI and produce non-reproducible/flaky test results. Consider pinning the resolved commit SHA in package.json (e.g. ...matrix-js-sdk#aa1aeed...) and periodically bumping it deliberately, keeping the develop branch as the documented workflow in rebuild_js_sdk.sh.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/api/js/js-sdk/package.json, line 13:

<comment>The matrix-js-sdk dependency points at a moving branch (`#develop`) instead of a pinned commit. While `yarn.lock` currently pins commit `aa1aeed63b5ef0327f0442d63e31efca58df46b0`, any `yarn add`/`yarn install` that regenerates the lockfile will silently resolve the latest `develop` commit, so the SDK tested by this repo can drift under CI and produce non-reproducible/flaky test results. Consider pinning the resolved commit SHA in `package.json` (e.g. `...matrix-js-sdk#aa1aeed...`) and periodically bumping it deliberately, keeping the `develop` branch as the documented workflow in `rebuild_js_sdk.sh`.</comment>

<file context>
@@ -10,7 +10,7 @@
   "dependencies": {
     "buffer": "^6.0.3",
-    "matrix-js-sdk": "^41.0.0",
+    "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk#develop",
     "vite": "^6.4.2"
   },
</file context>
Suggested change
"matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk#develop",
"matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk#aa1aeed63b5ef0327f0442d63e31efca58df46b0",

"vite": "^6.4.2"
},
"packageManager": "yarn@1.22.22"
Expand Down
61 changes: 23 additions & 38 deletions internal/api/js/js-sdk/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# yarn lockfile v1


"@babel/runtime@^7.12.5":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/runtime@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-8.0.0.tgz#d7bd513e6843662346552c2798ab895716cf97f2"
integrity sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==

"@esbuild/aix-ppc64@0.25.12":
version "0.25.12"
Expand Down Expand Up @@ -137,10 +137,10 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==

"@matrix-org/matrix-sdk-crypto-wasm@^18.2.0":
version "18.3.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz#a5206792a78a3ebebd901662c65753e0914d204a"
integrity sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==
"@matrix-org/matrix-sdk-crypto-wasm@^18.4.0":
version "18.7.0"
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.7.0.tgz#81cf44109b91439a57c500e960c6dac03ea77835"
integrity sha512-IiO8YrahwBN23iii4OBS6AUIEDZePa72bK7WN/DBjOXPc6g/wt891vn0vxmlopTahqFFOz/LErcHisdmzKVTsg==

"@rollup/rollup-android-arm-eabi@4.60.4":
version "4.60.4"
Expand Down Expand Up @@ -307,10 +307,10 @@ buffer@^6.0.3:
base64-js "^1.3.1"
ieee754 "^1.2.1"

content-type@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
content-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-3.0.0.tgz#c946981e09aba276f5f5b21223bd0c01fded9c15"
integrity sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw==

esbuild@^0.25.0:
version "0.25.12"
Expand Down Expand Up @@ -369,11 +369,6 @@ is-network-error@^1.3.0:
resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.3.2.tgz#9460bc30f8419a4bca77114f4de88a3ee5e0c519"
integrity sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==

jwt-decode@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==

loglevel@^1.9.2:
version "1.9.2"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08"
Expand All @@ -384,29 +379,26 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==

matrix-js-sdk@^41.0.0:
version "41.5.0"
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-41.5.0.tgz#8519fc9a4626be7fe3f6ebe1ed13019e0b097314"
integrity sha512-CK3h+qQJ4wkVEUgEWc5MdLjccXyiFqncCC53P+auqOhnX2U6tAFsRfnbML1QQiKIsFMzqTrAnF/4a5LUUOIeXg==
"matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk#develop":
version "42.2.0"
resolved "https://github.com/matrix-org/matrix-js-sdk#aa1aeed63b5ef0327f0442d63e31efca58df46b0"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^18.2.0"
"@babel/runtime" "^8.0.0"
"@matrix-org/matrix-sdk-crypto-wasm" "^18.4.0"
another-json "^0.2.0"
bs58 "^6.0.0"
content-type "^1.0.4"
jwt-decode "^4.0.0"
content-type "^3.0.0"
loglevel "^1.9.2"
matrix-events-sdk "0.0.1"
matrix-widget-api "^1.16.1"
oidc-client-ts "^3.0.1"
matrix-widget-api "^1.18.0"
p-retry "8"
sdp-transform "^3.0.0"
unhomoglyph "^1.0.6"

matrix-widget-api@^1.16.1:
version "1.17.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz#2336de2186fe70d8bd741c1603c162f60b2099c2"
integrity sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==
matrix-widget-api@^1.18.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.19.0.tgz#77819e0307adbba972ae8178c83b77fada896e29"
integrity sha512-Qe7L6G1mQJphBC47/Bzp5ljPIK1UARgLiuGgS+/KmEs/eaNHkQ3+ArD0/0kvMV+d6kCGAZ9OKRqsnuPjizI+tw==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
Expand All @@ -416,13 +408,6 @@ nanoid@^3.3.11:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==

oidc-client-ts@^3.0.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz#7cad95ba7213cb93b7c141965ce03ad658baa30f"
integrity sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==
dependencies:
jwt-decode "^4.0.0"

p-retry@8:
version "8.0.0"
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-8.0.0.tgz#1505faf14942326e18d4091f5d605bae65634e7b"
Expand Down
23 changes: 15 additions & 8 deletions internal/api/js/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,12 @@ func (c *JSClient) GetEvent(t ct.TestLike, roomID, eventID string) (*api.Event,
// }
// else just returns { event }
evSerialised, err := chrome.RunAsyncFn[string](t, c.browser.Ctx, fmt.Sprintf(`
return JSON.stringify(window.__client.getRoom("%s")?.getLiveTimeline()?.getEvents().filter((ev, i) => {
console.log("MustGetEvent["+i+"] => " + ev.getId()+ " " + JSON.stringify(ev.toJSON()));
return ev.getId() === "%s";
})[0].toJSON());
const room = window.__client.getRoom("%s");
const ev = room?.findEventById("%s");
if (!ev) {
throw new Error("event not found in room timelines");
}
return JSON.stringify(ev.toJSON());
`, roomID, eventID))
if err != nil {
return nil, fmt.Errorf("failed to get event %s: %s", eventID, err)
Expand Down Expand Up @@ -561,9 +563,11 @@ func (c *JSClient) GetEventShield(t ct.TestLike, roomID, eventID string) (*api.E
// shieldReason: 0 ... 7
// }
encryptionInfoSerialised, err := chrome.RunAsyncFn[string](t, c.browser.Ctx, fmt.Sprintf(`
const ev = window.__client.getRoom("%s")?.getLiveTimeline()?.getEvents().filter((ev, i) => {
return ev.getId() === "%s";
})[0];
const room = window.__client.getRoom("%s");
const ev = room?.findEventById("%s");
if (!ev) {
throw new Error("event not found in room timelines");
}
const encryptionInfo = await window.__client.getCrypto().getEncryptionInfoForEvent(ev);
return JSON.stringify(encryptionInfo);
`, roomID, eventID))
Expand Down Expand Up @@ -688,8 +692,11 @@ func (c *JSClient) IsRoomEncrypted(t ct.TestLike, roomID string) (bool, error) {
return *isEncrypted, nil
}

func (c *JSClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
func (c *JSClient) SendMessage(t ct.TestLike, roomID, text string, timeout ...time.Duration) (eventID string, err error) {
t.Helper()
// JS has no internal wait-for-local-echo timeout to override (chrome.RunAsyncFn awaits
// the underlying JS promise directly), so a caller-supplied timeout is a no-op here.
_ = timeout
res, err := chrome.RunAsyncFn[map[string]interface{}](t, c.browser.Ctx, fmt.Sprintf(`
return await window.__client.sendMessage("%s", {
"msgtype": "m.text",
Expand Down
51 changes: 43 additions & 8 deletions internal/api/rust/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

// LogTarget is the name of the `target` we use in logToFile: it is in effect a fake "crate" that we tell the
// rust-sdk is producing the logs.
const LogTarget = "complement_crypto";
const LogTarget = "complement_crypto"

func DeleteOldLogs(prefix string) {
// delete old log files
Expand All @@ -39,7 +39,7 @@ func SetupLogs(prefix string) {
// log new files
matrix_sdk_ffi.InitPlatform(matrix_sdk_ffi.TracingConfiguration{
LogLevel: matrix_sdk_ffi.LogLevelTrace,
ExtraTargets: []string {LogTarget},
ExtraTargets: []string{LogTarget},
WriteToStdoutOrSystem: false,
WriteToFiles: &matrix_sdk_ffi.TracingFileConfiguration{
Path: "./logs",
Expand Down Expand Up @@ -104,11 +104,10 @@ func NewRustClient(t ct.TestLike, opts api.ClientCreationOpts) (api.Client, erro
xprocessName := opts.GetExtraOption(CrossProcessStoreLocksHolderName, "").(string)
if xprocessName != "" {
t.Logf("setting cross process store locks holder name=%s", xprocessName)
ab = ab.CrossProcessLockConfig(matrix_sdk_ffi.CrossProcessLockConfigMultiProcess { xprocessName })
ab = ab.CrossProcessLockConfig(matrix_sdk_ffi.CrossProcessLockConfigMultiProcess{xprocessName})
}
// @alice:hs1, FOOBAR => alice_hs1_FOOBAR
username := strings.Replace(opts.UserID[1:], ":", "_", -1) + "_" + opts.DeviceID
ab = ab.Username(username)

sessionPath := "rust_storage/" + username
storeKey := []byte("my_secret_thirty-two_byte_string")
Expand Down Expand Up @@ -675,11 +674,15 @@ func (c *RustClient) Type() api.ClientTypeLang {
return api.ClientTypeRust
}

func (c *RustClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
func (c *RustClient) SendMessage(t ct.TestLike, roomID, text string, timeout ...time.Duration) (eventID string, err error) {
c.FFISpan.Enter()
defer c.FFISpan.Exit()

t.Helper()
waitFor := 11 * time.Second
if len(timeout) > 0 {
waitFor = timeout[0]
}
var isChannelClosed atomic.Bool
ch := make(chan bool)
// we need a timeline listener before we can send messages, AND that listener must be attached to the
Expand Down Expand Up @@ -723,8 +726,8 @@ func (c *RustClient) SendMessage(t ct.TestLike, roomID, text string) (eventID st
}
timeline.Send(matrix_sdk_ffi.MessageEventContentFromHtml(text, text))
select {
case <-time.After(11 * time.Second):
err = fmt.Errorf("SendMessage(rust) %s: timed out after 11s", c.userID)
case <-time.After(waitFor):
err = fmt.Errorf("SendMessage(rust) %s: timed out after %s", c.userID, waitFor)
return
case <-ch:
return
Expand Down Expand Up @@ -829,7 +832,7 @@ func (c *RustClient) Logf(t ct.TestLike, format string, args ...interface{}) {
func (c *RustClient) logToFile(t ct.TestLike, format string, args ...interface{}) {
c.FFISpan.Enter()
defer c.FFISpan.Exit()
matrix_sdk_ffi.LogEvent("rust.go", &zero, matrix_sdk_ffi.LogLevelInfo, LogTarget + "::" + t.Name(), fmt.Sprintf(format, args...))
matrix_sdk_ffi.LogEvent("rust.go", &zero, matrix_sdk_ffi.LogLevelInfo, LogTarget+"::"+t.Name(), fmt.Sprintf(format, args...))
}

func (c *RustClient) ensureListening(t ct.TestLike, roomID string) {
Expand Down Expand Up @@ -863,6 +866,22 @@ func (c *RustClient) ensureListening(t ct.TestLike, roomID string) {
return
}

// Without an explicit room subscription, the sliding sync `pos` for this room
// only ever advances via whatever small timeline_limit the "all rooms" list
// uses for previews (e.g. 1-10). That's fine when events trickle in one at a
// time, but under concurrent load a burst of room state (joins, membership
// changes) plus messages can exceed that window in a single poll; the server
// correctly reports `limited: true` with a `prev_batch` pointing before the
// gap, but nothing here ever triggers backpagination to close it, so a
// message that fell into the truncated portion is silently never delivered.
// Subscribing before we start consuming the timeline requests a much larger
// window (SDK default: 20) on every subsequent poll for this room, giving
// real headroom so this doesn't happen in the first place - this mirrors
// what a real client does when a room is actually open/visible.
if err := c.syncService.RoomListService().SubscribeToRooms([]string{roomID}); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When ensureListening finds a cached room before StartSyncing, c.syncService is nil and this call panics instead of installing the waiter. Call SubscribeToRoom(t, roomID), which already handles an uninitialized sync service and preserves the FFI span handling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/api/rust/rust.go, line 877:

<comment>When `ensureListening` finds a cached room before `StartSyncing`, `c.syncService` is nil and this call panics instead of installing the waiter. Call `SubscribeToRoom(t, roomID)`, which already handles an uninitialized sync service and preserves the FFI span handling.</comment>

<file context>
@@ -862,6 +862,22 @@ func (c *RustClient) ensureListening(t ct.TestLike, roomID string) {
+	// window (SDK default: 20) on every subsequent poll for this room, giving
+	// real headroom so this doesn't happen in the first place - this mirrors
+	// what a real client does when a room is actually open/visible.
+	if err := c.syncService.RoomListService().SubscribeToRooms([]string{roomID}); err != nil {
+		c.Logf(t, "[%s]ensureListening[%s] failed to subscribe to room: %s", c.userID, roomID, err)
+	}
</file context>
Suggested change
if err := c.syncService.RoomListService().SubscribeToRooms([]string{roomID}); err != nil {
if err := c.SubscribeToRoom(t, roomID); err != nil {

c.Logf(t, "[%s]ensureListening[%s] failed to subscribe to room: %s", c.userID, roomID, err)
}

c.Logf(t, "[%s]AddTimelineListener[%s]", c.userID, roomID)
// we need a timeline listener before we can send messages. Ensure we insert the initial
// set of items prior to handling updates. If we don't wait, we risk the listener firing
Expand Down Expand Up @@ -936,6 +955,22 @@ func (c *RustClient) ensureListening(t ct.TestLike, roomID string) {
ev := timelineItemToEvent(x.Value)
timeline = slices.Insert(timeline, 0, ev)
newEvents = append(newEvents, ev)
case matrix_sdk_ffi.TimelineDiffClear:
timeline = make([]*api.Event, 0)
c.logToFile(t, "[%s]_______ CLEAR", c.userID)
case matrix_sdk_ffi.TimelineDiffPopFront:
if len(timeline) > 0 {
timeline = slices.Delete(timeline, 0, 1)
}
case matrix_sdk_ffi.TimelineDiffPopBack:
if len(timeline) > 0 {
timeline = slices.Delete(timeline, len(timeline)-1, len(timeline))
}
case matrix_sdk_ffi.TimelineDiffTruncate:
n := int(x.Length)
if n < len(timeline) {
timeline = timeline[:n]
}
default:
t.Logf("Unhandled TimelineDiff change %v", d)
}
Expand Down
15 changes: 10 additions & 5 deletions internal/deploy/rpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ func (r *LanguageBindings) MustCreateClient(t ct.TestLike, cfg api.ClientCreatio
ct.Fatalf(t, "RPC (%s): failed to create RPC client: %s", contextID, err)
}
return &RPCClient{
client: client,
lang: r.clientType,
rpcCmd: rpcCmd,
client: client,
lang: r.clientType,
rpcCmd: rpcCmd,
logsFlushed: logsFlushed,
}
case <-time.After(time.Second):
Expand Down Expand Up @@ -190,7 +190,7 @@ func (c *RPCClient) Close(t ct.TestLike) {

// Wait for the goroutine that copies stdout to the logs to complete
t.Logf("RPCClient.Close: waiting for server to shut down")
<- c.logsFlushed
<-c.logsFlushed
t.Logf("RPCClient.Close: done")
}

Expand Down Expand Up @@ -277,11 +277,16 @@ func (c *RPCClient) IsRoomEncrypted(t ct.TestLike, roomID string) (bool, error)
}

// SendMessage tries to send the message, but can fail.
func (c *RPCClient) SendMessage(t ct.TestLike, roomID, text string) (eventID string, err error) {
func (c *RPCClient) SendMessage(t ct.TestLike, roomID, text string, timeout ...time.Duration) (eventID string, err error) {
var to time.Duration
if len(timeout) > 0 {
to = timeout[0]
}
err = c.client.Call("Server.SendMessage", RPCSendMessage{
TestName: t.Name(),
RoomID: roomID,
Text: text,
Timeout: to,
}, &eventID)
return
}
Expand Down
Loading