Skip to content
Open
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
35 changes: 35 additions & 0 deletions .github/workflows/mcp-eval-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: MCP Eval Tests

on:
push:
branches: [ main ]
pull_request:
paths:
- 'mcp-eval/**'
- .github/workflows/mcp-eval-tests.yml

jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: mcp-eval
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: 'mcp-eval/.node-version'
cache: 'yarn'
cache-dependency-path: 'mcp-eval/yarn.lock'

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Run linting (prettier)
run: yarn lint

- name: Run tests
run: yarn test
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<img src="https://img.shields.io/github/license/OpsLevel/opslevel-mcp.svg" alt="License" /></a>
<a href="https://GitHub.com/OpsLevel/opslevel-mcp/releases/">
<img src="https://img.shields.io/github/v/release/OpsLevel/opslevel-mcp" alt="Release" /></a>
<a href="https://github.com/OpsLevel/opslevel-mcp/actions/workflows/tests.yml">
<img src="https://github.com/OpsLevel/opslevel-mcp/actions/workflows/tests.yml/badge.svg" alt="Tests" /></a>
<a href="https://github.com/OpsLevel/opslevel-mcp/actions/workflows/mcp-eval-tests.yml">
<img src="https://github.com/OpsLevel/opslevel-mcp/actions/workflows/mcp-eval-tests.yml/badge.svg" alt="MCP Eval Tests" /></a>
<a href="https://masterminds.github.io/stability/active.html">
<img src="https://masterminds.github.io/stability/active.svg" alt="Stability: Active" /></a>
<a href="https://github.com/OpsLevel/opslevel-mcp/graphs/contributors">
Expand Down Expand Up @@ -201,3 +205,63 @@ If you didn't install the binary directly and instead pulled the docker image yo
"public.ecr.aws/opslevel/mcp:latest"
],
```

# Development

## Testing

This repository uses a comprehensive test suite to ensure code quality and functionality.

### Running Tests

#### Go Tests

The Go codebase (main MCP server) includes unit tests for core functionality:

```bash
# Run all Go tests
cd src
go test ./...

# Run tests with coverage
go test -race -coverprofile=coverage.txt -covermode=atomic ./...

# Run tests with verbose output
go test -v ./...
```

#### JavaScript Tests

The `mcp-eval` evaluation framework includes Jest-based tests:

```bash
# Run all JavaScript tests
cd mcp-eval
npm test

# Run tests in watch mode
npm test -- --watch
```

#### Using Task

The repository uses [Task](https://taskfile.dev/) as a task runner. To run the full CI pipeline locally:

```bash
# Run linting and tests
task ci

# Run only tests
task test

# Run only linting
task lint
```

### Continuous Integration

All tests are automatically run on:
- Pull requests to the main branch
- Pushes to the main branch

The CI pipeline runs linting, formatting checks, and the complete test suite to ensure code quality.
218 changes: 218 additions & 0 deletions src/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package cmd

import (
"encoding/json"
"testing"

"github.com/opslevel/opslevel-go/v2025"
)

func TestNewToolResult(t *testing.T) {
// Test successful result with simple data
data := map[string]string{"key": "value"}
result, err := newToolResult(data, nil)

if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if result == nil {
t.Fatal("Expected result to not be nil")
}

// Test with error
testErr := opslevel.NewGQLError(nil, "test error")
result, err = newToolResult(nil, testErr)

if err != nil {
t.Errorf("Expected no error from newToolResult, got %v", err)
}

if result == nil {
t.Fatal("Expected result to not be nil")
}
}

func TestSerializedComponentStructure(t *testing.T) {
// Test that serializedComponent can be instantiated and marshaled
component := serializedComponent{
Id: "test-id",
Framework: "Django",
Language: "Python",
Name: "test-service",
Owner: "test-team",
Url: "https://example.com",
Level: serializedLevel{
Alias: "Gold",
Index: 2,
},
Lifecycle: serializedLifecycle{
Alias: "Production",
Index: 3,
},
Tier: serializedTier{
Alias: "Tier 1",
Index: 0,
},
}

// Marshal to JSON to verify structure
data, err := json.Marshal(component)
if err != nil {
t.Errorf("Failed to marshal component: %v", err)
}

// Unmarshal to verify round-trip
var decoded serializedComponent
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Errorf("Failed to unmarshal component: %v", err)
}

if decoded.Name != "test-service" {
t.Errorf("Expected Name 'test-service', got '%s'", decoded.Name)
}

if decoded.Language != "Python" {
t.Errorf("Expected Language 'Python', got '%s'", decoded.Language)
}
}

func TestSerializedCheckStructure(t *testing.T) {
// Test that serializedCheck can be instantiated and marshaled
check := serializedCheck{
Id: "check-id",
Name: "test-check",
Owner: "test-team",
Description: "Test description",
Notes: "Test notes",
Enabled: true,
Type: "manual",
Level: serializedLevel{
Alias: "Silver",
Index: 1,
},
Category: "Security",
}

data, err := json.Marshal(check)
if err != nil {
t.Errorf("Failed to marshal check: %v", err)
}

var decoded serializedCheck
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Errorf("Failed to unmarshal check: %v", err)
}

if decoded.Name != "test-check" {
t.Errorf("Expected Name 'test-check', got '%s'", decoded.Name)
}

if !decoded.Enabled {
t.Error("Expected Enabled to be true")
}
}

func TestSerializedInfrastructureResourceStructure(t *testing.T) {
// Test that serializedInfrastructureResource can be instantiated
resource := serializedInfrastructureResource{
Id: "infra-id",
Name: "test-db",
Owner: "test-team",
Aliases: []string{"alias1", "alias2"},
Schema: "postgresql",
ProviderType: "aws",
}

data, err := json.Marshal(resource)
if err != nil {
t.Errorf("Failed to marshal infrastructure resource: %v", err)
}

var decoded serializedInfrastructureResource
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Errorf("Failed to unmarshal infrastructure resource: %v", err)
}

if decoded.Name != "test-db" {
t.Errorf("Expected Name 'test-db', got '%s'", decoded.Name)
}

if len(decoded.Aliases) != 2 {
t.Errorf("Expected 2 aliases, got %d", len(decoded.Aliases))
}
}

func TestAllAccountMetadataStrings(t *testing.T) {
metadataTypes := AllAccountMetadataStrings()

if len(metadataTypes) == 0 {
t.Error("Expected at least one metadata type, got none")
}

// Verify expected types are present
expectedTypes := []string{"lifecycles", "levels", "tiers", "componentTypes"}
for _, expected := range expectedTypes {
found := false
for _, actual := range metadataTypes {
if actual == expected {
found = true
break
}
}
if !found {
t.Errorf("Expected metadata type '%s' not found", expected)
}
}
}

func TestComponentFilterStructure(t *testing.T) {
// Test simple filter
filter := componentFilter{
Key: "name",
Type: "equals",
Arg: "service-name",
}

data, err := json.Marshal(filter)
if err != nil {
t.Errorf("Failed to marshal filter: %v", err)
}

var decoded componentFilter
err = json.Unmarshal(data, &decoded)
if err != nil {
t.Errorf("Failed to unmarshal filter: %v", err)
}

if decoded.Key != "name" {
t.Errorf("Expected Key 'name', got '%s'", decoded.Key)
}

// Test composite filter
compositeFilter := componentFilter{
Connective: "and",
Predicates: []componentFilter{
{Key: "language", Type: "equals", Arg: "Python"},
{Key: "owner_id", Type: "equals", Arg: "team-123"},
},
}

data, err = json.Marshal(compositeFilter)
if err != nil {
t.Errorf("Failed to marshal composite filter: %v", err)
}

var decodedComposite componentFilter
err = json.Unmarshal(data, &decodedComposite)
if err != nil {
t.Errorf("Failed to unmarshal composite filter: %v", err)
}

if len(decodedComposite.Predicates) != 2 {
t.Errorf("Expected 2 predicates, got %d", len(decodedComposite.Predicates))
}
}
Loading
Loading