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,改一下前端代码/); });