Download src/server/__tests__/memory.test.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 7.03 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/__tests__/memory.test.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/__tests__/memory.test.ts
-
curl -L -o memory.test.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/__tests__/memory.test.ts
7.03 kB
| import { afterEach, beforeEach, describe, expect, it } from 'bun:test' | |
| import * as fs from 'node:fs/promises' | |
| import * as os from 'node:os' | |
| import * as path from 'node:path' | |
| import { handleMemoryApi } from '../api/memory.js' | |
| import { sanitizePath } from '../../utils/path.js' | |
| let tmpDir: string | |
| let originalConfigDir: string | undefined | |
| let originalHome: string | undefined | |
| let originalUserProfile: string | undefined | |
| beforeEach(async () => { | |
| tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-memory-api-')) | |
| originalConfigDir = process.env.CLAUDE_CONFIG_DIR | |
| originalHome = process.env.HOME | |
| originalUserProfile = process.env.USERPROFILE | |
| process.env.CLAUDE_CONFIG_DIR = tmpDir | |
| process.env.HOME = tmpDir | |
| process.env.USERPROFILE = tmpDir | |
| }) | |
| afterEach(async () => { | |
| if (originalConfigDir !== undefined) { | |
| process.env.CLAUDE_CONFIG_DIR = originalConfigDir | |
| } else { | |
| delete process.env.CLAUDE_CONFIG_DIR | |
| } | |
| if (originalHome !== undefined) { | |
| process.env.HOME = originalHome | |
| } else { | |
| delete process.env.HOME | |
| } | |
| if (originalUserProfile !== undefined) { | |
| process.env.USERPROFILE = originalUserProfile | |
| } else { | |
| delete process.env.USERPROFILE | |
| } | |
| await fs.rm(tmpDir, { recursive: true, force: true }) | |
| }) | |
| describe('memory API', () => { | |
| it('lists current project memory files with frontmatter metadata', async () => { | |
| const cwd = path.join(tmpDir, 'workspace', 'app') | |
| const projectId = sanitizePath(cwd) | |
| const memoryDir = path.join(tmpDir, 'projects', projectId, 'memory') | |
| await fs.mkdir(path.join(memoryDir, 'notes'), { recursive: true }) | |
| await fs.writeFile( | |
| path.join(memoryDir, 'MEMORY.md'), | |
| [ | |
| '---', | |
| 'type: project', | |
| 'description: Stable project conventions.', | |
| '---', | |
| '', | |
| '# Project Memory', | |
| ].join('\n'), | |
| ) | |
| await fs.writeFile(path.join(memoryDir, 'notes', 'manual.md'), '# Manual') | |
| const projectsRes = await request('GET', `/api/memory/projects?cwd=${encodeURIComponent(cwd)}`) | |
| expect(projectsRes.status).toBe(200) | |
| const projectsBody = await projectsRes.json() as { | |
| projects: Array<{ id: string; isCurrent: boolean; fileCount: number }> | |
| } | |
| expect(projectsBody.projects[0]).toMatchObject({ | |
| id: projectId, | |
| isCurrent: true, | |
| fileCount: 2, | |
| }) | |
| const filesRes = await request('GET', `/api/memory/files?projectId=${encodeURIComponent(projectId)}`) | |
| expect(filesRes.status).toBe(200) | |
| const filesBody = await filesRes.json() as { | |
| files: Array<{ path: string; type?: string; description?: string; isIndex: boolean }> | |
| } | |
| expect(filesBody.files[0]).toMatchObject({ | |
| path: 'MEMORY.md', | |
| type: 'project', | |
| description: 'Stable project conventions.', | |
| isIndex: true, | |
| }) | |
| expect(filesBody.files.some((file) => file.path === 'notes/manual.md')).toBe(true) | |
| }) | |
| it('uses session metadata for project labels when sanitized paths contain non-ascii characters', async () => { | |
| const cwd = path.join(tmpDir, '中文 项目', 'GLM', '5V', 'turbo') | |
| const projectId = sanitizePath(cwd) | |
| const projectDir = path.join(tmpDir, 'projects', projectId) | |
| const memoryDir = path.join(projectDir, 'memory') | |
| await fs.mkdir(memoryDir, { recursive: true }) | |
| await fs.writeFile(path.join(memoryDir, 'MEMORY.md'), '# Project Memory') | |
| await fs.writeFile( | |
| path.join(projectDir, 'session.jsonl'), | |
| JSON.stringify({ | |
| type: 'user', | |
| cwd, | |
| message: { role: 'user', content: 'hello' }, | |
| }) + '\n', | |
| ) | |
| const projectsRes = await request('GET', '/api/memory/projects') | |
| expect(projectsRes.status).toBe(200) | |
| const projectsBody = await projectsRes.json() as { | |
| projects: Array<{ id: string; label: string }> | |
| } | |
| const project = projectsBody.projects.find((item) => item.id === projectId) | |
| expect(project).toMatchObject({ label: cwd }) | |
| }) | |
| it('recovers project labels from existing directories when no session metadata exists', async () => { | |
| const cwd = path.join(tmpDir, '个人自媒体', '314', 'opus4', 'PicTacticAgent') | |
| const projectId = sanitizePath(cwd) | |
| const memoryDir = path.join(tmpDir, 'projects', projectId, 'memory') | |
| await fs.mkdir(cwd, { recursive: true }) | |
| await fs.mkdir(memoryDir, { recursive: true }) | |
| await fs.writeFile(path.join(memoryDir, 'MEMORY.md'), '# Project Memory') | |
| const projectsRes = await request('GET', '/api/memory/projects') | |
| expect(projectsRes.status).toBe(200) | |
| const projectsBody = await projectsRes.json() as { | |
| projects: Array<{ id: string; label: string }> | |
| } | |
| const project = projectsBody.projects.find((item) => item.id === projectId) | |
| expect(project).toMatchObject({ label: cwd }) | |
| }) | |
| it('reads and writes only markdown files inside the project memory directory', async () => { | |
| const projectId = sanitizePath(path.join(tmpDir, 'workspace', 'app')) | |
| const writeRes = await request('PUT', '/api/memory/file', { | |
| projectId, | |
| path: 'notes/project.md', | |
| content: '# Edited Memory\n', | |
| }) | |
| expect(writeRes.status).toBe(200) | |
| const filePath = path.join(tmpDir, 'projects', projectId, 'memory', 'notes', 'project.md') | |
| expect(await fs.readFile(filePath, 'utf-8')).toBe('# Edited Memory\n') | |
| const readRes = await request('GET', `/api/memory/file?projectId=${encodeURIComponent(projectId)}&path=notes%2Fproject.md`) | |
| expect(readRes.status).toBe(200) | |
| const body = await readRes.json() as { file: { path: string; content: string } } | |
| expect(body.file).toMatchObject({ | |
| path: 'notes/project.md', | |
| content: '# Edited Memory\n', | |
| }) | |
| }) | |
| it('rejects traversal and symlink escapes', async () => { | |
| const projectId = sanitizePath(path.join(tmpDir, 'workspace', 'app')) | |
| const memoryDir = path.join(tmpDir, 'projects', projectId, 'memory') | |
| const outsideDir = path.join(tmpDir, 'outside') | |
| await fs.mkdir(memoryDir, { recursive: true }) | |
| await fs.mkdir(outsideDir, { recursive: true }) | |
| await fs.symlink(outsideDir, path.join(memoryDir, 'linked'), 'dir') | |
| const traversalRes = await request('PUT', '/api/memory/file', { | |
| projectId, | |
| path: '../outside.md', | |
| content: 'escape', | |
| }) | |
| expect(traversalRes.status).toBe(400) | |
| const symlinkRes = await request('PUT', '/api/memory/file', { | |
| projectId, | |
| path: 'linked/outside.md', | |
| content: 'escape', | |
| }) | |
| expect(symlinkRes.status).toBe(400) | |
| await expect(fs.readFile(path.join(outsideDir, 'outside.md'), 'utf-8')).rejects.toThrow() | |
| }) | |
| }) | |
| function request(method: string, pathname: string, body?: Record<string, unknown>): Promise<Response> { | |
| const url = new URL(pathname, 'http://localhost:3456') | |
| const init: RequestInit = { method } | |
| if (body) { | |
| init.headers = { 'Content-Type': 'application/json' } | |
| init.body = JSON.stringify(body) | |
| } | |
| return handleMemoryApi( | |
| new Request(url.toString(), init), | |
| url, | |
| url.pathname.split('/').filter(Boolean), | |
| ) | |
| } | |