Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document provides guidance for LLM agents using the `atl` CLI tool.

## Overview

`atl` is a command-line tool for Jira, Confluence, and Tempo. All commands support `--json` for structured output, making it ideal for programmatic use.
`atl` is a command-line tool for Jira and Confluence. All commands support `--json` for structured output, making it ideal for programmatic use.

## Authentication

Expand Down Expand Up @@ -312,7 +312,6 @@ The CLI returns non-zero exit codes on failure. Common errors:

## Limitations

- `worklog` commands are not yet implemented (Tempo API pending)
- No automatic pagination for large result sets
- Rate limiting may apply for bulk operations

Expand Down Expand Up @@ -359,7 +358,6 @@ internal/
issue/ # issue view|list|create|edit|transition|comment|assign
confluence/ # confluence space|page subcommands
board/ # board list|rank
worklog/ # worklog add|list|edit|delete (stubs)
config/ # config get|set|list|use-context|current-context|set-alias|delete-alias
config/ # Configuration management (~/.config/atlassian/)
iostreams/ # I/O abstraction for testability
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Atlassian CLI (atl)

A command-line tool for working with Jira, Confluence, and Tempo. Designed with LLM-friendly output for easy integration with AI assistants.
A command-line tool for working with Jira and Confluence. Designed with LLM-friendly output for easy integration with AI assistants.

## Installation

Expand Down
118 changes: 101 additions & 17 deletions internal/api/markdown_code_mark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,84 @@ func TestMarkdownToADF_CodeInsideBold(t *testing.T) {
func TestMarkdownToADF_CodeInsideItalic(t *testing.T) {
adf := MarkdownToADF("*italic `code` here*")

if len(adf.Content) != 1 {
t.Fatalf("expected 1 block, got %d", len(adf.Content))
}

para := adf.Content[0]
for _, c := range para.Content {
if hasCodeMark(c) {
if len(c.Marks) != 1 {
t.Errorf("code node should have only code mark, got %v", c.Marks)
}
}
if para.Type != "paragraph" {
t.Fatalf("expected paragraph, got %s", para.Type)
}

if len(para.Content) != 3 {
b, _ := json.MarshalIndent(adf, "", " ")
t.Fatalf("expected 3 inline nodes, got %d:\n%s", len(para.Content), b)
}

// "italic " should have em mark
if para.Content[0].Text != "italic " {
t.Errorf("expected 'italic ', got %q", para.Content[0].Text)
}
if len(para.Content[0].Marks) != 1 || para.Content[0].Marks[0].Type != "em" {
t.Errorf("expected [em] marks, got %v", para.Content[0].Marks)
}

// "code" should have ONLY code mark
if para.Content[1].Text != "code" {
t.Errorf("expected 'code', got %q", para.Content[1].Text)
}
if len(para.Content[1].Marks) != 1 || para.Content[1].Marks[0].Type != "code" {
t.Errorf("expected [code] marks only, got %v", para.Content[1].Marks)
}

// " here" should have em mark
if para.Content[2].Text != " here" {
t.Errorf("expected ' here', got %q", para.Content[2].Text)
}
if len(para.Content[2].Marks) != 1 || para.Content[2].Marks[0].Type != "em" {
t.Errorf("expected [em] marks, got %v", para.Content[2].Marks)
}
}

func TestMarkdownToADF_CodeInsideStrikethrough(t *testing.T) {
adf := MarkdownToADF("~~deleted `code` here~~")

if len(adf.Content) != 1 {
t.Fatalf("expected 1 block, got %d", len(adf.Content))
}

para := adf.Content[0]
for _, c := range para.Content {
if hasCodeMark(c) {
if len(c.Marks) != 1 {
t.Errorf("code node should have only code mark, got %v", c.Marks)
}
}
if para.Type != "paragraph" {
t.Fatalf("expected paragraph, got %s", para.Type)
}

if len(para.Content) != 3 {
b, _ := json.MarshalIndent(adf, "", " ")
t.Fatalf("expected 3 inline nodes, got %d:\n%s", len(para.Content), b)
}

// "deleted " should have strike mark
if para.Content[0].Text != "deleted " {
t.Errorf("expected 'deleted ', got %q", para.Content[0].Text)
}
if len(para.Content[0].Marks) != 1 || para.Content[0].Marks[0].Type != "strike" {
t.Errorf("expected [strike] marks, got %v", para.Content[0].Marks)
}

// "code" should have ONLY code mark
if para.Content[1].Text != "code" {
t.Errorf("expected 'code', got %q", para.Content[1].Text)
}
if len(para.Content[1].Marks) != 1 || para.Content[1].Marks[0].Type != "code" {
t.Errorf("expected [code] marks only, got %v", para.Content[1].Marks)
}

// " here" should have strike mark
if para.Content[2].Text != " here" {
t.Errorf("expected ' here', got %q", para.Content[2].Text)
}
if len(para.Content[2].Marks) != 1 || para.Content[2].Marks[0].Type != "strike" {
t.Errorf("expected [strike] marks, got %v", para.Content[2].Marks)
}
}
Comment on lines 50 to 132

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Great job on making these tests more thorough! It's a solid improvement.

I noticed that there's a lot of repetitive boilerplate code across TestMarkdownToADF_CodeInsideItalic, TestMarkdownToADF_CodeInsideStrikethrough, and the existing TestMarkdownToADF_CodeInsideBold. The new assertions in TestMarkdownToADF_BoldWithCodeAndCodeBlock also follow this pattern.

To improve maintainability and make it easier to add new test cases, I suggest refactoring these into a single table-driven test. This would centralize the test logic and make the different cases clear and concise.

Here's an example of how you could structure it:

func TestMarkdownToADF_CodeInsideFormatting(t *testing.T) {
	cases := []struct {
		name          string
		markdown      string
		expectedNodes []struct {
			text  string
			marks []string
		}
	}{
		{
			name:     "code inside bold",
			markdown: "**Bold `code` here**",
			expectedNodes: []struct {
				text  string
				marks []string
			}{
				{"Bold ", []string{"strong"}},
				{"code", []string{"code"}},
				{" here", []string{"strong"}},
			},
		},
		{
			name:     "code inside italic",
			markdown: "*italic `code` here*",
			expectedNodes: []struct {
				text  string
				marks []string
			}{
				{"italic ", []string{"em"}},
				{"code", []string{"code"}},
				{" here", []string{"em"}},
			},
		},
		{
			name:     "code inside strikethrough",
			markdown: "~~deleted `code` here~~",
			expectedNodes: []struct {
				text  string
				marks []string
			}{
				{"deleted ", []string{"strike"}},
				{"code", []string{"code"}},
				{" here", []string{"strike"}},
			},
		},
		// You could also move the case from TestMarkdownToADF_BoldWithCodeAndCodeBlock here
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			adf := MarkdownToADF(tc.markdown)

			if len(adf.Content) != 1 {
				t.Fatalf("expected 1 block, got %d", len(adf.Content))
			}

			para := adf.Content[0]
			if para.Type != "paragraph" {
				t.Fatalf("expected paragraph, got %s", para.Type)
			}

			if len(para.Content) != len(tc.expectedNodes) {
				b, _ := json.MarshalIndent(adf, "", "  ")
				t.Fatalf("expected %d inline nodes, got %d:\n%s", len(tc.expectedNodes), len(para.Content), b)
			}

			for i, expected := range tc.expectedNodes {
				actual := para.Content[i]
				if actual.Text != expected.text {
					t.Errorf("node %d: expected text %q, got %q", i, expected.text, actual.Text)
				}

				if len(actual.Marks) != len(expected.marks) {
					t.Errorf("node %d: expected %d marks, got %d (%v)", i, len(expected.marks), len(actual.Marks), actual.Marks)
					continue
				}

				// A simple check for mark types. For more robustness, you could
				// compare the slices of mark types irrespective of order.
				for j, markType := range expected.marks {
					if actual.Marks[j].Type != markType {
						t.Errorf("node %d, mark %d: expected mark type %q, got %q", i, j, markType, actual.Marks[j].Type)
					}
				}
			}
		})
	}
}


Expand All @@ -83,12 +141,38 @@ func TestMarkdownToADF_BoldWithCodeAndCodeBlock(t *testing.T) {
t.Fatalf("expected 2 blocks (paragraph + codeBlock), got %d:\n%s", len(adf.Content), b)
}

// Verify no code node has additional marks
// Verify paragraph content
para := adf.Content[0]
for _, c := range para.Content {
if hasCodeMark(c) && len(c.Marks) > 1 {
t.Errorf("code node should have only code mark, got %v", c.Marks)
}
if para.Type != "paragraph" {
t.Fatalf("expected paragraph, got %s", para.Type)
}
if len(para.Content) != 3 {
b, _ := json.MarshalIndent(para, "", " ")
t.Fatalf("expected 3 inline nodes in paragraph, got %d:\n%s", len(para.Content), b)
}

// "Migration: Falsche " should have strong mark
if para.Content[0].Text != "Migration: Falsche " {
t.Errorf("expected 'Migration: Falsche ', got %q", para.Content[0].Text)
}
if len(para.Content[0].Marks) != 1 || para.Content[0].Marks[0].Type != "strong" {
t.Errorf("expected [strong] marks, got %v", para.Content[0].Marks)
}

// "s_action" should have ONLY code mark
if para.Content[1].Text != "s_action" {
t.Errorf("expected 's_action', got %q", para.Content[1].Text)
}
if len(para.Content[1].Marks) != 1 || para.Content[1].Marks[0].Type != "code" {
t.Errorf("expected [code] marks only, got %v", para.Content[1].Marks)
}

// " korrigieren" should have strong mark
if para.Content[2].Text != " korrigieren" {
t.Errorf("expected ' korrigieren', got %q", para.Content[2].Text)
}
if len(para.Content[2].Marks) != 1 || para.Content[2].Marks[0].Type != "strong" {
t.Errorf("expected [strong] marks, got %v", para.Content[2].Marks)
}

// Verify code block is present
Expand Down
5 changes: 1 addition & 4 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
configCmd "github.com/enthus-appdev/atl-cli/internal/cmd/config"
confluenceCmd "github.com/enthus-appdev/atl-cli/internal/cmd/confluence"
issueCmd "github.com/enthus-appdev/atl-cli/internal/cmd/issue"
worklogCmd "github.com/enthus-appdev/atl-cli/internal/cmd/worklog"
"github.com/enthus-appdev/atl-cli/internal/iostreams"
)

Expand All @@ -35,13 +34,12 @@ func Execute(ios *iostreams.IOStreams, buildInfo BuildInfo) int {
func NewRootCmd(ios *iostreams.IOStreams, buildInfo BuildInfo) *cobra.Command {
cmd := &cobra.Command{
Use: "atl",
Short: "Atlassian CLI - Work with Jira, Confluence, and Tempo from the command line",
Short: "Atlassian CLI - Work with Jira and Confluence from the command line",
Long: `atl is a CLI tool for interacting with Atlassian products.

It provides commands for:
- Jira: View, create, and manage issues
- Confluence: Read and edit pages
- Tempo: Log and manage worklogs

Get started by running 'atl auth login' to authenticate with your Atlassian account.

Expand All @@ -66,7 +64,6 @@ Environment variables:
cmd.AddCommand(issueCmd.NewCmdIssue(ios))
cmd.AddCommand(boardCmd.NewCmdBoard(ios))
cmd.AddCommand(confluenceCmd.NewCmdConfluence(ios))
cmd.AddCommand(worklogCmd.NewCmdWorklog(ios))
cmd.AddCommand(configCmd.NewCmdConfig(ios))
cmd.AddCommand(newVersionCmd(ios, buildInfo))
cmd.AddCommand(newCompletionCmd(ios))
Expand Down
67 changes: 0 additions & 67 deletions internal/cmd/worklog/add.go

This file was deleted.

52 changes: 0 additions & 52 deletions internal/cmd/worklog/delete.go

This file was deleted.

56 changes: 0 additions & 56 deletions internal/cmd/worklog/edit.go

This file was deleted.

Loading