644f7fc632
Memind CI / Test, build, and release guards (push) Has been cancelled
Ship the WeChat Cursor executor helpers referenced by the page delivery path so tests and runtime imports resolve consistently. Co-authored-by: Cursor <cursoragent@cursor.com>
342 lines
13 KiB
JavaScript
342 lines
13 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { createWechatMpService as createProductionWechatMpService } from './wechat-mp.mjs';
|
|
import { prepareWechatHtmlDeliveryAtWorkspace } from './mindspace-wechat-html-delivery.mjs';
|
|
import {
|
|
isWechatCursorChannelReply,
|
|
prepareWechatCursorPageDelivery,
|
|
} from './wechat-cursor-page-delivery.mjs';
|
|
import { PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
|
|
|
function sha1(parts) {
|
|
return crypto.createHash('sha1').update([...parts].sort().join('')).digest('hex');
|
|
}
|
|
|
|
function signatureFor(token, timestamp, nonce) {
|
|
return sha1([token, timestamp, nonce]);
|
|
}
|
|
|
|
function inboundXml({ fromUser = 'openid-test-1', content = '帮我做个测试页面' } = {}) {
|
|
return [
|
|
'<xml>',
|
|
'<ToUserName><![CDATA[gh_test]]></ToUserName>',
|
|
`<FromUserName><![CDATA[${fromUser}]]></FromUserName>`,
|
|
'<CreateTime>1710000000</CreateTime>',
|
|
'<MsgType><![CDATA[text]]></MsgType>',
|
|
`<Content><![CDATA[${content}]]></Content>`,
|
|
'<MsgId>msg-cursor-test-1</MsgId>',
|
|
'</xml>',
|
|
].join('');
|
|
}
|
|
|
|
function createWechatMpService(options) {
|
|
const resolvePublishDir = async (userId) =>
|
|
options.userAuth?.resolveWorkingDir?.(userId) ?? `/tmp/${userId}`;
|
|
const buildCanonicalUrl = (userId, relativePath) => {
|
|
const base = String(options.config?.publicBaseUrl ?? 'https://example.com').replace(/\/+$/, '');
|
|
const normalized = String(relativePath).replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/');
|
|
return `${base}/MindSpace/${encodeURIComponent(userId)}/${normalized}`;
|
|
};
|
|
const htmlDeliveryAuthority = options.htmlDeliveryAuthority ?? {
|
|
async prepareWechatHtmlDelivery(input) {
|
|
const publishDir = await resolvePublishDir(input.userId);
|
|
return prepareWechatHtmlDeliveryAtWorkspace({
|
|
reply: input.reply,
|
|
intent: input.intent,
|
|
publishDir,
|
|
requestStartedAt: input.requestStartedAt,
|
|
allowRecentArtifacts: input.allowRecentArtifacts,
|
|
buildCanonicalUrl: (relativePath) => buildCanonicalUrl(input.userId, relativePath),
|
|
});
|
|
},
|
|
async ensureWechatFreshPageThumbnails() {
|
|
return { artifacts: [], images: [] };
|
|
},
|
|
};
|
|
return createProductionWechatMpService({
|
|
...options,
|
|
htmlDeliveryAuthority,
|
|
});
|
|
}
|
|
|
|
function createBoundWechatService(overrides = {}) {
|
|
const apiCalls = [];
|
|
const wechatCalls = [];
|
|
const cursorCalls = [];
|
|
const token = overrides.token ?? 'test-token';
|
|
const userId = overrides.userId ?? 'user-cursor-test';
|
|
|
|
const agentRunGateway = overrides.agentRunGateway ?? {
|
|
async createRun(runUserId, payload) {
|
|
cursorCalls.push(['createRun', runUserId, payload]);
|
|
const touchNow = Date.now();
|
|
fs.utimesSync(htmlPath, touchNow / 1000, touchNow / 1000);
|
|
return { id: 'run-cursor-1', status: 'queued' };
|
|
},
|
|
dispatchRun(runId) {
|
|
cursorCalls.push(['dispatchRun', runId]);
|
|
},
|
|
async getRunForUser(runUserId, runId) {
|
|
cursorCalls.push(['getRunForUser', runUserId, runId]);
|
|
return { id: runId, status: 'succeeded', error: null };
|
|
},
|
|
async listRunEventsForUser(runUserId, runId) {
|
|
cursorCalls.push(['listRunEventsForUser', runUserId, runId]);
|
|
return {
|
|
events: [{
|
|
id: 'evt-1',
|
|
eventType: 'tool_gateway_result',
|
|
data: {
|
|
executor: 'cursor',
|
|
stdoutTail: '已生成 public/cursor-wechat-test-page.html',
|
|
},
|
|
createdAt: Date.now(),
|
|
}],
|
|
};
|
|
},
|
|
};
|
|
|
|
const wechatCursorExecutorPolicyService = overrides.wechatCursorExecutorPolicyService ?? {
|
|
async getEffectivePolicy() {
|
|
return {
|
|
enabled: false,
|
|
userAllowed: false,
|
|
userAllowlist: [],
|
|
intentAllowlist: ['page.generate'],
|
|
fallbackToDeepseek: true,
|
|
};
|
|
},
|
|
};
|
|
|
|
const h5Root = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-cursor-h5-'));
|
|
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, userId);
|
|
fs.mkdirSync(path.join(publishDir, 'public'), { recursive: true });
|
|
const htmlPath = path.join(publishDir, 'public', 'cursor-wechat-test-page.html');
|
|
fs.writeFileSync(htmlPath, '<html><head><title>Cursor Page</title></head><body><main>hi</main></body></html>');
|
|
const now = Date.now();
|
|
fs.utimesSync(htmlPath, now / 1000, now / 1000);
|
|
|
|
const service = createWechatMpService({
|
|
h5Root,
|
|
config: {
|
|
enabled: true,
|
|
appId: 'wx_local_test_only',
|
|
appSecret: 'local-secret',
|
|
token,
|
|
publicBaseUrl: 'https://example.com',
|
|
mediaPublicBaseUrl: 'https://example.com',
|
|
bindPath: '/auth/wechat/authorize?intent=login',
|
|
ackText: 'ack',
|
|
unsupportedText: 'unsupported',
|
|
unboundTextPrefix: '请先绑定',
|
|
progressDelayMs: 0,
|
|
requireFreshPageThumbnail: false,
|
|
...(overrides.config ?? {}),
|
|
},
|
|
userAuth: {
|
|
async findWechatUserByOpenid(_appId, openid) {
|
|
return {
|
|
userId,
|
|
status: 'active',
|
|
nickname: '测试用户',
|
|
username: 'test-user',
|
|
openid,
|
|
};
|
|
},
|
|
async getWechatAgentRoute() {
|
|
return { agentSessionId: 'session-cursor-1', isNewSession: false };
|
|
},
|
|
async clearWechatAgentRoute() {},
|
|
async canUseChat() {
|
|
return { ok: true };
|
|
},
|
|
async resolveWorkingDir() {
|
|
return publishDir;
|
|
},
|
|
async getAgentSessionPolicy() {
|
|
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
|
},
|
|
async getUserPublishLayout() {
|
|
return { displayName: '测试用户', username: 'test-user', slug: 'test-user', constraints: null };
|
|
},
|
|
async registerAgentSession() {},
|
|
async upsertWechatAgentRoute() {},
|
|
async billSessionUsage() {},
|
|
async recordWechatMpMessage() {
|
|
return { inserted: true };
|
|
},
|
|
async finishWechatMpMessage() {},
|
|
async insertWechatMpMessageDetail() {},
|
|
...(overrides.userAuth ?? {}),
|
|
},
|
|
agentRunGateway,
|
|
wechatCursorExecutorPolicyService,
|
|
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
|
apiCalls.push([pathname, init.method ?? 'GET']);
|
|
if (pathname.endsWith('/events')) {
|
|
return new Response(
|
|
[
|
|
'data: {"type":"Message","request_id":"req-fallback-1","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"DeepSeek 回退页面已完成\\nhttps://example.com/MindSpace/user-cursor-test/public/fallback-page.html"}]}}\n\n',
|
|
'data: {"type":"Finish","request_id":"req-fallback-1","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
|
].join(''),
|
|
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
|
);
|
|
}
|
|
if (pathname.endsWith('/reply')) {
|
|
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
if (pathname.includes('/agent/harness_remember') || pathname.includes('/agent/session/reconcile') || pathname.includes('/agent/harness_bootstrap')) {
|
|
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
throw new Error(`unexpected api path: ${pathname} session=${sessionId}`);
|
|
},
|
|
wechatFetch: async (url, init = {}) => {
|
|
wechatCalls.push([String(url), init.method ?? 'GET', init]);
|
|
if (String(url).includes('/cgi-bin/stable_token')) {
|
|
return new Response(JSON.stringify({ access_token: 'local-access-token', expires_in: 7200 }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
|
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`unexpected wechat url: ${url}`);
|
|
},
|
|
linkExists: async (urlText) => String(urlText).includes('cursor-wechat-test-page.html')
|
|
|| String(urlText).includes('fallback-page.html'),
|
|
...(overrides.extra ?? {}),
|
|
});
|
|
|
|
return {
|
|
service,
|
|
apiCalls,
|
|
wechatCalls,
|
|
cursorCalls,
|
|
publishDir,
|
|
token,
|
|
};
|
|
}
|
|
|
|
test('non-allowlisted wechat user keeps deepseek session reply path', async () => {
|
|
const { service, apiCalls, cursorCalls, token } = createBoundWechatService();
|
|
const timestamp = '1710000000';
|
|
const nonce = 'nonce-no-cursor';
|
|
const result = await service.handleInboundMessage(
|
|
inboundXml({ content: '帮我做一个简单测试页面' }),
|
|
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
|
);
|
|
assert.equal(result.status, 200);
|
|
await result.task;
|
|
assert.equal(cursorCalls.some(([kind]) => kind === 'createRun'), false);
|
|
assert.equal(apiCalls.some(([pathname]) => String(pathname).endsWith('/reply')), true);
|
|
});
|
|
|
|
test('allowlisted wechat user uses cursor agent-run path without session reply', async () => {
|
|
const { service, apiCalls, cursorCalls, wechatCalls, token } = createBoundWechatService({
|
|
wechatCursorExecutorPolicyService: {
|
|
async getEffectivePolicy(_userId, user) {
|
|
return {
|
|
enabled: true,
|
|
userAllowed: true,
|
|
userAllowlist: [user?.userId ?? 'user-cursor-test'],
|
|
intentAllowlist: ['page.generate'],
|
|
fallbackToDeepseek: true,
|
|
};
|
|
},
|
|
},
|
|
});
|
|
const timestamp = '1710000001';
|
|
const nonce = 'nonce-cursor';
|
|
const result = await service.handleInboundMessage(
|
|
inboundXml({ content: '帮我做一个简单测试页面' }),
|
|
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
|
);
|
|
assert.equal(result.status, 200);
|
|
await result.task;
|
|
assert.equal(cursorCalls.some(([kind]) => kind === 'createRun'), true);
|
|
assert.equal(apiCalls.some(([pathname]) => String(pathname).endsWith('/reply')), false);
|
|
const customSend = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
|
assert.ok(customSend, 'expected wechat custom send');
|
|
const sendBody = JSON.parse(String(customSend[2]?.body ?? '{}'));
|
|
const outboundText = String(sendBody?.text?.content ?? '');
|
|
assert.doesNotMatch(outboundText, /没有按服务号页面技能真正生成成功/);
|
|
assert.match(outboundText, /cursor-wechat-test-page\.html/);
|
|
});
|
|
|
|
test('allowlisted user falls back to deepseek when cursor run fails', async () => {
|
|
const { service, apiCalls, cursorCalls, token } = createBoundWechatService({
|
|
agentRunGateway: {
|
|
async createRun(runUserId, payload) {
|
|
cursorCalls.push(['createRun', runUserId, payload]);
|
|
return { id: 'run-fail-1', status: 'queued' };
|
|
},
|
|
dispatchRun(runId) {
|
|
cursorCalls.push(['dispatchRun', runId]);
|
|
},
|
|
async getRunForUser(runUserId, runId) {
|
|
cursorCalls.push(['getRunForUser', runUserId, runId]);
|
|
return { id: runId, status: 'failed', error: 'simulated cursor failure' };
|
|
},
|
|
async listRunEventsForUser() {
|
|
return { events: [] };
|
|
},
|
|
},
|
|
wechatCursorExecutorPolicyService: {
|
|
async getEffectivePolicy() {
|
|
return {
|
|
enabled: true,
|
|
userAllowed: true,
|
|
userAllowlist: ['user-cursor-test'],
|
|
intentAllowlist: ['page.generate'],
|
|
fallbackToDeepseek: true,
|
|
};
|
|
},
|
|
},
|
|
});
|
|
const timestamp = '1710000002';
|
|
const nonce = 'nonce-fallback';
|
|
const result = await service.handleInboundMessage(
|
|
inboundXml({ content: '帮我做一个简单测试页面' }),
|
|
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
|
);
|
|
assert.equal(result.status, 200);
|
|
await result.task;
|
|
assert.equal(cursorCalls.some(([kind]) => kind === 'createRun'), true);
|
|
assert.equal(apiCalls.some(([pathname]) => String(pathname).endsWith('/reply')), true);
|
|
});
|
|
|
|
test('production-like openid in script default is not used when policy disabled', async () => {
|
|
const prodLikeOpenid = 'ooil-0VFj68QK1tkHl39uL610et8';
|
|
const { service, cursorCalls, token } = createBoundWechatService({
|
|
userId: '1c99b83b-0454-474f-a5d2-129d34506a32',
|
|
userAuth: {
|
|
async findWechatUserByOpenid(_appId, openid) {
|
|
assert.equal(openid, prodLikeOpenid);
|
|
return {
|
|
userId: '1c99b83b-0454-474f-a5d2-129d34506a32',
|
|
status: 'active',
|
|
nickname: 'John',
|
|
username: 'john',
|
|
};
|
|
},
|
|
},
|
|
});
|
|
const timestamp = '1710000003';
|
|
const nonce = 'nonce-prod-openid';
|
|
const result = await service.handleInboundMessage(
|
|
inboundXml({ fromUser: prodLikeOpenid, content: '你好' }),
|
|
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
|
|
);
|
|
assert.equal(result.status, 200);
|
|
await result.task;
|
|
assert.equal(cursorCalls.length, 0);
|
|
});
|