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});