basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2026 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as fs from 'node:fs/promises';8import * as os from 'node:os';9import * as path from 'node:path';10import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';11import type { Config } from '../config/config.js';12import type { PermissionManager } from '../permissions/permission-manager.js';13import type { ForkedAgentResult } from '../utils/forkedAgent.js';14import { runForkedAgent } from '../utils/forkedAgent.js';15import {16 buildBareRememberPrompt,17 buildManagedRememberPrompt,18 runManagedRememberByAgent,19} from './remember.js';20import {21 clearAutoMemoryRootCache,22 getAutoMemoryRoot,23 getUserAutoMemoryRoot,24} from './paths.js';25import {26 rebuildManagedAutoMemoryIndex,27 rebuildUserAutoMemoryIndex,28} from './indexer.js';29 30vi.mock('../utils/forkedAgent.js', () => ({31 runForkedAgent: vi.fn(),32}));33 34vi.mock('./indexer.js', () => ({35 rebuildManagedAutoMemoryIndex: vi.fn(),36 rebuildUserAutoMemoryIndex: vi.fn(),37}));38 39function createConfig(projectRoot: string, managed = true): Config {40 return {41 isManagedMemoryAvailable: vi.fn().mockReturnValue(managed),42 getProjectRoot: vi.fn().mockReturnValue(projectRoot),43 getUserMemory: vi.fn().mockReturnValue('QWEN/AGENTS guidance'),44 } as unknown as Config;45}46 47describe('remember memory helper', () => {48 const originalMemoryBase = process.env['QWEN_CODE_MEMORY_BASE_DIR'];49 let tempDir: string;50 let projectRoot: string;51 52 beforeEach(async () => {53 tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remember-helper-'));54 projectRoot = path.join(tempDir, 'project');55 await fs.mkdir(projectRoot, { recursive: true });56 process.env['QWEN_CODE_MEMORY_BASE_DIR'] = path.join(tempDir, 'memory');57 clearAutoMemoryRootCache();58 vi.mocked(runForkedAgent).mockReset();59 vi.mocked(rebuildManagedAutoMemoryIndex).mockReset();60 vi.mocked(rebuildUserAutoMemoryIndex).mockReset();61 vi.mocked(rebuildManagedAutoMemoryIndex).mockResolvedValue('');62 vi.mocked(rebuildUserAutoMemoryIndex).mockResolvedValue('');63 });64 65 afterEach(async () => {66 if (originalMemoryBase === undefined) {67 delete process.env['QWEN_CODE_MEMORY_BASE_DIR'];68 } else {69 process.env['QWEN_CODE_MEMORY_BASE_DIR'] = originalMemoryBase;70 }71 clearAutoMemoryRootCache();72 await fs.rm(tempDir, { recursive: true, force: true });73 });74 75 it('builds the same managed and bare prompts used by /remember', () => {76 const managed = buildManagedRememberPrompt(77 ' prefers focused tests ',78 projectRoot,79 );80 81 expect(managed).toContain(82 'Please save the following to your memory system.',83 );84 expect(managed).toContain('USER memory at');85 expect(managed).toContain('PROJECT memory at');86 expect(managed).toContain(getAutoMemoryRoot(projectRoot));87 expect(managed).toContain('prefers focused tests');88 expect(managed).not.toContain('<user-content>');89 expect(managed).not.toContain('</user-content>');90 expect(managed).not.toContain(' prefers focused tests ');91 92 const wrapped = buildManagedRememberPrompt(93 ' hidden context ',94 projectRoot,95 { wrapUserContent: true },96 );97 expect(wrapped).toContain(98 '<user-content>\nhidden context\n</user-content>',99 );100 101 const bare = buildBareRememberPrompt(' appends to qwen ');102 expect(bare).toBe(103 'Please save the following fact to memory (e.g. append to QWEN.md in the project root):\n\nappends to qwen',104 );105 });106 107 it('runs clean context with managed-memory tools only', async () => {108 const touched = path.join(getAutoMemoryRoot(projectRoot), 'project.md');109 vi.mocked(runForkedAgent).mockResolvedValue({110 status: 'completed',111 finalText: 'Saved project memory.',112 filesTouched: [touched],113 filesWritten: [touched],114 } satisfies ForkedAgentResult);115 116 const result = await runManagedRememberByAgent({117 config: createConfig(projectRoot),118 projectRoot,119 content: 'Remember the project uses vitest.',120 contextMode: 'clean',121 });122 123 expect(result).toEqual({124 summary: 'Memory update completed.',125 filesTouched: [touched],126 touchedScopes: ['project'],127 });128 expect(runForkedAgent).toHaveBeenCalledTimes(1);129 const params = vi.mocked(runForkedAgent).mock.calls[0]?.[0] as {130 config: Config;131 extraHistory?: unknown[];132 preserveEmptyExtraHistory?: boolean;133 systemPrompt: string;134 taskPrompt: string;135 tools: string[];136 };137 expect(params.extraHistory).toEqual([]);138 expect(params.preserveEmptyExtraHistory).toBe(true);139 expect(params.tools).toEqual([140 'read_file',141 'grep_search',142 'list_directory',143 'write_file',144 'edit',145 ]);146 expect(params.config.getUserMemory()).toBe('');147 expect(params.config.getDisableAllHooks()).toBe(true);148 expect(params.config.getHookSystem()).toBeUndefined();149 expect(params.config.getMessageBus()).toBeUndefined();150 const pm = params.config.getPermissionManager() as PermissionManager;151 await expect(152 pm.evaluate({153 toolName: 'grep_search',154 filePath: getAutoMemoryRoot(projectRoot),155 }),156 ).resolves.toBe('allow');157 await expect(158 pm.evaluate({159 toolName: 'list_directory',160 filePath: getUserAutoMemoryRoot(),161 }),162 ).resolves.toBe('allow');163 await expect(164 pm.evaluate({165 toolName: 'grep_search',166 filePath: path.join(projectRoot, 'src'),167 }),168 ).resolves.toBe('deny');169 expect(params.systemPrompt).toContain('managed auto-memory system only');170 expect(params.taskPrompt).toContain('Remember the project uses vitest.');171 expect(params.taskPrompt).toContain('<user-content>');172 expect(rebuildManagedAutoMemoryIndex).toHaveBeenCalledWith(projectRoot);173 });174 175 it('classifies only successful memory writes', async () => {176 const projectFile = path.join(getAutoMemoryRoot(projectRoot), 'project.md');177 vi.mocked(runForkedAgent).mockResolvedValue({178 status: 'completed',179 finalText: 'Saved project memory.',180 filesTouched: [path.join(projectRoot, 'README.md'), projectFile],181 filesWritten: [projectFile],182 } satisfies ForkedAgentResult);183 184 const result = await runManagedRememberByAgent({185 config: createConfig(projectRoot),186 projectRoot,187 content: 'Remember write-only paths.',188 contextMode: 'workspace',189 });190 191 expect(result).toEqual({192 summary: 'Memory update completed.',193 filesTouched: [projectFile],194 touchedScopes: ['project'],195 });196 const params = vi.mocked(runForkedAgent).mock.calls[0]?.[0] as {197 extraHistory?: unknown[];198 preserveEmptyExtraHistory?: boolean;199 };200 expect(params.extraHistory).toBeUndefined();201 expect(params.preserveEmptyExtraHistory).toBe(false);202 expect(rebuildManagedAutoMemoryIndex).toHaveBeenCalledWith(projectRoot);203 });204 205 it('disables chat recording for hidden remember agents', async () => {206 const touched = path.join(getAutoMemoryRoot(projectRoot), 'project.md');207 vi.mocked(runForkedAgent).mockResolvedValue({208 status: 'completed',209 filesTouched: [touched],210 filesWritten: [touched],211 } satisfies ForkedAgentResult);212 213 await runManagedRememberByAgent({214 config: createConfig(projectRoot),215 projectRoot,216 content: 'Remember without creating a visible session.',217 contextMode: 'workspace',218 });219 220 const params = vi.mocked(runForkedAgent).mock.calls[0]?.[0] as {221 config: Config;222 suppressChatRecording?: boolean;223 };224 expect(params.config.getChatRecordingService()).toBeUndefined();225 expect(params.config.getTranscriptPath()).toBe('');226 expect(params.suppressChatRecording).toBe(true);227 });228 229 it('rebuilds touched project indexes and best-effort user indexes', async () => {230 const projectFile = path.join(getAutoMemoryRoot(projectRoot), 'project.md');231 const userFile = path.join(getUserAutoMemoryRoot(), 'user.md');232 vi.mocked(rebuildUserAutoMemoryIndex).mockRejectedValue(233 new Error('user index unavailable'),234 );235 vi.mocked(runForkedAgent).mockResolvedValue({236 status: 'completed',237 filesTouched: [userFile, projectFile],238 filesWritten: [userFile, projectFile],239 } satisfies ForkedAgentResult);240 241 const result = await runManagedRememberByAgent({242 config: createConfig(projectRoot),243 projectRoot,244 content: 'Remember both scopes.',245 contextMode: 'workspace',246 });247 248 expect(result.touchedScopes).toEqual(['project', 'user']);249 expect(rebuildManagedAutoMemoryIndex).toHaveBeenCalledWith(projectRoot);250 expect(rebuildUserAutoMemoryIndex).toHaveBeenCalledTimes(1);251 });252 253 it('classifies symlinked project memory paths by realpath', async () => {254 const projectMemoryRoot = getAutoMemoryRoot(projectRoot);255 await fs.mkdir(projectMemoryRoot, { recursive: true });256 const linkedMemoryRoot = path.join(tempDir, 'linked-project-memory');257 await fs.symlink(projectMemoryRoot, linkedMemoryRoot, 'dir');258 const touched = path.join(linkedMemoryRoot, 'project.md');259 await fs.writeFile(touched, 'memory');260 vi.mocked(runForkedAgent).mockResolvedValue({261 status: 'completed',262 filesTouched: [touched],263 filesWritten: [touched],264 } satisfies ForkedAgentResult);265 266 const result = await runManagedRememberByAgent({267 config: createConfig(projectRoot),268 projectRoot,269 content: 'Remember symlinked memory.',270 contextMode: 'workspace',271 });272 273 expect(result.touchedScopes).toEqual(['project']);274 expect(rebuildManagedAutoMemoryIndex).toHaveBeenCalledWith(projectRoot);275 });276 277 it('rejects when managed memory is unavailable', async () => {278 await expect(279 runManagedRememberByAgent({280 config: createConfig(projectRoot, false),281 projectRoot,282 content: 'Remember me.',283 contextMode: 'workspace',284 }),285 ).rejects.toMatchObject({ code: 'managed_memory_unavailable' });286 expect(runForkedAgent).not.toHaveBeenCalled();287 });288 289 it('fails if the hidden agent touches a non-memory path', async () => {290 vi.mocked(runForkedAgent).mockResolvedValue({291 status: 'completed',292 filesTouched: [path.join(projectRoot, 'README.md')],293 filesWritten: [path.join(projectRoot, 'README.md')],294 } satisfies ForkedAgentResult);295 296 await expect(297 runManagedRememberByAgent({298 config: createConfig(projectRoot),299 projectRoot,300 content: 'Remember me.',301 contextMode: 'workspace',302 }),303 ).rejects.toMatchObject({ code: 'remember_path_escape' });304 expect(rebuildManagedAutoMemoryIndex).not.toHaveBeenCalled();305 });306 307 it('propagates failed termination reasons before auditing written paths', async () => {308 vi.mocked(runForkedAgent).mockResolvedValue({309 status: 'failed',310 terminateReason: 'max turns exceeded',311 filesTouched: [path.join(projectRoot, 'README.md')],312 filesWritten: [path.join(projectRoot, 'README.md')],313 } satisfies ForkedAgentResult);314 315 await expect(316 runManagedRememberByAgent({317 config: createConfig(projectRoot),318 projectRoot,319 content: 'Remember me.',320 contextMode: 'workspace',321 }),322 ).rejects.toThrow('max turns exceeded');323 expect(rebuildManagedAutoMemoryIndex).not.toHaveBeenCalled();324 });325 326 it('propagates failed and cancelled agent termination reasons', async () => {327 vi.mocked(runForkedAgent).mockResolvedValueOnce({328 status: 'failed',329 terminateReason: 'max turns exceeded',330 filesTouched: [],331 } satisfies ForkedAgentResult);332 333 await expect(334 runManagedRememberByAgent({335 config: createConfig(projectRoot),336 projectRoot,337 content: 'Remember me.',338 contextMode: 'workspace',339 }),340 ).rejects.toThrow('max turns exceeded');341 342 vi.mocked(runForkedAgent).mockResolvedValueOnce({343 status: 'cancelled',344 terminateReason: 'aborted',345 filesTouched: [],346 } satisfies ForkedAgentResult);347 348 await expect(349 runManagedRememberByAgent({350 config: createConfig(projectRoot),351 projectRoot,352 content: 'Remember me.',353 contextMode: 'workspace',354 }),355 ).rejects.toThrow('aborted');356 });357 358 it('remember agent always receives the full protocol even when all indexes are empty', async () => {359 const touched = path.join(getAutoMemoryRoot(projectRoot), 'user.md');360 vi.mocked(runForkedAgent).mockResolvedValue({361 status: 'completed',362 finalText: 'Saved.',363 filesTouched: [touched],364 filesWritten: [touched],365 } satisfies ForkedAgentResult);366 367 await runManagedRememberByAgent({368 config: createConfig(projectRoot),369 projectRoot,370 content: 'Remember this fact.',371 contextMode: 'clean',372 });373 374 const params = vi.mocked(runForkedAgent).mock.calls[0]?.[0] as {375 systemPrompt: string;376 };377 // Full-protocol markers must be present (forceFullProtocol: true)378 expect(params.systemPrompt).toContain('## Types of memory');379 expect(params.systemPrompt).toContain('## What NOT to save in memory');380 expect(params.systemPrompt).toContain('## When to access memories');381 expect(params.systemPrompt).toContain('## Before recommending from memory');382 // Condensed-only markers must NOT appear383 expect(params.systemPrompt).not.toContain('## Memory types');384 expect(params.systemPrompt).not.toContain('## Do not save');385 });386});387 