import assert from 'node:assert/strict'; import test from 'node:test'; import { bootstrapPortalAgentServices } from './portal-agent-services-bootstrap.mjs'; function createSetup(overrides = {}) { const calls = []; const pool = { id: 'pool' }; const agentJobService = { id: 'jobs' }; const mindSpaceAssets = { async syncWorkspaceAssets(userId, options) { calls.push([ 'sync-workspace', userId, options, ]); }, }; let backgroundOptions; let runnerOptions; let llmOptions; let assetConfigOptions; let imageAdminOptions; let reviewOptions; let generationOptions; const llmProviderService = { async ensureBootstrapRelay() { calls.push(['relay-bootstrap']); return { created: true }; }, async syncSelectedToGoosed() { calls.push(['provider-sync']); }, }; const options = { pool, env: { EXPERIENCE_PG_URL: 'postgres://experience', }, runtime: { experienceEnabled: true, agentWorker: { enabled: true }, publishRoot: '/publish', }, apiTarget: 'http://api-primary', apiTargets: [ 'http://api-primary', 'http://api-secondary', ], apiSecret: 'api-secret', userAuth: { async ensureAdminUser() { calls.push(['ensure-admin']); }, async ensureAllUserDataSpaces() { calls.push(['backfill-user-spaces']); return { errors: [{ userId: 'failed-user' }] }; }, }, sessionAccess: { id: 'session-access' }, mindSpaceRuntimeAdapter: { agentJobService, startBackgroundJobs(receivedOptions) { calls.push(['start-background']); backgroundOptions = receivedOptions; }, }, mindSpaceAssets, async resolveUserIdByDirKey(dirKey) { calls.push(['resolve-dir-key', dirKey]); return dirKey === 'missing' ? null : 'user-1'; }, workspaceMaintenanceEnabled: false, startWorkspaceThumbnailWatcher() {}, startWorkspaceAssetSyncWatcher() {}, logger: { log(...args) { calls.push(['log', ...args]); }, warn(...args) { calls.push(['warn', ...args]); }, error(...args) { calls.push(['error', ...args]); }, }, createExperienceServiceFn(receivedPool) { calls.push(['mysql-experience', receivedPool]); return { id: 'mysql-experience' }; }, async createPgExperienceServiceFn(receivedOptions) { calls.push(['pg-experience', receivedOptions]); return { id: 'pg-experience' }; }, createMindSpaceAgentRunnerFn(receivedOptions) { calls.push(['agent-runner']); runnerOptions = receivedOptions; return { id: 'runner' }; }, createMindSpaceAuditWriterFn(receivedPool) { calls.push(['audit', receivedPool]); return { id: 'audit' }; }, createLlmProviderServiceFn( receivedPool, receivedOptions, ) { calls.push(['llm-provider', receivedPool]); llmOptions = receivedOptions; return llmProviderService; }, createAssetGatewayConfigServiceFn( receivedPool, receivedOptions, ) { calls.push(['asset-config', receivedPool]); assetConfigOptions = receivedOptions; return { id: 'asset-config' }; }, createImageMakeAdminConfigServiceFn( receivedPool, receivedOptions, ) { calls.push(['image-admin', receivedPool]); imageAdminOptions = receivedOptions; return { id: 'image-admin', async ensureSchema() { calls.push(['image-schema']); }, }; }, createImageMakeClientFromEnvFn(receivedEnv) { calls.push(['image-client', receivedEnv]); return { id: 'image-client' }; }, createMindSpaceImageReviewServiceFn( receivedOptions, ) { calls.push(['image-review']); reviewOptions = receivedOptions; return { id: 'image-review' }; }, createMindSpaceImageGenerationServiceFn( receivedOptions, ) { calls.push(['image-generation']); generationOptions = receivedOptions; return { id: 'image-generation' }; }, createWordFilterServiceFn(receivedPool) { calls.push(['word-filter', receivedPool]); return { id: 'word-filter' }; }, relayBootstrap: { name: 'relay-name' }, ...overrides, }; return { calls, options, pool, agentJobService, mindSpaceAssets, llmProviderService, getCaptured() { return { backgroundOptions, runnerOptions, llmOptions, assetConfigOptions, imageAdminOptions, reviewOptions, generationOptions, }; }, }; } test('requires the Agent runtime service dependencies', async () => { await assert.rejects( bootstrapPortalAgentServices(), /requires runtime service dependencies/, ); }); test('preserves Agent, background, admin, and LLM assembly order', async () => { const setup = createSetup(); const result = await bootstrapPortalAgentServices( setup.options, ); await Promise.all([ result.relayBootstrapTask, result.providerSyncTask, ]); const captured = setup.getCaptured(); assert.deepEqual( setup.calls.slice(0, 8).map(([name]) => name), [ 'pg-experience', 'log', 'agent-runner', 'audit', 'start-background', 'ensure-admin', 'backfill-user-spaces', 'warn', ], ); assert.equal( result.mindSpaceAgentJobs, setup.agentJobService, ); assert.deepEqual(captured.runnerOptions, { apiTarget: 'http://api-primary', apiSecret: 'api-secret', userAuth: setup.options.userAuth, sessionAccess: setup.options.sessionAccess, agentJobService: setup.agentJobService, experienceService: { id: 'pg-experience' }, }); assert.deepEqual(captured.llmOptions, { apiTarget: 'http://api-primary', apiTargets: [ 'http://api-primary', 'http://api-secondary', ], apiSecret: 'api-secret', }); assert.equal( captured.assetConfigOptions.llmProviderService, setup.llmProviderService, ); assert.equal( captured.imageAdminOptions.llmProviderService, setup.llmProviderService, ); assert.equal( captured.imageAdminOptions.env, setup.options.env, ); }); test('preserves background workspace synchronization callbacks', async () => { const setup = createSetup(); await bootstrapPortalAgentServices(setup.options); const { backgroundOptions } = setup.getCaptured(); assert.equal( backgroundOptions.publicationCleanupIntervalMs, 60 * 1000, ); assert.equal( backgroundOptions.expireStaleUploadsIntervalMs, 5 * 60 * 1000, ); assert.equal( backgroundOptions.agentWorker, setup.options.runtime.agentWorker, ); assert.equal( backgroundOptions.workspaceMaintenanceEnabled, false, ); assert.equal( backgroundOptions.publishRoot, '/publish', ); assert.equal( backgroundOptions.startWorkspaceThumbnailWatcher, setup.options.startWorkspaceThumbnailWatcher, ); assert.equal( backgroundOptions.startWorkspaceAssetSyncWatcher, setup.options.startWorkspaceAssetSyncWatcher, ); await backgroundOptions.syncUserWorkspaceByDirKey( 'known', { force: true }, ); await backgroundOptions.syncUserWorkspaceByDirKey( 'missing', { force: false }, ); assert.deepEqual( setup.calls.filter( ([name]) => name === 'sync-workspace', ), [['sync-workspace', 'user-1', { force: true }]], ); }); test('preserves image service wiring', async () => { const setup = createSetup(); const result = await bootstrapPortalAgentServices( setup.options, ); const captured = setup.getCaptured(); assert.equal( captured.reviewOptions.llmProviderService, setup.llmProviderService, ); assert.equal( captured.reviewOptions.env, setup.options.env, ); assert.equal( captured.generationOptions.configService, result.assetGatewayConfigService, ); assert.equal( captured.generationOptions.assetService, setup.mindSpaceAssets, ); assert.equal( captured.generationOptions.imageMakeClient, result.imageMakeClient, ); assert.equal( captured.generationOptions.imageReviewService, result.imageReviewService, ); assert.equal( result.mindSpaceImageGeneration.id, 'image-generation', ); assert.equal(result.wordFilterService.id, 'word-filter'); }); test('falls back from PG experience and degrades invalid image config', async () => { const setup = createSetup({ createPgExperienceServiceFn: async () => { throw new Error('pg unavailable'); }, createImageMakeClientFromEnvFn: () => { throw new Error('bad image config'); }, }); const result = await bootstrapPortalAgentServices( setup.options, ); assert.equal(result.experienceService.id, 'mysql-experience'); assert.equal(result.imageMakeClient, null); assert.ok( setup.calls.some( ([name, message, detail]) => name === 'error' && message === 'Experience PG init failed, falling back to MySQL store:' && detail === 'pg unavailable', ), ); assert.ok( setup.calls.some( ([name, message, detail]) => name === 'warn' && message === '[image_make] invalid local configuration; integration stays disabled:' && detail === 'bad image config', ), ); }); test('keeps disabled experience null and reports background LLM failures', async () => { const setup = createSetup({ runtime: { experienceEnabled: false, agentWorker: { enabled: false }, publishRoot: '/publish', }, createLlmProviderServiceFn() { return { async ensureBootstrapRelay() { throw new Error('relay failed'); }, async syncSelectedToGoosed() { throw new Error('sync failed'); }, }; }, }); const result = await bootstrapPortalAgentServices( setup.options, ); await Promise.all([ result.relayBootstrapTask, result.providerSyncTask, ]); assert.equal(result.experienceService, null); assert.ok( setup.calls.some( ([name, message, detail]) => name === 'warn' && message === 'LLM relay bootstrap skipped:' && detail === 'relay failed', ), ); assert.ok( setup.calls.some( ([name, message, detail]) => name === 'warn' && message === 'LLM provider boot sync skipped:' && detail === 'sync failed', ), ); }); test('can disable relay bootstrap for backend-LLM-only isolated runs', async () => { const setup = createSetup({ env: { EXPERIENCE_PG_URL: 'postgres://experience', H5_RELAY_BOOTSTRAP_DISABLED: '1', }, }); const result = await bootstrapPortalAgentServices(setup.options); await Promise.all([result.relayBootstrapTask, result.providerSyncTask]); assert.equal(result.relayBootstrapTask instanceof Promise, true); assert.equal(setup.calls.some(([name]) => name === 'relay-bootstrap'), false); assert.equal(setup.calls.some(([name]) => name === 'provider-sync'), true); }); test('passive candidate runtime does not start MindSpace background jobs', async () => { const setup = createSetup({ env: { EXPERIENCE_PG_URL: 'postgres://experience', MEMIND_RUNTIME_ROLE: 'candidate', MEMIND_CANARY_PASSIVE_RUNTIME: '1', }, }); const result = await bootstrapPortalAgentServices(setup.options); await Promise.all([result.relayBootstrapTask, result.providerSyncTask]); assert.equal(setup.calls.some(([name]) => name === 'start-background'), false); assert.ok( setup.calls.some( ([name, message]) => name === 'log' && message === 'Passive candidate runtime: MindSpace background jobs disabled', ), ); });