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
54 changes: 54 additions & 0 deletions .github/workflows/contract-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Contract tests

# Issue #37 phase 3 (#47): enforce, on every PR, that the CLI's API calls and
# the Python SDK's consumed models stay aligned with the website-mirrored
# public OpenAPI contract. The cli/python *-publish workflows only run on
# release tags, so without this the contract guards would not gate PRs.

on:
pull_request:
paths:
- "docs/openapi/**"
- "packages/cli/src/**"
- "packages/cli/test/openapi-contract.test.mjs"
- "packages/python-sdk/qveris/**"
- "packages/python-sdk/tests/test_openapi_contract.py"
- ".github/workflows/contract-tests.yml"
push:
branches:
- main
paths:
- "docs/openapi/**"
- "packages/cli/src/**"
- "packages/cli/test/openapi-contract.test.mjs"
- "packages/python-sdk/qveris/**"
- "packages/python-sdk/tests/test_openapi_contract.py"
- ".github/workflows/contract-tests.yml"

permissions:
contents: read

jobs:
cli-contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: CLI ↔ OpenAPI contract test
working-directory: packages/cli
run: node --test test/openapi-contract.test.mjs

python-contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Python SDK ↔ generated-contract test
working-directory: packages/python-sdk
run: |
python -m pip install --quiet pytest "pydantic>=2.0.0"
PYTHONPATH=. python -m pytest tests/test_openapi_contract.py -q
69 changes: 69 additions & 0 deletions .github/workflows/openapi-types.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: OpenAPI generated types

# Issue #37 phase 2: the checked-in generated artifacts must be reproducible
# from docs/openapi/qveris-public-api.openapi.json. Regenerate with the pinned
# generators and fail if the working tree differs (contract / generator drift).

on:
pull_request:
paths:
- "docs/openapi/**"
- "packages/mcp/src/generated/**"
- "packages/python-sdk/qveris/generated/**"
- "packages/mcp/package.json"
- "packages/python-sdk/pyproject.toml"
- ".github/workflows/openapi-types.yml"
push:
branches:
- main
paths:
- "docs/openapi/**"
- "packages/mcp/src/generated/**"
- "packages/python-sdk/qveris/generated/**"
- "packages/mcp/package.json"
- "packages/python-sdk/pyproject.toml"
- ".github/workflows/openapi-types.yml"

permissions:
contents: read

jobs:
regen-and-diff:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Regenerate MCP TypeScript types
run: npx --yes openapi-typescript@7.4.4 docs/openapi/qveris-public-api.openapi.json -o packages/mcp/src/generated/openapi.d.ts

- name: Regenerate Python SDK models
run: |
python -m pip install --quiet "datamodel-code-generator==0.26.3"
python -m datamodel_code_generator \
--input docs/openapi/qveris-public-api.openapi.json \
--input-file-type openapi \
--output packages/python-sdk/qveris/generated/openapi_models.py \
--output-model-type pydantic_v2.BaseModel \
--target-python-version 3.8 \
--use-schema-description \
--disable-timestamp

- name: Fail on drift
run: |
if ! git diff --exit-code -- \
packages/mcp/src/generated/openapi.d.ts \
packages/python-sdk/qveris/generated/openapi_models.py; then
echo "::error::Generated OpenAPI artifacts are out of date. Run 'npm --prefix packages/mcp run gen:openapi' and regenerate the Python models, then commit."
exit 1
fi
46 changes: 46 additions & 0 deletions packages/cli/test/openapi-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Issue #37 phase 3 (#47): CLI is JavaScript/MJS, so before any type
// generation it gets contract tests that assert every endpoint + method the
// CLI actually calls exists in the website-mirrored public OpenAPI spec.
// This catches CLI/contract drift without introducing a generator.

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import test from "node:test";

const here = path.dirname(fileURLToPath(import.meta.url));
const specPath = path.resolve(
here,
"../../../docs/openapi/qveris-public-api.openapi.json",
);

const spec = JSON.parse(readFileSync(specPath, "utf8"));

// Endpoints the CLI calls today, kept in sync with packages/cli/src/client/api.mjs.
// (path, method) — method lowercased to match OpenAPI operation keys.
const CLI_OPERATIONS = [
["/search", "post"],
["/tools/by-ids", "post"],
["/tools/execute", "post"],
["/auth/credits", "get"],
["/auth/usage/history/v2", "get"],
["/auth/credits/ledger", "get"],
];

test("OpenAPI spec is structurally usable", () => {
assert.equal(typeof spec, "object");
assert.ok(spec.info && typeof spec.info.version === "string", "info.version present");
assert.ok(spec.paths && typeof spec.paths === "object", "paths present");
});

for (const [p, method] of CLI_OPERATIONS) {
test(`contract covers CLI call ${method.toUpperCase()} ${p}`, () => {
const item = spec.paths[p];
assert.ok(item, `path ${p} missing from public OpenAPI contract`);
assert.ok(
item[method],
`method ${method.toUpperCase()} for ${p} missing — CLI calls it but the contract does not declare it`,
);
});
}
12 changes: 7 additions & 5 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@qverisai/mcp",
"version": "0.6.0",
"description": "QVeris MCP server for agent tool discovery, inspection, and calling",
"version": "0.6.0",
"description": "QVeris MCP server for agent tool discovery, inspection, and calling",
"keywords": [
"qveris",
"mcp",
Expand All @@ -14,12 +14,12 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/QVerisAI/qveris-agent-toolkit",
"url": "https://github.com/QVerisAI/qveris-agent-toolkit",
"directory": "packages/mcp"
},
"homepage": "https://qveris.ai",
"bugs": {
"url": "https://github.com/QVerisAI/qveris-agent-toolkit/issues"
"url": "https://github.com/QVerisAI/qveris-agent-toolkit/issues"
},
"type": "module",
"main": "dist/index.js",
Expand All @@ -38,7 +38,8 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"prepublishOnly": "npm run build",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"gen:openapi": "openapi-typescript ../../docs/openapi/qveris-public-api.openapi.json -o src/generated/openapi.d.ts && npx prettier --write src/generated/openapi.d.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
Expand All @@ -47,6 +48,7 @@
"devDependencies": {
"@types/node": "^22.10.1",
"@types/uuid": "^10.0.0",
"openapi-typescript": "7.4.4",
"typescript": "^5.7.2",
"vitest": "^2.1.0"
},
Expand Down
Loading
Loading