Files
EdgeSSH/tests/ws-reconnect.test.ts
gdz66601 4efa997ad1 fix(ssh): port upstream non-retryable failure handling / 移植上游 SSH 确定性失败处理
Adapt cmliu/CF-Workers-WebSSH b0265afcaf40681066d24cd5d859dc32d73c8863 without importing unrelated network-monitoring changes. Classify asynchronous read-loop failures, stop retries on control messages or close code 4001, and preserve manual reconnect and auxiliary-channel behavior. Add focused backend, reconnect, and browser regressions; accept CRLF in the existing deployment workflow test.
2026-10-02 13:05:19 +08:00

55 lines
2.0 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { WebSocketReconnectManager } from '../frontend/src/ws-reconnect.ts';
class MockSocket extends EventTarget {
readyState = WebSocket.CONNECTING;
close(code: number) {
this.readyState = WebSocket.CLOSED;
const event = new Event('close');
Object.assign(event, { code, reason: 'test' });
this.dispatchEvent(event);
}
}
test('deterministic close never schedules a reconnect', (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let calls = 0;
const manager = new WebSocketReconnectManager({ id: 'SSH', nonRetryableCloseCodes: [4001], onConnect: () => { calls++; return new MockSocket() as any; } });
const socket = new MockSocket();
manager.attach(socket as any);
socket.close(4001);
t.mock.timers.tick(10_000);
assert.equal(calls, 0);
});
test('a rejected reconnect clears its state and cannot schedule another attempt', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let calls = 0;
const replacement = new MockSocket();
const manager = new WebSocketReconnectManager({ id: 'SSH', delays: [1], nonRetryableCloseCodes: [4001], onConnect: () => { calls++; return replacement as any; } });
const socket = new MockSocket();
manager.attach(socket as any);
socket.close(1011);
t.mock.timers.tick(1);
await Promise.resolve();
replacement.close(4001);
t.mock.timers.tick(10_000);
assert.equal(calls, 1);
assert.equal((manager as any).reconnecting, false);
assert.equal((manager as any).timer, null);
});
test('other channels retain their existing retry policy', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let calls = 0;
const manager = new WebSocketReconnectManager({ id: 'SFTP', delays: [1], onConnect: () => { calls++; return new MockSocket() as any; } });
const socket = new MockSocket();
manager.attach(socket as any);
socket.close(4001);
t.mock.timers.tick(1);
await Promise.resolve();
assert.equal(calls, 1);
manager.reset();
});