Files
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
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>
2026-03-01 11:35:41 +00:00

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);
});
});
});