@@ -9,9 +9,13 @@ const EXCESS = { FUNDING: '0', MAIN: '0', UMFUTURE: '100' };
99const TARGET = { FUNDING : '50' , MAIN : '40' , UMFUTURE : '10' } ;
1010const transfers = api => api . snapshot ( ) . requests . filter ( r => r . pathname === ACCOUNT_PATHS . transfer ) ;
1111
12- async function episodes ( page ) {
12+ async function episodeRecords ( page ) {
1313 return page . evaluate ( prefix => Object . keys ( localStorage ) . filter ( k => k . startsWith ( prefix ) )
14- . map ( k => JSON . parse ( localStorage . getItem ( k ) ) . status ) , PREFIX ) ;
14+ . map ( k => JSON . parse ( localStorage . getItem ( k ) ) ) , PREFIX ) ;
15+ }
16+
17+ async function episodes ( page ) {
18+ return ( await episodeRecords ( page ) ) . map ( record => record . status ) ;
1519}
1620
1721async function openAutomatic ( page , api ) {
@@ -129,15 +133,15 @@ test('user sees automatic rebalance status through SPA locale changes without re
129133
130134 // Then both planned withdrawals complete once and retain a Chinese result.
131135 await expect . poll ( ( ) => api . snapshot ( ) . balances ) . toEqual ( TARGET ) ;
132- await expectAutomaticStatus ( page , '自动再平衡已完成 · 2/2 笔 ' ) ;
136+ await expectAutomaticStatus ( page , '已自动进行账户再平衡 ' ) ;
133137 expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
134138 expect ( await episodes ( page ) ) . toEqual ( [ 'consumed' ] ) ;
135139
136140 // When the completed account page switches to English without reloading.
137141 await switchLocale ( page , 'en' ) ;
138142
139143 // Then the completed result and tooltip translate while the episode stays consumed.
140- await expectAutomaticStatus ( page , 'Automatic USDT transfers completed: 2/2 ' ) ;
144+ await expectAutomaticStatus ( page , 'Account automatically rebalanced ' ) ;
141145 expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
142146 expect ( await episodes ( page ) ) . toEqual ( [ 'consumed' ] ) ;
143147
@@ -146,7 +150,7 @@ test('user sees automatic rebalance status through SPA locale changes without re
146150 await page . clock . runFor ( 5000 ) ;
147151
148152 // Then the result returns to Chinese and no repeated allocation occurs.
149- await expectAutomaticStatus ( page , '自动再平衡已完成 · 2/2 笔 ' ) ;
153+ await expectAutomaticStatus ( page , '已自动进行账户再平衡 ' ) ;
150154 expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
151155 expect ( api . snapshot ( ) . balances ) . toEqual ( TARGET ) ;
152156 expect ( await episodes ( page ) ) . toEqual ( [ 'consumed' ] ) ;
@@ -210,6 +214,18 @@ test('user automatically withdraws excess once after a stable empty account with
210214 await expect ( page . getByRole ( 'dialog' ) ) . toHaveCount ( 0 ) ;
211215 expect ( ( await readFixtureState ( page ) ) . events . filter ( e => / o r d e r - s u b m i t t e d | c a n c e l - r e q u e s t e d / . test ( e . type ) ) ) . toEqual ( [ ] ) ;
212216 expect ( errors ) . toEqual ( [ ] ) ;
217+
218+ // When the user reloads after every planned transfer and balance has been confirmed.
219+ await reloadPageWithCoverage ( page ) ;
220+ await becomeFlat ( page ) ;
221+
222+ // Then the completed result survives without repeating a transfer.
223+ await expectAutomaticStatus ( page , '已自动进行账户再平衡' ) ;
224+ expect ( await episodeRecords ( page ) ) . toEqual ( [
225+ { version : 1 , status : 'consumed' , outcome : 'automatic_completed' } ,
226+ ] ) ;
227+ expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
228+ expect ( api . snapshot ( ) . balances ) . toEqual ( TARGET ) ;
213229} ) ;
214230
215231test ( 'user keeps reserves after a loss and does not repeat an empty episode after reload' , async ( { page } ) => {
@@ -234,9 +250,154 @@ test('user keeps reserves after a loss and does not repeat an empty episode afte
234250 await expect ( page . locator ( '[data-usdt-rebalance]' ) ) . toBeEnabled ( ) ;
235251
236252 // Then reloading never creates a second automatic execution entitlement.
237- await expectAutomaticStatus ( page , '本轮空仓已检查自动再平衡 ' ) ;
253+ await expectAutomaticStatus ( page , '本轮不再自动执行账户再平衡 ' ) ;
238254 expect ( await episodes ( page ) ) . toEqual ( [ 'consumed' ] ) ;
239255 expect ( transfers ( api ) ) . toEqual ( [ ] ) ;
256+
257+ // When the retained no-transfer episode is displayed in English.
258+ await switchLocale ( page , 'en' ) ;
259+
260+ // Then the message describes the episode without claiming a completed transfer or current balance.
261+ await expectAutomaticStatus ( page , 'No further automatic rebalance in this round' ) ;
262+ expect ( await episodeRecords ( page ) ) . toEqual ( [ { version : 1 , status : 'consumed' } ] ) ;
263+ expect ( api . snapshot ( ) . balances ) . toEqual ( EXCESS ) ;
264+ expect ( transfers ( api ) ) . toEqual ( [ ] ) ;
265+ } ) ;
266+
267+ test ( 'user does not see a completed transfer inferred from an older episode record' , async ( { page } ) => {
268+ // Given an earlier script stored a consumed episode without a financial outcome.
269+ const api = createAccountRebalanceApi ( EXCESS ) ;
270+ await openAutomatic ( page , api ) ;
271+ await page . evaluate ( async prefix => {
272+ const digest = await crypto . subtle . digest ( 'SHA-256' , new TextEncoder ( ) . encode ( 'fixture-account' ) ) ;
273+ const accountKey = prefix + Array . from ( new Uint8Array ( digest ) , byte => byte . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
274+ localStorage . setItem ( accountKey , JSON . stringify ( { version : 1 , status : 'consumed' } ) ) ;
275+ } , PREFIX ) ;
276+
277+ // When the current script observes the same empty episode.
278+ await becomeFlat ( page ) ;
279+
280+ // Then the old record remains protected without claiming success or moving funds.
281+ await expectAutomaticStatus ( page , '本轮不再自动执行账户再平衡' ) ;
282+ expect ( await episodeRecords ( page ) ) . toEqual ( [ { version : 1 , status : 'consumed' } ] ) ;
283+ expect ( transfers ( api ) ) . toEqual ( [ ] ) ;
284+ expect ( api . snapshot ( ) . balances ) . toEqual ( EXCESS ) ;
285+ } ) ;
286+
287+ test ( 'user sees a localized refusal for an invalid persisted completion outcome' , async ( { page } ) => {
288+ // Given a stored completion marker conflicts with its in-flight protection.
289+ const api = createAccountRebalanceApi ( EXCESS ) ;
290+ await openAutomatic ( page , api ) ;
291+ await page . evaluate ( async prefix => {
292+ const digest = await crypto . subtle . digest ( 'SHA-256' , new TextEncoder ( ) . encode ( 'fixture-account' ) ) ;
293+ const accountKey = prefix + Array . from ( new Uint8Array ( digest ) , byte => byte . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
294+ localStorage . setItem ( accountKey , JSON . stringify ( { version : 1 , status : 'in_flight' , outcome : 'automatic_completed' } ) ) ;
295+ } , PREFIX ) ;
296+
297+ // When the script reads the conflicting record on the Chinese route.
298+ await becomeFlat ( page ) ;
299+
300+ // Then the refusal explains the invalid result without moving funds or resetting the record.
301+ await expectAutomaticStatus ( page , '自动再平衡已停止:自动再平衡结果记录无效' , '自动再平衡结果记录无效' ) ;
302+ expect ( await episodeRecords ( page ) ) . toEqual ( [
303+ { version : 1 , status : 'in_flight' , outcome : 'automatic_completed' } ,
304+ ] ) ;
305+ expect ( transfers ( api ) ) . toEqual ( [ ] ) ;
306+
307+ // When the same refusal is retained on the English route.
308+ await switchLocale ( page , 'en' ) ;
309+
310+ // Then both its text and tooltip translate without retrying the refused action.
311+ await expectAutomaticStatus ( page , 'Automatic USDT transfer stopped: Invalid automatic rebalance episode outcome' , 'Invalid automatic rebalance episode outcome' ) ;
312+ expect ( transfers ( api ) ) . toEqual ( [ ] ) ;
313+ } ) ;
314+
315+ test ( 'user does not see a manual rebalance reported as an automatic completion after reload' , async ( { page } ) => {
316+ // Given automatic qualification left the reserves alone and the user reviewed a manual plan.
317+ const api = createAccountRebalanceApi ( { FUNDING : '100' , MAIN : '0' , UMFUTURE : '0' } ) ;
318+ await openAutomatic ( page , api ) ;
319+ await becomeFlat ( page ) ;
320+ await expectAutomaticStatus ( page , '自动再平衡:合约账户无多余 USDT' ) ;
321+ await page . clock . runFor ( 32 ) ;
322+ await expect ( page . locator ( '[data-usdt-rebalance]' ) ) . toBeVisible ( ) ;
323+ await page . locator ( '[data-usdt-rebalance]' ) . click ( ) ;
324+ const dialog = page . getByRole ( 'dialog' , { name : '账户再平衡' } ) ;
325+ await expect ( dialog ) . toBeVisible ( ) ;
326+
327+ // When the user confirms the complete manual allocation.
328+ await dialog . getByRole ( 'button' , { name : '确认再平衡' , exact : true } ) . click ( ) ;
329+
330+ // Then the manual result reaches its target without an automatic completion marker.
331+ await expect ( page . locator ( '#jh-binance-ladder-status' ) ) . toHaveText ( '账户再平衡已完成 · 2/2 笔' ) ;
332+ expect ( await episodeRecords ( page ) ) . toEqual ( [ { version : 1 , status : 'consumed' } ] ) ;
333+ expect ( api . snapshot ( ) . balances ) . toEqual ( TARGET ) ;
334+ expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
335+
336+ // When the user reloads the same account episode.
337+ await reloadPageWithCoverage ( page ) ;
338+ await becomeFlat ( page ) ;
339+
340+ // Then no automatic completion or duplicate transfer is reported.
341+ await expectAutomaticStatus ( page , '本轮不再自动执行账户再平衡' ) ;
342+ expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
343+ } ) ;
344+
345+ test ( 'user sees automatic completion only after the final transferred balance is confirmed' , async ( { page } ) => {
346+ // Given the only planned transfer will be acknowledged before its balance is published.
347+ const before = { FUNDING : '50' , MAIN : '30' , UMFUTURE : '20' } ;
348+ const api = createAccountRebalanceApi ( before , { commitTransfers : false } ) ;
349+ await openAutomatic ( page , api ) ;
350+
351+ // When the server accepts the transfer while its wallet still has the old balance.
352+ await becomeFlat ( page ) ;
353+ await expect . poll ( ( ) => api . snapshot ( ) . pendingTransfers ) . toBe ( 1 ) ;
354+ await expect . poll ( ( ) => api . snapshot ( ) . requests . filter ( r => r . pathname === ACCOUNT_PATHS . wallets ) . length ) . toBe ( 3 ) ;
355+
356+ // Then the result remains pending and cannot carry a completed marker.
357+ await expectAutomaticStatus ( page , '自动再平衡中 · 1/1 笔' ) ;
358+ expect ( await episodeRecords ( page ) ) . toEqual ( [ { version : 1 , status : 'in_flight' } ] ) ;
359+ expect ( api . snapshot ( ) . balances ) . toEqual ( before ) ;
360+
361+ // When the authoritative balance publishes the accepted transfer.
362+ api . commitPendingTransfers ( ) ;
363+ await page . clock . runFor ( 1000 ) ;
364+
365+ // Then the complete result is shown and persisted only after the balance matches.
366+ await expectAutomaticStatus ( page , '已自动进行账户再平衡' ) ;
367+ expect ( await episodeRecords ( page ) ) . toEqual ( [
368+ { version : 1 , status : 'consumed' , outcome : 'automatic_completed' } ,
369+ ] ) ;
370+ expect ( api . snapshot ( ) . balances ) . toEqual ( TARGET ) ;
371+ expect ( transfers ( api ) ) . toHaveLength ( 1 ) ;
372+ } ) ;
373+
374+ test ( 'user does not see partial automatic transfers reported as completed after reload' , async ( { page } ) => {
375+ // Given the next account check will fail after the first transfer has committed.
376+ const api = createAccountRebalanceApi ( EXCESS ) ;
377+ await openAutomatic ( page , api ) ;
378+ await page . route ( 'https://www.binance.com' + ACCOUNT_PATHS . transfer , async route => {
379+ const response = api . handle ( { pathname : ACCOUNT_PATHS . transfer , method : 'POST' , body : route . request ( ) . postDataJSON ( ) } ) ;
380+ api . failNext ( ACCOUNT_PATHS . identity , { status : 401 , body : { success : false } } ) ;
381+ await route . fulfill ( { status : response . status , contentType : 'application/json' , body : JSON . stringify ( response . body ) } ) ;
382+ } ) ;
383+
384+ // When the automatic plan stops between the first and second transfer.
385+ await becomeFlat ( page ) ;
386+
387+ // Then a confirmed partial transfer does not claim the whole plan completed.
388+ await expectAutomaticStatus ( page , '自动再平衡已停止:Binance 登录态已失效' , 'Binance 登录态已失效' ) ;
389+ expect ( api . snapshot ( ) . balances ) . toEqual ( { FUNDING : '50' , MAIN : '0' , UMFUTURE : '50' } ) ;
390+ expect ( await episodeRecords ( page ) ) . toEqual ( [ { version : 1 , status : 'consumed' } ] ) ;
391+ expect ( transfers ( api ) ) . toHaveLength ( 1 ) ;
392+
393+ // When the user reloads the partially processed episode.
394+ await reloadPageWithCoverage ( page ) ;
395+ await becomeFlat ( page ) ;
396+
397+ // Then the result remains non-successful and the remaining transfer is not retried.
398+ await expectAutomaticStatus ( page , '本轮不再自动执行账户再平衡' ) ;
399+ expect ( transfers ( api ) ) . toHaveLength ( 1 ) ;
400+ expect ( api . snapshot ( ) . balances ) . toEqual ( { FUNDING : '50' , MAIN : '0' , UMFUTURE : '50' } ) ;
240401} ) ;
241402
242403for ( const kind of [ 'basic' , 'conditional' ] ) {
@@ -268,12 +429,15 @@ test('user receives one automatic allocation across two simultaneous currency ta
268429 // When both pages qualify for the same empty account at the same time.
269430 await Promise . all ( [ page , second ] . map ( tab => tab . evaluate ( ( ) => window . __BINANCE_FIXTURE__ . setOrders ( [ ] ) ) ) ) ;
270431 await page . clock . runFor ( 2032 ) ;
432+ await second . clock . runFor ( 2032 ) ;
271433
272434 // Then shared locking and durable state permit only the two planned transfers.
273435 await expect . poll ( ( ) => api . snapshot ( ) . balances ) . toEqual ( TARGET ) ;
274436 await expect . poll ( ( ) => episodes ( page ) ) . toEqual ( [ 'consumed' ] ) ;
275437 expect ( transfers ( api ) ) . toHaveLength ( 2 ) ;
276438 expect ( await episodes ( second ) ) . toEqual ( [ 'consumed' ] ) ;
439+ await expectAutomaticStatus ( page , '已自动进行账户再平衡' ) ;
440+ await expectAutomaticStatus ( second , '已自动进行账户再平衡' ) ;
277441} ) ;
278442
279443test ( 'user qualifies again only after independently verified new account activity' , async ( { page } ) => {
@@ -288,6 +452,7 @@ test('user qualifies again only after independently verified new account activit
288452 await page . evaluate ( orders => window . __BINANCE_FIXTURE__ . setOrders ( orders ) , ORDER_SETS . other ) ;
289453 await page . clock . runFor ( 32 ) ;
290454 await expect . poll ( ( ) => episodes ( page ) ) . toEqual ( [ 'active' ] ) ;
455+ expect ( await episodeRecords ( page ) ) . toEqual ( [ { version : 1 , status : 'active' } ] ) ;
291456 api . setOrders ( { basic : [ ] , conditional : [ ] } ) ;
292457 api . setBalances ( EXCESS ) ;
293458 await becomeFlat ( page ) ;
0 commit comments