From b104bc26e9358e3e28fdbe9d0a84abb161aa979b Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:39:04 +0800 Subject: [PATCH] fix(strategy27): distinguish monitoring status from feed connectivity --- docs/binance-strategy27-events-development.md | 20 ++++++- scripts/binance-strategy27-events.user.js | 58 +++++++++++++++++-- .../core/compound-candidate-controller.js | 6 +- .../dom/strategy27-event-panel.js | 49 +++++++++++++++- src/binance-strategy27-events/index.user.js | 6 +- .../compound-candidate-controller.test.js | 4 +- .../strategy27-entrypoint.test.js | 40 +++++++++++++ .../strategy27-event-panel.test.js | 38 ++++++++++++ test/unit/userscript-release-contract.test.js | 2 +- 9 files changed, 209 insertions(+), 14 deletions(-) diff --git a/docs/binance-strategy27-events-development.md b/docs/binance-strategy27-events-development.md index 6710040..28f3de8 100644 --- a/docs/binance-strategy27-events-development.md +++ b/docs/binance-strategy27-events-development.md @@ -138,7 +138,7 @@ ADR 032 in CorsairQuant owns the server-side rule and transport contract. The browser does not reconstruct candidates from ordinary events or recalculate market evidence. The client, lifecycle, panel, native chart layer and optional-job controller are wired into the entrypoint and tested together. The source and -generated install artifact are version 0.4.4 with identical metadata headers. +generated install artifact are version 0.4.5 with identical metadata headers. The generated artifact passes syntax, release-contract and isolated execution checks, including candidate delivery, paired entities, clear and context stop. Binance operator-page validation remains outstanding. Server/gateway rollout @@ -266,3 +266,21 @@ the next response had zero ordinary markers. Earlier event_closed responses had retained those markers. Version 0.4.4 separates protocol reset from bounded display ownership. This evidence identifies the browser deletion path; it does not distinguish the underlying server Redis connection error from a timeout. + +## Monitoring status and connection status + +Version 0.4.5 adds a top-level last-reported monitoring status independent of +selected history, drawing completion and data connection status. An accepted +universe_removed event explicitly identifies removal; monitor_stopped identifies +stopped monitoring. These reports survive stream resets and manual history clear. +A newer event identity supersedes the report; delayed outcomes for older events +and active bootstrap replay for the same closed event cannot undo it. Outcome-only +bootstrap records also restore this status. A stream reset without a removal/stop +report displays that monitoring status awaits new evidence. + +The ordinary connection line reports request delivery independently. The compound +connection text explicitly states that connectivity does not confirm monitoring +for the current symbol. Both clients use the same gateway with separate existing +paths; neither a successful empty response nor compound connectivity can mark a +symbol as re-added. No live universe-membership endpoint is available to this UI, +so the banner reports the last event evidence, not a current membership guarantee. diff --git a/scripts/binance-strategy27-events.user.js b/scripts/binance-strategy27-events.user.js index 85f43da..68785bb 100644 --- a/scripts/binance-strategy27-events.user.js +++ b/scripts/binance-strategy27-events.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy27.events // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 0.4.4 +// @version 0.4.5 // @author jackhai9 // @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察 // @match https://www.binance.com/*/futures/* @@ -1548,6 +1548,19 @@ role: "panel-body", styles: { overflow: "auto", maxHeight: "calc(100vh - 190px)" } }); + const monitoring = createElement(document, "div", { + role: "ordinary-monitoring-status", + text: "Monitoring status unknown. Waiting for event evidence.", + styles: { padding: "9px", fontWeight: "600", color: "#F0B90B", borderBottom: "1px solid rgba(132, 142, 156, .18)" } + }); + monitoring.dataset.state = "unknown"; + monitoring.setAttribute("aria-live", "polite"); + const ordinaryConnection = createElement(document, "div", { + role: "ordinary-connection-status", + text: "Event data: Connecting", + styles: { padding: "5px 9px 0", color: "#848E9C", fontSize: "11px" } + }); + let monitoringObservation = null; const detail = createElement(document, "div", { role: "event-detail", styles: { display: "grid", gap: "5px", padding: "9px" } @@ -1578,7 +1591,7 @@ role: "compound-list", styles: { display: "grid", gap: "2px", padding: "0 6px 7px" } }); - body.append(detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent); + body.append(monitoring, ordinaryConnection, detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent); panel.appendChild(body); document.body.appendChild(panel); const initialPosition = assertPanelPosition(loadPosition()) ?? createDefaultPosition(chartRoot); @@ -1741,6 +1754,32 @@ render(); } return Object.freeze({ + /** Event identity order prevents delayed outcomes or bootstrap replay from undoing removal or reentry. */ + observeOrdinaryEvent(event, observedAtMs) { + const previous = monitoringObservation; + if (previous && event.triggered_at_ms < previous.triggeredAtMs) return; + if (previous && event.triggered_at_ms === previous.triggeredAtMs) { + if (observedAtMs < previous.observedAtMs) return; + if (previous.terminal && event.event_status === "active") return; + } + monitoringObservation = { + triggeredAtMs: event.triggered_at_ms, + observedAtMs, + terminal: event.event_status !== "active" + }; + const state = event.close_reason === "universe_removed" ? "removed" : event.close_reason === "monitor_stopped" ? "stopped" : "observed"; + const text = state === "removed" ? "Symbol removed from monitoring. Retained records are historical." : state === "stopped" ? "Monitoring stopped. Retained records are historical." : "Observation received. Waiting for further events."; + monitoring.dataset.state = state; + monitoring.textContent = "Last reported monitoring status (" + formatClock(observedAtMs) + "): " + text; + monitoring.style.color = state === "observed" ? "#848E9C" : "#F0B90B"; + }, + setOrdinaryConnection(state) { + const labels = { connected: "Connected", reconnecting: "Reconnecting", stopped: "Stopped" }; + if (!Object.hasOwn(labels, state)) throw new Error("Invalid ordinary connection state"); + ordinaryConnection.textContent = "Event data: " + labels[state]; + ordinaryConnection.dataset.state = state; + ordinaryConnection.style.color = state === "stopped" ? "#F6465D" : "#848E9C"; + }, upsert(eventId, annotation, observedAtMs) { upsertRecord(records, maxEvents, eventId, annotation, observedAtMs); }, @@ -1755,6 +1794,11 @@ }, /** Retain facts and selection without presenting a previous stream as live. */ retainHistory() { + if (monitoring.dataset.state === "observed") { + monitoring.dataset.state = "historical"; + monitoring.textContent = "Stream restarted. Monitoring status awaits new event evidence; retained records are historical."; + monitoring.style.color = "#F0B90B"; + } for (const record of records.values()) record.historical = true; render(); }, @@ -2270,9 +2314,9 @@ // src/binance-strategy27-events/core/compound-candidate-controller.js var CONNECTION_STATUS = Object.freeze({ - connected: ["复合候选已连接", "normal"], - reconnecting: ["复合候选连接中断,正在重连", "inactive"], - unavailable: ["复合候选暂不可用,正在重连", "inactive"], + connected: ["Compound data: connected. Connection does not confirm symbol monitoring.", "normal"], + reconnecting: ["Compound data connection lost; reconnecting.", "inactive"], + unavailable: ["Compound data temporarily unavailable; reconnecting.", "inactive"], unsupported: ["网关尚未启用复合候选", "inactive"] }); function createCompoundCandidateController({ @@ -2739,6 +2783,7 @@ context.failed = true; context.controller.abort(); context.layer.suspend(); + context.panel.setOrdinaryConnection("stopped"); showStatus(context.target.chartRoot, `Strategy 27 stopped; history retained. Use the reconnect menu to resume: ${error.message}`, "error"); } function reconcileOrdinary(context) { @@ -2755,6 +2800,7 @@ } async function renderGatewayResponse(context, response) { if (active !== context || context.failed) return; + context.panel.setOrdinaryConnection("connected"); pruneOrdinaryEvents(context); if (response.status === "reset") { context.lifecycle.reset(response.reason); @@ -2791,6 +2837,7 @@ continue; } if (action.type === "event_evicted") continue; + context.panel.observeOrdinaryEvent(action.event, action.observedAtMs); const retainedAtMs = retainOrdinaryEvent(context, action.eventId, action.observedAtMs); if (!context.ordinaryHistory.has(action.eventId)) continue; const annotation = stabilizeCandidatePresentation( @@ -2864,6 +2911,7 @@ canonicalSymbol, onConnectionStateChange: (state) => { if (active !== context || context.failed) return; + context.panel.setOrdinaryConnection(state); if (state === "reconnecting") { showStatus(context.target.chartRoot, "Strategy 27 网关连接中断,正在重连", "inactive"); } else { diff --git a/src/binance-strategy27-events/core/compound-candidate-controller.js b/src/binance-strategy27-events/core/compound-candidate-controller.js index 0a2364a..dea719e 100644 --- a/src/binance-strategy27-events/core/compound-candidate-controller.js +++ b/src/binance-strategy27-events/core/compound-candidate-controller.js @@ -3,9 +3,9 @@ import { createCompoundCandidateClient } from './compound-candidate-client.js'; import { CompoundCandidateLifecycle } from './compound-candidate-lifecycle.js'; const CONNECTION_STATUS = Object.freeze({ - connected: ['复合候选已连接', 'normal'], - reconnecting: ['复合候选连接中断,正在重连', 'inactive'], - unavailable: ['复合候选暂不可用,正在重连', 'inactive'], + connected: ['Compound data: connected. Connection does not confirm symbol monitoring.', 'normal'], + reconnecting: ['Compound data connection lost; reconnecting.', 'inactive'], + unavailable: ['Compound data temporarily unavailable; reconnecting.', 'inactive'], unsupported: ['网关尚未启用复合候选', 'inactive'], }); diff --git a/src/binance-strategy27-events/dom/strategy27-event-panel.js b/src/binance-strategy27-events/dom/strategy27-event-panel.js index 0d75386..8ac39e8 100644 --- a/src/binance-strategy27-events/dom/strategy27-event-panel.js +++ b/src/binance-strategy27-events/dom/strategy27-event-panel.js @@ -228,6 +228,18 @@ export function createStrategy27EventPanel(document, chartRoot, { role: 'panel-body', styles: { overflow: 'auto', maxHeight: 'calc(100vh - 190px)' }, }); + const monitoring = createElement(document, 'div', { + role: 'ordinary-monitoring-status', + text: 'Monitoring status unknown. Waiting for event evidence.', + styles: { padding: '9px', fontWeight: '600', color: '#F0B90B', borderBottom: '1px solid rgba(132, 142, 156, .18)' }, + }); + monitoring.dataset.state = 'unknown'; + monitoring.setAttribute('aria-live', 'polite'); + const ordinaryConnection = createElement(document, 'div', { + role: 'ordinary-connection-status', text: 'Event data: Connecting', + styles: { padding: '5px 9px 0', color: '#848E9C', fontSize: '11px' }, + }); + let monitoringObservation = null; const detail = createElement(document, 'div', { role: 'event-detail', styles: { display: 'grid', gap: '5px', padding: '9px' }, @@ -258,7 +270,7 @@ export function createStrategy27EventPanel(document, chartRoot, { role: 'compound-list', styles: { display: 'grid', gap: '2px', padding: '0 6px 7px' }, }); - body.append(detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent); + body.append(monitoring, ordinaryConnection, detail, compoundTitle, compoundStatus, compoundRecent, recentTitle, recent); panel.appendChild(body); document.body.appendChild(panel); const initialPosition = assertPanelPosition(loadPosition()) ?? createDefaultPosition(chartRoot); @@ -435,6 +447,36 @@ export function createStrategy27EventPanel(document, chartRoot, { } return Object.freeze({ + /** Event identity order prevents delayed outcomes or bootstrap replay from undoing removal or reentry. */ + observeOrdinaryEvent(event, observedAtMs) { + const previous = monitoringObservation; + if (previous && event.triggered_at_ms < previous.triggeredAtMs) return; + if (previous && event.triggered_at_ms === previous.triggeredAtMs) { + if (observedAtMs < previous.observedAtMs) return; + if (previous.terminal && event.event_status === 'active') return; + } + monitoringObservation = { + triggeredAtMs: event.triggered_at_ms, + observedAtMs, + terminal: event.event_status !== 'active', + }; + const state = event.close_reason === 'universe_removed' ? 'removed' + : event.close_reason === 'monitor_stopped' ? 'stopped' : 'observed'; + const text = state === 'removed' + ? 'Symbol removed from monitoring. Retained records are historical.' + : state === 'stopped' ? 'Monitoring stopped. Retained records are historical.' + : 'Observation received. Waiting for further events.'; + monitoring.dataset.state = state; + monitoring.textContent = 'Last reported monitoring status (' + formatClock(observedAtMs) + '): ' + text; + monitoring.style.color = state === 'observed' ? '#848E9C' : '#F0B90B'; + }, + setOrdinaryConnection(state) { + const labels = { connected: 'Connected', reconnecting: 'Reconnecting', stopped: 'Stopped' }; + if (!Object.hasOwn(labels, state)) throw new Error('Invalid ordinary connection state'); + ordinaryConnection.textContent = 'Event data: ' + labels[state]; + ordinaryConnection.dataset.state = state; + ordinaryConnection.style.color = state === 'stopped' ? '#F6465D' : '#848E9C'; + }, upsert(eventId, annotation, observedAtMs) { upsertRecord(records, maxEvents, eventId, annotation, observedAtMs); }, @@ -449,6 +491,11 @@ export function createStrategy27EventPanel(document, chartRoot, { }, /** Retain facts and selection without presenting a previous stream as live. */ retainHistory() { + if (monitoring.dataset.state === 'observed') { + monitoring.dataset.state = 'historical'; + monitoring.textContent = 'Stream restarted. Monitoring status awaits new event evidence; retained records are historical.'; + monitoring.style.color = '#F0B90B'; + } for (const record of records.values()) record.historical = true; render(); }, diff --git a/src/binance-strategy27-events/index.user.js b/src/binance-strategy27-events/index.user.js index dac0306..7eca49b 100644 --- a/src/binance-strategy27-events/index.user.js +++ b/src/binance-strategy27-events/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy27.events // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 0.4.4 +// @version 0.4.5 // @author jackhai9 // @description 在 Binance 一秒图表标注 VPS Strategy 27 的实时订单流候选观察 // @match https://www.binance.com/*/futures/* @@ -120,6 +120,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js'; context.failed = true; context.controller.abort(); context.layer.suspend(); + context.panel.setOrdinaryConnection('stopped'); showStatus(context.target.chartRoot, `Strategy 27 stopped; history retained. Use the reconnect menu to resume: ${error.message}`, 'error'); } @@ -138,6 +139,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js'; async function renderGatewayResponse(context, response) { if (active !== context || context.failed) return; + context.panel.setOrdinaryConnection('connected'); pruneOrdinaryEvents(context); if (response.status === 'reset') { context.lifecycle.reset(response.reason); @@ -176,6 +178,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js'; continue; } if (action.type === 'event_evicted') continue; + context.panel.observeOrdinaryEvent(action.event, action.observedAtMs); const retainedAtMs = retainOrdinaryEvent(context, action.eventId, action.observedAtMs); if (!context.ordinaryHistory.has(action.eventId)) continue; const annotation = stabilizeCandidatePresentation( @@ -245,6 +248,7 @@ import { installSpaRouteChangeListener } from '../shared/spa-route-change.js'; canonicalSymbol, onConnectionStateChange: (state) => { if (active !== context || context.failed) return; + context.panel.setOrdinaryConnection(state); if (state === 'reconnecting') { showStatus(context.target.chartRoot, 'Strategy 27 网关连接中断,正在重连', 'inactive'); } else { diff --git a/test/dom/binance-strategy27-events/compound-candidate-controller.test.js b/test/dom/binance-strategy27-events/compound-candidate-controller.test.js index ea98500..cfeab5c 100644 --- a/test/dom/binance-strategy27-events/compound-candidate-controller.test.js +++ b/test/dom/binance-strategy27-events/compound-candidate-controller.test.js @@ -119,7 +119,7 @@ test('controller composes real protocol/lifecycle/panel and renders each indepen assert.deepEqual(h.renders.map((item) => item.decisionAtMs), [7000, 7000]); assert.deepEqual(h.renders.map((item) => item.annotation.markerShape), ['arrow_down', 'arrow_up']); assert.equal(h.layerCreates, 1); - assert.equal(h.status(), '复合候选已连接'); + assert.equal(h.status(), 'Compound data: connected. Connection does not confirm symbol monitoring.'); assert.throws(() => h.controller.run(), /already started/); h.controller.stop('route_changed'); assert.equal(h.shapes.size, 0); @@ -162,7 +162,7 @@ test('503 clears compound state and recovers with a new cursor without clearing await h.parked; assert.equal(h.panel.compoundSize, 1); assert.deepEqual([...h.shapes.keys()], [fixtures[1].candidate_id]); - assert.equal(h.status(), '复合候选已连接'); + assert.equal(h.status(), 'Compound data: connected. Connection does not confirm symbol monitoring.'); }); test('network reconnect retains compound history and original cursor', async (t) => { diff --git a/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js b/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js index 24bf169..e0504e5 100644 --- a/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js +++ b/test/dom/binance-strategy27-events/strategy27-entrypoint.test.js @@ -578,3 +578,43 @@ test('older bootstrap replay cannot shorten the retained display lifetime', asyn assert.deepEqual([...h.shapes.keys()], ['user-owned']); assert.equal(h.page.document.querySelectorAll('[data-role="event-row"]').length, 0); }); + +for (const generated of [false, true]) { + test(`${generated ? 'generated' : 'source'} removed-symbol bootstrap exposes monitoring status across stream reset`, async (t) => { + const h = await harness(t, { generated }); + const removed = { ...ordinaryMessage(), message_kind: 'event_closed', data_status: 'incomplete', + payload: { event: { ...ordinaryOutcomeMessage().payload.event, event_status: 'incomplete', close_reason: 'universe_removed' } } }; + await h.respond('ordinary', { schema_version: 1, status: 'bootstrap', projection_kind: 'strategy27_events', requested_cursor: null, + next_cursor: '2-0', runtime_epoch: removed.runtime_epoch, last_sequence: 2, bootstrap_observed_at_ms: 7000, + records: [{ event_id: removed.event_id, event_envelope: removed, marker_envelope: removed, outcome_envelope: null }] }); + await until(() => h.pending('ordinary').length === 1); + const status = h.page.document.querySelector('[data-role="ordinary-monitoring-status"]'); + assert.equal(status?.dataset.state, 'removed'); + assert.match(status.textContent, /Symbol removed from monitoring/); + await h.respond('ordinary', { schema_version: 1, status: 'ok', requested_cursor: '2-0', next_cursor: '3-0', messages: [ordinaryStreamReset()] }); + await until(() => h.pending('ordinary').length === 1); + assert.equal(status.dataset.state, 'removed'); + const connection = h.page.document.querySelector('[data-role="ordinary-connection-status"]'); + assert.match(connection.textContent, /Connected/); + assert.equal(h.shapes.size, 2); + h.setResolution('1'); h.tick(); + assert.equal(h.page.document.querySelector('[data-role="ordinary-monitoring-status"]'), null); + }); +} + +for (const generated of [false, true]) { + test(`${generated ? 'generated' : 'source'} outcome-only bootstrap restores removed-symbol status`, async (t) => { + const h = await harness(t, { generated }); + const outcome = ordinaryOutcomeMessage(); + outcome.data_status = 'terminated'; + outcome.payload.event.event_status = 'incomplete'; + outcome.payload.event.close_reason = 'universe_removed'; + Object.assign(outcome.payload.outcome, { outcome_status: 'terminated', terminated_at_ms: 2000, termination_reason: 'universe_removed', directional_outcome: null }); + await h.respond('ordinary', { schema_version: 1, status: 'bootstrap', projection_kind: 'strategy27_events', requested_cursor: null, + next_cursor: '3-0', runtime_epoch: outcome.runtime_epoch, last_sequence: 3, bootstrap_observed_at_ms: 7000, + records: [{ event_id: outcome.event_id, event_envelope: outcome, marker_envelope: null, outcome_envelope: outcome }] }); + await until(() => h.pending('ordinary').length === 1); + assert.equal(h.page.document.querySelector('[data-role="ordinary-monitoring-status"]').dataset.state, 'removed'); + assert.equal(h.page.document.querySelector('[data-role="ordinary-connection-status"]').dataset.state, 'connected'); + }); +} diff --git a/test/dom/binance-strategy27-events/strategy27-event-panel.test.js b/test/dom/binance-strategy27-events/strategy27-event-panel.test.js index cf19b12..6dee13b 100644 --- a/test/dom/binance-strategy27-events/strategy27-event-panel.test.js +++ b/test/dom/binance-strategy27-events/strategy27-event-panel.test.js @@ -256,3 +256,41 @@ test('transport history marking preserves selection, facts, bounds and compound panel.destroy(); dom.window.close(); }); + +test('monitoring status remains independent of selected history and connection recovery', () => { + const dom = loadFixtureDom('
'); + const { document } = dom.window; + const panel = createStrategy27EventPanel(document, document.querySelector('.chart-widget-root'), panelOptions(2)); + const removed = { triggered_at_ms: 2000, event_status: 'incomplete', close_reason: 'universe_removed' }; + panel.upsert('old', annotation({ time: 1000 }), 1100); + panel.upsert('removed', annotation({ time: 2000 }), 3000); + panel.observeOrdinaryEvent(removed, 3000); + const monitoring = document.querySelector('[data-role="ordinary-monitoring-status"]'); + const connection = document.querySelector('[data-role="ordinary-connection-status"]'); + assert.equal(monitoring.dataset.state, 'removed'); + assert.match(monitoring.textContent, /Symbol removed from monitoring/); + document.querySelector('[data-event-id="old"]').click(); + panel.retainHistory(); + panel.setOrdinaryConnection('connected'); + panel.setCompoundStatus('Connected', 'normal'); + assert.equal(monitoring.dataset.state, 'removed'); + assert.match(connection.textContent, /Connected/); + panel.observeOrdinaryEvent({ ...removed, triggered_at_ms: 1000 }, 9000); + panel.observeOrdinaryEvent({ triggered_at_ms: 2000, event_status: 'active', close_reason: null }, 10000); + assert.equal(monitoring.dataset.state, 'removed'); + panel.observeOrdinaryEvent({ triggered_at_ms: 4000, event_status: 'active', close_reason: null }, 11000); + assert.equal(monitoring.dataset.state, 'observed'); + assert.doesNotMatch(monitoring.textContent, /removed/); + panel.observeOrdinaryEvent(removed, 12000); + assert.equal(monitoring.dataset.state, 'observed'); + panel.retainHistory(); + assert.equal(monitoring.dataset.state, 'historical'); + panel.setOrdinaryConnection('reconnecting'); + assert.match(connection.textContent, /Reconnecting/); + panel.setOrdinaryConnection('stopped'); + assert.match(connection.textContent, /Stopped/); + panel.clear(); + assert.equal(monitoring.dataset.state, 'historical'); + panel.destroy(); + dom.window.close(); +}); diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js index d4dc4c8..1e51b43 100644 --- a/test/unit/userscript-release-contract.test.js +++ b/test/unit/userscript-release-contract.test.js @@ -63,7 +63,7 @@ test('release contract identifies the generated Strategy 27 annotation artifact' assert.equal(contract.name, '【自写】Binance Strategy 27 事件标注'); assert.equal(contract.namespace, 'binance.strategy27.events'); - assert.equal(contract.version, '0.4.4'); + assert.equal(contract.version, '0.4.5'); assert.equal(contract.runAt, 'document-idle'); assert.equal(contract.updateURL, contract.downloadURL); assert.deepEqual(contract.matches, [