238 lines
8.8 KiB
JavaScript
238 lines
8.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
buildInitialUserMemoryProfile,
|
|
buildTaskRoutingAgentText,
|
|
resolveCodeExecutorRouting,
|
|
renderCodeExecutorGuidance,
|
|
renderCurrentTimeAnchor,
|
|
resolveTimeOfDayPeriod,
|
|
suggestCodeExecutorForTask,
|
|
buildSessionMemoryEntries,
|
|
buildCurrentTimeAgentPrefix,
|
|
ensureUserMemoryProfile,
|
|
hasMemoryStore,
|
|
loadUserMemoryProfile,
|
|
renderMemoryStoreGuidance,
|
|
USER_MEMORY_PROFILE_FILENAME,
|
|
} from './user-memory-profile.mjs';
|
|
import { buildAgentExtensionPolicy, DEFAULT_USER_CAPABILITIES } from './capabilities.mjs';
|
|
|
|
test('DEFAULT_USER_CAPABILITIES enables memory_store (L3)', () => {
|
|
assert.equal(DEFAULT_USER_CAPABILITIES.memory_store, true);
|
|
assert.equal(DEFAULT_USER_CAPABILITIES.context_memory, true);
|
|
});
|
|
|
|
test('buildAgentExtensionPolicy adds builtin memory when memory_store is enabled', () => {
|
|
const policy = buildAgentExtensionPolicy(DEFAULT_USER_CAPABILITIES);
|
|
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'memory'));
|
|
assert.ok(policy.extensionOverrides.some((ext) => ext.name === 'projectmemory'));
|
|
});
|
|
|
|
test('hasMemoryStore detects memory extension in session policy', () => {
|
|
const policy = buildAgentExtensionPolicy({ ...DEFAULT_USER_CAPABILITIES, memory_store: false });
|
|
assert.equal(hasMemoryStore(policy), false);
|
|
assert.equal(hasMemoryStore(buildAgentExtensionPolicy(DEFAULT_USER_CAPABILITIES)), true);
|
|
});
|
|
|
|
test('ensureUserMemoryProfile creates and updates profile file', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tkmind-profile-'));
|
|
const first = ensureUserMemoryProfile(root, {
|
|
userId: 'user-1',
|
|
displayName: 'Alice',
|
|
username: 'alice',
|
|
});
|
|
assert.equal(first.displayName, 'Alice');
|
|
assert.equal(fs.existsSync(path.join(root, USER_MEMORY_PROFILE_FILENAME)), true);
|
|
|
|
const updated = ensureUserMemoryProfile(root, {
|
|
userId: 'user-1',
|
|
displayName: '艾丽丝',
|
|
username: 'alice',
|
|
});
|
|
assert.equal(updated.displayName, '艾丽丝');
|
|
assert.equal(loadUserMemoryProfile(root)?.displayName, '艾丽丝');
|
|
});
|
|
|
|
test('buildSessionMemoryEntries injects sandbox, guidance, and profile', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tkmind-memory-'));
|
|
ensureUserMemoryProfile(root, {
|
|
userId: 'user-2',
|
|
displayName: 'Bob',
|
|
username: 'bob',
|
|
});
|
|
const sessionPolicy = {
|
|
...buildAgentExtensionPolicy({
|
|
...DEFAULT_USER_CAPABILITIES,
|
|
aider: true,
|
|
openhands: true,
|
|
}, { toolMode: 'code' }),
|
|
policies: {
|
|
code_delegate_executor: 'openhands',
|
|
code_task_routing: 'split',
|
|
},
|
|
};
|
|
const entries = buildSessionMemoryEntries({
|
|
workingDir: root,
|
|
sessionPolicy,
|
|
sandboxConstraints: '## sandbox',
|
|
userContext: { userId: 'user-2', displayName: 'Bob', username: 'bob' },
|
|
now: Date.UTC(2026, 5, 29, 5, 0, 0),
|
|
});
|
|
assert.equal(entries.length, 5);
|
|
assert.equal(entries[0].title, 'TKMind 用户空间沙箱');
|
|
assert.equal(entries[1].title, 'TKMind 代码委托策略');
|
|
assert.match(entries[1].content, /openhands/);
|
|
assert.equal(entries[2].title, 'TKMind 当前时间基准');
|
|
assert.match(entries[2].content, /2026-06-29(星期一)/);
|
|
assert.match(entries[2].content, /13:00(中午)/);
|
|
assert.match(entries[2].content, /若需时段问候可参考:中午好/);
|
|
assert.match(entries[3].content, /长期记忆/);
|
|
assert.match(entries[4].content, /Bob/);
|
|
});
|
|
|
|
test('buildSessionMemoryEntries injects stored conversation memories', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tkmind-memory-'));
|
|
const sessionPolicy = buildAgentExtensionPolicy(DEFAULT_USER_CAPABILITIES);
|
|
const entries = buildSessionMemoryEntries({
|
|
workingDir: root,
|
|
sessionPolicy,
|
|
userContext: { userId: 'user-3', displayName: 'Carol', username: 'carol' },
|
|
userMemories: [{ label: 'interest', text: '用户关注 AI 产品设计' }],
|
|
now: Date.UTC(2026, 5, 29, 5, 0, 0),
|
|
});
|
|
assert.equal(entries.at(-1).title, 'TKMind 已沉淀用户记忆');
|
|
assert.match(entries.at(-1).content, /AI 产品设计/);
|
|
});
|
|
|
|
test('buildSessionMemoryEntries injects time anchor even without memory store', () => {
|
|
const sessionPolicy = buildAgentExtensionPolicy({
|
|
...DEFAULT_USER_CAPABILITIES,
|
|
memory_store: false,
|
|
});
|
|
const entries = buildSessionMemoryEntries({
|
|
workingDir: '/tmp/unused',
|
|
sessionPolicy,
|
|
now: Date.UTC(2026, 5, 29, 5, 0, 0),
|
|
});
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].title, 'TKMind 当前时间基准');
|
|
assert.match(entries[0].content, /Asia\/Shanghai/);
|
|
assert.match(entries[0].content, /13:00(中午)/);
|
|
});
|
|
|
|
test('buildCurrentTimeAgentPrefix wraps Shanghai anchor for agent-only injection', () => {
|
|
const text = buildCurrentTimeAgentPrefix({
|
|
now: Date.UTC(2026, 5, 29, 5, 0, 0),
|
|
timezone: 'Asia/Shanghai',
|
|
});
|
|
assert.match(text, /不要向用户复述/);
|
|
assert.match(text, /2026-06-29(星期一)/);
|
|
assert.match(text, /13:00(中午)/);
|
|
});
|
|
|
|
test('renderCurrentTimeAnchor formats Shanghai weekday from exact date', () => {
|
|
const text = renderCurrentTimeAnchor({
|
|
now: Date.UTC(2026, 5, 29, 5, 0, 0),
|
|
timezone: 'Asia/Shanghai',
|
|
});
|
|
assert.match(text, /2026-06-29(星期一)/);
|
|
assert.match(text, /Asia\/Shanghai/);
|
|
assert.match(text, /13:00(中午)/);
|
|
assert.match(text, /若需时段问候可参考:中午好/);
|
|
assert.match(text, /普通任务回复不要每条都加/);
|
|
});
|
|
|
|
test('renderCurrentTimeAnchor uses local morning greeting at 07:03 Shanghai', () => {
|
|
const text = renderCurrentTimeAnchor({
|
|
now: Date.UTC(2026, 5, 29, 23, 3, 0),
|
|
timezone: 'Asia/Shanghai',
|
|
});
|
|
assert.match(text, /2026-06-30(星期二)/);
|
|
assert.match(text, /07:03(早上)/);
|
|
assert.match(text, /若需时段问候可参考:早上好/);
|
|
});
|
|
|
|
test('resolveTimeOfDayPeriod maps hours to greeting labels', () => {
|
|
assert.deepEqual(resolveTimeOfDayPeriod(2), { period: '凌晨', greeting: '你好' });
|
|
assert.deepEqual(resolveTimeOfDayPeriod(7), { period: '早上', greeting: '早上好' });
|
|
assert.deepEqual(resolveTimeOfDayPeriod(10), { period: '上午', greeting: '上午好' });
|
|
assert.deepEqual(resolveTimeOfDayPeriod(20), { period: '晚上', greeting: '晚上好' });
|
|
});
|
|
|
|
test('renderMemoryStoreGuidance scopes memory to one user', () => {
|
|
const text = renderMemoryStoreGuidance({ addressName: '小陈' });
|
|
assert.match(text, /小陈/);
|
|
assert.match(text, /不得与其他用户混淆/);
|
|
});
|
|
|
|
test('buildInitialUserMemoryProfile seeds defaults', () => {
|
|
const profile = buildInitialUserMemoryProfile({
|
|
userId: 'abc',
|
|
displayName: '测试',
|
|
username: 'tester',
|
|
});
|
|
assert.equal(profile.version, 1);
|
|
assert.equal(profile.language, 'zh-CN');
|
|
assert.equal(profile.responseStyle, 'balanced');
|
|
assert.deepEqual(profile.preferences, []);
|
|
});
|
|
|
|
test('renderCodeExecutorGuidance reflects preferred executor and fallback', () => {
|
|
const text = renderCodeExecutorGuidance({
|
|
extensionOverrides: [
|
|
{ name: 'aider' },
|
|
{ name: 'openhands' },
|
|
],
|
|
policies: {
|
|
code_delegate_executor: 'aider',
|
|
code_task_routing: 'force_aider',
|
|
},
|
|
});
|
|
assert.match(text, /aider/i);
|
|
assert.match(text, /openhands/i);
|
|
assert.match(text, /优先使用/);
|
|
assert.match(text, /统一走 `aider`/);
|
|
});
|
|
|
|
test('resolveCodeExecutorRouting falls back when preferred executor is unavailable', () => {
|
|
const routing = resolveCodeExecutorRouting({
|
|
extensionOverrides: [{ name: 'aider' }],
|
|
policies: {
|
|
code_delegate_executor: 'openhands',
|
|
code_task_routing: 'split',
|
|
},
|
|
});
|
|
assert.deepEqual(routing.available, ['aider']);
|
|
assert.equal(routing.preferred, 'auto');
|
|
assert.equal(routing.routing, 'split');
|
|
assert.match(routing.rules[0], /auto/);
|
|
});
|
|
|
|
test('suggestCodeExecutorForTask prefers openhands for repo refactor style tasks', () => {
|
|
const suggestion = suggestCodeExecutorForTask(
|
|
'请帮我重构这个仓库里的认证流程,涉及多文件并跑一下测试',
|
|
{
|
|
extensionOverrides: [{ name: 'aider' }, { name: 'openhands' }],
|
|
policies: { code_delegate_executor: 'auto', code_task_routing: 'split' },
|
|
},
|
|
);
|
|
assert.equal(suggestion?.suggestedExecutor, 'openhands');
|
|
});
|
|
|
|
test('buildTaskRoutingAgentText preserves task body and adds hidden routing hint', () => {
|
|
const text = buildTaskRoutingAgentText(
|
|
'请修复这个按钮点击无效的 bug,改一下前端代码',
|
|
{
|
|
extensionOverrides: [{ name: 'aider' }, { name: 'openhands' }],
|
|
policies: { code_delegate_executor: 'aider', code_task_routing: 'force_aider' },
|
|
},
|
|
);
|
|
assert.match(text, /TKMind 路由提示/);
|
|
assert.match(text, /aider/i);
|
|
assert.match(text, /请修复这个按钮点击无效的 bug,改一下前端代码/);
|
|
});
|