1+ import { describe , beforeEach , test , expect , jest } from '@jest/globals' ;
2+ import React from 'react' ;
3+ import { renderTestApp } from '../utils/renderApp.js' ;
4+ import {
5+ resetTestData ,
6+ setupBasicProject ,
7+ setupTestWorktree ,
8+ memoryStore ,
9+ } from '../utils/testHelpers.js' ;
10+ import { FakeTmuxService } from '../fakes/FakeTmuxService.js' ;
11+ import { CommentStore } from '../../src/models.js' ;
12+ import { commentStoreManager } from '../../src/services/CommentStoreManager.js' ;
13+ import * as commandExecutor from '../../src/shared/utils/commandExecutor.js' ;
14+
15+ const h = React . createElement ;
16+
17+ describe ( 'Comment Send to Claude E2E' , ( ) => {
18+ let fakeTmuxService : FakeTmuxService ;
19+
20+ beforeEach ( ( ) => {
21+ resetTestData ( ) ;
22+ fakeTmuxService = new FakeTmuxService ( ) ;
23+
24+ // Mock runCommand to capture tmux send-keys calls
25+ jest . spyOn ( commandExecutor , 'runCommand' ) . mockImplementation ( ( args , opts ) => {
26+ if ( args [ 0 ] === 'tmux' && args [ 1 ] === 'send-keys' ) {
27+ // Extract session and keys from args
28+ const sessionIndex = args . findIndex ( arg => arg === '-t' ) + 1 ;
29+ const session = args [ sessionIndex ] ?. split ( ':' ) [ 0 ] || '' ;
30+ const keys = args . slice ( sessionIndex + 1 ) ;
31+
32+ fakeTmuxService . recordSentKeys ( session , keys ) ;
33+ return 'mocked send-keys' ;
34+ }
35+
36+ // Mock other commands as needed
37+ if ( args . includes ( 'command' ) && args . includes ( 'claude' ) ) {
38+ return 'claude' ; // Claude is available
39+ }
40+
41+ if ( args [ 0 ] === 'tmux' && args [ 1 ] === 'new-session' ) {
42+ return 'session created' ;
43+ }
44+
45+ if ( args [ 0 ] === 'tmux' && args [ 1 ] === 'has-session' ) {
46+ return '' ; // Session doesn't exist initially
47+ }
48+
49+ return '' ;
50+ } ) ;
51+ } ) ;
52+
53+ afterEach ( ( ) => {
54+ jest . restoreAllMocks ( ) ;
55+ } ) ;
56+
57+ test ( 'should send all comments including the last one with proper newlines' , async ( ) => {
58+ // Setup: Create a project with a worktree
59+ setupBasicProject ( 'test-project' ) ;
60+ const worktree = setupTestWorktree ( 'test-project' , 'feature-branch' ) ;
61+ const worktreePath = worktree . path ;
62+
63+ // Get comment store for this worktree
64+ const commentStore = commentStoreManager . getStore ( worktreePath ) ;
65+
66+ // Add multiple comments
67+ commentStore . addComment ( 10 , 'file1.ts' , 'const x = 1;' , 'First comment' ) ;
68+ commentStore . addComment ( 20 , 'file2.ts' , 'const y = 2;' , 'Second comment' ) ;
69+ commentStore . addComment ( 30 , 'file3.ts' , 'const z = 3;' , 'Last comment' ) ;
70+
71+ expect ( commentStore . count ) . toBe ( 3 ) ;
72+
73+ // Clear any previous sent keys
74+ fakeTmuxService . clearSentKeys ( ) ;
75+
76+ // Simulate the sendCommentsToTmux function logic
77+ const comments = commentStore . getAllComments ( ) ;
78+ const sessionName = `dev-test-project-feature-branch` ;
79+
80+ // Create the message format (mimicking DiffView.ts logic)
81+ const messageLines : string [ ] = [ ] ;
82+ messageLines . push ( "Please address the following code review comments:" ) ;
83+ messageLines . push ( "" ) ;
84+
85+ const commentsByFile : { [ key : string ] : typeof comments } = { } ;
86+ comments . forEach ( comment => {
87+ if ( ! commentsByFile [ comment . fileName ] ) {
88+ commentsByFile [ comment . fileName ] = [ ] ;
89+ }
90+ commentsByFile [ comment . fileName ] . push ( comment ) ;
91+ } ) ;
92+
93+ Object . entries ( commentsByFile ) . forEach ( ( [ fileName , fileComments ] ) => {
94+ messageLines . push ( `**${ fileName } :**` ) ;
95+ fileComments . forEach ( comment => {
96+ messageLines . push ( `- Line ${ comment . lineIndex } : ${ comment . commentText } ` ) ;
97+ messageLines . push ( ` \`${ comment . lineText } \`` ) ;
98+ } ) ;
99+ messageLines . push ( "" ) ;
100+ } ) ;
101+
102+ // Mock creating session (similar to DiffView.ts)
103+ const sessionExists = commandExecutor . runCommand ( [ 'tmux' , 'has-session' , '-t' , sessionName ] ) . trim ( ) ;
104+ if ( ! sessionExists ) {
105+ commandExecutor . runCommand ( [ 'tmux' , 'new-session' , '-ds' , sessionName , '-c' , worktreePath ] ) ;
106+ const hasClaude = commandExecutor . runCommand ( [ 'bash' , '-lc' , 'command -v claude || true' ] ) . trim ( ) ;
107+ if ( hasClaude ) {
108+ commandExecutor . runCommand ( [ 'tmux' , 'send-keys' , '-t' , `${ sessionName } :0.0` , 'claude' , 'C-m' ] ) ;
109+ }
110+ }
111+
112+ // Send all lines with Alt+Enter (using the fixed logic)
113+ messageLines . forEach ( ( line , index ) => {
114+ // Send the line text
115+ commandExecutor . runCommand ( [ 'tmux' , 'send-keys' , '-t' , `${ sessionName } :0.0` , line ] ) ;
116+
117+ // FIXED: Send newline after every line including the last one
118+ commandExecutor . runCommand ( [ 'tmux' , 'send-keys' , '-t' , `${ sessionName } :0.0` , 'Escape' , 'Enter' ] ) ;
119+ } ) ;
120+
121+ // Verify the sent keys
122+ const sentKeys = fakeTmuxService . getSentKeys ( sessionName ) ;
123+
124+ // Should have sent:
125+ // 1. 'claude', 'C-m' (start Claude)
126+ // 2-N. Each message line
127+ // 2-N. 'Escape', 'Enter' after each line except the last
128+
129+ expect ( sentKeys . length ) . toBeGreaterThan ( 0 ) ;
130+
131+ // Find the message lines in sent keys (skip the initial 'claude', 'C-m')
132+ const messageStartIndex = sentKeys . findIndex ( keys =>
133+ keys . length === 1 && keys [ 0 ] === "Please address the following code review comments:"
134+ ) ;
135+ expect ( messageStartIndex ) . toBeGreaterThan ( - 1 ) ;
136+
137+ // Count message lines and newlines
138+ let messageLineCount = 0 ;
139+ let newlineCount = 0 ;
140+
141+ for ( let i = messageStartIndex ; i < sentKeys . length ; i ++ ) {
142+ const keys = sentKeys [ i ] ;
143+ if ( keys . length === 1 && ! keys . includes ( 'Escape' ) && ! keys . includes ( 'Enter' ) ) {
144+ messageLineCount ++ ;
145+ } else if ( keys . length === 2 && keys [ 0 ] === 'Escape' && keys [ 1 ] === 'Enter' ) {
146+ newlineCount ++ ;
147+ }
148+ }
149+
150+ expect ( messageLineCount ) . toBe ( messageLines . length ) ;
151+
152+ // FIXED: Now all lines should get newlines, including the last one
153+ expect ( newlineCount ) . toBe ( messageLines . length ) ;
154+
155+ // Clean up
156+ commentStore . clear ( ) ;
157+ } ) ;
158+
159+ test ( 'should handle single comment correctly' , async ( ) => {
160+ // Setup: Create a project with a worktree
161+ setupBasicProject ( 'single-project' ) ;
162+ const worktree = setupTestWorktree ( 'single-project' , 'single-feature' ) ;
163+ const worktreePath = worktree . path ;
164+
165+ // Get comment store for this worktree
166+ const commentStore = commentStoreManager . getStore ( worktreePath ) ;
167+
168+ // Add single comment
169+ commentStore . addComment ( 5 , 'single.ts' , 'const single = true;' , 'Only comment' ) ;
170+
171+ expect ( commentStore . count ) . toBe ( 1 ) ;
172+
173+ // Clear any previous sent keys
174+ fakeTmuxService . clearSentKeys ( ) ;
175+
176+ const comments = commentStore . getAllComments ( ) ;
177+ const sessionName = `dev-single-project-single-feature` ;
178+
179+ // Create minimal message
180+ const messageLines = [
181+ "Please address the following code review comments:" ,
182+ "" ,
183+ "**single.ts:**" ,
184+ "- Line 5: Only comment" ,
185+ " `const single = true;`" ,
186+ ""
187+ ] ;
188+
189+ // Send each line with the fixed logic
190+ messageLines . forEach ( ( line , index ) => {
191+ commandExecutor . runCommand ( [ 'tmux' , 'send-keys' , '-t' , `${ sessionName } :0.0` , line ] ) ;
192+ // FIXED: Send newline after every line including the last one
193+ commandExecutor . runCommand ( [ 'tmux' , 'send-keys' , '-t' , `${ sessionName } :0.0` , 'Escape' , 'Enter' ] ) ;
194+ } ) ;
195+
196+ const sentKeys = fakeTmuxService . getSentKeys ( sessionName ) ;
197+
198+ // Count newlines - should now be messageLines.length with the fix
199+ const newlineCount = sentKeys . filter ( keys =>
200+ keys . length === 2 && keys [ 0 ] === 'Escape' && keys [ 1 ] === 'Enter'
201+ ) . length ;
202+
203+ // FIXED: Now all lines get newlines including the last one
204+ expect ( newlineCount ) . toBe ( messageLines . length ) ;
205+
206+ commentStore . clear ( ) ;
207+ } ) ;
208+ } ) ;
0 commit comments