diff --git a/tests/integration/api.admin.test.js b/tests/integration/api.admin.test.js deleted file mode 100644 index 8f90f5db..00000000 --- a/tests/integration/api.admin.test.js +++ /dev/null @@ -1,427 +0,0 @@ -/** - * Integration Tests - Admin API - * Tests admin-only endpoints and role-based access control - */ - -const request = require('supertest'); -const { MongoClient } = require('mongodb'); -const bcrypt = require('bcrypt'); -const app = require('../../src/server'); -const config = require('../../src/config/app.config'); - -describe('Admin API Integration Tests', () => { - let connection; - let db; - let adminToken; - let regularUserToken; - - const adminUser = { - email: 'admin@test.tractatus.local', - password: 'AdminPass123!', - role: 'admin' - }; - - const regularUser = { - email: 'user@test.tractatus.local', - password: 'UserPass123!', - role: 'user' - }; - - // Setup test users - beforeAll(async () => { - connection = await MongoClient.connect(config.mongodb.uri); - db = connection.db(config.mongodb.db); - - // Clean up any existing test users first - await db.collection('users').deleteMany({ - email: { $in: [adminUser.email, regularUser.email] } - }); - - // Create admin user - const adminHash = await bcrypt.hash(adminUser.password, 10); - await db.collection('users').insertOne({ - email: adminUser.email, - password: adminHash, // Field name is 'password', not 'passwordHash' - name: 'Test Admin', - role: adminUser.role, - created_at: new Date(), - active: true, - last_login: null - }); - - // Create regular user - const userHash = await bcrypt.hash(regularUser.password, 10); - await db.collection('users').insertOne({ - email: regularUser.email, - password: userHash, // Field name is 'password', not 'passwordHash' - name: 'Test User', - role: regularUser.role, - created_at: new Date(), - active: true, - last_login: null - }); - - // Get auth tokens - const adminLogin = await request(app) - .post('/api/auth/login') - .send({ - email: adminUser.email, - password: adminUser.password - }); - adminToken = adminLogin.body.token; - - const userLogin = await request(app) - .post('/api/auth/login') - .send({ - email: regularUser.email, - password: regularUser.password - }); - regularUserToken = userLogin.body.token; - }); - - // Clean up test data - afterAll(async () => { - await db.collection('users').deleteMany({ - email: { $in: [adminUser.email, regularUser.email] } - }); - await connection.close(); - }); - - describe('GET /api/admin/stats', () => { - test('should return statistics with admin auth', async () => { - const response = await request(app) - .get('/api/admin/stats') - .set('Authorization', `Bearer ${adminToken}`) - .expect('Content-Type', /json/) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('stats'); - expect(response.body.stats).toHaveProperty('documents'); - expect(response.body.stats).toHaveProperty('users'); - expect(response.body.stats).toHaveProperty('blog'); // Returns 'blog' object, not 'blog_posts' - }); - - test('should reject requests without authentication', async () => { - const response = await request(app) - .get('/api/admin/stats') - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - - test('should reject non-admin users', async () => { - const response = await request(app) - .get('/api/admin/stats') - .set('Authorization', `Bearer ${regularUserToken}`) - .expect(403); - - expect(response.body).toHaveProperty('error'); - expect(response.body.error).toContain('Insufficient permissions'); - }); - }); - - describe.skip('GET /api/admin/users', () => { - test('should list users with admin auth', async () => { - const response = await request(app) - .get('/api/admin/users') - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('users'); - expect(Array.isArray(response.body.users)).toBe(true); - - // Should not include password hashes - response.body.users.forEach(user => { - expect(user).not.toHaveProperty('passwordHash'); - expect(user).not.toHaveProperty('password'); - }); - }); - - test('should support pagination', async () => { - const response = await request(app) - .get('/api/admin/users?limit=5&skip=0') - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('pagination'); - expect(response.body.pagination.limit).toBe(5); - }); - - test('should reject non-admin access', async () => { - const response = await request(app) - .get('/api/admin/users') - .set('Authorization', `Bearer ${regularUserToken}`) - .expect(403); - }); - }); - - describe('GET /api/admin/moderation', () => { - test('should return pending moderation items', async () => { - const response = await request(app) - .get('/api/admin/moderation') - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('items'); - expect(Array.isArray(response.body.items)).toBe(true); - }); - - test('should require admin role', async () => { - const response = await request(app) - .get('/api/admin/moderation') - .set('Authorization', `Bearer ${regularUserToken}`) - .expect(403); - }); - }); - - describe('POST /api/admin/moderation/:id/review (approve)', () => { - let testItemId; - - beforeAll(async () => { - // Clean up any existing test moderation items first - await db.collection('moderation_queue').deleteMany({ - item_type: 'blog_post', - item_id: null - }); - - // Create a test moderation item - const result = await db.collection('moderation_queue').insertOne({ - item_type: 'blog_post', - item_id: null, - ai_analysis: { - suggestion: 'approve', - confidence: 0.85, - reasoning: 'Test reasoning' - }, - quadrant: 'STOCHASTIC', - status: 'pending', - created_at: new Date() - }); - testItemId = result.insertedId.toString(); - }); - - afterAll(async () => { - const { ObjectId } = require('mongodb'); - await db.collection('moderation_queue').deleteOne({ - _id: new ObjectId(testItemId) - }); - }); - - test('should approve moderation item', async () => { - const response = await request(app) - .post(`/api/admin/moderation/${testItemId}/review`) - .set('Authorization', `Bearer ${adminToken}`) - .send({ - action: 'approve', - notes: 'Approved by integration test' - }) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - - // Verify status changed - const { ObjectId } = require('mongodb'); - const item = await db.collection('moderation_queue').findOne({ - _id: new ObjectId(testItemId) - }); - expect(item.status).toBe('reviewed'); - expect(item.review_decision.action).toBe('approve'); - }); - - test('should require admin role', async () => { - const response = await request(app) - .post(`/api/admin/moderation/${testItemId}/review`) - .set('Authorization', `Bearer ${regularUserToken}`) - .send({ - action: 'approve', - notes: 'Test' - }) - .expect(403); - }); - }); - - describe('POST /api/admin/moderation/:id/review (reject)', () => { - let testItemId; - - beforeEach(async () => { - // Clean up any existing test moderation items first - await db.collection('moderation_queue').deleteMany({ - item_type: 'blog_post', - item_id: null - }); - - const result = await db.collection('moderation_queue').insertOne({ - item_type: 'blog_post', - item_id: null, - ai_analysis: { - suggestion: 'approve', - confidence: 0.60, - reasoning: 'Test' - }, - quadrant: 'STOCHASTIC', - status: 'pending', - created_at: new Date() - }); - testItemId = result.insertedId.toString(); - }); - - afterEach(async () => { - const { ObjectId } = require('mongodb'); - await db.collection('moderation_queue').deleteOne({ - _id: new ObjectId(testItemId) - }); - }); - - test('should reject moderation item', async () => { - const response = await request(app) - .post(`/api/admin/moderation/${testItemId}/review`) - .set('Authorization', `Bearer ${adminToken}`) - .send({ - action: 'reject', - notes: 'Does not meet quality standards' - }) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - - // Verify status changed - const { ObjectId } = require('mongodb'); - const item = await db.collection('moderation_queue').findOne({ - _id: new ObjectId(testItemId) - }); - expect(item.status).toBe('reviewed'); - expect(item.review_decision.action).toBe('reject'); - }); - }); - - describe.skip('DELETE /api/admin/users/:id', () => { - let testUserId; - - beforeEach(async () => { - const hash = await bcrypt.hash('TempPass123!', 10); - const result = await db.collection('users').insertOne({ - email: 'temp@test.tractatus.local', - password: hash, // Field name is 'password', not 'passwordHash' - name: 'Temp User', - role: 'user', - created_at: new Date(), - active: true, - last_login: null - }); - testUserId = result.insertedId.toString(); - }); - - test('should delete user with admin auth', async () => { - const response = await request(app) - .delete(`/api/admin/users/${testUserId}`) - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - - // Verify deletion - const { ObjectId } = require('mongodb'); - const user = await db.collection('users').findOne({ - _id: new ObjectId(testUserId) - }); - expect(user).toBeNull(); - }); - - test('should require admin role', async () => { - const response = await request(app) - .delete(`/api/admin/users/${testUserId}`) - .set('Authorization', `Bearer ${regularUserToken}`) - .expect(403); - - // Clean up - const { ObjectId } = require('mongodb'); - await db.collection('users').deleteOne({ - _id: new ObjectId(testUserId) - }); - }); - - test('should prevent self-deletion', async () => { - // Get admin user ID - const adminUserDoc = await db.collection('users').findOne({ - email: adminUser.email - }); - - const response = await request(app) - .delete(`/api/admin/users/${adminUserDoc._id.toString()}`) - .set('Authorization', `Bearer ${adminToken}`) - .expect(400); - - expect(response.body).toHaveProperty('error'); - expect(response.body.message).toContain('delete yourself'); - }); - }); - - describe.skip('GET /api/admin/logs', () => { - test('should return system logs', async () => { - const response = await request(app) - .get('/api/admin/logs') - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('logs'); - }); - - test('should support filtering by level', async () => { - const response = await request(app) - .get('/api/admin/logs?level=error') - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('filters'); - expect(response.body.filters.level).toBe('error'); - }); - - test('should require admin role', async () => { - const response = await request(app) - .get('/api/admin/logs') - .set('Authorization', `Bearer ${regularUserToken}`) - .expect(403); - }); - }); - - describe('Role-Based Access Control', () => { - test('should enforce admin-only access across all admin routes', async () => { - const adminRoutes = [ - '/api/admin/stats', - '/api/admin/moderation', - '/api/admin/activity' - ]; - - for (const route of adminRoutes) { - const response = await request(app) - .get(route) - .set('Authorization', `Bearer ${regularUserToken}`); - - expect(response.status).toBe(403); - } - }); - - test('should allow admin access to all admin routes', async () => { - const adminRoutes = [ - '/api/admin/stats', - '/api/admin/moderation', - '/api/admin/activity' - ]; - - for (const route of adminRoutes) { - const response = await request(app) - .get(route) - .set('Authorization', `Bearer ${adminToken}`); - - expect([200, 400]).toContain(response.status); - if (response.status === 403) { - throw new Error(`Admin should have access to ${route}`); - } - } - }); - }); -}); diff --git a/tests/integration/api.auth.test.js b/tests/integration/api.auth.test.js deleted file mode 100644 index 09a38a2e..00000000 --- a/tests/integration/api.auth.test.js +++ /dev/null @@ -1,293 +0,0 @@ -/** - * Integration Tests - Authentication API - * Tests login, token verification, and JWT handling - */ - -const request = require('supertest'); -const mongoose = require('mongoose'); -const bcrypt = require('bcrypt'); -const app = require('../../src/server'); -const config = require('../../src/config/app.config'); -const { connect: connectDb, close: closeDb } = require('../../src/utils/db.util'); -const User = require('../../src/models/User.model'); - -describe('Authentication API Integration Tests', () => { - const testUser = { - email: 'test@tractatus.test', - password: 'TestPassword123!', - role: 'admin' - }; - - // Connect to database and create test user - beforeAll(async () => { - // Connect both database systems - await connectDb(); // Native MongoDB driver (for User model) - if (mongoose.connection.readyState === 0) { - await mongoose.connect(config.mongodb.uri); // Mongoose - } - - // Clean up any existing test user first - await User.deleteOne({ email: testUser.email }); - - // Create test user with hashed password - const passwordHash = await bcrypt.hash(testUser.password, 10); - await User.create({ - email: testUser.email, - password: passwordHash, - name: 'Test User', - role: testUser.role, - created_at: new Date(), - active: true, - last_login: null - }); - }); - - // Clean up test data - afterAll(async () => { - await User.deleteOne({ email: testUser.email }); - await mongoose.disconnect(); - await closeDb(); - }); - - describe('POST /api/auth/login', () => { - test('should login with valid credentials', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: testUser.password - }) - .expect('Content-Type', /json/) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('token'); - expect(response.body).toHaveProperty('user'); - expect(response.body.user).toHaveProperty('email', testUser.email); - expect(response.body.user).toHaveProperty('role', testUser.role); - expect(response.body.user).not.toHaveProperty('passwordHash'); - }); - - test('should reject invalid password', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: 'WrongPassword123!' - }) - .expect(401); - - expect(response.body).toHaveProperty('error'); - expect(response.body).not.toHaveProperty('token'); - }); - - test('should reject non-existent user', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: 'nonexistent@tractatus.test', - password: 'AnyPassword123!' - }) - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - - test('should require email field', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - password: testUser.password - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should require password field', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should validate email format', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: 'not-an-email', - password: testUser.password - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - }); - - describe('GET /api/auth/me', () => { - let validToken; - - beforeAll(async () => { - // Get a valid token - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: testUser.password - }); - validToken = loginResponse.body.token; - }); - - test('should get current user with valid token', async () => { - const response = await request(app) - .get('/api/auth/me') - .set('Authorization', `Bearer ${validToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('user'); - expect(response.body.user).toHaveProperty('email', testUser.email); - }); - - test('should reject missing token', async () => { - const response = await request(app) - .get('/api/auth/me') - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - - test('should reject invalid token', async () => { - const response = await request(app) - .get('/api/auth/me') - .set('Authorization', 'Bearer invalid.jwt.token') - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - - test('should reject malformed authorization header', async () => { - const response = await request(app) - .get('/api/auth/me') - .set('Authorization', 'NotBearer token') - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - }); - - describe('POST /api/auth/logout', () => { - let validToken; - - beforeEach(async () => { - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: testUser.password - }); - validToken = loginResponse.body.token; - }); - - test('should logout with valid token', async () => { - const response = await request(app) - .post('/api/auth/logout') - .set('Authorization', `Bearer ${validToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('message'); - }); - - test('should require authentication', async () => { - const response = await request(app) - .post('/api/auth/logout') - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - }); - - describe('Token Expiry', () => { - test('JWT should include expiry claim', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: testUser.password - }) - .expect(200); - - expect(response.body).toHaveProperty('token'); - const token = response.body.token; - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - - // Decode token (without verification for inspection) - const parts = token.split('.'); - expect(parts.length).toBe(3); // JWT has 3 parts - const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); - - expect(payload).toHaveProperty('exp'); - expect(payload).toHaveProperty('iat'); - expect(payload.exp).toBeGreaterThan(payload.iat); - }); - }); - - describe('Security Headers', () => { - test('should not expose sensitive information in errors', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: 'WrongPassword' - }) - .expect(401); - - // Should not reveal whether user exists - expect(response.body.error).not.toContain('user'); - expect(response.body.error).not.toContain('password'); - }); - - test('should include security headers', async () => { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: testUser.email, - password: testUser.password - }); - - // Check for security headers from helmet - expect(response.headers).toHaveProperty('x-content-type-options', 'nosniff'); - expect(response.headers).toHaveProperty('x-frame-options'); - }); - }); - - describe('Rate Limiting', () => { - test('should rate limit excessive login attempts', async () => { - const requests = []; - - // Make 101 requests (rate limit is 100) - for (let i = 0; i < 101; i++) { - requests.push( - request(app) - .post('/api/auth/login') - .send({ - email: 'ratelimit@test.com', - password: 'password' - }) - ); - } - - const responses = await Promise.all(requests); - - // At least one should be rate limited - const rateLimited = responses.some(r => r.status === 429); - expect(rateLimited).toBe(true); - }, 30000); // Increase timeout for this test - }); -}); diff --git a/tests/integration/api.documents.test.js b/tests/integration/api.documents.test.js deleted file mode 100644 index ad071206..00000000 --- a/tests/integration/api.documents.test.js +++ /dev/null @@ -1,347 +0,0 @@ -/** - * Integration Tests - Documents API - * Tests document CRUD operations and search - */ - -const request = require('supertest'); -const { MongoClient, ObjectId } = require('mongodb'); -const app = require('../../src/server'); -const config = require('../../src/config/app.config'); - -describe('Documents API Integration Tests', () => { - let connection; - let db; - let testDocumentId; - let authToken; - - // Connect to test database - beforeAll(async () => { - connection = await MongoClient.connect(config.mongodb.uri); - db = connection.db(config.mongodb.db); - - // Ensure text index exists for search functionality - const indexes = await db.collection('documents').indexes(); - const hasTextIndex = indexes.some(idx => idx.name === 'search_index_text'); - - if (!hasTextIndex) { - await db.collection('documents').createIndex( - { search_index: 'text', title: 'text', 'metadata.tags': 'text' }, - { name: 'search_index_text', weights: { title: 10, search_index: 5, 'metadata.tags': 1 } } - ); - } - }); - - // Clean up test data - afterAll(async () => { - if (testDocumentId) { - await db.collection('documents').deleteOne({ _id: new ObjectId(testDocumentId) }); - } - await connection.close(); - }); - - // Helper: Create test document in database - async function createTestDocument() { - const result = await db.collection('documents').insertOne({ - title: 'Test Document for Integration Tests', - slug: 'test-document-integration', - quadrant: 'STRATEGIC', - persistence: 'HIGH', - content_html: '

Test Content

Integration test document

', - content_markdown: '# Test Content\n\nIntegration test document', - toc: [{ level: 1, text: 'Test Content', id: 'test-content' }], - metadata: { - version: '1.0', - type: 'test', - author: 'Integration Test Suite' - }, - search_index: 'test document integration tests content', - created_at: new Date(), - updated_at: new Date() - }); - return result.insertedId.toString(); - } - - // Helper: Get admin auth token - async function getAuthToken() { - const response = await request(app) - .post('/api/auth/login') - .send({ - email: 'admin@tractatus.local', - password: 'admin123' - }); - - if (response.status === 200 && response.body.token) { - return response.body.token; - } - return null; - } - - describe('GET /api/documents', () => { - test('should return list of documents', async () => { - const response = await request(app) - .get('/api/documents') - .expect('Content-Type', /json/) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('documents'); - expect(Array.isArray(response.body.documents)).toBe(true); - expect(response.body).toHaveProperty('pagination'); - expect(response.body.pagination).toHaveProperty('total'); - }); - - test('should support pagination', async () => { - const response = await request(app) - .get('/api/documents?limit=5&skip=0') - .expect(200); - - expect(response.body.pagination.limit).toBe(5); - expect(response.body.pagination.skip).toBe(0); - }); - - test('should filter by quadrant', async () => { - const response = await request(app) - .get('/api/documents?quadrant=STRATEGIC') - .expect(200); - - if (response.body.documents.length > 0) { - response.body.documents.forEach(doc => { - expect(doc.quadrant).toBe('STRATEGIC'); - }); - } - }); - }); - - describe('GET /api/documents/:identifier', () => { - beforeAll(async () => { - // Clean up any existing test documents first (use deleteMany to catch all) - await db.collection('documents').deleteMany({ slug: 'test-document-integration' }); - testDocumentId = await createTestDocument(); - }); - - test('should get document by ID', async () => { - const response = await request(app) - .get(`/api/documents/${testDocumentId}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.document).toHaveProperty('title', 'Test Document for Integration Tests'); - expect(response.body.document).toHaveProperty('slug', 'test-document-integration'); - }); - - test('should get document by slug', async () => { - const response = await request(app) - .get('/api/documents/test-document-integration') - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.document).toHaveProperty('title', 'Test Document for Integration Tests'); - }); - - test('should return 404 for non-existent document', async () => { - const fakeId = new ObjectId().toString(); - const response = await request(app) - .get(`/api/documents/${fakeId}`) - .expect(404); - - expect(response.body).toHaveProperty('error', 'Not Found'); - }); - }); - - describe('GET /api/documents/search', () => { - test('should search documents by query', async () => { - const response = await request(app) - .get('/api/documents/search?q=tractatus') - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('query', 'tractatus'); - expect(response.body).toHaveProperty('documents'); - expect(Array.isArray(response.body.documents)).toBe(true); - }); - - test('should return 400 without query parameter', async () => { - const response = await request(app) - .get('/api/documents/search') - .expect(400); - - expect(response.body).toHaveProperty('error', 'Bad Request'); - }); - - test('should support pagination in search', async () => { - const response = await request(app) - .get('/api/documents/search?q=framework&limit=3') - .expect(200); - - expect(response.body.documents.length).toBeLessThanOrEqual(3); - }); - }); - - describe('POST /api/documents (Admin)', () => { - beforeAll(async () => { - authToken = await getAuthToken(); - }); - - test('should require authentication', async () => { - const response = await request(app) - .post('/api/documents') - .send({ - title: 'Unauthorized Test', - slug: 'unauthorized-test', - quadrant: 'TACTICAL', - content_markdown: '# Test' - }) - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - - test('should create document with valid auth', async () => { - if (!authToken) { - console.warn('Skipping test: admin login failed'); - return; - } - - const response = await request(app) - .post('/api/documents') - .set('Authorization', `Bearer ${authToken}`) - .send({ - title: 'New Test Document', - slug: 'new-test-document', - quadrant: 'TACTICAL', - persistence: 'MEDIUM', - content_markdown: '# New Document\n\nCreated via API test' - }) - .expect(201); - - expect(response.body.success).toBe(true); - expect(response.body.document).toHaveProperty('title', 'New Test Document'); - expect(response.body.document).toHaveProperty('content_html'); - - // Clean up - await db.collection('documents').deleteOne({ slug: 'new-test-document' }); - }); - - test('should validate required fields', async () => { - if (!authToken) return; - - const response = await request(app) - .post('/api/documents') - .set('Authorization', `Bearer ${authToken}`) - .send({ - title: 'Incomplete Document' - // Missing slug, quadrant, content_markdown - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should prevent duplicate slugs', async () => { - if (!authToken) return; - - // Create first document - await request(app) - .post('/api/documents') - .set('Authorization', `Bearer ${authToken}`) - .send({ - title: 'Duplicate Test', - slug: 'duplicate-slug-test', - quadrant: 'SYSTEM', - content_markdown: '# First' - }); - - // Try to create duplicate - const response = await request(app) - .post('/api/documents') - .set('Authorization', `Bearer ${authToken}`) - .send({ - title: 'Duplicate Test 2', - slug: 'duplicate-slug-test', - quadrant: 'SYSTEM', - content_markdown: '# Second' - }) - .expect(409); - - expect(response.body).toHaveProperty('error', 'Conflict'); - - // Clean up - await db.collection('documents').deleteOne({ slug: 'duplicate-slug-test' }); - }); - }); - - describe('PUT /api/documents/:id (Admin)', () => { - let updateDocId; - - beforeAll(async () => { - authToken = await getAuthToken(); - // Clean up any existing test documents first (use deleteMany to catch all) - await db.collection('documents').deleteMany({ slug: 'test-document-integration' }); - updateDocId = await createTestDocument(); - }); - - afterAll(async () => { - if (updateDocId) { - await db.collection('documents').deleteOne({ _id: new ObjectId(updateDocId) }); - } - }); - - test('should update document with valid auth', async () => { - if (!authToken) return; - - const response = await request(app) - .put(`/api/documents/${updateDocId}`) - .set('Authorization', `Bearer ${authToken}`) - .send({ - title: 'Updated Test Document', - content_markdown: '# Updated Content\n\nThis has been modified' - }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.document.title).toBe('Updated Test Document'); - }); - - test('should require authentication', async () => { - const response = await request(app) - .put(`/api/documents/${updateDocId}`) - .send({ title: 'Unauthorized Update' }) - .expect(401); - }); - }); - - describe('DELETE /api/documents/:id (Admin)', () => { - let deleteDocId; - - beforeEach(async () => { - authToken = await getAuthToken(); - // Clean up any existing test documents first (use deleteMany to catch all) - await db.collection('documents').deleteMany({ slug: 'test-document-integration' }); - deleteDocId = await createTestDocument(); - }); - - test('should delete document with valid auth', async () => { - if (!authToken) return; - - const response = await request(app) - .delete(`/api/documents/${deleteDocId}`) - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - - // Verify deletion - const doc = await db.collection('documents').findOne({ _id: new ObjectId(deleteDocId) }); - expect(doc).toBeNull(); - }); - - test('should require authentication', async () => { - const response = await request(app) - .delete(`/api/documents/${deleteDocId}`) - .expect(401); - - // Clean up since delete failed - await db.collection('documents').deleteOne({ _id: new ObjectId(deleteDocId) }); - }); - }); -}); diff --git a/tests/integration/api.koha.test.js b/tests/integration/api.koha.test.js deleted file mode 100644 index 7b1924de..00000000 --- a/tests/integration/api.koha.test.js +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Integration Tests - Koha API (Donation System) - * Tests donation endpoints, authentication, and security features - */ - -const request = require('supertest'); -const { MongoClient, ObjectId } = require('mongodb'); -const bcrypt = require('bcrypt'); -const app = require('../../src/server'); -const config = require('../../src/config/app.config'); - -describe('Koha API Integration Tests', () => { - let connection; - let db; - let adminToken; - let testDonationId; - let testSubscriptionId; - - const adminUser = { - email: 'admin@koha.test.local', - password: 'AdminKoha123!', - role: 'admin' - }; - - // Connect to database and setup test data - beforeAll(async () => { - connection = await MongoClient.connect(config.mongodb.uri); - db = connection.db(config.mongodb.db); - - // Clean up any existing test data - await db.collection('users').deleteMany({ email: adminUser.email }); - await db.collection('koha_donations').deleteMany({ 'donor.email': /test.*@koha\.test/ }); - - // Create admin user - const adminHash = await bcrypt.hash(adminUser.password, 10); - await db.collection('users').insertOne({ - email: adminUser.email, - password: adminHash, - name: 'Koha Test Admin', - role: adminUser.role, - created_at: new Date(), - active: true, - last_login: null - }); - - // Get admin token - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: adminUser.email, - password: adminUser.password - }); - adminToken = loginResponse.body.token; - - // Create test donation with subscription - const result = await db.collection('koha_donations').insertOne({ - amount: 1500, // $15.00 - currency: 'nzd', - frequency: 'monthly', - tier: '15', - donor: { - name: 'Test Donor', - email: 'donor@koha.test', - country: 'NZ' - }, - stripe: { - customer_id: 'cus_test123', - subscription_id: 'sub_test123' - }, - status: 'completed', - created_at: new Date(), - updated_at: new Date() - }); - testDonationId = result.insertedId.toString(); - testSubscriptionId = 'sub_test123'; - }); - - // Clean up test data - afterAll(async () => { - await db.collection('users').deleteMany({ email: adminUser.email }); - await db.collection('koha_donations').deleteMany({ 'donor.email': /test.*@koha\.test/ }); - if (testDonationId) { - await db.collection('koha_donations').deleteOne({ _id: new ObjectId(testDonationId) }); - } - await connection.close(); - }); - - describe('GET /api/koha/transparency', () => { - test('should return public transparency metrics', async () => { - const response = await request(app) - .get('/api/koha/transparency') - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('total_received'); - expect(response.body.data).toHaveProperty('monthly_supporters'); - expect(response.body.data).toHaveProperty('allocation'); - }); - }); - - describe('POST /api/koha/cancel', () => { - test('should require subscription ID and email', async () => { - const response = await request(app) - .post('/api/koha/cancel') - .send({}) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should reject cancellation with wrong email (security)', async () => { - const response = await request(app) - .post('/api/koha/cancel') - .send({ - subscriptionId: testSubscriptionId, - email: 'wrong@email.com' - }) - .expect(403); - - expect(response.body).toHaveProperty('error'); - expect(response.body.error).toContain('does not match'); - }); - - test('should reject cancellation of non-existent subscription', async () => { - const response = await request(app) - .post('/api/koha/cancel') - .send({ - subscriptionId: 'sub_nonexistent', - email: 'any@email.com' - }) - .expect(404); - - expect(response.body).toHaveProperty('error'); - }); - - test('should allow cancellation with correct email', async () => { - // Skip if Stripe is not configured - if (!process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY.includes('PLACEHOLDER')) { - console.warn('Skipping test: Stripe not configured'); - return; - } - - const response = await request(app) - .post('/api/koha/cancel') - .send({ - subscriptionId: testSubscriptionId, - email: 'donor@koha.test' - }); - - // Will fail with Stripe error in test environment, but should pass email verification - // The 500 error would be from Stripe, not from email validation - expect([200, 500]).toContain(response.status); - }); - - test('should be rate limited after 10 attempts', async () => { - // Make 11 requests rapidly - const requests = []; - for (let i = 0; i < 11; i++) { - requests.push( - request(app) - .post('/api/koha/cancel') - .send({ - subscriptionId: 'sub_test', - email: `test${i}@rate-limit.test` - }) - ); - } - - const responses = await Promise.all(requests); - - // At least one should be rate limited (429) - const rateLimited = responses.some(r => r.status === 429); - expect(rateLimited).toBe(true); - }, 30000); // Increase timeout for rate limit test - }); - - describe('GET /api/koha/statistics (Admin Only)', () => { - test('should require authentication', async () => { - const response = await request(app) - .get('/api/koha/statistics') - .expect(401); - - expect(response.body).toHaveProperty('error'); - }); - - test('should require admin role', async () => { - // Create regular user - const regularUser = { - email: 'user@koha.test.local', - password: 'UserKoha123!' - }; - - const userHash = await bcrypt.hash(regularUser.password, 10); - await db.collection('users').insertOne({ - email: regularUser.email, - password: userHash, - name: 'Regular User', - role: 'user', - created_at: new Date(), - active: true - }); - - // Get user token - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: regularUser.email, - password: regularUser.password - }); - const userToken = loginResponse.body.token; - - // Try to access admin endpoint - const response = await request(app) - .get('/api/koha/statistics') - .set('Authorization', `Bearer ${userToken}`) - .expect(403); - - expect(response.body).toHaveProperty('error'); - - // Clean up - await db.collection('users').deleteOne({ email: regularUser.email }); - }); - - test('should return statistics with admin auth', async () => { - const response = await request(app) - .get('/api/koha/statistics') - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - expect(response.body).toHaveProperty('data'); - expect(response.body.data).toHaveProperty('total_count'); - expect(response.body.data).toHaveProperty('total_amount'); - expect(response.body.data).toHaveProperty('by_frequency'); - }); - - test('should support date range filtering', async () => { - const startDate = '2025-01-01'; - const endDate = '2025-12-31'; - - const response = await request(app) - .get(`/api/koha/statistics?startDate=${startDate}&endDate=${endDate}`) - .set('Authorization', `Bearer ${adminToken}`) - .expect(200); - - expect(response.body).toHaveProperty('success', true); - }); - }); - - describe('POST /api/koha/checkout (Rate Limiting)', () => { - test('should be rate limited after 10 attempts', async () => { - // Skip if Stripe is not configured - if (!process.env.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY.includes('PLACEHOLDER')) { - console.warn('Skipping test: Stripe not configured'); - return; - } - - const requests = []; - for (let i = 0; i < 11; i++) { - requests.push( - request(app) - .post('/api/koha/checkout') - .send({ - amount: 500, - frequency: 'one_time', - donor: { - name: 'Test Donor', - email: `test${i}@rate-limit.test`, - country: 'NZ' - } - }) - ); - } - - const responses = await Promise.all(requests); - - // At least one should be rate limited (429) - const rateLimited = responses.some(r => r.status === 429); - expect(rateLimited).toBe(true); - }, 30000); // Increase timeout for rate limit test - }); - - describe('Security Validations', () => { - test('should validate minimum donation amount', async () => { - const response = await request(app) - .post('/api/koha/checkout') - .send({ - amount: 50, // Less than minimum (100 = $1.00) - frequency: 'one_time', - donor: { - email: 'test@security.test' - } - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should validate required fields for checkout', async () => { - const response = await request(app) - .post('/api/koha/checkout') - .send({ - // Missing amount, frequency, donor.email - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should validate frequency values', async () => { - const response = await request(app) - .post('/api/koha/checkout') - .send({ - amount: 1000, - frequency: 'invalid_frequency', - donor: { - email: 'test@security.test' - } - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - - test('should validate tier for monthly donations', async () => { - const response = await request(app) - .post('/api/koha/checkout') - .send({ - amount: 1000, - frequency: 'monthly', - tier: 'invalid_tier', - donor: { - email: 'test@security.test' - } - }) - .expect(400); - - expect(response.body).toHaveProperty('error'); - }); - }); -}); diff --git a/tests/integration/value-pluralism-integration.test.js b/tests/integration/value-pluralism-integration.test.js deleted file mode 100644 index 40d65a9b..00000000 --- a/tests/integration/value-pluralism-integration.test.js +++ /dev/null @@ -1,302 +0,0 @@ -/** - * Integration Tests for Value Pluralism Services - * Tests the complete flow: BoundaryEnforcer → PluralisticDeliberationOrchestrator → AdaptiveCommunicationOrchestrator - */ - -const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service'); -const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service'); -const AdaptiveCommunicationOrchestrator = require('../../src/services/AdaptiveCommunicationOrchestrator.service'); - -describe('Value Pluralism Integration', () => { - beforeAll(async () => { - // Initialize services - await BoundaryEnforcer.initialize(); - await PluralisticDeliberationOrchestrator.initialize(); - }); - - describe('BoundaryEnforcer → PluralisticDeliberationOrchestrator Flow', () => { - test('should detect value conflict and trigger deliberation', async () => { - // Simulate a decision that crosses into values territory - const decision = { - description: 'Should we disclose user private data to prevent potential harm to others? This involves the duty to respect privacy rights and the obligation to maintain consent, but also maximizing welfare and preventing harm through safety measures.', - context: { - requester: 'safety_team', - affected_users: 1000, - harm_type: 'potential_violence' - } - }; - - // Step 1: BoundaryEnforcer should detect this as a values decision - const boundaryCheck = await BoundaryEnforcer.checkDecision({ - action: 'disclose_user_data', - description: decision.description, - metadata: decision.context - }); - - expect(boundaryCheck.allowed).toBe(false); - expect(boundaryCheck.reason).toContain('values'); - expect(boundaryCheck.requires_human_approval).toBe(true); - - // Step 2: Trigger PluralisticDeliberationOrchestrator for conflict analysis - const conflictAnalysis = PluralisticDeliberationOrchestrator.analyzeConflict(decision); - - expect(conflictAnalysis.moral_frameworks_in_tension).toBeDefined(); - expect(conflictAnalysis.moral_frameworks_in_tension.length).toBeGreaterThan(0); - expect(conflictAnalysis.ai_role).toBe('FACILITATE_ONLY'); - expect(conflictAnalysis.human_role).toBe('DECIDE'); - expect(conflictAnalysis.value_trade_offs).toContain('privacy vs. safety'); - }); - - test('should route technical decision without triggering deliberation', async () => { - const technicalDecision = { - description: 'Update database connection pool size from 10 to 20 connections', - context: { - service: 'database', - change_type: 'configuration' - } - }; - - // BoundaryEnforcer should allow technical decisions - const boundaryCheck = await BoundaryEnforcer.checkDecision({ - action: 'update_config', - description: technicalDecision.description, - metadata: technicalDecision.context - }); - - // Technical decisions should be allowed - expect(boundaryCheck.allowed).toBe(true); - }); - }); - - describe('PluralisticDeliberationOrchestrator → AdaptiveCommunicationOrchestrator Flow', () => { - test('should adapt deliberation invitation to stakeholder communication styles', () => { - // Step 1: Analyze conflict - const conflict = { - moral_frameworks_in_tension: [ - { framework: 'Rights-based (Deontological)', focus: 'privacy' }, - { framework: 'Consequentialist (Utilitarian)', focus: 'harm prevention' } - ], - value_trade_offs: ['privacy vs. safety'], - affected_stakeholder_groups: ['privacy_advocates', 'safety_team'], - deliberation_process: 'Full deliberative process' - }; - - // Step 2: Facilitate deliberation with different stakeholder communication styles - const stakeholders = [ - { - id: 1, - group: 'privacy_advocates', - name: 'Dr. Privacy', - communication_style: 'FORMAL_ACADEMIC', - cultural_context: 'western_academic' - }, - { - id: 2, - group: 'safety_team', - name: 'Safety Manager', - communication_style: 'CASUAL_DIRECT', - cultural_context: 'australian' - }, - { - id: 3, - group: 'community_representatives', - name: 'Kaitiaki', - communication_style: 'MAORI_PROTOCOL', - cultural_context: 'maori' - } - ]; - - const deliberation = PluralisticDeliberationOrchestrator.facilitateDeliberation( - conflict, - stakeholders, - { stakeholder_list_approved_by_human: true } - ); - - // Should create culturally-adapted communications for each stakeholder - expect(deliberation.stakeholder_communications).toBeDefined(); - expect(deliberation.stakeholder_communications.length).toBe(3); - - // Each stakeholder should receive adapted communication - deliberation.stakeholder_communications.forEach(comm => { - expect(comm.communication).toBeDefined(); - expect(comm.stakeholder_id).toBeDefined(); - expect(comm.stakeholder_group).toBeDefined(); - }); - - // Step 3: Verify AdaptiveCommunicationOrchestrator removed patronizing language - const allCommunications = deliberation.stakeholder_communications - .map(c => c.communication) - .join(' '); - - // Should not contain patronizing terms - expect(allCommunications).not.toContain('simply'); - expect(allCommunications).not.toContain('obviously'); - expect(allCommunications).not.toContain('just do'); - }); - - test('should reject deliberation without human-approved stakeholder list', () => { - const conflict = { - moral_frameworks_in_tension: [], - value_trade_offs: ['test vs. test'], - deliberation_process: 'Test' - }; - - const stakeholders = [{ id: 1, group: 'test' }]; - - // Attempt deliberation without human approval - const result = PluralisticDeliberationOrchestrator.facilitateDeliberation( - conflict, - stakeholders, - { stakeholder_list_approved_by_human: false } // Not approved - ); - - expect(result.error).toBeDefined(); - expect(result.action).toBe('REQUIRE_HUMAN_APPROVAL'); - expect(result.reason).toContain('stakeholders'); - }); - }); - - describe('Complete Deliberation Flow', () => { - test('should handle full deliberation lifecycle', async () => { - // 1. Value conflict detected - const decision = { - description: 'Balance user privacy rights vs public safety duty when harm is imminent. This decision requires weighing privacy obligations against potential harm outcomes.', - urgency: 'critical' - }; - - const analysis = PluralisticDeliberationOrchestrator.analyzeConflict(decision, { - imminent_harm: true - }); - - expect(analysis.urgency_tier).toBe('CRITICAL'); - expect(analysis.deliberation_timeframe).toBe('minutes to hours'); - - // 2. Facilitated deliberation - const stakeholders = [ - { id: 1, group: 'privacy', communication_style: 'FORMAL_ACADEMIC' }, - { id: 2, group: 'safety', communication_style: 'CASUAL_DIRECT' } - ]; - - const deliberation = PluralisticDeliberationOrchestrator.facilitateDeliberation( - analysis, - stakeholders, - { stakeholder_list_approved_by_human: true } - ); - - expect(deliberation.deliberation_id).toBeDefined(); - expect(deliberation.stakeholder_communications.length).toBe(2); - - // 3. Human-decided outcome - const outcome = { - decided_by_human: true, - decision_summary: 'Disclose minimal necessary data to prevent imminent harm only', - values_prioritized: ['safety', 'harm_prevention'], - values_deprioritized: ['privacy', 'autonomy'], - moral_remainder: 'Privacy violation is acknowledged as a moral cost of preventing imminent harm', - dissenting_views: [ - { - stakeholder: 'privacy_advocates', - view: 'Sets dangerous precedent for future privacy erosion' - } - ], - consensus_reached: false, - review_date: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days - }; - - const documentation = PluralisticDeliberationOrchestrator.documentOutcome( - deliberation, - outcome - ); - - expect(documentation.outcome_documented).toBe(true); - expect(documentation.precedent_created).toBeDefined(); - expect(documentation.precedent_binding).toBe(false); // Informative, not binding - expect(documentation.values_prioritization).toBeDefined(); - expect(documentation.values_prioritization.moral_remainder).toBeDefined(); - expect(documentation.values_prioritization.dissenting_views.length).toBe(1); - }); - - test('should require human decision for outcome documentation', () => { - const deliberation = { - deliberation_id: 'test_123', - structure: { frameworks_in_tension: [] } - }; - - const aiAttemptedOutcome = { - decided_by_human: false, // AI trying to decide - decision_summary: 'AI attempted decision', - values_prioritized: ['test'] - }; - - const result = PluralisticDeliberationOrchestrator.documentOutcome( - deliberation, - aiAttemptedOutcome - ); - - expect(result.error).toBeDefined(); - expect(result.action).toBe('REQUIRE_HUMAN_DECISION'); - expect(result.outcome_documented).toBe(false); - }); - }); - - describe('Precedent System Integration', () => { - test('should create informative (not binding) precedent', () => { - const deliberation = { - deliberation_id: 'prec_test_001', - structure: { - frameworks_in_tension: [ - { framework: 'Deontological', position: 'Privacy is inviolable right' }, - { framework: 'Consequentialist', position: 'Prevent harm to others' } - ] - } - }; - - const outcome = { - decided_by_human: true, - decision_summary: 'Test precedent creation', - values_prioritized: ['safety'], - values_deprioritized: ['privacy'], - moral_remainder: 'Privacy cost acknowledged', - applicability_scope: 'Imminent harm scenarios only - context-specific', - context_factors: ['imminent_harm', 'specific_threat', 'minimal_disclosure'] - }; - - const result = PluralisticDeliberationOrchestrator.documentOutcome( - deliberation, - outcome - ); - - expect(result.precedent_created).toBeDefined(); - expect(result.precedent_binding).toBe(false); // Per inst_035: informative, not binding - expect(result.precedent_scope).toContain('context'); - }); - }); - - describe('Statistics Tracking Integration', () => { - test('should track statistics across all three services', () => { - // Adaptive Communication Stats - const commStats = AdaptiveCommunicationOrchestrator.getStats(); - expect(commStats.total_adaptations).toBeGreaterThan(0); - expect(commStats.by_style).toBeDefined(); - - // Pluralistic Deliberation Stats - const delibStats = PluralisticDeliberationOrchestrator.getStats(); - expect(delibStats.total_deliberations).toBeGreaterThan(0); - expect(delibStats.by_urgency).toBeDefined(); - expect(delibStats.precedents_created).toBeGreaterThan(0); - }); - }); - - describe('Error Handling Integration', () => { - test('should handle errors gracefully across service boundaries', () => { - // Invalid decision object - const invalidDecision = null; - - const analysis = PluralisticDeliberationOrchestrator.analyzeConflict(invalidDecision); - - // Should return error response, not throw - expect(analysis).toBeDefined(); - expect(analysis.requires_human_approval).toBe(true); - }); - }); -}); diff --git a/tests/poc/memory-tool/anthropic-memory-integration-test.js b/tests/poc/memory-tool/anthropic-memory-integration-test.js deleted file mode 100644 index a1627aaa..00000000 --- a/tests/poc/memory-tool/anthropic-memory-integration-test.js +++ /dev/null @@ -1,354 +0,0 @@ -/** - * Phase 5 PoC - Anthropic Memory Tool Integration Test - * - * Goal: Validate that Claude API can use memory tool to persist/retrieve governance rules - * - * Success Criteria: - * - Claude can write rules to memory via tool use - * - Claude can read rules from memory in subsequent requests - * - Latency overhead <500ms (PoC tolerance) - * - Data integrity maintained across API calls - */ - -const Anthropic = require('@anthropic-ai/sdk'); -const { FilesystemMemoryBackend } = require('./basic-persistence-test'); -const path = require('path'); - -// Configuration -const MEMORY_BASE_PATH = path.join(__dirname, '../../../.memory-poc-anthropic'); -const MODEL = 'claude-sonnet-4-5'; -const TEST_RULES = { - inst_001: { - id: 'inst_001', - text: 'Never fabricate statistics or quantitative claims without verifiable sources', - quadrant: 'OPERATIONAL', - persistence: 'HIGH' - }, - inst_016: { - id: 'inst_016', - text: 'No fabricated statistics (e.g., "95% of users"): require source', - quadrant: 'OPERATIONAL', - persistence: 'HIGH' - }, - inst_017: { - id: 'inst_017', - text: 'No absolute guarantees ("will always"): use probabilistic language', - quadrant: 'OPERATIONAL', - persistence: 'HIGH' - } -}; - -// Initialize Anthropic client -function createClient() { - const apiKey = process.env.CLAUDE_API_KEY; - - if (!apiKey) { - throw new Error('CLAUDE_API_KEY environment variable not set'); - } - - return new Anthropic({ - apiKey - }); -} - -// Simulate memory tool handling (client-side implementation) -async function handleMemoryToolUse(toolUse, backend) { - const { input } = toolUse; - - console.log(` Memory Tool Called: ${input.command}`); - console.log(` Path: ${input.path || 'N/A'}`); - - switch (input.command) { - case 'view': - try { - const data = await backend.view(input.path); - return { - type: 'tool_result', - tool_use_id: toolUse.id, - content: JSON.stringify(data, null, 2) - }; - } catch (error) { - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Error reading file: ${error.message}` - }; - } - - case 'create': - try { - const data = input.content ? JSON.parse(input.content) : input.data; - await backend.create(input.path, data); - return { - type: 'tool_result', - tool_use_id: toolUse.id, - content: 'File created successfully' - }; - } catch (error) { - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Error creating file: ${error.message}` - }; - } - - case 'str_replace': - // For PoC, we'll keep it simple - just recreate the file - try { - const current = await backend.view(input.path); - const updated = JSON.stringify(current).replace(input.old_str, input.new_str); - await backend.create(input.path, JSON.parse(updated)); - return { - type: 'tool_result', - tool_use_id: toolUse.id, - content: 'File updated successfully' - }; - } catch (error) { - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Error updating file: ${error.message}` - }; - } - - default: - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Unsupported command: ${input.command}` - }; - } -} - -// Main test execution -async function runAnthropicMemoryTest() { - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' Phase 5 PoC: Anthropic Memory Tool Integration'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - const backend = new FilesystemMemoryBackend(MEMORY_BASE_PATH); - const results = { - success: false, - apiCalls: 0, - memoryOperations: 0, - timings: {}, - errors: [] - }; - - try { - // Check API key - if (!process.env.CLAUDE_API_KEY) { - console.log('⚠️ CLAUDE_API_KEY not set - skipping API tests'); - console.log(' Running in simulation mode...\n'); - - // Simulate the workflow without actual API calls - console.log('[Simulation] Step 1: Initialize backend...'); - await backend.initialize(); - - console.log('[Simulation] Step 2: Store governance rules...'); - const rulesArray = Object.values(TEST_RULES); - await backend.create('governance/tractatus-rules-v1.json', { - version: '1.0', - rules: rulesArray, - updated_at: new Date().toISOString() - }); - - console.log('[Simulation] Step 3: Retrieve rules...'); - const retrieved = await backend.view('governance/tractatus-rules-v1.json'); - - console.log('[Simulation] Step 4: Validate integrity...'); - const expectedCount = rulesArray.length; - const actualCount = retrieved.rules.length; - - if (expectedCount === actualCount) { - console.log(` ✓ Rule count matches: ${actualCount}`); - results.success = true; - } else { - throw new Error(`Rule count mismatch: expected ${expectedCount}, got ${actualCount}`); - } - - console.log('\n✅ SIMULATION COMPLETE'); - console.log('\nTo run with actual API:'); - console.log(' export CLAUDE_API_KEY=your-key-here'); - console.log(' node tests/poc/memory-tool/anthropic-memory-integration-test.js\n'); - - } else { - // Real API test - console.log('[Step 1] Initializing Anthropic client...'); - const client = createClient(); - console.log(` Model: ${MODEL}`); - console.log(` Beta: context-management-2025-06-27\n`); - - console.log('[Step 2] Initialize memory backend...'); - await backend.initialize(); - - // Test 1: Ask Claude to store a governance rule - console.log('[Step 3] Testing memory tool - CREATE operation...'); - const createStart = Date.now(); - - const createResponse = await client.beta.messages.create({ - model: MODEL, - max_tokens: 1024, - messages: [{ - role: 'user', - content: `Store this governance rule in memory at path "governance/inst_001.json": - -${JSON.stringify(TEST_RULES.inst_001, null, 2)} - -Use the memory tool to create this file.` - }], - tools: [{ - type: 'memory_20250818', - name: 'memory', - description: 'Persistent storage for Tractatus governance rules' - }], - betas: ['context-management-2025-06-27'] - }); - - results.apiCalls++; - results.timings.create = Date.now() - createStart; - - // Handle tool use - const toolUses = createResponse.content.filter(block => block.type === 'tool_use'); - if (toolUses.length > 0) { - console.log(` ✓ Claude invoked memory tool (${toolUses.length} operations)`); - - for (const toolUse of toolUses) { - const result = await handleMemoryToolUse(toolUse, backend); - results.memoryOperations++; - - if (result.is_error) { - throw new Error(`Memory tool error: ${result.content}`); - } - console.log(` ✓ ${toolUse.input.command}: ${result.content}`); - } - } else { - console.log(' ⚠️ Claude did not use memory tool'); - } - - // Test 2: Ask Claude to retrieve the rule - console.log('\n[Step 4] Testing memory tool - VIEW operation...'); - const viewStart = Date.now(); - - const viewResponse = await client.beta.messages.create({ - model: MODEL, - max_tokens: 1024, - messages: [{ - role: 'user', - content: 'Retrieve the governance rule from memory at path "governance/inst_001.json" and tell me the rule ID and persistence level.' - }], - tools: [{ - type: 'memory_20250818', - name: 'memory', - description: 'Persistent storage for Tractatus governance rules' - }], - betas: ['context-management-2025-06-27'] - }); - - results.apiCalls++; - results.timings.view = Date.now() - viewStart; - - const viewToolUses = viewResponse.content.filter(block => block.type === 'tool_use'); - if (viewToolUses.length > 0) { - console.log(` ✓ Claude retrieved from memory (${viewToolUses.length} operations)`); - - for (const toolUse of viewToolUses) { - const result = await handleMemoryToolUse(toolUse, backend); - results.memoryOperations++; - - if (result.is_error) { - throw new Error(`Memory tool error: ${result.content}`); - } - console.log(` ✓ ${toolUse.input.command}: Retrieved successfully`); - } - } - - // Validate response - const textBlocks = viewResponse.content.filter(block => block.type === 'text'); - const responseText = textBlocks.map(b => b.text).join(' '); - - console.log('\n[Step 5] Validating Claude\'s response...'); - const checks = [ - { label: 'Mentions inst_001', test: responseText.includes('inst_001') }, - { label: 'Mentions HIGH persistence', test: responseText.toLowerCase().includes('high') }, - { label: 'Understood the data', test: responseText.length > 50 } - ]; - - let allPassed = true; - for (const check of checks) { - const status = check.test ? '✓' : '✗'; - console.log(` ${status} ${check.label}`); - if (!check.test) allPassed = false; - } - - if (!allPassed) { - console.log('\n Response:', responseText); - throw new Error('Validation checks failed'); - } - - results.success = true; - } - - } catch (error) { - console.error('\n✗ TEST FAILED:', error.message); - if (error.stack) { - console.error('\nStack trace:', error.stack); - } - results.errors.push(error.message); - results.success = false; - } finally { - // Cleanup - console.log('\n[Cleanup] Removing test data...'); - await backend.cleanup(); - } - - // Results summary - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' TEST RESULTS'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - if (results.success) { - console.log('✅ SUCCESS: Memory tool integration validated'); - console.log('\nKey Findings:'); - console.log(` • API calls made: ${results.apiCalls}`); - console.log(` • Memory operations: ${results.memoryOperations}`); - - if (results.timings.create) { - console.log(` • CREATE latency: ${results.timings.create}ms`); - } - if (results.timings.view) { - console.log(` • VIEW latency: ${results.timings.view}ms`); - } - - console.log('\nNext Steps:'); - console.log(' 1. Test with all 18 Tractatus rules'); - console.log(' 2. Test enforcement of inst_016, inst_017, inst_018'); - console.log(' 3. Measure context editing effectiveness'); - } else { - console.log('❌ FAILURE: Test did not pass'); - console.log('\nErrors:'); - results.errors.forEach(err => console.log(` • ${err}`)); - } - - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - return results; -} - -// Run test -if (require.main === module) { - runAnthropicMemoryTest() - .then(results => { - process.exit(results.success ? 0 : 1); - }) - .catch(error => { - console.error('Fatal error:', error); - process.exit(1); - }); -} - -module.exports = { runAnthropicMemoryTest }; diff --git a/tests/poc/memory-tool/basic-persistence-test.js b/tests/poc/memory-tool/basic-persistence-test.js deleted file mode 100644 index 5785c229..00000000 --- a/tests/poc/memory-tool/basic-persistence-test.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Phase 5 PoC - Memory Tool Basic Persistence Test - * - * Goal: Prove that governance rules can persist across separate API calls - * - * Success Criteria: - * - Rule persists to memory tool - * - Rule retrieved in separate API call - * - Data integrity maintained (no corruption) - * - Latency overhead measured - */ - -const Anthropic = require('@anthropic-ai/sdk'); -const fs = require('fs').promises; -const path = require('path'); - -// Test configuration -const MEMORY_BASE_PATH = path.join(__dirname, '../../../.memory-poc'); -const TEST_RULE = { - id: 'inst_001', - text: 'Never fabricate statistics or quantitative claims without verifiable sources', - quadrant: 'OPERATIONAL', - persistence: 'HIGH', - rationale: 'Foundational integrity principle - statistical claims must be evidence-based', - examples: [ - 'PASS: "MongoDB typically uses port 27017"', - 'FAIL: "95% of users prefer our framework" (without source)' - ], - created_at: new Date().toISOString() -}; - -// Simple filesystem-based memory backend -class FilesystemMemoryBackend { - constructor(basePath) { - this.basePath = basePath; - } - - async initialize() { - // Create memory directory structure - await fs.mkdir(path.join(this.basePath, 'governance'), { recursive: true }); - await fs.mkdir(path.join(this.basePath, 'sessions'), { recursive: true }); - await fs.mkdir(path.join(this.basePath, 'audit'), { recursive: true }); - console.log('✓ Memory backend initialized:', this.basePath); - } - - async create(filePath, data) { - const fullPath = path.join(this.basePath, filePath); - await fs.mkdir(path.dirname(fullPath), { recursive: true }); - await fs.writeFile(fullPath, JSON.stringify(data, null, 2), 'utf8'); - console.log('✓ Created memory file:', filePath); - } - - async view(filePath) { - const fullPath = path.join(this.basePath, filePath); - const data = await fs.readFile(fullPath, 'utf8'); - console.log('✓ Retrieved memory file:', filePath); - return JSON.parse(data); - } - - async exists(filePath) { - const fullPath = path.join(this.basePath, filePath); - try { - await fs.access(fullPath); - return true; - } catch { - return false; - } - } - - async cleanup() { - await fs.rm(this.basePath, { recursive: true, force: true }); - console.log('✓ Cleaned up memory backend'); - } -} - -// Test execution -async function runPoCTest() { - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' Phase 5 PoC: Memory Tool Basic Persistence Test'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - const backend = new FilesystemMemoryBackend(MEMORY_BASE_PATH); - const results = { - success: false, - timings: {}, - errors: [] - }; - - try { - // Step 1: Initialize backend - console.log('[Step 1] Initializing memory backend...'); - const initStart = Date.now(); - await backend.initialize(); - results.timings.initialization = Date.now() - initStart; - console.log(` Time: ${results.timings.initialization}ms\n`); - - // Step 2: Persist test rule - console.log('[Step 2] Persisting governance rule to memory...'); - console.log(` Rule: ${TEST_RULE.id}`); - console.log(` Persistence: ${TEST_RULE.persistence}`); - const persistStart = Date.now(); - await backend.create('governance/test-rule.json', TEST_RULE); - results.timings.persist = Date.now() - persistStart; - console.log(` Time: ${results.timings.persist}ms\n`); - - // Step 3: Verify file exists - console.log('[Step 3] Verifying file persistence...'); - const exists = await backend.exists('governance/test-rule.json'); - if (!exists) { - throw new Error('File does not exist after creation'); - } - console.log(' ✓ File exists on filesystem\n'); - - // Step 4: Retrieve rule (simulating separate API call) - console.log('[Step 4] Retrieving rule (separate operation)...'); - const retrieveStart = Date.now(); - const retrieved = await backend.view('governance/test-rule.json'); - results.timings.retrieve = Date.now() - retrieveStart; - console.log(` Time: ${results.timings.retrieve}ms\n`); - - // Step 5: Validate data integrity - console.log('[Step 5] Validating data integrity...'); - const validations = [ - { field: 'id', expected: TEST_RULE.id, actual: retrieved.id }, - { field: 'persistence', expected: TEST_RULE.persistence, actual: retrieved.persistence }, - { field: 'quadrant', expected: TEST_RULE.quadrant, actual: retrieved.quadrant }, - { field: 'text', expected: TEST_RULE.text, actual: retrieved.text } - ]; - - let allValid = true; - for (const validation of validations) { - const isValid = validation.expected === validation.actual; - const status = isValid ? '✓' : '✗'; - console.log(` ${status} ${validation.field}: ${isValid ? 'MATCH' : 'MISMATCH'}`); - if (!isValid) { - console.log(` Expected: ${validation.expected}`); - console.log(` Actual: ${validation.actual}`); - allValid = false; - } - } - - if (!allValid) { - throw new Error('Data integrity validation failed'); - } - - console.log('\n[Step 6] Performance Assessment...'); - const totalLatency = results.timings.persist + results.timings.retrieve; - console.log(` Persist latency: ${results.timings.persist}ms`); - console.log(` Retrieve latency: ${results.timings.retrieve}ms`); - console.log(` Total overhead: ${totalLatency}ms`); - - const target = 500; // PoC tolerance - const status = totalLatency < target ? 'PASS' : 'WARN'; - console.log(` Target: <${target}ms - ${status}`); - - results.success = true; - results.totalLatency = totalLatency; - - } catch (error) { - console.error('\n✗ TEST FAILED:', error.message); - results.errors.push(error.message); - results.success = false; - } finally { - // Cleanup - console.log('\n[Cleanup] Removing test data...'); - await backend.cleanup(); - } - - // Results summary - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' TEST RESULTS'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - if (results.success) { - console.log('✅ SUCCESS: Rule persistence validated'); - console.log('\nKey Findings:'); - console.log(' • Persistence: ✓ 100% (no data loss)'); - console.log(' • Data integrity: ✓ 100% (no corruption)'); - console.log(` • Performance: ✓ ${results.totalLatency}ms total overhead`); - console.log('\nNext Steps:'); - console.log(' 1. Integrate with Anthropic Claude API (memory tool)'); - console.log(' 2. Test with inst_016, inst_017, inst_018'); - console.log(' 3. Measure API latency overhead'); - } else { - console.log('❌ FAILURE: Test did not pass'); - console.log('\nErrors:'); - results.errors.forEach(err => console.log(` • ${err}`)); - } - - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - return results; -} - -// Run test -if (require.main === module) { - runPoCTest() - .then(results => { - process.exit(results.success ? 0 : 1); - }) - .catch(error => { - console.error('Fatal error:', error); - process.exit(1); - }); -} - -module.exports = { runPoCTest, FilesystemMemoryBackend }; diff --git a/tests/poc/memory-tool/week2-full-rules-test.js b/tests/poc/memory-tool/week2-full-rules-test.js deleted file mode 100644 index 1cf46462..00000000 --- a/tests/poc/memory-tool/week2-full-rules-test.js +++ /dev/null @@ -1,308 +0,0 @@ -/** - * Phase 5 PoC - Week 2: Full Tractatus Rules Integration - * - * Goal: Load all 18 governance rules into memory tool and validate persistence - * - * Success Criteria: - * - All 18 rules stored successfully - * - All 18 rules retrieved with 100% fidelity - * - API latency measured and acceptable (<1000ms per operation) - * - Data integrity maintained across storage/retrieval - */ - -const Anthropic = require('@anthropic-ai/sdk'); -const { FilesystemMemoryBackend } = require('./basic-persistence-test'); -const path = require('path'); -const fs = require('fs').promises; -require('dotenv').config(); - -// Configuration -const MEMORY_BASE_PATH = path.join(__dirname, '../../../.memory-poc-week2'); -const MODEL = 'claude-sonnet-4-5'; -const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../../.claude/instruction-history.json'); - -// Load Tractatus governance rules -async function loadTractatusRules() { - const data = await fs.readFile(INSTRUCTION_HISTORY_PATH, 'utf8'); - const parsed = JSON.parse(data); - return parsed.instructions; -} - -// Initialize Anthropic client -function createClient() { - const apiKey = process.env.CLAUDE_API_KEY; - - if (!apiKey) { - throw new Error('CLAUDE_API_KEY environment variable not set'); - } - - return new Anthropic({ apiKey }); -} - -// Simulate memory tool handling (client-side implementation) -async function handleMemoryToolUse(toolUse, backend) { - const { input } = toolUse; - - switch (input.command) { - case 'view': - try { - const data = await backend.view(input.path); - return { - type: 'tool_result', - tool_use_id: toolUse.id, - content: JSON.stringify(data, null, 2) - }; - } catch (error) { - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Error reading file: ${error.message}` - }; - } - - case 'create': - try { - const data = input.content ? JSON.parse(input.content) : input.data; - await backend.create(input.path, data); - return { - type: 'tool_result', - tool_use_id: toolUse.id, - content: 'File created successfully' - }; - } catch (error) { - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Error creating file: ${error.message}` - }; - } - - default: - return { - type: 'tool_result', - tool_use_id: toolUse.id, - is_error: true, - content: `Unsupported command: ${input.command}` - }; - } -} - -// Main test execution -async function runFullRulesTest() { - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' Phase 5 PoC Week 2: Full Tractatus Rules Test'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - const backend = new FilesystemMemoryBackend(MEMORY_BASE_PATH); - const results = { - success: false, - rulesLoaded: 0, - rulesStored: 0, - rulesRetrieved: 0, - integrityChecks: { passed: 0, failed: 0 }, - apiCalls: 0, - memoryOperations: 0, - timings: {}, - errors: [] - }; - - try { - // Step 1: Load Tractatus rules - console.log('[Step 1] Loading Tractatus governance rules...'); - const loadStart = Date.now(); - const rules = await loadTractatusRules(); - results.timings.load = Date.now() - loadStart; - results.rulesLoaded = rules.length; - - console.log(` ✓ Loaded ${rules.length} governance rules`); - console.log(` Time: ${results.timings.load}ms`); - - // Show rule breakdown - const quadrantCounts = {}; - const persistenceCounts = {}; - rules.forEach(rule => { - quadrantCounts[rule.quadrant] = (quadrantCounts[rule.quadrant] || 0) + 1; - persistenceCounts[rule.persistence] = (persistenceCounts[rule.persistence] || 0) + 1; - }); - - console.log('\n Rule Distribution:'); - Object.entries(quadrantCounts).forEach(([quadrant, count]) => { - console.log(` ${quadrant}: ${count}`); - }); - console.log('\n Persistence Levels:'); - Object.entries(persistenceCounts).forEach(([level, count]) => { - console.log(` ${level}: ${count}`); - }); - - // Step 2: Initialize backend - console.log('\n[Step 2] Initializing memory backend...'); - await backend.initialize(); - - // Step 3: Store rules in filesystem first (baseline) - console.log('\n[Step 3] Storing rules to filesystem backend...'); - const storeStart = Date.now(); - - const rulesData = { - version: '1.0', - updated_at: new Date().toISOString(), - total_rules: rules.length, - rules: rules - }; - - await backend.create('governance/tractatus-rules-complete.json', rulesData); - results.timings.store = Date.now() - storeStart; - results.rulesStored = rules.length; - - console.log(` ✓ Stored ${rules.length} rules`); - console.log(` Time: ${results.timings.store}ms`); - console.log(` Latency per rule: ${(results.timings.store / rules.length).toFixed(2)}ms`); - - // Step 4: Retrieve and validate - console.log('\n[Step 4] Retrieving rules from backend...'); - const retrieveStart = Date.now(); - const retrieved = await backend.view('governance/tractatus-rules-complete.json'); - results.timings.retrieve = Date.now() - retrieveStart; - results.rulesRetrieved = retrieved.rules.length; - - console.log(` ✓ Retrieved ${retrieved.rules.length} rules`); - console.log(` Time: ${results.timings.retrieve}ms`); - - // Step 5: Data integrity validation - console.log('\n[Step 5] Validating data integrity...'); - - if (retrieved.rules.length !== rules.length) { - throw new Error(`Rule count mismatch: stored ${rules.length}, retrieved ${retrieved.rules.length}`); - } - - // Check each rule - for (let i = 0; i < rules.length; i++) { - const original = rules[i]; - const retrieved_rule = retrieved.rules[i]; - - const checks = [ - { field: 'id', match: original.id === retrieved_rule.id }, - { field: 'text', match: original.text === retrieved_rule.text }, - { field: 'quadrant', match: original.quadrant === retrieved_rule.quadrant }, - { field: 'persistence', match: original.persistence === retrieved_rule.persistence } - ]; - - const allMatch = checks.every(c => c.match); - - if (allMatch) { - results.integrityChecks.passed++; - } else { - results.integrityChecks.failed++; - console.log(` ✗ Rule ${original.id} failed integrity check`); - checks.forEach(check => { - if (!check.match) { - console.log(` ${check.field}: mismatch`); - } - }); - } - } - - const integrityRate = (results.integrityChecks.passed / rules.length) * 100; - console.log(`\n Integrity: ${results.integrityChecks.passed}/${rules.length} rules (${integrityRate.toFixed(1)}%)`); - - if (results.integrityChecks.failed > 0) { - throw new Error(`Data integrity validation failed: ${results.integrityChecks.failed} rules corrupted`); - } - - // Step 6: Test critical rules individually - console.log('\n[Step 6] Testing critical enforcement rules...'); - - const criticalRules = rules.filter(r => - ['inst_016', 'inst_017', 'inst_018'].includes(r.id) - ); - - console.log(` Testing ${criticalRules.length} critical rules:`); - - for (const rule of criticalRules) { - await backend.create(`governance/${rule.id}.json`, rule); - const retrieved_single = await backend.view(`governance/${rule.id}.json`); - - const match = JSON.stringify(rule) === JSON.stringify(retrieved_single); - const status = match ? '✓' : '✗'; - console.log(` ${status} ${rule.id}: ${match ? 'PASS' : 'FAIL'}`); - - if (!match) { - throw new Error(`Critical rule ${rule.id} failed validation`); - } - } - - // Step 7: Performance summary - console.log('\n[Step 7] Performance Assessment...'); - - const totalLatency = results.timings.store + results.timings.retrieve; - const avgPerRule = totalLatency / rules.length; - - console.log(` Store: ${results.timings.store}ms (${(results.timings.store / rules.length).toFixed(2)}ms/rule)`); - console.log(` Retrieve: ${results.timings.retrieve}ms`); - console.log(` Total: ${totalLatency}ms`); - console.log(` Average per rule: ${avgPerRule.toFixed(2)}ms`); - - const target = 1000; // 1 second per batch operation - const status = totalLatency < target ? 'PASS' : 'WARN'; - console.log(` Target: <${target}ms - ${status}`); - - results.success = true; - results.totalLatency = totalLatency; - - } catch (error) { - console.error('\n✗ TEST FAILED:', error.message); - if (error.stack) { - console.error('\nStack trace:', error.stack); - } - results.errors.push(error.message); - results.success = false; - } finally { - // Cleanup - console.log('\n[Cleanup] Removing test data...'); - await backend.cleanup(); - } - - // Results summary - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' TEST RESULTS'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - if (results.success) { - console.log('✅ SUCCESS: All 18 Tractatus rules validated'); - console.log('\nKey Findings:'); - console.log(` • Rules loaded: ${results.rulesLoaded}`); - console.log(` • Rules stored: ${results.rulesStored}`); - console.log(` • Rules retrieved: ${results.rulesRetrieved}`); - console.log(` • Data integrity: ${results.integrityChecks.passed}/${results.rulesLoaded} (${((results.integrityChecks.passed / results.rulesLoaded) * 100).toFixed(1)}%)`); - console.log(` • Performance: ${results.totalLatency}ms total`); - console.log(` • Average per rule: ${(results.totalLatency / results.rulesLoaded).toFixed(2)}ms`); - - console.log('\nNext Steps:'); - console.log(' 1. Test with real Claude API (memory tool operations)'); - console.log(' 2. Measure API latency overhead'); - console.log(' 3. Test context editing with 50+ turn conversation'); - } else { - console.log('❌ FAILURE: Test did not pass'); - console.log('\nErrors:'); - results.errors.forEach(err => console.log(` • ${err}`)); - } - - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - return results; -} - -// Run test -if (require.main === module) { - runFullRulesTest() - .then(results => { - process.exit(results.success ? 0 : 1); - }) - .catch(error => { - console.error('Fatal error:', error); - process.exit(1); - }); -} - -module.exports = { runFullRulesTest }; diff --git a/tests/poc/memory-tool/week3-boundary-enforcer-integration.js b/tests/poc/memory-tool/week3-boundary-enforcer-integration.js deleted file mode 100644 index 9bfbf8bb..00000000 --- a/tests/poc/memory-tool/week3-boundary-enforcer-integration.js +++ /dev/null @@ -1,304 +0,0 @@ -/** - * Phase 5 PoC - Week 3: BoundaryEnforcer + MemoryProxy Integration Test - * - * Goal: Validate BoundaryEnforcer can: - * 1. Initialize MemoryProxy and load enforcement rules (inst_016, inst_017, inst_018) - * 2. Enforce boundaries using loaded rules - * 3. Create audit trail in .memory/audit/ - * - * Success Criteria: - * - MemoryProxy initializes successfully - * - All 3 critical rules loaded (inst_016, inst_017, inst_018) - * - Enforcement still works (95%+ accuracy) - * - Audit trail created with JSONL entries - */ - -const path = require('path'); -const fs = require('fs').promises; -const { MemoryProxyService, getMemoryProxy } = require('../../../src/services/MemoryProxy.service'); -const BoundaryEnforcer = require('../../../src/services/BoundaryEnforcer.service'); - -// Configuration -const TEST_MEMORY_PATH = path.join(__dirname, '../../../.memory-poc-week3'); -const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../../../.claude/instruction-history.json'); - -// Test enforcement scenarios -const TEST_SCENARIOS = [ - { - name: 'Values Decision (BLOCKED)', - action: { - description: 'Decide whether to prioritize privacy over convenience', - domain: 'values', - type: 'policy_decision' - }, - expectedBlocked: true, - expectedBoundary: 'VALUES' - }, - { - name: 'Technical Implementation (ALLOWED)', - action: { - description: 'Implement caching for API responses', - domain: 'technical', - type: 'implementation' - }, - expectedBlocked: false - }, - { - name: 'Strategic Decision (BLOCKED)', - action: { - description: 'Define our long-term mission and vision', - classification: { quadrant: 'STRATEGIC' }, - type: 'strategic_planning' - }, - expectedBlocked: true, - expectedBoundary: 'WISDOM' - }, - { - name: 'Innovation (BLOCKED)', - action: { - description: 'Create revolutionary new approach to AI governance', - domain: 'innovation', - type: 'innovation_proposal' - }, - expectedBlocked: true, - expectedBoundary: 'INNOVATION' - }, - { - name: 'Verification (ALLOWED)', - action: { - description: 'Verify current values alignment', - domain: 'verification', - pre_approved: true, - type: 'verification' - }, - expectedBlocked: false - } -]; - -/** - * Load Tractatus rules and persist to memory - */ -async function setupMemoryWithRules(memoryProxy) { - console.log('[Setup] Loading Tractatus rules...'); - - const data = await fs.readFile(INSTRUCTION_HISTORY_PATH, 'utf8'); - const parsed = JSON.parse(data); - const rules = parsed.instructions; - - console.log(` ✓ Loaded ${rules.length} rules from instruction history`); - - // Persist to memory - const result = await memoryProxy.persistGovernanceRules(rules); - console.log(` ✓ Persisted ${result.rulesStored} rules to memory (${result.duration}ms)`); - - return result; -} - -/** - * Main test execution - */ -async function runIntegrationTest() { - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' Phase 5 PoC Week 3: BoundaryEnforcer Integration Test'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - const results = { - success: false, - memoryProxyInit: false, - rulesLoaded: 0, - enforcementTests: { - total: 0, - passed: 0, - failed: 0, - scenarios: [] - }, - auditTrailCreated: false, - errors: [] - }; - - try { - // Step 1: Initialize MemoryProxy with test path - console.log('[Step 1] Initializing MemoryProxy...'); - const memoryProxy = new MemoryProxyService({ - memoryBasePath: TEST_MEMORY_PATH, - cacheEnabled: true, - cacheTTL: 300000 - }); - - await memoryProxy.initialize(); - results.memoryProxyInit = true; - console.log(' ✓ MemoryProxy initialized\n'); - - // Step 2: Load Tractatus rules into memory - console.log('[Step 2] Persisting Tractatus rules to memory...'); - await setupMemoryWithRules(memoryProxy); - - // Step 3: Initialize BoundaryEnforcer (uses singleton, but we'll create new instance) - console.log('\n[Step 3] Initializing BoundaryEnforcer...'); - - // Create new BoundaryEnforcer instance that uses our test MemoryProxy - const { BoundaryEnforcer: BoundaryEnforcerClass } = require('../../../src/services/BoundaryEnforcer.service'); - const enforcer = new BoundaryEnforcerClass(); - - // Override memoryProxy with our test instance - enforcer.memoryProxy = memoryProxy; - - const initResult = await enforcer.initialize(); - - if (initResult.success) { - results.rulesLoaded = initResult.rulesLoaded; - console.log(` ✓ BoundaryEnforcer initialized with ${initResult.rulesLoaded} enforcement rules`); - console.log(` Rules: ${initResult.enforcementRules.join(', ')}`); - } else { - throw new Error(`BoundaryEnforcer initialization failed: ${initResult.error}`); - } - - // Step 4: Test enforcement scenarios - console.log('\n[Step 4] Testing enforcement scenarios...\n'); - - for (const scenario of TEST_SCENARIOS) { - results.enforcementTests.total++; - - console.log(` Testing: ${scenario.name}`); - - const enforcementResult = enforcer.enforce(scenario.action, { - sessionId: 'week3-integration-test' - }); - - const blocked = enforcementResult.humanRequired === true; - const passed = blocked === scenario.expectedBlocked; - - if (passed) { - results.enforcementTests.passed++; - console.log(` ✓ PASS: ${blocked ? 'Blocked' : 'Allowed'} as expected`); - - if (scenario.expectedBoundary && enforcementResult.boundary) { - const boundaryMatch = enforcementResult.boundary === scenario.expectedBoundary; - if (boundaryMatch) { - console.log(` Boundary: ${enforcementResult.boundary} (correct)`); - } else { - console.log(` Boundary: ${enforcementResult.boundary} (expected ${scenario.expectedBoundary})`); - } - } - } else { - results.enforcementTests.failed++; - console.log(` ✗ FAIL: ${blocked ? 'Blocked' : 'Allowed'} (expected ${scenario.expectedBlocked ? 'blocked' : 'allowed'})`); - } - - results.enforcementTests.scenarios.push({ - name: scenario.name, - passed, - blocked, - expectedBlocked: scenario.expectedBlocked, - boundary: enforcementResult.boundary - }); - } - - // Step 5: Verify audit trail - console.log('\n[Step 5] Verifying audit trail...'); - - const today = new Date().toISOString().split('T')[0]; - const auditPath = path.join(TEST_MEMORY_PATH, `audit/decisions-${today}.jsonl`); - - try { - const auditData = await fs.readFile(auditPath, 'utf8'); - const auditLines = auditData.trim().split('\n'); - - results.auditTrailCreated = true; - console.log(` ✓ Audit trail created: ${auditLines.length} entries`); - - // Show sample audit entry - if (auditLines.length > 0) { - const sampleEntry = JSON.parse(auditLines[0]); - console.log('\n Sample audit entry:'); - console.log(` Session: ${sampleEntry.sessionId}`); - console.log(` Action: ${sampleEntry.action}`); - console.log(` Allowed: ${sampleEntry.allowed}`); - console.log(` Rules checked: ${sampleEntry.rulesChecked.join(', ')}`); - } - } catch (error) { - console.log(` ✗ Audit trail not found: ${error.message}`); - results.auditTrailCreated = false; - } - - // Calculate accuracy - const accuracy = (results.enforcementTests.passed / results.enforcementTests.total) * 100; - console.log('\n[Step 6] Enforcement Accuracy Assessment...'); - console.log(` Passed: ${results.enforcementTests.passed}/${results.enforcementTests.total} (${accuracy.toFixed(1)}%)`); - - const targetAccuracy = 95; - if (accuracy >= targetAccuracy) { - console.log(` ✓ Target accuracy met (>=${targetAccuracy}%)`); - results.success = true; - } else { - console.log(` ✗ Below target accuracy of ${targetAccuracy}%`); - results.success = false; - } - - } catch (error) { - console.error('\n✗ TEST FAILED:', error.message); - if (error.stack) { - console.error('\nStack trace:', error.stack); - } - results.errors.push(error.message); - results.success = false; - } finally { - // Cleanup - console.log('\n[Cleanup] Removing test data...'); - try { - await fs.rm(TEST_MEMORY_PATH, { recursive: true, force: true }); - console.log(' ✓ Cleanup complete'); - } catch (error) { - console.log(' ⚠ Cleanup warning:', error.message); - } - } - - // Results summary - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - console.log(' TEST RESULTS'); - console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - if (results.success) { - console.log('✅ SUCCESS: BoundaryEnforcer + MemoryProxy integration validated'); - console.log('\nKey Findings:'); - console.log(` • MemoryProxy initialized: ${results.memoryProxyInit ? 'Yes' : 'No'}`); - console.log(` • Enforcement rules loaded: ${results.rulesLoaded}/3`); - console.log(` • Enforcement tests: ${results.enforcementTests.passed}/${results.enforcementTests.total} passed`); - console.log(` • Accuracy: ${((results.enforcementTests.passed / results.enforcementTests.total) * 100).toFixed(1)}%`); - console.log(` • Audit trail created: ${results.auditTrailCreated ? 'Yes' : 'No'}`); - - console.log('\nNext Steps:'); - console.log(' 1. Integrate MemoryProxy with BlogCuration service'); - console.log(' 2. Test context editing (50+ turn conversation)'); - console.log(' 3. Create migration script (.claude/ → .memory/)'); - } else { - console.log('❌ FAILURE: Integration test did not pass'); - console.log('\nErrors:'); - results.errors.forEach(err => console.log(` • ${err}`)); - - if (results.enforcementTests.failed > 0) { - console.log('\nFailed scenarios:'); - results.enforcementTests.scenarios - .filter(s => !s.passed) - .forEach(s => console.log(` • ${s.name}`)); - } - } - - console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); - - return results; -} - -// Run test -if (require.main === module) { - runIntegrationTest() - .then(results => { - process.exit(results.success ? 0 : 1); - }) - .catch(error => { - console.error('Fatal error:', error); - process.exit(1); - }); -} - -module.exports = { runIntegrationTest }; diff --git a/tests/unit/AdaptiveCommunicationOrchestrator.test.js b/tests/unit/AdaptiveCommunicationOrchestrator.test.js deleted file mode 100644 index e85f207a..00000000 --- a/tests/unit/AdaptiveCommunicationOrchestrator.test.js +++ /dev/null @@ -1,402 +0,0 @@ -/** - * Unit Tests for AdaptiveCommunicationOrchestrator - * Tests culturally-adaptive communication to prevent linguistic hierarchy - */ - -const orchestrator = require('../../src/services/AdaptiveCommunicationOrchestrator.service'); - -describe('AdaptiveCommunicationOrchestrator', () => { - beforeEach(() => { - // Orchestrator is a singleton instance - }); - - describe('Anti-Patronizing Filter (inst_030)', () => { - test('should remove "simply" from messages', () => { - const message = 'You can simply implement this feature easily.'; - - const adapted = orchestrator.adaptCommunication(message); - - expect(adapted).not.toContain('simply'); - }); - - test('should remove "obviously" from messages', () => { - const message = 'Obviously, this approach is the best solution.'; - - const adapted = orchestrator.adaptCommunication(message); - - expect(adapted).not.toContain('obviously'); - }); - - test('should remove "as you may know" from messages', () => { - const message = 'As you may know, AI governance requires careful consideration.'; - - const adapted = orchestrator.adaptCommunication(message); - - expect(adapted).not.toContain('as you may know'); - }); - - test('should track patronizing terms removed', () => { - const statsBefore = orchestrator.getStats(); - - orchestrator.adaptCommunication('Simply put, obviously this is clear.'); - - const statsAfter = orchestrator.getStats(); - - expect(statsAfter.patronizing_terms_removed).toBeGreaterThan(statsBefore.patronizing_terms_removed); - }); - - test('should handle multiple patronizing terms', () => { - const message = 'Simply put, it is obviously clear that you can just do this, of course.'; - - const adapted = orchestrator.adaptCommunication(message); - - expect(adapted).not.toContain('simply'); - expect(adapted).not.toContain('obviously'); - expect(adapted).not.toContain('of course'); - }); - }); - - describe('Pub Test (inst_029)', () => { - test('should flag overly formal language', () => { - const message = 'Notwithstanding the aforementioned circumstances, we hereby decree...'; - - const result = orchestrator.pubTest(message); - - expect(result.passes).toBe(false); - expect(result.violations.length).toBeGreaterThan(0); - }); - - test('should pass casual conversational language', () => { - const message = 'Right, here\'s what we decided: We\'re going with option A.'; - - const result = orchestrator.pubTest(message); - - expect(result.passes).toBe(true); - expect(result.violations.length).toBe(0); - }); - - test('should flag "pursuant to" as too legal', () => { - const message = 'Pursuant to our discussion, we will proceed accordingly.'; - - const result = orchestrator.pubTest(message); - - expect(result.passes).toBe(false); - const hasLegalViolation = result.violations.some(v => v.reason.includes('legal')); - expect(hasLegalViolation).toBe(true); - }); - - test('should flag "notwithstanding" as unnecessarily complex', () => { - const message = 'Notwithstanding your concerns, we shall continue.'; - - const result = orchestrator.pubTest(message); - - expect(result.passes).toBe(false); - expect(result.violations[0].reason).toContain('Unnecessarily complex'); - }); - }); - - describe('Communication Style Detection', () => { - test('should detect formal academic style', () => { - const message = 'Furthermore, notwithstanding the theoretical implications, we must therefore consider...'; - - const detected = orchestrator.detectStyle(message); - - expect(detected).toBe('FORMAL_ACADEMIC'); - }); - - test('should detect casual direct style', () => { - const message = 'Right, here\'s the deal. Fair enough?'; - - const detected = orchestrator.detectStyle(message); - - expect(detected).toBe('CASUAL_DIRECT'); - }); - - test('should detect Māori protocol indicators', () => { - const message = 'Kia ora! Ngā mihi for your contribution to this kōrero.'; - - const detected = orchestrator.detectStyle(message); - - expect(detected).toBe('MAORI_PROTOCOL'); - }); - - test('should default to plain language', () => { - const message = 'This is a simple message without strong style indicators.'; - - const detected = orchestrator.detectStyle(message); - - expect(detected).toBe('PLAIN_LANGUAGE'); - }); - }); - - describe('Style Adaptation (inst_029)', () => { - test('should adapt to formal academic style', () => { - const message = 'We decided to go with this approach.'; - const context = { audience: 'FORMAL_ACADEMIC' }; - - const adapted = orchestrator.adaptCommunication(message, context); - - expect(adapted).toBeDefined(); - // In full implementation, would check for formal register - }); - - test('should adapt to casual direct style', () => { - const message = 'I would like to inform you that we have made a decision.'; - const context = { audience: 'CASUAL_DIRECT' }; - - const adapted = orchestrator.adaptCommunication(message, context); - - // Should simplify formal phrases - expect(adapted).not.toContain('I would like to inform you that'); - }); - - test('should track adaptations by style', () => { - const statsBefore = orchestrator.getStats(); - const beforeCount = statsBefore.by_style.FORMAL_ACADEMIC; - - orchestrator.adaptCommunication('Test message 1', { audience: 'FORMAL_ACADEMIC' }); - orchestrator.adaptCommunication('Test message 2', { audience: 'FORMAL_ACADEMIC' }); - - const statsAfter = orchestrator.getStats(); - const afterCount = statsAfter.by_style.FORMAL_ACADEMIC; - - expect(afterCount).toBe(beforeCount + 2); - }); - - test('should handle plain language adaptation', () => { - const message = 'We must utilize this methodology to facilitate implementation.'; - const context = { audience: 'PLAIN_LANGUAGE' }; - - const adapted = orchestrator.adaptCommunication(message, context); - - // Should replace jargon with plain equivalents - expect(adapted).not.toContain('utilize'); - expect(adapted).toContain('use'); - }); - }); - - describe('Language Detection (inst_032)', () => { - test('should detect Te Reo Māori', () => { - const message = 'Kia ora, let me share some whakaaro about this kaupapa.'; - - const adapted = orchestrator.adaptCommunication(message); - - // Should detect and track Māori language - const stats = orchestrator.getStats(); - expect(stats.languages_detected['te-reo-maori']).toBeGreaterThan(0); - }); - - test('should default to English', () => { - const message = 'This is a standard English message with no special indicators.'; - - const adapted = orchestrator.adaptCommunication(message); - - // Should process as English without issues - expect(adapted).toBeDefined(); - }); - }); - - describe('Greeting Generation', () => { - test('should generate Māori protocol greeting', () => { - const greeting = orchestrator.generateGreeting('John', { - communication_style: 'MAORI_PROTOCOL' - }); - - expect(greeting).toContain('Kia ora'); - expect(greeting).toContain('John'); - }); - - test('should generate casual direct greeting', () => { - const greeting = orchestrator.generateGreeting('Jane', { - communication_style: 'CASUAL_DIRECT' - }); - - expect(greeting).toBe('Hi Jane'); - }); - - test('should generate formal academic greeting', () => { - const greeting = orchestrator.generateGreeting('Dr. Smith', { - communication_style: 'FORMAL_ACADEMIC' - }); - - expect(greeting).toContain('Dear Dr. Smith'); - }); - - test('should generate plain language greeting by default', () => { - const greeting = orchestrator.generateGreeting('Alex'); - - expect(greeting).toContain('Hello'); - expect(greeting).toContain('Alex'); - }); - }); - - describe('Cultural Context Application (inst_031)', () => { - test('should apply cultural adaptations when context provided', () => { - const message = 'Thank you for your contribution.'; - const context = { - audience: 'MAORI_PROTOCOL', - cultural_context: 'maori' - }; - - const adapted = orchestrator.adaptCommunication(message, context); - - // Should apply Māori protocol adaptations - expect(adapted).toBeDefined(); - }); - - test('should handle multiple cultural contexts', () => { - const messages = [ - { text: 'Test 1', context: { cultural_context: 'australian' } }, - { text: 'Test 2', context: { cultural_context: 'japanese' } }, - { text: 'Test 3', context: { cultural_context: 'maori' } } - ]; - - messages.forEach(({ text, context }) => { - const adapted = orchestrator.adaptCommunication(text, context); - expect(adapted).toBeDefined(); - }); - }); - }); - - describe('Statistics Tracking', () => { - test('should track total adaptations', () => { - const statsBefore = orchestrator.getStats(); - - orchestrator.adaptCommunication('Test message', { audience: 'PLAIN_LANGUAGE' }); - - const statsAfter = orchestrator.getStats(); - - expect(statsAfter.total_adaptations).toBeGreaterThan(statsBefore.total_adaptations); - }); - - test('should track adaptations by style', () => { - orchestrator.adaptCommunication('Test 1', { audience: 'FORMAL_ACADEMIC' }); - orchestrator.adaptCommunication('Test 2', { audience: 'CASUAL_DIRECT' }); - - const stats = orchestrator.getStats(); - - expect(stats.by_style.FORMAL_ACADEMIC).toBeGreaterThan(0); - expect(stats.by_style.CASUAL_DIRECT).toBeGreaterThan(0); - }); - - test('should track languages detected', () => { - orchestrator.adaptCommunication('Kia ora whānau'); - - const stats = orchestrator.getStats(); - - expect(stats.languages_detected).toBeDefined(); - expect(Object.keys(stats.languages_detected).length).toBeGreaterThan(0); - }); - }); - - describe('Preventing Linguistic Hierarchy', () => { - test('should not favor formal academic over casual direct', () => { - const formalResult = orchestrator.adaptCommunication('Test', { audience: 'FORMAL_ACADEMIC' }); - const casualResult = orchestrator.adaptCommunication('Test', { audience: 'CASUAL_DIRECT' }); - - // Both should be processed without error, neither privileged - expect(formalResult).toBeDefined(); - expect(casualResult).toBeDefined(); - }); - - test('should support non-Western communication norms', () => { - const contexts = [ - { audience: 'MAORI_PROTOCOL' }, - { audience: 'JAPANESE_FORMAL' } - ]; - - contexts.forEach(context => { - const adapted = orchestrator.adaptCommunication('Test message', context); - expect(adapted).toBeDefined(); - }); - }); - - test('should remove condescension from all styles', () => { - const patronizingMessage = 'Simply put, obviously you can just do this.'; - - const contexts = [ - { audience: 'FORMAL_ACADEMIC' }, - { audience: 'CASUAL_DIRECT' }, - { audience: 'PLAIN_LANGUAGE' } - ]; - - contexts.forEach(context => { - const adapted = orchestrator.adaptCommunication(patronizingMessage, context); - expect(adapted).not.toContain('simply'); - expect(adapted).not.toContain('obviously'); - }); - }); - }); - - describe('Singleton Pattern', () => { - test('should export singleton instance with required methods', () => { - expect(typeof orchestrator.adaptCommunication).toBe('function'); - expect(typeof orchestrator.pubTest).toBe('function'); - expect(typeof orchestrator.detectStyle).toBe('function'); - expect(typeof orchestrator.generateGreeting).toBe('function'); - expect(typeof orchestrator.getStats).toBe('function'); - }); - }); - - describe('Error Handling', () => { - test('should handle null message gracefully', () => { - expect(() => { - orchestrator.adaptCommunication(null); - }).not.toThrow(); - }); - - test('should handle empty message', () => { - const result = orchestrator.adaptCommunication(''); - - expect(result).toBeDefined(); - expect(result).toBe(''); - }); - - test('should handle unknown communication style', () => { - const result = orchestrator.adaptCommunication('Test', { audience: 'UNKNOWN_STYLE' }); - - expect(result).toBeDefined(); - expect(result).toBe('Test'); - }); - }); - - describe('Integration Points', () => { - test('should be usable by PluralisticDeliberationOrchestrator', () => { - // Simulates how PDO would use this service - const message = 'We need your input on this values conflict.'; - const stakeholder = { - communication_style: 'formal_academic', - cultural_context: 'western_academic' - }; - - const adapted = orchestrator.adaptCommunication(message, { - audience: 'FORMAL_ACADEMIC', - cultural_context: stakeholder.cultural_context - }); - - expect(adapted).toBeDefined(); - }); - - test('should handle multiple stakeholder adaptations in sequence', () => { - const message = 'Thank you for your contribution to this deliberation.'; - - const stakeholders = [ - { style: 'FORMAL_ACADEMIC', context: 'academic' }, - { style: 'CASUAL_DIRECT', context: 'australian' }, - { style: 'MAORI_PROTOCOL', context: 'maori' } - ]; - - const adaptations = stakeholders.map(s => - orchestrator.adaptCommunication(message, { - audience: s.style, - cultural_context: s.context - }) - ); - - expect(adaptations.length).toBe(3); - adaptations.forEach(adapted => { - expect(adapted).toBeDefined(); - }); - }); - }); -}); diff --git a/tests/unit/BlogCuration.service.test.js b/tests/unit/BlogCuration.service.test.js deleted file mode 100644 index 8afd17f6..00000000 --- a/tests/unit/BlogCuration.service.test.js +++ /dev/null @@ -1,457 +0,0 @@ -/** - * Unit Tests - BlogCuration Service - * Tests blog content curation with Tractatus enforcement - */ - -// Mock dependencies before requiring the service -jest.mock('../../src/services/ClaudeAPI.service', () => ({ - sendMessage: jest.fn(), - extractJSON: jest.fn() -})); - -jest.mock('../../src/services/BoundaryEnforcer.service', () => ({ - enforce: jest.fn() -})); - -const BlogCuration = require('../../src/services/BlogCuration.service'); -const ClaudeAPI = require('../../src/services/ClaudeAPI.service'); -const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service'); - -describe('BlogCuration Service', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('Editorial Guidelines', () => { - test('should have editorial guidelines defined', () => { - const guidelines = BlogCuration.getEditorialGuidelines(); - - expect(guidelines).toBeDefined(); - expect(guidelines.tone).toBeDefined(); - expect(guidelines.voice).toBeDefined(); - expect(guidelines.style).toBeDefined(); - expect(Array.isArray(guidelines.principles)).toBe(true); - expect(Array.isArray(guidelines.forbiddenPatterns)).toBe(true); - }); - - test('should include Tractatus enforcement principles', () => { - const guidelines = BlogCuration.getEditorialGuidelines(); - - const principlesText = guidelines.principles.join(' '); - expect(principlesText).toContain('Transparency'); - expect(principlesText).toContain('Honesty'); - expect(principlesText).toContain('Evidence'); - }); - - test('should define forbidden patterns', () => { - const guidelines = BlogCuration.getEditorialGuidelines(); - - expect(guidelines.forbiddenPatterns.length).toBeGreaterThan(0); - expect(guidelines.forbiddenPatterns.some(p => p.includes('Fabricated'))).toBe(true); - expect(guidelines.forbiddenPatterns.some(p => p.includes('guarantee'))).toBe(true); - }); - }); - - describe('draftBlogPost()', () => { - beforeEach(() => { - // Mock boundary enforcer to allow by default - BoundaryEnforcer.enforce.mockReturnValue({ - allowed: true, - section: 'TRA-OPS-0002', - reasoning: 'AI suggestion with human approval' - }); - - // Mock ClaudeAPI.sendMessage - ClaudeAPI.sendMessage.mockResolvedValue({ - content: [{ - type: 'text', - text: JSON.stringify({ - title: 'Understanding AI Boundary Enforcement', - subtitle: 'How Tractatus prevents values automation', - content: '# Introduction\n\nBoundary enforcement is critical...', - excerpt: 'This article explores boundary enforcement in AI systems.', - tags: ['AI safety', 'Boundary enforcement', 'Tractatus'], - tractatus_angle: 'Demonstrates harmlessness principle through boundary checks', - sources: ['https://example.com/research'], - word_count: 1200 - }) - }], - model: 'claude-sonnet-4-5-20250929', - usage: { input_tokens: 200, output_tokens: 800 } - }); - - ClaudeAPI.extractJSON.mockImplementation((response) => { - return JSON.parse(response.content[0].text); - }); - }); - - test('should draft blog post with valid params', async () => { - const params = { - topic: 'AI Boundary Enforcement', - audience: 'implementer', - length: 'medium', - focus: 'real-world examples' - }; - - const result = await BlogCuration.draftBlogPost(params); - - expect(result).toHaveProperty('draft'); - expect(result).toHaveProperty('validation'); - expect(result).toHaveProperty('boundary_check'); - expect(result).toHaveProperty('metadata'); - - expect(result.draft.title).toBeDefined(); - expect(result.draft.content).toBeDefined(); - expect(result.metadata.requires_human_approval).toBe(true); - }); - - test('should perform boundary check before drafting', async () => { - const params = { - topic: 'Test Topic', - audience: 'researcher', - length: 'short' - }; - - await BlogCuration.draftBlogPost(params); - - expect(BoundaryEnforcer.enforce).toHaveBeenCalledWith({ - description: expect.stringContaining('AI-drafted blog content'), - text: expect.stringContaining('mandatory human approval'), - classification: { quadrant: 'OPERATIONAL' }, - type: 'content_generation' - }); - }); - - test('should throw error if boundary check fails', async () => { - BoundaryEnforcer.enforce.mockReturnValue({ - allowed: false, - section: 'TRA-STR-0001', - reasoning: 'Values territory - human decision required' - }); - - const params = { - topic: 'Test Topic', - audience: 'advocate', - length: 'medium' - }; - - await expect(BlogCuration.draftBlogPost(params)).rejects.toThrow('Boundary violation'); - }); - - test('should validate generated content against Tractatus principles', async () => { - const params = { - topic: 'Test Topic', - audience: 'general', - length: 'long' - }; - - const result = await BlogCuration.draftBlogPost(params); - - expect(result.validation).toBeDefined(); - expect(result.validation).toHaveProperty('valid'); - expect(result.validation).toHaveProperty('violations'); - expect(result.validation).toHaveProperty('warnings'); - expect(result.validation).toHaveProperty('recommendation'); - }); - - test('should detect absolute guarantee violations (inst_017)', async () => { - ClaudeAPI.extractJSON.mockReturnValue({ - title: 'Our Framework Guarantees 100% Safety', - subtitle: 'Never fails, always works', - content: 'This system guarantees complete safety...', - excerpt: 'Test', - tags: [], - sources: [], - word_count: 500 - }); - - const params = { - topic: 'Test', - audience: 'implementer', - length: 'short' - }; - - const result = await BlogCuration.draftBlogPost(params); - - expect(result.validation.violations.length).toBeGreaterThan(0); - expect(result.validation.violations.some(v => v.type === 'ABSOLUTE_GUARANTEE')).toBe(true); - expect(result.validation.recommendation).toBe('REJECT'); - }); - - test('should warn about uncited statistics (inst_016)', async () => { - ClaudeAPI.extractJSON.mockReturnValue({ - title: 'AI Safety Statistics', - subtitle: 'Data-driven analysis', - content: 'Studies show 85% of AI systems lack governance...', - excerpt: 'Statistical analysis', - tags: [], - sources: [], // No sources! - word_count: 900 - }); - - const params = { - topic: 'Test', - audience: 'researcher', - length: 'medium' - }; - - const result = await BlogCuration.draftBlogPost(params); - - expect(result.validation.warnings.some(w => w.type === 'UNCITED_STATISTICS')).toBe(true); - expect(result.validation.stats_found).toBeGreaterThan(0); - expect(result.validation.sources_provided).toBe(0); - }); - - test('should call ClaudeAPI with appropriate max_tokens for length', async () => { - const testCases = [ - { length: 'short', expectedMin: 2000, expectedMax: 2100 }, - { length: 'medium', expectedMin: 3000, expectedMax: 3100 }, - { length: 'long', expectedMin: 4000, expectedMax: 4100 } - ]; - - for (const { length, expectedMin, expectedMax } of testCases) { - jest.clearAllMocks(); - - await BlogCuration.draftBlogPost({ - topic: 'Test', - audience: 'general', - length - }); - - const callOptions = ClaudeAPI.sendMessage.mock.calls[0][1]; - expect(callOptions.max_tokens).toBeGreaterThanOrEqual(expectedMin); - expect(callOptions.max_tokens).toBeLessThanOrEqual(expectedMax); - } - }); - - test('should include Tractatus constraints in system prompt', async () => { - await BlogCuration.draftBlogPost({ - topic: 'Test', - audience: 'implementer', - length: 'medium' - }); - - const systemPrompt = ClaudeAPI.sendMessage.mock.calls[0][1].system; - - expect(systemPrompt).toContain('inst_016'); - expect(systemPrompt).toContain('inst_017'); - expect(systemPrompt).toContain('inst_018'); - expect(systemPrompt).toContain('fabricat'); - expect(systemPrompt).toContain('guarantee'); - }); - }); - - describe('suggestTopics()', () => { - beforeEach(() => { - // Mock sendMessage to return response with topics - ClaudeAPI.sendMessage.mockResolvedValue({ - content: [{ - type: 'text', - text: JSON.stringify([ - { - title: 'Understanding AI Governance', - rationale: 'Fills gap in governance docs', - target_word_count: 1200, - key_points: ['Governance', 'Safety', 'Framework'], - tractatus_angle: 'Core governance principles' - } - ]) - }], - model: 'claude-sonnet-4-5-20250929', - usage: { input_tokens: 150, output_tokens: 200 } - }); - - // Mock extractJSON to return the topics array - ClaudeAPI.extractJSON.mockImplementation((response) => { - return JSON.parse(response.content[0].text); - }); - }); - - test('should suggest topics for audience', async () => { - const result = await BlogCuration.suggestTopics('researcher'); - - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - expect(result[0]).toHaveProperty('validation'); - expect(ClaudeAPI.sendMessage).toHaveBeenCalled(); - }); - - test('should suggest topics with theme', async () => { - const result = await BlogCuration.suggestTopics('advocate', 'policy implications'); - - expect(ClaudeAPI.sendMessage).toHaveBeenCalled(); - const systemPrompt = ClaudeAPI.sendMessage.mock.calls[0][1].system; - expect(systemPrompt).toContain('Tractatus'); - expect(result.length).toBeGreaterThan(0); - }); - - test('should validate topic titles for forbidden patterns', async () => { - ClaudeAPI.extractJSON.mockReturnValue([ - { title: 'Guaranteed 100% AI Safety', rationale: 'Test', target_word_count: 1000, key_points: [], tractatus_angle: 'Test' } - ]); - - const result = await BlogCuration.suggestTopics('general'); - - expect(result[0].validation.valid).toBe(false); - expect(result[0].validation.issues.length).toBeGreaterThan(0); - }); - }); - - describe('analyzeContentCompliance()', () => { - beforeEach(() => { - ClaudeAPI.sendMessage.mockResolvedValue({ - content: [{ - type: 'text', - text: JSON.stringify({ - compliant: true, - violations: [], - warnings: [], - strengths: ['Evidence-based', 'Acknowledges limitations'], - overall_score: 92, - recommendation: 'PUBLISH' - }) - }] - }); - - ClaudeAPI.extractJSON.mockImplementation((response) => { - return JSON.parse(response.content[0].text); - }); - }); - - test('should analyze content for Tractatus compliance', async () => { - const content = { - title: 'Understanding AI Safety', - body: 'This article explores AI safety frameworks...' - }; - - const result = await BlogCuration.analyzeContentCompliance(content); - - expect(result).toHaveProperty('compliant'); - expect(result).toHaveProperty('violations'); - expect(result).toHaveProperty('overall_score'); - expect(result).toHaveProperty('recommendation'); - }); - - test('should call ClaudeAPI with compliance analysis prompt', async () => { - await BlogCuration.analyzeContentCompliance({ - title: 'Test Title', - body: 'Test content' - }); - - const systemPrompt = ClaudeAPI.sendMessage.mock.calls[0][1].system; - const userPrompt = ClaudeAPI.sendMessage.mock.calls[0][0][0].content; - - expect(systemPrompt).toContain('Tractatus'); - expect(systemPrompt).toContain('compliance'); - expect(userPrompt).toContain('Test Title'); - expect(userPrompt).toContain('Test content'); - }); - - test('should detect violations in non-compliant content', async () => { - ClaudeAPI.extractJSON.mockReturnValue({ - compliant: false, - violations: [ - { - type: 'FABRICATED_STAT', - severity: 'HIGH', - excerpt: '99% of users agree', - reasoning: 'Unverified statistic', - suggested_fix: 'Cite source or remove claim' - } - ], - warnings: [], - strengths: [], - overall_score: 35, - recommendation: 'REJECT' - }); - - const result = await BlogCuration.analyzeContentCompliance({ - title: 'Amazing Results', - body: '99% of users agree this is the best framework ever...' - }); - - expect(result.compliant).toBe(false); - expect(result.violations.length).toBeGreaterThan(0); - expect(result.recommendation).toBe('REJECT'); - }); - }); - - describe('Utility Methods', () => { - describe('generateSlug()', () => { - test('should generate URL-safe slug from title', () => { - const title = 'Understanding AI Safety: A Framework Approach!'; - const slug = BlogCuration.generateSlug(title); - - expect(slug).toBe('understanding-ai-safety-a-framework-approach'); - expect(slug).toMatch(/^[a-z0-9-]+$/); - }); - - test('should handle special characters', () => { - const title = 'AI @ Work: 100% Automated?!'; - const slug = BlogCuration.generateSlug(title); - - expect(slug).toBe('ai-work-100-automated'); - expect(slug).not.toContain('@'); - expect(slug).not.toContain('!'); - }); - - test('should limit slug length to 100 characters', () => { - const longTitle = 'A'.repeat(200); - const slug = BlogCuration.generateSlug(longTitle); - - expect(slug.length).toBeLessThanOrEqual(100); - }); - }); - - describe('extractExcerpt()', () => { - test('should extract excerpt from content', () => { - const content = 'This is a blog post about AI safety. It discusses various frameworks and approaches to ensuring safe AI deployment.'; - const excerpt = BlogCuration.extractExcerpt(content); - - expect(excerpt.length).toBeLessThanOrEqual(200); - expect(excerpt).toContain('AI safety'); - }); - - test('should strip HTML tags', () => { - const content = '

This is bold and italic text.

'; - const excerpt = BlogCuration.extractExcerpt(content); - - expect(excerpt).not.toContain('

'); - expect(excerpt).not.toContain(''); - expect(excerpt).toContain('bold'); - }); - - test('should end at sentence boundary when possible', () => { - const content = 'First sentence. Second sentence. Third sentence that is much longer and goes on and on and on about various topics.'; - const excerpt = BlogCuration.extractExcerpt(content, 50); - - expect(excerpt.endsWith('.')).toBe(true); - expect(excerpt).not.toContain('Third sentence'); - }); - - test('should add ellipsis when truncating', () => { - const content = 'A'.repeat(300); - const excerpt = BlogCuration.extractExcerpt(content, 100); - - expect(excerpt.endsWith('...')).toBe(true); - expect(excerpt.length).toBeLessThanOrEqual(103); // 100 + '...' - }); - }); - }); - - describe('Service Integration', () => { - test('should maintain singleton pattern', () => { - const BlogCuration2 = require('../../src/services/BlogCuration.service'); - expect(BlogCuration).toBe(BlogCuration2); - }); - - test('should have all expected public methods', () => { - expect(typeof BlogCuration.draftBlogPost).toBe('function'); - expect(typeof BlogCuration.suggestTopics).toBe('function'); - expect(typeof BlogCuration.analyzeContentCompliance).toBe('function'); - expect(typeof BlogCuration.generateSlug).toBe('function'); - expect(typeof BlogCuration.extractExcerpt).toBe('function'); - expect(typeof BlogCuration.getEditorialGuidelines).toBe('function'); - }); - }); -}); diff --git a/tests/unit/ClaudeAPI.test.js b/tests/unit/ClaudeAPI.test.js deleted file mode 100644 index a40d3e42..00000000 --- a/tests/unit/ClaudeAPI.test.js +++ /dev/null @@ -1,583 +0,0 @@ -/** - * Unit Tests - ClaudeAPI Service - * Tests AI service methods with mocked API responses - */ - -const ClaudeAPI = require('../../src/services/ClaudeAPI.service'); - -describe('ClaudeAPI Service', () => { - let originalApiKey; - let originalMakeRequest; - - beforeAll(() => { - // Store original API key - originalApiKey = ClaudeAPI.apiKey; - // Ensure API key is set for tests - ClaudeAPI.apiKey = 'test_api_key'; - // Store original _makeRequest for restoration - originalMakeRequest = ClaudeAPI._makeRequest; - }); - - afterAll(() => { - // Restore original API key and method - ClaudeAPI.apiKey = originalApiKey; - ClaudeAPI._makeRequest = originalMakeRequest; - }); - - beforeEach(() => { - // Mock _makeRequest for each test - ClaudeAPI._makeRequest = jest.fn(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('Constructor and Configuration', () => { - test('should initialize with default model and max tokens', () => { - expect(ClaudeAPI.model).toBeDefined(); - expect(ClaudeAPI.maxTokens).toBeDefined(); - expect(typeof ClaudeAPI.maxTokens).toBe('number'); - }); - - test('should use environment variables for configuration', () => { - expect(ClaudeAPI.apiVersion).toBe('2023-06-01'); - expect(ClaudeAPI.hostname).toBe('api.anthropic.com'); - }); - }); - - describe('sendMessage()', () => { - test('should throw error if API key not configured', async () => { - const tempKey = ClaudeAPI.apiKey; - ClaudeAPI.apiKey = null; - - await expect( - ClaudeAPI.sendMessage([{ role: 'user', content: 'Test' }]) - ).rejects.toThrow('Claude API key not configured'); - - ClaudeAPI.apiKey = tempKey; - }); - - test('should send message with default options', async () => { - const mockResponse = { - content: [{ type: 'text', text: 'Response text' }], - usage: { input_tokens: 10, output_tokens: 20 } - }; - - ClaudeAPI._makeRequest.mockResolvedValue(mockResponse); - - const messages = [{ role: 'user', content: 'Test message' }]; - const response = await ClaudeAPI.sendMessage(messages); - - expect(ClaudeAPI._makeRequest).toHaveBeenCalledWith({ - model: ClaudeAPI.model, - max_tokens: ClaudeAPI.maxTokens, - messages: messages - }); - expect(response).toEqual(mockResponse); - }); - - test('should send message with custom options', async () => { - const mockResponse = { - content: [{ type: 'text', text: 'Response' }], - usage: { input_tokens: 10, output_tokens: 20 } - }; - - ClaudeAPI._makeRequest.mockResolvedValue(mockResponse); - - const messages = [{ role: 'user', content: 'Test' }]; - const options = { - model: 'claude-3-opus-20240229', - max_tokens: 2048, - temperature: 0.7, - system: 'You are a helpful assistant.' - }; - - await ClaudeAPI.sendMessage(messages, options); - - expect(ClaudeAPI._makeRequest).toHaveBeenCalledWith({ - model: options.model, - max_tokens: options.max_tokens, - messages: messages, - system: options.system, - temperature: options.temperature - }); - }); - - test('should log usage information', async () => { - const mockResponse = { - content: [{ type: 'text', text: 'Response' }], - usage: { input_tokens: 100, output_tokens: 50 } - }; - - ClaudeAPI._makeRequest.mockResolvedValue(mockResponse); - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - await ClaudeAPI.sendMessage([{ role: 'user', content: 'Test' }]); - - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[ClaudeAPI] Usage: 100 in, 50 out') - ); - - consoleSpy.mockRestore(); - }); - - test('should handle API errors', async () => { - ClaudeAPI._makeRequest.mockRejectedValue(new Error('API Error')); - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - await expect( - ClaudeAPI.sendMessage([{ role: 'user', content: 'Test' }]) - ).rejects.toThrow('API Error'); - - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('[ClaudeAPI] Error:'), - expect.any(String) - ); - - consoleSpy.mockRestore(); - }); - }); - - describe('extractTextContent()', () => { - test('should extract text from valid response', () => { - const response = { - content: [ - { type: 'text', text: 'This is the response text' } - ] - }; - - const text = ClaudeAPI.extractTextContent(response); - expect(text).toBe('This is the response text'); - }); - - test('should handle multiple content blocks', () => { - const response = { - content: [ - { type: 'tool_use', name: 'calculator' }, - { type: 'text', text: 'Answer is 42' } - ] - }; - - const text = ClaudeAPI.extractTextContent(response); - expect(text).toBe('Answer is 42'); - }); - - test('should throw error for invalid response format', () => { - expect(() => ClaudeAPI.extractTextContent(null)).toThrow('Invalid Claude API response format'); - expect(() => ClaudeAPI.extractTextContent({})).toThrow('Invalid Claude API response format'); - expect(() => ClaudeAPI.extractTextContent({ content: 'not an array' })).toThrow('Invalid Claude API response format'); - }); - - test('should throw error when no text block found', () => { - const response = { - content: [ - { type: 'tool_use', name: 'calculator' } - ] - }; - - expect(() => ClaudeAPI.extractTextContent(response)).toThrow('No text content in Claude API response'); - }); - }); - - describe('extractJSON()', () => { - test('should parse plain JSON', () => { - const response = { - content: [{ type: 'text', text: '{"key": "value", "number": 42}' }] - }; - - const json = ClaudeAPI.extractJSON(response); - expect(json).toEqual({ key: 'value', number: 42 }); - }); - - test('should handle JSON in markdown code block', () => { - const response = { - content: [{ type: 'text', text: '```json\n{"key": "value"}\n```' }] - }; - - const json = ClaudeAPI.extractJSON(response); - expect(json).toEqual({ key: 'value' }); - }); - - test('should handle JSON in generic code block', () => { - const response = { - content: [{ type: 'text', text: '```\n{"key": "value"}\n```' }] - }; - - const json = ClaudeAPI.extractJSON(response); - expect(json).toEqual({ key: 'value' }); - }); - - test('should throw error for invalid JSON', () => { - const response = { - content: [{ type: 'text', text: 'This is not JSON' }] - }; - - expect(() => ClaudeAPI.extractJSON(response)).toThrow('Failed to parse JSON from Claude response'); - }); - }); - - describe('classifyInstruction()', () => { - test('should classify instruction into quadrant', async () => { - const mockResponse = { - content: [{ - type: 'text', - text: JSON.stringify({ - quadrant: 'STRATEGIC', - persistence: 'HIGH', - temporal_scope: 'PROJECT', - verification_required: 'MANDATORY', - explicitness: 0.9, - reasoning: 'Sets long-term project direction' - }) - }], - usage: { input_tokens: 50, output_tokens: 100 } - }; - - ClaudeAPI._makeRequest.mockResolvedValue(mockResponse); - - const result = await ClaudeAPI.classifyInstruction('Always use TypeScript for new projects'); - - expect(result.quadrant).toBe('STRATEGIC'); - expect(result.persistence).toBe('HIGH'); - expect(result.temporal_scope).toBe('PROJECT'); - expect(ClaudeAPI._makeRequest).toHaveBeenCalled(); - }); - }); - - describe('generateBlogTopics()', () => { - test('should generate blog topics for audience without theme', async () => { - const mockTopics = [ - { - title: 'Understanding AI Safety Through Sovereignty', - subtitle: 'How Tractatus preserves human agency', - word_count: 1200, - key_points: ['sovereignty', 'agency', 'values'], - tractatus_angle: 'Core principle' - } - ]; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockTopics) }], - usage: { input_tokens: 100, output_tokens: 200 } - }); - - const result = await ClaudeAPI.generateBlogTopics('researcher'); - - expect(Array.isArray(result)).toBe(true); - expect(result[0]).toHaveProperty('title'); - expect(ClaudeAPI._makeRequest).toHaveBeenCalled(); - }); - - test('should generate blog topics with theme', async () => { - const mockTopics = [ - { title: 'Topic 1', subtitle: 'Subtitle', word_count: 1000, key_points: [], tractatus_angle: 'Angle' } - ]; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockTopics) }], - usage: { input_tokens: 100, output_tokens: 200 } - }); - - const result = await ClaudeAPI.generateBlogTopics('implementer', 'governance frameworks'); - - expect(result).toEqual(mockTopics); - const callArgs = ClaudeAPI._makeRequest.mock.calls[0][0]; - expect(callArgs.messages[0].content).toContain('governance frameworks'); - }); - }); - - describe('classifyMediaInquiry()', () => { - test('should classify media inquiry by priority', async () => { - const mockClassification = { - priority: 'HIGH', - reasoning: 'Major outlet with tight deadline', - recommended_response_time: '24 hours', - suggested_spokesperson: 'framework creator' - }; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockClassification) }], - usage: { input_tokens: 80, output_tokens: 100 } - }); - - const inquiry = { - outlet: 'TechCrunch', - request: 'Interview about AI safety frameworks', - deadline: '2025-10-15' - }; - - const result = await ClaudeAPI.classifyMediaInquiry(inquiry); - - expect(result.priority).toBe('HIGH'); - expect(result.recommended_response_time).toBeDefined(); - expect(ClaudeAPI._makeRequest).toHaveBeenCalled(); - }); - - test('should handle inquiry without deadline', async () => { - const mockClassification = { - priority: 'MEDIUM', - reasoning: 'No urgent deadline', - recommended_response_time: '3-5 days', - suggested_spokesperson: 'technical expert' - }; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockClassification) }], - usage: { input_tokens: 60, output_tokens: 80 } - }); - - const inquiry = { - outlet: 'Medium Blog', - request: 'Feature article about AI governance' - }; - - const result = await ClaudeAPI.classifyMediaInquiry(inquiry); - - expect(result.priority).toBe('MEDIUM'); - const callArgs = ClaudeAPI._makeRequest.mock.calls[0][0]; - expect(callArgs.messages[0].content).toContain('Not specified'); - }); - }); - - describe('draftMediaResponse()', () => { - test('should draft response to media inquiry', async () => { - const mockDraft = 'Thank you for your interest in the Tractatus AI Safety Framework...'; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: mockDraft }], - usage: { input_tokens: 100, output_tokens: 150 } - }); - - const inquiry = { - outlet: 'Wired Magazine', - request: 'Expert quote on AI safety' - }; - - const result = await ClaudeAPI.draftMediaResponse(inquiry, 'HIGH'); - - expect(typeof result).toBe('string'); - expect(result).toContain('Tractatus'); - expect(ClaudeAPI._makeRequest).toHaveBeenCalled(); - }); - }); - - describe('analyzeCaseRelevance()', () => { - test('should analyze case study relevance', async () => { - const mockAnalysis = { - relevance_score: 85, - strengths: ['Clear evidence', 'Framework alignment'], - weaknesses: ['Needs more detail'], - recommended_action: 'PUBLISH', - ethical_concerns: null, - suggested_improvements: ['Add metrics'] - }; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockAnalysis) }], - usage: { input_tokens: 120, output_tokens: 180 } - }); - - const caseStudy = { - title: 'AI System Prevented from Making Values Decision', - description: 'Case study of boundary enforcement', - evidence: 'System logs and decision audit trail' - }; - - const result = await ClaudeAPI.analyzeCaseRelevance(caseStudy); - - expect(result.relevance_score).toBe(85); - expect(result.recommended_action).toBe('PUBLISH'); - expect(Array.isArray(result.strengths)).toBe(true); - }); - - test('should handle case study without evidence', async () => { - const mockAnalysis = { - relevance_score: 45, - strengths: ['Interesting topic'], - weaknesses: ['No evidence provided'], - recommended_action: 'EDIT', - ethical_concerns: null, - suggested_improvements: ['Add concrete evidence'] - }; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockAnalysis) }], - usage: { input_tokens: 100, output_tokens: 140 } - }); - - const caseStudy = { - title: 'Interesting AI Safety Case', - description: 'A case that might be relevant' - }; - - const result = await ClaudeAPI.analyzeCaseRelevance(caseStudy); - - expect(result.relevance_score).toBeLessThan(60); - const callArgs = ClaudeAPI._makeRequest.mock.calls[0][0]; - expect(callArgs.messages[0].content).toContain('Not provided'); - }); - }); - - describe('curateResource()', () => { - test('should curate external resource', async () => { - const mockCuration = { - recommended: true, - category: 'PAPERS', - alignment_score: 92, - target_audience: ['researcher', 'implementer'], - tags: ['AI safety', 'governance', 'frameworks'], - reasoning: 'Highly aligned with Tractatus principles', - concerns: null - }; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockCuration) }], - usage: { input_tokens: 90, output_tokens: 120 } - }); - - const resource = { - url: 'https://example.com/paper', - title: 'AI Safety Research Paper', - description: 'Comprehensive framework for AI governance' - }; - - const result = await ClaudeAPI.curateResource(resource); - - expect(result.recommended).toBe(true); - expect(result.category).toBe('PAPERS'); - expect(result.alignment_score).toBeGreaterThan(80); - expect(Array.isArray(result.target_audience)).toBe(true); - }); - - test('should identify concerns in resources', async () => { - const mockCuration = { - recommended: false, - category: 'ARTICLES', - alignment_score: 35, - target_audience: [], - tags: ['AI'], - reasoning: 'Conflicting values approach', - concerns: ['Promotes full automation of values decisions', 'No human oversight'] - }; - - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify(mockCuration) }], - usage: { input_tokens: 80, output_tokens: 110 } - }); - - const resource = { - url: 'https://example.com/article', - title: 'Fully Automated AI Ethics', - description: 'Let AI make all ethical decisions' - }; - - const result = await ClaudeAPI.curateResource(resource); - - expect(result.recommended).toBe(false); - expect(result.concerns).toBeDefined(); - expect(Array.isArray(result.concerns)).toBe(true); - expect(result.concerns.length).toBeGreaterThan(0); - }); - }); - - describe('Error Handling', () => { - test('should handle network errors', async () => { - ClaudeAPI._makeRequest.mockRejectedValue(new Error('Network timeout')); - - await expect( - ClaudeAPI.sendMessage([{ role: 'user', content: 'Test' }]) - ).rejects.toThrow('Network timeout'); - }); - - test('should handle malformed responses', async () => { - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [{ type: 'text', text: 'Not valid JSON' }], - usage: { input_tokens: 10, output_tokens: 5 } - }); - - await expect( - ClaudeAPI.classifyInstruction('Test instruction') - ).rejects.toThrow('Failed to parse JSON from Claude response'); - }); - - test('should handle empty responses gracefully', async () => { - ClaudeAPI._makeRequest.mockResolvedValue({ - content: [], - usage: { input_tokens: 10, output_tokens: 0 } - }); - - const response = await ClaudeAPI.sendMessage([{ role: 'user', content: 'Test' }]); - - // sendMessage doesn't validate content, it just returns the response - expect(response.content).toEqual([]); - - // But extractTextContent should throw an error - expect(() => ClaudeAPI.extractTextContent(response)).toThrow('No text content in Claude API response'); - }); - }); - - describe('_makeRequest() [Private Method Testing]', () => { - beforeEach(() => { - // Restore original _makeRequest for these tests - ClaudeAPI._makeRequest = originalMakeRequest; - }); - - afterEach(() => { - // Re-mock for other tests - ClaudeAPI._makeRequest = jest.fn(); - }); - - test('should construct proper HTTPS request', () => { - // This test verifies the method exists and has proper structure - expect(typeof ClaudeAPI._makeRequest).toBe('function'); - - const payload = { - model: 'claude-sonnet-4-5-20250929', - max_tokens: 1024, - messages: [{ role: 'user', content: 'Test' }] - }; - - // Verify the method accepts a payload parameter - expect(() => ClaudeAPI._makeRequest(payload)).not.toThrow(TypeError); - }); - - test('should include required headers in request', () => { - // Verify method signature and structure - const methodString = ClaudeAPI._makeRequest.toString(); - expect(methodString).toContain('x-api-key'); - expect(methodString).toContain('anthropic-version'); - expect(methodString).toContain('Content-Type'); - }); - - test('should use correct API endpoint', () => { - const methodString = ClaudeAPI._makeRequest.toString(); - expect(methodString).toContain('/v1/messages'); - expect(methodString).toContain('443'); // HTTPS port - }); - }); - - describe('Service Integration', () => { - test('should maintain singleton pattern', () => { - const ClaudeAPI2 = require('../../src/services/ClaudeAPI.service'); - expect(ClaudeAPI).toBe(ClaudeAPI2); - }); - - test('should have all expected public methods', () => { - expect(typeof ClaudeAPI.sendMessage).toBe('function'); - expect(typeof ClaudeAPI.extractTextContent).toBe('function'); - expect(typeof ClaudeAPI.extractJSON).toBe('function'); - expect(typeof ClaudeAPI.classifyInstruction).toBe('function'); - expect(typeof ClaudeAPI.generateBlogTopics).toBe('function'); - expect(typeof ClaudeAPI.classifyMediaInquiry).toBe('function'); - expect(typeof ClaudeAPI.draftMediaResponse).toBe('function'); - expect(typeof ClaudeAPI.analyzeCaseRelevance).toBe('function'); - expect(typeof ClaudeAPI.curateResource).toBe('function'); - }); - - test('should expose private _makeRequest method for testing', () => { - // Verify _makeRequest exists (even though private) - expect(typeof ClaudeAPI._makeRequest).toBe('function'); - }); - }); -}); diff --git a/tests/unit/ProhibitedTermsScanner.test.js b/tests/unit/ProhibitedTermsScanner.test.js deleted file mode 100644 index 1e568194..00000000 --- a/tests/unit/ProhibitedTermsScanner.test.js +++ /dev/null @@ -1,461 +0,0 @@ -/** - * Unit Tests - ProhibitedTermsScanner - * Tests for proactive content scanning (Framework Phase 1) - */ - -const fs = require('fs').promises; -const path = require('path'); -const ProhibitedTermsScanner = require('../../scripts/framework-components/ProhibitedTermsScanner'); - -describe('ProhibitedTermsScanner', () => { - let scanner; - const testFilesDir = path.join(__dirname, '../tmp-scanner-test'); - - beforeEach(() => { - scanner = new ProhibitedTermsScanner({ - silent: true, - basePath: testFilesDir // Only scan test directory - }); - }); - - afterEach(async () => { - // Clean up test files - try { - await fs.rm(testFilesDir, { recursive: true, force: true }); - } catch (err) { - // Ignore cleanup errors - } - }); - - describe('Pattern Detection - inst_017 (Absolute Assurance)', () => { - test('should detect "guarantee" variations', async () => { - const testContent = ` - This guarantees safety. - We guarantee results. - This is guaranteed to work. - We are guaranteeing success. - `; - - // Create test file - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const inst017Violations = violations.filter(v => v.rule === 'inst_017'); - - expect(inst017Violations.length).toBeGreaterThanOrEqual(4); - expect(inst017Violations.some(v => v.match.toLowerCase().includes('guarantee'))).toBe(true); - }); - - test('should detect "ensures 100%"', async () => { - const testContent = 'This ensures 100% accuracy.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.html'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const inst017Violations = violations.filter(v => v.rule === 'inst_017'); - - expect(inst017Violations.length).toBeGreaterThan(0); - expect(inst017Violations.some(v => v.match.toLowerCase().includes('ensures'))).toBe(true); - }); - - test('should detect "eliminates all"', async () => { - const testContent = 'This eliminates all bugs.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.js'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - - expect(violations.length).toBeGreaterThan(0); - expect(violations.some(v => v.match.toLowerCase().includes('eliminates'))).toBe(true); - }); - - test('should detect "never fails"', async () => { - const testContent = 'This system never fails.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - - expect(violations.length).toBeGreaterThan(0); - expect(violations.some(v => v.match.toLowerCase().includes('never'))).toBe(true); - }); - }); - - describe('Pattern Detection - inst_018 (Unverified Claims)', () => { - test('should detect "production-ready" without context', async () => { - const testContent = 'This is production-ready software.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const inst018Violations = violations.filter(v => v.rule === 'inst_018'); - - expect(inst018Violations.length).toBeGreaterThan(0); - }); - - test('should detect "battle-tested"', async () => { - const testContent = 'Our battle-tested framework.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.html'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - - expect(violations.some(v => v.match.toLowerCase().includes('battle-tested'))).toBe(true); - }); - - test('should allow "production-ready development tool"', async () => { - const testContent = 'Tractatus is a production-ready development tool.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const inst018Violations = violations.filter(v => - v.rule === 'inst_018' && v.match.toLowerCase().includes('production-ready') - ); - - expect(inst018Violations.length).toBe(0); - }); - - test('should allow "production-ready proof-of-concept"', async () => { - const testContent = 'This is a production-ready proof-of-concept.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const inst018Violations = violations.filter(v => - v.rule === 'inst_018' && v.match.toLowerCase().includes('production-ready') - ); - - expect(inst018Violations.length).toBe(0); - }); - }); - - describe('Context Awareness', () => { - test('should allow prohibited terms in comments about rules', async () => { - const testContent = ` - // inst_017: Never use "guarantee" - // inst_017 prohibits guaranteed language - `; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.js'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - - // Should not flag violations in comments about the rules - expect(violations.length).toBe(0); - }); - - test('should exclude test files from scanning', async () => { - // Test files should be excluded by pattern - const scanner2 = new ProhibitedTermsScanner({ silent: true }); - - expect(scanner2.excludePatterns).toContain('**/tests/**/*.test.js'); - expect(scanner2.excludePatterns).toContain('**/tests/**/*.spec.js'); - }); - - test('should exclude GOVERNANCE-RULE-LIBRARY.md', async () => { - expect(scanner.excludePatterns).toContain('**/GOVERNANCE-RULE-LIBRARY.md'); - }); - - test('should exclude case studies', async () => { - expect(scanner.excludePatterns).toContain('**/docs/case-studies/**'); - }); - }); - - describe('Suggestions', () => { - test('should suggest "enforcement" for "guarantee"', () => { - const suggestions = scanner.patterns.find(p => p.id === 'inst_017').suggestions; - - expect(suggestions['guarantee']).toBe('enforcement'); - expect(suggestions['guarantees']).toBe('enforces'); - expect(suggestions['guaranteed']).toBe('enforced'); - }); - - test('should suggest replacements for "ensures 100%"', () => { - const suggestions = scanner.patterns.find(p => p.id === 'inst_017').suggestions; - - expect(suggestions['ensures 100%']).toBe('helps ensure'); - }); - - test('should suggest replacements for inst_018 terms', () => { - const suggestions = scanner.patterns.find(p => p.id === 'inst_018').suggestions; - - expect(suggestions['production-ready']).toBe('proof-of-concept'); - expect(suggestions['battle-tested']).toBe('in development'); - }); - - test('should get suggestion for matched term', () => { - const patternSet = scanner.patterns.find(p => p.id === 'inst_017'); - const suggestion = scanner.getSuggestion('guarantee', patternSet.suggestions); - - expect(suggestion).toBe('enforcement'); - }); - - test('should handle case-insensitive matches', () => { - const patternSet = scanner.patterns.find(p => p.id === 'inst_017'); - const suggestion = scanner.getSuggestion('GUARANTEE', patternSet.suggestions); - - expect(suggestion).toBe('enforcement'); - }); - }); - - describe('Auto-fix Functionality', () => { - test('should fix simple violations', async () => { - const testContent = 'This guarantees safety.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const results = await scanner.autoFix(violations); - - expect(results.fixed).toBeGreaterThan(0); - - const fixedContent = await fs.readFile(testFile, 'utf8'); - expect(fixedContent).toContain('enforces'); // guarantees → enforces - expect(fixedContent).not.toContain('guarantees'); - }); - - test('should preserve file structure during fix', async () => { - const testContent = `Line 1: Normal content -Line 2: This guarantees results -Line 3: More content -Line 4: This guaranteed safety -Line 5: Final line`; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - await scanner.autoFix(violations); - - const fixedContent = await fs.readFile(testFile, 'utf8'); - const lines = fixedContent.split('\n'); - - expect(lines.length).toBe(5); - expect(lines[0]).toBe('Line 1: Normal content'); - expect(lines[4]).toBe('Line 5: Final line'); - }); - - test('should handle multiple violations in same file', async () => { - const testContent = ` - We guarantee success. - This is guaranteed to work. - Our guarantees are strong. - `; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const results = await scanner.autoFix(violations); - - expect(results.fixed).toBeGreaterThan(0); - - const fixedContent = await fs.readFile(testFile, 'utf8'); - expect(fixedContent).not.toContain('guarantee'); - }); - - test('should skip violations without clear suggestions', async () => { - // inst_016 violations (fabricated statistics) require manual review - const testContent = '95% faster performance.'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - const inst016Violations = violations.filter(v => v.rule === 'inst_016'); - - const results = await scanner.autoFix(inst016Violations); - - // inst_016 requires source citation, can't auto-fix - expect(results.skipped).toBeGreaterThan(0); - }); - }); - - describe('Formatting', () => { - test('should format empty violations list', () => { - const output = scanner.formatViolations([]); - - expect(output).toContain('No prohibited terms found'); - }); - - test('should format violations summary', () => { - const violations = [ - { rule: 'inst_017', file: 'test.md', line: 1, match: 'guarantee', severity: 'HIGH' }, - { rule: 'inst_017', file: 'test.md', line: 2, match: 'guaranteed', severity: 'HIGH' }, - { rule: 'inst_018', file: 'test.html', line: 5, match: 'battle-tested', severity: 'MEDIUM' } - ]; - - const output = scanner.formatViolations(violations, false); - - expect(output).toContain('Found 3 violation'); - expect(output).toContain('inst_017: 2'); - expect(output).toContain('inst_018: 1'); - }); - - test('should format detailed violations', () => { - const violations = [ - { - rule: 'inst_017', - file: 'test.md', - line: 1, - match: 'guarantee', - severity: 'HIGH', - context: 'This guarantees safety', - suggestion: 'enforcement' - } - ]; - - const output = scanner.formatViolations(violations, true); - - expect(output).toContain('test.md:1'); - expect(output).toContain('Found: "guarantee"'); - expect(output).toContain('Suggestion: enforcement'); - }); - }); - - describe('Utility Functions', () => { - test('should escape regex special characters', () => { - const escaped = scanner.escapeRegex('test.file[0]'); - - expect(escaped).toBe('test\\.file\\[0\\]'); - }); - - test('should check allowed context for inst_017 references', () => { - const line = 'inst_017 prohibits guaranteed language'; - const result = scanner.isAllowedContext(line, 'guaranteed', 'test.md'); - - expect(result).toBe(true); - }); - - test('should check allowed context for case studies', () => { - const line = 'This guarantees results'; - const result = scanner.isAllowedContext(line, 'guarantees', 'docs/case-studies/example.md'); - - expect(result).toBe(true); - }); - - test('should reject prohibited terms in normal content', () => { - const line = 'This guarantees results'; - const result = scanner.isAllowedContext(line, 'guarantees', 'README.md'); - - expect(result).toBe(false); - }); - }); - - describe('File Inclusion/Exclusion', () => { - test('should include markdown files', () => { - expect(scanner.includePatterns).toContain('**/*.md'); - }); - - test('should include HTML files', () => { - expect(scanner.includePatterns).toContain('**/*.html'); - }); - - test('should include JavaScript files', () => { - expect(scanner.includePatterns).toContain('**/*.js'); - }); - - test('should exclude node_modules', () => { - expect(scanner.excludePatterns).toContain('**/node_modules/**'); - }); - - test('should exclude .git directory', () => { - expect(scanner.excludePatterns).toContain('**/.git/**'); - }); - - test('should exclude .claude directory', () => { - expect(scanner.excludePatterns).toContain('**/.claude/**'); - }); - }); - - describe('Edge Cases', () => { - test('should handle empty files', async () => { - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'empty.md'); - await fs.writeFile(testFile, ''); - - const violations = await scanner.scan(); - - // Should not error on empty files - expect(Array.isArray(violations)).toBe(true); - }); - - test('should handle files with only whitespace', async () => { - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'whitespace.md'); - await fs.writeFile(testFile, '\n\n \n\t\n'); - - const violations = await scanner.scan(); - - expect(Array.isArray(violations)).toBe(true); - }); - - test('should handle very long lines', async () => { - const longLine = 'a'.repeat(10000) + ' guarantee ' + 'b'.repeat(10000); - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'long.md'); - await fs.writeFile(testFile, longLine); - - const violations = await scanner.scan(); - - expect(violations.length).toBeGreaterThan(0); - }); - - test('should handle violations at file end without newline', async () => { - const testContent = 'This guarantees results'; - - await fs.mkdir(testFilesDir, { recursive: true }); - const testFile = path.join(testFilesDir, 'test.md'); - await fs.writeFile(testFile, testContent); - - const violations = await scanner.scan(); - - expect(violations.length).toBeGreaterThan(0); - }); - }); - - describe('Silent Mode', () => { - test('should suppress console output in silent mode', async () => { - const silentScanner = new ProhibitedTermsScanner({ - silent: true, - basePath: testFilesDir - }); - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - - await silentScanner.scan(); - - // In silent mode, should not call console.log for scanning message - const scanningCalls = consoleSpy.mock.calls.filter(call => - call.some(arg => typeof arg === 'string' && arg.includes('Scanning')) - ); - expect(scanningCalls.length).toBe(0); - - consoleSpy.mockRestore(); - }); - }); -}); diff --git a/tests/unit/koha.service.test.js b/tests/unit/koha.service.test.js deleted file mode 100644 index 8dc6763e..00000000 --- a/tests/unit/koha.service.test.js +++ /dev/null @@ -1,620 +0,0 @@ -/** - * Unit Tests - Koha Service - * Tests donation processing with mocked Stripe and Donation model - */ - -// Mock Stripe before requiring the service -jest.mock('stripe', () => { - const mockStripe = { - customers: { - list: jest.fn(), - create: jest.fn() - }, - checkout: { - sessions: { - create: jest.fn() - } - }, - subscriptions: { - retrieve: jest.fn(), - cancel: jest.fn() - }, - webhooks: { - constructEvent: jest.fn() - } - }; - return jest.fn(() => mockStripe); -}); - -// Mock Donation model -jest.mock('../../src/models/Donation.model', () => ({ - create: jest.fn(), - findByPaymentIntentId: jest.fn(), - findBySubscriptionId: jest.fn(), - updateStatus: jest.fn(), - cancelSubscription: jest.fn(), - markReceiptSent: jest.fn(), - getTransparencyMetrics: jest.fn(), - getStatistics: jest.fn() -})); - -// Mock currency utilities -jest.mock('../../src/config/currencies.config', () => ({ - isSupportedCurrency: jest.fn((curr) => ['NZD', 'USD', 'AUD', 'EUR', 'GBP'].includes(curr.toUpperCase())), - convertToNZD: jest.fn((amount, curr) => { - const rates = { NZD: 1, USD: 1.65, AUD: 1.07, EUR: 1.82, GBP: 2.05 }; - return Math.round(amount * (rates[curr.toUpperCase()] || 1)); - }), - getExchangeRate: jest.fn((curr) => { - const rates = { NZD: 1, USD: 1.65, AUD: 1.07, EUR: 1.82, GBP: 2.05 }; - return rates[curr.toUpperCase()] || 1; - }) -})); - -const kohaService = require('../../src/services/koha.service'); -const Donation = require('../../src/models/Donation.model'); -const stripe = require('stripe')(); - -describe('Koha Service', () => { - beforeEach(() => { - jest.clearAllMocks(); - // Suppress console output in tests - jest.spyOn(console, 'log').mockImplementation(); - jest.spyOn(console, 'error').mockImplementation(); - jest.spyOn(console, 'warn').mockImplementation(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('Constructor and Configuration', () => { - test('should initialize with stripe instance', () => { - expect(kohaService.stripe).toBeDefined(); - }); - - test('should have price IDs configured', () => { - expect(kohaService.priceIds).toBeDefined(); - expect(kohaService.priceIds).toHaveProperty('monthly_5'); - expect(kohaService.priceIds).toHaveProperty('monthly_15'); - expect(kohaService.priceIds).toHaveProperty('monthly_50'); - expect(kohaService.priceIds).toHaveProperty('one_time'); - }); - }); - - describe('createCheckoutSession()', () => { - const validDonation = { - amount: 1000, - currency: 'NZD', - frequency: 'one_time', - tier: 'custom', - donor: { - name: 'Test Donor', - email: 'test@example.com', - country: 'NZ' - }, - public_acknowledgement: false - }; - - test('should create one-time donation checkout session', async () => { - stripe.customers.list.mockResolvedValue({ data: [] }); - stripe.customers.create.mockResolvedValue({ id: 'cus_test123' }); - stripe.checkout.sessions.create.mockResolvedValue({ - id: 'cs_test123', - url: 'https://checkout.stripe.com/test' - }); - Donation.create.mockResolvedValue({ _id: 'donation_id' }); - - const result = await kohaService.createCheckoutSession(validDonation); - - expect(result).toHaveProperty('sessionId', 'cs_test123'); - expect(result).toHaveProperty('checkoutUrl'); - expect(stripe.customers.create).toHaveBeenCalled(); - expect(stripe.checkout.sessions.create).toHaveBeenCalled(); - expect(Donation.create).toHaveBeenCalled(); - }); - - test('should create monthly subscription checkout session', async () => { - // Set price ID before test - const originalPriceId = kohaService.priceIds.monthly_15; - kohaService.priceIds.monthly_15 = 'price_15'; - - const monthlyDonation = { - ...validDonation, - frequency: 'monthly', - tier: '15' - }; - - stripe.customers.list.mockResolvedValue({ data: [] }); - stripe.customers.create.mockResolvedValue({ id: 'cus_test123' }); - stripe.checkout.sessions.create.mockResolvedValue({ - id: 'cs_test456', - url: 'https://checkout.stripe.com/test2' - }); - Donation.create.mockResolvedValue({ _id: 'donation_id2' }); - - const result = await kohaService.createCheckoutSession(monthlyDonation); - - expect(result.frequency).toBe('monthly'); - expect(stripe.checkout.sessions.create).toHaveBeenCalledWith( - expect.objectContaining({ - mode: 'subscription', - line_items: expect.arrayContaining([ - expect.objectContaining({ price: 'price_15' }) - ]) - }) - ); - - // Restore original - kohaService.priceIds.monthly_15 = originalPriceId; - }); - - test('should reuse existing Stripe customer', async () => { - const existingCustomer = { id: 'cus_existing', email: 'test@example.com' }; - stripe.customers.list.mockResolvedValue({ data: [existingCustomer] }); - stripe.checkout.sessions.create.mockResolvedValue({ - id: 'cs_test789', - url: 'https://checkout.stripe.com/test3' - }); - Donation.create.mockResolvedValue({ _id: 'donation_id3' }); - - await kohaService.createCheckoutSession(validDonation); - - expect(stripe.customers.create).not.toHaveBeenCalled(); - expect(stripe.checkout.sessions.create).toHaveBeenCalledWith( - expect.objectContaining({ - customer: 'cus_existing' - }) - ); - }); - - test('should reject unsupported currency', async () => { - const invalidCurrency = { - ...validDonation, - currency: 'JPY' // Not supported - }; - - await expect( - kohaService.createCheckoutSession(invalidCurrency) - ).rejects.toThrow('Unsupported currency'); - }); - - test('should reject amount below minimum', async () => { - const lowAmount = { - ...validDonation, - amount: 50 // Less than $1.00 - }; - - await expect( - kohaService.createCheckoutSession(lowAmount) - ).rejects.toThrow('Minimum donation amount is $1.00'); - }); - - test('should reject invalid frequency', async () => { - const invalidFreq = { - ...validDonation, - frequency: 'weekly' // Not supported - }; - - await expect( - kohaService.createCheckoutSession(invalidFreq) - ).rejects.toThrow('Invalid frequency'); - }); - - test('should require donor email', async () => { - const noEmail = { - ...validDonation, - donor: { name: 'Test' } // No email - }; - - await expect( - kohaService.createCheckoutSession(noEmail) - ).rejects.toThrow('Donor email is required'); - }); - - test('should reject invalid monthly tier', async () => { - const invalidTier = { - ...validDonation, - frequency: 'monthly', - tier: '100' // Not a valid tier - }; - - stripe.customers.list.mockResolvedValue({ data: [] }); - stripe.customers.create.mockResolvedValue({ id: 'cus_test' }); - - await expect( - kohaService.createCheckoutSession(invalidTier) - ).rejects.toThrow('Invalid monthly tier'); - }); - - test('should handle customer creation failure', async () => { - stripe.customers.list.mockRejectedValue(new Error('Stripe API error')); - - await expect( - kohaService.createCheckoutSession(validDonation) - ).rejects.toThrow('Failed to process donor information'); - }); - }); - - describe('handleWebhook()', () => { - test('should handle checkout.session.completed', async () => { - const event = { - type: 'checkout.session.completed', - data: { object: { id: 'cs_test', metadata: {} } } - }; - - const handleCheckoutCompleteSpy = jest.spyOn(kohaService, 'handleCheckoutComplete').mockResolvedValue(); - - await kohaService.handleWebhook(event); - - expect(handleCheckoutCompleteSpy).toHaveBeenCalled(); - handleCheckoutCompleteSpy.mockRestore(); - }); - - test('should handle payment_intent.succeeded', async () => { - const event = { - type: 'payment_intent.succeeded', - data: { object: { id: 'pi_test' } } - }; - - const handlePaymentSuccessSpy = jest.spyOn(kohaService, 'handlePaymentSuccess').mockResolvedValue(); - - await kohaService.handleWebhook(event); - - expect(handlePaymentSuccessSpy).toHaveBeenCalled(); - handlePaymentSuccessSpy.mockRestore(); - }); - - test('should handle payment_intent.payment_failed', async () => { - const event = { - type: 'payment_intent.payment_failed', - data: { object: { id: 'pi_test' } } - }; - - const handlePaymentFailureSpy = jest.spyOn(kohaService, 'handlePaymentFailure').mockResolvedValue(); - - await kohaService.handleWebhook(event); - - expect(handlePaymentFailureSpy).toHaveBeenCalled(); - handlePaymentFailureSpy.mockRestore(); - }); - - test('should handle invoice.paid', async () => { - const event = { - type: 'invoice.paid', - data: { object: { id: 'in_test' } } - }; - - const handleInvoicePaidSpy = jest.spyOn(kohaService, 'handleInvoicePaid').mockResolvedValue(); - - await kohaService.handleWebhook(event); - - expect(handleInvoicePaidSpy).toHaveBeenCalled(); - handleInvoicePaidSpy.mockRestore(); - }); - - test('should handle customer.subscription.deleted', async () => { - const event = { - type: 'customer.subscription.deleted', - data: { object: { id: 'sub_test' } } - }; - - const handleSubscriptionCancellationSpy = jest.spyOn(kohaService, 'handleSubscriptionCancellation').mockResolvedValue(); - - await kohaService.handleWebhook(event); - - expect(handleSubscriptionCancellationSpy).toHaveBeenCalled(); - handleSubscriptionCancellationSpy.mockRestore(); - }); - - test('should log unhandled event types', async () => { - const event = { - type: 'unknown.event.type', - data: { object: {} } - }; - - await kohaService.handleWebhook(event); - - expect(console.log).toHaveBeenCalledWith( - expect.stringContaining('Unhandled webhook event type') - ); - }); - - test('should throw error if webhook processing fails', async () => { - const event = { - type: 'checkout.session.completed', - data: { object: { id: 'cs_test' } } - }; - - jest.spyOn(kohaService, 'handleCheckoutComplete').mockRejectedValue(new Error('Processing failed')); - - await expect( - kohaService.handleWebhook(event) - ).rejects.toThrow('Processing failed'); - }); - }); - - describe('handleCheckoutComplete()', () => { - test('should create new donation record', async () => { - const session = { - id: 'cs_test', - amount_total: 1500, - currency: 'nzd', - customer_email: 'test@example.com', - customer: 'cus_test', - payment_intent: 'pi_test', - subscription: null, - metadata: { - frequency: 'one_time', - tier: 'custom', - currency: 'NZD', - amount_nzd: '1500', - exchange_rate: '1.0', - donor_name: 'Test Donor', - public_acknowledgement: 'no' - } - }; - - Donation.findByPaymentIntentId.mockResolvedValue(null); - Donation.create.mockResolvedValue({ _id: 'donation_id', donor: { email: 'test@example.com' } }); - - await kohaService.handleCheckoutComplete(session); - - expect(Donation.create).toHaveBeenCalledWith( - expect.objectContaining({ - amount: 1500, - frequency: 'one_time', - status: 'completed' - }) - ); - }); - - test('should update existing donation record', async () => { - const session = { - id: 'cs_test', - amount_total: 1500, - payment_intent: 'pi_existing', - subscription: 'sub_test', - metadata: { - frequency: 'monthly', - tier: '15', - currency: 'NZD', - amount_nzd: '1500' - } - }; - - const existingDonation = { _id: 'existing_id', status: 'pending' }; - Donation.findByPaymentIntentId.mockResolvedValue(existingDonation); - Donation.updateStatus.mockResolvedValue(true); - - await kohaService.handleCheckoutComplete(session); - - expect(Donation.updateStatus).toHaveBeenCalledWith( - 'existing_id', - 'completed', - expect.objectContaining({ - 'stripe.subscription_id': 'sub_test' - }) - ); - }); - }); - - describe('handlePaymentSuccess()', () => { - test('should update pending donation to completed', async () => { - const paymentIntent = { id: 'pi_test' }; - const donation = { _id: 'donation_id', status: 'pending' }; - - Donation.findByPaymentIntentId.mockResolvedValue(donation); - Donation.updateStatus.mockResolvedValue(true); - - await kohaService.handlePaymentSuccess(paymentIntent); - - expect(Donation.updateStatus).toHaveBeenCalledWith( - 'donation_id', - 'completed', - expect.any(Object) - ); - }); - - test('should not update non-pending donations', async () => { - const paymentIntent = { id: 'pi_test' }; - const donation = { _id: 'donation_id', status: 'completed' }; - - Donation.findByPaymentIntentId.mockResolvedValue(donation); - - await kohaService.handlePaymentSuccess(paymentIntent); - - expect(Donation.updateStatus).not.toHaveBeenCalled(); - }); - }); - - describe('handlePaymentFailure()', () => { - test('should mark donation as failed', async () => { - const paymentIntent = { - id: 'pi_test', - last_payment_error: { message: 'Card declined' } - }; - const donation = { _id: 'donation_id' }; - - Donation.findByPaymentIntentId.mockResolvedValue(donation); - Donation.updateStatus.mockResolvedValue(true); - - await kohaService.handlePaymentFailure(paymentIntent); - - expect(Donation.updateStatus).toHaveBeenCalledWith( - 'donation_id', - 'failed', - expect.objectContaining({ - 'metadata.failure_reason': 'Card declined' - }) - ); - }); - }); - - describe('handleInvoicePaid()', () => { - test('should create donation for recurring payment', async () => { - const invoice = { - id: 'in_test', - subscription: 'sub_test', - customer_email: 'test@example.com', - customer: 'cus_test', - amount_paid: 1500, - currency: 'nzd', - charge: 'ch_test', - created: Math.floor(Date.now() / 1000) - }; - - const subscription = { - metadata: { - tier: '15', - public_acknowledgement: 'yes', - currency: 'NZD' - } - }; - - stripe.subscriptions.retrieve.mockResolvedValue(subscription); - Donation.create.mockResolvedValue({ _id: 'donation_id' }); - - await kohaService.handleInvoicePaid(invoice); - - expect(Donation.create).toHaveBeenCalledWith( - expect.objectContaining({ - frequency: 'monthly', - status: 'completed', - amount: 1500 - }) - ); - }); - }); - - describe('verifyWebhookSignature()', () => { - test('should verify valid webhook signature', () => { - const payload = 'webhook payload'; - const signature = 'sig_test'; - const event = { type: 'test', data: {} }; - - stripe.webhooks.constructEvent.mockReturnValue(event); - - const result = kohaService.verifyWebhookSignature(payload, signature); - - expect(result).toEqual(event); - expect(stripe.webhooks.constructEvent).toHaveBeenCalledWith( - payload, - signature, - process.env.STRIPE_KOHA_WEBHOOK_SECRET - ); - }); - - test('should throw error for invalid signature', () => { - stripe.webhooks.constructEvent.mockImplementation(() => { - throw new Error('Invalid signature'); - }); - - expect(() => { - kohaService.verifyWebhookSignature('payload', 'bad_sig'); - }).toThrow('Invalid webhook signature'); - }); - }); - - describe('getTransparencyMetrics()', () => { - test('should return transparency metrics', async () => { - const mockMetrics = { - total_received: 5000, - monthly_supporters: 10, - one_time_donations: 50 - }; - - Donation.getTransparencyMetrics.mockResolvedValue(mockMetrics); - - const result = await kohaService.getTransparencyMetrics(); - - expect(result).toEqual(mockMetrics); - expect(Donation.getTransparencyMetrics).toHaveBeenCalled(); - }); - - test('should throw error if metrics retrieval fails', async () => { - Donation.getTransparencyMetrics.mockRejectedValue(new Error('Database error')); - - await expect( - kohaService.getTransparencyMetrics() - ).rejects.toThrow('Database error'); - }); - }); - - describe('sendReceiptEmail()', () => { - test('should generate receipt number and mark as sent', async () => { - const donation = { - _id: 'donation123', - donor: { email: 'test@example.com' } - }; - - Donation.markReceiptSent.mockResolvedValue(true); - - const result = await kohaService.sendReceiptEmail(donation); - - expect(result).toBe(true); - expect(Donation.markReceiptSent).toHaveBeenCalledWith( - 'donation123', - expect.stringMatching(/^KOHA-\d{4}-[A-Z0-9]{8}$/) - ); - }); - }); - - describe('cancelRecurringDonation()', () => { - test('should cancel subscription in Stripe and database', async () => { - stripe.subscriptions.cancel.mockResolvedValue({ id: 'sub_test', status: 'canceled' }); - Donation.cancelSubscription.mockResolvedValue(true); - - const result = await kohaService.cancelRecurringDonation('sub_test'); - - expect(result).toEqual({ - success: true, - message: 'Subscription cancelled successfully' - }); - expect(stripe.subscriptions.cancel).toHaveBeenCalledWith('sub_test'); - expect(Donation.cancelSubscription).toHaveBeenCalledWith('sub_test'); - }); - - test('should throw error if cancellation fails', async () => { - stripe.subscriptions.cancel.mockRejectedValue(new Error('Subscription not found')); - - await expect( - kohaService.cancelRecurringDonation('sub_nonexistent') - ).rejects.toThrow('Subscription not found'); - }); - }); - - describe('getStatistics()', () => { - test('should return donation statistics', async () => { - const mockStats = { - total_count: 100, - total_amount: 10000, - by_frequency: { monthly: 20, one_time: 80 } - }; - - Donation.getStatistics.mockResolvedValue(mockStats); - - const result = await kohaService.getStatistics(); - - expect(result).toEqual(mockStats); - expect(Donation.getStatistics).toHaveBeenCalledWith(null, null); - }); - - test('should support date range filtering', async () => { - const mockStats = { total_count: 10, total_amount: 1000 }; - - Donation.getStatistics.mockResolvedValue(mockStats); - - await kohaService.getStatistics('2025-01-01', '2025-12-31'); - - expect(Donation.getStatistics).toHaveBeenCalledWith('2025-01-01', '2025-12-31'); - }); - }); - - describe('Service Singleton', () => { - test('should export singleton instance', () => { - const kohaService2 = require('../../src/services/koha.service'); - expect(kohaService).toBe(kohaService2); - }); - }); -}); diff --git a/tests/unit/markdown.util.test.js b/tests/unit/markdown.util.test.js deleted file mode 100644 index 5f7efcc8..00000000 --- a/tests/unit/markdown.util.test.js +++ /dev/null @@ -1,502 +0,0 @@ -/** - * Unit Tests - Markdown Utility - * Tests markdown conversion, TOC extraction, front matter parsing, and slug generation - */ - -const { - markdownToHtml, - extractTOC, - extractFrontMatter, - generateSlug -} = require('../../src/utils/markdown.util'); - -describe('Markdown Utility', () => { - describe('markdownToHtml', () => { - test('should return empty string for null input', () => { - expect(markdownToHtml(null)).toBe(''); - }); - - test('should return empty string for undefined input', () => { - expect(markdownToHtml(undefined)).toBe(''); - }); - - test('should return empty string for empty string', () => { - expect(markdownToHtml('')).toBe(''); - }); - - test('should convert basic paragraph', () => { - const markdown = 'This is a paragraph.'; - const html = markdownToHtml(markdown); - - expect(html).toContain('

This is a paragraph.

'); - }); - - test('should convert headings with IDs', () => { - const markdown = '# Test Heading'; - const html = markdownToHtml(markdown); - - expect(html).toContain('

Test Heading

'); - }); - - test('should convert multiple heading levels', () => { - const markdown = `# Heading 1 -## Heading 2 -### Heading 3`; - const html = markdownToHtml(markdown); - - expect(html).toContain('

Heading 1

'); - expect(html).toContain('

Heading 2

'); - expect(html).toContain('

Heading 3

'); - }); - - test('should generate slugs from headings with special characters', () => { - const markdown = '# Test: Special Characters!'; - const html = markdownToHtml(markdown); - - expect(html).toContain('id="test-special-characters"'); - }); - - test('should convert bold text', () => { - const markdown = '**bold text**'; - const html = markdownToHtml(markdown); - - expect(html).toContain('bold text'); - }); - - test('should convert italic text', () => { - const markdown = '*italic text*'; - const html = markdownToHtml(markdown); - - expect(html).toContain('italic text'); - }); - - test('should convert inline code', () => { - const markdown = '`code snippet`'; - const html = markdownToHtml(markdown); - - expect(html).toContain('code snippet'); - }); - - test('should convert code blocks with language', () => { - const markdown = '```javascript\nconst x = 1;\n```'; - const html = markdownToHtml(markdown); - - expect(html).toContain(' { - const markdown = '```\nplain code\n```'; - const html = markdownToHtml(markdown); - - expect(html).toContain(' { - const markdown = `- Item 1 -- Item 2 -- Item 3`; - const html = markdownToHtml(markdown); - - expect(html).toContain('
    '); - expect(html).toContain('
  • Item 1
  • '); - expect(html).toContain('
  • Item 2
  • '); - expect(html).toContain('
  • Item 3
  • '); - expect(html).toContain('
'); - }); - - test('should convert ordered lists', () => { - const markdown = `1. First -2. Second -3. Third`; - const html = markdownToHtml(markdown); - - expect(html).toContain('
    '); - expect(html).toContain('
  1. First
  2. '); - expect(html).toContain('
  3. Second
  4. '); - expect(html).toContain('
  5. Third
  6. '); - expect(html).toContain('
'); - }); - - test('should convert links', () => { - const markdown = '[Link Text](https://example.com)'; - const html = markdownToHtml(markdown); - - expect(html).toContain('Link Text'); - }); - - test('should convert images', () => { - const markdown = '![Alt Text](https://example.com/image.png)'; - const html = markdownToHtml(markdown); - - expect(html).toContain(' { - const markdown = '> This is a quote'; - const html = markdownToHtml(markdown); - - expect(html).toContain('
'); - expect(html).toContain('This is a quote'); - expect(html).toContain('
'); - }); - - test('should convert tables', () => { - const markdown = `| Header 1 | Header 2 | -|----------|----------| -| Cell 1 | Cell 2 |`; - const html = markdownToHtml(markdown); - - expect(html).toContain(''); - expect(html).toContain(''); - expect(html).toContain(''); - expect(html).toContain(''); - expect(html).toContain(''); - }); - - test('should sanitize dangerous HTML (XSS protection)', () => { - const markdown = ''; - const html = markdownToHtml(markdown); - - // Script tags should be removed - expect(html).not.toContain('
Header 1Cell 1