fix(web): repeindre les terminaux au retour-visible + flush ACK ciblé
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

Un terminal caché dans une grille de groupe (ou hors-écran) n'était pas repeint
au retour — `visibilitychange` ne couvre que l'onglet — d'où l'écran noir avec
curseur ; et son ACK ne progressait plus (callback xterm.write throttlé) → le
serveur mettait le PTY en pause → sortie « par à-coups ». Ajoute un
IntersectionObserver (repaint + flushAck ciblé au retour-visible) et expose
Attachment.flushAck (factorisé avec flushAcks).
This commit is contained in:
2026-06-24 10:15:29 +02:00
parent b5236b41c8
commit 0f126cf911
3 changed files with 91 additions and 6 deletions

View File

@@ -0,0 +1,53 @@
// Flush ACK ciblé (Attachment.flushAck) : relance un PTY mis en pause au retour-visible d'une cellule.
// On teste la logique d'ACK sans WebSocket réel (sendControl est public → stubbable).
import { describe, it, expect, vi } from 'vitest';
import { WsClient, Attachment, type TerminalSink } from '../src/lib/ws-client';
const sink: TerminalSink = {
write: () => {},
reset: () => {},
onDetached: () => {},
onControlChanged: () => {},
};
describe('ws-client — flush ACK ciblé (Attachment.flushAck)', () => {
it('envoie un unique ACK cumulatif du reliquat traité, puis est idempotent', () => {
const client = new WsClient();
const send = vi.spyOn(client, 'sendControl').mockImplementation(() => {});
const att = new Attachment(client, 'sid', 'interactive', sink, 80, 24);
att.channel = 2;
att.processedBytes = 1000;
att.flushAck();
expect(send).toHaveBeenCalledTimes(1);
expect(send).toHaveBeenCalledWith({ type: 'ack', channel: 2, bytes: 1000 });
expect(att.lastAckBytes).toBe(1000);
// rien de neuf traité → aucun ACK supplémentaire (n'ACK QUE du réellement traité)
att.flushAck();
expect(send).toHaveBeenCalledTimes(1);
// nouveau reliquat traité → ACK cumulatif mis à jour
att.processedBytes = 1500;
att.flushAck();
expect(send).toHaveBeenCalledTimes(2);
expect(send).toHaveBeenLastCalledWith({ type: 'ack', channel: 2, bytes: 1500 });
});
it('no-op si le canal nest pas attaché (-1) ou si lattachment est fermé', () => {
const client = new WsClient();
const send = vi.spyOn(client, 'sendControl').mockImplementation(() => {});
const notAttached = new Attachment(client, 'a', 'interactive', sink, 80, 24); // channel = -1
notAttached.processedBytes = 500;
notAttached.flushAck();
const closed = new Attachment(client, 'b', 'interactive', sink, 80, 24);
closed.channel = 3;
closed.processedBytes = 500;
closed.closed = true;
closed.flushAck();
expect(send).not.toHaveBeenCalled();
});
});