609 lines
22 KiB
Rust
609 lines
22 KiB
Rust
// Integration test: Agent Memory with API Platform Engineer role requirements
|
|
// Tests contract-first design per agency-agents/engineering/engineering-api-platform-engineer.md
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::{json, Value};
|
|
|
|
// Test constants aligned with API Platform Engineer role
|
|
const API_VERSION: &str = "v1";
|
|
const PROJECT_ID: &str = "poimen";
|
|
const TEST_AGENT_ID: &str = "api-platform-engineer";
|
|
const API_PLATFORM_ENGINEER_ROLE: &str = "api-platform-engineer";
|
|
|
|
// API Platform Engineer role prompt templates
|
|
const CONTRACT_FIRST_PROMPT: &str = r#"
|
|
You are an API Platform Engineer designing a contract-first API.
|
|
|
|
Task: Review the following API specification for:
|
|
1. Naming consistency (pick snake_case or camelCase and never waver)
|
|
2. Backward compatibility (no breaking changes without versioning)
|
|
3. Error responses (consistent structure, stable codes, correct HTTP status semantics)
|
|
4. Rate limiting (communicated, not just enforced)
|
|
5. Documentation (SDKs and docs generated from spec, never drift)
|
|
|
|
Specification:
|
|
{{spec}}
|
|
|
|
Output JSON with:
|
|
{
|
|
"contract_valid": boolean,
|
|
"breaking_changes": [string],
|
|
"naming_inconsistencies": [string],
|
|
"error_issues": [string],
|
|
"rate_limit_issues": [string],
|
|
"recommendations": [string]
|
|
}
|
|
"#;
|
|
|
|
const BACKWARD_COMPATIBILITY_PROMPT: &str = r#"
|
|
You are an API versioning expert.
|
|
|
|
Analyze the proposed change:
|
|
{{change}}
|
|
|
|
Determine:
|
|
1. Is this a breaking change?
|
|
2. Does it require a new version?
|
|
3. What's the migration path?
|
|
4. What deprecation runway is needed?
|
|
|
|
Output JSON with:
|
|
{
|
|
"breaking": boolean,
|
|
"requires_new_version": boolean,
|
|
"migration_path": string,
|
|
"deprecation_runway_days": number,
|
|
"is_safe_additive": boolean
|
|
}
|
|
"#;
|
|
|
|
const SDK_GENERATION_PROMPT: &str = r#"
|
|
You are an SDK generation specialist.
|
|
|
|
Given this OpenAPI spec:
|
|
{{spec}}
|
|
|
|
Generate SDK requirements for:
|
|
1. Language: {{language}}
|
|
2. Idiomatic patterns for that language
|
|
3. Error handling
|
|
4. Retry logic and idempotency
|
|
5. Type safety
|
|
|
|
Output JSON with:
|
|
{
|
|
"sdk_structure": object,
|
|
"error_handling": string,
|
|
"idempotency_strategy": string,
|
|
"type_safety_level": string,
|
|
"generated_package_version": string
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
fn test_contract_first_api_specification() {
|
|
// Contract-first principle: OpenAPI spec is source of truth
|
|
let api_spec = json!({
|
|
"openapi": "3.0.0",
|
|
"info": {
|
|
"title": "Poimen Agent Memory API",
|
|
"version": API_VERSION,
|
|
"description": "Agent memory with role-to-prompt mapping"
|
|
},
|
|
"paths": {
|
|
"/memory/agents/{project_id}/prompts": {
|
|
"post": {
|
|
"operationId": "createPrompt",
|
|
"parameters": [
|
|
{
|
|
"name": "project_id",
|
|
"in": "path",
|
|
"required": true,
|
|
"schema": { "type": "string" }
|
|
}
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["name", "template", "task_category"],
|
|
"properties": {
|
|
"name": { "type": "string", "minLength": 1 },
|
|
"template": { "type": "string", "description": "Prompt template with {{placeholders}}" },
|
|
"target_model": { "type": "string", "example": "ornith:35b" },
|
|
"task_category": { "type": "string", "enum": ["extraction", "reasoning", "summarization", "validation"] },
|
|
"tags": { "type": "array", "items": { "type": "string" } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"201": {
|
|
"description": "Prompt created",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": { "$ref": "#/components/schemas/Prompt" }
|
|
}
|
|
}
|
|
},
|
|
"400": { "$ref": "#/components/responses/BadRequest" },
|
|
"429": { "$ref": "#/components/responses/RateLimited" }
|
|
}
|
|
}
|
|
},
|
|
"/memory/agents/{project_id}/roles": {
|
|
"post": {
|
|
"operationId": "mapRoleToPrompt",
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["role_name", "prompt_id"],
|
|
"properties": {
|
|
"role_name": { "type": "string", "minLength": 1 },
|
|
"prompt_id": { "type": "string", "format": "uuid" },
|
|
"priority": { "type": "integer", "default": 0 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Mapping created" },
|
|
"400": { "$ref": "#/components/responses/BadRequest" }
|
|
}
|
|
}
|
|
},
|
|
"/memory/agents/{project_id}/roles/{role_name}/prompts": {
|
|
"get": {
|
|
"operationId": "getRolePrompts",
|
|
"responses": {
|
|
"200": { "description": "List of prompts for role" },
|
|
"404": { "$ref": "#/components/responses/NotFound" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"components": {
|
|
"schemas": {
|
|
"Prompt": {
|
|
"type": "object",
|
|
"required": ["id", "name", "template", "task_category"],
|
|
"properties": {
|
|
"id": { "type": "string", "format": "uuid" },
|
|
"name": { "type": "string" },
|
|
"template": { "type": "string" },
|
|
"target_model": { "type": "string", "nullable": true },
|
|
"task_category": { "type": "string" },
|
|
"usage_count": { "type": "integer" },
|
|
"avg_quality": { "type": "number", "format": "float" },
|
|
"version": { "type": "integer" },
|
|
"created_at": { "type": "string", "format": "date-time" }
|
|
}
|
|
},
|
|
"Error": {
|
|
"type": "object",
|
|
"required": ["code", "message"],
|
|
"properties": {
|
|
"code": { "type": "string", "description": "Machine-readable error code" },
|
|
"message": { "type": "string", "description": "Human-readable error message" },
|
|
"details": { "type": "object", "description": "Field-level or contextual detail" },
|
|
"request_id": { "type": "string", "description": "Trace this to support" }
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"BadRequest": {
|
|
"description": "Bad request",
|
|
"content": {
|
|
"application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
|
|
}
|
|
},
|
|
"NotFound": {
|
|
"description": "Resource not found",
|
|
"content": {
|
|
"application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
|
|
}
|
|
},
|
|
"RateLimited": {
|
|
"description": "Rate limited",
|
|
"headers": {
|
|
"Retry-After": { "schema": { "type": "integer" } },
|
|
"X-RateLimit-Limit": { "schema": { "type": "integer" } },
|
|
"X-RateLimit-Remaining": { "schema": { "type": "integer" } },
|
|
"X-RateLimit-Reset": { "schema": { "type": "integer" } }
|
|
},
|
|
"content": {
|
|
"application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Validate contract structure
|
|
assert_eq!(api_spec["openapi"], "3.0.0");
|
|
assert_eq!(api_spec["info"]["version"], API_VERSION);
|
|
|
|
// Validate error schema is consistent
|
|
let error_schema = &api_spec["components"]["schemas"]["Error"];
|
|
assert!(error_schema["required"]
|
|
.as_array()
|
|
.unwrap()
|
|
.contains(&Value::String("code".to_string())));
|
|
assert!(error_schema["required"]
|
|
.as_array()
|
|
.unwrap()
|
|
.contains(&Value::String("message".to_string())));
|
|
|
|
// Validate naming consistency (snake_case)
|
|
assert!(
|
|
api_spec["paths"]["/memory/agents/{project_id}/prompts"]["post"]["operationId"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("createPrompt")
|
|
);
|
|
assert!(
|
|
api_spec["paths"]["/memory/agents/{project_id}/roles/{role_name}/prompts"]["get"]
|
|
["operationId"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("getRolePrompts")
|
|
);
|
|
|
|
// Validate backward compatibility: all fields are optional except required ones
|
|
let create_prompt_schema = &api_spec["paths"]["/memory/agents/{project_id}/prompts"]
|
|
["post"]["requestBody"]["content"]["application/json"]["schema"];
|
|
assert_eq!(
|
|
create_prompt_schema["required"].as_array().unwrap(),
|
|
&vec![
|
|
Value::String("name".to_string()),
|
|
Value::String("template".to_string()),
|
|
Value::String("task_category".to_string())
|
|
]
|
|
);
|
|
|
|
println!("✓ Contract-first API specification validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_backward_compatibility_rules() {
|
|
// Rule 1: Adding optional fields is safe
|
|
let safe_change = json!({
|
|
"type": "add_field",
|
|
"field": "metadata",
|
|
"required": false,
|
|
"breaking": false
|
|
});
|
|
assert!(!safe_change["breaking"].as_bool().unwrap());
|
|
|
|
// Rule 2: Removing fields is breaking
|
|
let breaking_change = json!({
|
|
"type": "remove_field",
|
|
"field": "template",
|
|
"breaking": true,
|
|
"requires_version_bump": true
|
|
});
|
|
assert!(breaking_change["breaking"].as_bool().unwrap());
|
|
assert!(breaking_change["requires_version_bump"].as_bool().unwrap());
|
|
|
|
// Rule 3: Adding new enum value is safe if clients tolerate unknowns
|
|
let safe_enum_addition = json!({
|
|
"type": "add_enum_value",
|
|
"enum": "task_category",
|
|
"new_value": "planning",
|
|
"breaking": false,
|
|
"requires_documentation": true
|
|
});
|
|
assert!(!safe_enum_addition["breaking"].as_bool().unwrap());
|
|
|
|
// Rule 4: Changing field type is breaking
|
|
let breaking_type_change = json!({
|
|
"type": "change_field_type",
|
|
"field": "usage_count",
|
|
"old_type": "integer",
|
|
"new_type": "string",
|
|
"breaking": true,
|
|
"requires_version_bump": true,
|
|
"migration_path": "Convert all consumers to parse as string"
|
|
});
|
|
assert!(breaking_type_change["breaking"].as_bool().unwrap());
|
|
|
|
println!("✓ Backward compatibility rules validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_limiting_communication() {
|
|
// Rate limits must be communicated in response headers
|
|
let response_headers = json!({
|
|
"X-RateLimit-Limit": 1000,
|
|
"X-RateLimit-Remaining": 847,
|
|
"X-RateLimit-Reset": 1720483200,
|
|
"Retry-After": 30
|
|
});
|
|
|
|
// All required rate limit headers present
|
|
assert!(response_headers.get("X-RateLimit-Limit").is_some());
|
|
assert!(response_headers.get("X-RateLimit-Remaining").is_some());
|
|
assert!(response_headers.get("X-RateLimit-Reset").is_some());
|
|
|
|
// On 429, Retry-After present
|
|
let rate_limited_response = json!({
|
|
"status": 429,
|
|
"error": {
|
|
"code": "rate_limit_exceeded",
|
|
"message": "1000 req/hr exceeded; retry after 30s",
|
|
"request_id": "req_a1b2"
|
|
},
|
|
"headers": {
|
|
"Retry-After": 30
|
|
}
|
|
});
|
|
|
|
assert_eq!(rate_limited_response["status"], 429);
|
|
assert_eq!(
|
|
rate_limited_response["error"]["code"],
|
|
"rate_limit_exceeded"
|
|
);
|
|
assert!(
|
|
rate_limited_response["headers"]["Retry-After"]
|
|
.as_i64()
|
|
.unwrap()
|
|
> 0
|
|
);
|
|
|
|
println!("✓ Rate limiting communication validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_response_consistency() {
|
|
// Error responses must have consistent structure everywhere
|
|
let errors = vec![
|
|
json!({
|
|
"code": "invalid_request",
|
|
"message": "name field required",
|
|
"details": { "field": "name" },
|
|
"request_id": "req-123"
|
|
}),
|
|
json!({
|
|
"code": "not_found",
|
|
"message": "Prompt not found",
|
|
"details": { "prompt_id": "uuid-456" },
|
|
"request_id": "req-789"
|
|
}),
|
|
json!({
|
|
"code": "permission_denied",
|
|
"message": "Insufficient capabilities",
|
|
"details": { "required": "memory:write" },
|
|
"request_id": "req-999"
|
|
}),
|
|
];
|
|
|
|
for error in errors {
|
|
// All errors have required structure
|
|
assert!(error["code"].is_string());
|
|
assert!(error["message"].is_string());
|
|
assert!(error["request_id"].is_string());
|
|
|
|
// No 200 with error (must use proper HTTP status)
|
|
assert_ne!(error["code"], ""); // code is stable, machine-readable
|
|
}
|
|
|
|
println!("✓ Error response consistency validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_deprecation_lifecycle() {
|
|
// Deprecation requires: Announce → Signal → Runway → Monitor → Sunset
|
|
let deprecation_plan = json!({
|
|
"endpoint": "/agents/{id}",
|
|
"lifecycle": {
|
|
"phase": "announced",
|
|
"deprecation_date": "2025-06-01",
|
|
"sunset_date": "2026-06-01",
|
|
"runway_days": 365
|
|
},
|
|
"signals": {
|
|
"deprecation_header": "Deprecation: true",
|
|
"sunset_header": "Sunset: Sun, 01 Jun 2026 00:00:00 GMT",
|
|
"warning_in_response": true
|
|
},
|
|
"migration_guide": "Use /agents/v2/{id} instead",
|
|
"monitoring": {
|
|
"track_usage_by_consumer": true,
|
|
"alert_on_remaining_usage": true
|
|
}
|
|
});
|
|
|
|
assert_eq!(deprecation_plan["lifecycle"]["runway_days"], 365);
|
|
assert!(deprecation_plan["signals"]["deprecation_header"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("Deprecation"));
|
|
assert!(deprecation_plan["monitoring"]["track_usage_by_consumer"]
|
|
.as_bool()
|
|
.unwrap());
|
|
|
|
println!("✓ Deprecation lifecycle validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_idempotency_and_retry_safety() {
|
|
// Write operations must be idempotent via Idempotency-Key
|
|
let request_with_key = json!({
|
|
"method": "POST",
|
|
"path": "/memory/agents/project1/prompts",
|
|
"headers": {
|
|
"Idempotency-Key": "req-unique-uuid-123"
|
|
},
|
|
"body": {
|
|
"name": "extract-entities",
|
|
"template": "Extract entities from {{text}}"
|
|
}
|
|
});
|
|
|
|
assert!(request_with_key["headers"]["Idempotency-Key"].is_string());
|
|
|
|
// Retry with same key returns cached response
|
|
let response_1 = json!({
|
|
"status": 201,
|
|
"id": "prompt-uuid-456"
|
|
});
|
|
|
|
let response_2_retry = json!({
|
|
"status": 201,
|
|
"id": "prompt-uuid-456",
|
|
"cached": true
|
|
});
|
|
|
|
// Both return same result → safe to retry
|
|
assert_eq!(response_1["id"], response_2_retry["id"]);
|
|
|
|
println!("✓ Idempotency and retry safety validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_platform_engineer_role_requirements() {
|
|
// Comprehensive validation per api-platform-engineer.md role
|
|
let role_requirements = json!({
|
|
"role": API_PLATFORM_ENGINEER_ROLE,
|
|
"requirements": {
|
|
"contract_first": {
|
|
"openapi_spec": "required",
|
|
"source_of_truth_before_code": true,
|
|
"consistency_reviewed": true
|
|
},
|
|
"backward_compatibility": {
|
|
"no_silent_breaking_changes": true,
|
|
"additive_changes_allowed": true,
|
|
"versioning_policy": "major version in path (/v1, /v2)",
|
|
"deprecation_runway": "6-12+ months"
|
|
},
|
|
"error_handling": {
|
|
"consistent_structure": true,
|
|
"stable_machine_readable_code": true,
|
|
"correct_http_status": true,
|
|
"request_id_for_tracing": true
|
|
},
|
|
"rate_limiting": {
|
|
"communicated_headers": true,
|
|
"no_ambush_429": true,
|
|
"retry_after_provided": true
|
|
},
|
|
"sdk_and_docs": {
|
|
"generated_from_spec": true,
|
|
"never_drift": true,
|
|
"typed_idiomatic": true,
|
|
"multiple_languages": true
|
|
},
|
|
"idempotency": {
|
|
"write_operations_idempotent": true,
|
|
"idempotency_key_support": true,
|
|
"safe_retry": true
|
|
}
|
|
}
|
|
});
|
|
|
|
// Validate all requirements
|
|
assert!(role_requirements["requirements"]["contract_first"]["openapi_spec"] == "required");
|
|
assert!(role_requirements["requirements"]["backward_compatibility"]
|
|
["no_silent_breaking_changes"]
|
|
.as_bool()
|
|
.unwrap());
|
|
assert!(
|
|
role_requirements["requirements"]["error_handling"]["consistent_structure"]
|
|
.as_bool()
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
role_requirements["requirements"]["rate_limiting"]["communicated_headers"]
|
|
.as_bool()
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
role_requirements["requirements"]["sdk_and_docs"]["generated_from_spec"]
|
|
.as_bool()
|
|
.unwrap()
|
|
);
|
|
assert!(
|
|
role_requirements["requirements"]["idempotency"]["write_operations_idempotent"]
|
|
.as_bool()
|
|
.unwrap()
|
|
);
|
|
|
|
println!("✓ API Platform Engineer role requirements validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_agent_prompts_for_api_platform_engineer() {
|
|
// Agent prompts aligned with API Platform Engineer role
|
|
let agent_prompts = vec![
|
|
("contract-review", CONTRACT_FIRST_PROMPT, "extraction"),
|
|
(
|
|
"compatibility-check",
|
|
BACKWARD_COMPATIBILITY_PROMPT,
|
|
"reasoning",
|
|
),
|
|
("sdk-generation", SDK_GENERATION_PROMPT, "generation"),
|
|
];
|
|
|
|
for (name, template, category) in agent_prompts {
|
|
let prompt = json!({
|
|
"name": name,
|
|
"template": template,
|
|
"task_category": category,
|
|
"target_model": "ornith:35b"
|
|
});
|
|
|
|
assert!(!prompt["template"].as_str().unwrap().is_empty());
|
|
assert!(
|
|
prompt["template"].as_str().unwrap().contains("{{")
|
|
|| prompt["template"].as_str().unwrap().contains("output")
|
|
);
|
|
}
|
|
|
|
println!("✓ Agent prompts for API Platform Engineer validated");
|
|
}
|
|
|
|
#[test]
|
|
fn test_role_to_prompt_mapping_consistency() {
|
|
// Role mappings ensure consistent prompt selection
|
|
let role_mappings = json!({
|
|
"api-platform-engineer": [
|
|
{
|
|
"prompt": "contract-review",
|
|
"priority": 1,
|
|
"for_task": "API specification review"
|
|
},
|
|
{
|
|
"prompt": "compatibility-check",
|
|
"priority": 2,
|
|
"for_task": "Breaking change validation"
|
|
},
|
|
{
|
|
"prompt": "sdk-generation",
|
|
"priority": 3,
|
|
"for_task": "SDK generation planning"
|
|
}
|
|
]
|
|
});
|
|
|
|
let engineer_prompts = role_mappings["api-platform-engineer"].as_array().unwrap();
|
|
assert_eq!(engineer_prompts.len(), 3);
|
|
|
|
// Prompts ordered by priority
|
|
assert!(
|
|
engineer_prompts[0]["priority"].as_i64().unwrap()
|
|
< engineer_prompts[1]["priority"].as_i64().unwrap()
|
|
);
|
|
|
|
println!("✓ Role-to-prompt mapping consistency validated");
|
|
}
|
|
}
|