{"version":3,"file":"terminal-title.test.d.ts","sourceRoot":"","sources":["../../src/core/terminal-title.test.ts"],"names":[],"mappings":"","sourcesContent":["import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { TerminalTitle } from \"./terminal-title.js\";\n\n/** A fake terminal that records title writes */\nclass FakeTerminal {\n\ttitles: string[] = [];\n\tsetTitle(title: string): void {\n\t\tthis.titles.push(title);\n\t}\n\tlastTitle(): string | undefined {\n\t\treturn this.titles[this.titles.length - 1];\n\t}\n}\n\n// Helper to simulate TTY state\nfunction setTty(value: boolean): void {\n\t(process.stdout as unknown as Record<string, unknown>).isTTY = value;\n}\n\ndescribe(\"TerminalTitle\", () => {\n\tlet terminal: FakeTerminal;\n\tconst originalIsTTY = process.stdout.isTTY;\n\n\tbeforeEach(() => {\n\t\tterminal = new FakeTerminal();\n\t\tsetTty(true);\n\t\tvi.useFakeTimers();\n\t});\n\n\tafterEach(() => {\n\t\tvi.useRealTimers();\n\t\tsetTty(originalIsTTY);\n\t});\n\n\tit(\"sets static title with Jensen prefix on initialize\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"django-mmo\");\n\t\ttt.initialize(\"django-mmo\");\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - django-mmo\");\n\t});\n\n\tit(\"does not contain π symbol\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test-project\");\n\t\ttt.initialize(\"test-project\");\n\t\tfor (const title of terminal.titles) {\n\t\t\texpect(title).not.toContain(\"π\");\n\t\t}\n\t});\n\n\tit(\"preserves workspace name in title\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"my-app\");\n\t\ttt.initialize(\"my-app\");\n\t\texpect(terminal.lastTitle()).toContain(\"my-app\");\n\t});\n\n\tit(\"handles workspace with spaces and unicode\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"café project\");\n\t\ttt.initialize(\"café project\");\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - café project\");\n\t});\n\n\tit(\"shows spinner frame when working\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = []; // Clear init title\n\n\t\ttt.setWorking(true);\n\t\texpect(terminal.titles.length).toBeGreaterThan(0);\n\t\tconst lastTitle = terminal.lastTitle()!;\n\t\texpect(lastTitle).toMatch(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Jensen - test$/u);\n\t});\n\n\tit(\"cycles spinner frames over time\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(true);\n\n\t\t// First frame written immediately\n\t\tconst frame0 = terminal.lastTitle()!;\n\n\t\t// Advance time and get next frame\n\t\tvi.advanceTimersByTime(100);\n\t\tconst frame1 = terminal.lastTitle()!;\n\n\t\texpect(frame0).not.toBe(frame1);\n\t});\n\n\tit(\"returns to static title when work stops\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(true);\n\t\tconst workingTitle = terminal.lastTitle()!;\n\t\texpect(workingTitle).toContain(\"⠋\");\n\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test\");\n\t});\n\n\tit(\"stops spinner on dispose\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(true);\n\t\ttt.dispose();\n\n\t\t// After dispose, advance time - no more writes\n\t\tconst countBefore = terminal.titles.length;\n\t\tvi.advanceTimersByTime(500);\n\t\texpect(terminal.titles.length).toBe(countBefore);\n\t});\n\n\tit(\"does not write after dispose\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\ttt.dispose();\n\t\tterminal.titles = [];\n\n\t\t// These should all be no-ops\n\t\ttt.setWorking(true);\n\t\ttt.setWorking(false);\n\t\ttt.initialize(\"other\");\n\n\t\texpect(terminal.titles.length).toBe(0);\n\t});\n\n\tit(\"setWorking(true) is idempotent\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(true);\n\t\tconst countAfterFirst = terminal.titles.length;\n\n\t\ttt.setWorking(true); // Second call should be no-op\n\t\texpect(terminal.titles.length).toBe(countAfterFirst);\n\t});\n\n\tit(\"setWorking(false) is idempotent\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(false); // Already idle, no-op\n\t\texpect(terminal.titles.length).toBe(0);\n\n\t\ttt.setWorking(true);\n\t\ttt.setWorking(false);\n\t\tconst countAfterStop = terminal.titles.length;\n\n\t\ttt.setWorking(false); // Already stopped, no-op\n\t\texpect(terminal.titles.length).toBe(countAfterStop);\n\t});\n\n\tit(\"exception in setWorking(false) after setWorking(true) still stops spinner\", () => {\n\t\t// Simulate: start work, then an exception causes stop\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(true);\n\t\texpect(terminal.lastTitle()).toMatch(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u);\n\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test\");\n\n\t\t// Verify spinner is fully stopped - no more frames\n\t\tconst countAfterStop = terminal.titles.length;\n\t\tvi.advanceTimersByTime(500);\n\t\texpect(terminal.titles.length).toBe(countAfterStop);\n\t});\n\n\tit(\"no-op in non-TTY mode\", () => {\n\t\tsetTty(false);\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(500);\n\t\ttt.setWorking(false);\n\n\t\texpect(terminal.titles.length).toBe(0);\n\t});\n\n\tit(\"handles multiple working cycles\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test\");\n\t\ttt.initialize(\"test\");\n\t\tterminal.titles = [];\n\n\t\t// Cycle 1\n\t\ttt.setWorking(true);\n\t\texpect(terminal.lastTitle()).toMatch(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u);\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test\");\n\n\t\t// Cycle 2\n\t\ttt.setWorking(true);\n\t\texpect(terminal.lastTitle()).toMatch(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u);\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test\");\n\t});\n\n\tit(\"reinitialize during working stops spinner and updates name\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"old\");\n\t\ttt.initialize(\"old\");\n\t\tterminal.titles = [];\n\n\t\ttt.setWorking(true);\n\t\ttt.initialize(\"new\");\n\n\t\t// Should now be static with new name\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - new\");\n\n\t\t// Spinner should be stopped\n\t\tconst beforeCount = terminal.titles.length;\n\t\tvi.advanceTimersByTime(500);\n\t\texpect(terminal.titles.length).toBe(beforeCount);\n\t});\n\n\tit(\"does not pollute stdout with title writes\", () => {\n\t\t// Title writes use setTitle() which writes directly to stdout.\n\t\t// We verify the writes are well-formed OSC sequences by checking\n\t\t// the FakeTerminal captures them, not stdout output content.\n\t\tconst tt = new TerminalTitle(terminal, \"clean\");\n\t\ttt.initialize(\"clean\");\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(200);\n\t\ttt.setWorking(false);\n\n\t\t// All titles should match expected format\n\t\tfor (const title of terminal.titles) {\n\t\t\texpect(title).toMatch(/^(?:Jensen - clean|[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Jensen - clean)$/u);\n\t\t}\n\t});\n\n\tit(\"no π symbol anywhere in any title\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"pi-test\");\n\t\ttt.initialize(\"pi-test\");\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(500);\n\t\ttt.setWorking(false);\n\n\t\tfor (const title of terminal.titles) {\n\t\t\texpect(title).not.toContain(\"π\");\n\t\t\texpect(title).toContain(\"Jensen\");\n\t\t}\n\t});\n});\n\ndescribe(\"TerminalTitle integration lifecycle\", () => {\n\tlet terminal: FakeTerminal;\n\tconst originalIsTTY = process.stdout.isTTY;\n\n\tbeforeEach(() => {\n\t\tterminal = new FakeTerminal();\n\t\tsetTty(true);\n\t\tvi.useFakeTimers();\n\t});\n\n\tafterEach(() => {\n\t\tvi.useRealTimers();\n\t\tsetTty(originalIsTTY);\n\t});\n\n\tit(\"full lifecycle: idle → working → tool → working → done\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"jensen-code\");\n\n\t\t// Session starts - idle\n\t\ttt.initialize(\"jensen-code\");\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - jensen-code\");\n\n\t\t// User sends prompt, agent starts working\n\t\ttt.setWorking(true);\n\t\texpect(terminal.lastTitle()).toMatch(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Jensen - jensen-code$/u);\n\n\t\t// Agent calls a tool (still working)\n\t\tvi.advanceTimersByTime(500); // Several frames during tool execution\n\t\texpect(terminal.lastTitle()).toMatch(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Jensen - jensen-code$/u);\n\n\t\t// Agent finishes\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - jensen-code\");\n\n\t\t// No stray frames after stop\n\t\tconst countAfterStop = terminal.titles.length;\n\t\tvi.advanceTimersByTime(1000);\n\t\texpect(terminal.titles.length).toBe(countAfterStop);\n\t});\n\n\tit(\"lifecycle with cancellation\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test-project\");\n\t\ttt.initialize(\"test-project\");\n\t\tterminal.titles = [];\n\n\t\t// Start working\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(200);\n\n\t\t// Cancelled - should stop immediately\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test-project\");\n\n\t\t// No orphaned frames\n\t\tconst countAfterCancel = terminal.titles.length;\n\t\tvi.advanceTimersByTime(1000);\n\t\texpect(terminal.titles.length).toBe(countAfterCancel);\n\t});\n\n\tit(\"lifecycle with error\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test-project\");\n\t\ttt.initialize(\"test-project\");\n\t\tterminal.titles = [];\n\n\t\t// Start working\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(200);\n\n\t\t// Error - should stop\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test-project\");\n\n\t\tconst countAfterError = terminal.titles.length;\n\t\tvi.advanceTimersByTime(1000);\n\t\texpect(terminal.titles.length).toBe(countAfterError);\n\t});\n\n\tit(\"new session does not inherit working state from previous\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"project-a\");\n\t\ttt.initialize(\"project-a\");\n\t\tterminal.titles = [];\n\n\t\t// Session A starts working\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(200);\n\n\t\t// Session A ends\n\t\ttt.setWorking(false);\n\n\t\t// New session B starts\n\t\ttt.initialize(\"project-b\");\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - project-b\");\n\n\t\t// No spinner from session A\n\t\tconst countAfterNewSession = terminal.titles.length;\n\t\tvi.advanceTimersByTime(1000);\n\t\texpect(terminal.titles.length).toBe(countAfterNewSession);\n\t});\n\n\tit(\"interaction/permission request stops spinner\", () => {\n\t\tconst tt = new TerminalTitle(terminal, \"test-project\");\n\t\ttt.initialize(\"test-project\");\n\t\tterminal.titles = [];\n\n\t\t// Agent is working\n\t\ttt.setWorking(true);\n\t\tvi.advanceTimersByTime(200);\n\n\t\t// Agent needs user permission - stops\n\t\ttt.setWorking(false);\n\t\texpect(terminal.lastTitle()).toBe(\"Jensen - test-project\");\n\n\t\tconst countBeforePermission = terminal.titles.length;\n\t\tvi.advanceTimersByTime(1000);\n\t\texpect(terminal.titles.length).toBe(countBeforePermission);\n\t});\n});\n"]}