531 lines
13 KiB
JavaScript
531 lines
13 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { bootstrapPortalIntegrationServices } from './portal-integration-services-bootstrap.mjs';
|
|
|
|
function createSetup(overrides = {}) {
|
|
const calls = [];
|
|
const pool = { id: 'pool' };
|
|
const userAuth = {
|
|
async getUserById(userId) {
|
|
calls.push(['get-user', userId]);
|
|
return { id: userId, username: 'John' };
|
|
},
|
|
setRechargeNotifier(notifier) {
|
|
calls.push(['set-recharge-notifier']);
|
|
this.rechargeNotifier = notifier;
|
|
},
|
|
};
|
|
const sessionAccess = { id: 'session-access' };
|
|
const tkmindProxy = {
|
|
apiFetch: { id: 'api-fetch' },
|
|
startSessionForUser(userId, options) {
|
|
calls.push(['start-session', userId, options]);
|
|
return { sessionId: 'session-1' };
|
|
},
|
|
async resolveTarget(sessionId) {
|
|
calls.push(['resolve-target', sessionId]);
|
|
return 'http://target';
|
|
},
|
|
apiFetchTo(target, pathname, init) {
|
|
calls.push([
|
|
'api-fetch-to',
|
|
target,
|
|
pathname,
|
|
init,
|
|
]);
|
|
return { ok: true };
|
|
},
|
|
submitSessionReplyForUser(...args) {
|
|
calls.push(['submit-reply', ...args]);
|
|
return { accepted: true };
|
|
},
|
|
applySessionLlmProvider(sessionId) {
|
|
calls.push(['apply-provider', sessionId]);
|
|
return { applied: true };
|
|
},
|
|
};
|
|
const wechatMpService = {
|
|
enabled: true,
|
|
sendTextToUser(userId, text) {
|
|
calls.push(['send-wechat-text', userId, text]);
|
|
return { sent: true };
|
|
},
|
|
};
|
|
const notificationDispatcher = {
|
|
async sendRechargeSuccess(options) {
|
|
calls.push(['recharge-success', options]);
|
|
},
|
|
};
|
|
const scheduleService = { id: 'schedule' };
|
|
const sessionSnapshotService = {
|
|
isEnabled() {
|
|
return true;
|
|
},
|
|
refresh(...args) {
|
|
calls.push(['snapshot-refresh', ...args]);
|
|
return { refreshed: true };
|
|
},
|
|
};
|
|
let wechatOptions;
|
|
let notificationOptions;
|
|
let reminderOptions;
|
|
let pageEditOptions;
|
|
let timerCallback;
|
|
let timerDelay;
|
|
let unrefCount = 0;
|
|
const subscriptionService = {
|
|
async processAutoRenewals() {
|
|
calls.push(['auto-renew']);
|
|
return { renewed: 2, failed: 1 };
|
|
},
|
|
async expireStaleSubscriptions() {
|
|
calls.push(['expire-subscriptions']);
|
|
return 3;
|
|
},
|
|
};
|
|
const options = {
|
|
pool,
|
|
h5Root: '/app',
|
|
env: {
|
|
H5_SCHEDULE_ENABLED: '1',
|
|
H5_REMINDER_WORKER_ENABLED: '1',
|
|
},
|
|
usersRoot: '/users',
|
|
wechatMpConfig: { enabled: true },
|
|
authPool: pool,
|
|
userAuth,
|
|
sessionAccess,
|
|
tkmindProxy,
|
|
scheduleService,
|
|
wechatScheduleLlmConfigService: {
|
|
id: 'schedule-llm',
|
|
},
|
|
llmProviderService: { id: 'llm' },
|
|
chatIntentRouter: {
|
|
classifySessionAction(options) {
|
|
calls.push(['classify-session', options]);
|
|
return { action: 'chat' };
|
|
},
|
|
},
|
|
systemDisclosurePolicyService: {
|
|
id: 'system-disclosure-policy',
|
|
},
|
|
sessionSnapshotService,
|
|
mindSpaceAnalyticsConfig: { enabled: true },
|
|
mindSpaceRybbitConfig: { enabled: true },
|
|
subscriptionService,
|
|
apiTarget: 'http://api',
|
|
apiSecret: 'secret',
|
|
mindSpacePages: { id: 'pages' },
|
|
mindSpacePageLiveEdit: { id: 'live-edit' },
|
|
logger: {
|
|
log(...args) {
|
|
calls.push(['log', ...args]);
|
|
},
|
|
warn(...args) {
|
|
calls.push(['warn', ...args]);
|
|
},
|
|
},
|
|
async loadWechatMpModuleFn(h5Root) {
|
|
calls.push(['load-wechat', h5Root]);
|
|
return {
|
|
createWechatMpService(receivedOptions) {
|
|
calls.push(['create-wechat']);
|
|
wechatOptions = receivedOptions;
|
|
return wechatMpService;
|
|
},
|
|
};
|
|
},
|
|
resolveMindSpaceRuntimeConfigFn(h5Root, env) {
|
|
calls.push(['runtime-config', h5Root, env]);
|
|
return { storageRoot: '/storage' };
|
|
},
|
|
resolveAnalyticsOwnerSegmentFn(user) {
|
|
calls.push(['owner-segment', user]);
|
|
return 'segment';
|
|
},
|
|
resolveAnalyticsOwnerLabelFn(user) {
|
|
calls.push(['owner-label', user]);
|
|
return 'label';
|
|
},
|
|
resolveAnalyticsPlanFn(user) {
|
|
calls.push(['plan-type', user]);
|
|
return 'pro';
|
|
},
|
|
sendMindSpaceAnalyticsEventFn(event) {
|
|
calls.push(['analytics', event]);
|
|
return Promise.resolve();
|
|
},
|
|
sendMindSpaceRybbitEventFn(event) {
|
|
calls.push(['rybbit', event]);
|
|
return Promise.resolve();
|
|
},
|
|
createNotificationDispatcherFn(receivedOptions) {
|
|
calls.push(['notification-dispatcher']);
|
|
notificationOptions = receivedOptions;
|
|
return notificationDispatcher;
|
|
},
|
|
startScheduleReminderWorkerFn(receivedOptions) {
|
|
calls.push(['reminder-worker']);
|
|
reminderOptions = receivedOptions;
|
|
return { id: 'reminder-worker' };
|
|
},
|
|
createPageEditSessionServiceFn(receivedOptions) {
|
|
calls.push(['page-edit']);
|
|
pageEditOptions = receivedOptions;
|
|
return { id: 'page-edit' };
|
|
},
|
|
setIntervalFn(callback, delay) {
|
|
calls.push(['set-interval']);
|
|
timerCallback = callback;
|
|
timerDelay = delay;
|
|
return {
|
|
unref() {
|
|
unrefCount += 1;
|
|
calls.push(['timer-unref']);
|
|
},
|
|
};
|
|
},
|
|
...overrides,
|
|
};
|
|
|
|
return {
|
|
calls,
|
|
options,
|
|
pool,
|
|
userAuth,
|
|
sessionAccess,
|
|
tkmindProxy,
|
|
wechatMpService,
|
|
notificationDispatcher,
|
|
scheduleService,
|
|
sessionSnapshotService,
|
|
subscriptionService,
|
|
getCaptured() {
|
|
return {
|
|
wechatOptions,
|
|
notificationOptions,
|
|
reminderOptions,
|
|
pageEditOptions,
|
|
timerCallback,
|
|
timerDelay,
|
|
unrefCount,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
test('requires Portal integration dependencies', async () => {
|
|
await assert.rejects(
|
|
bootstrapPortalIntegrationServices(),
|
|
/requires Portal integration dependencies/,
|
|
);
|
|
});
|
|
|
|
test('preserves WeChat service configuration and proxy callbacks', async () => {
|
|
const setup = createSetup();
|
|
await bootstrapPortalIntegrationServices(
|
|
setup.options,
|
|
);
|
|
const { wechatOptions } = setup.getCaptured();
|
|
|
|
assert.deepEqual(wechatOptions.pageDataFinishGuard, {
|
|
pool: setup.pool,
|
|
h5Root: '/app',
|
|
storageRoot: '/storage',
|
|
});
|
|
assert.equal(
|
|
wechatOptions.apiFetch,
|
|
setup.tkmindProxy.apiFetch,
|
|
);
|
|
assert.equal(
|
|
wechatOptions.scheduleService,
|
|
setup.scheduleService,
|
|
);
|
|
assert.equal(
|
|
wechatOptions.systemDisclosurePolicyService,
|
|
setup.options.systemDisclosurePolicyService,
|
|
);
|
|
assert.deepEqual(
|
|
wechatOptions.startAgentSession({
|
|
userId: 'user-1',
|
|
workingDir: '/workspace',
|
|
sessionPolicy: { sandboxed: true },
|
|
}),
|
|
{ sessionId: 'session-1' },
|
|
);
|
|
assert.deepEqual(
|
|
await wechatOptions.sessionApiFetch(
|
|
'session-1',
|
|
'/status',
|
|
{ method: 'GET' },
|
|
),
|
|
{ ok: true },
|
|
);
|
|
assert.deepEqual(
|
|
wechatOptions.submitSessionReply({
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
requestId: 'request-1',
|
|
userMessage: 'hello',
|
|
options: { deep: true },
|
|
}),
|
|
{ accepted: true },
|
|
);
|
|
assert.deepEqual(
|
|
wechatOptions.sessionIntentClassifier({
|
|
text: 'hello',
|
|
}),
|
|
{ action: 'chat' },
|
|
);
|
|
assert.deepEqual(
|
|
wechatOptions.applySessionLlmProvider('session-1'),
|
|
{ applied: true },
|
|
);
|
|
});
|
|
|
|
test('preserves snapshot refresh through the resolved session target', async () => {
|
|
const setup = createSetup();
|
|
await bootstrapPortalIntegrationServices(
|
|
setup.options,
|
|
);
|
|
const { wechatOptions } = setup.getCaptured();
|
|
|
|
assert.equal(
|
|
typeof wechatOptions.refreshSessionSnapshot,
|
|
'function',
|
|
);
|
|
assert.deepEqual(
|
|
wechatOptions.refreshSessionSnapshot(
|
|
'session-1',
|
|
'user-1',
|
|
),
|
|
{ refreshed: true },
|
|
);
|
|
const refreshCall = setup.calls.find(
|
|
([name]) => name === 'snapshot-refresh',
|
|
);
|
|
const fetchSnapshot = refreshCall[3];
|
|
assert.deepEqual(
|
|
await fetchSnapshot('/snapshot', {
|
|
method: 'GET',
|
|
}),
|
|
{ ok: true },
|
|
);
|
|
assert.ok(
|
|
setup.calls.some(
|
|
([name, sessionId]) =>
|
|
name === 'resolve-target' &&
|
|
sessionId === 'session-1',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('preserves generated-page analytics projection', async () => {
|
|
const setup = createSetup();
|
|
await bootstrapPortalIntegrationServices(
|
|
setup.options,
|
|
);
|
|
const { wechatOptions } = setup.getCaptured();
|
|
|
|
await wechatOptions.onPageGenerated({
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
artifacts: [
|
|
{
|
|
relativePath: 'public/page.html',
|
|
url: 'https://example/page',
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(
|
|
setup.calls.filter(([name]) => name === 'get-user')
|
|
.length,
|
|
1,
|
|
);
|
|
const analyticsCall = setup.calls.find(
|
|
([name]) => name === 'analytics',
|
|
);
|
|
assert.deepEqual(analyticsCall[1], {
|
|
config: { enabled: true },
|
|
eventName: 'page_generated',
|
|
ownerId: 'user-1',
|
|
ownerSegment: 'segment',
|
|
ownerLabel: 'label',
|
|
planType: 'pro',
|
|
generatedAt: analyticsCall[1].generatedAt,
|
|
pageId: 'public/page.html',
|
|
publicationId: 'session-1',
|
|
agentRunId: 'session-1',
|
|
channel: 'wechat_mp',
|
|
url: 'https://example/page',
|
|
});
|
|
assert.match(
|
|
analyticsCall[1].generatedAt,
|
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
);
|
|
const rybbitCall = setup.calls.find(
|
|
([name]) => name === 'rybbit',
|
|
);
|
|
assert.deepEqual(rybbitCall[1], {
|
|
...analyticsCall[1],
|
|
config: { enabled: true },
|
|
});
|
|
});
|
|
|
|
test('preserves notification, recharge, reminder, and page-edit wiring', async () => {
|
|
const setup = createSetup();
|
|
const result =
|
|
await bootstrapPortalIntegrationServices(
|
|
setup.options,
|
|
);
|
|
const captured = setup.getCaptured();
|
|
|
|
assert.equal(
|
|
captured.notificationOptions.sendWechatTextToUser(
|
|
'user-1',
|
|
'hello',
|
|
).sent,
|
|
true,
|
|
);
|
|
await setup.userAuth.rechargeNotifier({
|
|
userId: 'user-1',
|
|
title: 'Recharge',
|
|
body: 'Success',
|
|
dedupeKey: 'dedupe-1',
|
|
});
|
|
assert.deepEqual(captured.reminderOptions, {
|
|
scheduleService: setup.scheduleService,
|
|
notificationDispatcher:
|
|
setup.notificationDispatcher,
|
|
});
|
|
assert.equal(result.scheduleReminderWorker.id, 'reminder-worker');
|
|
assert.deepEqual(captured.pageEditOptions, {
|
|
apiTarget: 'http://api',
|
|
apiSecret: 'secret',
|
|
userAuth: setup.userAuth,
|
|
sessionAccess: setup.sessionAccess,
|
|
pageService: setup.options.mindSpacePages,
|
|
pageLiveEdit: setup.options.mindSpacePageLiveEdit,
|
|
llmProviderService:
|
|
setup.options.llmProviderService,
|
|
});
|
|
});
|
|
|
|
test('preserves subscription timer work, logs, and unref', async () => {
|
|
const setup = createSetup();
|
|
await bootstrapPortalIntegrationServices(
|
|
setup.options,
|
|
);
|
|
const captured = setup.getCaptured();
|
|
|
|
assert.equal(captured.timerDelay, 60 * 60 * 1000);
|
|
assert.equal(captured.unrefCount, 1);
|
|
await captured.timerCallback();
|
|
assert.ok(
|
|
setup.calls.some(
|
|
([name, message]) =>
|
|
name === 'log' &&
|
|
message === 'Auto-renewed 2 subscription(s)',
|
|
),
|
|
);
|
|
assert.ok(
|
|
setup.calls.some(
|
|
([name, message]) =>
|
|
name === 'log' &&
|
|
message ===
|
|
'Auto-renew failed for 1 subscription(s) (balance insufficient)',
|
|
),
|
|
);
|
|
assert.ok(
|
|
setup.calls.some(
|
|
([name, message]) =>
|
|
name === 'log' &&
|
|
message === 'Expired 3 stale subscription(s)',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('keeps optional integrations disabled and contains timer failures', async () => {
|
|
const setup = createSetup({
|
|
env: {},
|
|
authPool: null,
|
|
scheduleService: null,
|
|
subscriptionService: {
|
|
async processAutoRenewals() {
|
|
throw new Error('renew failed');
|
|
},
|
|
async expireStaleSubscriptions() {
|
|
return 0;
|
|
},
|
|
},
|
|
sessionSnapshotService: {
|
|
isEnabled: () => false,
|
|
},
|
|
async loadWechatMpModuleFn() {
|
|
return {
|
|
createWechatMpService(options) {
|
|
setup.disabledWechatOptions = options;
|
|
return { enabled: false };
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
const result =
|
|
await bootstrapPortalIntegrationServices(
|
|
setup.options,
|
|
);
|
|
const captured = setup.getCaptured();
|
|
await captured.timerCallback();
|
|
|
|
assert.equal(
|
|
setup.disabledWechatOptions.pageDataFinishGuard,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
setup.disabledWechatOptions.scheduleService,
|
|
null,
|
|
);
|
|
assert.equal(
|
|
setup.disabledWechatOptions.refreshSessionSnapshot,
|
|
null,
|
|
);
|
|
assert.equal(result.scheduleReminderWorker, null);
|
|
assert.equal(
|
|
captured.notificationOptions.sendWechatTextToUser,
|
|
null,
|
|
);
|
|
assert.ok(
|
|
setup.calls.some(
|
|
([name, message, error]) =>
|
|
name === 'warn' &&
|
|
message ===
|
|
'Subscription expiry check failed:' &&
|
|
error?.message === 'renew failed',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('passive candidate runtime disables singleton reminder and subscription timers', async () => {
|
|
const setup = createSetup({
|
|
env: {
|
|
H5_SCHEDULE_ENABLED: '1',
|
|
H5_REMINDER_WORKER_ENABLED: '1',
|
|
MEMIND_RUNTIME_ROLE: 'candidate',
|
|
MEMIND_CANARY_PASSIVE_RUNTIME: '1',
|
|
},
|
|
});
|
|
|
|
const result = await bootstrapPortalIntegrationServices(setup.options);
|
|
const captured = setup.getCaptured();
|
|
|
|
assert.equal(result.scheduleReminderWorker, null);
|
|
assert.equal(result.subscriptionExpiryTimer, null);
|
|
assert.equal(captured.reminderOptions, undefined);
|
|
assert.equal(captured.timerCallback, undefined);
|
|
assert.equal(setup.calls.some(([name]) => name === 'reminder-worker'), false);
|
|
assert.equal(setup.calls.some(([name]) => name === 'set-interval'), false);
|
|
});
|