|
1 | | -import { describe, it, expect, beforeEach, mock, spyOn, afterAll } from 'bun:test' |
| 1 | +import { describe, it, expect, beforeEach, afterEach, mock, spyOn, afterAll } from 'bun:test' |
2 | 2 | import { ptySpawn } from '../src/plugin/pty/tools/spawn.ts' |
3 | 3 | import { ptyRead } from '../src/plugin/pty/tools/read.ts' |
4 | 4 | import { ptyList } from '../src/plugin/pty/tools/list.ts' |
5 | 5 | import { RingBuffer } from '../src/plugin/pty/buffer.ts' |
6 | 6 | import { manager } from '../src/plugin/pty/manager.ts' |
| 7 | +import { sanitizeTerminalText } from '../src/plugin/pty/sanitize.ts' |
7 | 8 |
|
8 | 9 | describe('PTY Tools', () => { |
9 | 10 | afterAll(() => { |
@@ -239,12 +240,76 @@ describe('PTY Tools', () => { |
239 | 240 |
|
240 | 241 | const result = await ptyRead.execute(args, ctx) |
241 | 242 |
|
242 | | - expect(manager.search).toHaveBeenCalledWith('test-session-id', /line/, 0, 500) |
| 243 | + expect(manager.search).toHaveBeenCalledWith( |
| 244 | + 'test-session-id', |
| 245 | + /line/, |
| 246 | + 0, |
| 247 | + 500, |
| 248 | + sanitizeTerminalText |
| 249 | + ) |
243 | 250 | expect(result).toContain('<pty_output id="test-session-id" status="running" pattern="line">') |
244 | 251 | expect(result).toContain('00001| line 1') |
245 | 252 | expect(result).toContain('(1 match from 2 total lines)') |
246 | 253 | }) |
247 | 254 |
|
| 255 | + describe('terminal control sequences', () => { |
| 256 | + const original = process.env.PTY_SANITIZE_OUTPUT |
| 257 | + const ctx = { |
| 258 | + sessionID: 'parent', |
| 259 | + messageID: 'msg', |
| 260 | + agent: 'agent', |
| 261 | + abort: new AbortController().signal, |
| 262 | + metadata: () => {}, |
| 263 | + ask: async () => {}, |
| 264 | + directory: '/tmp', |
| 265 | + worktree: '/tmp', |
| 266 | + } |
| 267 | + |
| 268 | + beforeEach(() => { |
| 269 | + spyOn(manager, 'read').mockReturnValue({ |
| 270 | + lines: ['\x1b[?9001h\x1b[?1004h\x1b[2J\x1b[H', '\x1b[31merror:\x1b[0m boom\r'], |
| 271 | + offset: 0, |
| 272 | + hasMore: false, |
| 273 | + totalLines: 2, |
| 274 | + }) |
| 275 | + }) |
| 276 | + |
| 277 | + afterEach(() => { |
| 278 | + if (original === undefined) { |
| 279 | + delete process.env.PTY_SANITIZE_OUTPUT |
| 280 | + } else { |
| 281 | + process.env.PTY_SANITIZE_OUTPUT = original |
| 282 | + } |
| 283 | + }) |
| 284 | + |
| 285 | + it('strips them from read output', async () => { |
| 286 | + delete process.env.PTY_SANITIZE_OUTPUT |
| 287 | + |
| 288 | + const result = await ptyRead.execute({ id: 'test-session-id' }, ctx) |
| 289 | + |
| 290 | + expect(result).not.toContain('\x1b') |
| 291 | + expect(result).not.toContain('\r') |
| 292 | + expect(result).toContain('00001| \n') |
| 293 | + expect(result).toContain('00002| error: boom\n') |
| 294 | + }) |
| 295 | + |
| 296 | + it('keeps them when PTY_SANITIZE_OUTPUT=0', async () => { |
| 297 | + process.env.PTY_SANITIZE_OUTPUT = '0' |
| 298 | + |
| 299 | + const result = await ptyRead.execute({ id: 'test-session-id' }, ctx) |
| 300 | + |
| 301 | + expect(result).toContain('00002| \x1b[31merror:\x1b[0m boom\r') |
| 302 | + }) |
| 303 | + |
| 304 | + it('does not normalize search lines when PTY_SANITIZE_OUTPUT=0', async () => { |
| 305 | + process.env.PTY_SANITIZE_OUTPUT = '0' |
| 306 | + |
| 307 | + await ptyRead.execute({ id: 'test-session-id', pattern: 'boom' }, ctx) |
| 308 | + |
| 309 | + expect(manager.search).toHaveBeenCalledWith('test-session-id', /boom/, 0, 500, undefined) |
| 310 | + }) |
| 311 | + }) |
| 312 | + |
248 | 313 | it('should throw for invalid session', async () => { |
249 | 314 | spyOn(manager, 'get').mockReturnValue(null) |
250 | 315 |
|
@@ -378,6 +443,18 @@ describe('PTY Tools', () => { |
378 | 443 | ]) |
379 | 444 | }) |
380 | 445 |
|
| 446 | + it('should match and return normalized lines when searching with a normalizer', () => { |
| 447 | + const buffer = new RingBuffer(200) |
| 448 | + buffer.append('\x1b[?25l\x1b[2J\n\x1b[31merror:\x1b[0m boom\nok 1\n') |
| 449 | + |
| 450 | + expect(buffer.search(/^error: boom$/, sanitizeTerminalText)).toEqual([ |
| 451 | + { lineNumber: 2, text: 'error: boom' }, |
| 452 | + ]) |
| 453 | + // Digits inside escape sequences must not produce matches. |
| 454 | + expect(buffer.search(/\d/, sanitizeTerminalText)).toEqual([{ lineNumber: 3, text: 'ok 1' }]) |
| 455 | + expect(buffer.readRaw()).toContain('\x1b[31m') |
| 456 | + }) |
| 457 | + |
381 | 458 | it('should clear buffer', () => { |
382 | 459 | const buffer = new RingBuffer(100) |
383 | 460 | buffer.append('line1\nline2') |
|
0 commit comments