Skip to content

Commit f157c3e

Browse files
wabicaiclaude
andauthored
fix(expo-example): passphrase session-count test transport race on chain switch (#762)
The passphrase session-count test hit "Didn't receive expected header signature." when switching chains between runs. Root cause: the stop path fired an un-awaited SDK.cancel() followed by a fire-and-forget SDK.getFeatures(), triggered synchronously inside the REQUEST_PASSPHRASE listener while the device was still mid-exchange. The next run's getFeatures then decoded leftover bytes from that race. Align with the recovery pattern used in blindSignature/automationTest timeout handlers (cancel(connectId) + awaited getFeatures with retryCount): - stopTest: async; SDK.cancel(connectId) then await SDK.getFeatures(connectId, { retryCount: 1 }) drains the transport to a known-clean frame boundary. Removes the dead-code try/catch around the fire-and-forget getFeatures. - testSessionCount entry: defensive SDK.cancel(connectId) + awaited getFeatures with retryCount so a dirty transport from a prior run (or prior chain) can't blow up the first request. - REQUEST_PASSPHRASE listener end-branch: instead of synchronously calling stopTest (which raced cancel against the pending UI exchange), reply with an empty passphrase so the device's current call completes cleanly, flip hasContinue, and let the outer loop drop out. - Verification loop: check hasContinue around the awaited request so a round ended by the listener mid-verification exits silently instead of logging a spurious "address not match". Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f2190c5 commit f157c3e

1 file changed

Lines changed: 53 additions & 8 deletions

File tree

packages/connect-examples/expo-example/src/testTools/passphraseTest/TestSessionCountView.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,10 @@ export default function TestSessionCountView() {
106106
}[]
107107
>([]);
108108

109-
const stopTest = useCallback(() => {
109+
const stopTest = useCallback(async () => {
110110
if (hardwareUiEventListener) {
111111
SDK?.off(UI_EVENT, hardwareUiEventListener);
112112
}
113-
SDK?.cancel();
114113

115114
hasContinue.current = false;
116115
testResult.current = {
@@ -120,10 +119,20 @@ export default function TestSessionCountView() {
120119

121120
pushRunnerLog([intl.formatMessage({ id: 'message__test_end' })]);
122121

122+
const connectId = selectedDevice?.connectId ?? '';
123+
if (!SDK || !connectId) return;
124+
125+
// Mirror the recovery used in blindSignature/automationTest timeout
126+
// handlers: cancel(connectId) rejects pending requests and fires
127+
// interruptionFromUser; the awaited getFeatures with retryCount drains any
128+
// leftover bytes so the transport returns to a known-clean frame boundary
129+
// before the next test starts.
130+
SDK.cancel(connectId);
123131
try {
124-
SDK?.getFeatures(selectedDevice?.connectId ?? '');
125-
} catch (e) {
126-
// ignore
132+
await SDK.getFeatures(connectId, { retryCount: 1 });
133+
} catch {
134+
// defensive: getFeatures normally resolves, but a transport race during
135+
// cancel can occasionally surface as a throw
127136
}
128137
}, [SDK, intl, pushRunnerLog, selectedDevice?.connectId]);
129138

@@ -137,8 +146,16 @@ export default function TestSessionCountView() {
137146
SDK.off(UI_EVENT, hardwareUiEventListener);
138147
}
139148

149+
// Defensive resync before each run: if a prior run (possibly on a
150+
// different chain) was interrupted mid-exchange, the transport may still
151+
// hold leftover bytes and the first getFeatures would decode a half-frame
152+
// and throw "Didn't receive expected header signature.".
153+
if (connectId) {
154+
SDK.cancel(connectId);
155+
}
156+
140157
// refresh device
141-
const featuresRes = await SDK.getFeatures(connectId);
158+
const featuresRes = await SDK.getFeatures(connectId, { retryCount: 1 });
142159
if (!featuresRes.success) {
143160
pushRunnerLog([
144161
intl.formatMessage({ id: 'message__get_features_error' }),
@@ -163,7 +180,19 @@ export default function TestSessionCountView() {
163180
}
164181
if (message.type === UI_REQUEST.REQUEST_PASSPHRASE) {
165182
if (!allowInputPassphrase.current) {
183+
// Device re-asking passphrase during verification = session slot
184+
// evicted = expected end of round. Don't synchronously cancel
185+
// here: the device is mid-exchange waiting for our reply, and
186+
// racing cancel against that exchange is what leaves the
187+
// transport dirty (root cause of the "Didn't receive expected
188+
// header signature." error when switching chains). Reply with
189+
// an empty passphrase so the device's current call completes
190+
// cleanly; the outer loop drops out via hasContinue.
166191
hasContinue.current = false;
192+
testResult.current = {
193+
done: true,
194+
payload: intl.formatMessage({ id: 'message__test_end' }),
195+
};
167196

168197
pushRunnerLog([
169198
intl.formatMessage({ id: 'message__test_result' }),
@@ -173,7 +202,13 @@ export default function TestSessionCountView() {
173202
}),
174203
passphraseStateList.current.length.toString(),
175204
]);
176-
stopTest();
205+
206+
setTimeout(() => {
207+
SDK.uiResponse({
208+
type: UI_RESPONSE.RECEIVE_PASSPHRASE,
209+
payload: { value: '' },
210+
});
211+
}, 200);
177212
return;
178213
}
179214

@@ -281,6 +316,8 @@ export default function TestSessionCountView() {
281316
// 查看一下之前的 passphraseState 是否还能用
282317
allowInputPassphrase.current = false;
283318
for (const item of [...passphraseStateList.current].reverse()) {
319+
if (!hasContinue.current) break;
320+
284321
pushRunnerLog([
285322
' ',
286323
intl.formatMessage({ id: 'message__fetch' }),
@@ -297,6 +334,12 @@ export default function TestSessionCountView() {
297334
showOnOneKey,
298335
});
299336

337+
// Listener may have flipped hasContinue while this call was in
338+
// flight (= session evicted mid-verification). The response came
339+
// from an empty-passphrase reply, so it isn't meaningful — exit
340+
// silently instead of misreporting address_not_match.
341+
if (!hasContinue.current) break;
342+
300343
if (!addressRes.success) {
301344
hasContinue.current = false;
302345
testResult.current = {
@@ -342,6 +385,9 @@ export default function TestSessionCountView() {
342385
]);
343386
}
344387

388+
// Don't record this wallet if the listener ended the test mid-round.
389+
if (!hasContinue.current) break;
390+
345391
passphraseStateList.current.push({
346392
walletName,
347393
passphraseState,
@@ -356,7 +402,6 @@ export default function TestSessionCountView() {
356402
pushRunnerLog,
357403
selectedDevice?.connectId,
358404
showOnOneKey,
359-
stopTest,
360405
testChain,
361406
]);
362407

0 commit comments

Comments
 (0)