/** * WorkspaceService 测试(v0.7.2 覆盖补齐 —— 此前零测试) * * 锁定工作空间生命周期契约: * 1. initialize 创建目录结构(logs/.metona)与必需文件(SOUL.md/MEMORY.md) * 2. MEMORY.md 模板元数据头 + 5 分区结构 * 3. appendMemory 分区追加语义(含未知 section 兜底) * 4. validateMemoryFormat 自动修正(缺元数据头/缺分区) * 5. updateMemoryTimestamp / reload(外部编辑同步) */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { WorkspaceService } from '../workspace.service'; let wsRoot: string; beforeEach(() => { wsRoot = mkdtempSync(join(tmpdir(), 'metona-ws-')); }); afterEach(() => { try { rmSync(wsRoot, { recursive: true, force: true }); } catch { /* ignore */ } }); describe('WorkspaceService — initialize', () => { it('创建工作空间目录结构 + 两个必需文件 + 自动目录', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); const info = svc.initialize(); expect(info.isValid).toBe(true); expect(info.path).toBe(wsPath); expect(existsSync(join(wsPath, 'SOUL.md'))).toBe(true); expect(existsSync(join(wsPath, 'MEMORY.md'))).toBe(true); expect(existsSync(join(wsPath, 'logs'))).toBe(true); expect(existsSync(join(wsPath, '.metona'))).toBe(true); // 本次启动自动创建的文件计入 missingFiles expect(info.missingFiles).toEqual(['SOUL.md', 'MEMORY.md']); }); it('已有完整文件的工作空间:missingFiles 为空且内容不被覆盖', () => { const wsPath = join(wsRoot, 'ws'); mkdirSync(wsPath, { recursive: true }); writeFileSync(join(wsPath, 'SOUL.md'), '# 自定义人格', 'utf-8'); writeFileSync( join(wsPath, 'MEMORY.md'), '# MEMORY.md\n> 创建时间: x\n> 最后更新: y\n> 工作空间: z\n', 'utf-8', ); const svc = new WorkspaceService(wsPath); const info = svc.initialize(); expect(info.missingFiles).toEqual([]); expect(svc.getFiles().soul).toBe('# 自定义人格'); }); it('MEMORY.md 模板包含元数据头与核心分区', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); const content = readFileSync(join(wsPath, 'MEMORY.md'), 'utf-8'); expect(content).toContain('# MEMORY.md'); expect(content).toContain('> 创建时间:'); expect(content).toContain('> 最后更新:'); expect(content).toContain('> 工作空间:'); for (const section of [ '## 用户偏好', '## 项目上下文', '## 重要决策', '## 待办事项', '## 已知问题', ]) { expect(content).toContain(section); } }); }); describe('WorkspaceService — appendMemory', () => { it('追加到指定 section 末尾(不侵入下一分区)', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); svc.appendMemory('用户偏好', '偏好深色主题'); svc.appendMemory('用户偏好', '偏好简洁回复'); const content = readFileSync(join(wsPath, 'MEMORY.md'), 'utf-8'); const sectionStart = content.indexOf('## 用户偏好'); const sectionEnd = content.indexOf('## 项目上下文'); const section = content.slice(sectionStart, sectionEnd); expect(section).toContain('- 偏好深色主题'); expect(section).toContain('- 偏好简洁回复'); }); it('未知 section 追加到文件末尾并自动创建分区头', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); svc.appendMemory('自定义分区', '条目内容'); const content = readFileSync(join(wsPath, 'MEMORY.md'), 'utf-8'); expect(content).toContain('## 自定义分区'); expect(content).toContain('- 条目内容'); }); }); describe('WorkspaceService — validateMemoryFormat 自动修正', () => { it('缺失元数据头与核心分区被自动补齐,内容不丢失', () => { const wsPath = join(wsRoot, 'ws'); mkdirSync(wsPath, { recursive: true }); writeFileSync(join(wsPath, 'MEMORY.md'), '# MEMORY.md\n- 用户留下的重要内容', 'utf-8'); const svc = new WorkspaceService(wsPath); expect(svc.validateMemoryFormat()).toBe(false); // false = 有修正 const content = readFileSync(join(wsPath, 'MEMORY.md'), 'utf-8'); expect(content).toContain('> 创建时间:'); expect(content).toContain('> 工作空间:'); for (const section of ['## 用户偏好', '## 项目上下文', '## 重要决策']) { expect(content).toContain(section); } // 原内容保留 expect(content).toContain('- 用户留下的重要内容'); }); it('格式完整时返回 true 且不触发写盘', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); expect(svc.validateMemoryFormat()).toBe(true); }); }); describe('WorkspaceService — 时间戳与 reload', () => { it('updateMemoryTimestamp 更新"最后更新"行', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); svc.updateMemoryTimestamp(); const content = svc.getFiles().memory; expect(content).toMatch(/> 最后更新: \d{4}-\d{2}-\d{2}T/); }); it('reload 同步外部编辑(用户手动改文件后)', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); writeFileSync(join(wsPath, 'SOUL.md'), '# 外部修改后的人格', 'utf-8'); const files = svc.reload(); expect(files.soul).toBe('# 外部修改后的人格'); }); it('getFiles 返回副本(外部修改引用不影响内部状态)', () => { const wsPath = join(wsRoot, 'ws'); const svc = new WorkspaceService(wsPath); svc.initialize(); const files = svc.getFiles(); files.soul = 'tampered'; expect(svc.getFiles().soul).not.toBe('tampered'); }); });