basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2026 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, beforeEach, afterEach } from 'vitest';8import {9 detectTerminal,10 osc,11 wrapForMultiplexer,12 oscITerm2Notify,13 oscKittyNotify,14 oscGhosttyNotify,15 sanitizeOscPayload,16 encodeKittyPayload,17 OSC,18 BEL,19 ST,20} from './osc.js';21 22describe('detectTerminal', () => {23 const originalEnv = { ...process.env };24 25 beforeEach(() => {26 delete process.env['TERM_PROGRAM'];27 delete process.env['TERM'];28 delete process.env['KITTY_WINDOW_ID'];29 });30 31 afterEach(() => {32 process.env = { ...originalEnv };33 });34 35 it('detects iTerm.app via TERM_PROGRAM', () => {36 process.env['TERM_PROGRAM'] = 'iTerm.app';37 expect(detectTerminal()).toBe('iTerm.app');38 });39 40 it('detects kitty via TERM_PROGRAM', () => {41 process.env['TERM_PROGRAM'] = 'kitty';42 expect(detectTerminal()).toBe('kitty');43 });44 45 it('detects ghostty via TERM_PROGRAM', () => {46 process.env['TERM_PROGRAM'] = 'ghostty';47 expect(detectTerminal()).toBe('ghostty');48 });49 50 it('detects Apple_Terminal via TERM_PROGRAM', () => {51 process.env['TERM_PROGRAM'] = 'Apple_Terminal';52 expect(detectTerminal()).toBe('Apple_Terminal');53 });54 55 it('detects kitty via TERM=xterm-kitty when TERM_PROGRAM is absent', () => {56 process.env['TERM'] = 'xterm-kitty';57 expect(detectTerminal()).toBe('kitty');58 });59 60 it('detects ghostty via TERM=xterm-ghostty when TERM_PROGRAM is absent', () => {61 process.env['TERM'] = 'xterm-ghostty';62 expect(detectTerminal()).toBe('ghostty');63 });64 65 it('detects kitty via KITTY_WINDOW_ID as fallback', () => {66 process.env['KITTY_WINDOW_ID'] = '1';67 expect(detectTerminal()).toBe('kitty');68 });69 70 it('returns unknown when no terminal is detected', () => {71 expect(detectTerminal()).toBe('unknown');72 });73 74 it('TERM_PROGRAM takes priority over TERM', () => {75 process.env['TERM'] = 'xterm-ghostty';76 process.env['TERM_PROGRAM'] = 'iTerm.app';77 expect(detectTerminal()).toBe('iTerm.app');78 });79 80 it('falls back to TERM when TERM_PROGRAM is not a known terminal', () => {81 process.env['TERM'] = 'xterm-ghostty';82 process.env['TERM_PROGRAM'] = 'some-unknown-terminal';83 expect(detectTerminal()).toBe('ghostty');84 });85 86 it('KITTY_WINDOW_ID is last resort fallback', () => {87 process.env['TERM'] = 'xterm-256color';88 process.env['KITTY_WINDOW_ID'] = '1';89 expect(detectTerminal()).toBe('kitty');90 });91});92 93describe('osc', () => {94 const originalEnv = { ...process.env };95 96 afterEach(() => {97 process.env = { ...originalEnv };98 });99 100 it('builds OSC sequence with BEL terminator for non-kitty', () => {101 process.env['TERM_PROGRAM'] = 'iTerm.app';102 delete process.env['TERM'];103 const result = osc(9, 'hello');104 expect(result).toBe(`\x1b]9;hello${BEL}`);105 });106 107 it('builds OSC sequence with ST terminator for kitty', () => {108 process.env['TERM'] = 'xterm-kitty';109 delete process.env['TERM_PROGRAM'];110 delete process.env['STY'];111 const result = osc(99, 'test');112 expect(result).toBe(`\x1b]99;test${ST}`);113 });114 115 it('falls back to BEL for kitty inside screen to avoid ST conflict with DCS wrapper', () => {116 process.env['TERM'] = 'xterm-kitty';117 delete process.env['TERM_PROGRAM'];118 process.env['STY'] = '12345.pts-0.host';119 const result = osc(99, 'test');120 expect(result).toBe(`\x1b]99;test${BEL}`);121 });122 123 it('joins multiple parts with semicolons', () => {124 process.env['TERM_PROGRAM'] = 'ghostty';125 delete process.env['TERM'];126 const result = osc(777, 'notify', 'Title', 'Body');127 expect(result).toBe(`\x1b]777;notify;Title;Body${BEL}`);128 });129});130 131describe('wrapForMultiplexer', () => {132 const originalEnv = { ...process.env };133 134 afterEach(() => {135 process.env = { ...originalEnv };136 });137 138 it('returns sequence unchanged outside multiplexer', () => {139 delete process.env['TMUX'];140 delete process.env['STY'];141 const seq = `\x1b]9;hello${BEL}`;142 expect(wrapForMultiplexer(seq)).toBe(seq);143 });144 145 it('wraps in DCS passthrough for tmux with ESC doubling', () => {146 process.env['TMUX'] = '/tmp/tmux-1000/default,12345,0';147 delete process.env['STY'];148 const seq = `\x1b]9;hello${BEL}`;149 // ESC bytes in payload should be doubled150 expect(wrapForMultiplexer(seq)).toBe(151 `\x1bPtmux;\x1b\x1b]9;hello${BEL}\x1b\\`,152 );153 });154 155 it('wraps in DCS passthrough for screen', () => {156 delete process.env['TMUX'];157 process.env['STY'] = '12345.pts-0.host';158 const seq = `\x1b]9;hello${BEL}`;159 expect(wrapForMultiplexer(seq)).toBe(`\x1bP${seq}\x1b\\`);160 });161});162 163describe('oscITerm2Notify', () => {164 const originalEnv = { ...process.env };165 166 afterEach(() => {167 process.env = { ...originalEnv };168 });169 170 it('formats notification with title and message', () => {171 process.env['TERM_PROGRAM'] = 'iTerm.app';172 delete process.env['TERM'];173 const result = oscITerm2Notify('Qwen Code', 'Hello');174 expect(result).toContain('Qwen Code:\nHello');175 });176 177 it('formats notification without title', () => {178 process.env['TERM_PROGRAM'] = 'iTerm.app';179 delete process.env['TERM'];180 const result = oscITerm2Notify('', 'Hello');181 expect(result).toContain('Hello');182 expect(result).not.toContain(':');183 });184});185 186describe('encodeKittyPayload', () => {187 it('base64-encodes ASCII text', () => {188 expect(encodeKittyPayload('Title')).toBe(189 Buffer.from('Title', 'utf8').toString('base64'),190 );191 });192 193 it('base64-encodes UTF-8 text', () => {194 expect(encodeKittyPayload('你好')).toBe(195 Buffer.from('你好', 'utf8').toString('base64'),196 );197 });198});199 200describe('oscKittyNotify', () => {201 const originalEnv = { ...process.env };202 203 afterEach(() => {204 process.env = { ...originalEnv };205 });206 207 it('returns three-step protocol sequences with base64-encoded payloads', () => {208 process.env['TERM'] = 'xterm-kitty';209 delete process.env['TERM_PROGRAM'];210 delete process.env['STY'];211 const seqs = oscKittyNotify('Title', 'Body', 42);212 expect(seqs).toHaveLength(3);213 const b64Title = Buffer.from('Title', 'utf8').toString('base64');214 const b64Body = Buffer.from('Body', 'utf8').toString('base64');215 // Step 1: title with e=1 (base64)216 expect(seqs[0]).toContain('i=42:d=0:p=title:e=1');217 expect(seqs[0]).toContain(b64Title);218 expect(seqs[0]).not.toContain('Title');219 // Step 2: body with e=1 (base64)220 expect(seqs[1]).toContain('i=42:p=body:e=1');221 expect(seqs[1]).toContain(b64Body);222 expect(seqs[1]).not.toContain('Body');223 // Step 3: activate (no payload to encode)224 expect(seqs[2]).toContain('i=42:d=1:a=focus');225 });226});227 228describe('oscGhosttyNotify', () => {229 const originalEnv = { ...process.env };230 231 afterEach(() => {232 process.env = { ...originalEnv };233 });234 235 it('formats OSC 777 notification', () => {236 process.env['TERM_PROGRAM'] = 'ghostty';237 delete process.env['TERM'];238 const result = oscGhosttyNotify('Title', 'Body');239 expect(result).toBe(`\x1b]${OSC.GHOSTTY};notify;Title;Body${BEL}`);240 });241});242 243describe('sanitizeOscPayload', () => {244 it('strips ESC character', () => {245 expect(sanitizeOscPayload('hello\x1bworld')).toBe('helloworld');246 });247 248 it('strips BEL character', () => {249 expect(sanitizeOscPayload('hello\x07world')).toBe('helloworld');250 });251 252 it('strips all C0 control characters', () => {253 expect(sanitizeOscPayload('a\x00b\x01c\x1fd')).toBe('abcd');254 });255 256 it('preserves normal text', () => {257 expect(sanitizeOscPayload('Hello World! 你好')).toBe('Hello World! 你好');258 });259 260 it('strips C1 control characters', () => {261 expect(sanitizeOscPayload('hello\x9cworld')).toBe('helloworld');262 });263});264 265describe('osc sanitizes payloads', () => {266 const originalEnv = { ...process.env };267 268 afterEach(() => {269 process.env = { ...originalEnv };270 });271 272 it('strips control characters from string parts', () => {273 process.env['TERM_PROGRAM'] = 'ghostty';274 delete process.env['TERM'];275 const result = osc(OSC.GHOSTTY, 'notify', 'Title\x1b]hack', 'Body\x07end');276 expect(result).not.toContain('\x1b]hack');277 expect(result).toContain('Title]hack');278 expect(result).toContain('Bodyend');279 });280});281 