CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
channel-memory.test.ts279 linesDownload Raw Back to memory
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as fs from 'node:fs';8import * as os from 'node:os';9import * as path from 'node:path';10import lockfile from 'proper-lockfile';11import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';12import {13  appendChannelMemory,14  CHANNEL_MEMORY_FILE_NAME,15  clearChannelMemory,16  getChannelMemoryFilePath,17  MAX_CHANNEL_MEMORY_BYTES,18  readChannelMemory,19  type ChannelMemoryTarget,20} from './channel-memory.js';21 22describe('channel memory', () => {23  const originalQwenHome = process.env['QWEN_HOME'];24  let qwenHome: string;25 26  beforeEach(() => {27    qwenHome = fs.mkdtempSync(path.join(os.tmpdir(), 'qwen-channel-memory-'));28    process.env['QWEN_HOME'] = qwenHome;29  });30 31  afterEach(() => {32    if (originalQwenHome === undefined) {33      delete process.env['QWEN_HOME'];34    } else {35      process.env['QWEN_HOME'] = originalQwenHome;36    }37    fs.rmSync(qwenHome, { recursive: true, force: true });38  });39 40  it('returns a path under QWEN_HOME ending with CHANNEL.md', () => {41    const filePath = getChannelMemoryFilePath({42      channelName: 'prod',43      chatId: 'chat-1',44    });45 46    expect(filePath.startsWith(qwenHome + path.sep)).toBe(true);47    expect(filePath.endsWith(path.join('', CHANNEL_MEMORY_FILE_NAME))).toBe(48      true,49    );50  });51 52  it('keeps channel names and chat/thread identifiers safe', () => {53    const filePath = getChannelMemoryFilePath({54      channelName: '../prod/channel',55      chatId: 'raw-chat-id',56      threadId: 'raw-thread-id',57    });58    const relativePath = path.relative(qwenHome, filePath);59 60    expect(relativePath.split(path.sep)).not.toContain('..');61    expect(filePath).not.toContain('raw-chat-id');62    expect(filePath).not.toContain('raw-thread-id');63  });64 65  it('keeps a readable channel-name slug in the path', () => {66    const filePath = getChannelMemoryFilePath({67      channelName: 'team..bot',68      chatId: 'chat-1',69    });70    const relativeSegments = path.relative(qwenHome, filePath).split(path.sep);71 72    expect(relativeSegments[2]).toMatch(/^team\.\.bot-[a-f0-9]{16}$/u);73  });74 75  it.each(['.', '..'])(76    'does not use exact %s as the channel directory segment',77    (channelName) => {78      const filePath = getChannelMemoryFilePath({79        channelName,80        chatId: 'chat-1',81      });82      const relativePath = path.relative(qwenHome, filePath);83      const relativeSegments = relativePath.split(path.sep);84 85      expect(filePath.startsWith(qwenHome + path.sep)).toBe(true);86      expect(relativeSegments).not.toContain('.');87      expect(relativeSegments).not.toContain('..');88      expect(relativeSegments[0]).toBe('channels');89      expect(relativeSegments[1]).toBe('memory');90      expect(relativeSegments[2]).toMatch(/^[._]+-[a-f0-9]{16}$/u);91    },92  );93 94  it('uses different paths for colliding sanitized channel names', () => {95    const first = getChannelMemoryFilePath({96      channelName: 'ops/alerts',97      chatId: 'chat-1',98    });99    const second = getChannelMemoryFilePath({100      channelName: 'ops alerts',101      chatId: 'chat-1',102    });103 104    expect(first).not.toBe(second);105  });106 107  it('uses different paths for different thread ids', () => {108    const target: ChannelMemoryTarget = {109      channelName: 'prod',110      chatId: 'chat-1',111    };112 113    expect(114      getChannelMemoryFilePath({ ...target, threadId: 'thread-1' }),115    ).not.toBe(getChannelMemoryFilePath({ ...target, threadId: 'thread-2' }));116  });117 118  it('appends entries and reads the exact content', async () => {119    const target: ChannelMemoryTarget = {120      channelName: 'prod',121      chatId: 'chat-1',122    };123 124    await appendChannelMemory(target, 'Use staging cluster by default.');125    await appendChannelMemory(target, 'Ask before running deploy commands.');126 127    await expect(readChannelMemory(target)).resolves.toBe(128      'Use staging cluster by default.\nAsk before running deploy commands.\n',129    );130  });131 132  it('does not create memory for whitespace-only appends', async () => {133    const target: ChannelMemoryTarget = {134      channelName: 'prod',135      chatId: 'chat-1',136    };137 138    const result = await appendChannelMemory(target, ' \n\t ');139 140    expect(result).toEqual({141      changed: false,142      filePath: getChannelMemoryFilePath(target),143    });144    await expect(readChannelMemory(target)).resolves.toBe('');145  });146 147  it('clears memory when present', async () => {148    const target: ChannelMemoryTarget = {149      channelName: 'prod',150      chatId: 'chat-1',151    };152 153    await appendChannelMemory(target, 'Use staging cluster by default.');154    await expect(clearChannelMemory(target)).resolves.toEqual({155      changed: true,156      filePath: getChannelMemoryFilePath(target),157    });158    await expect(readChannelMemory(target)).resolves.toBe('');159  });160 161  it('reports no change when clearing missing memory', async () => {162    const target: ChannelMemoryTarget = {163      channelName: 'prod',164      chatId: 'chat-1',165    };166 167    await expect(clearChannelMemory(target)).resolves.toEqual({168      changed: false,169      filePath: getChannelMemoryFilePath(target),170    });171  });172 173  it('rejects writes over the maximum size', async () => {174    await expect(175      appendChannelMemory(176        { channelName: 'prod', chatId: 'chat-1' },177        'a'.repeat(MAX_CHANNEL_MEMORY_BYTES),178      ),179    ).rejects.toThrow('Channel memory exceeds maximum size');180  });181 182  it('continues appends after a rejected append', async () => {183    const target: ChannelMemoryTarget = {184      channelName: 'prod',185      chatId: 'chat-1',186    };187 188    await expect(189      appendChannelMemory(target, 'a'.repeat(MAX_CHANNEL_MEMORY_BYTES)),190    ).rejects.toThrow('Channel memory exceeds maximum size');191    await appendChannelMemory(target, 'after failure');192 193    await expect(readChannelMemory(target)).resolves.toBe('after failure\n');194  });195 196  it('retries append when the file disappears before locking', async () => {197    const target: ChannelMemoryTarget = {198      channelName: 'prod',199      chatId: 'chat-1',200    };201    const filePath = getChannelMemoryFilePath(target);202    const realLock = lockfile.lock.bind(lockfile);203    let deletedBeforeLock = false;204    const lockSpy = vi205      .spyOn(lockfile, 'lock')206      .mockImplementation(async (targetPath, options) => {207        if (!deletedBeforeLock && targetPath === filePath) {208          deletedBeforeLock = true;209          fs.rmSync(filePath, { force: true });210          throw Object.assign(new Error('missing'), { code: 'ENOENT' });211        }212        return realLock(targetPath, options);213      });214 215    try {216      await expect(appendChannelMemory(target, 'after clear')).resolves.toEqual(217        {218          changed: true,219          filePath,220        },221      );222      await expect(readChannelMemory(target)).resolves.toBe('after clear\n');223      expect(lockSpy).toHaveBeenCalledTimes(2);224    } finally {225      lockSpy.mockRestore();226    }227  });228 229  it('keeps concurrent appends within the maximum size', async () => {230    const target: ChannelMemoryTarget = {231      channelName: 'prod',232      chatId: 'chat-1',233    };234    const firstEntry = 'a'.repeat(MAX_CHANNEL_MEMORY_BYTES - 3);235    await appendChannelMemory(target, firstEntry);236 237    const results = await Promise.allSettled([238      appendChannelMemory(target, 'b'),239      appendChannelMemory(target, 'c'),240    ]);241 242    expect(243      results.filter((result) => result.status === 'fulfilled'),244    ).toHaveLength(1);245    expect(246      results.filter((result) => result.status === 'rejected'),247    ).toHaveLength(1);248    expect(249      fs.statSync(getChannelMemoryFilePath(target)).size,250    ).toBeLessThanOrEqual(MAX_CHANNEL_MEMORY_BYTES);251  });252 253  it('serializes clear after pending appends', async () => {254    const target: ChannelMemoryTarget = {255      channelName: 'prod',256      chatId: 'chat-1',257    };258 259    const appends = Array.from({ length: 20 }, (_, index) =>260      appendChannelMemory(target, `entry ${index}`),261    );262    await Promise.all([...appends, clearChannelMemory(target)]);263 264    await expect(readChannelMemory(target)).resolves.toBe('');265  });266 267  it('reads oversized existing memory as empty', async () => {268    const target: ChannelMemoryTarget = {269      channelName: 'prod',270      chatId: 'chat-1',271    };272    const filePath = getChannelMemoryFilePath(target);273    fs.mkdirSync(path.dirname(filePath), { recursive: true });274    fs.writeFileSync(filePath, Buffer.alloc(MAX_CHANNEL_MEMORY_BYTES + 1));275 276    await expect(readChannelMemory(target)).resolves.toBe('');277  });278});279 
basant307/AI_Governance_Project · CoolFace