Files
memind/server/portal-agent-services-bootstrap.test.mjs
john 286069449b
Memind CI / Test, build, and release guards (push) Failing after 2m14s
feat: add guarded portal canary release
2026-07-26 19:51:44 +08:00

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',
),
);
});