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
123 changes: 123 additions & 0 deletions pkg/cli/jq_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//go:build integration

package cli

import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"time"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

// TestMCPServer_StatusToolWithJq tests the status tool with jq filter parameter
func TestMCPServer_StatusToolWithJq(t *testing.T) {
// Skip if jq is not available
if _, err := exec.LookPath("jq"); err != nil {
t.Skip("Skipping test: jq not found in PATH")
}

// Skip if the binary doesn't exist
binaryPath := "../../gh-aw"
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.")
}

// Create a temporary directory with a workflow file
tmpDir := t.TempDir()
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatalf("Failed to create workflows directory: %v", err)
}

// Create a test workflow file
workflowContent := `---
on: push
engine: copilot
---
# Test Workflow
`
workflowFile := filepath.Join(workflowsDir, "test.md")
if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

// Save current directory and change to temp directory
originalDir, _ := os.Getwd()
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

The error from os.Getwd() is being silently ignored using the blank identifier _. If os.Getwd() fails, originalDir will be an empty string, and the deferred os.Chdir(originalDir) call will fail to restore the working directory, potentially affecting other tests.

Consider handling the error:

originalDir, err := os.Getwd()
if err != nil {
    t.Fatalf("Failed to get current directory: %v", err)
}
defer os.Chdir(originalDir)
Suggested change
originalDir, _ := os.Getwd()
originalDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current directory: %v", err)
}

Copilot uses AI. Check for mistakes.
defer os.Chdir(originalDir)

// Initialize git repository in the temp directory
initCmd := exec.Command("git", "init")
initCmd.Dir = tmpDir
if err := initCmd.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// Create MCP client
client := mcp.NewClient(&mcp.Implementation{
Name: "test-client",
Version: "1.0.0",
}, nil)

// Start the MCP server as a subprocess
serverCmd := exec.Command(filepath.Join(originalDir, binaryPath), "mcp-server")
serverCmd.Dir = tmpDir
transport := &mcp.CommandTransport{Command: serverCmd}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

session, err := client.Connect(ctx, transport, nil)
if err != nil {
t.Fatalf("Failed to connect to MCP server: %v", err)
}
defer session.Close()

// Test 1: Call status tool with jq filter to get just workflow names
params := &mcp.CallToolParams{
Name: "status",
Arguments: map[string]any{
"jq": ".[].workflow",
},
}
result, err := session.CallTool(ctx, params)
if err != nil {
t.Fatalf("Failed to call status tool with jq filter: %v", err)
}

// Verify result contains the workflow name
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Accessing result.Content[0] without checking if the slice is empty could cause a panic if result.Content is empty or nil. Consider adding a bounds check before accessing the first element:

if len(result.Content) == 0 {
    t.Fatal("Expected content in result, but got empty content")
}
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
    // ... validation logic
} else {
    t.Error("Expected text content from status tool with jq filter")
}

Copilot uses AI. Check for mistakes.
if textContent.Text == "" {
t.Error("Expected non-empty text content from status tool with jq filter")
}
// The output should contain "test" (the workflow name)
t.Logf("Status tool output with jq filter: %s", textContent.Text)
} else {
t.Error("Expected text content from status tool with jq filter")
}

// Test 2: Call status tool with jq filter to count workflows
params = &mcp.CallToolParams{
Name: "status",
Arguments: map[string]any{
"jq": "length",
},
}
result, err = session.CallTool(ctx, params)
if err != nil {
t.Fatalf("Failed to call status tool with jq count filter: %v", err)
}

// Verify result contains a number
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

Accessing result.Content[0] without checking if the slice is empty could cause a panic if result.Content is empty or nil. Consider adding a bounds check before accessing the first element:

if len(result.Content) == 0 {
    t.Fatal("Expected content in result, but got empty content")
}
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
    // ... validation logic
} else {
    t.Error("Expected text content from status tool with jq count filter")
}
Suggested change
// Verify result contains a number
// Verify result contains a number
if len(result.Content) == 0 {
t.Fatal("Expected content in result, but got empty content")
}

Copilot uses AI. Check for mistakes.
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
if textContent.Text == "" {
t.Error("Expected non-empty text content from status tool with jq count filter")
}
t.Logf("Status tool count output: %s", textContent.Text)
} else {
t.Error("Expected text content from status tool with jq count filter")
}
}
172 changes: 55 additions & 117 deletions pkg/cli/jq_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
//go:build integration

package cli

import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

func TestApplyJqFilter(t *testing.T) {
Expand All @@ -26,6 +19,18 @@ func TestApplyJqFilter(t *testing.T) {
wantErr bool
validate func(t *testing.T, output string)
}{
{
name: "simple filter - identity",
jsonInput: `{"name":"test"}`,
jqFilter: ".",
wantErr: false,
validate: func(t *testing.T, output string) {
output = strings.TrimSpace(output)
if !strings.Contains(output, "test") {
t.Errorf("Expected output to contain 'test', got %q", output)
}
},
},
{
name: "simple filter - get first element",
jsonInput: `[{"name":"a"},{"name":"b"}]`,
Expand Down Expand Up @@ -59,6 +64,41 @@ func TestApplyJqFilter(t *testing.T) {
}
},
},
{
name: "filter - extract specific field",
jsonInput: `{"name":"value","id":123}`,
jqFilter: ".name",
wantErr: false,
validate: func(t *testing.T, output string) {
output = strings.TrimSpace(output)
if !strings.Contains(output, "value") {
t.Errorf("Expected output to contain 'value', got %q", output)
}
},
},
{
name: "filter - empty input",
jsonInput: `{}`,
jqFilter: ".",
wantErr: false,
validate: func(t *testing.T, output string) {
output = strings.TrimSpace(output)
if output != "{}" {
t.Errorf("Expected '{}', got %q", output)
}
},
},
{
name: "filter - array transformation",
jsonInput: `[1,2,3]`,
jqFilter: "map(. * 2)",
wantErr: false,
validate: func(t *testing.T, output string) {
if !strings.Contains(output, "2") && !strings.Contains(output, "4") && !strings.Contains(output, "6") {
t.Error("Expected transformed array output")
Comment on lines +97 to +98
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

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

The logical condition in this validation is incorrect. Using && (AND) with !strings.Contains() means the test will only fail if none of the values are present, but will pass if any one is present.

For example, if the output is [2,2,2], the test would pass because it contains "2", even though it doesn't contain "4" or "6". This doesn't properly validate the transformation map(. * 2) which should produce [2,4,6].

Consider using || (OR) instead to ensure all three expected values are present:

if !strings.Contains(output, "2") || !strings.Contains(output, "4") || !strings.Contains(output, "6") {
    t.Error("Expected transformed array output containing 2, 4, and 6")
}
Suggested change
if !strings.Contains(output, "2") && !strings.Contains(output, "4") && !strings.Contains(output, "6") {
t.Error("Expected transformed array output")
if !strings.Contains(output, "2") || !strings.Contains(output, "4") || !strings.Contains(output, "6") {
t.Error("Expected transformed array output containing 2, 4, and 6")

Copilot uses AI. Check for mistakes.
}
},
},
{
name: "invalid filter - syntax error",
jsonInput: `[{"name":"a"}]`,
Expand All @@ -73,6 +113,13 @@ func TestApplyJqFilter(t *testing.T) {
wantErr: true,
validate: nil,
},
{
name: "empty filter",
jsonInput: `{"data":"test"}`,
jqFilter: "",
wantErr: true,
validate: nil,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -106,112 +153,3 @@ func TestApplyJqFilter_JqNotAvailable(t *testing.T) {
t.Errorf("Expected 'jq not found in PATH' error, got: %v", err)
}
}

// TestMCPServer_StatusToolWithJq tests the status tool with jq filter parameter
func TestMCPServer_StatusToolWithJq(t *testing.T) {
// Skip if jq is not available
if _, err := exec.LookPath("jq"); err != nil {
t.Skip("Skipping test: jq not found in PATH")
}

// Skip if the binary doesn't exist
binaryPath := "../../gh-aw"
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.")
}

// Create a temporary directory with a workflow file
tmpDir := t.TempDir()
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
t.Fatalf("Failed to create workflows directory: %v", err)
}

// Create a test workflow file
workflowContent := `---
on: push
engine: copilot
---
# Test Workflow
`
workflowFile := filepath.Join(workflowsDir, "test.md")
if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

// Save current directory and change to temp directory
originalDir, _ := os.Getwd()
defer os.Chdir(originalDir)

// Initialize git repository in the temp directory
initCmd := exec.Command("git", "init")
initCmd.Dir = tmpDir
if err := initCmd.Run(); err != nil {
t.Fatalf("Failed to initialize git repository: %v", err)
}

// Create MCP client
client := mcp.NewClient(&mcp.Implementation{
Name: "test-client",
Version: "1.0.0",
}, nil)

// Start the MCP server as a subprocess
serverCmd := exec.Command(filepath.Join(originalDir, binaryPath), "mcp-server")
serverCmd.Dir = tmpDir
transport := &mcp.CommandTransport{Command: serverCmd}

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

session, err := client.Connect(ctx, transport, nil)
if err != nil {
t.Fatalf("Failed to connect to MCP server: %v", err)
}
defer session.Close()

// Test 1: Call status tool with jq filter to get just workflow names
params := &mcp.CallToolParams{
Name: "status",
Arguments: map[string]any{
"jq": ".[].workflow",
},
}
result, err := session.CallTool(ctx, params)
if err != nil {
t.Fatalf("Failed to call status tool with jq filter: %v", err)
}

// Verify result contains the workflow name
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
if textContent.Text == "" {
t.Error("Expected non-empty text content from status tool with jq filter")
}
// The output should contain "test" (the workflow name)
t.Logf("Status tool output with jq filter: %s", textContent.Text)
} else {
t.Error("Expected text content from status tool with jq filter")
}

// Test 2: Call status tool with jq filter to count workflows
params = &mcp.CallToolParams{
Name: "status",
Arguments: map[string]any{
"jq": "length",
},
}
result, err = session.CallTool(ctx, params)
if err != nil {
t.Fatalf("Failed to call status tool with jq count filter: %v", err)
}

// Verify result contains a number
if textContent, ok := result.Content[0].(*mcp.TextContent); ok {
if textContent.Text == "" {
t.Error("Expected non-empty text content from status tool with jq count filter")
}
t.Logf("Status tool count output: %s", textContent.Text)
} else {
t.Error("Expected text content from status tool with jq count filter")
}
}
Loading