@robota-sdk/agent-transport 3.0.0-beta.75 → 3.0.0-beta.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +17 -14
  3. package/dist/node/headless/index.cjs +1 -1
  4. package/dist/node/headless/index.d.ts +1 -1
  5. package/dist/node/headless/index.js +1 -1
  6. package/dist/node/{headless-mRYilLfC.js → headless-DG_MEbf0.js} +7 -4
  7. package/dist/node/headless-DG_MEbf0.js.map +1 -0
  8. package/dist/node/headless-xq77JBq0.cjs +18 -0
  9. package/dist/node/{index-E8Gx4-lc.d.ts → index-CrJ2Lhn3.d.ts} +16 -3
  10. package/dist/node/index-CrJ2Lhn3.d.ts.map +1 -0
  11. package/dist/node/index-JIvzrtaA.d.ts +52 -0
  12. package/dist/node/index-JIvzrtaA.d.ts.map +1 -0
  13. package/dist/node/{index-CYl7ksS6.d.ts → index-JTD_xtEQ.d.ts} +15 -2
  14. package/dist/node/index-JTD_xtEQ.d.ts.map +1 -0
  15. package/dist/node/index-y_HBOfzO.d.ts +52 -0
  16. package/dist/node/index-y_HBOfzO.d.ts.map +1 -0
  17. package/dist/node/index.cjs +1 -1
  18. package/dist/node/index.d.ts +10 -9
  19. package/dist/node/index.d.ts.map +1 -1
  20. package/dist/node/index.js +1 -1
  21. package/dist/node/index.js.map +1 -1
  22. package/dist/node/programmatic/index.cjs +1 -0
  23. package/dist/node/programmatic/index.d.ts +2 -0
  24. package/dist/node/programmatic/index.js +1 -0
  25. package/dist/node/programmatic-6wLf68qJ.cjs +1 -0
  26. package/dist/node/programmatic-70dxIXtS.js +2 -0
  27. package/dist/node/programmatic-70dxIXtS.js.map +1 -0
  28. package/dist/node/testing/index.cjs +1 -1
  29. package/dist/node/testing/index.d.ts +2 -21
  30. package/dist/node/testing/index.js +1 -2
  31. package/package.json +15 -71
  32. package/src/__tests__/programmatic/programmatic-ask-user.test.ts +32 -0
  33. package/src/__tests__/programmatic/programmatic-driver.test.ts +107 -0
  34. package/src/__tests__/programmatic/programmatic-flows.test.ts +140 -0
  35. package/src/__tests__/transport-registry.stop-all.test.ts +53 -0
  36. package/src/headless/HeadlessInteractionChannel.ts +22 -5
  37. package/src/headless/__tests__/headless-goal-runner.test.ts +108 -0
  38. package/src/headless/__tests__/headless-runner.test.ts +2 -1
  39. package/src/headless/headless-runner.ts +67 -1
  40. package/src/headless/headless-stream-json.ts +1 -1
  41. package/src/headless/print-terminal.ts +1 -0
  42. package/src/index.ts +2 -5
  43. package/src/programmatic/ProgrammaticInteractionChannel.ts +82 -0
  44. package/src/programmatic/createProgrammaticAgent.ts +71 -0
  45. package/src/programmatic/index.ts +5 -0
  46. package/src/testing/index.ts +7 -4
  47. package/src/transport-registry.ts +15 -12
  48. package/dist/node/headless-CT2ibQnr.cjs +0 -15
  49. package/dist/node/headless-mRYilLfC.js.map +0 -1
  50. package/dist/node/http/index.cjs +0 -1
  51. package/dist/node/http/index.d.ts +0 -2
  52. package/dist/node/http/index.js +0 -1
  53. package/dist/node/http-2Jiuflc1.js +0 -2
  54. package/dist/node/http-2Jiuflc1.js.map +0 -1
  55. package/dist/node/http-CBAvefLw.cjs +0 -1
  56. package/dist/node/index-BNccqSpv.d.ts +0 -86
  57. package/dist/node/index-BNccqSpv.d.ts.map +0 -1
  58. package/dist/node/index-BUhHIf7X.d.ts +0 -86
  59. package/dist/node/index-BUhHIf7X.d.ts.map +0 -1
  60. package/dist/node/index-BnAGE-u9.d.ts +0 -33
  61. package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
  62. package/dist/node/index-BrQ4gGw0.d.ts +0 -213
  63. package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
  64. package/dist/node/index-CYl7ksS6.d.ts.map +0 -1
  65. package/dist/node/index-CoeBF21y.d.ts +0 -213
  66. package/dist/node/index-CoeBF21y.d.ts.map +0 -1
  67. package/dist/node/index-DHt-2VQ-.d.ts +0 -46
  68. package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
  69. package/dist/node/index-DMwKN5Le.d.ts +0 -33
  70. package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
  71. package/dist/node/index-E8Gx4-lc.d.ts.map +0 -1
  72. package/dist/node/index-c0M42fsA.d.ts +0 -46
  73. package/dist/node/index-c0M42fsA.d.ts.map +0 -1
  74. package/dist/node/mcp/index.cjs +0 -1
  75. package/dist/node/mcp/index.d.ts +0 -2
  76. package/dist/node/mcp/index.js +0 -1
  77. package/dist/node/mcp-BOglBJNy.cjs +0 -1
  78. package/dist/node/mcp-D3BBVK7C.js +0 -2
  79. package/dist/node/mcp-D3BBVK7C.js.map +0 -1
  80. package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
  81. package/dist/node/testing/index.d.ts.map +0 -1
  82. package/dist/node/testing/index.js.map +0 -1
  83. package/dist/node/tui/index.cjs +0 -1
  84. package/dist/node/tui/index.d.ts +0 -2
  85. package/dist/node/tui/index.js +0 -1
  86. package/dist/node/tui-CcH5EsQh.js +0 -25
  87. package/dist/node/tui-CcH5EsQh.js.map +0 -1
  88. package/dist/node/tui-DznRbcku.cjs +0 -24
  89. package/dist/node/ws/index.cjs +0 -1
  90. package/dist/node/ws/index.d.ts +0 -2
  91. package/dist/node/ws/index.js +0 -1
  92. package/dist/node/ws-Dc2RUwVs.js +0 -2
  93. package/dist/node/ws-Dc2RUwVs.js.map +0 -1
  94. package/dist/node/ws-QNMQn5kg.cjs +0 -1
  95. package/src/http/__tests__/http-transport.test.ts +0 -55
  96. package/src/http/__tests__/routes.test.ts +0 -168
  97. package/src/http/http-transport.ts +0 -41
  98. package/src/http/index.ts +0 -4
  99. package/src/http/routes.ts +0 -152
  100. package/src/mcp/__tests__/mcp-server.test.ts +0 -66
  101. package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
  102. package/src/mcp/index.ts +0 -4
  103. package/src/mcp/mcp-server.ts +0 -163
  104. package/src/mcp/mcp-transport.ts +0 -48
  105. package/src/testing/__tests__/scripted-provider.test.ts +0 -73
  106. package/src/testing/scripted-provider.ts +0 -73
  107. package/src/tui/App.tsx +0 -491
  108. package/src/tui/BackgroundTaskPanel.tsx +0 -36
  109. package/src/tui/CjkTextInput.tsx +0 -199
  110. package/src/tui/ConfirmPrompt.tsx +0 -70
  111. package/src/tui/ContextWarningBanner.tsx +0 -34
  112. package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
  113. package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
  114. package/src/tui/InputArea.tsx +0 -310
  115. package/src/tui/InteractivePrompt.tsx +0 -59
  116. package/src/tui/ListPicker.tsx +0 -95
  117. package/src/tui/MenuSelect.tsx +0 -104
  118. package/src/tui/MessageList.tsx +0 -284
  119. package/src/tui/PermissionPrompt.tsx +0 -86
  120. package/src/tui/PluginTUI.tsx +0 -258
  121. package/src/tui/SessionPicker.tsx +0 -68
  122. package/src/tui/SessionStatusBar.tsx +0 -73
  123. package/src/tui/SlashAutocomplete.tsx +0 -110
  124. package/src/tui/StatusBar.tsx +0 -236
  125. package/src/tui/StreamingIndicator.tsx +0 -93
  126. package/src/tui/TextPrompt.tsx +0 -81
  127. package/src/tui/ToolCommandOutput.tsx +0 -39
  128. package/src/tui/ToolDiffBlock.tsx +0 -32
  129. package/src/tui/TransportTUI.tsx +0 -117
  130. package/src/tui/TuiInteractionChannel.ts +0 -495
  131. package/src/tui/UpdateNotice.tsx +0 -14
  132. package/src/tui/UsageSummaryEntry.tsx +0 -39
  133. package/src/tui/WaveText.tsx +0 -44
  134. package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
  135. package/src/tui/__tests__/ListPicker.test.tsx +0 -159
  136. package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
  137. package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
  138. package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
  139. package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
  140. package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
  141. package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
  142. package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
  143. package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
  144. package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
  145. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
  146. package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
  147. package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
  148. package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
  149. package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
  150. package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
  151. package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
  152. package/src/tui/__tests__/command-output-summary.test.ts +0 -95
  153. package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
  154. package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
  155. package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
  156. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
  157. package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
  158. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
  159. package/src/tui/__tests__/input-area-flow.test.ts +0 -164
  160. package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
  161. package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
  162. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
  163. package/src/tui/__tests__/pty/pty-driver.ts +0 -135
  164. package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
  165. package/src/tui/__tests__/render-channel-options.test.ts +0 -32
  166. package/src/tui/__tests__/render-markdown.test.ts +0 -72
  167. package/src/tui/__tests__/selection-flow.test.ts +0 -61
  168. package/src/tui/__tests__/session-init-poller.test.ts +0 -102
  169. package/src/tui/__tests__/session-naming.test.ts +0 -64
  170. package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
  171. package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
  172. package/src/tui/__tests__/status-activity.test.ts +0 -71
  173. package/src/tui/__tests__/status-bar.test.tsx +0 -177
  174. package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
  175. package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
  176. package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
  177. package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
  178. package/src/tui/background-task-row-format.ts +0 -53
  179. package/src/tui/command-interaction.ts +0 -9
  180. package/src/tui/command-output-summary.ts +0 -122
  181. package/src/tui/create-default-tui-cli-adapter.ts +0 -41
  182. package/src/tui/execution-workspace-view-model.ts +0 -123
  183. package/src/tui/flows/cjk-text-input-flow.ts +0 -285
  184. package/src/tui/flows/confirm-prompt-flow.ts +0 -45
  185. package/src/tui/flows/input-area-flow.ts +0 -189
  186. package/src/tui/flows/permission-prompt-flow.ts +0 -85
  187. package/src/tui/flows/selection-flow.ts +0 -126
  188. package/src/tui/flows/session-init-poller.ts +0 -77
  189. package/src/tui/flows/text-prompt-flow.ts +0 -98
  190. package/src/tui/hooks/command-effect-handler.ts +0 -97
  191. package/src/tui/hooks/command-effect-queue.ts +0 -39
  192. package/src/tui/hooks/side-effects-types.ts +0 -35
  193. package/src/tui/hooks/useAutocomplete.ts +0 -87
  194. package/src/tui/hooks/usePluginCallbacks.ts +0 -31
  195. package/src/tui/hooks/usePluginScreenData.ts +0 -85
  196. package/src/tui/hooks/useSideEffects.ts +0 -175
  197. package/src/tui/hooks/useSlashRouting.ts +0 -118
  198. package/src/tui/hooks/useStatusLineSettings.ts +0 -37
  199. package/src/tui/hooks/useTuiChannel.ts +0 -95
  200. package/src/tui/index.ts +0 -14
  201. package/src/tui/interactions/CommandConfirm.tsx +0 -36
  202. package/src/tui/interactions/CommandPicker.tsx +0 -77
  203. package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
  204. package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
  205. package/src/tui/plugin-tui-handlers.ts +0 -163
  206. package/src/tui/render-markdown.ts +0 -130
  207. package/src/tui/render.tsx +0 -129
  208. package/src/tui/session-naming.ts +0 -33
  209. package/src/tui/status-activity.ts +0 -63
  210. package/src/tui/tui-cli-adapter-context.tsx +0 -13
  211. package/src/tui/tui-cli-adapter.ts +0 -25
  212. package/src/tui/tui-state-manager.ts +0 -226
  213. package/src/tui/tui-transport.ts +0 -35
  214. package/src/tui/types.ts +0 -15
  215. package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
  216. package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
  217. package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
  218. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
  219. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
  220. package/src/tui/utils/edit-diff.ts +0 -153
  221. package/src/tui/utils/paste-labels.ts +0 -9
  222. package/src/tui/utils/tool-call-extractor.ts +0 -92
  223. package/src/tui/utils/tool-diff-summary.ts +0 -75
  224. package/src/ws/__tests__/ws-handler.test.ts +0 -409
  225. package/src/ws/__tests__/ws-transport.test.ts +0 -53
  226. package/src/ws/index.ts +0 -13
  227. package/src/ws/ws-background-messages.ts +0 -170
  228. package/src/ws/ws-handler.ts +0 -280
  229. package/src/ws/ws-protocol.ts +0 -78
  230. package/src/ws/ws-transport-configurable.ts +0 -128
  231. package/src/ws/ws-transport.ts +0 -42
@@ -1,426 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import { generateDiffLines, generateDiffLinesWithContext, extractEditDiff } from '../edit-diff.js';
6
-
7
- describe('generateDiffLines', () => {
8
- it('single line change: 1 remove + 1 add', () => {
9
- const lines = generateDiffLines('hello', 'world');
10
- expect(lines).toEqual([
11
- { type: 'remove', text: 'hello', lineNumber: 1 },
12
- { type: 'add', text: 'world', lineNumber: 1 },
13
- ]);
14
- });
15
-
16
- it('multi-line change: each old line is remove, each new line is add', () => {
17
- const lines = generateDiffLines('line1\nline2\nline3', 'lineA\nlineB');
18
- expect(lines.filter((l) => l.type === 'remove')).toHaveLength(3);
19
- expect(lines.filter((l) => l.type === 'add')).toHaveLength(2);
20
- expect(lines[0]).toEqual({ type: 'remove', text: 'line1', lineNumber: 1 });
21
- expect(lines[1]).toEqual({ type: 'remove', text: 'line2', lineNumber: 2 });
22
- expect(lines[2]).toEqual({ type: 'remove', text: 'line3', lineNumber: 3 });
23
- expect(lines[3]).toEqual({ type: 'add', text: 'lineA', lineNumber: 1 });
24
- expect(lines[4]).toEqual({ type: 'add', text: 'lineB', lineNumber: 2 });
25
- });
26
-
27
- it('identical strings return empty array', () => {
28
- expect(generateDiffLines('same', 'same')).toEqual([]);
29
- });
30
-
31
- it('empty old string (new content) returns only add lines', () => {
32
- const lines = generateDiffLines('', 'new line');
33
- expect(lines.filter((l) => l.type === 'remove')).toHaveLength(1); // '' splits to ['']
34
- expect(lines.filter((l) => l.type === 'add')).toHaveLength(1);
35
- });
36
-
37
- it('empty new string (deletion) returns only remove lines', () => {
38
- const lines = generateDiffLines('old line', '');
39
- expect(lines.filter((l) => l.type === 'remove')).toHaveLength(1);
40
- expect(lines.filter((l) => l.type === 'add')).toHaveLength(1); // '' splits to ['']
41
- });
42
-
43
- it('marks ALL old lines as remove and ALL new lines as add (no smart diff)', () => {
44
- // Even when some lines are the same, the function does not detect unchanged lines
45
- const lines = generateDiffLines('shared\nold', 'shared\nnew');
46
- expect(lines).toEqual([
47
- { type: 'remove', text: 'shared', lineNumber: 1 },
48
- { type: 'remove', text: 'old', lineNumber: 2 },
49
- { type: 'add', text: 'shared', lineNumber: 1 },
50
- { type: 'add', text: 'new', lineNumber: 2 },
51
- ]);
52
- });
53
-
54
- it('default startLine is 1', () => {
55
- const lines = generateDiffLines('a', 'b');
56
- expect(lines[0]).toEqual({ type: 'remove', text: 'a', lineNumber: 1 });
57
- expect(lines[1]).toEqual({ type: 'add', text: 'b', lineNumber: 1 });
58
- });
59
-
60
- it('custom startLine produces correct line numbers', () => {
61
- const lines = generateDiffLines('a', 'b', 42);
62
- expect(lines[0]).toEqual({ type: 'remove', text: 'a', lineNumber: 42 });
63
- expect(lines[1]).toEqual({ type: 'add', text: 'b', lineNumber: 42 });
64
- });
65
-
66
- it('multi-line with startLine: remove lines get startLine, startLine+1, etc.', () => {
67
- const lines = generateDiffLines('line1\nline2\nline3', 'lineA\nlineB', 10);
68
- expect(lines[0]).toEqual({ type: 'remove', text: 'line1', lineNumber: 10 });
69
- expect(lines[1]).toEqual({ type: 'remove', text: 'line2', lineNumber: 11 });
70
- expect(lines[2]).toEqual({ type: 'remove', text: 'line3', lineNumber: 12 });
71
- expect(lines[3]).toEqual({ type: 'add', text: 'lineA', lineNumber: 10 });
72
- expect(lines[4]).toEqual({ type: 'add', text: 'lineB', lineNumber: 11 });
73
- });
74
- });
75
-
76
- describe('extractEditDiff', () => {
77
- it('Edit tool with valid args returns file and lines', () => {
78
- const result = extractEditDiff('Edit', {
79
- file_path: '/src/index.ts',
80
- old_string: 'hello',
81
- new_string: 'world',
82
- });
83
- expect(result).not.toBeNull();
84
- expect(result!.file).toBe('/src/index.ts');
85
- expect(result!.lines).toEqual([
86
- { type: 'remove', text: 'hello', lineNumber: 1 },
87
- { type: 'add', text: 'world', lineNumber: 1 },
88
- ]);
89
- });
90
-
91
- it('non-Edit tool returns null', () => {
92
- expect(
93
- extractEditDiff('Read', {
94
- file_path: '/src/index.ts',
95
- old_string: 'a',
96
- new_string: 'b',
97
- }),
98
- ).toBeNull();
99
- });
100
-
101
- it('missing toolArgs returns null', () => {
102
- expect(extractEditDiff('Edit')).toBeNull();
103
- expect(extractEditDiff('Edit', undefined)).toBeNull();
104
- });
105
-
106
- it('missing file_path returns null', () => {
107
- expect(
108
- extractEditDiff('Edit', {
109
- old_string: 'a',
110
- new_string: 'b',
111
- }),
112
- ).toBeNull();
113
- });
114
-
115
- it('missing old_string returns null', () => {
116
- expect(
117
- extractEditDiff('Edit', {
118
- file_path: '/src/index.ts',
119
- new_string: 'b',
120
- }),
121
- ).toBeNull();
122
- });
123
-
124
- it('missing new_string returns null', () => {
125
- expect(
126
- extractEditDiff('Edit', {
127
- file_path: '/src/index.ts',
128
- old_string: 'a',
129
- }),
130
- ).toBeNull();
131
- });
132
-
133
- it('identical old_string and new_string returns null', () => {
134
- expect(
135
- extractEditDiff('Edit', {
136
- file_path: '/src/index.ts',
137
- old_string: 'same',
138
- new_string: 'same',
139
- }),
140
- ).toBeNull();
141
- });
142
-
143
- it('handles camelCase field names (filePath, oldString, newString)', () => {
144
- const result = extractEditDiff('Edit', {
145
- filePath: '/src/index.ts',
146
- oldString: 'old',
147
- newString: 'new',
148
- });
149
- expect(result).not.toBeNull();
150
- expect(result!.file).toBe('/src/index.ts');
151
- expect(result!.lines).toHaveLength(2);
152
- });
153
-
154
- it('handles snake_case field names (file_path, old_string, new_string)', () => {
155
- const result = extractEditDiff('Edit', {
156
- file_path: '/src/main.ts',
157
- old_string: 'foo',
158
- new_string: 'bar',
159
- });
160
- expect(result).not.toBeNull();
161
- expect(result!.file).toBe('/src/main.ts');
162
- });
163
-
164
- it('prefers snake_case over camelCase when both present', () => {
165
- const result = extractEditDiff('Edit', {
166
- file_path: '/snake.ts',
167
- filePath: '/camel.ts',
168
- old_string: 'a',
169
- oldString: 'x',
170
- new_string: 'b',
171
- newString: 'y',
172
- });
173
- expect(result).not.toBeNull();
174
- expect(result!.file).toBe('/snake.ts');
175
- });
176
-
177
- it('accepts optional startLine parameter', () => {
178
- const result = extractEditDiff(
179
- 'Edit',
180
- {
181
- file_path: '/src/index.ts',
182
- old_string: 'hello',
183
- new_string: 'world',
184
- },
185
- 5,
186
- );
187
- expect(result).not.toBeNull();
188
- expect(result!.lines).toEqual([
189
- { type: 'remove', text: 'hello', lineNumber: 5 },
190
- { type: 'add', text: 'world', lineNumber: 5 },
191
- ]);
192
- });
193
-
194
- it('defaults startLine to 1 when not provided', () => {
195
- const result = extractEditDiff('Edit', {
196
- file_path: '/src/index.ts',
197
- old_string: 'a',
198
- new_string: 'b',
199
- });
200
- expect(result).not.toBeNull();
201
- expect(result!.lines[0].lineNumber).toBe(1);
202
- expect(result!.lines[1].lineNumber).toBe(1);
203
- });
204
- });
205
-
206
- // ─── Regression tests: context lines, absolute line numbers, startLine resolution ───
207
-
208
- describe('generateDiffLinesWithContext', () => {
209
- let tmpDir: string;
210
-
211
- afterEach(() => {
212
- if (tmpDir) {
213
- rmSync(tmpDir, { recursive: true, force: true });
214
- }
215
- });
216
-
217
- function makeTempFile(content: string): string {
218
- tmpDir = mkdtempSync(join(tmpdir(), 'edit-diff-test-'));
219
- const filePath = join(tmpDir, 'test.ts');
220
- writeFileSync(filePath, content, 'utf-8');
221
- return filePath;
222
- }
223
-
224
- it('should include 3 context lines before the change', () => {
225
- // 10-line file, edit line 5 (replaced already in file)
226
- const fileLines = Array.from({ length: 10 }, (_, i) => `line${i + 1}`);
227
- // Simulate: line5 was replaced with lineNEW
228
- const modifiedLines = [...fileLines];
229
- modifiedLines[4] = 'lineNEW';
230
- const filePath = makeTempFile(modifiedLines.join('\n'));
231
-
232
- const result = generateDiffLinesWithContext('line5', 'lineNEW', 5, filePath);
233
-
234
- // Context before: line2, line3, line4
235
- const contextBefore = result.filter((l) => l.type === 'context' && l.lineNumber < 5);
236
- expect(contextBefore).toHaveLength(3);
237
- expect(contextBefore[0]).toEqual({ type: 'context', text: 'line2', lineNumber: 2 });
238
- expect(contextBefore[1]).toEqual({ type: 'context', text: 'line3', lineNumber: 3 });
239
- expect(contextBefore[2]).toEqual({ type: 'context', text: 'line4', lineNumber: 4 });
240
- });
241
-
242
- it('should include 3 context lines after the change', () => {
243
- const fileLines = Array.from({ length: 10 }, (_, i) => `line${i + 1}`);
244
- const modifiedLines = [...fileLines];
245
- modifiedLines[4] = 'lineNEW';
246
- const filePath = makeTempFile(modifiedLines.join('\n'));
247
-
248
- const result = generateDiffLinesWithContext('line5', 'lineNEW', 5, filePath);
249
-
250
- // Context after: line6, line7, line8
251
- const contextAfter = result.filter((l) => l.type === 'context' && l.lineNumber > 5);
252
- expect(contextAfter).toHaveLength(3);
253
- expect(contextAfter[0]).toEqual({ type: 'context', text: 'line6', lineNumber: 6 });
254
- expect(contextAfter[1]).toEqual({ type: 'context', text: 'line7', lineNumber: 7 });
255
- expect(contextAfter[2]).toEqual({ type: 'context', text: 'line8', lineNumber: 8 });
256
- });
257
-
258
- it('should handle edit at start of file (no lines before)', () => {
259
- const fileLines = Array.from({ length: 5 }, (_, i) => `line${i + 1}`);
260
- const modifiedLines = [...fileLines];
261
- modifiedLines[0] = 'lineNEW';
262
- const filePath = makeTempFile(modifiedLines.join('\n'));
263
-
264
- const result = generateDiffLinesWithContext('line1', 'lineNEW', 1, filePath);
265
-
266
- const contextBefore = result.filter((l) => l.type === 'context' && l.lineNumber < 1);
267
- expect(contextBefore).toHaveLength(0);
268
-
269
- // Context after: line2, line3, line4
270
- const contextAfter = result.filter((l) => l.type === 'context' && l.lineNumber > 1);
271
- expect(contextAfter).toHaveLength(3);
272
- expect(contextAfter[0]).toEqual({ type: 'context', text: 'line2', lineNumber: 2 });
273
- expect(contextAfter[1]).toEqual({ type: 'context', text: 'line3', lineNumber: 3 });
274
- expect(contextAfter[2]).toEqual({ type: 'context', text: 'line4', lineNumber: 4 });
275
- });
276
-
277
- it('should handle edit at end of file (no lines after)', () => {
278
- const fileLines = Array.from({ length: 5 }, (_, i) => `line${i + 1}`);
279
- const modifiedLines = [...fileLines];
280
- modifiedLines[4] = 'lineNEW';
281
- const filePath = makeTempFile(modifiedLines.join('\n'));
282
-
283
- const result = generateDiffLinesWithContext('line5', 'lineNEW', 5, filePath);
284
-
285
- // Context before: line2, line3, line4
286
- const contextBefore = result.filter((l) => l.type === 'context' && l.lineNumber < 5);
287
- expect(contextBefore).toHaveLength(3);
288
-
289
- // Context after: none (line5 is the last line)
290
- const contextAfter = result.filter((l) => l.type === 'context' && l.lineNumber > 5);
291
- expect(contextAfter).toHaveLength(0);
292
- });
293
-
294
- it('context lines should have type "context"', () => {
295
- const fileLines = Array.from({ length: 10 }, (_, i) => `line${i + 1}`);
296
- const modifiedLines = [...fileLines];
297
- modifiedLines[4] = 'lineNEW';
298
- const filePath = makeTempFile(modifiedLines.join('\n'));
299
-
300
- const result = generateDiffLinesWithContext('line5', 'lineNEW', 5, filePath);
301
-
302
- const contextLines = result.filter((l) => l.type === 'context');
303
- expect(contextLines.length).toBeGreaterThan(0);
304
- for (const line of contextLines) {
305
- expect(line.type).toBe('context');
306
- }
307
- });
308
-
309
- it('starts readable context diffs with a hunk header', () => {
310
- const fileLines = Array.from({ length: 10 }, (_, i) => `line${i + 1}`);
311
- const modifiedLines = [...fileLines];
312
- modifiedLines[4] = 'lineNEW';
313
- const filePath = makeTempFile(modifiedLines.join('\n'));
314
-
315
- const result = generateDiffLinesWithContext('line5', 'lineNEW', 5, filePath);
316
-
317
- expect(result[0]).toEqual({
318
- type: 'hunk',
319
- text: '@@ -2,7 +2,7 @@',
320
- lineNumber: 2,
321
- });
322
- });
323
-
324
- it('all lines should have absolute lineNumber', () => {
325
- const fileLines = Array.from({ length: 10 }, (_, i) => `line${i + 1}`);
326
- const modifiedLines = [...fileLines];
327
- modifiedLines[4] = 'lineNEW';
328
- const filePath = makeTempFile(modifiedLines.join('\n'));
329
-
330
- const result = generateDiffLinesWithContext('line5', 'lineNEW', 5, filePath);
331
-
332
- for (const line of result) {
333
- expect(typeof line.lineNumber).toBe('number');
334
- expect(line.lineNumber).toBeGreaterThan(0);
335
- }
336
- });
337
-
338
- it('returns diff without context when file is not readable', () => {
339
- const result = generateDiffLinesWithContext('old', 'new', 5, '/nonexistent/path/file.ts');
340
-
341
- // Should still return diff lines, just no context
342
- expect(result.length).toBeGreaterThan(0);
343
- const contextLines = result.filter((l) => l.type === 'context');
344
- expect(contextLines).toHaveLength(0);
345
- });
346
-
347
- it('identical strings return empty array', () => {
348
- const filePath = makeTempFile('some content');
349
- const result = generateDiffLinesWithContext('same', 'same', 1, filePath);
350
- expect(result).toEqual([]);
351
- });
352
- });
353
-
354
- describe('extractEditDiff startLine resolution', () => {
355
- let tmpDir: string;
356
-
357
- afterEach(() => {
358
- if (tmpDir) {
359
- rmSync(tmpDir, { recursive: true, force: true });
360
- }
361
- });
362
-
363
- function makeTempFile(content: string): string {
364
- tmpDir = mkdtempSync(join(tmpdir(), 'edit-diff-test-'));
365
- const filePath = join(tmpDir, 'test.ts');
366
- writeFileSync(filePath, content, 'utf-8');
367
- return filePath;
368
- }
369
-
370
- it('should use provided startLine', () => {
371
- const result = extractEditDiff(
372
- 'Edit',
373
- {
374
- file_path: '/nonexistent/file.ts',
375
- old_string: 'hello',
376
- new_string: 'world',
377
- },
378
- 42,
379
- );
380
- expect(result).not.toBeNull();
381
- const removeLines = result!.lines.filter((l) => l.type === 'remove');
382
- expect(removeLines[0].lineNumber).toBe(42);
383
- });
384
-
385
- it('should resolve startLine from file when not provided', () => {
386
- // Create file where newStr appears at line 4
387
- const content = 'line1\nline2\nline3\nREPLACED\nline5';
388
- const filePath = makeTempFile(content);
389
-
390
- const result = extractEditDiff('Edit', {
391
- file_path: filePath,
392
- old_string: 'original',
393
- new_string: 'REPLACED',
394
- });
395
-
396
- expect(result).not.toBeNull();
397
- // newStr "REPLACED" is at line 4 in the file
398
- const removeLines = result!.lines.filter((l) => l.type === 'remove');
399
- expect(removeLines[0].lineNumber).toBe(4);
400
- const addLines = result!.lines.filter((l) => l.type === 'add');
401
- expect(addLines[0].lineNumber).toBe(4);
402
- });
403
-
404
- it('should fall back to line 1 when file not readable', () => {
405
- const result = extractEditDiff('Edit', {
406
- file_path: '/nonexistent/path/file.ts',
407
- old_string: 'a',
408
- new_string: 'b',
409
- });
410
- expect(result).not.toBeNull();
411
- expect(result!.lines[0].lineNumber).toBe(1);
412
- });
413
-
414
- it('should fall back to line 1 when newString not found in file', () => {
415
- const content = 'line1\nline2\nline3';
416
- const filePath = makeTempFile(content);
417
-
418
- const result = extractEditDiff('Edit', {
419
- file_path: filePath,
420
- old_string: 'something',
421
- new_string: 'NOT_IN_FILE_ANYWHERE',
422
- });
423
- expect(result).not.toBeNull();
424
- expect(result!.lines[0].lineNumber).toBe(1);
425
- });
426
- });
@@ -1,116 +0,0 @@
1
- /**
2
- * Tests for paste detection and the full paste → label → expand pipeline.
3
- */
4
-
5
- import { describe, it, expect } from 'vitest';
6
- import { filterPrintable } from '../../flows/cjk-text-input-flow.js';
7
- import { expandPasteLabels } from '../paste-labels.js';
8
-
9
- describe('filterPrintable and newlines', () => {
10
- it('should strip newlines from input (they are control chars)', () => {
11
- expect(filterPrintable('line1\nline2')).toBe('line1line2');
12
- });
13
-
14
- it('should strip carriage returns', () => {
15
- expect(filterPrintable('line1\r\nline2')).toBe('line1line2');
16
- });
17
-
18
- it('should strip tabs', () => {
19
- expect(filterPrintable('a\tb')).toBe('ab');
20
- });
21
-
22
- it('should preserve normal text', () => {
23
- expect(filterPrintable('hello world')).toBe('hello world');
24
- });
25
- });
26
-
27
- describe('Full paste pipeline: detect → label → expand', () => {
28
- it('should correctly round-trip multiline paste content', () => {
29
- const pastedText = 'const a = 1;\nconst b = 2;\nconst c = 3;';
30
- const store = new Map<number, string>();
31
-
32
- // Simulate paste detection
33
- expect(pastedText.length > 1 && pastedText.includes('\n')).toBe(true);
34
-
35
- // Simulate store + label creation (as InputArea.handlePaste does)
36
- const id = 1;
37
- store.set(id, pastedText);
38
- const lineCount = pastedText.split('\n').length;
39
- const label = `[Pasted text #${id} +${lineCount} lines]`;
40
- expect(label).toBe('[Pasted text #1 +3 lines]');
41
-
42
- // Simulate submit → expand
43
- const expanded = expandPasteLabels(label, store);
44
- expect(expanded).toBe(pastedText);
45
- });
46
-
47
- it('should handle paste mixed with typed text', () => {
48
- const pastedText = 'function hello() {\n return "world";\n}';
49
- const store = new Map<number, string>();
50
- store.set(1, pastedText);
51
-
52
- const userInput = 'Review this code: [Pasted text #1 +3 lines] and fix any bugs';
53
- const expanded = expandPasteLabels(userInput, store);
54
- expect(expanded).toBe(
55
- 'Review this code: function hello() {\n return "world";\n} and fix any bugs',
56
- );
57
- });
58
-
59
- it('should handle multiple pastes in one message', () => {
60
- const store = new Map<number, string>();
61
- store.set(1, 'first\npaste');
62
- store.set(2, 'second\npaste\nhere');
63
-
64
- const input = '[Pasted text #1 +2 lines] compare with [Pasted text #2 +3 lines]';
65
- const expanded = expandPasteLabels(input, store);
66
- expect(expanded).toBe('first\npaste compare with second\npaste\nhere');
67
- });
68
- });
69
-
70
- describe('Paste at cursor position', () => {
71
- it('label is inserted at cursor position, not end', () => {
72
- const existingText = 'hello world';
73
- const cursorPosition = 5; // between "hello" and " world"
74
- const pastedText = 'line1\nline2';
75
- const store = new Map<number, string>();
76
- const id = 1;
77
- store.set(id, pastedText);
78
- const lineCount = pastedText.split('\n').length;
79
- const label = `[Pasted text #${id} +${lineCount} lines]`;
80
-
81
- // Simulate cursor-aware insertion (as handlePaste does)
82
- const result =
83
- existingText.slice(0, cursorPosition) + label + existingText.slice(cursorPosition);
84
- expect(result).toBe('hello[Pasted text #1 +2 lines] world');
85
-
86
- // Expand should restore original text at correct position
87
- const expanded = expandPasteLabels(result, store);
88
- expect(expanded).toBe('helloline1\nline2 world');
89
- });
90
-
91
- it('cursor hint equals cursorPosition + label.length', () => {
92
- const cursorPosition = 5;
93
- const label = '[Pasted text #1 +3 lines]';
94
- const newCursorPos = cursorPosition + label.length;
95
- expect(newCursorPos).toBe(5 + 25); // label is 25 chars
96
- expect(newCursorPos).toBe(30);
97
- });
98
-
99
- it('paste at start (cursor = 0)', () => {
100
- const existingText = 'existing';
101
- const cursorPosition = 0;
102
- const label = '[Pasted text #1 +2 lines]';
103
- const result =
104
- existingText.slice(0, cursorPosition) + label + existingText.slice(cursorPosition);
105
- expect(result).toBe('[Pasted text #1 +2 lines]existing');
106
- });
107
-
108
- it('paste at end (cursor = text.length)', () => {
109
- const existingText = 'existing';
110
- const cursorPosition = existingText.length;
111
- const label = '[Pasted text #1 +2 lines]';
112
- const result =
113
- existingText.slice(0, cursorPosition) + label + existingText.slice(cursorPosition);
114
- expect(result).toBe('existing[Pasted text #1 +2 lines]');
115
- });
116
- });
@@ -1,46 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { expandPasteLabels } from '../paste-labels.js';
3
-
4
- describe('expandPasteLabels', () => {
5
- it('should expand single paste label', () => {
6
- const store = new Map([[1, 'line1\nline2\nline3']]);
7
- const result = expandPasteLabels('[Pasted text #1 +3 lines]', store);
8
- expect(result).toBe('line1\nline2\nline3');
9
- });
10
-
11
- it('should expand multiple paste labels', () => {
12
- const store = new Map([
13
- [1, 'first paste'],
14
- [2, 'second paste'],
15
- ]);
16
- const result = expandPasteLabels(
17
- 'before [Pasted text #1 +1 lines] middle [Pasted text #2 +1 lines] after',
18
- store,
19
- );
20
- expect(result).toBe('before first paste middle second paste after');
21
- });
22
-
23
- it('should return empty string for missing store entry', () => {
24
- const store = new Map<number, string>();
25
- const result = expandPasteLabels('[Pasted text #1 +3 lines]', store);
26
- expect(result).toBe('');
27
- });
28
-
29
- it('should preserve text without paste labels', () => {
30
- const store = new Map<number, string>();
31
- const result = expandPasteLabels('regular text without labels', store);
32
- expect(result).toBe('regular text without labels');
33
- });
34
-
35
- it('should handle label with different line counts', () => {
36
- const store = new Map([[1, 'a\nb']]);
37
- expect(expandPasteLabels('[Pasted text #1 +2 lines]', store)).toBe('a\nb');
38
- expect(expandPasteLabels('[Pasted text #1 +99 lines]', store)).toBe('a\nb');
39
- });
40
-
41
- it('should expand label without line count (debounce pending)', () => {
42
- const store = new Map([[1, 'partial paste']]);
43
- const result = expandPasteLabels('[Pasted text #1]', store);
44
- expect(result).toBe('partial paste');
45
- });
46
- });