449 lines
12 KiB
JavaScript
449 lines
12 KiB
JavaScript
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',
|
|
),
|
|
);
|
|
});
|