400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
const IntegrityValidator = require('../../src/validation/validators/IntegrityValidator');
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
describe('IntegrityValidator', () => {
|
|
let validator;
|
|
const testRegistryPath = path.join(process.cwd(), '.claude/security/component-hashes.json');
|
|
|
|
beforeEach(() => {
|
|
validator = new IntegrityValidator();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up test registry file
|
|
if (await fs.pathExists(testRegistryPath)) {
|
|
await fs.remove(testRegistryPath);
|
|
}
|
|
});
|
|
|
|
describe('Hash Generation', () => {
|
|
it('should generate consistent SHA256 hash', () => {
|
|
const content = 'test content';
|
|
const hash1 = validator.generateHash(content);
|
|
const hash2 = validator.generateHash(content);
|
|
|
|
expect(hash1).toBe(hash2);
|
|
expect(hash1).toHaveLength(64); // SHA256 in hex is 64 chars
|
|
});
|
|
|
|
it('should generate different hashes for different content', () => {
|
|
const hash1 = validator.generateHash('content 1');
|
|
const hash2 = validator.generateHash('content 2');
|
|
|
|
expect(hash1).not.toBe(hash2);
|
|
});
|
|
|
|
it('should generate expected hash for known content', () => {
|
|
const content = 'Hello, World!';
|
|
const expectedHash = crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
const actualHash = validator.generateHash(content);
|
|
|
|
expect(actualHash).toBe(expectedHash);
|
|
});
|
|
});
|
|
|
|
describe('Basic Validation', () => {
|
|
it('should validate and generate hash for valid component', async () => {
|
|
const component = {
|
|
content: `---
|
|
name: test-agent
|
|
description: A test agent
|
|
tools: Read
|
|
version: 1.0.0
|
|
---
|
|
Content`,
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.hash).toBeDefined();
|
|
expect(result.hash).toHaveLength(64);
|
|
expect(result.version).toBe('1.0.0');
|
|
});
|
|
|
|
it('should error when content is missing', async () => {
|
|
const component = {
|
|
content: null,
|
|
path: 'test.md',
|
|
type: 'agent'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'INT_E001'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Hash Verification', () => {
|
|
it('should pass when hash matches expected hash', async () => {
|
|
const content = 'test content';
|
|
const expectedHash = validator.generateHash(content);
|
|
|
|
const component = {
|
|
content,
|
|
path: 'test.md',
|
|
type: 'agent'
|
|
};
|
|
|
|
const result = await validator.validate(component, { expectedHash });
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.info.some(i => i.code === 'INT_I002')).toBe(true);
|
|
});
|
|
|
|
it('should error when hash does not match expected hash', async () => {
|
|
const component = {
|
|
content: 'actual content',
|
|
path: 'test.md',
|
|
type: 'agent'
|
|
};
|
|
|
|
const wrongHash = validator.generateHash('different content');
|
|
|
|
const result = await validator.validate(component, { expectedHash: wrongHash });
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'INT_E002'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Version Validation', () => {
|
|
it('should accept valid semantic version', async () => {
|
|
const component = {
|
|
content: 'test content',
|
|
path: 'test.md',
|
|
type: 'agent',
|
|
version: '1.2.3'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.info.some(i => i.code === 'INT_I007')).toBe(true);
|
|
});
|
|
|
|
it('should accept simple version format', async () => {
|
|
const component = {
|
|
content: 'test content',
|
|
path: 'test.md',
|
|
type: 'agent',
|
|
version: '1.0'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should warn about invalid version format', async () => {
|
|
const component = {
|
|
content: 'test content',
|
|
path: 'test.md',
|
|
type: 'agent',
|
|
version: 'v1.0-beta'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.warnings).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'INT_W003'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should warn when no version is specified', async () => {
|
|
const component = {
|
|
content: 'test content',
|
|
path: 'test.md',
|
|
type: 'agent'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.warnings).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'INT_W002'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Hash Registry', () => {
|
|
it('should update registry when requested', async () => {
|
|
const component = {
|
|
content: 'test content',
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
await validator.validate(component, { updateRegistry: true });
|
|
|
|
const registry = await validator.loadHashRegistry();
|
|
const normalizedPath = validator.normalizePath('test-agent.md');
|
|
|
|
expect(registry[normalizedPath]).toBeDefined();
|
|
expect(registry[normalizedPath].hash).toBe(validator.generateHash('test content'));
|
|
expect(registry[normalizedPath].version).toBe('1.0.0');
|
|
});
|
|
|
|
it('should detect hash changes from registry', async () => {
|
|
const component1 = {
|
|
content: 'original content',
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
// First validation - update registry
|
|
await validator.validate(component1, { updateRegistry: true });
|
|
|
|
// Second validation with modified content
|
|
const component2 = {
|
|
content: 'modified content',
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
const result = await validator.validate(component2);
|
|
|
|
expect(result.warnings).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'INT_W001'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should detect version changes', async () => {
|
|
const component1 = {
|
|
content: 'test content',
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
await validator.validate(component1, { updateRegistry: true });
|
|
|
|
const component2 = {
|
|
content: 'test content',
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '2.0.0'
|
|
};
|
|
|
|
const result = await validator.validate(component2);
|
|
|
|
expect(result.info.some(i => i.code === 'INT_I004')).toBe(true);
|
|
});
|
|
|
|
it('should handle new components not in registry', async () => {
|
|
const component = {
|
|
content: 'new content',
|
|
path: 'new-agent.md',
|
|
type: 'agent'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.info.some(i => i.code === 'INT_I005' || i.code === 'INT_I006')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Batch Validation', () => {
|
|
it('should validate multiple components', async () => {
|
|
const components = [
|
|
{
|
|
content: 'content 1',
|
|
path: 'agent1.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
},
|
|
{
|
|
content: 'content 2',
|
|
path: 'agent2.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
},
|
|
{
|
|
content: 'content 3',
|
|
path: 'agent3.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
}
|
|
];
|
|
|
|
const result = await validator.batchValidate(components, { updateRegistry: true });
|
|
|
|
expect(result.total).toBe(3);
|
|
expect(result.passed).toBe(3);
|
|
expect(result.failed).toBe(0);
|
|
expect(result.components).toHaveLength(3);
|
|
});
|
|
|
|
it('should count failures correctly in batch validation', async () => {
|
|
const components = [
|
|
{
|
|
content: 'valid content',
|
|
path: 'agent1.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
},
|
|
{
|
|
content: null, // Invalid
|
|
path: 'agent2.md',
|
|
type: 'agent'
|
|
},
|
|
{
|
|
content: 'valid content',
|
|
path: 'agent3.md',
|
|
type: 'agent'
|
|
}
|
|
];
|
|
|
|
const result = await validator.batchValidate(components);
|
|
|
|
expect(result.total).toBe(3);
|
|
expect(result.passed).toBe(2);
|
|
expect(result.failed).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('Integrity Report', () => {
|
|
it('should generate comprehensive integrity report', async () => {
|
|
const component = {
|
|
content: `---
|
|
name: test-agent
|
|
description: A test agent
|
|
tools: Read
|
|
version: 1.0.0
|
|
---
|
|
Content`,
|
|
path: 'test-agent.md',
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
const report = await validator.generateIntegrityReport(component);
|
|
|
|
expect(report).toHaveProperty('valid');
|
|
expect(report).toHaveProperty('hash');
|
|
expect(report).toHaveProperty('version');
|
|
expect(report).toHaveProperty('timestamp');
|
|
expect(report).toHaveProperty('issues');
|
|
expect(report.issues).toHaveProperty('errors');
|
|
expect(report.issues).toHaveProperty('warnings');
|
|
});
|
|
});
|
|
|
|
describe('Real Component Validation', () => {
|
|
it('should validate real frontend-developer agent', async () => {
|
|
const agentPath = path.join(
|
|
__dirname,
|
|
'../../components/agents/development-team/frontend-developer.md'
|
|
);
|
|
|
|
if (!await fs.pathExists(agentPath)) {
|
|
console.log('Skipping test: frontend-developer.md not found');
|
|
return;
|
|
}
|
|
|
|
const content = await fs.readFile(agentPath, 'utf8');
|
|
|
|
const component = {
|
|
content,
|
|
path: agentPath,
|
|
type: 'agent',
|
|
version: '1.0.0'
|
|
};
|
|
|
|
const result = await validator.validate(component);
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.hash).toBeDefined();
|
|
expect(result.hash).toHaveLength(64);
|
|
});
|
|
});
|
|
|
|
describe('Path Normalization', () => {
|
|
it('should normalize absolute paths to relative', () => {
|
|
const absolutePath = path.join(process.cwd(), 'components/agent.md');
|
|
const normalized = validator.normalizePath(absolutePath);
|
|
|
|
expect(normalized).toBe('components/agent.md');
|
|
});
|
|
|
|
it('should keep relative paths unchanged', () => {
|
|
const relativePath = 'components/agent.md';
|
|
const normalized = validator.normalizePath(relativePath);
|
|
|
|
expect(normalized).toBe(relativePath);
|
|
});
|
|
});
|
|
});
|