difit 2.2.0 → 2.2.2

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 (31) hide show
  1. package/LICENSE +2 -2
  2. package/README.ja.md +4 -1
  3. package/README.ko.md +4 -1
  4. package/README.md +4 -1
  5. package/README.zh.md +4 -1
  6. package/dist/client/assets/index-9RijpjCf.js +220 -0
  7. package/dist/client/assets/index-DpUTViqw.css +1 -0
  8. package/dist/client/assets/{prism-csharp-B-CFRzGF.js → prism-csharp-BKbuORRj.js} +1 -1
  9. package/dist/client/assets/{prism-java-IqkDgVxE.js → prism-java-C2ppxXW3.js} +1 -1
  10. package/dist/client/assets/{prism-php-COdrRzRl.js → prism-php-JOWoQkNd.js} +1 -1
  11. package/dist/client/assets/{prism-ruby-ig7xCRi-.js → prism-ruby-C2sQ8Hg4.js} +1 -1
  12. package/dist/client/assets/{prism-solidity-lkGQYrB_.js → prism-solidity-BRuCGEwy.js} +1 -1
  13. package/dist/client/index.html +2 -2
  14. package/dist/server/git-diff.d.ts +4 -0
  15. package/dist/server/git-diff.js +131 -7
  16. package/dist/server/git-diff.test.js +318 -0
  17. package/dist/server/schemas/commentSchema.d.ts +22 -0
  18. package/dist/server/schemas/commentSchema.js +16 -0
  19. package/dist/server/schemas/commentSchema.test.d.ts +1 -0
  20. package/dist/server/schemas/commentSchema.test.js +229 -0
  21. package/dist/server/server.js +1 -13
  22. package/dist/server/server.test.js +103 -0
  23. package/dist/utils/commentFormatter.d.ts +26 -0
  24. package/dist/utils/commentFormatter.js +50 -0
  25. package/dist/utils/commentFormatting.d.ts +4 -0
  26. package/dist/utils/commentFormatting.js +24 -0
  27. package/dist/utils/commentFormatting.test.d.ts +1 -0
  28. package/dist/utils/commentFormatting.test.js +220 -0
  29. package/package.json +3 -4
  30. package/dist/client/assets/index-BG6tLkMt.js +0 -221
  31. package/dist/client/assets/index-Bz9yRRZ7.css +0 -1
@@ -114,8 +114,10 @@ describe('GitDiffParser', () => {
114
114
  'Binary files /dev/null and b/image.jpg differ',
115
115
  ];
116
116
  const summary = {
117
+ file: 'image.jpg',
117
118
  insertions: 0,
118
119
  deletions: 0,
120
+ binary: true,
119
121
  };
120
122
  // Access private method for testing
121
123
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
@@ -138,8 +140,10 @@ describe('GitDiffParser', () => {
138
140
  'Binary files a/old-image.png and /dev/null differ',
139
141
  ];
140
142
  const summary = {
143
+ file: 'old-image.png',
141
144
  insertions: 0,
142
145
  deletions: 0,
146
+ binary: true,
143
147
  };
144
148
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
145
149
  expect(result).toEqual({
@@ -160,8 +164,10 @@ describe('GitDiffParser', () => {
160
164
  'Binary files a/photo.jpg and b/photo.jpg differ',
161
165
  ];
162
166
  const summary = {
167
+ file: 'photo.jpg',
163
168
  insertions: 0,
164
169
  deletions: 0,
170
+ binary: true,
165
171
  };
166
172
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
167
173
  expect(result).toEqual({
@@ -181,8 +187,11 @@ describe('GitDiffParser', () => {
181
187
  'rename to new-name.gif',
182
188
  ];
183
189
  const summary = {
190
+ file: 'new-name.gif',
191
+ from: 'old-name.gif',
184
192
  insertions: 0,
185
193
  deletions: 0,
194
+ binary: true,
186
195
  };
187
196
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
188
197
  expect(result).toEqual({
@@ -206,8 +215,10 @@ describe('GitDiffParser', () => {
206
215
  ' // end',
207
216
  ];
208
217
  const summary = {
218
+ file: 'script.js',
209
219
  insertions: 1,
210
220
  deletions: 0,
221
+ binary: false,
211
222
  };
212
223
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
213
224
  expect(result).toEqual({
@@ -234,8 +245,10 @@ describe('GitDiffParser', () => {
234
245
  ' // end',
235
246
  ];
236
247
  const summary = {
248
+ file: 'script.js',
237
249
  insertions: 0,
238
250
  deletions: 1,
251
+ binary: false,
239
252
  };
240
253
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
241
254
  expect(result).toEqual({
@@ -258,8 +271,10 @@ describe('GitDiffParser', () => {
258
271
  '+line 2',
259
272
  ];
260
273
  const summary = {
274
+ file: 'new-file.txt',
261
275
  insertions: 2,
262
276
  deletions: 0,
277
+ binary: false,
263
278
  };
264
279
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
265
280
  expect(result.status).toBe('added');
@@ -275,13 +290,312 @@ describe('GitDiffParser', () => {
275
290
  '-line 2',
276
291
  ];
277
292
  const summary = {
293
+ file: 'deleted-file.txt',
278
294
  insertions: 0,
279
295
  deletions: 2,
296
+ binary: false,
280
297
  };
281
298
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
282
299
  expect(result.status).toBe('deleted');
283
300
  });
284
301
  });
302
+ describe('parseFileBlock with quoted paths', () => {
303
+ it('parses file paths with spaces correctly', () => {
304
+ const diffLines = [
305
+ 'diff --git "a/test with spaces/file name.txt" "b/test with spaces/file name.txt"',
306
+ 'new file mode 100644',
307
+ 'index 0000000..257cc56',
308
+ '--- /dev/null',
309
+ '+++ "b/test with spaces/file name.txt"',
310
+ '@@ -0,0 +1 @@',
311
+ '+foo',
312
+ ];
313
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
314
+ expect(result).toBeDefined();
315
+ expect(result.path).toBe('test with spaces/file name.txt');
316
+ expect(result.status).toBe('added');
317
+ expect(result.additions).toBe(1);
318
+ expect(result.deletions).toBe(0);
319
+ });
320
+ it('parses summary-provided filenames with escaped spaces', () => {
321
+ const diffLines = [
322
+ 'diff --git "a/templates/test file.py" "b/templates/test file.py"',
323
+ 'index abc123..def456 100644',
324
+ '--- "a/templates/test file.py"',
325
+ '+++ "b/templates/test file.py"',
326
+ '@@ -1 +1 @@',
327
+ '-old',
328
+ '+new',
329
+ ];
330
+ const summary = {
331
+ file: 'templates/test\\040file.py',
332
+ insertions: 1,
333
+ deletions: 1,
334
+ binary: false,
335
+ };
336
+ const result = parser.parseFileBlock(diffLines.join('\n'), summary);
337
+ expect(result?.path).toBe('templates/test file.py');
338
+ expect(result?.oldPath).toBeUndefined();
339
+ });
340
+ it('parses file paths with Jinja template brackets correctly', () => {
341
+ const diffLines = [
342
+ 'diff --git "a/templates/test_000_{{ package_name }}/__.py" "b/templates/test_000_{{ package_name }}/__.py"',
343
+ 'new file mode 100644',
344
+ 'index 0000000..257cc56',
345
+ '--- /dev/null',
346
+ '+++ "b/templates/test_000_{{ package_name }}/__.py"',
347
+ '@@ -0,0 +1 @@',
348
+ '+test',
349
+ ];
350
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
351
+ expect(result).toBeDefined();
352
+ expect(result.path).toBe('templates/test_000_{{ package_name }}/__.py');
353
+ expect(result.status).toBe('added');
354
+ });
355
+ it('parses file paths with escaped characters correctly', () => {
356
+ const diffLines = [
357
+ 'diff --git "a/file\\twith\\ttabs.txt" "b/file\\twith\\ttabs.txt"',
358
+ 'new file mode 100644',
359
+ 'index 0000000..257cc56',
360
+ '--- /dev/null',
361
+ '+++ "b/file\\twith\\ttabs.txt"',
362
+ '@@ -0,0 +1 @@',
363
+ '+content',
364
+ ];
365
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
366
+ expect(result).toBeDefined();
367
+ expect(result.path).toBe('file\twith\ttabs.txt');
368
+ expect(result.status).toBe('added');
369
+ });
370
+ it('parses renamed files with spaces correctly', () => {
371
+ const diffLines = [
372
+ 'diff --git "a/old folder/old name.txt" "b/new folder/new name.txt"',
373
+ 'similarity index 100%',
374
+ 'rename from old folder/old name.txt',
375
+ 'rename to new folder/new name.txt',
376
+ ];
377
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
378
+ expect(result).toBeDefined();
379
+ expect(result.path).toBe('new folder/new name.txt');
380
+ expect(result.oldPath).toBe('old folder/old name.txt');
381
+ expect(result.status).toBe('renamed');
382
+ });
383
+ it('still handles unquoted paths correctly', () => {
384
+ const diffLines = [
385
+ 'diff --git a/src/file.js b/src/file.js',
386
+ 'index 1234567..8901234 100644',
387
+ '--- a/src/file.js',
388
+ '+++ b/src/file.js',
389
+ '@@ -1,3 +1,3 @@',
390
+ ' line1',
391
+ '-old',
392
+ '+new',
393
+ ' line3',
394
+ ];
395
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
396
+ expect(result).toBeDefined();
397
+ expect(result.path).toBe('src/file.js');
398
+ expect(result.status).toBe('modified');
399
+ expect(result.additions).toBe(1);
400
+ expect(result.deletions).toBe(1);
401
+ });
402
+ it('handles unquoted paths with spaces when core.quotePath=false', () => {
403
+ const diffLines = [
404
+ 'diff --git a/path with spaces/file.txt b/path with spaces/file.txt',
405
+ 'index 1234567..8901234 100644',
406
+ '--- a/path with spaces/file.txt',
407
+ '+++ b/path with spaces/file.txt',
408
+ '@@ -1 +1 @@',
409
+ '-old content',
410
+ '+new content',
411
+ ];
412
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
413
+ expect(result).toBeDefined();
414
+ expect(result.path).toBe('path with spaces/file.txt');
415
+ expect(result.status).toBe('modified');
416
+ expect(result.additions).toBe(1);
417
+ expect(result.deletions).toBe(1);
418
+ });
419
+ it('decodes unquoted octal escapes in diff headers', () => {
420
+ const diffLines = [
421
+ 'diff --git a/some\\040folder/file\\040name.ts b/some\\040folder/file\\040name.ts',
422
+ 'index 3333333..4444444 100644',
423
+ '--- a/some\\040folder/file\\040name.ts',
424
+ '+++ b/some\\040folder/file\\040name.ts',
425
+ '@@ -1 +1 @@',
426
+ '-old',
427
+ '+new',
428
+ ];
429
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
430
+ expect(result?.path).toBe('some folder/file name.ts');
431
+ expect(result?.oldPath).toBeUndefined();
432
+ });
433
+ it('handles unquoted paths containing "b/" in filename', () => {
434
+ const diffLines = [
435
+ 'diff --git a/dir b/sub/file b/dir b/sub/file',
436
+ 'index 1234567..8901234 100644',
437
+ '--- a/dir b/sub/file',
438
+ '+++ b/dir b/sub/file',
439
+ '@@ -1 +1 @@',
440
+ '-old',
441
+ '+new',
442
+ ];
443
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
444
+ expect(result).toBeDefined();
445
+ expect(result.path).toBe('dir b/sub/file');
446
+ expect(result.oldPath).toBeUndefined();
447
+ expect(result.status).toBe('modified');
448
+ });
449
+ it('handles renamed files with "b/" in the path', () => {
450
+ const diffLines = [
451
+ 'diff --git a/old b/path/file b/new b/path/file',
452
+ 'similarity index 100%',
453
+ 'rename from old b/path/file',
454
+ 'rename to new b/path/file',
455
+ ];
456
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
457
+ expect(result).toBeDefined();
458
+ expect(result.path).toBe('new b/path/file');
459
+ expect(result.oldPath).toBe('old b/path/file');
460
+ expect(result.status).toBe('renamed');
461
+ });
462
+ it('handles alternative git diff prefixes for working tree comparisons', () => {
463
+ const diffLines = [
464
+ 'diff --git c/a/test.txt w/a/test.txt',
465
+ 'index 1234567..8901234 100644',
466
+ '--- c/a/test.txt',
467
+ '+++ w/a/test.txt\t',
468
+ '@@ -1 +1 @@',
469
+ '-old',
470
+ '+new',
471
+ ];
472
+ const summary = {
473
+ file: 'a/test.txt',
474
+ insertions: 1,
475
+ deletions: 1,
476
+ binary: false,
477
+ };
478
+ const result = parser.parseFileBlock(diffLines.join('\n'), summary);
479
+ expect(result).toBeDefined();
480
+ expect(result.path).toBe('a/test.txt');
481
+ expect(result.oldPath).toBeUndefined();
482
+ expect(result.status).toBe('modified');
483
+ });
484
+ it('handles rename metadata with alternative git prefixes', () => {
485
+ const diffLines = [
486
+ 'diff --git c/old/name.txt w/new/name.txt',
487
+ 'similarity index 100%',
488
+ 'rename from c/old/name.txt\t',
489
+ 'rename to w/new/name.txt\t',
490
+ ];
491
+ const summary = {
492
+ file: 'new/name.txt',
493
+ from: 'old/name.txt',
494
+ insertions: 0,
495
+ deletions: 0,
496
+ binary: false,
497
+ };
498
+ const result = parser.parseFileBlock(diffLines.join('\n'), summary);
499
+ expect(result).toBeDefined();
500
+ expect(result.path).toBe('new/name.txt');
501
+ expect(result.oldPath).toBe('old/name.txt');
502
+ expect(result.status).toBe('renamed');
503
+ });
504
+ it('ignores trailing metadata separators in diff path lines', () => {
505
+ const diffLines = [
506
+ 'diff --git c/foo bar.txt w/foo bar.txt',
507
+ 'index 1234567..8901234 100644',
508
+ '--- c/foo bar.txt\t',
509
+ '+++ w/foo bar.txt\t',
510
+ '@@ -1 +1 @@',
511
+ '-old',
512
+ '+new',
513
+ ];
514
+ const summary = {
515
+ file: 'foo bar.txt',
516
+ insertions: 1,
517
+ deletions: 1,
518
+ binary: false,
519
+ };
520
+ const result = parser.parseFileBlock(diffLines.join('\n'), summary);
521
+ expect(result).toBeDefined();
522
+ expect(result.path).toBe('foo bar.txt');
523
+ expect(result.status).toBe('modified');
524
+ });
525
+ it('prefers header paths over summary paths when they differ', () => {
526
+ const diffLines = [
527
+ 'diff --git a/a/test.txt b/a/test.txt',
528
+ 'new file mode 100644',
529
+ 'index 0000000..257cc56',
530
+ '--- /dev/null',
531
+ '+++ b/a/test.txt',
532
+ '@@ -0,0 +1 @@',
533
+ '+content',
534
+ ];
535
+ const summary = {
536
+ file: 'a/test.txt',
537
+ insertions: 1,
538
+ deletions: 0,
539
+ binary: false,
540
+ };
541
+ const result = parser.parseFileBlock(diffLines.join('\n'), summary);
542
+ expect(result).toBeDefined();
543
+ expect(result?.path).toBe('a/test.txt');
544
+ expect(result?.status).toBe('added');
545
+ });
546
+ it('does not treat added files as renamed even if summary includes from path', () => {
547
+ const diffLines = [
548
+ 'diff --git a/test.js b/test.js',
549
+ 'new file mode 100644',
550
+ 'index 0000000..257cc56',
551
+ '--- /dev/null',
552
+ '+++ b/test.js',
553
+ '@@ -0,0 +1 @@',
554
+ '+console.log("test");',
555
+ ];
556
+ const summary = {
557
+ file: 'test.js',
558
+ from: 'c/test.js',
559
+ insertions: 1,
560
+ deletions: 0,
561
+ binary: false,
562
+ };
563
+ const result = parser.parseFileBlock(diffLines.join('\n'), summary);
564
+ expect(result).toBeDefined();
565
+ expect(result?.status).toBe('added');
566
+ expect(result?.oldPath).toBeUndefined();
567
+ });
568
+ it('parses file paths with octal escape sequences correctly', () => {
569
+ const diffLines = [
570
+ 'diff --git "a/file\\303\\244.txt" "b/file\\303\\244.txt"',
571
+ 'new file mode 100644',
572
+ 'index 0000000..257cc56',
573
+ '--- /dev/null',
574
+ '+++ "b/file\\303\\244.txt"',
575
+ '@@ -0,0 +1 @@',
576
+ '+content',
577
+ ];
578
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
579
+ expect(result).toBeDefined();
580
+ expect(result.path).toBe('fileä.txt');
581
+ expect(result.status).toBe('added');
582
+ });
583
+ it('parses file paths with mixed escape sequences correctly', () => {
584
+ const diffLines = [
585
+ 'diff --git "a/dir\\303\\251/file\\twith\\nmixed.txt" "b/dir\\303\\251/file\\twith\\nmixed.txt"',
586
+ 'new file mode 100644',
587
+ 'index 0000000..257cc56',
588
+ '--- /dev/null',
589
+ '+++ "b/dir\\303\\251/file\\twith\\nmixed.txt"',
590
+ '@@ -0,0 +1 @@',
591
+ '+test',
592
+ ];
593
+ const result = parser.parseFileBlock(diffLines.join('\n'), null);
594
+ expect(result).toBeDefined();
595
+ expect(result.path).toBe('diré/file\twith\nmixed.txt');
596
+ expect(result.status).toBe('added');
597
+ });
598
+ });
285
599
  describe('File status detection improvements', () => {
286
600
  it('prioritizes new file mode over other indicators', () => {
287
601
  const diffLines = [
@@ -292,8 +606,10 @@ describe('GitDiffParser', () => {
292
606
  '+++ b/test.txt',
293
607
  ];
294
608
  const summary = {
609
+ file: 'test.txt',
295
610
  insertions: 5,
296
611
  deletions: 0,
612
+ binary: false,
297
613
  };
298
614
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
299
615
  expect(result.status).toBe('added');
@@ -307,8 +623,10 @@ describe('GitDiffParser', () => {
307
623
  '+++ b/test.txt', // This might confuse simple parsers
308
624
  ];
309
625
  const summary = {
626
+ file: 'test.txt',
310
627
  insertions: 0,
311
628
  deletions: 5,
629
+ binary: false,
312
630
  };
313
631
  const result = parser.parseFileBlock(diffLines.join('\n'), summary);
314
632
  expect(result.status).toBe('deleted');
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ export declare const LineNumberSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>]>;
3
+ export declare const CommentSchema: z.ZodObject<{
4
+ id: z.ZodString;
5
+ file: z.ZodString;
6
+ line: z.ZodUnion<readonly [z.ZodNumber, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>]>;
7
+ body: z.ZodString;
8
+ timestamp: z.ZodString;
9
+ codeContent: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>;
11
+ export declare const CommentsPayloadSchema: z.ZodObject<{
12
+ comments: z.ZodOptional<z.ZodArray<z.ZodObject<{
13
+ id: z.ZodString;
14
+ file: z.ZodString;
15
+ line: z.ZodUnion<readonly [z.ZodNumber, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>]>;
16
+ body: z.ZodString;
17
+ timestamp: z.ZodString;
18
+ codeContent: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$strip>>>;
20
+ }, z.core.$strip>;
21
+ export type ValidatedComment = z.infer<typeof CommentSchema>;
22
+ export type ValidatedCommentsPayload = z.infer<typeof CommentsPayloadSchema>;
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ // Schema for line number: either a single number or a tuple of two numbers
3
+ export const LineNumberSchema = z.union([z.number(), z.tuple([z.number(), z.number()])]);
4
+ // Schema for a single comment
5
+ export const CommentSchema = z.object({
6
+ id: z.string(),
7
+ file: z.string(),
8
+ line: LineNumberSchema,
9
+ body: z.string(),
10
+ timestamp: z.string(),
11
+ codeContent: z.string().optional(),
12
+ });
13
+ // Schema for the comments payload sent from client
14
+ export const CommentsPayloadSchema = z.object({
15
+ comments: z.array(CommentSchema).optional(),
16
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,229 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CommentSchema, CommentsPayloadSchema, LineNumberSchema } from './commentSchema';
3
+ describe('Comment Schema Validation', () => {
4
+ describe('LineNumberSchema', () => {
5
+ it('should accept single number', () => {
6
+ const result = LineNumberSchema.safeParse(42);
7
+ expect(result.success).toBe(true);
8
+ if (result.success) {
9
+ expect(result.data).toBe(42);
10
+ }
11
+ });
12
+ it('should accept tuple of two numbers', () => {
13
+ const result = LineNumberSchema.safeParse([10, 20]);
14
+ expect(result.success).toBe(true);
15
+ if (result.success) {
16
+ expect(result.data).toEqual([10, 20]);
17
+ }
18
+ });
19
+ it('should reject string', () => {
20
+ const result = LineNumberSchema.safeParse('42');
21
+ expect(result.success).toBe(false);
22
+ });
23
+ it('should reject array with more than 2 numbers', () => {
24
+ const result = LineNumberSchema.safeParse([1, 2, 3]);
25
+ expect(result.success).toBe(false);
26
+ });
27
+ it('should reject array with less than 2 numbers', () => {
28
+ const result = LineNumberSchema.safeParse([1]);
29
+ expect(result.success).toBe(false);
30
+ });
31
+ });
32
+ describe('CommentSchema', () => {
33
+ it('should accept valid comment with single line number', () => {
34
+ const comment = {
35
+ id: '123',
36
+ file: 'src/app.ts',
37
+ line: 42,
38
+ body: 'Test comment',
39
+ timestamp: '2024-01-01T00:00:00Z',
40
+ };
41
+ const result = CommentSchema.safeParse(comment);
42
+ expect(result.success).toBe(true);
43
+ if (result.success) {
44
+ expect(result.data).toEqual(comment);
45
+ }
46
+ });
47
+ it('should accept valid comment with line range', () => {
48
+ const comment = {
49
+ id: '456',
50
+ file: 'src/utils.ts',
51
+ line: [10, 20],
52
+ body: 'Range comment',
53
+ timestamp: '2024-01-01T00:00:00Z',
54
+ };
55
+ const result = CommentSchema.safeParse(comment);
56
+ expect(result.success).toBe(true);
57
+ if (result.success) {
58
+ expect(result.data).toEqual(comment);
59
+ }
60
+ });
61
+ it('should accept comment with optional codeContent', () => {
62
+ const comment = {
63
+ id: '789',
64
+ file: 'src/index.ts',
65
+ line: 1,
66
+ body: 'Comment with code',
67
+ timestamp: '2024-01-01T00:00:00Z',
68
+ codeContent: 'const x = 42;',
69
+ };
70
+ const result = CommentSchema.safeParse(comment);
71
+ expect(result.success).toBe(true);
72
+ if (result.success) {
73
+ expect(result.data.codeContent).toBe('const x = 42;');
74
+ }
75
+ });
76
+ it('should reject comment without required fields', () => {
77
+ const invalidComment = {
78
+ id: '123',
79
+ // missing file
80
+ line: 42,
81
+ body: 'Test',
82
+ timestamp: '2024-01-01T00:00:00Z',
83
+ };
84
+ const result = CommentSchema.safeParse(invalidComment);
85
+ expect(result.success).toBe(false);
86
+ if (!result.success) {
87
+ expect(result.error.issues[0]?.path).toContain('file');
88
+ }
89
+ });
90
+ it('should reject comment with wrong type for file', () => {
91
+ const invalidComment = {
92
+ id: '123',
93
+ file: 123, // should be string
94
+ line: 42,
95
+ body: 'Test',
96
+ timestamp: '2024-01-01T00:00:00Z',
97
+ };
98
+ const result = CommentSchema.safeParse(invalidComment);
99
+ expect(result.success).toBe(false);
100
+ });
101
+ it('should reject comment with invalid line format', () => {
102
+ const invalidComment = {
103
+ id: '123',
104
+ file: 'test.ts',
105
+ line: 'invalid', // should be number or [number, number]
106
+ body: 'Test',
107
+ timestamp: '2024-01-01T00:00:00Z',
108
+ };
109
+ const result = CommentSchema.safeParse(invalidComment);
110
+ expect(result.success).toBe(false);
111
+ });
112
+ });
113
+ describe('CommentsPayloadSchema', () => {
114
+ it('should accept valid payload with comments array', () => {
115
+ const payload = {
116
+ comments: [
117
+ {
118
+ id: '1',
119
+ file: 'file1.ts',
120
+ line: 10,
121
+ body: 'Comment 1',
122
+ timestamp: '2024-01-01T00:00:00Z',
123
+ },
124
+ {
125
+ id: '2',
126
+ file: 'file2.ts',
127
+ line: [20, 30],
128
+ body: 'Comment 2',
129
+ timestamp: '2024-01-01T00:01:00Z',
130
+ },
131
+ ],
132
+ };
133
+ const result = CommentsPayloadSchema.safeParse(payload);
134
+ expect(result.success).toBe(true);
135
+ if (result.success) {
136
+ expect(result.data.comments).toHaveLength(2);
137
+ }
138
+ });
139
+ it('should accept empty comments array', () => {
140
+ const payload = { comments: [] };
141
+ const result = CommentsPayloadSchema.safeParse(payload);
142
+ expect(result.success).toBe(true);
143
+ if (result.success) {
144
+ expect(result.data.comments).toEqual([]);
145
+ }
146
+ });
147
+ it('should accept payload without comments property', () => {
148
+ const payload = {};
149
+ const result = CommentsPayloadSchema.safeParse(payload);
150
+ expect(result.success).toBe(true);
151
+ if (result.success) {
152
+ expect(result.data.comments).toBeUndefined();
153
+ }
154
+ });
155
+ it('should reject payload with invalid comment in array', () => {
156
+ const payload = {
157
+ comments: [
158
+ {
159
+ id: '1',
160
+ file: 'file1.ts',
161
+ line: 10,
162
+ body: 'Valid comment',
163
+ timestamp: '2024-01-01T00:00:00Z',
164
+ },
165
+ {
166
+ id: '2',
167
+ // missing file property
168
+ line: 20,
169
+ body: 'Invalid comment',
170
+ timestamp: '2024-01-01T00:01:00Z',
171
+ },
172
+ ],
173
+ };
174
+ const result = CommentsPayloadSchema.safeParse(payload);
175
+ expect(result.success).toBe(false);
176
+ });
177
+ it('should reject payload with comments as non-array', () => {
178
+ const payload = {
179
+ comments: 'not an array',
180
+ };
181
+ const result = CommentsPayloadSchema.safeParse(payload);
182
+ expect(result.success).toBe(false);
183
+ });
184
+ it('should handle real-world DiffComment format from client', () => {
185
+ // This simulates the actual DiffComment format sent from client
186
+ const diffCommentPayload = {
187
+ comments: [
188
+ {
189
+ id: 'abc123',
190
+ filePath: 'src/components/Button.tsx', // Note: 'filePath' instead of 'file'
191
+ position: {
192
+ line: 42,
193
+ side: 'new',
194
+ },
195
+ body: 'Fix this button',
196
+ createdAt: '2024-01-01T00:00:00Z',
197
+ },
198
+ ],
199
+ };
200
+ const result = CommentsPayloadSchema.safeParse(diffCommentPayload);
201
+ expect(result.success).toBe(false); // Should fail because of incorrect property names
202
+ if (!result.success) {
203
+ // Check that it fails because 'file' is missing
204
+ const fileIssue = result.error.issues.find((issue) => issue.path.includes('file'));
205
+ expect(fileIssue).toBeDefined();
206
+ }
207
+ });
208
+ it('should accept properly transformed comments from client', () => {
209
+ // This is what the client should send after transformation
210
+ const transformedPayload = {
211
+ comments: [
212
+ {
213
+ id: 'abc123',
214
+ file: 'src/components/Button.tsx', // Correct property name
215
+ line: 42, // Extracted from position.line
216
+ body: 'Fix this button',
217
+ timestamp: '2024-01-01T00:00:00Z', // Renamed from createdAt
218
+ },
219
+ ],
220
+ };
221
+ const result = CommentsPayloadSchema.safeParse(transformedPayload);
222
+ expect(result.success).toBe(true);
223
+ if (result.success) {
224
+ expect(result.data.comments).toHaveLength(1);
225
+ expect(result.data.comments[0].file).toBe('src/components/Button.tsx');
226
+ }
227
+ });
228
+ });
229
+ });