Skip to content

Commit 55632e7

Browse files
authored
integration test suite (#91)
2 parents 210db6f + 92529c5 commit 55632e7

File tree

7 files changed

+251
-0
lines changed

7 files changed

+251
-0
lines changed

.github/workflows/integration.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: "Integration Tests"
2+
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
models: read
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
integration:
19+
runs-on: ubuntu-latest
20+
env:
21+
GOPROXY: https://proxy.golang.org/,direct
22+
GOPRIVATE: ""
23+
GONOPROXY: ""
24+
GONOSUMDB: github.com/github/*
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Setup Go
28+
uses: actions/setup-go@v5
29+
with:
30+
go-version-file: 'go.mod'
31+
- name: Run integration tests
32+
run: make integration
33+
env:
34+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/gh-models
22
/gh-models.exe
3+
/gh-models-test
34
/gh-models-darwin-*
45
/gh-models-linux-*
56
/gh-models-windows-*
67
/gh-models-android-*
8+
9+
# Integration test dependencies
10+
integration/go.sum

DEV.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ make vet # to find suspicious constructs
3434
make tidy # to keep dependencies up-to-date
3535
```
3636

37+
### Integration Tests
38+
39+
In addition to unit tests, we have integration tests that use the compiled binary to test against live endpoints:
40+
41+
```shell
42+
# Build the binary first
43+
make build
44+
45+
# Run integration tests
46+
cd integration
47+
go test -v
48+
```
49+
50+
Integration tests are located in the `integration/` directory and automatically skip tests requiring authentication when no GitHub token is available. See `integration/README.md` for more details.
51+
3752
## Releasing
3853

3954
When upgrading or installing the extension using `gh extension upgrade github/gh-models` or

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
check: fmt vet tidy test
22
.PHONY: check
33

4+
build:
5+
@echo "==> building gh-models binary <=="
6+
script/build
7+
.PHONY: build
8+
9+
integration: build
10+
@echo "==> running integration tests <=="
11+
cd integration && go mod tidy && go test -v -timeout=5m
12+
.PHONY: integration
13+
414
fmt:
515
@echo "==> running Go format <=="
616
gofmt -s -l -w .

integration/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Integration Tests
2+
3+
This directory contains integration tests for the `gh-models` CLI extension. These tests are separate from the unit tests and use the compiled binary to test actual functionality.
4+
5+
## Overview
6+
7+
The integration tests:
8+
- Use the compiled `gh-models` binary (not mocked clients)
9+
- Test basic functionality of each command (`list`, `run`, `view`, `eval`)
10+
- Are designed to work with or without GitHub authentication
11+
- Skip tests requiring live endpoints when authentication is unavailable
12+
- Keep assertions minimal to avoid brittleness
13+
14+
## Running the Tests
15+
16+
### Prerequisites
17+
18+
1. Build the `gh-models` binary:
19+
```bash
20+
cd ..
21+
script/build
22+
```
23+
24+
2. (Optional) Authenticate with GitHub CLI for full testing:
25+
```bash
26+
gh auth login
27+
```
28+
29+
### Running Locally
30+
31+
From the integration directory:
32+
```bash
33+
go test -v
34+
```
35+
36+
Without authentication, some tests will be skipped:
37+
```
38+
=== RUN TestIntegrationHelp
39+
--- PASS: TestIntegrationHelp (0.05s)
40+
=== RUN TestIntegrationList
41+
integration_test.go:90: Skipping integration test - no GitHub authentication available
42+
--- SKIP: TestIntegrationList (0.04s)
43+
```
44+
45+
With authentication, all tests should run and test live endpoints.
46+
47+
## CI/CD
48+
49+
The integration tests run automatically on pushes to `main` via the GitHub Actions workflow `.github/workflows/integration.yml`.
50+
51+
The workflow:
52+
1. Builds the binary
53+
2. Runs tests without authentication (tests basic functionality)
54+
3. On manual dispatch, can also run with authentication for full testing
55+
56+
## Test Structure
57+
58+
Each test follows this pattern:
59+
- Check for binary existence (skip if not built)
60+
- Check for authentication (skip live endpoint tests if unavailable)
61+
- Execute the binary with specific arguments
62+
- Verify basic output format and success/failure
63+
64+
Tests are intentionally simple and focus on:
65+
- Commands execute without errors
66+
- Help text is present and correctly formatted
67+
- Basic output format is as expected
68+
- Authentication requirements are respected
69+
70+
## Adding New Tests
71+
72+
When adding new commands or features:
73+
1. Add a corresponding integration test
74+
2. Follow the existing pattern of checking authentication
75+
3. Keep assertions minimal but meaningful
76+
4. Ensure tests work both with and without authentication

integration/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/github/gh-models/integration
2+
3+
go 1.22
4+
5+
require github.com/stretchr/testify v1.10.0
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
9+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

integration/integration_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package integration
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
const (
15+
binaryName = "gh-models-test"
16+
timeoutDuration = 30 * time.Second
17+
)
18+
19+
// getBinaryPath returns the path to the compiled gh-models binary
20+
func getBinaryPath(t *testing.T) string {
21+
wd, err := os.Getwd()
22+
require.NoError(t, err)
23+
24+
// Binary should be in the parent directory
25+
binaryPath := filepath.Join(filepath.Dir(wd), binaryName)
26+
27+
// Check if binary exists
28+
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
29+
t.Skipf("Binary %s not found. Run 'script/build' first.", binaryPath)
30+
}
31+
32+
return binaryPath
33+
}
34+
35+
// runCommand executes the gh-models binary with given arguments
36+
func runCommand(t *testing.T, args ...string) (stdout, stderr string, err error) {
37+
binaryPath := getBinaryPath(t)
38+
39+
cmd := exec.Command(binaryPath, args...)
40+
cmd.Env = os.Environ()
41+
42+
// Set timeout
43+
done := make(chan error, 1)
44+
var stdoutBytes, stderrBytes []byte
45+
46+
go func() {
47+
stdoutBytes, err = cmd.Output()
48+
if err != nil {
49+
if exitError, ok := err.(*exec.ExitError); ok {
50+
stderrBytes = exitError.Stderr
51+
}
52+
}
53+
done <- err
54+
}()
55+
56+
select {
57+
case err = <-done:
58+
return string(stdoutBytes), string(stderrBytes), err
59+
case <-time.After(timeoutDuration):
60+
if cmd.Process != nil {
61+
cmd.Process.Kill()
62+
}
63+
t.Fatalf("Command timed out after %v", timeoutDuration)
64+
return "", "", nil
65+
}
66+
}
67+
68+
func TestList(t *testing.T) {
69+
stdout, stderr, err := runCommand(t, "list")
70+
if err != nil {
71+
t.Logf("List command failed. stdout: %s, stderr: %s", stdout, stderr)
72+
// If the command fails due to auth issues, skip the test
73+
if strings.Contains(stderr, "authentication") || strings.Contains(stderr, "token") {
74+
t.Skip("Skipping - authentication issue")
75+
}
76+
require.NoError(t, err, "List command should succeed with valid auth")
77+
}
78+
79+
// Basic verification that list command produces expected output format
80+
require.NotEmpty(t, stdout, "List should produce output")
81+
// Should contain some indication of models or table headers
82+
lowerOut := strings.ToLower(stdout)
83+
hasExpectedContent := strings.Contains(lowerOut, "openai/gpt-4.1")
84+
require.True(t, hasExpectedContent, "List output should contain model information")
85+
}
86+
87+
// TestRun tests the run command with a simple prompt
88+
// This test is more limited since it requires actual model inference
89+
func TestRun(t *testing.T) {
90+
stdout, _, err := runCommand(t, "run", "openai/gpt-4.1-nano", "say 'pain' in french")
91+
require.NoError(t, err, "Run should work")
92+
require.Contains(t, strings.ToLower(stdout), "pain")
93+
}
94+
95+
// TestIntegrationRunWithOrg tests the run command with --org flag
96+
func TestRunWithOrg(t *testing.T) {
97+
// Test run command with --org flag (using help to avoid expensive API calls)
98+
stdout, _, err := runCommand(t, "run", "openai/gpt-4.1-nano", "say 'pain' in french", "--org", "github")
99+
require.NoError(t, err, "Run should work")
100+
require.Contains(t, strings.ToLower(stdout), "pain")
101+
}

0 commit comments

Comments
 (0)