Files
memind/experience-service.test.mjs
T
john 25f8223253 refactor: Business logic and dependencies updates
- 核心服务代码更新 (db, server, auth, proxy)
- Agent 相关模块更新 (mindspace, experience)
- 前端组件和 hooks 更新
- 数据库 schema 更新
- 依赖版本更新
2026-06-27 08:25:02 +08:00

115 lines
3.9 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createExperienceService } from './experience-service.mjs';
// Minimal in-memory pool covering the SQL shapes experience-service issues:
// INSERT, scope-filtered SELECT, and the use_count bump UPDATE.
function createMockPool() {
const rows = [];
const query = async (sql, params = []) => {
if (sql.includes('INSERT INTO h5_experience')) {
const [
id,
scope,
kind,
title,
body,
tagsJson,
sourceSessionId,
sourceUserId,
createdAt,
updatedAt,
] = params;
rows.push({
id,
scope,
kind,
title,
body,
tags_json: tagsJson,
source_session_id: sourceSessionId,
source_user_id: sourceUserId,
use_count: 0,
created_at: createdAt,
updated_at: updatedAt,
});
return [{ affectedRows: 1 }];
}
if (sql.includes('FROM h5_experience') && sql.includes('WHERE scope = ?')) {
const [scope] = params;
const matched = rows
.filter((r) => r.scope === scope)
.sort((a, b) => b.updated_at - a.updated_at);
return [matched.map((r) => ({ ...r }))];
}
if (sql.includes('UPDATE h5_experience SET use_count')) {
for (const id of params) {
const row = rows.find((r) => r.id === id);
if (row) row.use_count += 1;
}
return [{ affectedRows: params.length }];
}
throw new Error(`unexpected SQL: ${sql}`);
};
return { pool: { query }, rows };
}
test('record persists an experience with normalized fields', async () => {
const { pool, rows } = createMockPool();
const svc = createExperienceService(pool, { now: () => 1000 });
const saved = await svc.record({
title: 'Headscale 部署',
body: '在 103 上用固定公网 IP 部署 headscale,注意防火墙放行 41641/udp。',
tags: ['headscale', 'headscale', ' 网络 ', ''],
});
assert.equal(rows.length, 1);
assert.equal(saved.scope, 'global');
assert.equal(saved.kind, 'lesson');
assert.deepEqual(saved.tags, ['headscale', '网络']);
assert.equal(saved.useCount, 0);
});
test('record rejects empty title or body', async () => {
const { pool } = createMockPool();
const svc = createExperienceService(pool);
await assert.rejects(() => svc.record({ title: '', body: 'x' }), /标题不能为空/);
await assert.rejects(() => svc.record({ title: 'x', body: '' }), /内容不能为空/);
});
test('search ranks keyword matches and ignores empty queries', async () => {
const { pool } = createMockPool();
const svc = createExperienceService(pool, { now: () => 1_000_000 });
await svc.record({ title: 'Headscale 部署', body: 'headscale 防火墙 udp' });
await svc.record({ title: '无关经验', body: '这条跟查询完全无关的内容' });
assert.deepEqual(await svc.search(' '), []);
const hits = await svc.search('headscale 部署');
assert.equal(hits.length, 1);
assert.equal(hits[0].title, 'Headscale 部署');
});
test('search applies recency decay so newer wins on equal keyword score', async () => {
const { pool } = createMockPool();
let clock = 0;
const svc = createExperienceService(pool, {
now: () => clock,
recencyHalfLifeMs: 1000,
});
clock = 0;
await svc.record({ title: '部署指南 A', body: 'deploy 部署 指南' });
clock = 10_000; // 10 half-lives newer
await svc.record({ title: '部署指南 B', body: 'deploy 部署 指南' });
clock = 10_000;
const hits = await svc.search('部署 deploy 指南', { limit: 2 });
assert.equal(hits[0].title, '部署指南 B');
});
test('search bumps use_count for returned rows', async () => {
const { pool, rows } = createMockPool();
const svc = createExperienceService(pool, { now: () => 5 });
await svc.record({ title: 'Caddy 灰度', body: 'caddy weighted_round_robin 分流' });
await svc.search('caddy 灰度');
assert.equal(rows[0].use_count, 1);
});