fix(tiles): final checkup review follow-ups

- docs/tile-grid-plan.md: the As built bullet on tile loads said a refresh
  clears the screen at its turn in the queue. Since the fetch-first refresh it
  fetches at its turn, keeps the last frame through the wait and its own round
  trip, and resets with the queued in-stream \x1bc only once the capture is in
  hand; a failed, aborted or empty fetch writes nothing and resets nothing.
- docs/architecture-invariants.md: the tile grid's One load queue paragraph
  gets the same correction, and its list of captures that go through the
  TileLoadQueue now names the server {t:'c'} refresh and the dropped-output
  recovery refresh.
- test/terminal-tile-input.test.ts: destroy() cancelling a pending recovery is
  now pinned on the timer itself (armed before destroy(), null right after it,
  read before any timer runs), since the recovery callback's own destroyed
  guard made the fetch check pass either way; a second test pins that
  destroy() starts the live-output count over, so a write callback xterm still
  owed counts nothing. Both fail with the _resetLiveFlow() call removed from
  destroy().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-09 09:48:55 +02:00
parent c614241c48
commit 330203c08b
3 changed files with 27 additions and 3 deletions
+21
View File
@@ -1040,12 +1040,33 @@ describe('TerminalTile live-output flow control: a flood cannot pile up in xterm
fetchMock.mockClear();
term.throwOnWrite = 'boom';
out(ws, 'boom');
const flow = tile as unknown as { _dropRecoveryTimer: unknown; _liveDropped: boolean };
expect(flow._dropRecoveryTimer).not.toBeNull(); // the drop armed one recovery
tile.destroy();
// Cleared by destroy() itself, read before any timer runs: the timer's own
// callback nulls the field and its destroyed guard skips the fetch, so the
// fetch check below alone cannot tell a cleared timer from a leaked one.
expect(flow._dropRecoveryTimer).toBeNull();
expect(flow._liveDropped).toBe(false);
await vi.advanceTimersByTimeAsync(5000);
expect(fetchMock).not.toHaveBeenCalled();
});
it('destroy() starts the count over: a write callback xterm still owed counts nothing', async () => {
TileStatics.LIVE_BACKLOG_BUDGET = 100;
const { tile, ws, term } = await connectTile(makeApp());
ws.open();
term.holdParse = true;
out(ws, 'a'.repeat(60));
expect(inFlight(tile)).toBe(60);
tile.destroy();
expect(inFlight(tile)).toBe(0);
term.parse(); // a callback from before destroy() carries the old epoch
expect(inFlight(tile)).toBe(0);
});
});
describe('the server coming back kicks Pane B', () => {