Skip to content

Commit 11a2fa2

Browse files
committed
test(ios): e2e orchestration flake mitigation
Fix reconnect send race after WS grace recovery under saturated macOS CI load: assign mocha-remote client before connection emit, single server-side pull-coverage, non-throwing Server.send, and Jet retry on No client connected. Regenerate yarn.lock after patch updates; document in okf-bundle.
1 parent 1af1f3b commit 11a2fa2

8 files changed

Lines changed: 137 additions & 132 deletions

File tree

.yarn/patches/jet-npm-0.9.0-dev.13-3321aeea6e.patch

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
diff --git a/lib/commonjs/cli.js b/lib/commonjs/cli.js
2-
index 35c04da87c63a79bc26707aaffe3156fb98eca79..5ef301146d35adf38a430f26d78a66f196d781f5 100644
2+
index 35c04da87c63a79bc26707aaffe3156fb98eca79..41de2f4918095084eca259e334d4deaa82f4bf96 100644
33
--- a/lib/commonjs/cli.js
44
+++ b/lib/commonjs/cli.js
5-
@@ -72,22 +72,68 @@ function cleanup() {
5+
@@ -72,22 +72,64 @@ function cleanup() {
66
});
77
}
88
async function startServer(server, config, after) {
@@ -22,10 +22,6 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..5ef301146d35adf38a430f26d78a66f1
2222
+ reconnectGraceTimer = null;
2323
+ pendingDisconnectCode = null;
2424
+ console.warn(`[jet-ws] reconnect_recovered code=${recoveredCode} elapsed_ms=${elapsedMs}`);
25-
+ if (config.coverage && server.send) {
26-
+ console.warn(`[jet-coverage] proactive pull-coverage after reconnect`);
27-
+ server.send({ action: 'pull-coverage' });
28-
+ }
2925
+ }
3026
});
3127
server.on('disconnection', (_, code, reason) => {
@@ -75,7 +71,7 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..5ef301146d35adf38a430f26d78a66f1
7571
});
7672
server.on('error', error => {
7773
if (error instanceof _mochaRemoteServer.ClientError) {
78-
@@ -100,6 +146,14 @@ async function startServer(server, config, after) {
74+
@@ -100,6 +142,14 @@ async function startServer(server, config, after) {
7975
cleanup();
8076
}
8177
});
@@ -90,7 +86,7 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..5ef301146d35adf38a430f26d78a66f1
9086
cleanupTasks.add(async () => {
9187
if (server.listening) {
9288
await server.stop();
93-
@@ -129,8 +183,13 @@ function attachHttpServer(wss) {
89+
@@ -129,8 +179,13 @@ function attachHttpServer(wss) {
9490
});
9591
req.on('end', () => {
9692
try {
@@ -105,7 +101,7 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..5ef301146d35adf38a430f26d78a66f1
105101
res.end(JSON.stringify({
106102
message: 'OK'
107103
}));
108-
@@ -302,12 +361,11 @@ function attachHttpServer(wss) {
104+
@@ -302,12 +357,11 @@ function attachHttpServer(wss) {
109105
slow: finalConfig.slow
110106
});
111107
return startServer(server, finalConfig, target.after).then(() => {
@@ -120,68 +116,3 @@ index 35c04da87c63a79bc26707aaffe3156fb98eca79..5ef301146d35adf38a430f26d78a66f1
120116
await cleanup();
121117
process.exit(failures > 0 ? 1 : 0);
122118
});
123-
diff --git a/lib/commonjs/index.js b/lib/commonjs/index.js
124-
index a72380ed2ace01b19b028bb8e6f9d29dc3affbcd..030475cff75349b2b7f820cf8a3556fb1d7bea45 100644
125-
--- a/lib/commonjs/index.js
126-
+++ b/lib/commonjs/index.js
127-
@@ -63,15 +63,7 @@ function JetProvider(props) {
128-
});
129-
after(async () => {
130-
if (_config.coverage) {
131-
- const coverage = global.__coverage__ ?? {};
132-
- const url = (props.url ?? 'ws://localhost:8090').replace('ws://', 'http://') + '/coverage';
133-
- return fetch(url, {
134-
- method: 'POST',
135-
- headers: {
136-
- 'Content-Type': 'application/json'
137-
- },
138-
- body: JSON.stringify(coverage)
139-
- });
140-
+ return client.uploadCoverage();
141-
}
142-
return Promise.resolve();
143-
});
144-
diff --git a/lib/module/index.js b/lib/module/index.js
145-
index 897b62288ae128e1ce071142fa3de136caa99239..2f8dd9edf79f6c095b4357307bdac5ea1efa00eb 100644
146-
--- a/lib/module/index.js
147-
+++ b/lib/module/index.js
148-
@@ -51,15 +51,7 @@ export function JetProvider(props) {
149-
});
150-
after(async () => {
151-
if (_config.coverage) {
152-
- const coverage = global.__coverage__ ?? {};
153-
- const url = (props.url ?? 'ws://localhost:8090').replace('ws://', 'http://') + '/coverage';
154-
- return fetch(url, {
155-
- method: 'POST',
156-
- headers: {
157-
- 'Content-Type': 'application/json'
158-
- },
159-
- body: JSON.stringify(coverage)
160-
- });
161-
+ return client.uploadCoverage();
162-
}
163-
return Promise.resolve();
164-
});
165-
diff --git a/src/index.tsx b/src/index.tsx
166-
index 1b28aca3fe817fee3e8701ac5096df6fc608bc39..17c631309f88a8017a9fd01e3420df2b4bab140e 100644
167-
--- a/src/index.tsx
168-
+++ b/src/index.tsx
169-
@@ -82,17 +82,7 @@ export function JetProvider(props: JetProviderProps): React.JSX.Element {
170-
});
171-
after(async () => {
172-
if (_config.coverage) {
173-
- const coverage = (global as any).__coverage__ ?? {};
174-
- const url =
175-
- (props.url ?? 'ws://localhost:8090').replace('ws://', 'http://') +
176-
- '/coverage';
177-
- return fetch(url, {
178-
- method: 'POST',
179-
- headers: {
180-
- 'Content-Type': 'application/json',
181-
- },
182-
- body: JSON.stringify(coverage),
183-
- });
184-
+ return client.uploadCoverage();
185-
}
186-
return Promise.resolve();
187-
});

.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
diff --git a/dist/Server.js b/dist/Server.js
2-
index ad9debe2086ab9b96e97a69aec966da8114ad102..1c7856a7b8983cc71bad33465bae1e8182bb016d 100644
2+
index ad9debe2086ab9b96e97a69aec966da8114ad102..51cc18658e11d5aec5d5a76c4eb5f965b32a587d 100644
33
--- a/dist/Server.js
44
+++ b/dist/Server.js
5-
@@ -73,6 +73,10 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
5+
@@ -73,38 +73,59 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
66
/** The options to send to the next connecting running client */
77
this.clientOptions = {};
88
this._listening = false;
@@ -12,32 +12,53 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..1c7856a7b8983cc71bad33465bae1e81
1212
+ this._maxInboundBufferBytes = 50 * 1024 * 1024;
1313
this.handleConnection = (ws, req) => {
1414
this.debug("Client connected");
15-
// Check that the protocol matches
16-
@@ -88,6 +92,15 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
15+
- // Check that the protocol matches
16+
const expectedProtocol = `mocha-remote-${this.config.id}`;
17+
- ws.on("close", (code, reason) => {
18+
- this.emit("disconnection", ws, code, reason.toString());
19+
- });
20+
- // Signal that a client has connected
21+
- this.emit("connection", ws, req);
22+
- // Disconnect if the protocol mismatch
23+
+
24+
if (ws.protocol !== expectedProtocol) {
25+
- // Protocol mismatch - close the connection
1726
ws.close(1002, `Expected "${expectedProtocol}" protocol got "${ws.protocol}"`);
1827
return;
1928
}
29+
+
30+
+ const wasReconnecting = Boolean(this._clientResetTimer);
2031
+ if (this._clientResetTimer) {
2132
+ clearTimeout(this._clientResetTimer);
2233
+ this._clientResetTimer = null;
2334
+ console.warn(`[mocha-remote-ws] reconnect_recovered preserving_runner`);
24-
+ if (this.runner && !this._awaitingInitialClientRun) {
25-
+ console.warn(`[jet-coverage] server send pull-coverage after reconnect`);
26-
+ this.send({ action: "pull-coverage" });
27-
+ }
2835
+ }
36+
+
2937
if (this.client) {
3038
this.debug("A client was already connected");
3139
this.client.close(1013 /* try again later */, "Got a connection from another client");
32-
@@ -96,15 +109,22 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
40+
- // Reset the server to prepare for the incoming client
41+
this.handleReset();
3342
}
34-
// Hang onto the client
43+
- // Hang onto the client
44+
+
3545
this.client = ws;
3646
+ this._inboundBuffer = "";
47+
+ this.client.on("close", (code, reason) => {
48+
+ this.emit("disconnection", ws, code, reason.toString());
49+
+ });
3750
this.client.on("message", this.handleMessage.bind(this, this.client));
3851
- this.client.once("close", this.handleReset);
52+
- // If we already have a runner, it can run now that we have a client
3953
+ this.client.once("close", (code) => this.handleClientDisconnect(code));
40-
// If we already have a runner, it can run now that we have a client
54+
+
55+
+ this.emit("connection", ws, req);
56+
+
57+
+ if (wasReconnecting && this.runner && !this._awaitingInitialClientRun) {
58+
+ console.warn(`[jet-coverage] server send pull-coverage after reconnect`);
59+
+ this.send({ action: "pull-coverage" });
60+
+ }
61+
+
4162
if (this.runner) {
4263
- if (this.clientOptions) {
4364
- this.send({ action: "run", options: this.clientOptions });
@@ -56,7 +77,7 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..1c7856a7b8983cc71bad33465bae1e81
5677
}
5778
}
5879
else if (this.config.autoRun) {
59-
@@ -115,8 +135,29 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
80+
@@ -115,8 +136,29 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
6081
}
6182
};
6283
this.handleMessage = (ws, message) => {
@@ -87,7 +108,7 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..1c7856a7b8983cc71bad33465bae1e81
87108
if (typeof msg.action !== "string") {
88109
throw new Error("Expected message to have an action property");
89110
}
90-
@@ -130,6 +171,18 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
111+
@@ -130,6 +172,18 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
91112
throw new Error("Received a message from the client, but server wasn't running");
92113
}
93114
}
@@ -106,7 +127,7 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..1c7856a7b8983cc71bad33465bae1e81
106127
else if (msg.action === "error") {
107128
if (typeof msg.message !== "string") {
108129
throw new Error("Expected 'error' action to have an error argument with a message");
109-
@@ -152,10 +205,45 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
130+
@@ -152,10 +206,45 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
110131
}
111132
}
112133
};
@@ -152,46 +173,21 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..1c7856a7b8983cc71bad33465bae1e81
152173
// Forget everything about the runner and the client
153174
const { runner, client } = this;
154175
delete this.runner;
155-
@@ -263,6 +351,7 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
176+
@@ -263,6 +352,7 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
156177
// this.runner = new Mocha.Runner(this.suite, this.options.delay || false);
157178
// TODO: Stub this to match the Runner's interface even better
158179
this.runner = new FakeRunner_1.FakeRunner();
159180
+ this._awaitingInitialClientRun = true;
160181
// Attach event listeners to update stats
161182
(0, stats_collector_1.createStatsCollector)(this.runner);
162183
// Set the client options, to be passed to the next running client
163-
diff --git a/dist/serialization.js b/dist/serialization.js
164-
index b2dd290111197907d125e3151a4a965e9fe0528d..a52262f30458c2d0406e0809252f345ce8a2b60e 100644
165-
--- a/dist/serialization.js
166-
+++ b/dist/serialization.js
167-
@@ -45,9 +45,29 @@ function createReviver() {
168-
};
169-
}
170-
exports.createReviver = createReviver;
171-
+function isIncompleteParseError(err) {
172-
+ if (!(err instanceof SyntaxError)) {
173-
+ return false;
174-
+ }
175-
+ const message = err.message || "";
176-
+ return (/Unexpected end of JSON input|Unterminated string in JSON|Unexpected token/.test(message));
177-
+}
178-
+exports.isIncompleteParseError = isIncompleteParseError;
179-
function deserialize(text, reviver = createReviver()) {
180-
debug("Deserializing %s", text);
181-
return flatted_1.default.parse(text, reviver);
182-
}
183-
exports.deserialize = deserialize;
184-
+function tryDeserialize(text, reviver = createReviver()) {
185-
+ try {
186-
+ return { ok: true, value: deserialize(text, reviver) };
187-
+ }
188-
+ catch (err) {
189-
+ if (err instanceof Error && isIncompleteParseError(err)) {
190-
+ return { ok: false, incomplete: true, error: err };
191-
+ }
192-
+ return { ok: false, incomplete: false, error: err };
193-
+ }
194-
+}
195-
+exports.tryDeserialize = tryDeserialize;
196-
//# sourceMappingURL=serialization.js.map
197-
\ No newline at end of file
184+
@@ -370,7 +460,8 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
185+
this.client.send(data);
186+
}
187+
else {
188+
- throw new Error("No client connected");
189+
+ const readyState = this.client?.readyState ?? "no-client";
190+
+ console.warn(`[mocha-remote-ws] send skipped action=${msg.action} readyState=${readyState}`);
191+
}
192+
}
193+
/**

okf-bundle/ci-workflows/android.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,31 @@ Full inventory: [detox-patches.md](detox-patches.md).
4242

4343
**Orchestration retry:** `firebase.test.js` still retries on `RETRYABLE_DISCONNECT`; attempt 2 can pass all tests even when attempt 1's teardown logged adb noise.
4444

45+
## CI failure: Jet JSON / WS protocol desync (Unexpected end of JSON input)
46+
47+
Observed on Android CI under load (e.g. [run 28044822049](https://github.com/invertase/react-native-firebase/actions/runs/28044822049)): Jet runs only ~24 tests before the mocha-remote transport desyncs — often after a transient 1006 under high `loadavg`.
48+
49+
**Symptom**
50+
51+
```
52+
[🟥] Unexpected end of JSON input
53+
[mocha-remote-ws] parse_buffering ...
54+
```
55+
56+
**Cause** — TCP/WebSocket chunks can split JSON frames; under I/O pressure, partial reads are more common. Unbuffered `JSON.parse` on each chunk corrupts the protocol stream.
57+
58+
**Mitigations (patches + orchestration)**
59+
60+
| Change | Location |
61+
|--------|----------|
62+
| Inbound parse buffer + `tryDeserialize` / `parse_skip` logging | mocha-remote-server patch → `Server.js` |
63+
| Outbound queue flushed on reconnect | mocha-remote-client patch |
64+
| `JET_PROTOCOL_ERROR_RE` → retryable Jet session (attempt 2) | `tests/e2e/firebase.test.js` |
65+
| Cold-boot ready wait + post-boot settle before Jet attempt 1 | `firebase.test.js` (`waitForAndroidEmulatorReady`, `RNFB_ANDROID_BOOT_SETTLE_MS`) |
66+
| Load gate before starting Jet (threshold 5, 3 consecutive polls) | `firebase.test.js` |
67+
68+
**Patch workflow reminder** — after editing `tests/node_modules/jet` or `mocha-remote-*`, run `yarn patch-commit` **and** `yarn install` from repo root so `yarn.lock` patch hashes update. CI applies patches from the lockfile, not live `node_modules` edits.
69+
4570
## Troubleshooting
4671

4772
| Symptom | Likely cause |

okf-bundle/ci-workflows/detox-patches.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Patches are maintained in-repo (including by agents). Prefer **editing the patch
1717
| Ignore missing adb reverse on teardown | `ADB.js` | Android | Jet WS 1006 triggers mid-run `reverse --remove` → adb exit 1 |
1818
| **2× device-registry lock stale** | `ExclusiveLockfile.js` | iOS, macOS, Android | `proper-lockfile` `ECOMPROMISED` before tests start |
1919

20-
Related non-Detox patches: `jet`, `mocha-remote-client`, `mocha-remote-server` — WS reconnect grace, coverage handshake (`coverage-ready` / `pull-coverage`), client keepalive. See [coverage design](../testing/coverage-design.md) and [iOS CI — issues 6–8](ios.md#6-jet-websocket-disconnect-1006--1001).
20+
Related non-Detox patches: `jet`, `mocha-remote-client`, `mocha-remote-server` — WS reconnect grace, coverage handshake (`coverage-ready` / `pull-coverage`), client keepalive, server/client parse buffering, reconnect client assignment order. See [coverage design](../testing/coverage-design.md) and [iOS CI — issues 6–8](ios.md#6-jet-websocket-disconnect-1006--1001).
2121

2222
## Device registry lock (`ECOMPROMISED`)
2323

@@ -92,7 +92,7 @@ SRC=tests/node_modules/detox
9292
yarn patch-commit -s "$PATCH_DIR" # non-interactive when using /bin/cp -f, not plain cp
9393
```
9494

95-
3. `yarn install` from repo root and confirm the change in `tests/node_modules/detox/...`.
95+
3. `yarn install` from repo root and confirm the change in `tests/node_modules/...` **and** updated `yarn.lock` patch resolution hashes.
9696
4. Update this doc and platform pages (`ios.md`, `android.md`) if behaviour or file list changes.
9797

9898
**Detox version bump:** change `tests/package.json`, run `yarn`, re-apply all hunks (or redo the headless flow from a fresh `yarn patch`), run iOS + Android E2E.

0 commit comments

Comments
 (0)