Files
memind/server/portal-gateway-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

441 lines
13 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
bootstrapPortalGatewayServices,
createPortalRunDeliverablesValidator,
} from './portal-gateway-services-bootstrap.mjs';
function createSetup(overrides = {}) {
const calls = [];
const pool = { id: 'pool' };
const userAuth = { id: 'user-auth' };
const sessionAccess = { id: 'session-access' };
const llmProviderService = { id: 'llm' };
const memoryV2 = {
async observePersonalMemory(options) {
calls.push(['observe-memory', options]);
},
};
const tkmindProxy = { id: 'proxy' };
const toolGateway = { id: 'tool-gateway' };
const agentRunGateway = { id: 'agent-run-gateway' };
let proxyOptions;
let toolOptions;
let agentOptions;
let validatorOptions;
const options = {
pool,
h5Root: '/app',
env: {
MEMIND_AGENT_RUN_AUTODISPATCH: 'yes',
MEMIND_AGENT_RUN_QUEUE_CONCURRENCY: '3',
MEMIND_AGENT_RUN_TIMEOUT_MS: '9000',
},
apiTarget: 'http://primary',
apiTargets: ['http://primary', 'http://secondary'],
apiSecret: 'secret',
userAuth,
sessionAccess,
sessionStreamStore: { id: 'stream-store' },
llmProviderService,
subscriptionService: { id: 'subscription' },
sessionSnapshotService: { id: 'snapshot' },
conversationMemoryService: {
id: 'conversation-memory',
},
memoryV2,
systemDisclosurePolicyService: {
id: 'system-disclosure-policy',
},
mindSpaceAssets: {
async readAsset(userId, assetId) {
calls.push(['read-asset', userId, assetId]);
return {
asset: { mimeType: 'image/png' },
path: '/assets/image.png',
};
},
},
directChatService: { id: 'direct-chat' },
chatIntentRouter: { id: 'intent-router' },
async syncUserGeneratedPages(userId, options) {
calls.push(['sync-pages', userId, options]);
return { synced: true };
},
isSessionPageDeliveryActive(sessionId) {
calls.push(['delivery-active', sessionId]);
return sessionId === 'busy-session';
},
createTkmindProxyFn(receivedOptions) {
calls.push(['proxy']);
proxyOptions = receivedOptions;
return tkmindProxy;
},
createToolGatewayFn(receivedOptions) {
calls.push(['tool-gateway']);
toolOptions = receivedOptions;
return toolGateway;
},
createAgentRunGatewayFn(receivedOptions) {
calls.push(['agent-run-gateway']);
agentOptions = receivedOptions;
return agentRunGateway;
},
createRunDeliverablesValidatorFn(receivedOptions) {
calls.push(['validator']);
validatorOptions = receivedOptions;
return async () => ({ errors: [] });
},
async readAssetFileFn(assetPath) {
calls.push(['read-asset-file', assetPath]);
return Buffer.from('image');
},
...overrides,
};
return {
calls,
options,
pool,
userAuth,
sessionAccess,
llmProviderService,
memoryV2,
tkmindProxy,
toolGateway,
agentRunGateway,
getCaptured() {
return {
proxyOptions,
toolOptions,
agentOptions,
validatorOptions,
};
},
};
}
test('requires the gateway dependencies', () => {
assert.throws(
() => bootstrapPortalGatewayServices(),
/requires gateway dependencies/,
);
});
test('preserves Proxy, Tool, and Agent gateway wiring', () => {
const setup = createSetup();
const result = bootstrapPortalGatewayServices(
setup.options,
);
const captured = setup.getCaptured();
assert.deepEqual(
setup.calls.map(([name]) => name),
['proxy', 'tool-gateway', 'validator', 'agent-run-gateway'],
);
assert.equal(result.tkmindProxy, setup.tkmindProxy);
assert.equal(result.toolGateway, setup.toolGateway);
assert.equal(
result.agentRunGateway,
setup.agentRunGateway,
);
assert.equal(captured.proxyOptions.userAuth, setup.userAuth);
assert.equal(
captured.proxyOptions.sessionAccess,
setup.sessionAccess,
);
assert.equal(
captured.proxyOptions.llmProviderService,
setup.llmProviderService,
);
assert.equal(
captured.proxyOptions.systemDisclosurePolicyService,
setup.options.systemDisclosurePolicyService,
);
assert.deepEqual(captured.toolOptions, {
llmProviderService: setup.llmProviderService,
});
assert.deepEqual(captured.validatorOptions, {
h5Root: '/app',
});
assert.equal(captured.agentOptions.tkmindProxy, setup.tkmindProxy);
assert.equal(captured.agentOptions.toolGateway, setup.toolGateway);
assert.equal(
captured.agentOptions.systemDisclosurePolicyService,
setup.options.systemDisclosurePolicyService,
);
assert.equal(captured.agentOptions.autoDispatch, true);
assert.equal(captured.agentOptions.maxConcurrentRuns, 3);
assert.equal(captured.agentOptions.runTimeoutMs, 9000);
assert.equal(captured.agentOptions.observeWorkflowRun, null);
assert.equal(captured.agentOptions.observeWorkflowValidation, null);
assert.equal(captured.agentOptions.enforcePageDataWorkflowValidation, false);
assert.equal(captured.agentOptions.maxConcurrentShadowObservations, 2);
assert.equal(captured.agentOptions.maxQueuedShadowObservations, 100);
});
test('wires Shadow observation only behind the explicit environment gate', () => {
const configService = { id: 'orchestrator-config' };
const observer = async () => ({ observed: false });
const validationObserver = async () => ({ observed: false });
observer.observeValidation = validationObserver;
let receivedObserverOptions = null;
const setup = createSetup({
env: {
MEMIND_ORCHESTRATOR_SHADOW_OBSERVATION_ENABLED: '1',
MEMIND_ORCHESTRATOR_SERVICE_TOKEN: 'shadow-token',
MEMIND_ORCHESTRATOR_SHADOW_MAX_CONCURRENCY: '4',
MEMIND_ORCHESTRATOR_SHADOW_MAX_QUEUE: '25',
MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1',
},
createOrchestratorAdminConfigServiceFn(receivedPool) {
assert.equal(receivedPool, setup.pool);
return configService;
},
createWorkflowShadowObserverFn(options) {
receivedObserverOptions = options;
return observer;
},
});
bootstrapPortalGatewayServices(setup.options);
const { agentOptions } = setup.getCaptured();
assert.equal(receivedObserverOptions.configService, configService);
assert.equal(receivedObserverOptions.serviceToken, 'shadow-token');
assert.equal(agentOptions.observeWorkflowRun, observer);
assert.equal(agentOptions.observeWorkflowValidation, validationObserver);
assert.equal(agentOptions.enforcePageDataWorkflowValidation, true);
assert.equal(agentOptions.maxConcurrentShadowObservations, 4);
assert.equal(agentOptions.maxQueuedShadowObservations, 25);
});
test('keeps the Page Data validation gate disabled without Shadow wiring', () => {
const setup = createSetup({
env: {
MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1',
},
});
bootstrapPortalGatewayServices(setup.options);
const { agentOptions } = setup.getCaptured();
assert.equal(agentOptions.observeWorkflowValidation, null);
assert.equal(agentOptions.enforcePageDataWorkflowValidation, false);
});
test('preserves local asset reads and optional asset absence', async () => {
const setup = createSetup();
bootstrapPortalGatewayServices(setup.options);
const { proxyOptions } = setup.getCaptured();
assert.deepEqual(
await proxyOptions.localFetchAsset('user-1', 'asset-1'),
{
buffer: Buffer.from('image'),
mimeType: 'image/png',
},
);
assert.deepEqual(
setup.calls.slice(-2),
[
['read-asset', 'user-1', 'asset-1'],
['read-asset-file', '/assets/image.png'],
],
);
const withoutAssets = createSetup({
mindSpaceAssets: null,
});
bootstrapPortalGatewayServices(withoutAssets.options);
assert.equal(
withoutAssets.getCaptured().proxyOptions.localFetchAsset,
null,
);
});
test('preserves memory, page sync, and busy callbacks', async () => {
const setup = createSetup();
bootstrapPortalGatewayServices(setup.options);
const { agentOptions } = setup.getCaptured();
await agentOptions.observePersonalMemoryOnSuccess({
userId: 'user-1',
sessionId: 'session-1',
userMessage: { role: 'user', content: 'remember' },
});
assert.deepEqual(setup.calls.at(-1), [
'observe-memory',
{
userId: 'user-1',
sessionId: 'session-1',
messages: [
{ role: 'user', content: 'remember' },
],
},
]);
assert.deepEqual(
await agentOptions.syncUserPagesOnSuccess({
userId: 'user-1',
sessionId: 'session-1',
runStartedAtMs: 123,
}),
{ synced: true },
);
assert.equal(
agentOptions.isSessionExternallyBusy({
sessionId: 'busy-session',
}),
true,
);
});
test('passive candidate runtime disables the singleton Agent recovery loop', () => {
let recoveryStarts = 0;
const setup = createSetup({
env: {
MEMIND_RUNTIME_ROLE: 'candidate',
MEMIND_CANARY_PASSIVE_RUNTIME: '1',
},
startAgentRunRecoveryLoopFn() {
recoveryStarts += 1;
return { id: 'recovery' };
},
});
const result = bootstrapPortalGatewayServices(setup.options);
assert.equal(recoveryStarts, 0);
assert.equal(result.agentRunRecoveryTimer, null);
});
test('keeps memory observation optional and parses disabled dispatch', async () => {
const setup = createSetup({
env: {
MEMIND_AGENT_RUN_AUTODISPATCH: 'off',
},
memoryV2: {},
});
bootstrapPortalGatewayServices(setup.options);
const { agentOptions } = setup.getCaptured();
await agentOptions.observePersonalMemoryOnSuccess({
userId: 'user-1',
sessionId: 'session-1',
userMessage: 'hello',
});
assert.equal(agentOptions.autoDispatch, false);
assert.equal(agentOptions.maxConcurrentRuns, 1);
assert.equal(agentOptions.runTimeoutMs, 15 * 60 * 1000);
});
test('validates Page Data issues, policy grants, and browser storage', async () => {
const policyChecks = [];
const validator = createPortalRunDeliverablesValidator({
h5Root: '/app',
resolveMindSpaceUserPublishDirFn: () =>
'/publish/user-1',
normalizeWorkspaceRelativePathFn: (value) => value,
resolvePathFn: (...parts) => parts.join('/').replaceAll('//', '/'),
pathSeparator: '/',
existsSyncFn: () => true,
readFileSyncFn: () => '<html>page data</html>',
evaluatePageDataHtmlContentFn: () => ({
usesPageDataApi: true,
issues: ['dataset_binding_missing'],
}),
readPageAccessPolicyFn: () => ({ id: 'policy' }),
detectPageDataDatasetUsageFromHtmlFn: () => [
[
'orders',
{ read: true, insert: true },
],
],
policyAllowsActionFn(policy, dataset, action) {
policyChecks.push([policy, dataset, action]);
return action === 'read';
},
scanWorkspaceFilesForProhibitedBrowserStorageFn:
(options) => {
assert.deepEqual(options, {
publishDir: '/publish/user-1',
relativePaths: ['public/orders.html'],
});
return [
{
relativePath: 'public/orders.html',
apis: ['localStorage', 'indexedDB'],
},
];
},
});
const result = await validator({
userId: 'user-1',
deliverables: {
pages: [
{
pageId: 'page-1',
workspaceRelativePath: 'public/orders.html',
},
],
},
});
assert.deepEqual(policyChecks, [
[{ id: 'policy' }, 'orders', 'read'],
[{ id: 'policy' }, 'orders', 'insert'],
]);
assert.deepEqual(
result.errors.map(({ code }) => code),
[
'dataset_binding_missing',
'page_data_policy_action_missing',
'browser_storage_forbidden',
],
);
});
test('skips non-public, missing, and path-escaping Page Data files', async () => {
const readPaths = [];
const validator = createPortalRunDeliverablesValidator({
h5Root: '/app',
resolveMindSpaceUserPublishDirFn: () =>
'/publish/user-1',
normalizeWorkspaceRelativePathFn: (value) => value,
resolvePathFn: (...parts) => {
const joined = parts.join('/');
if (joined.includes('escape')) return '/outside/file.html';
return joined.replaceAll('//', '/');
},
pathSeparator: '/',
existsSyncFn: (filePath) =>
!filePath.includes('missing'),
readFileSyncFn(filePath) {
readPaths.push(filePath);
return '<html></html>';
},
evaluatePageDataHtmlContentFn: () => ({
usesPageDataApi: false,
issues: [],
}),
scanWorkspaceFilesForProhibitedBrowserStorageFn:
() => [],
});
const result = await validator({
userId: 'user-1',
deliverables: {
pages: [
{ workspaceRelativePath: 'private/a.html' },
{ workspaceRelativePath: 'public/missing.html' },
{ workspaceRelativePath: 'public/escape.html' },
{ workspaceRelativePath: 'public/valid.html' },
],
},
});
assert.deepEqual(readPaths, [
'/publish/user-1/public/valid.html',
]);
assert.deepEqual(result, { errors: [] });
});