CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
extractionAgentPlanner.test.ts332 linesDownload Raw Back to memory
1/**2 * @license3 * Copyright 2026 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { beforeEach, describe, expect, it, vi } from 'vitest';8import type { Config } from '../config/config.js';9import { runAutoMemoryExtractionByAgent } from './extractionAgentPlanner.js';10import { scanAutoMemoryTopicDocuments } from './scan.js';11import { getAutoMemoryRoot, getUserAutoMemoryRoot } from './paths.js';12import { runForkedAgent, getCacheSafeParams } from '../utils/forkedAgent.js';13import { ToolNames } from '../tools/tool-names.js';14 15vi.mock('./scan.js', async (importOriginal) => {16  const actual = await importOriginal<typeof import('./scan.js')>();17  return {18    ...actual,19    scanAutoMemoryTopicDocuments: vi.fn(),20    // Explicit mock so the production scan does not silently fall through21    // to the real filesystem (it would only "work" because /tmp/user-memory22    // doesn't exist and listMarkdownFiles swallows ENOENT). Each test that23    // cares about user docs sets a mockReturnValue.24    scanUserAutoMemoryTopicDocuments: vi.fn().mockResolvedValue([]),25  };26});27 28vi.mock('./paths.js', async (importOriginal) => {29  const actual = await importOriginal<typeof import('./paths.js')>();30  return {31    ...actual,32    getAutoMemoryRoot: vi.fn().mockReturnValue('/tmp/auto-memory'),33    getUserAutoMemoryRoot: vi.fn().mockReturnValue('/tmp/user-memory'),34  };35});36 37vi.mock('../utils/forkedAgent.js', () => ({38  runForkedAgent: vi.fn(),39  getCacheSafeParams: vi.fn(),40}));41 42describe('runAutoMemoryExtractionByAgent', () => {43  const mockConfig = {44    getSessionId: vi.fn().mockReturnValue('session-1'),45    getModel: vi.fn().mockReturnValue('qwen3-coder-plus'),46    getApprovalMode: vi.fn(),47  } as unknown as Config;48 49  beforeEach(() => {50    vi.clearAllMocks();51    vi.mocked(getCacheSafeParams).mockReturnValue({52      generationConfig: {},53      history: [54        { role: 'user', parts: [{ text: 'I prefer terse responses.' }] },55        { role: 'model', parts: [{ text: 'Understood.' }] },56      ],57      model: 'qwen3-coder-plus',58      version: 1,59    });60    vi.mocked(scanAutoMemoryTopicDocuments).mockResolvedValue([61      {62        type: 'user',63        filePath: '/tmp/auto-memory/user/prefs.md',64        relativePath: 'user/prefs.md',65        filename: 'prefs.md',66        title: 'User Memory',67        description: 'User preferences',68        body: '- Existing terse preference.',69        mtimeMs: 1,70      },71    ]);72  });73 74  it('derives touchedTopics from filesTouched and returns systemMessage', async () => {75    vi.mocked(runForkedAgent).mockResolvedValue({76      status: 'completed',77      finalText: '',78      filesTouched: ['/tmp/auto-memory/user/prefs.md'],79      filesWritten: ['/tmp/auto-memory/user/prefs.md'],80    });81 82    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');83 84    expect(result).toEqual({85      touchedTopics: ['user'],86      touchedProjectScope: true,87      touchedUserScope: false,88      hasToolActivity: true,89      systemMessage: 'Managed auto-memory updated: user.md',90    });91    expect(runForkedAgent).toHaveBeenCalledWith(92      expect.objectContaining({93        tools: [94          'read_file',95          'grep_search',96          'glob',97          'list_directory',98          'run_shell_command',99          'write_file',100          'edit',101        ],102        maxTurns: 5,103        maxTimeMinutes: 2,104      }),105    );106  });107 108  it('returns empty touchedTopics when agent touches no files', async () => {109    vi.mocked(runForkedAgent).mockResolvedValue({110      status: 'completed',111      finalText: '',112      filesTouched: [],113      filesWritten: [],114    });115 116    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');117    expect(result).toEqual({118      touchedTopics: [],119      touchedProjectScope: false,120      touchedUserScope: false,121      hasToolActivity: false,122      systemMessage: undefined,123    });124  });125 126  it('uses a scoped config that allows shell and denies outside writes', async () => {127    vi.mocked(runForkedAgent).mockResolvedValue({128      status: 'completed',129      finalText: '',130      filesTouched: [],131    });132 133    await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');134 135    const call = vi.mocked(runForkedAgent).mock.calls[0]?.[0];136    const permissionManager = call?.config.getPermissionManager?.();137    expect(permissionManager).toBeDefined();138    expect(await permissionManager!.isToolEnabled(ToolNames.SHELL)).toBe(true);139    expect(140      permissionManager!.findMatchingDenyRule({141        toolName: ToolNames.WRITE_FILE,142        filePath: '/tmp/outside.md',143      }),144    ).toBe(145      'ManagedAutoMemory(write_file: only within /tmp/user-memory or /tmp/auto-memory)',146    );147    expect(148      await permissionManager!.evaluate({149        toolName: ToolNames.WRITE_FILE,150        filePath: '/tmp/outside.md',151      }),152    ).toBe('deny');153  });154 155  it('throws when getCacheSafeParams returns null', async () => {156    vi.mocked(getCacheSafeParams).mockReturnValue(null);157    await expect(158      runAutoMemoryExtractionByAgent(mockConfig, '/tmp'),159    ).rejects.toThrow('no cache-safe params');160  });161 162  it('throws when the agent fails to complete', async () => {163    vi.mocked(runForkedAgent).mockResolvedValue({164      status: 'failed',165      terminateReason: 'timeout',166      filesTouched: [],167    });168 169    await expect(170      runAutoMemoryExtractionByAgent(mockConfig, '/tmp/project'),171    ).rejects.toThrow('timeout');172  });173 174  it('ignores non-memory file paths in filesTouched', async () => {175    vi.mocked(runForkedAgent).mockResolvedValue({176      status: 'completed',177      finalText: '',178      filesTouched: [179        '/tmp/auto-memory/project/arch.md',180        '/tmp/auto-memory/reference/api.md',181        '/tmp/some/other/file.ts',182      ],183      filesWritten: [184        '/tmp/auto-memory/project/arch.md',185        '/tmp/auto-memory/reference/api.md',186        '/tmp/some/other/file.ts',187      ],188    });189 190    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');191    expect(result.touchedTopics).toEqual(192      expect.arrayContaining(['project', 'reference']),193    );194    expect(result.touchedTopics).not.toContain('user');195    expect(result.touchedProjectScope).toBe(true);196    expect(result.touchedUserScope).toBe(false);197  });198 199  it('attributes user-rooted writes to the user scope (not project)', async () => {200    vi.mocked(runForkedAgent).mockResolvedValue({201      status: 'completed',202      finalText: '',203      filesTouched: [204        '/tmp/user-memory/user/role.md',205        '/tmp/user-memory/feedback/terse.md',206      ],207      filesWritten: [208        '/tmp/user-memory/user/role.md',209        '/tmp/user-memory/feedback/terse.md',210      ],211    });212 213    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');214    expect(result.touchedTopics).toEqual(215      expect.arrayContaining(['user', 'feedback']),216    );217    expect(result.touchedUserScope).toBe(true);218    expect(result.touchedProjectScope).toBe(false);219  });220 221  it('classifies file paths when the root is backslash-native (Windows) but agent reports forward slashes', async () => {222    // On Windows the roots returned by getAutoMemoryRoot/getUserAutoMemoryRoot223    // are backslash-separated (`C:\Users\foo\...\memory`). The model's tool224    // calls (and the writes the agent reports as `filesTouched`) commonly225    // come back forward-slash-normalized. The classification must succeed in226    // that case — otherwise user-scope writes silently fail to rebuild the227    // index on Windows.228    //229    // sticky mockReturnValue (not Once) — the production code calls each230    // helper twice per extraction (prompt builder + touched-topics231    // classifier) so a Once-mock only covers the first call. Restored232    // below to keep subsequent tests on the suite's POSIX defaults.233    vi.mocked(getAutoMemoryRoot).mockReturnValue(234      'C:\\Users\\foo\\.qwen\\projects\\proj\\memory',235    );236    vi.mocked(getUserAutoMemoryRoot).mockReturnValue(237      'C:\\Users\\foo\\.qwen\\memories',238    );239    vi.mocked(runForkedAgent).mockResolvedValue({240      status: 'completed',241      finalText: '',242      filesTouched: [243        'C:/Users/foo/.qwen/projects/proj/memory/project/release.md',244        'C:/Users/foo/.qwen/memories/user/role.md',245      ],246      filesWritten: [247        'C:/Users/foo/.qwen/projects/proj/memory/project/release.md',248        'C:/Users/foo/.qwen/memories/user/role.md',249      ],250    });251 252    try {253      const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');254      expect(result.touchedTopics).toEqual(255        expect.arrayContaining(['project', 'user']),256      );257      expect(result.touchedProjectScope).toBe(true);258      expect(result.touchedUserScope).toBe(true);259    } finally {260      vi.mocked(getAutoMemoryRoot).mockReturnValue('/tmp/auto-memory');261      vi.mocked(getUserAutoMemoryRoot).mockReturnValue('/tmp/user-memory');262    }263  });264 265  it('classifies file paths regardless of which separator the agent reported', async () => {266    // Roots come back from the mocked getAutoMemoryRoot/getUserAutoMemoryRoot267    // as POSIX paths (`/tmp/...`). The agent's filesTouched may use either268    // separator on Windows hosts — the check must accept both.269    vi.mocked(runForkedAgent).mockResolvedValue({270      status: 'completed',271      finalText: '',272      filesTouched: [273        '/tmp/auto-memory\\project\\arch.md',274        '/tmp/user-memory\\user\\role.md',275      ],276      filesWritten: [277        '/tmp/auto-memory\\project\\arch.md',278        '/tmp/user-memory\\user\\role.md',279      ],280    });281 282    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');283    expect(result.touchedTopics).toEqual(284      expect.arrayContaining(['project', 'user']),285    );286    expect(result.touchedProjectScope).toBe(true);287    expect(result.touchedUserScope).toBe(true);288  });289 290  it('rejects sibling directories that share a root prefix (no startsWith collision)', async () => {291    // getAutoMemoryRoot mocked → /tmp/auto-memory.292    // A path inside /tmp/auto-memory-other/ shares the string prefix but is293    // a different directory entirely; the trailing-separator guard must keep294    // it out of both scopes.295    vi.mocked(runForkedAgent).mockResolvedValue({296      status: 'completed',297      finalText: '',298      filesTouched: [299        '/tmp/auto-memory-other/user/x.md',300        '/tmp/user-memory-backup/user/y.md',301      ],302    });303 304    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');305    expect(result.touchedTopics).toEqual([]);306    expect(result.touchedProjectScope).toBe(false);307    expect(result.touchedUserScope).toBe(false);308  });309 310  it('reports both scopes when the agent writes to both roots in one run', async () => {311    vi.mocked(runForkedAgent).mockResolvedValue({312      status: 'completed',313      finalText: '',314      filesTouched: [315        '/tmp/user-memory/user/role.md',316        '/tmp/auto-memory/project/release.md',317      ],318      filesWritten: [319        '/tmp/user-memory/user/role.md',320        '/tmp/auto-memory/project/release.md',321      ],322    });323 324    const result = await runAutoMemoryExtractionByAgent(mockConfig, '/tmp');325    expect(result.touchedTopics).toEqual(326      expect.arrayContaining(['user', 'project']),327    );328    expect(result.touchedProjectScope).toBe(true);329    expect(result.touchedUserScope).toBe(true);330  });331});332 
basant307/AI_Governance_Project · CoolFace