feat(#37-41): Add domains, ideas, relationships, agents, widgets schema

Schema additions for issues #37-41:

New models:
- Domain (#37): Life domains (work, marriage, homelab, etc.)
- Idea (#38): Brain dumps with pgvector embeddings
- Relationship (#39): Generic entity linking (blocks, depends_on)
- Agent (#40): ClawdBot agent tracking with metrics
- AgentSession (#40): Conversation session tracking
- WidgetDefinition (#41): HUD widget registry
- UserLayout (#41): Per-user dashboard configuration

Updated models:
- Task, Event, Project: Added domainId foreign key
- User, Workspace: Added new relations

New enums:
- IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED
- RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc.
- AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED
- EntityType: Added IDEA, DOMAIN

Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
Jason Woltje
2026-01-29 12:29:21 -06:00
parent a220c2dc0a
commit 973502f26e
308 changed files with 18374 additions and 113 deletions

View File

@@ -0,0 +1,509 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Docker Stack Integration Tests
* Tests the full Docker Compose stack deployment
*/
describe('Docker Stack Integration Tests', () => {
const TIMEOUT = 120000; // 2 minutes for Docker operations
const HEALTH_CHECK_RETRIES = 30;
const HEALTH_CHECK_INTERVAL = 2000;
/**
* Helper function to execute Docker Compose commands
*/
async function dockerCompose(command: string): Promise<string> {
const { stdout } = await execAsync(`docker compose ${command}`, {
cwd: process.cwd(),
});
return stdout;
}
/**
* Helper function to check if a service is healthy
*/
async function waitForService(
serviceName: string,
retries = HEALTH_CHECK_RETRIES,
): Promise<boolean> {
for (let i = 0; i < retries; i++) {
try {
const { stdout } = await execAsync(
`docker compose ps --format json ${serviceName}`,
);
const serviceInfo = JSON.parse(stdout);
if (
serviceInfo.Health === 'healthy' ||
serviceInfo.State === 'running'
) {
return true;
}
} catch (error) {
// Service not ready yet
}
await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL));
}
return false;
}
/**
* Helper function to check HTTP endpoint
*/
async function checkHttpEndpoint(url: string): Promise<boolean> {
try {
const response = await fetch(url);
return response.ok;
} catch (error) {
return false;
}
}
describe('Core Services', () => {
beforeAll(async () => {
// Ensure clean state
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
afterAll(async () => {
// Cleanup
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should start PostgreSQL with health check',
async () => {
// Start only PostgreSQL
await dockerCompose('up -d postgres');
// Wait for service to be healthy
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
// Verify container is running
const ps = await dockerCompose('ps postgres');
expect(ps).toContain('mosaic-postgres');
expect(ps).toContain('Up');
},
TIMEOUT,
);
it(
'should start Valkey with health check',
async () => {
// Start Valkey
await dockerCompose('up -d valkey');
// Wait for service to be healthy
const isHealthy = await waitForService('valkey');
expect(isHealthy).toBe(true);
// Verify container is running
const ps = await dockerCompose('ps valkey');
expect(ps).toContain('mosaic-valkey');
expect(ps).toContain('Up');
},
TIMEOUT,
);
it(
'should have proper network configuration',
async () => {
// Check if mosaic-internal network exists
const { stdout } = await execAsync('docker network ls');
expect(stdout).toContain('mosaic-internal');
expect(stdout).toContain('mosaic-public');
},
TIMEOUT,
);
it(
'should have proper volume configuration',
async () => {
// Check if volumes are created
const { stdout } = await execAsync('docker volume ls');
expect(stdout).toContain('mosaic-postgres-data');
expect(stdout).toContain('mosaic-valkey-data');
},
TIMEOUT,
);
});
describe('Application Services', () => {
beforeAll(async () => {
// Start core services first
await dockerCompose('up -d postgres valkey');
await waitForService('postgres');
await waitForService('valkey');
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should start API service with dependencies',
async () => {
// Start API
await dockerCompose('up -d api');
// Wait for API to be healthy
const isHealthy = await waitForService('api');
expect(isHealthy).toBe(true);
// Verify API is accessible
const apiHealthy = await checkHttpEndpoint('http://localhost:3001/health');
expect(apiHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should start Web service with dependencies',
async () => {
// Ensure API is running
await dockerCompose('up -d api');
await waitForService('api');
// Start Web
await dockerCompose('up -d web');
// Wait for Web to be healthy
const isHealthy = await waitForService('web');
expect(isHealthy).toBe(true);
// Verify Web is accessible
const webHealthy = await checkHttpEndpoint('http://localhost:3000');
expect(webHealthy).toBe(true);
},
TIMEOUT,
);
});
describe('Optional Services (Profiles)', () => {
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should start Authentik services with profile',
async () => {
// Start Authentik services using profile
await dockerCompose('--profile authentik up -d');
// Wait for Authentik dependencies
await waitForService('authentik-postgres');
await waitForService('authentik-redis');
// Verify Authentik server starts
const isHealthy = await waitForService('authentik-server');
expect(isHealthy).toBe(true);
// Verify Authentik is accessible
const authentikHealthy = await checkHttpEndpoint(
'http://localhost:9000/-/health/live/',
);
expect(authentikHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should start Ollama service with profile',
async () => {
// Start Ollama using profile
await dockerCompose('--profile ollama up -d ollama');
// Wait for Ollama to start
const isHealthy = await waitForService('ollama');
expect(isHealthy).toBe(true);
// Verify Ollama is accessible
const ollamaHealthy = await checkHttpEndpoint(
'http://localhost:11434/api/tags',
);
expect(ollamaHealthy).toBe(true);
},
TIMEOUT,
);
});
describe('Full Stack', () => {
it(
'should start entire stack with all services',
async () => {
// Clean slate
await dockerCompose('down -v').catch(() => {});
// Start all core services (without profiles)
await dockerCompose('up -d');
// Wait for all core services
const postgresHealthy = await waitForService('postgres');
const valkeyHealthy = await waitForService('valkey');
const apiHealthy = await waitForService('api');
const webHealthy = await waitForService('web');
expect(postgresHealthy).toBe(true);
expect(valkeyHealthy).toBe(true);
expect(apiHealthy).toBe(true);
expect(webHealthy).toBe(true);
// Verify all services are running
const ps = await dockerCompose('ps');
expect(ps).toContain('mosaic-postgres');
expect(ps).toContain('mosaic-valkey');
expect(ps).toContain('mosaic-api');
expect(ps).toContain('mosaic-web');
},
TIMEOUT * 2,
);
});
describe('Service Dependencies', () => {
beforeAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should enforce dependency order',
async () => {
// Start web service (should auto-start dependencies)
await dockerCompose('up -d web');
// Wait a bit for services to start
await new Promise((resolve) => setTimeout(resolve, 10000));
// Verify all dependencies are running
const ps = await dockerCompose('ps');
expect(ps).toContain('mosaic-postgres');
expect(ps).toContain('mosaic-valkey');
expect(ps).toContain('mosaic-api');
expect(ps).toContain('mosaic-web');
},
TIMEOUT,
);
});
describe('Container Labels', () => {
beforeAll(async () => {
await dockerCompose('up -d postgres valkey');
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it('should have proper labels on containers', async () => {
// Check PostgreSQL labels
const { stdout: pgLabels } = await execAsync(
'docker inspect mosaic-postgres --format "{{json .Config.Labels}}"',
);
const pgLabelsObj = JSON.parse(pgLabels);
expect(pgLabelsObj['com.mosaic.service']).toBe('database');
expect(pgLabelsObj['com.mosaic.description']).toBeDefined();
// Check Valkey labels
const { stdout: valkeyLabels } = await execAsync(
'docker inspect mosaic-valkey --format "{{json .Config.Labels}}"',
);
const valkeyLabelsObj = JSON.parse(valkeyLabels);
expect(valkeyLabelsObj['com.mosaic.service']).toBe('cache');
expect(valkeyLabelsObj['com.mosaic.description']).toBeDefined();
});
describe('Failure Scenarios', () => {
const TIMEOUT = 120000;
beforeAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
afterAll(async () => {
await dockerCompose('down -v').catch(() => {});
}, TIMEOUT);
it(
'should handle service restart after crash',
async () => {
await dockerCompose('up -d postgres');
await waitForService('postgres');
const { stdout: containerName } = await execAsync(
'docker compose ps -q postgres',
);
const trimmedName = containerName.trim();
await execAsync(`docker kill ${trimmedName}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
await dockerCompose('up -d postgres');
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should handle port conflict gracefully',
async () => {
try {
const { stdout } = await execAsync(
'docker run -d -p 5432:5432 --name port-blocker postgres:17-alpine',
);
await dockerCompose('up -d postgres').catch((error) => {
expect(error.message).toContain('port is already allocated');
});
} finally {
await execAsync('docker rm -f port-blocker').catch(() => {});
}
},
TIMEOUT,
);
it(
'should handle invalid volume mount paths',
async () => {
try {
await execAsync(
'docker run -d --name invalid-mount -v /nonexistent/path:/data postgres:17-alpine',
);
const { stdout } = await execAsync(
'docker ps -a -f name=invalid-mount --format "{{.Status}}"',
);
expect(stdout).toBeDefined();
} catch (error) {
expect(error).toBeDefined();
} finally {
await execAsync('docker rm -f invalid-mount').catch(() => {});
}
},
TIMEOUT,
);
it(
'should handle network partition scenarios',
async () => {
await dockerCompose('up -d postgres valkey');
await waitForService('postgres');
await waitForService('valkey');
const { stdout: postgresContainer } = await execAsync(
'docker compose ps -q postgres',
);
const trimmedPostgres = postgresContainer.trim();
await execAsync(
`docker network disconnect mosaic-internal ${trimmedPostgres}`,
);
await new Promise((resolve) => setTimeout(resolve, 2000));
await execAsync(
`docker network connect mosaic-internal ${trimmedPostgres}`,
);
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
},
TIMEOUT,
);
it(
'should handle container out of memory scenario',
async () => {
try {
const { stdout } = await execAsync(
'docker run -d --name mem-limited --memory="10m" postgres:17-alpine',
);
await new Promise((resolve) => setTimeout(resolve, 5000));
const { stdout: status } = await execAsync(
'docker inspect mem-limited --format "{{.State.Status}}"',
);
expect(['exited', 'running']).toContain(status.trim());
} finally {
await execAsync('docker rm -f mem-limited').catch(() => {});
}
},
TIMEOUT,
);
it(
'should handle disk space issues gracefully',
async () => {
await dockerCompose('up -d postgres');
await waitForService('postgres');
const { stdout } = await execAsync('docker system df');
expect(stdout).toContain('Images');
expect(stdout).toContain('Containers');
expect(stdout).toContain('Volumes');
},
TIMEOUT,
);
it(
'should handle service dependency failures',
async () => {
await dockerCompose('up -d postgres').catch(() => {});
const { stdout: postgresContainer } = await execAsync(
'docker compose ps -q postgres',
);
const trimmedPostgres = postgresContainer.trim();
if (trimmedPostgres) {
await execAsync(`docker stop ${trimmedPostgres}`);
try {
await dockerCompose('up -d api');
} catch (error) {
expect(error).toBeDefined();
}
await dockerCompose('start postgres').catch(() => {});
await waitForService('postgres');
await dockerCompose('up -d api');
const apiHealthy = await waitForService('api');
expect(apiHealthy).toBe(true);
}
},
TIMEOUT,
);
it(
'should recover from corrupted volume data',
async () => {
await dockerCompose('up -d postgres');
await waitForService('postgres');
await dockerCompose('down');
await execAsync('docker volume rm mosaic-postgres-data').catch(() => {});
await dockerCompose('up -d postgres');
const isHealthy = await waitForService('postgres');
expect(isHealthy).toBe(true);
},
TIMEOUT,
);
});
});