2026-10-06 23:11:07 -04:00
|
|
|
import test from 'node:test';
|
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import {
|
|
|
|
|
Client, FlowControl, decodeServer, encodeBinaryInput, encodeInit, encodeInput, encodeResize,
|
2026-10-07 20:15:47 -04:00
|
|
|
parseListOutput, parseTitle, pasteSafe, sessionFromSearch, slugify, stripAnsi, tokenUrl, wsUrl,
|
2026-10-06 23:11:07 -04:00
|
|
|
} from '../web/src/protocol.js';
|
|
|
|
|
|
|
|
|
|
const text = (bytes) => new TextDecoder().decode(bytes);
|
|
|
|
|
|
|
|
|
|
test('init message is bare JSON with the auth token and size', () => {
|
|
|
|
|
assert.deepEqual(JSON.parse(text(encodeInit('dG9r', 120, 40))), { AuthToken: 'dG9r', columns: 120, rows: 40 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('input frames are prefixed with "0" and UTF-8 encoded', () => {
|
|
|
|
|
assert.equal(text(encodeInput('héllo\n')), '0héllo\n');
|
|
|
|
|
assert.deepEqual([...encodeBinaryInput('\x1b\xff')], [0x30, 0x1b, 0xff]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('resize frames are "1" + JSON', () => {
|
|
|
|
|
assert.equal(text(encodeResize(80, 24)), '1{"columns":80,"rows":24}');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('server frames split into command and payload', () => {
|
|
|
|
|
const frame = new TextEncoder().encode('0output').buffer;
|
|
|
|
|
const { type, payload } = decodeServer(frame);
|
|
|
|
|
assert.equal(type, '0');
|
|
|
|
|
assert.equal(text(payload), 'output');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('URLs follow the page location, including a path prefix', () => {
|
|
|
|
|
const loc = { protocol: 'https:', host: 'dev.lotusguild.org', pathname: '/' };
|
|
|
|
|
assert.equal(wsUrl(loc, 'main'), 'wss://dev.lotusguild.org/ws?arg=main');
|
|
|
|
|
assert.equal(wsUrl(loc, null), 'wss://dev.lotusguild.org/ws');
|
|
|
|
|
assert.equal(tokenUrl(loc), 'https://dev.lotusguild.org/token');
|
|
|
|
|
const sub = { protocol: 'http:', host: 'localhost:7681', pathname: '/desk/index.html' };
|
|
|
|
|
assert.equal(wsUrl(sub, '--list'), 'ws://localhost:7681/desk/ws?arg=--list');
|
2026-10-07 03:03:09 -04:00
|
|
|
assert.equal(wsUrl(loc, ['--rename', 'old', 'new']), 'wss://dev.lotusguild.org/ws?arg=--rename&arg=old&arg=new');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('free text becomes a valid session name', () => {
|
|
|
|
|
assert.equal(slugify(' PBS upgrade! '), 'PBS-upgrade');
|
|
|
|
|
assert.equal(slugify('--../etc'), 'etc');
|
|
|
|
|
assert.equal(slugify('tinker_tickets'), 'tinker_tickets');
|
|
|
|
|
assert.equal(slugify('!!!'), '');
|
|
|
|
|
assert.equal(slugify('x'.repeat(80)).length, 64);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('ANSI colour and title sequences are stripped', () => {
|
|
|
|
|
assert.equal(stripAnsi('\x1b[1;31mdevdesk: no\x1b[0m\x1b]0;t\x07'), 'devdesk: no');
|
2026-10-06 23:11:07 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('only valid session names are taken from the URL', () => {
|
|
|
|
|
assert.equal(sessionFromSearch('?arg=tinker_tickets'), 'tinker_tickets');
|
|
|
|
|
assert.equal(sessionFromSearch('?arg=../etc'), null);
|
|
|
|
|
assert.equal(sessionFromSearch('?arg=--list'), null);
|
|
|
|
|
assert.equal(sessionFromSearch(''), null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('tmux titles carry the session and the pane title', () => {
|
|
|
|
|
assert.deepEqual(parseTitle('devdesk:main|✳ Refactor | parser'), { session: 'main', pane: '✳ Refactor | parser' });
|
|
|
|
|
assert.deepEqual(parseTitle('devdesk:picker|Pick a session'), { session: 'picker', pane: 'Pick a session' });
|
|
|
|
|
assert.equal(parseTitle('bash'), null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('session list JSON survives pty line endings', () => {
|
|
|
|
|
const out = '\r\n{"default":"main","sessions":[],\r\n"projects":[]}\r\n';
|
|
|
|
|
assert.deepEqual(parseListOutput(out), { default: 'main', sessions: [], projects: [] });
|
|
|
|
|
const colored = '\x1b[1;39m{\x1b[0m\x1b[34;1m"default"\x1b[0m:\x1b[0;32m"main"\x1b[0m}';
|
|
|
|
|
assert.deepEqual(parseListOutput(colored), { default: 'main' });
|
|
|
|
|
assert.throws(() => parseListOutput('devdesk: tmux server is not running'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('flow control pauses after highWater pending writes and resumes below lowWater', () => {
|
|
|
|
|
const flow = new FlowControl({ limit: 10, highWater: 2, lowWater: 1 });
|
|
|
|
|
assert.deepEqual(flow.onData(5), { command: null, track: false });
|
|
|
|
|
assert.deepEqual(flow.onData(10), { command: null, track: true }); // pending 1
|
|
|
|
|
assert.deepEqual(flow.onData(11), { command: null, track: true }); // pending 2
|
|
|
|
|
assert.deepEqual(flow.onData(11), { command: Client.PAUSE, track: true }); // pending 3
|
|
|
|
|
assert.equal(flow.release(), null); // 2
|
|
|
|
|
assert.equal(flow.release(), null); // 1
|
|
|
|
|
assert.equal(flow.release(), Client.RESUME); // 0
|
|
|
|
|
});
|
2026-10-07 20:15:47 -04:00
|
|
|
|
|
|
|
|
test('untrusted text loses control characters before it is typed (#2)', () => {
|
|
|
|
|
assert.equal(pasteSafe('ok\nrm -rf /\r\x1b[31mred\x07'), 'ok rm -rf / [31mred ');
|
|
|
|
|
assert.equal(pasteSafe('plain text'), 'plain text');
|
|
|
|
|
assert.equal(pasteSafe(null), '');
|
|
|
|
|
});
|