shared.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { afterEach, describe, expect, mock, test } from 'bun:test';
  2. type SpawnResult = {
  3. exited: Promise<number>;
  4. stdout: () => Promise<string>;
  5. stderr: () => Promise<string>;
  6. };
  7. const crossSpawnMock = mock(
  8. (_args: string[]): SpawnResult => ({
  9. exited: Promise.resolve(0),
  10. stdout: () => Promise.resolve(''),
  11. stderr: () => Promise.resolve(''),
  12. }),
  13. );
  14. mock.module('../utils/compat', () => ({
  15. crossSpawn: crossSpawnMock,
  16. }));
  17. let importCounter = 0;
  18. async function importShared() {
  19. return import(`./shared?test=${importCounter++}`);
  20. }
  21. describe('gracefulClosePane', () => {
  22. afterEach(() => {
  23. crossSpawnMock.mockReset();
  24. });
  25. test('sends Ctrl+C, waits 250ms, then closes, returning true on exit 0', async () => {
  26. const calls: string[][] = [];
  27. crossSpawnMock.mockImplementation((args: string[]) => {
  28. calls.push(args);
  29. return {
  30. exited: Promise.resolve(0),
  31. stdout: () => Promise.resolve(''),
  32. stderr: () => Promise.resolve(''),
  33. };
  34. });
  35. const { gracefulClosePane } = await importShared();
  36. const ok = await gracefulClosePane('tmux', '%1', {
  37. ctrlC: ['send-keys', '-t', '%1', 'C-c'],
  38. close: ['kill-pane', '-t', '%1'],
  39. });
  40. expect(ok).toBe(true);
  41. expect(calls).toHaveLength(2);
  42. });
  43. test('returns true when acceptExitCode1 and exit code is 1', async () => {
  44. crossSpawnMock.mockImplementation(() => ({
  45. exited: Promise.resolve(1),
  46. stdout: () => Promise.resolve(''),
  47. stderr: () => Promise.resolve(''),
  48. }));
  49. const { gracefulClosePane } = await importShared();
  50. const ok = await gracefulClosePane('zellij', 'terminal_1', {
  51. ctrlC: ['action', 'write', '--pane-id', 'terminal_1', '\u0003'],
  52. close: ['action', 'close-pane', '--pane-id', 'terminal_1'],
  53. acceptExitCode1: true,
  54. });
  55. expect(ok).toBe(true);
  56. });
  57. test('returns false on exit 1 when acceptExitCode1 is false', async () => {
  58. crossSpawnMock.mockImplementation(() => ({
  59. exited: Promise.resolve(1),
  60. stdout: () => Promise.resolve(''),
  61. stderr: () => Promise.resolve(''),
  62. }));
  63. const { gracefulClosePane } = await importShared();
  64. const ok = await gracefulClosePane('tmux', '%1', {
  65. ctrlC: ['send-keys', '-t', '%1', 'C-c'],
  66. close: ['kill-pane', '-t', '%1'],
  67. });
  68. expect(ok).toBe(false);
  69. });
  70. test('returns emptyPaneReturnsTrue when paneId is empty', async () => {
  71. const { gracefulClosePane } = await importShared();
  72. const ok = await gracefulClosePane('zellij', '', {
  73. ctrlC: ['action', 'write', '--pane-id', '', '\u0003'],
  74. close: ['action', 'close-pane', '--pane-id', ''],
  75. emptyPaneReturnsTrue: true,
  76. });
  77. expect(ok).toBe(true);
  78. expect(crossSpawnMock.mock.calls).toHaveLength(0);
  79. });
  80. test('returns false when binary is null', async () => {
  81. const { gracefulClosePane } = await importShared();
  82. const ok = await gracefulClosePane(null, '%1', {
  83. ctrlC: ['x'],
  84. close: ['y'],
  85. });
  86. expect(ok).toBe(false);
  87. });
  88. });
  89. describe('buildOpencodeAttachCommand', () => {
  90. test('normalizes Windows backslash paths to forward slashes', async () => {
  91. const original = process.platform;
  92. Object.defineProperty(process, 'platform', {
  93. value: 'win32',
  94. configurable: true,
  95. });
  96. try {
  97. const { buildOpencodeAttachCommand } = await importShared();
  98. const cmd = buildOpencodeAttachCommand(
  99. 'sess',
  100. 'url',
  101. 'C:\\Users\\foo\\repo',
  102. );
  103. expect(cmd).toContain('C:/Users/foo/repo');
  104. } finally {
  105. Object.defineProperty(process, 'platform', {
  106. value: original,
  107. configurable: true,
  108. });
  109. }
  110. });
  111. test('leaves non-Windows paths unchanged', async () => {
  112. const original = process.platform;
  113. Object.defineProperty(process, 'platform', {
  114. value: 'linux',
  115. configurable: true,
  116. });
  117. try {
  118. const { buildOpencodeAttachCommand } = await importShared();
  119. const cmd = buildOpencodeAttachCommand('sess', 'url', '/home/user/repo');
  120. expect(cmd).toContain('/home/user/repo');
  121. } finally {
  122. Object.defineProperty(process, 'platform', {
  123. value: original,
  124. configurable: true,
  125. });
  126. }
  127. });
  128. });