This guide helps developers migrate existing Yemaya (creative production studio) codebases to the unified Oshun monorepo.
Table of Contents#
- Overview
- Breaking Changes
- Pre-Migration Checklist
- Architecture Changes
- Step-by-Step Migration
- Asset Pipeline Changes
- Collaboration Features
- AI Integration
- Plugin System
- SDK Migration
- Testing Updates
- Common Issues
Overview#
The Yemaya platform is the creative production studio for AAA game and movie production. Migrating to Oshun consolidates AI generation (Isis), research capabilities (Sophia), worldbuilding (Hathor), and engine integration (Bellona).
Benefits of Migration#
| Capability | Before | After |
|---|---|---|
| AI Generation | Direct API calls | Unified Isis with tracking, cost management |
| Research | Manual document search | Sophia RAG with citations |
| Worldbuilding | Separate tools | Integrated Hathor |
| Engine Export | Custom pipelines | Bellona adapters for all engines |
| Real-time Collab | Custom implementation | Shared @oshun/websocket |
| Authentication | Yemaya-specific | Unified team/organization auth |
What Changes#
| Aspect | Before | After |
|---|---|---|
| Repository | Standalone yemaya-studio repo |
oshun/apps/yemaya/, oshun/libs/yemaya/ |
| Asset generation | Direct Replicate/RunPod | @isis/client with provenance |
| Real-time sync | Custom Yjs server | @oshun/websocket + Yjs |
| File storage | Direct S3 | @oshun/storage |
| Background jobs | BullMQ direct | @oshun/queue wrapper |
Breaking Changes#
Yemaya's migration changes several integration contracts. Treat these as required migrations, not optional cleanup:
| Area | Required Change | Compatibility Strategy |
|---|---|---|
| Asset generation | Submit generation through @isis/client rather than provider SDK calls |
Preserve provider-specific options only through typed Isis parameters |
| Engine export | Use Bellona export jobs for Godot, Unreal, Unity, and Blender packages | Validate exported package manifests before disabling legacy exporters |
| Collaboration | Move realtime sync onto the shared websocket/Yjs service | Run dual-write awareness events only during a controlled migration window |
| Storage | Store files through @oshun/storage with provenance metadata |
Backfill asset metadata and reject uploads missing project ownership |
| Authentication | Replace Yemaya-local team auth with unified organization/session auth | Migrate permissions before enabling shared gateway enforcement |
These changes centralize provenance, cost tracking, permissions, and release gates, which means old direct-provider or direct-storage paths must be removed before production traffic is switched.
Pre-Migration Checklist#
Before starting:
- Export all project data and assets
- Document all custom workflows
- List all third-party integrations
- Backup user permissions and team configurations
- Document asset pipeline configurations
- Review custom plugin code
Local Development Environment Setup#
Before migrating, set up the local development environment:
# Clone the Oshun monorepo
git clone git@github.com:GreyChimp/oshun.git
cd oshun
# Start core infrastructure (PostgreSQL, Redis, MinIO, Mailpit)
docker compose -f docker/docker-compose.dev.yml up -d
# Verify all services are running
docker compose -f docker/docker-compose.dev.yml ps
# Install dependencies
pnpm install
# Create your local .env from template (already done if using docker setup)
cp docker/.env.example docker/.env
The development databases are automatically created:
yemaya- Yemaya domain database (PostgreSQL with pgvector)oshun_dev- Shared development database
Connection URL: postgresql://oshun:oshun_dev@localhost:5432/yemaya
For detailed setup instructions, see CLAUDE.md.
Architecture Changes#
Old Architecture#
yemaya-studio/
├── apps/
│ ├── api/ # Main API (Express)
│ ├── web/ # React web app
│ ├── desktop/ # Electron app
│ ├── cli/ # CLI tool
│ └── workers/
│ ├── generation/ # AI generation worker
│ ├── export/ # Asset export worker
│ └── collaboration/ # Real-time sync worker
├── packages/
│ ├── core/ # Core business logic
│ ├── database/ # Prisma client
│ ├── auth/ # Authentication
│ ├── storage/ # S3 integration
│ ├── ai-providers/ # AI service integrations
│ ├── collaboration/ # Yjs/CRDT
│ ├── export/ # Engine exporters
│ └── sdk/ # Public SDK
└── plugins/
├── blender/ # Blender plugin
├── unreal/ # Unreal plugin
└── godot/ # Godot plugin
New Architecture#
oshun/
├── apps/yemaya/
│ ├── api/ # API Gateway (Hono)
│ ├── studio-web/ # React web app
│ ├── studio-desktop/ # Electron desktop app
│ ├── workers/ # Background processors
│ └── cli/ # CLI interface
│
├── libs/yemaya/
│ ├── sdk/ # TypeScript SDK
│ ├── sdk-cpp/ # C++ SDK
│ ├── sdk-python/ # Python SDK
│ ├── database/ # Prisma schema
│ ├── projects/ # Project management
│ ├── orchestration/ # Workflow orchestration
│ ├── collaboration/ # Real-time (Yjs)
│ ├── autonomous-pipelines/ # AI workflows
│ ├── agents/ # AI agent framework
│ ├── asset-library/ # Asset management
│ ├── ui/ # React components
│ └── event-handlers/ # Event subscriptions
│
├── libs/isis/ # ← Replaces packages/ai-providers
│ ├── client/
│ └── workflows/
│
├── libs/bellona/ # ← Replaces packages/export
│ ├── godot/
│ ├── unreal/
│ ├── unity/
│ └── blender/
│
└── libs/shared/ # ← Replaces duplicated utilities
├── storage/
├── queue/
├── websocket/
└── ...
Step-by-Step Migration#
Step 1: Project Structure Migration#
# Create Yemaya directory structure in Oshun
mkdir -p apps/yemaya/{api,studio-web,studio-desktop,workers,cli}
mkdir -p libs/yemaya/{sdk,database,projects,collaboration,ui}
# Copy main app code
cp -r yemaya-studio/apps/api/* oshun/apps/yemaya/api/
cp -r yemaya-studio/apps/web/* oshun/apps/yemaya/studio-web/
cp -r yemaya-studio/apps/desktop/* oshun/apps/yemaya/studio-desktop/
# Copy library code
cp -r yemaya-studio/packages/core/* oshun/libs/yemaya/projects/
cp -r yemaya-studio/packages/collaboration/* oshun/libs/yemaya/collaboration/
cp -r yemaya-studio/packages/sdk/* oshun/libs/yemaya/sdk/
Step 2: Update project.json#
Create Nx project configuration:
// apps/yemaya/api/project.json
{
"name": "yemaya-api",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/yemaya/api/src",
"projectType": "application",
"tags": ["scope:yemaya", "type:app", "platform:node"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/yemaya/api",
"main": "apps/yemaya/api/src/main.ts",
"tsConfig": "apps/yemaya/api/tsconfig.app.json"
}
},
"dev": {
"executor": "nx:run-commands",
"options": {
"command": "tsx watch apps/yemaya/api/src/main.ts"
}
},
"test": {
"executor": "@nx/vite:test",
"options": {
"config": "apps/yemaya/api/vitest.config.ts"
}
}
}
}
Step 3: Migrate API Framework#
// BEFORE: Express
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
app.use(authMiddleware);
app.get('/api/projects', async (req, res) => {
const projects = await getProjects(req.user.id);
res.json(projects);
});
app.listen(4000);
// AFTER: Hono with OpenAPI
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { OpenAPIHono } from '@hono/zod-openapi';
import { authMiddleware, rateLimitMiddleware } from '@oshun/traefik-config';
import { tracingMiddleware } from '@oshun/tracing';
import { metricsMiddleware } from '@oshun/metrics';
const app = new OpenAPIHono();
// Middleware stack
app.use('*', tracingMiddleware());
app.use('*', metricsMiddleware());
app.use('*', cors({ origin: ['https://studio.oshun.io'] }));
app.use('/api/*', rateLimitMiddleware({ windowMs: 60000, max: 100 }));
app.use('/api/*', authMiddleware({ audience: 'yemaya' }));
// Routes
app.route('/api/projects', projectsRouter);
app.route('/api/assets', assetsRouter);
app.route('/api/generation', generationRouter);
// OpenAPI documentation
app.doc('/openapi.json', {
openapi: '3.1.0',
info: { title: 'Yemaya API', version: '1.0.0' },
});
export default app;
Step 4: Migrate Storage Layer#
// BEFORE: Direct S3
import {
S3Client,
PutObjectCommand,
GetObjectCommand,
} from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
async function uploadAsset(file: Buffer, key: string) {
await s3.send(
new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: file,
})
);
return `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`;
}
// AFTER: Oshun storage abstraction
import { createStorageClient, StorageProvider } from '@oshun/storage';
const storage = createStorageClient({
provider: StorageProvider.S3,
bucket: process.env.ASSET_BUCKET,
region: process.env.AWS_REGION,
// Automatic CDN URL generation
cdnBaseUrl: process.env.ASSET_CDN_URL,
});
async function uploadAsset(
file: Buffer,
key: string,
metadata?: AssetMetadata
) {
const result = await storage.upload({
key: `projects/${projectId}/assets/${key}`,
body: file,
contentType: metadata?.mimeType,
metadata: {
projectId,
uploadedBy: userId,
...metadata,
},
});
return {
url: result.cdnUrl, // CDN URL
storageUrl: result.url, // Direct S3 URL
key: result.key,
size: result.size,
};
}
// Presigned URLs for large uploads
const uploadUrl = await storage.createPresignedUpload({
key: `projects/${projectId}/assets/${filename}`,
expiresIn: 3600,
maxSize: 500 * 1024 * 1024, // 500MB
});
Step 5: Migrate to Isis for AI Generation#
// BEFORE: Direct Replicate/RunPod
import Replicate from 'replicate';
const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });
async function generateImage(prompt: string) {
const output = await replicate.run(
'stability-ai/stable-diffusion:27b93a2413e...',
{ input: { prompt, width: 1024, height: 1024 } }
);
return output[0];
}
async function generateVideo(prompt: string) {
const output = await replicate.run(
'anotherjesse/zeroscope-v2-xl:9f747673...',
{ input: { prompt, num_frames: 24 } }
);
return output;
}
// AFTER: Isis client with unified interface
import { createIsisClient } from '@isis/client';
const isis = createIsisClient({ baseUrl: process.env.ISIS_API_URL });
async function generateImage(prompt: string, options: ImageOptions = {}) {
const job = await isis.generation.create({
type: 'image',
prompt,
model: options.model || 'stable-diffusion-xl',
parameters: {
width: options.width || 1024,
height: options.height || 1024,
steps: options.steps || 50,
guidance: options.guidance || 7.5,
negativePrompt: options.negativePrompt,
},
// Project context for tracking
metadata: {
projectId: options.projectId,
userId: options.userId,
source: 'yemaya',
},
});
// Wait for completion with progress
return await isis.generation.waitForCompletion(job.id, {
onProgress: options.onProgress,
});
}
async function generateVideo(prompt: string, options: VideoOptions = {}) {
const job = await isis.generation.create({
type: 'video',
prompt,
model: options.model || 'zeroscope-v2',
parameters: {
frames: options.frames || 24,
fps: options.fps || 8,
duration: options.duration,
},
metadata: {
projectId: options.projectId,
userId: options.userId,
source: 'yemaya',
},
});
return await isis.generation.waitForCompletion(job.id);
}
// 3D generation through Isis
async function generate3DModel(prompt: string) {
const job = await isis.generation.create({
type: '3d',
prompt,
model: 'point-e',
parameters: {
outputFormat: 'glb',
quality: 'high',
},
});
return await isis.generation.waitForCompletion(job.id);
}
Step 6: Migrate to Bellona for Exports#
// BEFORE: Custom export pipelines
async function exportToUnreal(projectId: string) {
const project = await getProject(projectId);
const assets = await getProjectAssets(projectId);
// Custom FBX conversion
const fbxAssets = await Promise.all(assets.map((a) => convertToFbx(a)));
// Package for Unreal
const package = await createUnrealPackage(project, fbxAssets);
return uploadPackage(package);
}
// AFTER: Bellona handles all exports
import { createBellonaClient } from '@bellona/client';
const bellona = createBellonaClient({ baseUrl: process.env.BELLONA_API_URL });
async function exportToEngine(
projectId: string,
engine: 'godot' | 'unreal' | 'unity' | 'blender'
) {
// Create export job
const job = await bellona.exports.create({
source: 'yemaya',
sourceId: projectId,
engine,
version: getEngineVersion(engine),
options: {
// Asset settings
includeTextures: true,
textureFormat: 'png',
textureSize: 2048,
// Mesh settings
meshFormat: engine === 'godot' ? 'gltf' : 'fbx',
lodLevels: [1.0, 0.5, 0.25],
// Animation settings
includeAnimations: true,
animationFormat: 'native',
// Project settings
generateProjectFile: true,
includeScripts: true,
},
});
// Stream progress to client
const progressStream = bellona.exports.streamProgress(job.id);
for await (const progress of progressStream) {
await notifyClient(projectId, {
type: 'export-progress',
stage: progress.stage,
percent: progress.percent,
});
}
// Get artifacts
const result = await bellona.exports.waitForCompletion(job.id);
return {
artifacts: result.artifacts,
downloadUrl: result.downloadUrl,
expiresAt: result.expiresAt,
};
}
Asset Pipeline Changes#
Asset References#
// BEFORE: Local asset references
interface Asset {
id: string;
url: string; // Direct S3 URL
type: string;
projectId: string;
}
// AFTER: Cross-domain asset references
interface Asset {
id: string;
yemayaId: string; // Yemaya's internal ID
isisAssetId?: string; // Reference to generated asset in Isis
url: string;
cdnUrl: string;
type: AssetType;
projectId: string;
metadata: {
width?: number;
height?: number;
duration?: number;
fileSize: number;
mimeType: string;
generationJob?: {
jobId: string;
prompt: string;
model: string;
};
};
provenance: {
source: 'upload' | 'generated' | 'import';
createdAt: string;
createdBy: string;
};
}
Asset Event Handlers#
// libs/yemaya/event-handlers/src/isis-handlers.ts
import { EventBus } from '@oshun/event-bus';
import { createYemayaService } from '@yemaya/service';
export function registerIsisHandlers(eventBus: EventBus) {
// Handle asset generation completion
eventBus.subscribe('isis.asset.generated', async (event) => {
const { assetId, metadata } = event.payload;
const projectId = metadata.projectId as string;
// Only process Yemaya-originated generations
if (metadata.source !== 'yemaya' || !projectId) {
return;
}
const yemaya = createYemayaService();
// Create asset reference in Yemaya
await yemaya.assets.createFromGeneration({
projectId,
isisAssetId: assetId,
url: event.payload.url,
type: event.payload.type,
metadata: event.payload.metadata,
generationJob: {
jobId: event.payload.jobId,
prompt: metadata.prompt as string,
model: metadata.model as string,
},
});
// Notify project collaborators
await eventBus.publish('yemaya.asset.generated', {
projectId,
assetId,
type: event.payload.type,
});
});
// Handle generation failures
eventBus.subscribe('isis.job.failed', async (event) => {
const { jobId, error, metadata } = event.payload;
const projectId = metadata?.projectId as string;
if (metadata?.source !== 'yemaya' || !projectId) {
return;
}
await eventBus.publish('yemaya.generation.failed', {
projectId,
jobId,
error,
});
});
}
Collaboration Features#
Real-time Sync Migration#
// BEFORE: Custom Yjs server
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const doc = new Y.Doc();
const provider = new WebsocketProvider(
'wss://collab.yemaya.io',
`project-${projectId}`,
doc
);
// AFTER: Oshun WebSocket with Yjs
import * as Y from 'yjs';
import { createCollaborationClient } from '@yemaya/collaboration';
const collaboration = createCollaborationClient({
wsUrl: process.env.WEBSOCKET_URL,
projectId,
userId,
});
// Project document sync
const projectDoc = await collaboration.getDocument('project');
const scenes = projectDoc.getArray('scenes');
const assets = projectDoc.getMap('assets');
// Awareness (cursors, selections)
collaboration.awareness.setLocalState({
user: { id: userId, name: userName, color: userColor },
cursor: null,
selection: null,
});
// Listen for updates
collaboration.on('update', (update) => {
console.log('Document updated', update);
});
collaboration.on('awareness', (changes) => {
console.log('Collaborators changed', changes);
});
Server-Side Collaboration Handler#
// apps/yemaya/api/src/collaboration/handler.ts
import { WebSocketServer } from '@oshun/websocket';
import { createCollaborationHandler } from '@yemaya/collaboration';
export function setupCollaboration(wss: WebSocketServer) {
const handler = createCollaborationHandler({
storage: {
// Persist Yjs documents
load: async (docId) => {
const doc = await redis.get(`yjs:${docId}`);
return doc ? Buffer.from(doc, 'base64') : null;
},
save: async (docId, update) => {
await redis.set(`yjs:${docId}`, update.toString('base64'));
},
},
auth: {
// Verify access permissions
canAccess: async (userId, projectId) => {
return await hasProjectAccess(userId, projectId);
},
canEdit: async (userId, projectId) => {
return await hasEditAccess(userId, projectId);
},
},
});
wss.on('connection', async (ws, req) => {
const projectId = req.query.projectId;
const userId = req.user.id;
if (!(await handler.auth.canAccess(userId, projectId))) {
ws.close(4003, 'Access denied');
return;
}
handler.handleConnection(ws, projectId, userId);
});
}
AI Integration#
Autonomous Pipeline Migration#
// BEFORE: Custom AI workflow
async function runCreativeWorkflow(prompt: string) {
// Manual orchestration of AI calls
const conceptArt = await replicate.run(/* ... */);
const variations = await Promise.all([
replicate.run(/* variation 1 */),
replicate.run(/* variation 2 */),
]);
const upscaled = await replicate.run(/* upscale */);
return { conceptArt, variations, upscaled };
}
// AFTER: Isis workflows
import { createIsisClient } from '@isis/client';
const isis = createIsisClient({ baseUrl: process.env.ISIS_API_URL });
async function runCreativeWorkflow(request: CreativeWorkflowRequest) {
// Use pre-built workflow in Isis
const workflow = await isis.workflows.run('creative-concept', {
prompt: request.prompt,
style: request.style,
variations: 4,
upscale: true,
metadata: {
projectId: request.projectId,
source: 'yemaya',
},
});
// Workflow handles all steps, we just wait for results
return await isis.workflows.waitForCompletion(workflow.id, {
onStepComplete: (step) => {
console.log(`Step ${step.name}: ${step.status}`);
},
});
}
// Create custom workflows
async function createCharacterWorkflow() {
await isis.workflows.create({
name: 'character-generation',
steps: [
{
name: 'concept',
type: 'image',
model: 'stable-diffusion-xl',
inputs: { prompt: '{{input.prompt}} character concept art' },
},
{
name: 'turnaround',
type: 'image',
model: 'stable-diffusion-xl',
inputs: { prompt: '{{input.prompt}} character turnaround sheet' },
dependsOn: ['concept'],
},
{
name: '3d-model',
type: '3d',
model: 'point-e',
inputs: { image: '{{steps.concept.output}}' },
dependsOn: ['concept'],
},
],
});
}
Research Integration with Sophia#
// NEW: Sophia integration for creative research
import { createSophiaClient } from '@sophia/client';
const sophia = createSophiaClient({ baseUrl: process.env.SOPHIA_API_URL });
// Research for creative projects
async function researchForProject(projectId: string, topic: string) {
// Search project-specific documents
const projectDocs = await sophia.search({
query: topic,
collections: [`project-${projectId}`],
limit: 10,
});
// Search global creative resources
const globalDocs = await sophia.search({
query: topic,
collections: ['creative-reference', 'art-history', 'film-techniques'],
limit: 20,
});
return {
projectReferences: projectDocs.hits,
globalReferences: globalDocs.hits,
};
}
// Ingest project documents for RAG
async function ingestProjectDocument(projectId: string, document: Document) {
await sophia.documents.ingest({
source: document.url,
collection: `project-${projectId}`,
metadata: {
projectId,
documentType: document.type,
uploadedBy: document.uploadedBy,
},
});
}
Plugin System#
Engine Plugin Updates#
// BEFORE: Plugin makes direct API calls
// blender-plugin/yemaya_addon.py
import requests
class YemayaOperator:
def execute(self, context):
response = requests.post(
f'{YEMAYA_API}/assets/upload',
files={'file': open(filepath, 'rb')},
headers={'Authorization': f'Bearer {token}'}
)
return response.json()
// AFTER: Use Yemaya SDK
// libs/yemaya/sdk-python/src/yemaya/client.py
from yemaya import YemayaClient
class YemayaClient:
def __init__(self, api_url: str, api_key: str):
self.api_url = api_url
self.api_key = api_key
def upload_asset(self, filepath: str, project_id: str) -> Asset:
"""Upload asset to Yemaya project."""
with open(filepath, 'rb') as f:
response = self._request(
'POST',
f'/projects/{project_id}/assets',
files={'file': f}
)
return Asset(**response)
def export_to_blender(self, project_id: str, options: ExportOptions) -> ExportJob:
"""Export project for Blender."""
response = self._request(
'POST',
f'/projects/{project_id}/export',
json={
'engine': 'blender',
'options': options.dict()
}
)
return ExportJob(**response)
# blender-plugin/yemaya_addon.py (updated)
from yemaya import YemayaClient
client = YemayaClient(
api_url=get_preference('yemaya_api_url'),
api_key=get_preference('yemaya_api_key')
)
class YEMAYA_OT_upload(bpy.types.Operator):
def execute(self, context):
asset = client.upload_asset(
filepath=bpy.data.filepath,
project_id=context.scene.yemaya_project_id
)
self.report({'INFO'}, f'Uploaded: {asset.id}')
return {'FINISHED'}
SDK Migration#
TypeScript SDK#
// BEFORE: packages/sdk/src/index.ts
export class YemayaClient {
constructor(options: { apiKey: string; baseUrl: string }) {
this.apiKey = options.apiKey;
this.baseUrl = options.baseUrl;
}
async getProject(id: string): Promise<Project> {
const response = await fetch(`${this.baseUrl}/projects/${id}`, {
headers: { 'X-API-Key': this.apiKey },
});
return response.json();
}
}
// AFTER: libs/yemaya/sdk/src/client.ts
import { createHttpClient, HttpClient } from '@oshun/http-client';
export interface YemayaClientOptions {
baseUrl?: string;
auth?: {
type: 'api-key' | 'jwt' | 'service-account';
credentials: string | ServiceAccountCredentials;
};
timeout?: number;
retries?: number;
}
export function createYemayaClient(options: YemayaClientOptions = {}) {
const http = createHttpClient({
baseUrl: options.baseUrl || 'https://api.oshun.io/yemaya',
timeout: options.timeout || 30000,
retries: options.retries || 3,
auth: options.auth,
});
return {
projects: createProjectsAPI(http),
assets: createAssetsAPI(http),
teams: createTeamsAPI(http),
generation: createGenerationAPI(http),
collaboration: createCollaborationAPI(http),
exports: createExportsAPI(http),
};
}
// Resource APIs
function createProjectsAPI(http: HttpClient) {
return {
list: (params?: ListParams) =>
http.get<PaginatedResponse<Project>>('/v1/projects', { params }),
get: (id: string) => http.get<Project>(`/v1/projects/${id}`),
create: (data: CreateProjectRequest) =>
http.post<Project>('/v1/projects', data),
update: (id: string, data: UpdateProjectRequest) =>
http.patch<Project>(`/v1/projects/${id}`, data),
delete: (id: string) => http.delete(`/v1/projects/${id}`),
// Generation through Isis
generateAsset: async (id: string, request: GenerateAssetRequest) => {
return http.post<GenerationJob>(`/v1/projects/${id}/generate`, request);
},
// Export through Bellona
export: async (id: string, request: ExportRequest) => {
return http.post<ExportJob>(`/v1/projects/${id}/export`, request);
},
};
}
// Usage
const yemaya = createYemayaClient({
auth: { type: 'api-key', credentials: process.env.YEMAYA_API_KEY },
});
const project = await yemaya.projects.create({
name: 'My Film Project',
type: 'film',
});
const generationJob = await yemaya.projects.generateAsset(project.id, {
type: 'image',
prompt: 'Concept art for sci-fi cityscape',
});
Testing Updates#
Test Configuration#
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.{test,spec}.ts'],
setupFiles: ['./test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['**/*.d.ts', '**/test/**'],
},
},
});
Mock External Domains#
// test/mocks/isis.ts
import { vi } from 'vitest';
export const mockIsisClient = {
generation: {
create: vi.fn().mockResolvedValue({ id: 'job-123', status: 'pending' }),
get: vi.fn().mockResolvedValue({ id: 'job-123', status: 'completed' }),
waitForCompletion: vi.fn().mockResolvedValue({
id: 'job-123',
status: 'completed',
assetId: 'asset-456',
url: 'https://cdn.oshun.io/assets/456.png',
}),
},
assets: {
get: vi.fn().mockResolvedValue({
id: 'asset-456',
url: 'https://cdn.oshun.io/assets/456.png',
}),
},
};
vi.mock('@isis/client', () => ({
createIsisClient: () => mockIsisClient,
}));
// test/mocks/bellona.ts
export const mockBellonaClient = {
exports: {
create: vi.fn().mockResolvedValue({ id: 'export-789', status: 'pending' }),
waitForCompletion: vi.fn().mockResolvedValue({
id: 'export-789',
status: 'completed',
artifacts: [{ url: 'https://...', filename: 'project.zip' }],
}),
},
};
vi.mock('@bellona/client', () => ({
createBellonaClient: () => mockBellonaClient,
}));
Integration Tests#
// test/integration/asset-generation.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { TestHarness } from '@oshun/testing';
describe('Asset Generation Integration', () => {
const harness = new TestHarness();
beforeAll(async () => {
await harness.setup();
});
afterAll(async () => {
await harness.teardown();
});
it('should generate asset and attach to project', async () => {
// Create project
const project = await harness.yemaya.createProject({ name: 'Test' });
// Generate asset
const job = await harness.yemaya.generateAsset(project.id, {
type: 'image',
prompt: 'A sunset',
});
// Wait for events
await harness.waitForEvent('isis.asset.generated', {
filter: (e) => e.metadata.projectId === project.id,
timeout: 60000,
});
// Verify asset attached
const assets = await harness.yemaya.getProjectAssets(project.id);
expect(assets).toHaveLength(1);
});
});
Common Issues#
Issue 1: Missing Cross-Domain Permissions#
Error: Forbidden - insufficient scope for isis.generation.create
Solution: Ensure service account has cross-domain permissions:
const yemaya = createYemayaClient({
auth: {
type: 'service-account',
credentials: {
clientId: process.env.SERVICE_CLIENT_ID,
clientSecret: process.env.SERVICE_CLIENT_SECRET,
scopes: ['yemaya:all', 'isis:generation:write', 'bellona:export:write'],
},
},
});
Issue 2: Event Handler Not Receiving Events#
No events received for 'isis.asset.generated'
Solution: Ensure consumer group is unique and handler is registered:
const eventBus = new EventBus({
redis: { url: process.env.REDIS_URL },
consumerGroup: 'yemaya-api-' + process.env.POD_NAME, // Unique per instance
});
// Register handlers BEFORE start()
registerIsisHandlers(eventBus);
await eventBus.start();
Issue 3: Asset URL Not Accessible#
Error: 403 Forbidden when accessing asset URL
Solution: Use CDN URLs and ensure proper CORS:
// Use CDN URL, not direct S3 URL
const asset = await isis.assets.get(assetId);
const url = asset.cdnUrl; // Not asset.storageUrl
Issue 4: Collaboration Sync Issues#
Yjs document diverged between clients
Solution: Ensure persistence and proper cleanup:
// Always persist updates
collaboration.on('update', async (update) => {
await redis.append(`yjs:${docId}:updates`, update);
});
// Compact periodically
await collaboration.compact(docId);
Migration Checklist#
Phase 1: Setup#
- Clone Oshun monorepo
- Set up development environment
- Configure environment variables
- Set up database schemas
Phase 2: Code Migration#
- Migrate apps to
apps/yemaya/ - Migrate libs to
libs/yemaya/ - Update import paths
- Replace utilities with
@oshun/*
Phase 3: AI Migration#
- Replace direct AI calls with Isis
- Migrate workflows to Isis workflows
- Set up event handlers for generation
- Add Sophia for research
Phase 4: Export Migration#
- Replace export pipelines with Bellona
- Update engine plugins
- Test export for all target engines
Phase 5: Collaboration#
- Migrate Yjs to shared infrastructure
- Update real-time sync handlers
- Test multi-user collaboration
Phase 6: Testing#
- Update test configuration
- Add cross-domain mocks
- Run integration tests
- Performance testing
Phase 7: SDK Updates#
- Update TypeScript SDK
- Update Python SDK
- Update C++ SDK
- Update plugin integrations
Phase 8: Deployment#
- Update CI/CD
- Configure Kubernetes
- Staging deployment
- Production rollout
Support#
For migration assistance:
- Slack:
#oshun-migration - Documentation:
/docs/migration/ - Issues: GitHub Issues with
migrationlabel