mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 12:18:59 +00:00
test(contracts): complete P1 contract tests with schemas and policy
- Add config.get, skills.list WS contract tests - Add /v1/providers HTTP contract test - Add JSON schemas for ws_connect, ws_chat_send, http_chat_completions - Document breaking change policy
This commit is contained in:
1 parent
cc21384ff4
commit
6e7e15cf4a
7 files changed
+211
No files matched your search
@@ -0,0 +1,61 @@
|
||||
# Contract Breaking Change Policy
|
||||
|
||||
## What is a Breaking Change?
|
||||
|
||||
A breaking change is any modification to the API response schema that could cause existing clients to fail:
|
||||
|
||||
| Change Type | Breaking? | Example |
|
||||
|------------|-----------|---------|
|
||||
| Remove required field | YES | `{id, name}` → `{id}` |
|
||||
| Change field type | YES | `"count": 5` → `"count": "5"` |
|
||||
| Rename field | YES | `user_id` → `userId` |
|
||||
| Add new required field | YES | Client must send new field |
|
||||
| Add optional field | NO | Backward compatible |
|
||||
| Add new enum value | MAYBE | If client validates strictly |
|
||||
|
||||
## Contract Test Categories
|
||||
|
||||
### P0 - Critical (WS Core)
|
||||
- `connect` - Session establishment
|
||||
- `chat.send` - Message handling
|
||||
|
||||
### P1 - High (Data APIs)
|
||||
- `agents.list`, `sessions.list`, `skills.list`
|
||||
- `/v1/chat/completions` - OpenAI compatibility
|
||||
- `/v1/agents` - Agent management
|
||||
|
||||
### P2 - Medium (Config/Admin)
|
||||
- `config.get`, `config.apply`
|
||||
- `/v1/providers`
|
||||
|
||||
## Breaking Change Process
|
||||
|
||||
1. **Detect**: Contract test fails in CI
|
||||
2. **Evaluate**: Is this intentional or accidental?
|
||||
3. **If Intentional**:
|
||||
- Bump protocol version in `pkg/protocol/version.go`
|
||||
- Update JSON schemas in `tests/contracts/schemas/`
|
||||
- Update contract tests
|
||||
- Document in CHANGELOG.md
|
||||
4. **If Accidental**: Fix the regression
|
||||
|
||||
## Running Contract Tests
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export CONTRACT_TEST_WS_URL="ws://localhost:8080/ws"
|
||||
export CONTRACT_TEST_HTTP_URL="http://localhost:8080"
|
||||
export CONTRACT_TEST_TOKEN="your-test-token"
|
||||
|
||||
# Run all contract tests
|
||||
go test -tags integration ./tests/contracts/...
|
||||
```
|
||||
|
||||
## Schema Files
|
||||
|
||||
JSON Schema definitions in `tests/contracts/schemas/`:
|
||||
- `ws_connect.json` - WS connect response
|
||||
- `ws_chat_send.json` - WS chat.send response
|
||||
- `http_chat_completions.json` - OpenAI-compatible response
|
||||
|
||||
These schemas serve as documentation and can be used for automated validation.
|
||||
@@ -0,0 +1,26 @@
|
||||
//go:build integration
|
||||
|
||||
package http_api
|
||||
|
||||
import "testing"
|
||||
|
||||
// CONTRACT: /v1/providers response MUST include providers array.
|
||||
func TestContract_HTTP_ProvidersList(t *testing.T) {
|
||||
baseURL, token := getTestServer(t)
|
||||
client := newHTTPClient(baseURL, token)
|
||||
|
||||
resp := client.get(t, "/v1/providers")
|
||||
|
||||
assertField(t, resp, "providers", "array")
|
||||
|
||||
providers, ok := resp["providers"].([]any)
|
||||
if !ok || len(providers) == 0 {
|
||||
t.Log("No providers - skipping field checks")
|
||||
return
|
||||
}
|
||||
|
||||
provider := providers[0].(map[string]any)
|
||||
assertField(t, provider, "id", "string")
|
||||
assertField(t, provider, "name", "string")
|
||||
assertField(t, provider, "type", "string")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "POST /v1/chat/completions response (OpenAI-compatible)",
|
||||
"type": "object",
|
||||
"required": ["id", "object", "created", "model", "choices"],
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"object": { "type": "string", "const": "chat.completion" },
|
||||
"created": { "type": "number" },
|
||||
"model": { "type": "string" },
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["index", "message", "finish_reason"],
|
||||
"properties": {
|
||||
"index": { "type": "number" },
|
||||
"message": {
|
||||
"type": "object",
|
||||
"required": ["role", "content"],
|
||||
"properties": {
|
||||
"role": { "type": "string" },
|
||||
"content": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"finish_reason": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_tokens": { "type": "number" },
|
||||
"completion_tokens": { "type": "number" },
|
||||
"total_tokens": { "type": "number" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WS chat.send response",
|
||||
"type": "object",
|
||||
"required": ["message_id", "content", "role"],
|
||||
"properties": {
|
||||
"message_id": { "type": "string" },
|
||||
"content": { "type": "string" },
|
||||
"role": { "type": "string", "enum": ["assistant", "user", "system"] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "WS connect response",
|
||||
"type": "object",
|
||||
"required": ["protocol", "role", "user_id", "tenant_id", "is_owner", "server"],
|
||||
"properties": {
|
||||
"protocol": { "type": "number" },
|
||||
"role": { "type": "string", "enum": ["admin", "operator", "viewer"] },
|
||||
"user_id": { "type": "string" },
|
||||
"tenant_id": { "type": "string" },
|
||||
"is_owner": { "type": "boolean" },
|
||||
"server": {
|
||||
"type": "object",
|
||||
"required": ["name", "version"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "const": "goclaw" },
|
||||
"version": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build integration
|
||||
|
||||
package ws_methods
|
||||
|
||||
import "testing"
|
||||
|
||||
// CONTRACT: config.get response MUST include config, hash, path.
|
||||
// Requires owner + master scope.
|
||||
func TestContract_WS_ConfigGet(t *testing.T) {
|
||||
wsURL, _ := getTestServer(t)
|
||||
client, _ := connect(t, wsURL, nil)
|
||||
|
||||
resp := client.send(t, "config.get", map[string]any{})
|
||||
|
||||
// Required fields
|
||||
assertField(t, resp, "config", "object")
|
||||
assertField(t, resp, "hash", "string")
|
||||
assertField(t, resp, "path", "string")
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build integration
|
||||
|
||||
package ws_methods
|
||||
|
||||
import "testing"
|
||||
|
||||
// CONTRACT: skills.list response MUST include skills array.
|
||||
func TestContract_WS_SkillsList(t *testing.T) {
|
||||
wsURL, _ := getTestServer(t)
|
||||
client, _ := connect(t, wsURL, nil)
|
||||
|
||||
resp := client.send(t, "skills.list", map[string]any{})
|
||||
|
||||
// Response must have skills array
|
||||
assertField(t, resp, "skills", "array")
|
||||
|
||||
// If skills exist, verify each has required fields
|
||||
skills, ok := resp["skills"].([]any)
|
||||
if !ok || len(skills) == 0 {
|
||||
t.Log("No skills returned - skipping field checks")
|
||||
return
|
||||
}
|
||||
|
||||
skill, ok := skills[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Error("CONTRACT VIOLATION: skills[0] is not an object")
|
||||
return
|
||||
}
|
||||
|
||||
// Required skill fields
|
||||
assertField(t, skill, "name", "string")
|
||||
assertField(t, skill, "description", "string")
|
||||
}
|
||||
Reference in new issue
Block a user