From 18e6a135643d8c4556132c636dd09eb1ecea1b98 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:11:06 +0800 Subject: [PATCH] test: model live cancel dialog scenarios --- docs/binance-orderbook-trade-ui-automation.md | 13 +++ .../helpers/live-order-scale-config.js | 36 ++++---- scripts/binance-live-performance.mjs | 23 +++-- .../binance-live-order-scale-config.test.js | 22 +++++ test/unit/binance-live-performance.test.js | 84 ++++++++++++++++--- 5 files changed, 141 insertions(+), 37 deletions(-) diff --git a/docs/binance-orderbook-trade-ui-automation.md b/docs/binance-orderbook-trade-ui-automation.md index 83b782a..2cd0e05 100644 --- a/docs/binance-orderbook-trade-ui-automation.md +++ b/docs/binance-orderbook-trade-ui-automation.md @@ -251,6 +251,19 @@ isolated samples. Every sample must prove restored UI state, no fills, zero resi test-owned orders, zero uncaught errors, and bounded long-task observations. Missing segments are invalid data, not zero-duration work. +Live capture parameters use explicit scenario kinds: + +- `no-orders`: requires no capacity evidence and an empty test-order ledger; +- `dialog-cancel`: declares a positive `testOrderCount`, proves capacity for those + test-owned orders, cancels the native dialog, and cleans up those orders afterward; +- `dialog-confirm`: declares a positive `testOrderCount`, proves capacity, confirms + native cancellation, and observes zero residual test-owned orders; and +- `order-scale`: retains the named small/medium/large scale contract. + +Do not label a dialog run as `no-orders`. One-order dialog smoke uses standalone +capacity evidence and does not require enough balance or order slots to construct +three distinct scale levels. The three-level minimum applies only to `order-scale`. + Order-scale scenarios also persist the internal profile name, semantic scale label, preferred and effective target counts, and sample count. Every sample persists its live capacity evidence. Capacity shortfalls must skip or abort the scale explicitly; diff --git a/e2e/binance-orderbook/helpers/live-order-scale-config.js b/e2e/binance-orderbook/helpers/live-order-scale-config.js index 833bf3c..f25e49e 100644 --- a/e2e/binance-orderbook/helpers/live-order-scale-config.js +++ b/e2e/binance-orderbook/helpers/live-order-scale-config.js @@ -133,8 +133,7 @@ function deriveScaleCount(capacity, ratio, minimum) { return Math.max(minimum, Math.round(capacity * ratio)); } -export function createLiveOrderScalePlan(profile, liveContext) { - validateLiveOrderScaleProfile(profile); +export function createLiveOrderCapacityEvidence(liveContext) { validateLiveContext(liveContext); const perOrderNotional = multiplyDecimalStrings( liveContext.perOrderPrice, @@ -151,10 +150,21 @@ export function createLiveOrderScalePlan(profile, liveContext) { ...liveContext, perOrderNotional, }); - const effectiveCapacity = Math.min( - profile.maxOrderCount, + return Object.freeze({ + ...liveContext, + perOrderNotional, maxNewOrdersBySlots, maxNewOrdersByMargin, + }); +} + +export function createLiveOrderScalePlan(profile, liveContext) { + validateLiveOrderScaleProfile(profile); + const capacityEvidence = createLiveOrderCapacityEvidence(liveContext); + const effectiveCapacity = Math.min( + profile.maxOrderCount, + capacityEvidence.maxNewOrdersBySlots, + capacityEvidence.maxNewOrdersByMargin, ); if (effectiveCapacity < 3) { throw new Error(`Live order capacity ${effectiveCapacity} cannot form three distinct scales`); @@ -170,12 +180,6 @@ export function createLiveOrderScalePlan(profile, liveContext) { throw new Error(`Scale ratios collapse at effective capacity ${effectiveCapacity}`); } - const capacityEvidence = Object.freeze({ - ...liveContext, - perOrderNotional, - maxNewOrdersBySlots, - maxNewOrdersByMargin, - }); return Object.freeze({ profileName: profile.profileName, sampleCount: profile.sampleCount, @@ -197,18 +201,12 @@ export function validateLiveOrderCapacityEvidence(evidence, path = 'capacityEvid assertRecord(evidence, path); assertExactKeys(evidence, CAPACITY_EVIDENCE_FIELDS, path); const context = Object.fromEntries(LIVE_CONTEXT_FIELDS.map((field) => [field, evidence[field]])); - const plan = createLiveOrderScalePlan({ - schemaVersion: 1, - profileName: 'evidence-validation', - sampleCount: 3, - maxOrderCount: 3, - scaleRatios: { small: 0.25, medium: 0.5, large: 1 }, - }, context); - if (evidence.perOrderNotional !== plan.capacityEvidence.perOrderNotional) { + const expected = createLiveOrderCapacityEvidence(context); + if (evidence.perOrderNotional !== expected.perOrderNotional) { throw new Error(`${path}.perOrderNotional does not match price x quantity`); } for (const field of ['maxNewOrdersBySlots', 'maxNewOrdersByMargin']) { - if (evidence[field] !== plan.capacityEvidence[field]) { + if (evidence[field] !== expected[field]) { throw new Error(`${path}.${field} does not match its inputs`); } } diff --git a/scripts/binance-live-performance.mjs b/scripts/binance-live-performance.mjs index ee989ab..7a1a887 100644 --- a/scripts/binance-live-performance.mjs +++ b/scripts/binance-live-performance.mjs @@ -113,8 +113,15 @@ function validateScenarioParameters(parameters, path) { assertExactKeys(parameters, ['kind'], path); return parameters; } + if (['dialog-cancel', 'dialog-confirm'].includes(parameters.kind)) { + assertExactKeys(parameters, ['kind', 'testOrderCount'], path); + if (!Number.isInteger(parameters.testOrderCount) || parameters.testOrderCount <= 0) { + throw new Error(`${path}.testOrderCount must be a positive integer`); + } + return parameters; + } if (parameters.kind !== 'order-scale') { - throw new Error(`${path}.kind must equal no-orders or order-scale`); + throw new Error(`${path}.kind must equal no-orders, dialog-cancel, dialog-confirm, or order-scale`); } assertExactKeys(parameters, [ 'kind', @@ -153,14 +160,20 @@ function validateCapacityEvidenceForScenario(evidence, parameters, ledger, path) return; } validateLiveOrderCapacityEvidence(evidence, path); - if (parameters.effectiveTargetOrderCount > evidence.maxNewOrdersBySlots) { + const expectedOrderCount = parameters.kind === 'order-scale' + ? parameters.effectiveTargetOrderCount + : parameters.testOrderCount; + if (expectedOrderCount > evidence.maxNewOrdersBySlots) { throw new Error(`${path} has insufficient order slots for the effective target`); } - if (parameters.effectiveTargetOrderCount > evidence.maxNewOrdersByMargin) { + if (expectedOrderCount > evidence.maxNewOrdersByMargin) { throw new Error(`${path} has insufficient margin capacity for the effective target`); } - if (ledger.created.length !== parameters.effectiveTargetOrderCount) { - throw new Error(`${path} created ledger count must match the effective target`); + if (ledger.created.length !== expectedOrderCount) { + const label = parameters.kind === 'order-scale' + ? 'the effective target' + : 'the declared test order count'; + throw new Error(`${path} created ledger count must match ${label}`); } } diff --git a/test/unit/binance-live-order-scale-config.test.js b/test/unit/binance-live-order-scale-config.test.js index 9321d93..37a1383 100644 --- a/test/unit/binance-live-order-scale-config.test.js +++ b/test/unit/binance-live-order-scale-config.test.js @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'; import test from 'node:test'; import { + createLiveOrderCapacityEvidence, createLiveOrderScalePlan, validateLiveOrderCapacityEvidence, validateLiveOrderScaleProfile, @@ -65,6 +66,27 @@ test('capacity evidence validates actual notional and slot arithmetic', () => { ); }); +test('one-order smoke capacity is valid without requiring three scale levels', () => { + const evidence = createLiveOrderCapacityEvidence(liveContext({ + availableBalance: '1.25', + currentLeverage: 1, + perOrderPrice: '1', + perOrderQuantity: '1', + })); + + assert.equal(evidence.maxNewOrdersByMargin, 1); + assert.equal(validateLiveOrderCapacityEvidence(evidence), evidence); + assert.throws( + () => createLiveOrderScalePlan(profile, liveContext({ + availableBalance: '1.25', + currentLeverage: 1, + perOrderPrice: '1', + perOrderQuantity: '1', + })), + /cannot form three distinct scales/, + ); +}); + test('live order slots subtract existing and outstanding test-owned orders', () => { const plan = createLiveOrderScalePlan(profile, liveContext({ liveMaxNumOrdersLimit: 8, diff --git a/test/unit/binance-live-performance.test.js b/test/unit/binance-live-performance.test.js index 80b29c4..f630747 100644 --- a/test/unit/binance-live-performance.test.js +++ b/test/unit/binance-live-performance.test.js @@ -11,19 +11,46 @@ import { validateLivePerformanceBaseline, validateLivePerformanceCapture, } from '../../scripts/binance-live-performance.mjs'; -import { createLiveOrderScalePlan } from '../../e2e/binance-orderbook/helpers/live-order-scale-config.js'; +import { + createLiveOrderCapacityEvidence, + createLiveOrderScalePlan, +} from '../../e2e/binance-orderbook/helpers/live-order-scale-config.js'; const EMPTY_ORDER_LEDGER = Object.freeze({ created: [], fills: [], residual: [] }); +function testOrder(index = 0) { + return { + symbol: 'XRPUSDT', + side: 'SELL', + positionSide: 'SHORT', + price: '2', + quantity: '2.5', + createdAt: `2026-08-26T08:00:0${index + 1}.000Z`, + }; +} + +function singleOrderCapacityEvidence() { + return createLiveOrderCapacityEvidence({ + availableBalance: '13.28', + currentLeverage: 5, + perOrderPrice: '2', + perOrderQuantity: '2.5', + safetyFactor: '0.8', + liveMaxNumOrdersLimit: 200, + existingCurrentSymbolOpenOrders: 0, + outstandingTestOwnedOrders: 0, + }); +} + function sample(clickToFeedback, dialog, finalReady, maxLongTaskMs = 0) { return { - capacityEvidence: null, + capacityEvidence: singleOrderCapacityEvidence(), segmentsMs: { clickToFirstFeedback: clickToFeedback, clickToDialog: dialog, decisionToFinalReady: finalReady, }, - testOrderLedger: structuredClone(EMPTY_ORDER_LEDGER), + testOrderLedger: { created: [testOrder()], fills: [], residual: [] }, stateRestored: true, noFills: true, residualTestOrders: 0, @@ -47,7 +74,7 @@ function capture(samples = [sample(90, 150, 140), sample(100, 160, 150), sample( }, scenarios: [{ name: 'cancel-dialog-cancel', - parameters: { kind: 'no-orders' }, + parameters: { kind: 'dialog-cancel', testOrderCount: 1 }, applicableSegments: ['clickToFirstFeedback', 'clickToDialog', 'decisionToFinalReady'], samples, }], @@ -72,14 +99,7 @@ function scaleSample(index) { existingCurrentSymbolOpenOrders: 0, outstandingTestOwnedOrders: 0, }).capacityEvidence; - result.testOrderLedger.created.push({ - symbol: 'XRPUSDT', - side: 'SELL', - positionSide: 'SHORT', - price: '2', - quantity: '2.5', - createdAt: `2026-08-26T08:00:0${index + 1}.000Z`, - }); + result.testOrderLedger.created = [testOrder(index)]; return result; } @@ -104,6 +124,44 @@ test('live performance capture requires three complete isolated samples', () => ); }); +test('live performance capture models no-orders, dialog-cancel, and dialog-confirm explicitly', () => { + const dialogCancel = capture(); + assert.equal(validateLivePerformanceCapture(dialogCancel).scenarios[0].parameters.kind, 'dialog-cancel'); + + const dialogConfirm = structuredClone(dialogCancel); + dialogConfirm.scenarios[0].name = 'cancel-dialog-confirm'; + dialogConfirm.scenarios[0].parameters.kind = 'dialog-confirm'; + assert.equal(validateLivePerformanceCapture(dialogConfirm).scenarios[0].parameters.kind, 'dialog-confirm'); + + const noOrders = structuredClone(dialogCancel); + noOrders.scenarios[0].name = 'cancel-current-symbol-no-orders'; + noOrders.scenarios[0].parameters = { kind: 'no-orders' }; + for (const entry of noOrders.scenarios[0].samples) { + entry.capacityEvidence = null; + entry.testOrderLedger = structuredClone(EMPTY_ORDER_LEDGER); + } + assert.equal(validateLivePerformanceCapture(noOrders).scenarios[0].parameters.kind, 'no-orders'); +}); + +test('dialog captures bind the declared test-order count to capacity and ledger evidence', () => { + const missingOrder = capture(); + missingOrder.scenarios[0].samples[0].testOrderLedger.created = []; + assert.throws( + () => validateLivePerformanceCapture(missingOrder), + /created ledger count must match the declared test order count/, + ); + + const insufficientCapacity = capture(); + insufficientCapacity.scenarios[0].samples[0].capacityEvidence = { + ...insufficientCapacity.scenarios[0].samples[0].capacityEvidence, + maxNewOrdersByMargin: 0, + }; + assert.throws( + () => validateLivePerformanceCapture(insufficientCapacity), + /does not match its inputs|insufficient margin capacity/, + ); +}); + test('live performance capture rejects missing segments and failed cleanup invariants', () => { const missingSegment = capture(); delete missingSegment.scenarios[0].samples[0].segmentsMs.clickToDialog; @@ -224,7 +282,7 @@ test('baseline comparison rejects the same scale name with different effective c scenario: 'cancel-dialog-cancel', metric: null, reason: 'configuration-mismatch', - baseline: { kind: 'no-orders' }, + baseline: { kind: 'dialog-cancel', testOrderCount: 1 }, current: current.scenarios[0].parameters, }], );