Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
208 lines
6.1 KiB
TypeScript
208 lines
6.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import yaml from 'js-yaml';
|
|
import {
|
|
generateComposeFile,
|
|
generateCaddyfile,
|
|
generateLabels,
|
|
} from './compose-generator.js';
|
|
import type { PreviewConfig } from './types.js';
|
|
|
|
describe('generateComposeFile', () => {
|
|
const baseOpts = {
|
|
projectPath: '/workspace/repos/my-project-abc123',
|
|
port: 9100,
|
|
deploymentId: 'test123',
|
|
labels: {
|
|
'cw.preview': 'true',
|
|
'cw.initiative-id': 'init-1',
|
|
'cw.port': '9100',
|
|
},
|
|
};
|
|
|
|
it('generates valid compose YAML with user services and Caddy proxy', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
app: {
|
|
name: 'app',
|
|
build: '.',
|
|
port: 3000,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = generateComposeFile(config, baseOpts);
|
|
const parsed = yaml.load(result) as Record<string, any>;
|
|
|
|
// Has both user service and caddy
|
|
expect(parsed.services.app).toBeDefined();
|
|
expect(parsed.services['caddy-proxy']).toBeDefined();
|
|
|
|
// Network present
|
|
expect(parsed.networks.preview).toBeDefined();
|
|
|
|
// Caddy publishes port
|
|
expect(parsed.services['caddy-proxy'].ports).toContain('9100:80');
|
|
|
|
// Labels propagated
|
|
expect(parsed.services.app.labels['cw.preview']).toBe('true');
|
|
});
|
|
|
|
it('handles object build config with context path joining', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
api: {
|
|
name: 'api',
|
|
build: { context: 'packages/api', dockerfile: 'Dockerfile.prod' },
|
|
port: 8080,
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = generateComposeFile(config, baseOpts);
|
|
const parsed = yaml.load(result) as Record<string, any>;
|
|
|
|
expect(parsed.services.api.build.context).toBe(
|
|
'/workspace/repos/my-project-abc123/packages/api',
|
|
);
|
|
expect(parsed.services.api.build.dockerfile).toBe('Dockerfile.prod');
|
|
});
|
|
|
|
it('handles image-based services', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
db: {
|
|
name: 'db',
|
|
image: 'postgres:16',
|
|
port: 5432,
|
|
internal: true,
|
|
env: { POSTGRES_PASSWORD: 'test' },
|
|
},
|
|
},
|
|
};
|
|
|
|
const result = generateComposeFile(config, baseOpts);
|
|
const parsed = yaml.load(result) as Record<string, any>;
|
|
|
|
expect(parsed.services.db.image).toBe('postgres:16');
|
|
expect(parsed.services.db.environment.POSTGRES_PASSWORD).toBe('test');
|
|
});
|
|
|
|
it('caddy depends on all user services', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
frontend: { name: 'frontend', build: '.', port: 3000 },
|
|
backend: { name: 'backend', build: '.', port: 8080 },
|
|
},
|
|
};
|
|
|
|
const result = generateComposeFile(config, baseOpts);
|
|
const parsed = yaml.load(result) as Record<string, any>;
|
|
|
|
expect(parsed.services['caddy-proxy'].depends_on).toContain('frontend');
|
|
expect(parsed.services['caddy-proxy'].depends_on).toContain('backend');
|
|
});
|
|
});
|
|
|
|
describe('generateCaddyfile', () => {
|
|
it('generates simple single-service Caddyfile', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
app: { name: 'app', build: '.', port: 3000 },
|
|
},
|
|
};
|
|
|
|
const caddyfile = generateCaddyfile(config);
|
|
expect(caddyfile).toContain(':80 {');
|
|
expect(caddyfile).toContain('reverse_proxy app:3000');
|
|
expect(caddyfile).toContain('}');
|
|
});
|
|
|
|
it('generates multi-service Caddyfile with handle_path for non-root routes', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
frontend: { name: 'frontend', build: '.', port: 3000, route: '/' },
|
|
backend: { name: 'backend', build: '.', port: 8080, route: '/api' },
|
|
},
|
|
};
|
|
|
|
const caddyfile = generateCaddyfile(config);
|
|
expect(caddyfile).toContain('handle_path /api/*');
|
|
expect(caddyfile).toContain('reverse_proxy backend:8080');
|
|
expect(caddyfile).toContain('handle {');
|
|
expect(caddyfile).toContain('reverse_proxy frontend:3000');
|
|
});
|
|
|
|
it('excludes internal services from Caddyfile', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
app: { name: 'app', build: '.', port: 3000 },
|
|
db: { name: 'db', image: 'postgres', port: 5432, internal: true },
|
|
},
|
|
};
|
|
|
|
const caddyfile = generateCaddyfile(config);
|
|
expect(caddyfile).not.toContain('postgres');
|
|
expect(caddyfile).not.toContain('db:5432');
|
|
});
|
|
|
|
it('sorts routes by specificity (longer paths first)', () => {
|
|
const config: PreviewConfig = {
|
|
version: 1,
|
|
services: {
|
|
app: { name: 'app', build: '.', port: 3000, route: '/' },
|
|
api: { name: 'api', build: '.', port: 8080, route: '/api' },
|
|
auth: { name: 'auth', build: '.', port: 9090, route: '/api/auth' },
|
|
},
|
|
};
|
|
|
|
const caddyfile = generateCaddyfile(config);
|
|
const apiAuthIdx = caddyfile.indexOf('/api/auth');
|
|
const apiIdx = caddyfile.indexOf('handle_path /api/*');
|
|
const handleIdx = caddyfile.indexOf('handle {');
|
|
|
|
// /api/auth should come before /api which should come before /
|
|
expect(apiAuthIdx).toBeLessThan(apiIdx);
|
|
expect(apiIdx).toBeLessThan(handleIdx);
|
|
});
|
|
});
|
|
|
|
describe('generateLabels', () => {
|
|
it('generates correct labels', () => {
|
|
const labels = generateLabels({
|
|
initiativeId: 'init-1',
|
|
phaseId: 'phase-1',
|
|
projectId: 'proj-1',
|
|
branch: 'feature/test',
|
|
port: 9100,
|
|
previewId: 'abc123',
|
|
});
|
|
|
|
expect(labels['cw.preview']).toBe('true');
|
|
expect(labels['cw.initiative-id']).toBe('init-1');
|
|
expect(labels['cw.phase-id']).toBe('phase-1');
|
|
expect(labels['cw.project-id']).toBe('proj-1');
|
|
expect(labels['cw.branch']).toBe('feature/test');
|
|
expect(labels['cw.port']).toBe('9100');
|
|
expect(labels['cw.preview-id']).toBe('abc123');
|
|
});
|
|
|
|
it('omits phaseId label when not provided', () => {
|
|
const labels = generateLabels({
|
|
initiativeId: 'init-1',
|
|
projectId: 'proj-1',
|
|
branch: 'main',
|
|
port: 9100,
|
|
previewId: 'abc123',
|
|
});
|
|
|
|
expect(labels['cw.phase-id']).toBeUndefined();
|
|
});
|
|
});
|