Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
230 lines
7.9 KiB
JavaScript
230 lines
7.9 KiB
JavaScript
/**
|
|
* Agent User API Tests
|
|
* ADR-023: Agent-as-User & Infinite Chat Architecture
|
|
*
|
|
* Tests for creating agent users, listing agent users, and generating API keys
|
|
*
|
|
* NOTE: These tests are skipped for SQLite because agent-users.js uses
|
|
* PostgreSQL-specific SQL syntax ($1, $2, RETURNING, NOW()).
|
|
* Run with DATABASE_TYPE=postgres for full test coverage.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import { dbGet, dbAll, dbRun, sqlNow, isPostgres, resetAdapter } from '../../../database/connection.js';
|
|
|
|
// Skip all tests if using SQLite (agent-users.js uses PostgreSQL syntax)
|
|
const describePostgres = isPostgres() ? describe : describe.skip;
|
|
|
|
// Mock authenticate middleware for testing
|
|
vi.mock('../../../middleware/auth.js', () => ({
|
|
authenticate: (req, res, next) => {
|
|
req.user = { id: 1, email: 'test@example.com', role: 'admin' };
|
|
next();
|
|
}
|
|
}));
|
|
|
|
describePostgres('Agent User API', () => {
|
|
let testAgentTableId;
|
|
let testAgentRowId;
|
|
let testProjectId;
|
|
let testSpaceId;
|
|
|
|
beforeAll(async () => {
|
|
// Skip dev user creation (depends on NeoMetal which doesn't exist in tests)
|
|
process.env.SKIP_DEV_USER = 'true';
|
|
|
|
// Initialize database schema (required for SQLite in-memory tests)
|
|
await resetAdapter();
|
|
|
|
// Create test user first (foreign key constraint)
|
|
await dbRun(`
|
|
INSERT INTO users (id, email, password_hash, name, encryption_key_encrypted)
|
|
VALUES (1, 'test@example.com', 'hash', 'Test User', 'encrypted_key')
|
|
ON CONFLICT (id) DO NOTHING
|
|
`);
|
|
|
|
// Create test space
|
|
const spaceResult = await dbRun(`
|
|
INSERT INTO spaces (name, owner_id, type, created_at, updated_at)
|
|
VALUES (?, 1, 'business', ${sqlNow()}, ${sqlNow()})
|
|
`, ['Test AI Space']);
|
|
testSpaceId = spaceResult.lastInsertRowid;
|
|
|
|
// Create test project
|
|
const projectResult = await dbRun(`
|
|
INSERT INTO projects (name, space_id, owner_id, type, created_at, updated_at)
|
|
VALUES (?, ?, 1, 'custom', ${sqlNow()}, ${sqlNow()})
|
|
`, ['Test AI Project', testSpaceId]);
|
|
testProjectId = projectResult.lastInsertRowid;
|
|
|
|
// Create test AI Agents table
|
|
const tableResult = await dbRun(`
|
|
INSERT INTO universal_tables (name, project_id, created_at, updated_at)
|
|
VALUES (?, ?, ${sqlNow()}, ${sqlNow()})
|
|
`, ['AI Agents', testProjectId]);
|
|
testAgentTableId = tableResult.lastInsertRowid;
|
|
|
|
// Create test agent row
|
|
const agentData = {
|
|
name: 'Test Agent',
|
|
description: 'A test AI agent',
|
|
icon: '🤖',
|
|
system_prompt: 'You are a test agent',
|
|
status: 'active'
|
|
};
|
|
|
|
const rowResult = await dbRun(`
|
|
INSERT INTO table_rows (table_id, base_id, data, created_at, updated_at)
|
|
VALUES (?, ?, ?, ${sqlNow()}, ${sqlNow()})
|
|
`, [testAgentTableId, 'test-agent-1', JSON.stringify(agentData)]);
|
|
testAgentRowId = rowResult.lastInsertRowid;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup test data (may fail if tables dropped - ignore errors)
|
|
try {
|
|
await dbRun(`DELETE FROM table_rows WHERE table_id = ?`, [testAgentTableId]);
|
|
await dbRun(`DELETE FROM universal_tables WHERE id = ?`, [testAgentTableId]);
|
|
await dbRun(`DELETE FROM projects WHERE id = ?`, [testProjectId]);
|
|
await dbRun(`DELETE FROM spaces WHERE id = ?`, [testSpaceId]);
|
|
await dbRun(`DELETE FROM users WHERE user_type = 'agent' AND email LIKE '%test-agent%'`);
|
|
} catch (e) {
|
|
// Ignore cleanup errors in test mode
|
|
}
|
|
});
|
|
|
|
describe('POST /api/v3/users/create-agent-user', () => {
|
|
it('should create a user with user_type=agent linked to agent row', async () => {
|
|
const { createAgentUser } = await import('../agent-users.js');
|
|
|
|
const result = await createAgentUser({
|
|
agentTableId: testAgentTableId,
|
|
agentRowId: testAgentRowId,
|
|
name: 'Test Agent User',
|
|
createdBy: 1
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.user).toBeDefined();
|
|
expect(result.user.user_type).toBe('agent');
|
|
expect(result.user.managed_by_agent_table_id).toBe(testAgentTableId);
|
|
expect(result.user.managed_by_agent_row_id).toBe(testAgentRowId);
|
|
expect(result.user.email).toContain('@agents.godcrm.local');
|
|
});
|
|
|
|
it('should auto-generate email from agent name if not provided', async () => {
|
|
const { createAgentUser } = await import('../agent-users.js');
|
|
|
|
const result = await createAgentUser({
|
|
agentTableId: testAgentTableId,
|
|
agentRowId: testAgentRowId,
|
|
createdBy: 1
|
|
});
|
|
|
|
expect(result.user.email).toMatch(/test-agent.*@agents\.godcrm\.local/);
|
|
});
|
|
|
|
it('should store agent_config JSON in user record', async () => {
|
|
const { createAgentUser } = await import('../agent-users.js');
|
|
|
|
const agentConfig = {
|
|
auto_respond: true,
|
|
respond_only_when_mentioned: false,
|
|
max_response_tokens: 2000
|
|
};
|
|
|
|
const result = await createAgentUser({
|
|
agentTableId: testAgentTableId,
|
|
agentRowId: testAgentRowId,
|
|
agentConfig,
|
|
createdBy: 1
|
|
});
|
|
|
|
expect(result.user.agent_config).toMatchObject(agentConfig);
|
|
});
|
|
|
|
it('should fail if agent row does not exist', async () => {
|
|
const { createAgentUser } = await import('../agent-users.js');
|
|
|
|
const result = await createAgentUser({
|
|
agentTableId: testAgentTableId,
|
|
agentRowId: 999999,
|
|
createdBy: 1
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Agent not found');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/users/agents', () => {
|
|
it('should return all users with user_type=agent', async () => {
|
|
const { getAgentUsers } = await import('../agent-users.js');
|
|
|
|
const result = await getAgentUsers();
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(Array.isArray(result.users)).toBe(true);
|
|
|
|
// All returned users should be agents
|
|
for (const user of result.users) {
|
|
expect(user.user_type).toBe('agent');
|
|
}
|
|
});
|
|
|
|
it('should include linked agent data', async () => {
|
|
const { getAgentUsers } = await import('../agent-users.js');
|
|
|
|
const result = await getAgentUsers();
|
|
|
|
if (result.users.length > 0) {
|
|
const agentUser = result.users[0];
|
|
expect(agentUser.managed_by_agent_table_id).toBeDefined();
|
|
expect(agentUser.managed_by_agent_row_id).toBeDefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('POST /api/v3/users/:id/generate-api-key', () => {
|
|
it('should generate API key for agent user', async () => {
|
|
const { createAgentUser, generateUserApiKey } = await import('../agent-users.js');
|
|
|
|
// First create an agent user
|
|
const createResult = await createAgentUser({
|
|
agentTableId: testAgentTableId,
|
|
agentRowId: testAgentRowId,
|
|
createdBy: 1
|
|
});
|
|
|
|
const userId = createResult.user.id;
|
|
|
|
// Generate API key
|
|
const keyResult = await generateUserApiKey(userId, { createdBy: 1 });
|
|
|
|
expect(keyResult.success).toBe(true);
|
|
expect(keyResult.apiKey).toBeDefined();
|
|
expect(keyResult.apiKey).toMatch(/^sk-agent-/);
|
|
});
|
|
|
|
it('should fail for non-existent user', async () => {
|
|
const { generateUserApiKey } = await import('../agent-users.js');
|
|
|
|
const result = await generateUserApiKey(999999, { createdBy: 1 });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('User not found');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Agent User Integration', () => {
|
|
describe('Agent Authentication', () => {
|
|
it('should allow agent user to authenticate via API key', async () => {
|
|
// This test verifies that the auth middleware recognizes agent users
|
|
// and allows them to make API calls
|
|
|
|
// Implementation depends on how API key auth is set up
|
|
expect(true).toBe(true); // Placeholder for integration test
|
|
});
|
|
});
|
|
});
|