221 lines
8.9 KiB
JavaScript
221 lines
8.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { classifyWechatIntent, isPageGenerateIntent } from './intent/classifier.mjs';
|
|
import { resolveWechatSessionAction } from './intent/session-action.mjs';
|
|
import {
|
|
filterSendableHtmlArtifacts,
|
|
isStubPublicHtmlContent,
|
|
selectSendableHtmlArtifacts,
|
|
verifyPageArtifactContent,
|
|
} from './verify/page-artifact.mjs';
|
|
import { guardScheduleConfirmationReply, looksLikeScheduleConfirmation } from './handlers/schedule-guard.mjs';
|
|
import { resolvePageGenerateOutcome } from './handlers/page-generate.mjs';
|
|
import { buildGreetingText, resolveSyncReply } from './handlers/sync-replies.mjs';
|
|
import { buildWechatAgentPrompt } from './prompts/chat-general.mjs';
|
|
import {
|
|
hasMindspaceCoverMeta,
|
|
hasPlatformBrandMarker,
|
|
hasShareDescription,
|
|
verifyArtifactSharePreview,
|
|
verifySharePreviewMeta,
|
|
} from './verify/share-preview.mjs';
|
|
|
|
test('classifyWechatIntent detects page.generate', () => {
|
|
const intent = classifyWechatIntent({
|
|
msgType: 'text',
|
|
agentText: '帮我生成一个唐诗页面,放一首诗',
|
|
});
|
|
assert.equal(intent.kind, 'page.generate');
|
|
assert.match(intent.topic, /唐诗页面/);
|
|
assert.equal(isPageGenerateIntent({ msgType: 'text', agentText: intent.topic }), true);
|
|
});
|
|
|
|
test('classifyWechatIntent detects session.reset', () => {
|
|
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换话题' }).kind, 'session.reset');
|
|
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');
|
|
});
|
|
|
|
test('resolveWechatSessionAction recognizes natural new-session expressions', async () => {
|
|
for (const text of ['换新会话', '新开一个会话', '我们重新开始吧', '从头来']) {
|
|
const result = await resolveWechatSessionAction(text);
|
|
assert.equal(result.action, 'reset', text);
|
|
}
|
|
});
|
|
|
|
test('resolveWechatSessionAction separates ignore-context from reset', async () => {
|
|
assert.equal((await resolveWechatSessionAction('不要参考刚才关于医联体的内容')).action, 'ignore_previous');
|
|
assert.equal((await resolveWechatSessionAction('继续刚才的话题')).action, 'continue');
|
|
});
|
|
|
|
test('resolveWechatSessionAction uses an injected semantic classifier for unclear text', async () => {
|
|
const result = await resolveWechatSessionAction('我们换个思路聊', {
|
|
semanticClassifier: async () => ({ action: 'reset', confidence: 0.88, reason: '新对话意图' }),
|
|
});
|
|
assert.equal(result.action, 'reset');
|
|
assert.equal(result.source, 'semantic_model');
|
|
});
|
|
|
|
test('selectSendableHtmlArtifacts never returns stub html', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-stub-'));
|
|
const stubPath = path.join(dir, 'tang-poem.html');
|
|
fs.writeFileSync(
|
|
stubPath,
|
|
'<html>服务号自动补出简版页面</html>',
|
|
'utf8',
|
|
);
|
|
const realPath = path.join(dir, 'real.html');
|
|
fs.writeFileSync(
|
|
realPath,
|
|
`<!doctype html><html><body><main>${'x'.repeat(600)}</main></body></html>`,
|
|
'utf8',
|
|
);
|
|
const artifacts = [
|
|
{ localPath: stubPath, relativePath: 'public/tang-poem.html', url: 'https://example/tang-poem.html' },
|
|
{ localPath: realPath, relativePath: 'public/real.html', url: 'https://example/real.html' },
|
|
];
|
|
const sendable = selectSendableHtmlArtifacts({ confirmedArtifacts: artifacts });
|
|
assert.equal(sendable.length, 1);
|
|
assert.match(sendable[0].relativePath, /real\.html/);
|
|
assert.equal(isStubPublicHtmlContent('<html>服务号自动补出简版页面</html>'), true);
|
|
});
|
|
|
|
test('resolveSyncReply handles greeting and status probes', () => {
|
|
const user = { nickname: '唐' };
|
|
assert.match(
|
|
resolveSyncReply(classifyWechatIntent({ msgType: 'text', agentText: '你好' }), user, {
|
|
statusFallbackText: '处理中',
|
|
}),
|
|
/唐/,
|
|
);
|
|
assert.match(
|
|
resolveSyncReply(classifyWechatIntent({ msgType: 'text', agentText: '??' }), user, {
|
|
statusFallbackText: '处理中',
|
|
}),
|
|
/我在这边/,
|
|
);
|
|
});
|
|
|
|
test('buildWechatAgentPrompt lives in wechat prompts package', () => {
|
|
const prompt = buildWechatAgentPrompt({
|
|
msgType: 'text',
|
|
agentText: '明天早上 6 点跑步,5 点半提醒我',
|
|
});
|
|
assert.match(prompt, /schedule-assistant/);
|
|
});
|
|
|
|
test('buildWechatAgentPrompt carries ignore-previous context boundary', () => {
|
|
const prompt = buildWechatAgentPrompt({
|
|
msgType: 'text',
|
|
agentText: '请分析这段新内容',
|
|
sessionAction: 'ignore_previous',
|
|
});
|
|
assert.match(prompt, /忽略此前相关内容/);
|
|
});
|
|
|
|
test('schedule confirmation guard blocks false positives', async () => {
|
|
assert.equal(looksLikeScheduleConfirmation('已经帮你设置好了待办提醒'), true);
|
|
const guarded = await guardScheduleConfirmationReply({
|
|
replyText: '已经帮你设置好了待办提醒',
|
|
scheduleService: { listItemsBySourceMessage: async () => [] },
|
|
userId: 'user-1',
|
|
sourceMessageId: 'msg-1',
|
|
logger: null,
|
|
});
|
|
assert.match(guarded, /不能算设置成功/);
|
|
});
|
|
|
|
test('resolvePageGenerateOutcome fails when only stub artifacts exist', () => {
|
|
const outcome = resolvePageGenerateOutcome({
|
|
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/tang-poem.html' },
|
|
confirmedArtifacts: [
|
|
{
|
|
localPath: '/tmp/stub.html',
|
|
relativePath: 'public/tang-poem.html',
|
|
},
|
|
],
|
|
htmlGenerationNeedsRetry: true,
|
|
replyHasPublicLinks: true,
|
|
});
|
|
assert.equal(outcome.action, 'fail');
|
|
assert.match(outcome.failureText, /服务号页面技能/);
|
|
});
|
|
|
|
test('verifyPageArtifactContent rejects small stub files', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-verify-'));
|
|
const file = path.join(dir, 'page.html');
|
|
fs.writeFileSync(file, '<html>服务号兜底</html>', 'utf8');
|
|
const artifact = { localPath: file, relativePath: 'public/page.html' };
|
|
assert.equal(verifyPageArtifactContent(artifact).ok, false);
|
|
assert.equal(filterSendableHtmlArtifacts([artifact]).length, 0);
|
|
});
|
|
|
|
function writePreviewReadyHtml(file, { withCover = true, withBrand = true } = {}) {
|
|
const coverMeta = withCover
|
|
? '<meta name="mindspace-cover" content=\'{"tag":"旅行","accent":"#ff6b35","accent2":"#24243e","subtitle":"测试页面"}\'>'
|
|
: '';
|
|
const brandMeta = withBrand
|
|
? '<p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p>'
|
|
: '';
|
|
fs.writeFileSync(
|
|
file,
|
|
`<!doctype html><html><head>
|
|
<meta name="description" content="一句话摘要">
|
|
${coverMeta}
|
|
</head><body><main>${'x'.repeat(600)}</main>${brandMeta}</body></html>`,
|
|
'utf8',
|
|
);
|
|
}
|
|
|
|
test('verifySharePreviewMeta requires mindspace-cover, description, and platform brand', () => {
|
|
const withAll =
|
|
'<meta name="description" content="摘要"><meta name="mindspace-cover" content=\'{"tag":"旅行"}\'><p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p>';
|
|
assert.equal(verifySharePreviewMeta(withAll).ok, true);
|
|
assert.equal(verifySharePreviewMeta('<meta name="description" content="摘要">').ok, false);
|
|
assert.equal(hasMindspaceCoverMeta(withAll), true);
|
|
assert.equal(hasShareDescription(withAll), true);
|
|
assert.equal(hasPlatformBrandMarker(withAll), true);
|
|
});
|
|
|
|
test('resolvePageGenerateOutcome fails when html lacks share preview meta', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-'));
|
|
const file = path.join(dir, 'page.html');
|
|
writePreviewReadyHtml(file, { withCover: false });
|
|
const outcome = resolvePageGenerateOutcome({
|
|
reply: { text: '页面已生成 https://m.tkmind.cn/MindSpace/u/public/page.html' },
|
|
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
|
|
});
|
|
assert.equal(outcome.action, 'fail');
|
|
assert.equal(outcome.reason, 'missing_mindspace_cover');
|
|
assert.match(outcome.failureText, /预览元数据/);
|
|
});
|
|
|
|
test('resolvePageGenerateOutcome fails when html lacks platform brand marker', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-brand-'));
|
|
const file = path.join(dir, 'page.html');
|
|
writePreviewReadyHtml(file, { withBrand: false });
|
|
const outcome = resolvePageGenerateOutcome({
|
|
reply: { text: '页面已生成' },
|
|
confirmedArtifacts: [{ localPath: file, relativePath: 'public/page.html' }],
|
|
});
|
|
assert.equal(outcome.action, 'fail');
|
|
assert.equal(outcome.reason, 'missing_platform_brand');
|
|
assert.match(outcome.failureText, /平台品牌/);
|
|
});
|
|
|
|
test('resolvePageGenerateOutcome sends when share preview meta is present', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-preview-ok-'));
|
|
const file = path.join(dir, 'page.html');
|
|
writePreviewReadyHtml(file);
|
|
const artifact = { localPath: file, relativePath: 'public/page.html' };
|
|
assert.equal(verifyArtifactSharePreview(artifact).ok, true);
|
|
const outcome = resolvePageGenerateOutcome({
|
|
reply: { text: '页面已生成' },
|
|
confirmedArtifacts: [artifact],
|
|
});
|
|
assert.equal(outcome.action, 'send');
|
|
assert.equal(outcome.artifacts.length, 1);
|
|
});
|