2baf29b3ae
Deliver encrypted health zone, observation API, baseline maturity pipeline, page-data bindings, and H5/WeChat channel integration for health P0. Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
6.4 KiB
JavaScript
205 lines
6.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createHealthChannelSessionStore } from './health-channel-session-store.mjs';
|
|
import { createInMemoryHealthObservationStore } from './health-observation-store.mjs';
|
|
import { createInMemoryHealthEventStore } from './health-event-store.mjs';
|
|
import { createHealthObservationService } from './health-observation-service.mjs';
|
|
import {
|
|
HEALTH_MENU_TEXT,
|
|
applyHealthChannelTurn,
|
|
applyHealthWechatTurn,
|
|
isHealthEnterText,
|
|
shouldFallThroughWechatHealthMenuEvent,
|
|
} from './health-wechat-turn.mjs';
|
|
import { createIdleHealthChannelState } from './health-channel-state.mjs';
|
|
import { handleWechatHealthChannel } from './wechat/handlers/health.mjs';
|
|
|
|
test('health menu event only falls through when the feature flag is on', () => {
|
|
const inbound = { event: 'click', eventKey: 'MEMIND_HEALTH' };
|
|
assert.equal(shouldFallThroughWechatHealthMenuEvent(inbound, {}), false);
|
|
assert.equal(
|
|
shouldFallThroughWechatHealthMenuEvent(inbound, { MEMIND_HEALTH_ENABLED: '1' }),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('wechat health channel requires confirm before writing observations', async () => {
|
|
const store = createHealthChannelSessionStore();
|
|
const observations = createInMemoryHealthObservationStore();
|
|
const observationService = createHealthObservationService({ store: observations });
|
|
const userId = 'user-1';
|
|
|
|
const entered = await handleWechatHealthChannel({
|
|
enabled: true,
|
|
userId,
|
|
intent: { msgType: 'text', agentText: '健康助手' },
|
|
inbound: {},
|
|
store,
|
|
observationService,
|
|
});
|
|
assert.match(entered, /专用通道/);
|
|
|
|
const parsed = await handleWechatHealthChannel({
|
|
enabled: true,
|
|
userId,
|
|
intent: { msgType: 'text', agentText: '137/82' },
|
|
inbound: {},
|
|
store,
|
|
observationService,
|
|
});
|
|
assert.match(parsed, /确认保存/);
|
|
assert.equal((await observations.list(userId)).length, 0);
|
|
|
|
const saved = await handleWechatHealthChannel({
|
|
enabled: true,
|
|
userId,
|
|
intent: { msgType: 'text', agentText: '1' },
|
|
inbound: {},
|
|
store,
|
|
observationService,
|
|
});
|
|
assert.match(saved, /已记录/);
|
|
const rows = await observations.list(userId);
|
|
assert.equal(rows.length, 2);
|
|
assert.equal(rows.some((row) => row.metricType === 'bp_systolic' && row.valueNum === 137), true);
|
|
});
|
|
|
|
test('wechat health channel prepends unread alerts on enter', async () => {
|
|
const store = createHealthChannelSessionStore();
|
|
const eventStore = createInMemoryHealthEventStore();
|
|
await eventStore.insertIfNew('user-alert', {
|
|
severity: 'alert',
|
|
eventType: 'threshold_breach',
|
|
ruleId: 'bp_sys_high',
|
|
metricType: 'bp_systolic',
|
|
message: '收缩压触发阈值',
|
|
});
|
|
|
|
const entered = await handleWechatHealthChannel({
|
|
enabled: true,
|
|
userId: 'user-alert',
|
|
intent: { msgType: 'text', agentText: '健康助手' },
|
|
inbound: {},
|
|
store,
|
|
eventStore,
|
|
});
|
|
|
|
assert.match(entered, /未读健康提醒/);
|
|
assert.match(entered, /专用通道/);
|
|
});
|
|
|
|
test('disabled health channel does not intercept ordinary wechat chat', async () => {
|
|
const reply = await handleWechatHealthChannel({
|
|
enabled: false,
|
|
userId: 'user-1',
|
|
intent: { msgType: 'text', agentText: '健康助手' },
|
|
inbound: {},
|
|
store: createHealthChannelSessionStore(),
|
|
});
|
|
assert.equal(reply, null);
|
|
});
|
|
|
|
test('enter keywords are specific and photo does not auto-commit', () => {
|
|
assert.equal(isHealthEnterText('健康助手'), true);
|
|
assert.equal(isHealthEnterText('健康小助手'), true);
|
|
assert.equal(isHealthEnterText('健康饮食怎么做'), false);
|
|
const inValue = applyHealthWechatTurn({
|
|
session: {
|
|
channelState: {
|
|
sessionKind: 'health',
|
|
channel: 'wechat',
|
|
step: 'await_value',
|
|
action: 'record',
|
|
metricSet: 'blood_pressure',
|
|
context: 'morning',
|
|
pendingDraftId: null,
|
|
lastPrompt: null,
|
|
},
|
|
pendingValues: { context: 'morning' },
|
|
},
|
|
msgType: 'image',
|
|
text: '',
|
|
});
|
|
assert.equal(inValue.handled, true);
|
|
assert.equal(inValue.commit, null);
|
|
assert.match(inValue.reply, /不会|手输|确认/);
|
|
});
|
|
|
|
test('unconfirmed insert is rejected by the observation store', async () => {
|
|
const store = createInMemoryHealthObservationStore();
|
|
await assert.rejects(
|
|
() => store.insert('user-1', { metricType: 'bp_systolic', valueNum: 120 }),
|
|
(error) => error.code === 'health_unconfirmed',
|
|
);
|
|
});
|
|
|
|
test('h5 channel can enter without menu event key', () => {
|
|
const entered = applyHealthChannelTurn({
|
|
channel: 'h5',
|
|
forceEnter: true,
|
|
text: '',
|
|
});
|
|
assert.equal(entered.handled, true);
|
|
assert.match(entered.reply, /专用通道/);
|
|
assert.equal(entered.session?.channelState?.channel, 'h5');
|
|
});
|
|
|
|
test('menu item 2 enters document upload mode', () => {
|
|
const picked = applyHealthChannelTurn({
|
|
channel: 'h5',
|
|
session: {
|
|
channelState: createIdleHealthChannelState({ channel: 'h5' }),
|
|
pendingValues: null,
|
|
},
|
|
text: '2',
|
|
});
|
|
assert.match(picked.reply, /报告照片/);
|
|
assert.equal(picked.session?.channelState?.action, 'document');
|
|
});
|
|
|
|
test('document image archives without device extraction commit shape', () => {
|
|
const uploaded = applyHealthChannelTurn({
|
|
channel: 'h5',
|
|
session: {
|
|
channelState: {
|
|
...createIdleHealthChannelState({ channel: 'h5' }),
|
|
action: 'document',
|
|
},
|
|
pendingValues: null,
|
|
},
|
|
msgType: 'image',
|
|
text: '',
|
|
});
|
|
assert.equal(uploaded.commit?.type, 'document');
|
|
assert.match(uploaded.reply, /归档/);
|
|
});
|
|
|
|
test('menu text is the exclusive wechat prompt', () => {
|
|
assert.match(HEALTH_MENU_TEXT, /0 退出健康通道/);
|
|
});
|
|
|
|
test('h5 idle free text falls through to agent instead of replaying menu', () => {
|
|
const result = applyHealthChannelTurn({
|
|
channel: 'h5',
|
|
session: {
|
|
channelState: createIdleHealthChannelState({ channel: 'h5' }),
|
|
pendingValues: null,
|
|
},
|
|
text: '帮我生成一份健康报告,分析一下最近血压',
|
|
});
|
|
assert.equal(result.handled, false);
|
|
});
|
|
|
|
test('wechat idle unknown text still replays menu', () => {
|
|
const result = applyHealthChannelTurn({
|
|
channel: 'wechat',
|
|
session: {
|
|
channelState: createIdleHealthChannelState({ channel: 'wechat' }),
|
|
pendingValues: null,
|
|
},
|
|
text: '帮我生成一份健康报告',
|
|
});
|
|
assert.equal(result.handled, true);
|
|
assert.match(result.reply, /专用通道/);
|
|
});
|