main
1/**
2 * Tests for Jira extension
3 *
4 * Run with: bun test jira.test.ts
5 */
6
7import { describe, expect, test } from "bun:test";
8import {
9 parseIssueList,
10 parseIssueListJSON,
11 extractIssueKey,
12 extractIssueKeys,
13 getStatusColor,
14 getPriorityColor,
15 truncate,
16 buildCreateConfirmation,
17 buildUpdateConfirmation,
18 buildTransitionConfirmation,
19 buildCommentConfirmation,
20 isAuthError,
21 isNetworkError,
22 isNotFoundError,
23 getErrorMessage,
24} from "./utils";
25
26// Mock theme for testing
27const mockTheme = {
28 fg: (color: string, text: string) => text,
29 bold: (text: string) => text,
30};
31
32// ============================================================================
33// Utility Functions
34// ============================================================================
35
36describe("Utility Functions", () => {
37 describe("parseIssueListJSON", () => {
38 test("parses jira CLI JSON output", () => {
39 const output = JSON.stringify([
40 {
41 key: "SRVKP-1234",
42 fields: {
43 summary: "Test summary",
44 issueType: { name: "Bug" },
45 status: { name: "To Do" },
46 assignee: { displayName: "Alice" },
47 priority: { name: "Major" },
48 },
49 },
50 {
51 key: "SRVKP-5678",
52 fields: {
53 summary: "Another issue",
54 issueType: { name: "Epic" },
55 status: { name: "In Progress" },
56 assignee: { displayName: "Bob" },
57 priority: { name: "Undefined" },
58 },
59 },
60 ]);
61
62 const issues = parseIssueListJSON(output);
63
64 expect(issues.length).toBe(2);
65 expect(issues[0].key).toBe("SRVKP-1234");
66 expect(issues[0].type).toBe("Bug");
67 expect(issues[0].summary).toBe("Test summary");
68 expect(issues[0].status).toBe("To Do");
69 expect(issues[0].assignee).toBe("Alice");
70 expect(issues[0].priority).toBe("Major");
71 expect(issues[1].key).toBe("SRVKP-5678");
72 expect(issues[1].type).toBe("Epic");
73 expect(issues[1].priority).toBeUndefined(); // "Undefined" is filtered out
74 });
75
76 test("handles unassigned issues", () => {
77 const output = JSON.stringify([
78 {
79 key: "SRVKP-1234",
80 fields: {
81 summary: "Test",
82 issueType: { name: "Task" },
83 status: { name: "To Do" },
84 priority: { name: "Undefined" },
85 },
86 },
87 ]);
88
89 const issues = parseIssueListJSON(output);
90
91 expect(issues.length).toBe(1);
92 expect(issues[0].assignee).toBe("Unassigned");
93 expect(issues[0].priority).toBeUndefined();
94 });
95
96 test("handles invalid JSON", () => {
97 const issues = parseIssueListJSON("not json");
98 expect(issues.length).toBe(0);
99 });
100
101 test("handles empty array", () => {
102 const issues = parseIssueListJSON("[]");
103 expect(issues.length).toBe(0);
104 });
105
106 test("handles missing fields gracefully", () => {
107 const output = JSON.stringify([
108 {
109 key: "SRVKP-1234",
110 fields: {},
111 },
112 ]);
113
114 const issues = parseIssueListJSON(output);
115
116 expect(issues.length).toBe(1);
117 expect(issues[0].key).toBe("SRVKP-1234");
118 expect(issues[0].type).toBe("?");
119 expect(issues[0].summary).toBe("");
120 expect(issues[0].status).toBe("?");
121 expect(issues[0].assignee).toBe("Unassigned");
122 });
123
124 test("handles null fields", () => {
125 const output = JSON.stringify([
126 {
127 key: "SRVKP-1234",
128 fields: {
129 summary: null,
130 issueType: null,
131 status: null,
132 assignee: null,
133 priority: null,
134 },
135 },
136 ]);
137
138 const issues = parseIssueListJSON(output);
139
140 expect(issues.length).toBe(1);
141 expect(issues[0].summary).toBe("");
142 expect(issues[0].type).toBe("?");
143 expect(issues[0].status).toBe("?");
144 expect(issues[0].assignee).toBe("Unassigned");
145 });
146
147 test("handles non-array response", () => {
148 const issues = parseIssueListJSON('{"error": "something"}');
149 expect(issues.length).toBe(0);
150 });
151 });
152
153 describe("parseIssueList", () => {
154 test("parses jira CLI plain output (TAB-delimited with alignment)", () => {
155 const output = `Bug\tSRVKP-1234\t\tSummary text here\t\t\tTo Do
156Task\tSRVKP-5678\tAnother summary\t\tIn Progress`;
157
158 const issues = parseIssueList(output);
159
160 expect(issues.length).toBe(2);
161 expect(issues[0].key).toBe("SRVKP-1234");
162 expect(issues[0].type).toBe("Bug");
163 expect(issues[0].summary).toBe("Summary text here");
164 expect(issues[0].status).toBe("To Do");
165 expect(issues[0].assignee).toBe("Unassigned");
166 expect(issues[1].key).toBe("SRVKP-5678");
167 expect(issues[1].summary).toBe("Another summary");
168 });
169
170 test("skips header lines", () => {
171 const output = `TYPE\tKEY\t\tSUMMARY\t\t\tSTATUS
172Bug\tSRVKP-1234\t\tSummary\t\t\tDone`;
173
174 const issues = parseIssueList(output);
175
176 expect(issues.length).toBe(1);
177 expect(issues[0].key).toBe("SRVKP-1234");
178 });
179
180 test("handles empty output", () => {
181 const issues = parseIssueList("");
182 expect(issues.length).toBe(0);
183 });
184
185 test("handles minimal columns", () => {
186 const output = `Bug\tSRVKP-1234\tSummary\tTo Do`;
187
188 const issues = parseIssueList(output);
189
190 expect(issues.length).toBe(1);
191 expect(issues[0].assignee).toBe("Unassigned");
192 expect(issues[0].status).toBe("To Do");
193 });
194 });
195
196 describe("extractIssueKey", () => {
197 test("extracts issue key from text", () => {
198 expect(extractIssueKey("Created SRVKP-1234 successfully")).toBe("SRVKP-1234");
199 expect(extractIssueKey("Issue KONFLUX-456 updated")).toBe("KONFLUX-456");
200 });
201
202 test("returns null when no key found", () => {
203 expect(extractIssueKey("No issue key here")).toBeNull();
204 });
205
206 test("extracts first key when multiple present", () => {
207 expect(extractIssueKey("SRVKP-1234 and SRVKP-5678")).toBe("SRVKP-1234");
208 });
209 });
210
211 describe("extractIssueKeys", () => {
212 test("extracts all issue keys", () => {
213 const keys = extractIssueKeys("SRVKP-1234 KONFLUX-456 RHCLOUD-789");
214 expect(keys).toEqual(["SRVKP-1234", "KONFLUX-456", "RHCLOUD-789"]);
215 });
216
217 test("removes duplicates", () => {
218 const keys = extractIssueKeys("SRVKP-1234 SRVKP-1234 SRVKP-1234");
219 expect(keys).toEqual(["SRVKP-1234"]);
220 });
221
222 test("matches issue keys in sentences", () => {
223 const keys = extractIssueKeys("Working on SRVKP-1234 which relates to KONFLUX-456");
224 expect(keys).toEqual(["SRVKP-1234", "KONFLUX-456"]);
225 });
226
227 test("doesn't match invalid patterns", () => {
228 const keys = extractIssueKeys("A-1 lowercase-123 NO-KEY");
229 expect(keys).toEqual([]);
230 });
231
232 test("returns empty array when none found", () => {
233 const keys = extractIssueKeys("no keys here");
234 expect(keys).toEqual([]);
235 });
236 });
237
238 describe("getStatusColor", () => {
239 test("returns success color for done status", () => {
240 const result = getStatusColor("Done", mockTheme as any);
241 expect(result).toContain("[Done]");
242 });
243
244 test("returns success color for closed status", () => {
245 const result = getStatusColor("Closed", mockTheme as any);
246 expect(result).toContain("[Closed]");
247 });
248
249 test("returns accent color for in progress", () => {
250 const result = getStatusColor("In Progress", mockTheme as any);
251 expect(result).toContain("[In Progress]");
252 });
253
254 test("returns error color for blocked", () => {
255 const result = getStatusColor("Blocked", mockTheme as any);
256 expect(result).toContain("[Blocked]");
257 });
258
259 test("returns muted color for other statuses", () => {
260 const result = getStatusColor("To Do", mockTheme as any);
261 expect(result).toContain("[To Do]");
262 });
263
264 test("is case insensitive", () => {
265 expect(getStatusColor("DONE", mockTheme as any)).toContain("[DONE]");
266 expect(getStatusColor("blocked", mockTheme as any)).toContain("[blocked]");
267 });
268 });
269
270 describe("getPriorityColor", () => {
271 test("returns error color for blocker", () => {
272 expect(getPriorityColor("Blocker", mockTheme as any)).toBe("Blocker");
273 });
274
275 test("returns error color for critical", () => {
276 expect(getPriorityColor("Critical", mockTheme as any)).toBe("Critical");
277 });
278
279 test("returns warning color for major", () => {
280 expect(getPriorityColor("Major", mockTheme as any)).toBe("Major");
281 });
282
283 test("returns dim color for minor", () => {
284 expect(getPriorityColor("Minor", mockTheme as any)).toBe("Minor");
285 });
286
287 test("is case insensitive", () => {
288 expect(getPriorityColor("BLOCKER", mockTheme as any)).toBe("BLOCKER");
289 });
290 });
291
292 describe("truncate", () => {
293 test("truncates long text", () => {
294 const result = truncate("This is a very long text that should be truncated", 20);
295 expect(result).toBe("This is a very lo...");
296 expect(result.length).toBe(20);
297 });
298
299 test("doesn't truncate short text", () => {
300 expect(truncate("Short", 20)).toBe("Short");
301 });
302
303 test("handles exact length", () => {
304 expect(truncate("Exactly 20 chars!!!!", 20)).toBe("Exactly 20 chars!!!!");
305 });
306
307 test("handles empty string", () => {
308 expect(truncate("", 20)).toBe("");
309 });
310
311 test("handles very short maxLength", () => {
312 expect(truncate("Hello World", 3)).toBe("...");
313 });
314 });
315});
316
317// ============================================================================
318// Issue Key Detection
319// ============================================================================
320
321describe("Issue Key Detection", () => {
322 describe("Pattern Matching", () => {
323 test("matches standard project keys", () => {
324 const pattern = /\b([A-Z]{2,}-\d+)\b/g;
325
326 expect("SRVKP-1234".match(pattern)).toEqual(["SRVKP-1234"]);
327 expect("KONFLUX-456".match(pattern)).toEqual(["KONFLUX-456"]);
328 expect("RHCLOUD-789".match(pattern)).toEqual(["RHCLOUD-789"]);
329 });
330
331 test("doesn't match single letter projects", () => {
332 const pattern = /\b([A-Z]{2,}-\d+)\b/g;
333
334 expect("A-1".match(pattern)).toBeNull();
335 expect("X-999".match(pattern)).toBeNull();
336 });
337
338 test("doesn't match lowercase", () => {
339 const pattern = /\b([A-Z]{2,}-\d+)\b/g;
340
341 expect("srvkp-1234".match(pattern)).toBeNull();
342 expect("lowercase-123".match(pattern)).toBeNull();
343 });
344
345 test("matches multiple keys in text", () => {
346 const pattern = /\b([A-Z]{2,}-\d+)\b/g;
347 const text = "Working on SRVKP-1234 and KONFLUX-456";
348 const matches = text.match(pattern);
349
350 expect(matches).toEqual(["SRVKP-1234", "KONFLUX-456"]);
351 });
352
353 test("matches keys with commas and punctuation", () => {
354 const pattern = /\b([A-Z]{2,}-\d+)\b/g;
355 const text = "Issues: SRVKP-1234, SRVKP-5678, and KONFLUX-999.";
356 const matches = text.match(pattern);
357
358 expect(matches).toEqual(["SRVKP-1234", "SRVKP-5678", "KONFLUX-999"]);
359 });
360 });
361
362 describe("Detection Logic", () => {
363 test("detects bare issue keys", () => {
364 const input = "SRVKP-1234";
365 const justKeys = input
366 .trim()
367 .split(/\s+/)
368 .every((word) => /^[A-Z]{2,}-\d+$/.test(word));
369
370 expect(justKeys).toBe(true);
371 });
372
373 test("detects multiple bare keys", () => {
374 const input = "SRVKP-1234 SRVKP-5678";
375 const justKeys = input
376 .trim()
377 .split(/\s+/)
378 .every((word) => /^[A-Z]{2,}-\d+$/.test(word));
379
380 expect(justKeys).toBe(true);
381 });
382
383 test("doesn't detect keys in context", () => {
384 const input = "Working on SRVKP-1234";
385 const justKeys = input
386 .trim()
387 .split(/\s+/)
388 .every((word) => /^[A-Z]{2,}-\d+$/.test(word));
389
390 expect(justKeys).toBe(false);
391 });
392
393 test("doesn't detect with mixed content", () => {
394 const input = "SRVKP-1234 and some text";
395 const justKeys = input
396 .trim()
397 .split(/\s+/)
398 .every((word) => /^[A-Z]{2,}-\d+$/.test(word));
399
400 expect(justKeys).toBe(false);
401 });
402 });
403});
404
405// ============================================================================
406// Confirmation Message Building
407// ============================================================================
408
409describe("Confirmation Message Building", () => {
410 test("buildCreateConfirmation includes all fields", () => {
411 const params = {
412 issueType: "Bug",
413 summary: "Test bug",
414 description: "Test description",
415 priority: "Major",
416 assignee: "alice",
417 labels: ["bug", "urgent"],
418 epic: "SRVKP-1000",
419 };
420
421 const message = buildCreateConfirmation(params);
422
423 expect(message).toContain("Type: Bug");
424 expect(message).toContain('Summary: "Test bug"');
425 expect(message).toContain("Priority: Major");
426 expect(message).toContain("Assignee: alice");
427 expect(message).toContain("Labels: bug, urgent");
428 expect(message).toContain("Epic: SRVKP-1000");
429 });
430
431 test("buildCreateConfirmation truncates long description", () => {
432 const params = {
433 issueType: "Bug",
434 summary: "Test",
435 description: "a".repeat(150),
436 };
437
438 const message = buildCreateConfirmation(params);
439
440 expect(message).toContain("Description:");
441 expect(message).toContain("...");
442 });
443
444 test("buildCreateConfirmation minimal fields", () => {
445 const params = {
446 issueType: "Task",
447 summary: "Simple task",
448 };
449
450 const message = buildCreateConfirmation(params);
451
452 expect(message).toContain("Type: Task");
453 expect(message).toContain('Summary: "Simple task"');
454 expect(message).not.toContain("Priority:");
455 expect(message).not.toContain("Labels:");
456 expect(message).toContain("This will create a new issue in Jira.");
457 });
458
459 test("buildUpdateConfirmation with current value", () => {
460 const params = { key: "SRVKP-1234", field: "priority", value: "Critical" };
461 const message = buildUpdateConfirmation(params, "Major");
462
463 expect(message).toContain("Issue: SRVKP-1234");
464 expect(message).toContain("Field: priority");
465 expect(message).toContain("From: Major");
466 expect(message).toContain("To: Critical");
467 });
468
469 test("buildUpdateConfirmation without current value", () => {
470 const params = { key: "SRVKP-1234", field: "assignee", value: "alice" };
471 const message = buildUpdateConfirmation(params);
472
473 expect(message).toContain("Issue: SRVKP-1234");
474 expect(message).not.toContain("From:");
475 expect(message).toContain("To: alice");
476 });
477
478 test("buildTransitionConfirmation with current state", () => {
479 const params = { key: "SRVKP-1234", state: "In Progress" };
480 const message = buildTransitionConfirmation(params, "To Do");
481
482 expect(message).toContain("Issue: SRVKP-1234");
483 expect(message).toContain("From: To Do");
484 expect(message).toContain("To: In Progress");
485 });
486
487 test("buildCommentConfirmation truncates long comments", () => {
488 const params = { key: "SRVKP-1234", comment: "a".repeat(300) };
489 const message = buildCommentConfirmation(params);
490
491 expect(message).toContain("Issue: SRVKP-1234");
492 expect(message).toContain("...");
493 expect(message).toContain("This will add a public comment");
494 });
495
496 test("buildCommentConfirmation short comment", () => {
497 const params = { key: "SRVKP-1234", comment: "Fixed in PR #123" };
498 const message = buildCommentConfirmation(params);
499
500 expect(message).toContain("Fixed in PR #123");
501 expect(message).not.toContain("...");
502 });
503});
504
505// ============================================================================
506// Error Handling
507// ============================================================================
508
509describe("Error Handling", () => {
510 test("isAuthError detects authentication failures", () => {
511 expect(isAuthError("authentication failed")).toBe(true);
512 expect(isAuthError("unauthorized access")).toBe(true);
513 expect(isAuthError("invalid token provided")).toBe(true);
514 expect(isAuthError("permission denied")).toBe(true);
515 expect(isAuthError("network timeout")).toBe(false);
516 });
517
518 test("isNetworkError detects network failures", () => {
519 expect(isNetworkError("connection refused")).toBe(true);
520 expect(isNetworkError("timeout waiting for response")).toBe(true);
521 expect(isNetworkError("network unreachable")).toBe(true);
522 expect(isNetworkError("dial tcp: connection failed")).toBe(true);
523 expect(isNetworkError("authentication failed")).toBe(false);
524 });
525
526 test("isNotFoundError detects not found errors", () => {
527 expect(isNotFoundError("issue not found")).toBe(true);
528 expect(isNotFoundError("does not exist")).toBe(true);
529 expect(isNotFoundError("authentication failed")).toBe(false);
530 });
531
532 test("getErrorMessage returns helpful messages", () => {
533 expect(getErrorMessage("authentication failed", "list")).toContain("API token");
534 expect(getErrorMessage("connection timeout", "view")).toContain("VPN");
535 expect(getErrorMessage("not found", "view")).toContain("not found");
536 expect(getErrorMessage("unknown error", "create")).toBe("unknown error");
537 });
538
539 test("isAuthError is case insensitive", () => {
540 expect(isAuthError("Authentication Failed")).toBe(true);
541 expect(isAuthError("UNAUTHORIZED")).toBe(true);
542 });
543
544 test("isNetworkError handles dial tcp errors", () => {
545 expect(isNetworkError("dial tcp 1.2.3.4:443: connect: connection refused")).toBe(true);
546 expect(isNetworkError("no such host issues.redhat.com")).toBe(true);
547 });
548});
549
550// ============================================================================
551// CLI Command Building (testing the actual args construction logic)
552// ============================================================================
553
554describe("CLI Command Building", () => {
555 describe("Create command", () => {
556 test("uses -b for description (not --description)", () => {
557 // Simulate what handleCreate does
558 const params = {
559 issueType: "Bug",
560 summary: "Test bug",
561 description: "This is a description",
562 };
563 const args = ["issue", "create", "--type", params.issueType, "--summary", params.summary, "--no-input"];
564 if (params.description) {
565 args.push("-b", params.description);
566 }
567
568 expect(args).toContain("-b");
569 expect(args).not.toContain("--description");
570 expect(args).toContain("This is a description");
571 });
572
573 test("uses -y for priority (not --priority)", () => {
574 const params = { priority: "Major" };
575 const args: string[] = [];
576 if (params.priority) {
577 args.push("-y", params.priority);
578 }
579
580 expect(args).toEqual(["-y", "Major"]);
581 });
582
583 test("uses -a for assignee", () => {
584 const params = { assignee: "alice" };
585 const args: string[] = [];
586 if (params.assignee) {
587 args.push("-a", params.assignee);
588 }
589
590 expect(args).toEqual(["-a", "alice"]);
591 });
592
593 test("uses separate -l flags for each label", () => {
594 const labels = ["bug", "urgent", "p1"];
595 const args: string[] = [];
596 for (const label of labels) {
597 args.push("-l", label);
598 }
599
600 expect(args).toEqual(["-l", "bug", "-l", "urgent", "-l", "p1"]);
601 });
602
603 test("uses -P for parent (sub-task)", () => {
604 const params = { parent: "SRVKP-1000" };
605 const args: string[] = [];
606 if (params.parent) {
607 args.push("-P", params.parent);
608 }
609
610 expect(args).toEqual(["-P", "SRVKP-1000"]);
611 });
612
613 test("always includes --no-input", () => {
614 const args = ["issue", "create", "--type", "Bug", "--summary", "Test", "--no-input"];
615 expect(args).toContain("--no-input");
616 });
617 });
618
619 describe("Edit/Update command", () => {
620 test("uses -b for description editing (not --description)", () => {
621 const args = ["issue", "edit", "SRVKP-1234", "-b", "New description", "--no-input"];
622
623 expect(args).toContain("-b");
624 expect(args).not.toContain("--description");
625 expect(args).toContain("--no-input");
626 });
627
628 test("uses -y for priority editing", () => {
629 const args = ["issue", "edit", "SRVKP-1234", "-y", "Critical", "--no-input"];
630
631 expect(args).toContain("-y");
632 expect(args).toContain("--no-input");
633 });
634
635 test("uses -s for summary editing", () => {
636 const args = ["issue", "edit", "SRVKP-1234", "-s", "New summary", "--no-input"];
637
638 expect(args).toContain("-s");
639 expect(args).toContain("--no-input");
640 });
641
642 test("uses separate -l flags for label editing", () => {
643 const value = "bug,urgent,p1";
644 const args = ["issue", "edit", "SRVKP-1234", "--no-input"];
645 for (const label of value.split(",")) {
646 args.push("-l", label.trim());
647 }
648
649 expect(args).toEqual(["issue", "edit", "SRVKP-1234", "--no-input", "-l", "bug", "-l", "urgent", "-l", "p1"]);
650 });
651
652 test("uses issue assign for assignee changes", () => {
653 const args = ["issue", "assign", "SRVKP-1234", "alice@redhat.com"];
654
655 expect(args[1]).toBe("assign");
656 expect(args[2]).toBe("SRVKP-1234");
657 expect(args[3]).toBe("alice@redhat.com");
658 });
659
660 test("always includes --no-input for edit commands", () => {
661 // Priority
662 const priorityArgs = ["issue", "edit", "SRVKP-1234", "-y", "Major", "--no-input"];
663 expect(priorityArgs).toContain("--no-input");
664
665 // Summary
666 const summaryArgs = ["issue", "edit", "SRVKP-1234", "-s", "New", "--no-input"];
667 expect(summaryArgs).toContain("--no-input");
668
669 // Description
670 const descArgs = ["issue", "edit", "SRVKP-1234", "-b", "Desc", "--no-input"];
671 expect(descArgs).toContain("--no-input");
672 });
673 });
674
675 describe("Comment command", () => {
676 test("uses positional argument for comment body", () => {
677 const args = ["issue", "comment", "add", "SRVKP-1234", "This is my comment", "--no-input"];
678
679 expect(args[0]).toBe("issue");
680 expect(args[1]).toBe("comment");
681 expect(args[2]).toBe("add");
682 expect(args[3]).toBe("SRVKP-1234");
683 expect(args[4]).toBe("This is my comment");
684 expect(args).toContain("--no-input");
685 });
686
687 test("handles multi-line comments", () => {
688 const comment = "Line 1\nLine 2\n\nLine 4";
689 const args = ["issue", "comment", "add", "SRVKP-1234", comment, "--no-input"];
690
691 expect(args[4]).toBe("Line 1\nLine 2\n\nLine 4");
692 });
693
694 test("handles comments with special characters", () => {
695 const comment = 'Fixed in PR #123 "quoted" and `backticks`';
696 const args = ["issue", "comment", "add", "SRVKP-1234", comment, "--no-input"];
697
698 expect(args[4]).toBe(comment);
699 });
700 });
701
702 describe("List command", () => {
703 test("uses --raw for JSON output", () => {
704 const args = ["issue", "list", "--raw"];
705 expect(args).toContain("--raw");
706 });
707
708 test("uses --paginate (not --limit)", () => {
709 const args = ["issue", "list", "--raw", "--paginate", "20"];
710 expect(args).toContain("--paginate");
711 expect(args).not.toContain("--limit");
712 });
713
714 test("uses -a for assignee filter", () => {
715 const args = ["issue", "list", "--raw", "-a", "vdemeest"];
716 expect(args).toContain("-a");
717 });
718
719 test("uses -s for status filter", () => {
720 const args = ["issue", "list", "--raw", "-s", "~Done"];
721 expect(args).toContain("-s");
722 });
723 });
724
725 describe("Transition command", () => {
726 test("uses issue move with positional state", () => {
727 const args = ["issue", "move", "SRVKP-1234", "In Progress"];
728
729 expect(args[1]).toBe("move");
730 expect(args[2]).toBe("SRVKP-1234");
731 expect(args[3]).toBe("In Progress");
732 });
733 });
734
735 describe("Link command", () => {
736 test("uses positional arguments for link", () => {
737 const args = ["issue", "link", "SRVKP-1234", "SRVKP-5678", "blocks"];
738
739 expect(args[1]).toBe("link");
740 expect(args[2]).toBe("SRVKP-1234");
741 expect(args[3]).toBe("SRVKP-5678");
742 expect(args[4]).toBe("blocks");
743 });
744 });
745
746 describe("View command", () => {
747 test("uses --plain for non-interactive output", () => {
748 const args = ["issue", "view", "SRVKP-1234", "--plain"];
749 expect(args).toContain("--plain");
750 });
751 });
752});
753
754// ============================================================================
755// Live CLI Tests (require VPN & jira config)
756// ============================================================================
757
758describe("Live CLI Integration", () => {
759 // These tests actually run the jira CLI and verify it works
760 // Skip if not connected or jira not available
761
762 const runJira = async (args: string[]): Promise<{ code: number; stdout: string; stderr: string }> => {
763 const proc = Bun.spawn(["jira", ...args], {
764 stdout: "pipe",
765 stderr: "pipe",
766 env: { ...process.env },
767 });
768 const stdout = await new Response(proc.stdout).text();
769 const stderr = await new Response(proc.stderr).text();
770 const code = await proc.exited;
771 return { code, stdout, stderr };
772 };
773
774 test("jira me returns current user", async () => {
775 const result = await runJira(["me"]);
776 if (result.code !== 0) {
777 console.log("Skipping live test: jira me failed (VPN?):", result.stderr);
778 return;
779 }
780 expect(result.stdout.trim()).toBeTruthy();
781 expect(result.stdout.trim()).not.toContain("error");
782 });
783
784 test("jira issue list --raw returns valid JSON", async () => {
785 const result = await runJira(["issue", "list", "--raw", "--paginate", "3"]);
786 if (result.code !== 0) {
787 console.log("Skipping live test: list failed:", result.stderr);
788 return;
789 }
790
791 // Should be valid JSON
792 const parsed = JSON.parse(result.stdout);
793 expect(Array.isArray(parsed)).toBe(true);
794
795 // Each issue should have expected structure
796 if (parsed.length > 0) {
797 const issue = parsed[0];
798 expect(issue).toHaveProperty("key");
799 expect(issue).toHaveProperty("fields");
800 expect(issue.fields).toHaveProperty("summary");
801 expect(issue.fields).toHaveProperty("status");
802 expect(issue.fields).toHaveProperty("issueType");
803 }
804 });
805
806 test("jira issue list --raw parses correctly through parseIssueListJSON", async () => {
807 const result = await runJira(["issue", "list", "--raw", "--paginate", "5"]);
808 if (result.code !== 0) {
809 console.log("Skipping live test:", result.stderr);
810 return;
811 }
812
813 const issues = parseIssueListJSON(result.stdout);
814
815 if (issues.length > 0) {
816 for (const issue of issues) {
817 // Key should match pattern
818 expect(issue.key).toMatch(/^[A-Z]+-\d+$/);
819 // Type should be non-empty
820 expect(issue.type).toBeTruthy();
821 // Summary should be non-empty
822 expect(issue.summary).toBeTruthy();
823 // Status should be non-empty
824 expect(issue.status).toBeTruthy();
825 // Assignee should be set (even if "Unassigned")
826 expect(issue.assignee).toBeTruthy();
827 }
828 }
829 });
830
831 test("jira issue view --plain returns valid output", async () => {
832 // First get an issue to view
833 const listResult = await runJira(["issue", "list", "--raw", "--paginate", "1"]);
834 if (listResult.code !== 0) {
835 console.log("Skipping live test:", listResult.stderr);
836 return;
837 }
838
839 const issues = parseIssueListJSON(listResult.stdout);
840 if (issues.length === 0) {
841 console.log("Skipping live test: no issues found");
842 return;
843 }
844
845 const key = issues[0].key;
846 const viewResult = await runJira(["issue", "view", key, "--plain"]);
847
848 if (viewResult.code !== 0) {
849 console.log("Skipping live test:", viewResult.stderr);
850 return;
851 }
852
853 // Should contain key information
854 expect(viewResult.stdout).toContain(key);
855 // Should not be interactive TUI output
856 expect(viewResult.stdout).not.toContain("\x1b[?1049h"); // No alternate screen
857 });
858
859 test("jira issue create --help works (verify -b flag)", async () => {
860 const result = await runJira(["issue", "create", "--help"]);
861 expect(result.code).toBe(0);
862 // Verify -b is the body/description flag
863 expect(result.stdout).toContain("-b, --body");
864 // Should NOT have --description
865 expect(result.stdout).not.toContain("--description");
866 });
867
868 test("jira issue edit --help works (verify -b, -s, -y flags)", async () => {
869 const result = await runJira(["issue", "edit", "--help"]);
870 expect(result.code).toBe(0);
871 // Verify flags
872 expect(result.stdout).toContain("-b, --body");
873 expect(result.stdout).toContain("-s, --summary");
874 expect(result.stdout).toContain("-y, --priority");
875 expect(result.stdout).toContain("--no-input");
876 });
877
878 test("jira issue comment add --help works (verify positional argument)", async () => {
879 const result = await runJira(["issue", "comment", "add", "--help"]);
880 expect(result.code).toBe(0);
881 // Should support positional COMMENT_BODY argument
882 expect(result.stdout).toContain("COMMENT_BODY");
883 expect(result.stdout).toContain("--no-input");
884 });
885});
886
887// ============================================================================
888// Parameter Validation Tests
889// ============================================================================
890
891describe("Parameter Validation", () => {
892 test("create requires issueType", () => {
893 const params = { action: "create", summary: "Test" };
894 expect(params.action).toBe("create");
895 expect("issueType" in params).toBe(false);
896 // handleCreate should return error for missing issueType
897 });
898
899 test("create requires summary", () => {
900 const params = { action: "create", issueType: "Bug" };
901 expect("summary" in params).toBe(false);
902 });
903
904 test("view requires key", () => {
905 const params = { action: "view" };
906 expect("key" in params).toBe(false);
907 });
908
909 test("search requires jql", () => {
910 const params = { action: "search" };
911 expect("jql" in params).toBe(false);
912 });
913
914 test("update requires key, field, and value", () => {
915 const params = { action: "update" };
916 expect("key" in params).toBe(false);
917 expect("field" in params).toBe(false);
918 expect("value" in params).toBe(false);
919 });
920
921 test("comment requires key and comment", () => {
922 const params = { action: "comment" };
923 expect("key" in params).toBe(false);
924 expect("comment" in params).toBe(false);
925 });
926
927 test("transition requires key and state", () => {
928 const params = { action: "transition" };
929 expect("key" in params).toBe(false);
930 expect("state" in params).toBe(false);
931 });
932
933 test("link requires from, to, and linkType", () => {
934 const params = { action: "link" };
935 expect("from" in params).toBe(false);
936 expect("to" in params).toBe(false);
937 expect("linkType" in params).toBe(false);
938 });
939
940 test("attach requires key and file", () => {
941 const params = { action: "attach" };
942 expect("key" in params).toBe(false);
943 expect("file" in params).toBe(false);
944 });
945});