diff --git a/docs/binance-strategy29-bollinger-development.md b/docs/binance-strategy29-bollinger-development.md index d2dd734..7c43b5d 100644 --- a/docs/binance-strategy29-bollinger-development.md +++ b/docs/binance-strategy29-bollinger-development.md @@ -9,7 +9,7 @@ already-loaded native chart candles. The optional summary reads the authenticate loopback observer gateway; it does not call Binance market-data or account APIs, submit orders, rotate hidden charts, or add remote events as chart drawings. -Install Strategy29 0.2.1 with orderbook 2.7.199 or later, or use it alone. +Install Strategy29 0.2.2 with orderbook 2.7.199 or later, or use it alone. Do not combine it with the embedded observer in orderbook 2.7.198. After updating/disabling the old script, reload the page. An embedded observer is an explicit conflict: Strategy29 stops and displays an upgrade/reload notice. @@ -35,6 +35,20 @@ BFCache pagehide pause it; visibility/pageshow resumes it. Permanent disposal removes its listeners, aborts an in-flight summary request, and invalidates pending drawing work. Non-trading routes perform no candle exports or gateway requests. +The read-only diagnostics retain one `lastLocalFailure` after local fatal cleanup +or stop. It records the export/reconcile/detect/render stage, thrown value type, +bounded name (64 characters) and message (512 characters), route, interval and +pre-cleanup counts. String rejections supply the message directly; missing string +name/message fields are null, and arbitrary rejected objects are not serialized. +Each field is read once. A throwing host accessor leaves that field null and adds +its name to `unreadableFields`; this diagnostic boundary cannot replace the original +monitor failure with a property-read failure. +If a host Proxy throws during rejection classification, the monitor stops that +context and records `classificationFailed: true`; normal fatal errors record false. +The frozen detector/core source and its direct invalid-context assertion remain unchanged. +It describes the last fatal event, not necessarily the active context. It does not +store stack traces, candles, requests or gateway credentials, and is never persisted +or sent to the remote service. Recoverable snapshot races leave it unchanged. Independent instances of each bundle share the same controller regardless of load order. Strategy27 does not participate in this protocol. diff --git a/scripts/binance-strategy29-bollinger.user.js b/scripts/binance-strategy29-bollinger.user.js index ee4258e..e6619e9 100644 --- a/scripts/binance-strategy29-bollinger.user.js +++ b/scripts/binance-strategy29-bollinger.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy29.bollinger // @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.2.1 +// @version 0.2.2 // @author jackhai9 // @description Native Bollinger/SMA60 markers with an optional read-only cross-timeframe summary // @match https://www.binance.com/*/futures/* @@ -1473,6 +1473,7 @@ let bearishBollingerAlertTask = null; let bearishBollingerAlertContext = null; let bollingerIntervalSession = null; + let lastLocalFailure = null; const retiredBollingerLayers = /* @__PURE__ */ new Set(); function clearBearishBollingerAlertContext() { if (bearishBollingerAlertContext) { @@ -1552,19 +1553,31 @@ context.cleanupPending = false; } if (context.failed || bearishBollingerAlertTask) return; + let stage = "export"; const task = (async () => { const bars = await exportClosedTradingViewBars(context.target, context.intervalSession); if (!bars || !isBearishBollingerAlertContextCurrent(context)) return; if (bars.length === 0) return; + stage = "reconcile"; const result = await reconcileBearishBollingerAlertWindow({ bars, cachedWindowKey: context.lastProcessedClosedBarsWindowKey, cachedContentSnapshot: context.lastProcessedClosedBarsContentSnapshot, cachedSignals: context.lastProcessedSignals, - detectSignals: detectBollingerSignals, - renderSignals: (signals) => context.layer.render(signals, { - isCurrent: () => isBearishBollingerAlertContextCurrent(context) - }) + detectSignals: (bars2) => { + stage = "detect"; + const signals = detectBollingerSignals(bars2); + stage = "reconcile"; + return signals; + }, + renderSignals: async (signals) => { + stage = "render"; + const rendered = await context.layer.render(signals, { + isCurrent: () => isBearishBollingerAlertContextCurrent(context) + }); + stage = "reconcile"; + return rendered; + } }); if (result.rendered && isBearishBollingerAlertContextCurrent(context)) { context.lastProcessedClosedBarsWindowKey = result.closedBarsWindowKey; @@ -1575,11 +1588,43 @@ bearishBollingerAlertTask = task; task.catch((error) => { if (bearishBollingerAlertContext !== context || context.intervalSession !== bollingerIntervalSession?.session || context.intervalRevision !== context.intervalSession.revision) return; - const failureKind = applyBollingerAlertTaskFailure(context, error); + let failureKind; + let classificationFailed = false; + try { + failureKind = applyBollingerAlertTaskFailure(context, error); + } catch { + classificationFailed = true; + context.failed = true; + context.cleanupPending = true; + failureKind = "fatal"; + } if (failureKind === "retry") { warn("布林带形态预警本轮快照不一致,保留现有标记并等待下一次采样:", error); return; } + const details = { name: null, message: null }; + const unreadableFields = []; + for (const [key, limit] of [["name", 64], ["message", 512]]) { + try { + const value = key === "message" && typeof error === "string" ? error : error?.[key]; + details[key] = typeof value === "string" ? value.slice(0, limit) : null; + } catch { + unreadableFields.push(key); + } + } + lastLocalFailure = Object.freeze({ + thrownType: error === null ? "null" : typeof error, + classificationFailed, + ...details, + unreadableFields: Object.freeze(unreadableFields), + stage, + routeSymbol: context.routeSymbol, + resolution: context.resolution, + cachedSignalCount: context.lastProcessedSignals === null ? null : context.lastProcessedSignals.length, + layerSizeBeforeCleanup: context.layer.size, + sessionRevision: context.intervalSession.revision, + contextIntervalRevision: context.intervalRevision + }); err("布林带形态预警已停止:", error); }).finally(() => { if (bearishBollingerAlertTask === task) bearishBollingerAlertTask = null; @@ -1598,6 +1643,7 @@ taskPending: bearishBollingerAlertTask !== null, contextPresent: context !== null, failed: context ? context.failed : null, + lastLocalFailure, cleanupPending: context ? context.cleanupPending : null, cachedSignalCount: context?.lastProcessedSignals === null || !context ? null : context.lastProcessedSignals.length, layerSize: context ? context.layer.size : null, diff --git a/src/binance-strategy29-bollinger/index.user.js b/src/binance-strategy29-bollinger/index.user.js index 6d2de0c..833df2c 100644 --- a/src/binance-strategy29-bollinger/index.user.js +++ b/src/binance-strategy29-bollinger/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.strategy29.bollinger // @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.2.1 +// @version 0.2.2 // @author jackhai9 // @description Native Bollinger/SMA60 markers with an optional read-only cross-timeframe summary // @match https://www.binance.com/*/futures/* diff --git a/src/binance-strategy29-bollinger/monitor.js b/src/binance-strategy29-bollinger/monitor.js index 9f33b12..efc7673 100644 --- a/src/binance-strategy29-bollinger/monitor.js +++ b/src/binance-strategy29-bollinger/monitor.js @@ -20,6 +20,7 @@ export function createBollingerMonitor({ let bearishBollingerAlertTask = null; let bearishBollingerAlertContext = null; let bollingerIntervalSession = null; + let lastLocalFailure = null; const retiredBollingerLayers = new Set(); function clearBearishBollingerAlertContext() { @@ -129,19 +130,31 @@ export function createBollingerMonitor({ context.cleanupPending = false; } if (context.failed || bearishBollingerAlertTask) return; + let stage = 'export'; const task = (async () => { const bars = await exportClosedTradingViewBars(context.target, context.intervalSession); if (!bars || !isBearishBollingerAlertContextCurrent(context)) return; if (bars.length === 0) return; + stage = 'reconcile'; const result = await reconcileBearishBollingerAlertWindow({ bars, cachedWindowKey: context.lastProcessedClosedBarsWindowKey, cachedContentSnapshot: context.lastProcessedClosedBarsContentSnapshot, cachedSignals: context.lastProcessedSignals, - detectSignals: detectBollingerSignals, - renderSignals: (signals) => context.layer.render(signals, { - isCurrent: () => isBearishBollingerAlertContextCurrent(context), - }), + detectSignals: (bars) => { + stage = 'detect'; + const signals = detectBollingerSignals(bars); + stage = 'reconcile'; + return signals; + }, + renderSignals: async (signals) => { + stage = 'render'; + const rendered = await context.layer.render(signals, { + isCurrent: () => isBearishBollingerAlertContextCurrent(context), + }); + stage = 'reconcile'; + return rendered; + }, }); if (result.rendered && isBearishBollingerAlertContextCurrent(context)) { context.lastProcessedClosedBarsWindowKey = result.closedBarsWindowKey; @@ -156,7 +169,17 @@ export function createBollingerMonitor({ || context.intervalSession !== bollingerIntervalSession?.session || context.intervalRevision !== context.intervalSession.revision ) return; - const failureKind = applyBollingerAlertTaskFailure(context, error); + let failureKind; + let classificationFailed = false; + try { + failureKind = applyBollingerAlertTaskFailure(context, error); + } catch { + // A revoked host Proxy can throw during instanceof; an unclassifiable rejection must stop this context. + classificationFailed = true; + context.failed = true; + context.cleanupPending = true; + failureKind = 'fatal'; + } if (failureKind === 'retry') { // TradingView can expose one feed-update race through exportData(). Keep the // already-rendered layer and retry the next poll instead of turning a transient @@ -164,6 +187,31 @@ export function createBollingerMonitor({ warn('布林带形态预警本轮快照不一致,保留现有标记并等待下一次采样:', error); return; } + /** Preserve host rejection types without serializing arbitrary objects or stacks. */ + const details = { name: null, message: null }; + const unreadableFields = []; + for (const [key, limit] of [['name', 64], ['message', 512]]) { + try { + const value = key === 'message' && typeof error === 'string' ? error : error?.[key]; + details[key] = typeof value === 'string' ? value.slice(0, limit) : null; + } catch { + // Host rejection accessors may throw; preserve that fact without replacing the original failure. + unreadableFields.push(key); + } + } + lastLocalFailure = Object.freeze({ + thrownType: error === null ? 'null' : typeof error, + classificationFailed, + ...details, + unreadableFields: Object.freeze(unreadableFields), + stage, + routeSymbol: context.routeSymbol, + resolution: context.resolution, + cachedSignalCount: context.lastProcessedSignals === null ? null : context.lastProcessedSignals.length, + layerSizeBeforeCleanup: context.layer.size, + sessionRevision: context.intervalSession.revision, + contextIntervalRevision: context.intervalRevision, + }); err('布林带形态预警已停止:', error); }).finally(() => { if (bearishBollingerAlertTask === task) bearishBollingerAlertTask = null; @@ -186,6 +234,7 @@ export function createBollingerMonitor({ taskPending: bearishBollingerAlertTask !== null, contextPresent: context !== null, failed: context ? context.failed : null, + lastLocalFailure, cleanupPending: context ? context.cleanupPending : null, cachedSignalCount: context?.lastProcessedSignals === null || !context ? null : context.lastProcessedSignals.length, diff --git a/test/dom/binance-strategy29-bollinger/tradingview-bearish-alerts.test.js b/test/dom/binance-strategy29-bollinger/tradingview-bearish-alerts.test.js index cb6cdd7..f55bdad 100644 --- a/test/dom/binance-strategy29-bollinger/tradingview-bearish-alerts.test.js +++ b/test/dom/binance-strategy29-bollinger/tradingview-bearish-alerts.test.js @@ -125,6 +125,7 @@ function createMonitorHarness(fixture, dependencyOverrides = {}) { let bearishBollingerAlertTask = null; let bearishBollingerAlertContext = null; let bollingerIntervalSession = null; + let lastLocalFailure = null; const retiredBollingerLayers = new Set(); const ladderTask = null, continuousLadderTask = null, singleOrderTask = null; const cancelCurrentSymbolOpenOrdersTask = null, chartOrdersRecoveryTask = null; @@ -666,7 +667,7 @@ test('on-demand diagnostics distinguish active, awaiting-data and torn-down stat fixture.chart.getAllShapes = () => { throw new Error('Diagnostics must not audit drawings'); }; fixture.chart.exportData = () => { throw new Error('Diagnostics must not export data'); }; assert.deepEqual(harness.monitor.diagnostics, { - taskPending: false, contextPresent: true, failed: false, + taskPending: false, contextPresent: true, failed: false, lastLocalFailure: null, cleanupPending: false, cachedSignalCount: 1, layerSize: 1, retiredCount: 0, markerSaveStats: { busy: true, mutations: 0, draining: 0, saveRequests: 0, serializations: 0, callbackCount: 0, failureCount: 0, pendingCallbacks: 0 }, @@ -686,6 +687,176 @@ test('on-demand diagnostics distinguish active, awaiting-data and torn-down stat harness.monitor.stop(); }); +for (const stage of ['export', 'render']) { + test(`retains the last local ${stage} failure after clearing the layer and stopping`, async () => { + const fixture = createChartDom(); + const harness = createMonitorHarness(fixture, { + detectBollingerSignals: bars => bars.map(bar => ({ + id: `${bar.time}:warning`, direction: 'bearish', type: 'warning', + time: bar.time, markerPrice: 13, + })), + }); + fixture.chart.exportData = async () => exportResult(Array.from({ length: 6 }, (_, index) => ({ + 0: (index + 1) * 60, 1: 10, 2: 12, 3: 9, 4: 11, + }))); + await harness.tick(); + assert.equal(harness.monitor.diagnostics.cachedSignalCount, 6); + assert.equal(harness.monitor.diagnostics.layerSize, 6); + if (stage === 'export') fixture.chart.exportData = async () => { throw new Error('synthetic export failure'); }; + else fixture.chart.getShapeById = () => undefined; + const message = stage === 'export' ? 'synthetic export failure' : 'TradingView Bollinger alert marker point is invalid'; + await assert.rejects(harness.tick(), { message }); + await harness.tick(); + assert.equal(harness.monitor.diagnostics.failed, true); + assert.equal(harness.monitor.diagnostics.cleanupPending, false); + assert.equal(harness.monitor.diagnostics.layerSize, 0); + const expected = { + thrownType: 'object', classificationFailed: false, name: 'Error', message, unreadableFields: [], stage, routeSymbol: 'BTRUSDT', resolution: '1', + cachedSignalCount: 6, layerSizeBeforeCleanup: 6, + sessionRevision: 0, contextIntervalRevision: 0, + }; + assert.deepEqual(harness.monitor.diagnostics.lastLocalFailure, expected); + assert.equal(Object.isFrozen(harness.monitor.diagnostics.lastLocalFailure), true); + harness.monitor.stop(); + assert.deepEqual(harness.monitor.diagnostics.lastLocalFailure, expected); + fixture.dom.window.close(); + }); +} + +for (const value of ['x'.repeat(600), null, { code: 7 }]) { + test(`records ${value === null ? 'null' : typeof value} host rejection without a diagnostic rejection`, async () => { + const fixture = createChartDom(); + const harness = createMonitorHarness(fixture); + fixture.chart.exportData = async () => { throw value; }; + await assert.rejects(harness.tick(), reason => reason === value); + await harness.tick(); + assert.equal(harness.monitor.diagnostics.failed, true); + assert.equal(harness.monitor.diagnostics.cleanupPending, false); + assert.deepEqual(harness.monitor.diagnostics.lastLocalFailure, { + thrownType: value === null ? 'null' : typeof value, + classificationFailed: false, + name: null, message: typeof value === 'string' ? 'x'.repeat(512) : null, + unreadableFields: [], stage: 'export', routeSymbol: 'BTRUSDT', resolution: '1', + cachedSignalCount: null, layerSizeBeforeCleanup: 0, + sessionRevision: 0, contextIntervalRevision: 0, + }); + assert.equal(harness.errors.length, 1); + assert.equal(harness.errors[0][1], value); + harness.monitor.stop(); + fixture.dom.window.close(); + }); +} + +test('keeps a recoverable snapshot race out of fatal diagnostics and resumes exports', async () => { + const fixture = createChartDom(); + const harness = createMonitorHarness(fixture); + const stable = async () => exportResult([{ 0: 60, 1: 10, 2: 12, 3: 9, 4: 11 }]); + fixture.chart.exportData = stable; + await harness.tick(); + const failure = new TradingViewBarSnapshotInconsistentError('synthetic snapshot race'); + fixture.chart.exportData = async () => { throw failure; }; + await assert.rejects(harness.tick(), reason => reason === failure); + assert.equal(harness.monitor.diagnostics.failed, false); + assert.equal(harness.monitor.diagnostics.cleanupPending, false); + assert.equal(harness.monitor.diagnostics.lastLocalFailure, null); + assert.equal(harness.monitor.diagnostics.layerSize, 1); + fixture.chart.exportData = stable; + await harness.tick(); + assert.equal(harness.monitor.diagnostics.cachedSignalCount, 1); + assert.equal(harness.monitor.diagnostics.lastLocalFailure, null); + assert.deepEqual(harness.errors, []); + harness.monitor.stop(); + fixture.dom.window.close(); +}); + +for (const revoked of [true, false]) { + test(`stops an unclassifiable host Proxy rejection (revoked=${revoked})`, async () => { + const fixture = createChartDom(); + const harness = createMonitorHarness(fixture); + const host = Proxy.revocable({}, { getPrototypeOf() { + throw new Error('synthetic host prototype failure'); + } }); + if (revoked) host.revoke(); + let exports = 0; + fixture.chart.exportData = async () => { exports += 1; throw host.proxy; }; + // assert.rejects itself reads properties of the rejected value, which a revoked Proxy forbids. + await harness.tick().then( + () => assert.fail('The original host rejection must propagate'), + reason => assert.equal(reason, host.proxy), + ); + await harness.tick(); + await harness.tick(); + const recorded = harness.monitor.diagnostics.lastLocalFailure; + assert.equal(recorded.classificationFailed, true); + assert.equal(recorded.thrownType, 'object'); + assert.equal(recorded.name, null); + assert.equal(recorded.message, null); + assert.deepEqual(recorded.unreadableFields, revoked ? ['name', 'message'] : []); + assert.equal(harness.monitor.diagnostics.failed, true); + assert.equal(harness.monitor.diagnostics.cleanupPending, false); + assert.equal(harness.monitor.diagnostics.layerSize, 0); + assert.equal(harness.errors[0][1], host.proxy); + assert.equal(exports, 1); + harness.monitor.stop(); + fixture.dom.window.close(); + }); +} + +for (const throwing of [true, false]) { + test(`reads host rejection accessors once and records unreadable fields (throwing=${throwing})`, async () => { + const fixture = createChartDom(); + const harness = createMonitorHarness(fixture); + const reads = { name: 0, message: 0 }; + const rejection = {}; + for (const key of ['name', 'message']) { + Object.defineProperty(rejection, key, { get() { + reads[key] += 1; + if (throwing) throw new Error('synthetic inaccessible diagnostic field'); + return reads[key] === 1 ? key : null; + } }); + } + fixture.chart.exportData = async () => { throw rejection; }; + await assert.rejects(harness.tick(), reason => reason === rejection); + await harness.tick(); + assert.deepEqual(reads, { name: 1, message: 1 }); + const recorded = harness.monitor.diagnostics.lastLocalFailure; + assert.deepEqual(recorded.unreadableFields, throwing ? ['name', 'message'] : []); + assert.equal(recorded.name, throwing ? null : 'name'); + assert.equal(recorded.message, throwing ? null : 'message'); + assert.equal(recorded.stage, 'export'); + assert.equal(harness.monitor.diagnostics.failed, true); + assert.equal(harness.monitor.diagnostics.cleanupPending, false); + assert.equal(harness.errors[0][1], rejection); + harness.monitor.stop(); + fixture.dom.window.close(); + }); +} + +for (const stage of ['detect', 'reconcile']) { + test(`identifies ${stage} failures before rendering`, async () => { + const fixture = createChartDom(); + const failure = new Error('synthetic detector failure'); + failure.name = 'N'.repeat(80); + const harness = createMonitorHarness(fixture, { + detectBollingerSignals: () => { + if (stage === 'detect') throw failure; + return undefined; + }, + }); + fixture.chart.exportData = async () => exportResult([{ 0: 60, 1: 10, 2: 12, 3: 9, 4: 11 }]); + const message = stage === 'detect' ? failure.message : 'Bollinger signal cache is invalid'; + await assert.rejects(harness.tick(), { message }); + await harness.tick(); + assert.equal(harness.monitor.diagnostics.lastLocalFailure.stage, stage); + assert.equal(harness.monitor.diagnostics.lastLocalFailure.name, stage === 'detect' ? 'N'.repeat(64) : 'Error'); + assert.equal(harness.monitor.diagnostics.lastLocalFailure.message, message); + assert.equal(harness.monitor.diagnostics.failed, true); + assert.equal(fixture.createdOptions.length, 0); + harness.monitor.stop(); + fixture.dom.window.close(); + }); +} + test('requires the current symbol and complete bearish-alert chart API', () => { const { dom, diff --git a/test/unit/binance-strategy29-bollinger/source-regressions.test.js b/test/unit/binance-strategy29-bollinger/source-regressions.test.js index ae26e67..1597196 100644 --- a/test/unit/binance-strategy29-bollinger/source-regressions.test.js +++ b/test/unit/binance-strategy29-bollinger/source-regressions.test.js @@ -52,7 +52,7 @@ test('Bollinger chart alert failures distinguish snapshot races from contract fa }); test('Strategy29 sandbox metadata is exact and coordinates through unsafeWindow', () => { - assert.equal(readUserscriptVersion(entrySource), '0.2.1'); + assert.equal(readUserscriptVersion(entrySource), '0.2.2'); assert.deepEqual( [...entrySource.matchAll(/^\/\/ @grant\s+(\S+)\s*$/gm)].map(match => match[1]), ['unsafeWindow', 'GM_xmlhttpRequest', 'GM_getValue', 'GM_setValue', 'GM_registerMenuCommand'], diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js index cf22d67..8784ae5 100644 --- a/test/unit/userscript-release-contract.test.js +++ b/test/unit/userscript-release-contract.test.js @@ -21,7 +21,7 @@ test('Strategy29 has an independent observation-only install identity', async () const metadata = parseUserscriptMetadata(text); assert.equal(contract.name, '【自写】Binance Strategy 29 布林带信号'); assert.equal(contract.namespace, 'binance.strategy29.bollinger'); - assert.equal(contract.version, '0.2.1'); + assert.equal(contract.version, '0.2.2'); assert.equal(contract.runAt, 'document-start'); assert.equal(contract.updateURL, 'https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-strategy29-bollinger.user.js'); assert.equal(contract.downloadURL, contract.updateURL);