Path: blob/main/src/vs/platform/agentHost/test/node/osc633Parser.test.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';7import { Osc633EventType, Osc633Parser } from '../../node/osc633Parser.js';89suite('Osc633Parser', () => {1011ensureNoDisposablesAreLeakedInTestSuite();1213let parser: Osc633Parser;1415setup(() => {16parser = new Osc633Parser();17});1819// -- Helper to build OSC 633 sequences --------------------------------2021function osc633(payload: string, terminator: 'bel' | 'st' = 'bel'): string {22const term = terminator === 'bel' ? '\x07' : '\x1b\\';23return `\x1b]633;${payload}${term}`;24}2526// -- Basic sequence extraction ----------------------------------------2728test('no sequences returns data unchanged with no events', () => {29const result = parser.parse('hello world');30assert.deepStrictEqual(result, {31cleanedData: 'hello world',32events: [],33});34});3536test('PromptStart (A) with BEL terminator', () => {37const result = parser.parse(`before${osc633('A')}after`);38assert.deepStrictEqual(result, {39cleanedData: 'beforeafter',40events: [{ type: Osc633EventType.PromptStart }],41});42});4344test('CommandStart (B)', () => {45const result = parser.parse(osc633('B'));46assert.deepStrictEqual(result, {47cleanedData: '',48events: [{ type: Osc633EventType.CommandStart }],49});50});5152test('CommandExecuted (C)', () => {53const result = parser.parse(osc633('C'));54assert.deepStrictEqual(result, {55cleanedData: '',56events: [{ type: Osc633EventType.CommandExecuted }],57});58});5960test('CommandFinished (D) without exit code', () => {61const result = parser.parse(osc633('D'));62assert.deepStrictEqual(result, {63cleanedData: '',64events: [{ type: Osc633EventType.CommandFinished, exitCode: undefined }],65});66});6768test('CommandFinished (D) with exit code 0', () => {69const result = parser.parse(osc633('D;0'));70assert.deepStrictEqual(result, {71cleanedData: '',72events: [{ type: Osc633EventType.CommandFinished, exitCode: 0 }],73});74});7576test('CommandFinished (D) with non-zero exit code', () => {77const result = parser.parse(osc633('D;127'));78assert.deepStrictEqual(result, {79cleanedData: '',80events: [{ type: Osc633EventType.CommandFinished, exitCode: 127 }],81});82});8384test('CommandLine (E) with command and nonce', () => {85const result = parser.parse(osc633('E;echo\\x20hello;my-nonce'));86assert.deepStrictEqual(result, {87cleanedData: '',88events: [{89type: Osc633EventType.CommandLine,90commandLine: 'echo hello',91nonce: 'my-nonce',92}],93});94});9596test('CommandLine (E) without nonce', () => {97const result = parser.parse(osc633('E;ls\\x20-la'));98assert.deepStrictEqual(result, {99cleanedData: '',100events: [{101type: Osc633EventType.CommandLine,102commandLine: 'ls -la',103nonce: undefined,104}],105});106});107108test('CommandLine (E) with escaped backslash', () => {109const result = parser.parse(osc633('E;echo\\x20\\\\hello'));110assert.deepStrictEqual(result, {111cleanedData: '',112events: [{113type: Osc633EventType.CommandLine,114commandLine: 'echo \\hello',115nonce: undefined,116}],117});118});119120test('Property (P) Cwd', () => {121const result = parser.parse(osc633('P;Cwd=/home/user'));122assert.deepStrictEqual(result, {123cleanedData: '',124events: [{125type: Osc633EventType.Property,126key: 'Cwd',127value: '/home/user',128}],129});130});131132test('Property (P) without value is ignored', () => {133const result = parser.parse(osc633('P;NoEquals'));134assert.deepStrictEqual(result, {135cleanedData: '',136events: [],137});138});139140// -- ST terminator ----------------------------------------------------141142test('PromptStart (A) with ST terminator', () => {143const result = parser.parse(`before${osc633('A', 'st')}after`);144assert.deepStrictEqual(result, {145cleanedData: 'beforeafter',146events: [{ type: Osc633EventType.PromptStart }],147});148});149150// -- Multiple sequences in one chunk ----------------------------------151152test('multiple sequences in one chunk', () => {153const data = `prompt${osc633('A')}$ ${osc633('B')}${osc633('E;ls;nonce1')}${osc633('C')}file1\nfile2\n${osc633('D;0')}`;154const result = parser.parse(data);155assert.deepStrictEqual(result, {156cleanedData: 'prompt$ file1\nfile2\n',157events: [158{ type: Osc633EventType.PromptStart },159{ type: Osc633EventType.CommandStart },160{ type: Osc633EventType.CommandLine, commandLine: 'ls', nonce: 'nonce1' },161{ type: Osc633EventType.CommandExecuted },162{ type: Osc633EventType.CommandFinished, exitCode: 0 },163],164});165});166167// -- Non-633 OSC sequences are preserved ------------------------------168169test('non-633 OSC sequences are preserved in output', () => {170const nonOsc = '\x1b]0;window title\x07';171const result = parser.parse(`before${nonOsc}after`);172assert.deepStrictEqual(result, {173cleanedData: `before${nonOsc}after`,174events: [],175});176});177178test('non-633 OSC sequences preserve ST terminator in output', () => {179const nonOsc = '\x1b]0;window title\x1b\\';180const result = parser.parse(`before${nonOsc}after`);181assert.deepStrictEqual(result, {182cleanedData: `before${nonOsc}after`,183events: [],184});185});186187// -- Partial sequences across chunks ----------------------------------188189test('sequence split across two chunks (split in payload)', () => {190const r1 = parser.parse('before\x1b]633;');191assert.strictEqual(r1.cleanedData, 'before');192assert.deepStrictEqual(r1.events, []);193194const r2 = parser.parse('A\x07after');195assert.strictEqual(r2.cleanedData, 'after');196assert.deepStrictEqual(r2.events, [{ type: Osc633EventType.PromptStart }]);197});198199test('sequence split across two chunks (split at ESC of ST terminator)', () => {200// First chunk ends with ESC (potential start of ST)201const r1 = parser.parse('data\x1b]633;D;42\x1b');202assert.strictEqual(r1.cleanedData, 'data');203assert.deepStrictEqual(r1.events, []);204205// Second chunk starts with \ (completing ST)206const r2 = parser.parse('\\more');207assert.strictEqual(r2.cleanedData, 'more');208assert.deepStrictEqual(r2.events, [{ type: Osc633EventType.CommandFinished, exitCode: 42 }]);209});210211test('non-633 OSC sequence split at ESC of ST terminator preserves ST', () => {212const r1 = parser.parse('before\x1b]0;window title\x1b');213assert.strictEqual(r1.cleanedData, 'before');214assert.deepStrictEqual(r1.events, []);215216const r2 = parser.parse('\\after');217assert.strictEqual(r2.cleanedData, '\x1b]0;window title\x1b\\after');218assert.deepStrictEqual(r2.events, []);219});220221test('sequence split across three chunks', () => {222const r1 = parser.parse('\x1b]63');223assert.strictEqual(r1.cleanedData, '');224assert.deepStrictEqual(r1.events, []);225226const r2 = parser.parse('3;C');227assert.strictEqual(r2.cleanedData, '');228assert.deepStrictEqual(r2.events, []);229230const r3 = parser.parse('\x07output');231assert.strictEqual(r3.cleanedData, 'output');232assert.deepStrictEqual(r3.events, [{ type: Osc633EventType.CommandExecuted }]);233});234235// -- Full command lifecycle -------------------------------------------236237test('full command lifecycle with interleaved data', () => {238const allEvents: typeof result.events = [];239let allCleaned = '';240241// Prompt appears242let result = parser.parse(`${osc633('A')}user@host:~ $ ${osc633('B')}`);243allEvents.push(...result.events);244allCleaned += result.cleanedData;245246// User types command, shell reports it and executes247result = parser.parse(`${osc633('E;echo\\x20hi;nonce1')}${osc633('C')}`);248allEvents.push(...result.events);249allCleaned += result.cleanedData;250251// Command output252result = parser.parse('hi\r\n');253allEvents.push(...result.events);254allCleaned += result.cleanedData;255256// Command finishes257result = parser.parse(`${osc633('D;0')}${osc633('A')}`);258allEvents.push(...result.events);259allCleaned += result.cleanedData;260261assert.strictEqual(allCleaned, 'user@host:~ $ hi\r\n');262assert.deepStrictEqual(allEvents, [263{ type: Osc633EventType.PromptStart },264{ type: Osc633EventType.CommandStart },265{ type: Osc633EventType.CommandLine, commandLine: 'echo hi', nonce: 'nonce1' },266{ type: Osc633EventType.CommandExecuted },267{ type: Osc633EventType.CommandFinished, exitCode: 0 },268{ type: Osc633EventType.PromptStart },269]);270});271272// -- Edge cases -------------------------------------------------------273274test('empty string', () => {275const result = parser.parse('');276assert.deepStrictEqual(result, { cleanedData: '', events: [] });277});278279test('data with regular ANSI escape sequences (non-OSC) passes through', () => {280const ansi = '\x1b[31mred\x1b[0m';281const result = parser.parse(ansi);282assert.deepStrictEqual(result, { cleanedData: ansi, events: [] });283});284285test('CommandLine (E) with semicolons in command', () => {286// Semicolons in the command line should be escaped as \x3b287const result = parser.parse(osc633('E;echo\\x3b\\x20hello;my-nonce'));288assert.deepStrictEqual(result, {289cleanedData: '',290events: [{291type: Osc633EventType.CommandLine,292commandLine: 'echo; hello',293nonce: 'my-nonce',294}],295});296});297});298299300