mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
615 lines
18 KiB
TypeScript
615 lines
18 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Schema Validator Service Unit Tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { SchemaValidatorService } from '../../../../src/domains/contract-testing/services/schema-validator';
|
|
import type { MemoryBackend } from '../../../../src/kernel/interfaces';
|
|
import type { SchemaDefinition } from '../../../../src/domains/contract-testing/interfaces';
|
|
|
|
// Mock MemoryBackend
|
|
function createMockMemoryBackend(): MemoryBackend {
|
|
return {
|
|
initialize: vi.fn().mockResolvedValue(undefined),
|
|
dispose: vi.fn().mockResolvedValue(undefined),
|
|
set: vi.fn().mockResolvedValue(undefined),
|
|
get: vi.fn().mockResolvedValue(undefined),
|
|
delete: vi.fn().mockResolvedValue(true),
|
|
has: vi.fn().mockResolvedValue(false),
|
|
search: vi.fn().mockResolvedValue([]),
|
|
vectorSearch: vi.fn().mockResolvedValue([]),
|
|
storeVector: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
}
|
|
|
|
describe('SchemaValidatorService', () => {
|
|
let service: SchemaValidatorService;
|
|
let mockMemory: MemoryBackend;
|
|
|
|
beforeEach(() => {
|
|
mockMemory = createMockMemoryBackend();
|
|
service = new SchemaValidatorService(mockMemory);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('validateJsonSchema', () => {
|
|
it('should validate data matching schema', async () => {
|
|
const schema = {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string' },
|
|
age: { type: 'number' },
|
|
},
|
|
required: ['name'],
|
|
};
|
|
|
|
const result = await service.validateJsonSchema({ name: 'John', age: 30 }, schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(true);
|
|
expect(result.value.errors).toHaveLength(0);
|
|
}
|
|
});
|
|
|
|
it('should return error for missing required field', async () => {
|
|
const schema = {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string' },
|
|
email: { type: 'string' },
|
|
},
|
|
required: ['name', 'email'],
|
|
};
|
|
|
|
const result = await service.validateJsonSchema({ name: 'John' }, schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(false);
|
|
expect(result.value.errors.some((e) => e.keyword === 'required')).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should return error for type mismatch', async () => {
|
|
const schema = {
|
|
type: 'object',
|
|
properties: {
|
|
count: { type: 'number' },
|
|
},
|
|
};
|
|
|
|
const result = await service.validateJsonSchema({ count: 'not a number' }, schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(false);
|
|
expect(result.value.errors.some((e) => e.keyword === 'type')).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should validate array items', async () => {
|
|
const schema = {
|
|
type: 'array',
|
|
items: { type: 'number' },
|
|
};
|
|
|
|
const validResult = await service.validateJsonSchema([1, 2, 3], schema);
|
|
const invalidResult = await service.validateJsonSchema([1, 'two', 3], schema);
|
|
|
|
expect(validResult.success && validResult.value.isValid).toBe(true);
|
|
expect(invalidResult.success && !invalidResult.value.isValid).toBe(true);
|
|
});
|
|
|
|
it('should validate minLength and maxLength', async () => {
|
|
const schema = {
|
|
type: 'string',
|
|
minLength: 3,
|
|
maxLength: 10,
|
|
};
|
|
|
|
const tooShort = await service.validateJsonSchema('ab', schema);
|
|
const tooLong = await service.validateJsonSchema('this is too long', schema);
|
|
const justRight = await service.validateJsonSchema('hello', schema);
|
|
|
|
expect(tooShort.success && tooShort.value.errors.some((e) => e.keyword === 'minLength')).toBe(true);
|
|
expect(tooLong.success && tooLong.value.errors.some((e) => e.keyword === 'maxLength')).toBe(true);
|
|
expect(justRight.success && justRight.value.isValid).toBe(true);
|
|
});
|
|
|
|
it('should validate enum values', async () => {
|
|
const schema = {
|
|
type: 'string',
|
|
enum: ['red', 'green', 'blue'],
|
|
};
|
|
|
|
const valid = await service.validateJsonSchema('red', schema);
|
|
const invalid = await service.validateJsonSchema('yellow', schema);
|
|
|
|
expect(valid.success && valid.value.isValid).toBe(true);
|
|
expect(invalid.success && invalid.value.errors.some((e) => e.keyword === 'enum')).toBe(true);
|
|
});
|
|
|
|
it('should validate number minimum and maximum', async () => {
|
|
const schema = {
|
|
type: 'number',
|
|
minimum: 0,
|
|
maximum: 100,
|
|
};
|
|
|
|
const tooSmall = await service.validateJsonSchema(-5, schema);
|
|
const tooBig = await service.validateJsonSchema(150, schema);
|
|
const inRange = await service.validateJsonSchema(50, schema);
|
|
|
|
expect(tooSmall.success && tooSmall.value.errors.some((e) => e.keyword === 'minimum')).toBe(true);
|
|
expect(tooBig.success && tooBig.value.errors.some((e) => e.keyword === 'maximum')).toBe(true);
|
|
expect(inRange.success && inRange.value.isValid).toBe(true);
|
|
});
|
|
|
|
it('should validate string patterns', async () => {
|
|
const schema = {
|
|
type: 'string',
|
|
pattern: '^[A-Z][a-z]+$',
|
|
};
|
|
|
|
const valid = await service.validateJsonSchema('Hello', schema);
|
|
const invalid = await service.validateJsonSchema('hello', schema);
|
|
|
|
expect(valid.success && valid.value.isValid).toBe(true);
|
|
expect(invalid.success && invalid.value.errors.some((e) => e.keyword === 'pattern')).toBe(true);
|
|
});
|
|
|
|
it('should validate email format', async () => {
|
|
const schema = {
|
|
type: 'string',
|
|
format: 'email',
|
|
};
|
|
|
|
const valid = await service.validateJsonSchema('test@example.com', schema);
|
|
const invalid = await service.validateJsonSchema('not-an-email', schema);
|
|
|
|
expect(valid.success && valid.value.isValid).toBe(true);
|
|
expect(invalid.success && invalid.value.errors.some((e) => e.keyword === 'format')).toBe(true);
|
|
});
|
|
|
|
it('should validate uuid format', async () => {
|
|
const schema = {
|
|
type: 'string',
|
|
format: 'uuid',
|
|
};
|
|
|
|
const valid = await service.validateJsonSchema('550e8400-e29b-41d4-a716-446655440000', schema);
|
|
const invalid = await service.validateJsonSchema('not-a-uuid', schema);
|
|
|
|
expect(valid.success && valid.value.isValid).toBe(true);
|
|
expect(invalid.success && invalid.value.errors.some((e) => e.keyword === 'format')).toBe(true);
|
|
});
|
|
|
|
it('should handle anyOf validation', async () => {
|
|
const schema = {
|
|
anyOf: [{ type: 'string' }, { type: 'number' }],
|
|
};
|
|
|
|
const validString = await service.validateJsonSchema('hello', schema);
|
|
const validNumber = await service.validateJsonSchema(42, schema);
|
|
const invalid = await service.validateJsonSchema(true, schema);
|
|
|
|
expect(validString.success && validString.value.isValid).toBe(true);
|
|
expect(validNumber.success && validNumber.value.isValid).toBe(true);
|
|
expect(invalid.success && invalid.value.errors.some((e) => e.keyword === 'anyOf')).toBe(true);
|
|
});
|
|
|
|
it('should respect maximum recursion depth', async () => {
|
|
const strictService = new SchemaValidatorService(mockMemory, { maxRecursionDepth: 2 });
|
|
const deepSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
level1: {
|
|
type: 'object',
|
|
properties: {
|
|
level2: {
|
|
type: 'object',
|
|
properties: {
|
|
level3: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = await strictService.validateJsonSchema(
|
|
{ level1: { level2: { level3: 'deep' } } },
|
|
deepSchema
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.errors.some((e) => e.keyword === 'maxDepth')).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('validateGraphQLSchema', () => {
|
|
it('should validate valid GraphQL schema', async () => {
|
|
const schema = `
|
|
type Query {
|
|
users: [User]
|
|
user(id: ID!): User
|
|
}
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String!
|
|
email: String
|
|
}
|
|
`;
|
|
|
|
const result = await service.validateGraphQLSchema(schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(true);
|
|
expect(result.value.typeCount).toBeGreaterThan(0);
|
|
expect(result.value.queryCount).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should detect unbalanced braces', async () => {
|
|
const schema = `
|
|
type Query {
|
|
users: [User]
|
|
|
|
type User {
|
|
id: ID!
|
|
}
|
|
`;
|
|
|
|
const result = await service.validateGraphQLSchema(schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(false);
|
|
expect(result.value.errors.some((e) => e.message.includes('braces'))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should require Query type or schema definition', async () => {
|
|
const schema = `
|
|
type User {
|
|
id: ID!
|
|
name: String!
|
|
}
|
|
`;
|
|
|
|
const result = await service.validateGraphQLSchema(schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(false);
|
|
expect(result.value.errors.some((e) => e.message.includes('Query'))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should count mutations', async () => {
|
|
const schema = `
|
|
type Query {
|
|
users: [User]
|
|
}
|
|
|
|
type Mutation {
|
|
createUser(name: String!): User
|
|
deleteUser(id: ID!): Boolean
|
|
}
|
|
|
|
type User {
|
|
id: ID!
|
|
name: String!
|
|
}
|
|
`;
|
|
|
|
const result = await service.validateGraphQLSchema(schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.mutationCount).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should detect invalid double colon syntax', async () => {
|
|
const schema = `
|
|
type Query {
|
|
users:: [User]
|
|
}
|
|
`;
|
|
|
|
const result = await service.validateGraphQLSchema(schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.errors.some((e) => e.message.includes('double colon'))).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('compareSchemas', () => {
|
|
it('should detect compatible schemas', async () => {
|
|
const oldSchema: SchemaDefinition = {
|
|
id: 'user-v1',
|
|
name: 'User',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({
|
|
type: 'object',
|
|
properties: { name: { type: 'string' } },
|
|
}),
|
|
};
|
|
|
|
const newSchema: SchemaDefinition = {
|
|
id: 'user-v2',
|
|
name: 'User',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string' },
|
|
email: { type: 'string' }, // Added optional field
|
|
},
|
|
}),
|
|
};
|
|
|
|
const result = await service.compareSchemas(oldSchema, newSchema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isCompatible).toBe(true);
|
|
expect(result.value.additions).toContain('email');
|
|
}
|
|
});
|
|
|
|
it('should detect incompatible type changes', async () => {
|
|
const oldSchema: SchemaDefinition = {
|
|
id: 'data-v1',
|
|
name: 'Data',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({ type: 'object' }),
|
|
};
|
|
|
|
const newSchema: SchemaDefinition = {
|
|
id: 'data-v2',
|
|
name: 'Data',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({ type: 'array' }),
|
|
};
|
|
|
|
const result = await service.compareSchemas(oldSchema, newSchema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isCompatible).toBe(false);
|
|
expect(result.value.modifications.some((m) => m.isBreaking)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should detect removed properties', async () => {
|
|
const oldSchema: SchemaDefinition = {
|
|
id: 'item-v1',
|
|
name: 'Item',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({
|
|
type: 'object',
|
|
properties: { id: { type: 'string' }, price: { type: 'number' } },
|
|
}),
|
|
};
|
|
|
|
const newSchema: SchemaDefinition = {
|
|
id: 'item-v2',
|
|
name: 'Item',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({
|
|
type: 'object',
|
|
properties: { id: { type: 'string' } }, // price removed
|
|
}),
|
|
};
|
|
|
|
const result = await service.compareSchemas(oldSchema, newSchema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isCompatible).toBe(false);
|
|
expect(result.value.removals).toContain('price');
|
|
}
|
|
});
|
|
|
|
it('should detect schema type change as breaking', async () => {
|
|
const oldSchema: SchemaDefinition = {
|
|
id: 'data-v1',
|
|
name: 'Data',
|
|
type: 'json-schema',
|
|
content: JSON.stringify({ type: 'object' }),
|
|
};
|
|
|
|
const newSchema: SchemaDefinition = {
|
|
id: 'data-v2',
|
|
name: 'Data',
|
|
type: 'graphql',
|
|
content: 'type Data { id: ID! }',
|
|
};
|
|
|
|
const result = await service.compareSchemas(oldSchema, newSchema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isCompatible).toBe(false);
|
|
expect(result.value.modifications.some((m) => m.path === 'type' && m.isBreaking)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should compare GraphQL schemas', async () => {
|
|
const oldSchema: SchemaDefinition = {
|
|
id: 'api-v1',
|
|
name: 'API',
|
|
type: 'graphql',
|
|
content: 'type Query { users: [User] } type User { id: ID! }',
|
|
};
|
|
|
|
const newSchema: SchemaDefinition = {
|
|
id: 'api-v2',
|
|
name: 'API',
|
|
type: 'graphql',
|
|
content: 'type Query { users: [User] } type User { id: ID! } type Post { id: ID! }',
|
|
};
|
|
|
|
const result = await service.compareSchemas(oldSchema, newSchema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.additions).toContain('type:Post');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('inferSchema', () => {
|
|
it('should infer schema from sample objects', async () => {
|
|
const samples = [
|
|
{ name: 'John', age: 30 },
|
|
{ name: 'Jane', age: 25 },
|
|
{ name: 'Bob', age: 35 },
|
|
];
|
|
|
|
const result = await service.inferSchema(samples);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.type).toBe('json-schema');
|
|
const content = JSON.parse(result.value.content);
|
|
expect(content.type).toBe('object');
|
|
expect(content.properties.name.type).toBe('string');
|
|
expect(content.properties.age.type).toBe('integer');
|
|
}
|
|
});
|
|
|
|
it('should return error for empty samples', async () => {
|
|
const result = await service.inferSchema([]);
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should infer array schema', async () => {
|
|
const samples = [[1, 2, 3], [4, 5], [6, 7, 8, 9]];
|
|
|
|
const result = await service.inferSchema(samples);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const content = JSON.parse(result.value.content);
|
|
expect(content.type).toBe('array');
|
|
expect(content.items.type).toBe('integer');
|
|
}
|
|
});
|
|
|
|
it('should infer email format', async () => {
|
|
const samples = [
|
|
{ email: 'user1@example.com' },
|
|
{ email: 'user2@test.org' },
|
|
{ email: 'admin@company.net' },
|
|
];
|
|
|
|
const result = await service.inferSchema(samples);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const content = JSON.parse(result.value.content);
|
|
expect(content.properties.email.format).toBe('email');
|
|
}
|
|
});
|
|
|
|
it('should infer date-time format', async () => {
|
|
const samples = [
|
|
{ timestamp: '2024-01-15T10:30:00Z' },
|
|
{ timestamp: '2024-02-20T14:45:00Z' },
|
|
];
|
|
|
|
const result = await service.inferSchema(samples);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
const content = JSON.parse(result.value.content);
|
|
expect(content.properties.timestamp.format).toBe('date-time');
|
|
}
|
|
});
|
|
|
|
it('should store inferred schema in memory', async () => {
|
|
const samples = [{ id: 1 }, { id: 2 }];
|
|
|
|
await service.inferSchema(samples);
|
|
|
|
expect(mockMemory.set).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle null values based on schema', async () => {
|
|
const schema = {
|
|
type: 'object',
|
|
properties: {
|
|
value: { type: 'string', nullable: true },
|
|
},
|
|
};
|
|
|
|
const result = await service.validateJsonSchema({ value: null }, schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.isValid).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should reject null in strict mode when not nullable', async () => {
|
|
const strictService = new SchemaValidatorService(mockMemory, { strictMode: true });
|
|
const schema = {
|
|
type: 'object',
|
|
properties: {
|
|
value: { type: 'string' },
|
|
},
|
|
};
|
|
|
|
const result = await strictService.validateJsonSchema({ value: null }, schema);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.value.errors.some((e) => e.message.includes('null'))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should validate unique items in arrays', async () => {
|
|
const schema = {
|
|
type: 'array',
|
|
items: { type: 'number' },
|
|
uniqueItems: true,
|
|
};
|
|
|
|
const unique = await service.validateJsonSchema([1, 2, 3], schema);
|
|
const duplicate = await service.validateJsonSchema([1, 2, 2], schema);
|
|
|
|
expect(unique.success && unique.value.isValid).toBe(true);
|
|
expect(duplicate.success && duplicate.value.errors.some((e) => e.keyword === 'uniqueItems')).toBe(true);
|
|
});
|
|
|
|
it('should validate multipleOf for numbers', async () => {
|
|
const schema = {
|
|
type: 'number',
|
|
multipleOf: 5,
|
|
};
|
|
|
|
const valid = await service.validateJsonSchema(15, schema);
|
|
const invalid = await service.validateJsonSchema(17, schema);
|
|
|
|
expect(valid.success && valid.value.isValid).toBe(true);
|
|
expect(invalid.success && invalid.value.errors.some((e) => e.keyword === 'multipleOf')).toBe(true);
|
|
});
|
|
});
|
|
});
|