Bläddra i källkod

fix(cockpit): 重连预算用尽可能明确提示并清理终态残留指标

ChenJZ 6 timmar sedan
förälder
incheckning
a49b3ce390
2 ändrade filer med 26 tillägg och 2 borttagningar
  1. 11 2
      src/modules/cockpit/services/videoLiveSession.js
  2. 15 0
      tests/cockpit-video/reconnect.mjs

+ 11 - 2
src/modules/cockpit/services/videoLiveSession.js

@@ -87,7 +87,10 @@ export function createVideoLiveSession(api, update, timers = globalThis) {
         // VideoRequestError carries `category`; accepted errors use `code`.
         const reason = error?.category || error?.code
         return !unrecoverable.has(reason)
-    }    function scheduleRetry(deviceCode, message) {
+    }
+
+    /** Spends one attempt of the budget and schedules the next attempt after its own delay. */
+    function scheduleRetry(deviceCode, message) {
         const attempt = current
         // Spend the budget here, before the timer exists, so the last retry cannot schedule another.
         retryBudget -= 1
@@ -110,7 +113,13 @@ export function createVideoLiveSession(api, update, timers = globalThis) {
         const stopping = stop(), version = generation
         const released = await stopping
         if (disposed || version !== generation) return
-        if (!retryable || !released) { publish({ phase: 'failed' }); return }
+        if (!retryable || !released) {
+            // Running the budget down is actionable information, distinct from a permanent capability error.
+            const exhausted = retryAttempt >= retryDelays.length && !unrecoverable.has(error?.category || error?.code)
+            const reason = exhausted ? `${error.message}(已自动重试 ${retryDelays.length} 次仍失败)` : error.message
+            publish({ phase: 'failed', error: reason, retryAttempt: 0, retryDelayMs: 0, retriesLeft: 0 })
+            return
+        }
         generation = version
         current = attempt
         attempt.version = version

+ 15 - 0
tests/cockpit-video/reconnect.mjs

@@ -132,6 +132,21 @@ test('unrecoverable outcomes never retry automatically', async () => {
     } finally { await retryable.live.dispose() }
 })
 
+test('an exhausted budget names the reason instead of pretending it was the first failure', async () => {
+    const clock = new Clock(), h = failingLive(clock)
+    try {
+        const opening = h.live.start('A')
+        await new Promise(setImmediate)
+        await clock.advance(200000)
+        await opening
+        assert.equal(h.visible().phase, 'failed')
+        assert.ok(h.visible().error.includes('已自动重试 5 次仍失败'), h.visible().error)
+        // Terminal state carries no stale retry indicators that the panel could still render.
+        assert.equal(h.visible().retryAttempt, 0)
+        assert.equal(h.visible().retriesLeft, 0)
+    } finally { await h.live.dispose() }
+})
+
 test('closing cancels pending retries and releases the prepared identity', async () => {
     const clock = new Clock(), h = failingLive(clock)
     try {