Skip to content

Commit 0a3fdde

Browse files
Add pytest test suite with coverage and CI workflow (#28)
* Add pytest test suite with coverage and CI workflow * Fix ExecutionContext tests for FetchResponse return type * Add tests for FetchResponse dataclass and headers * Fix: preserve pytest exit code with pipefail in CI * Add testing and coverage documentation to AGENTS.md, README.md, and RELEASING.md * Add tests to reach 99% coverage: default config path, polling trigger validation, JSON parse fallback * Add CI, PyPI, Python, and license badges to READMEs; add coverage badge support to CI * Add coverage badge to READMEs * Update gistID for coverage badge * Include commit hash and message in coverage report title * Add release notes step to RELEASING.md and action-error-demo sample to README * Use actual PR head commit for coverage report title instead of merge commit * Show commit message and author in coverage report title * Use GitHub username for coverage title and always show coverage table * Replace MishaKav action with custom flat coverage comment * Fix coverage comment: write markdown to file to avoid backtick escaping issues * Link commit SHA and missing line numbers in coverage comment
1 parent a32afe8 commit 0a3fdde

File tree

11 files changed

+1366
-0
lines changed

11 files changed

+1366
-0
lines changed

.github/workflows/tests.yml

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
python-version: ["3.13"]
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Set up Python ${{ matrix.python-version }}
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install -e ".[test]"
34+
35+
- name: Get commit info
36+
id: commit-info
37+
run: |
38+
if [ "${{ github.event_name }}" = "pull_request" ]; then
39+
COMMIT_SHA="${{ github.event.pull_request.head.sha }}"
40+
else
41+
COMMIT_SHA="${{ github.sha }}"
42+
fi
43+
echo "sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
44+
echo "short_sha=$(echo "$COMMIT_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT"
45+
echo "message=$(git log -1 --pretty=%s "$COMMIT_SHA")" >> "$GITHUB_OUTPUT"
46+
47+
- name: Run tests with coverage
48+
run: |
49+
set -o pipefail
50+
python -m pytest tests/ -v --tb=short \
51+
--cov=autohive_integrations_sdk \
52+
--cov-report=term-missing \
53+
--cov-report=xml:coverage.xml \
54+
| tee pytest-coverage.txt
55+
56+
- name: Build coverage comment
57+
id: coverage
58+
run: |
59+
TOTAL=$(grep '^TOTAL' pytest-coverage.txt | awk '{print $(NF-1)}')
60+
echo "total=${TOTAL}" >> "$GITHUB_OUTPUT"
61+
62+
REPO_URL="${{ github.server_url }}/${{ github.repository }}"
63+
SHA="${{ steps.commit-info.outputs.sha }}"
64+
SHORT_SHA="${{ steps.commit-info.outputs.short_sha }}"
65+
COMMIT_LINK="[\`${SHORT_SHA}\`](${REPO_URL}/commit/${SHA})"
66+
TITLE="Coverage — ${COMMIT_LINK} (${{ steps.commit-info.outputs.message }}) by @${{ github.actor }}"
67+
68+
# Convert "308, 315-320, 347" into linked line numbers
69+
linkify_lines() {
70+
local file="$1" missing="$2"
71+
if [ -z "$missing" ]; then
72+
echo ""
73+
return
74+
fi
75+
local result=""
76+
IFS=', ' read -ra PARTS <<< "$missing"
77+
for part in "${PARTS[@]}"; do
78+
[ -z "$part" ] && continue
79+
if [[ "$part" == *-* ]]; then
80+
local start="${part%-*}" end="${part#*-}"
81+
local link="[${part}](${REPO_URL}/blob/${SHA}/${file}#L${start}-L${end})"
82+
else
83+
local link="[${part}](${REPO_URL}/blob/${SHA}/${file}#L${part})"
84+
fi
85+
if [ -n "$result" ]; then
86+
result="${result}, ${link}"
87+
else
88+
result="${link}"
89+
fi
90+
done
91+
echo "$result"
92+
}
93+
94+
{
95+
echo "<!-- coverage-comment -->"
96+
echo "### ${TITLE}"
97+
echo ""
98+
echo "**Total coverage: ${TOTAL}**"
99+
echo ""
100+
echo "| File | Stmts | Miss | Cover | Missing |"
101+
echo "|------|-------|------|-------|---------|"
102+
grep '^src/' pytest-coverage.txt | while IFS= read -r line; do
103+
FILE=$(echo "$line" | awk '{print $1}')
104+
STMTS=$(echo "$line" | awk '{print $2}')
105+
MISS=$(echo "$line" | awk '{print $3}')
106+
COV=$(echo "$line" | awk '{print $4}')
107+
MISSING=$(echo "$line" | awk '{for(i=5;i<=NF;i++) printf "%s ", $i; print ""}' | xargs)
108+
LINKED=$(linkify_lines "$FILE" "$MISSING")
109+
echo "| \`${FILE}\` | ${STMTS} | ${MISS} | ${COV} | ${LINKED} |"
110+
done
111+
} > coverage-comment.md
112+
113+
- name: Post coverage comment on PR
114+
if: github.event_name == 'pull_request'
115+
uses: actions/github-script@v7
116+
with:
117+
script: |
118+
const fs = require('fs');
119+
const body = fs.readFileSync('coverage-comment.md', 'utf8');
120+
const marker = '<!-- coverage-comment -->';
121+
122+
const { data: comments } = await github.rest.issues.listComments({
123+
owner: context.repo.owner,
124+
repo: context.repo.repo,
125+
issue_number: context.issue.number,
126+
});
127+
const existing = comments.find(c => c.body.includes(marker));
128+
129+
if (existing) {
130+
await github.rest.issues.updateComment({
131+
owner: context.repo.owner,
132+
repo: context.repo.repo,
133+
comment_id: existing.id,
134+
body,
135+
});
136+
} else {
137+
await github.rest.issues.createComment({
138+
owner: context.repo.owner,
139+
repo: context.repo.repo,
140+
issue_number: context.issue.number,
141+
body,
142+
});
143+
}
144+
145+
- name: Update coverage badge
146+
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
147+
uses: schneegans/dynamic-badges-action@v1.7.0
148+
with:
149+
auth: ${{ secrets.GIST_SECRET }}
150+
gistID: e8adf35c8508876ab8ba09422ddc2535
151+
filename: coverage-badge.json
152+
label: coverage
153+
message: ${{ steps.coverage.outputs.total }}
154+
valColorRange: ${{ steps.coverage.outputs.total }}
155+
minColorRange: 50
156+
maxColorRange: 100

AGENTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@
1010
```
1111
- Always commit the regenerated docs alongside the code changes that caused them.
1212

13+
## Testing
14+
15+
- Tests live in `tests/` and use **pytest** with **pytest-asyncio** and **aioresponses**.
16+
- After any code change in `src/autohive_integrations_sdk/`, run the test suite:
17+
```
18+
python -m pytest tests/ -v
19+
```
20+
- Run with coverage to check for regressions:
21+
```
22+
python -m pytest tests/ -v --cov=autohive_integrations_sdk --cov-report=term-missing
23+
```
24+
- Add or update tests for any new or modified functionality. Aim to maintain ≥95% coverage.
25+
- CI runs automatically on PRs via GitHub Actions (`.github/workflows/tests.yml`).
26+
1327
## Releasing
1428

1529
- Follow the process in [RELEASING.md](RELEASING.md).

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Integrations SDK for Autohive
22

3+
[![Tests](https://github.com/Autohive-AI/integrations-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/Autohive-AI/integrations-sdk/actions/workflows/tests.yml)
4+
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/TheRealAgentK/e8adf35c8508876ab8ba09422ddc2535/raw/coverage-badge.json)](https://github.com/Autohive-AI/integrations-sdk/actions/workflows/tests.yml)
5+
[![PyPI version](https://img.shields.io/pypi/v/autohive-integrations-sdk)](https://pypi.org/project/autohive-integrations-sdk/)
6+
[![Python](https://img.shields.io/pypi/pyversions/autohive-integrations-sdk)](https://pypi.org/project/autohive-integrations-sdk/)
7+
[![License: MIT](https://img.shields.io/pypi/l/autohive-integrations-sdk)](https://github.com/Autohive-AI/integrations-sdk/blob/master/LICENSE)
8+
39
## Overview
410

511
This is the SDK for building integrations into Autohive's AI agent platform.
@@ -25,6 +31,26 @@ Start with the **[Building Your First Integration](docs/manual/building_your_fir
2531
|--------|-------------|
2632
| [`samples/template/`](samples/template/) | Clean starter template — copy this to begin a new integration |
2733
| [`samples/api-fetch/`](samples/api-fetch/) | Working example with unauthenticated, Basic Auth, and Bearer token API calls |
34+
| [`samples/action-error-demo/`](samples/action-error-demo/) | Demonstrates `ActionError` for expected application-level errors |
35+
36+
## Testing
37+
38+
Install test dependencies:
39+
```bash
40+
pip install -e ".[test]"
41+
```
42+
43+
Run tests:
44+
```bash
45+
python -m pytest tests/ -v
46+
```
47+
48+
Run with coverage:
49+
```bash
50+
python -m pytest tests/ -v --cov=autohive_integrations_sdk --cov-report=term-missing
51+
```
52+
53+
CI runs automatically on PRs via GitHub Actions — see [`.github/workflows/tests.yml`](.github/workflows/tests.yml).
2854

2955
## Validation & CI
3056

README_PYPI.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Integrations SDK for Autohive
22

3+
[![Tests](https://github.com/Autohive-AI/integrations-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/Autohive-AI/integrations-sdk/actions/workflows/tests.yml)
4+
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/TheRealAgentK/e8adf35c8508876ab8ba09422ddc2535/raw/coverage-badge.json)](https://github.com/Autohive-AI/integrations-sdk/actions/workflows/tests.yml)
35
[![PyPI version](https://img.shields.io/pypi/v/autohive-integrations-sdk)](https://pypi.org/project/autohive-integrations-sdk/)
46
[![Python](https://img.shields.io/pypi/pyversions/autohive-integrations-sdk)](https://pypi.org/project/autohive-integrations-sdk/)
57
[![License: MIT](https://img.shields.io/pypi/l/autohive-integrations-sdk)](https://github.com/Autohive-AI/integrations-sdk/blob/master/LICENSE)

RELEASING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
- Authors
1111
- Dependencies
1212

13+
* Update `RELEASENOTES.md` with an entry for the new version.
14+
15+
* Run the test suite and ensure all tests pass:
16+
```
17+
python -m pytest tests/ -v --cov=autohive_integrations_sdk --cov-report=term-missing
18+
```
19+
1320
* Release to PyPi:
1421
- `build` is required (`python3 -m pip install --upgrade build`)
1522
- `twine` is required (`python3 -m pip install --upgrade twine`)

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ dependencies = [
2929
"aiohttp",
3030
"jsonschema==4.17.3"
3131
]
32+
33+
[project.optional-dependencies]
34+
test = [
35+
"pytest>=8.0",
36+
"pytest-asyncio>=0.24",
37+
"aioresponses>=0.7",
38+
"pytest-cov>=6.0",
39+
]
40+
41+
[tool.pytest.ini_options]
42+
testpaths = ["tests"]
43+
asyncio_mode = "auto"
44+
3245
[project.urls]
3346
Homepage = "https://github.com/Autohive-AI/integrations-sdk"
3447
Issues = "https://github.com/Autohive-AI/integrations-sdk/issues"

tests/.gitkeep

Whitespace-only changes.

tests/conftest.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Shared fixtures for the Autohive Integrations SDK test suite."""
2+
3+
import json
4+
import pytest
5+
from pathlib import Path
6+
7+
from autohive_integrations_sdk import Integration, ExecutionContext
8+
9+
10+
@pytest.fixture
11+
def config_dict():
12+
"""Minimal config.json content with one action, one trigger, and auth fields."""
13+
return {
14+
"name": "test-integration",
15+
"version": "0.1.0",
16+
"description": "Integration used by the test suite",
17+
"auth": {
18+
"auth_type": "Custom",
19+
"fields": {
20+
"type": "object",
21+
"properties": {
22+
"api_key": {"type": "string"}
23+
},
24+
"required": ["api_key"]
25+
}
26+
},
27+
"actions": {
28+
"test_action": {
29+
"description": "A simple test action",
30+
"input_schema": {
31+
"type": "object",
32+
"properties": {
33+
"name": {"type": "string"}
34+
},
35+
"required": ["name"]
36+
},
37+
"output_schema": {
38+
"type": "object",
39+
"properties": {
40+
"greeting": {"type": "string"}
41+
},
42+
"required": ["greeting"]
43+
}
44+
}
45+
},
46+
"polling_triggers": {
47+
"test_trigger": {
48+
"description": "A simple test trigger",
49+
"polling_interval": "5m",
50+
"input_schema": {
51+
"type": "object",
52+
"properties": {
53+
"channel": {"type": "string"}
54+
},
55+
"required": ["channel"]
56+
},
57+
"output_schema": {
58+
"type": "object",
59+
"properties": {
60+
"message": {"type": "string"}
61+
},
62+
"required": ["message"]
63+
}
64+
}
65+
}
66+
}
67+
68+
69+
@pytest.fixture
70+
def tmp_config(tmp_path, config_dict):
71+
"""Write the config dict to a temporary file and return its path."""
72+
config_file = tmp_path / "config.json"
73+
config_file.write_text(json.dumps(config_dict))
74+
return config_file
75+
76+
77+
@pytest.fixture
78+
def integration(tmp_config):
79+
"""An Integration instance loaded from the temporary config."""
80+
return Integration.load(tmp_config)
81+
82+
83+
@pytest.fixture
84+
def execution_context():
85+
"""A simple ExecutionContext with a Custom API key."""
86+
return ExecutionContext(
87+
auth={"api_key": "test-key-123"},
88+
request_config={"max_retries": 1, "timeout": 1},
89+
)

0 commit comments

Comments
 (0)