Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/terminal.test.mjs

3.8 KiB, 1 run

created by r2519314175:1520, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* ============================================================
2 Test — the terminal parser survives a control string split
3 between two `write` calls.
4 ------------------------------------------------------------
5 Drives the REAL www/js/terminal.js screen model. The String
6 Terminator that ends an OSC or DCS sequence is two bytes,
7 ESC then '\' (0x1b 0x5c). A pty read splits a burst of output
8 anywhere, so those two bytes routinely arrive in DIFFERENT
9 `write` calls -- the comment at the top of the parser says a
10 split sequence is "the normal case, not the odd one".
11
12 The bug this proves against: the ST was recognised by looking
13 BACK one byte in the current chunk (`s.charCodeAt(i - 1)`),
14 which is NaN when the ESC ended the previous chunk. The
15 terminator was then missed and the parser went on swallowing
16 the rest of the session into the string payload, drawing
17 nothing -- a live terminal that looks hung, which is exactly
18 what an interactive `ssh` produced (its remote shell and tmux
19 emit OSC/DCS strings, and ssh delivers the remote's output in
20 many small, arbitrarily split writes).
21
22 Run: node www/js/terminal.test.mjs
23 ============================================================ */
24import { readFileSync } from 'node:fs';
25import { fileURLToPath } from 'node:url';
26import { dirname, join } from 'node:path';
27
28const HERE = dirname(fileURLToPath(import.meta.url));
29
30// The file is a browser IIFE that assigns window.DaimondTerminal.
31// It touches `window` only inside functions, so a bare object is
32// enough to load it; the screen model needs no DOM at all.
33globalThis.window = globalThis.window || {};
34(0, eval)(readFileSync(join(HERE, 'terminal.js'), 'utf8'));
35const T = globalThis.window.DaimondTerminal;
36
37const enc = new TextEncoder();
38let failures = 0;
39function check(name, cond) {
40 if (cond) { console.log(' ok ' + name); }
41 else { console.log(' FAIL ' + name); failures++; }
42}
43
44// The first row's text, trailing blanks trimmed.
45function row0(chunks) {
46 const S = T.screen(40, 4, {});
47 for (const c of chunks) S.write(c);
48 return S.lineText(S.absOfRow(0)).replace(/\s+$/, '');
49}
50
51console.log('String Terminator handling');
52
53// The sequences that already worked, kept so a fix cannot regress them.
54check('OSC ended by ST in one write draws the text after it',
55 row0(['\x1b]0;title\x1b\\AFTER']) === 'AFTER');
56check('OSC ended by BEL in one write draws the text after it',
57 row0(['\x1b]0;title\x07AFTER']) === 'AFTER');
58
59// The bug: the ST split across two writes -- ESC ends the first,
60// '\' begins the second. Everything after it must still draw.
61check('ST split across writes still terminates the string',
62 row0(['\x1b]0;title\x1b', '\\AFTER']) === 'AFTER');
63check('ST split across writes, at the byte level (Uint8Array)',
64 row0([enc.encode('\x1b]0;title\x1b'), enc.encode('\\AFTER')]) === 'AFTER');
65
66// A semantic-prompt marker (OSC 133) split the same way: the ground
67// text before it and the shell prompt after it must both survive.
68check('a split OSC-133 prompt marker does not swallow the prompt',
69 row0(['pre\x1b]133;A\x1b', '\\$ hi']) === 'pre$ hi');
70
71// A DCS string (ESC P ... ST) split the same way: the payload is
72// discarded, but the text after the ST draws.
73check('DCS payload split at its ST discards cleanly and keeps drawing',
74 row0(['\x1bPq;junk\x1b', '\\AFTER']) === 'AFTER');
75
76// Preserved oddities: an ESC inside a string that is NOT part of an ST
77// is dropped and the string continues to its real terminator, exactly
78// as before the fix.
79check('ESC not followed by \\ inside a string is dropped, string continues',
80 row0(['\x1b]0;a\x1bXb\x07AFTER']) === 'AFTER');
81check('ESC ESC \\ within one write terminates on the real ST',
82 row0(['\x1b]0;t\x1b\x1b\\AFTER']) === 'AFTER');
83
84console.log(failures === 0 ? '\nALL PASS' : ('\n' + failures + ' FAILURE(S)'));
85if (failures) process.exitCode = 1;