// 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 n’est pas attaché (-1) ou si l’attachment 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(); }); });