const path = require('path') const fs = require('fs-extra') const os = require('os') const HelloCommand = require('../../lib/core/pouch/commands/HelloCommand') describe('用户角色发现机制 集成测试', () => { let tempDir let projectDir let helloCommand beforeEach(async () => { // 创建临时项目目录 tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'user-role-discovery-')) projectDir = path.join(tempDir, 'test-project') // 创建完整的项目结构 await fs.ensureDir(path.join(projectDir, 'prompt', 'domain')) await fs.ensureDir(path.join(projectDir, '.promptx', 'user-roles')) helloCommand = new HelloCommand() }) afterEach(async () => { if (tempDir) { await fs.remove(tempDir) } if (helloCommand.roleRegistry) { helloCommand.roleRegistry = null } }) describe('用户角色路径扫描', () => { test('应该能扫描 .promptx/user-roles 目录', async () => { // 创建用户自定义角色 const userRoleDir = path.join(projectDir, '.promptx', 'user-roles', 'custom-analyst') await fs.ensureDir(userRoleDir) const userRoleContent = ` # 数据分析思维 我是一个专注于数据洞察的分析师,善于从复杂数据中发现业务价值。 # 分析原则 - 数据驱动决策 - 业务价值导向 - 简洁清晰表达 # 专业技能 - 统计分析方法 - 数据可视化技能 - 业务理解能力 ` await fs.writeFile( path.join(userRoleDir, 'custom-analyst.role.md'), userRoleContent ) // 这个测试假设我们已经实现了用户角色发现功能 // 实际实现时,discoverLocalRoles会被扩展以支持用户角色路径 // 验证文件创建成功 expect(await fs.pathExists(path.join(userRoleDir, 'custom-analyst.role.md'))).toBe(true) }) test('应该同时支持系统角色和用户角色', async () => { // 创建系统角色 const systemRoleDir = path.join(projectDir, 'prompt', 'domain', 'assistant') await fs.ensureDir(systemRoleDir) await fs.writeFile( path.join(systemRoleDir, 'assistant.role.md'), ` 系统助手思维 ` ) // 创建用户角色 const userRoleDir = path.join(projectDir, '.promptx', 'user-roles', 'my-role') await fs.ensureDir(userRoleDir) await fs.writeFile( path.join(userRoleDir, 'my-role.role.md'), ` 用户自定义思维 ` ) jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => { return class MockPackageProtocol { async getPackageRoot() { return projectDir } } }) delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')] const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand') const mockedCommand = new MockedHelloCommand() // 模拟双路径扫描实现 mockedCommand.discoverLocalRoles = async function() { const PackageProtocol = require('../../lib/core/resource/protocols/PackageProtocol') const packageProtocol = new PackageProtocol() const glob = require('glob') const discoveredRoles = {} try { const packageRoot = await packageProtocol.getPackageRoot() // 扫描路径:系统角色 + 用户角色 const scanPaths = [ path.join(packageRoot, 'prompt', 'domain'), // 系统角色 path.join(packageRoot, '.promptx', 'user-roles') // 用户角色 ] for (const scanPath of scanPaths) { if (await fs.pathExists(scanPath)) { const domains = await fs.readdir(scanPath) for (const domain of domains) { const domainDir = path.join(scanPath, domain) const stat = await fs.stat(domainDir) if (stat.isDirectory()) { const roleFile = path.join(domainDir, `${domain}.role.md`) if (await fs.pathExists(roleFile)) { const content = await fs.readFile(roleFile, 'utf-8') const relativePath = path.relative(packageRoot, roleFile) let name = `🎭 ${domain}` let description = '本地发现的角色' let source = 'local-discovery' // 区分系统角色和用户角色 if (scanPath.includes('.promptx')) { source = 'user-generated' description = '用户自定义角色' } const nameMatch = content.match(/name:\s*(.+?)(?:\n|$)/i) if (nameMatch) { name = nameMatch[1].trim() } const descMatch = content.match(/description:\s*(.+?)(?:\n|$)/i) if (descMatch) { description = descMatch[1].trim() } discoveredRoles[domain] = { file: scanPath.includes('.promptx') ? `@project://${relativePath}` : `@package://${relativePath}`, name, description, source } } } } } } return discoveredRoles } catch (error) { console.warn('角色发现失败:', error.message) return {} } } const discoveredRoles = await mockedCommand.discoverLocalRoles() // 验证同时发现了系统角色和用户角色 expect(discoveredRoles).toHaveProperty('assistant') expect(discoveredRoles).toHaveProperty('my-role') expect(discoveredRoles.assistant.source).toBe('local-discovery') expect(discoveredRoles.assistant.file).toContain('@package://') expect(discoveredRoles['my-role'].source).toBe('user-generated') expect(discoveredRoles['my-role'].file).toContain('@project://') jest.unmock('../../lib/core/resource/protocols/PackageProtocol') }) }) describe('DPML格式元数据提取', () => { test('应该能从DPML格式中提取元数据', async () => { const userRoleDir = path.join(projectDir, '.promptx', 'user-roles', 'dpml-role') await fs.ensureDir(userRoleDir) // DPML格式的角色文件(根据文档设计的格式) const dpmlRoleContent = ` # 数据分析师思维模式 ## 核心思维特征 - **数据敏感性思维**:善于从数字中发现故事和趋势模式 - **逻辑分析思维**:系统性地分解复杂数据问题,追求因果关系 - **结果导向思维**:专注于为业务决策提供可行洞察和建议 # 数据分析师行为原则 ## 核心工作原则 - **数据驱动决策**:所有分析建议必须有可靠数据支撑 - **简洁清晰表达**:复杂分析结果要用简单易懂的方式呈现 - **业务价值优先**:分析要紧密围绕业务目标和价值创造 # 数据分析专业知识体系 ## 数据处理技能 - **数据清洗方法**:缺失值处理、异常值识别、数据标准化 - **数据整合技巧**:多源数据合并、关联分析、数据建模 - **质量控制流程**:数据校验、一致性检查、完整性验证 ## 分析方法论 - **描述性分析**:趋势分析、对比分析、分布分析 - **诊断性分析**:钻取分析、根因分析、相关性分析 ` await fs.writeFile( path.join(userRoleDir, 'dpml-role.role.md'), dpmlRoleContent ) jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => { return class MockPackageProtocol { async getPackageRoot() { return projectDir } } }) delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')] const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand') const mockedCommand = new MockedHelloCommand() // 实现DPML元数据提取逻辑(这是我们要实现的功能) function extractDPMLMetadata(content, roleId) { // 从标签中提取角色名称 const personalityMatch = content.match(/]*>([\s\S]*?)<\/personality>/i) const roleNameFromPersonality = personalityMatch ? personalityMatch[1].split('\n')[0].replace(/^#\s*/, '').trim() : null // 从标签中提取专业能力描述 const knowledgeMatch = content.match(/]*>([\s\S]*?)<\/knowledge>/i) const roleDescription = knowledgeMatch ? knowledgeMatch[1].split('\n').slice(0, 3).join(' ').replace(/[#\-\*]/g, '').trim() : null return { file: `@project://.promptx/user-roles/${roleId}/${roleId}.role.md`, name: roleNameFromPersonality || `🎭 ${roleId}`, description: roleDescription || '用户自定义DPML角色', source: 'user-generated', format: 'dpml' } } mockedCommand.discoverLocalRoles = async function() { const PackageProtocol = require('../../lib/core/resource/protocols/PackageProtocol') const packageProtocol = new PackageProtocol() const discoveredRoles = {} try { const packageRoot = await packageProtocol.getPackageRoot() const userRolesPath = path.join(packageRoot, '.promptx', 'user-roles') if (await fs.pathExists(userRolesPath)) { const userRoleDirs = await fs.readdir(userRolesPath) for (const roleId of userRoleDirs) { const roleDir = path.join(userRolesPath, roleId) const stat = await fs.stat(roleDir) if (stat.isDirectory()) { const roleFile = path.join(roleDir, `${roleId}.role.md`) if (await fs.pathExists(roleFile)) { const content = await fs.readFile(roleFile, 'utf-8') // 使用DPML元数据提取 const roleInfo = extractDPMLMetadata(content, roleId) discoveredRoles[roleId] = roleInfo } } } } return discoveredRoles } catch (error) { console.warn('DPML角色发现失败:', error.message) return {} } } const discoveredRoles = await mockedCommand.discoverLocalRoles() // 验证DPML元数据提取 expect(discoveredRoles).toHaveProperty('dpml-role') expect(discoveredRoles['dpml-role'].name).toBe('数据分析师思维模式') expect(discoveredRoles['dpml-role'].description).toContain('数据分析专业知识体系') expect(discoveredRoles['dpml-role'].format).toBe('dpml') expect(discoveredRoles['dpml-role'].source).toBe('user-generated') jest.unmock('../../lib/core/resource/protocols/PackageProtocol') }) }) describe('错误处理和边界情况', () => { test('应该处理不存在的用户角色目录', async () => { // 只创建系统角色目录,不创建用户角色目录 const systemRoleDir = path.join(projectDir, 'prompt', 'domain', 'assistant') await fs.ensureDir(systemRoleDir) await fs.writeFile( path.join(systemRoleDir, 'assistant.role.md'), `助手` ) jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => { return class MockPackageProtocol { async getPackageRoot() { return projectDir } } }) delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')] const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand') const mockedCommand = new MockedHelloCommand() // 模拟处理不存在目录的逻辑 mockedCommand.discoverLocalRoles = async function() { const PackageProtocol = require('../../lib/core/resource/protocols/PackageProtocol') const packageProtocol = new PackageProtocol() const discoveredRoles = {} try { const packageRoot = await packageProtocol.getPackageRoot() const scanPaths = [ { path: path.join(packageRoot, 'prompt', 'domain'), prefix: '@package://' }, { path: path.join(packageRoot, '.promptx', 'user-roles'), prefix: '@project://' } ] for (const { path: scanPath, prefix } of scanPaths) { if (await fs.pathExists(scanPath)) { const domains = await fs.readdir(scanPath) for (const domain of domains) { const domainDir = path.join(scanPath, domain) const stat = await fs.stat(domainDir) if (stat.isDirectory()) { const roleFile = path.join(domainDir, `${domain}.role.md`) if (await fs.pathExists(roleFile)) { const content = await fs.readFile(roleFile, 'utf-8') const relativePath = path.relative(packageRoot, roleFile) discoveredRoles[domain] = { file: `${prefix}${relativePath}`, name: `🎭 ${domain}`, description: '本地发现的角色', source: prefix.includes('project') ? 'user-generated' : 'local-discovery' } } } } } } return discoveredRoles } catch (error) { return {} } } const discoveredRoles = await mockedCommand.discoverLocalRoles() // 应该只发现系统角色,不会因为用户角色目录不存在而出错 expect(discoveredRoles).toHaveProperty('assistant') expect(Object.keys(discoveredRoles)).toHaveLength(1) jest.unmock('../../lib/core/resource/protocols/PackageProtocol') }) test('应该处理用户角色ID冲突', async () => { // 创建同名的系统角色和用户角色 const systemRoleDir = path.join(projectDir, 'prompt', 'domain', 'analyst') await fs.ensureDir(systemRoleDir) await fs.writeFile( path.join(systemRoleDir, 'analyst.role.md'), ` 系统分析师` ) const userRoleDir = path.join(projectDir, '.promptx', 'user-roles', 'analyst') await fs.ensureDir(userRoleDir) await fs.writeFile( path.join(userRoleDir, 'analyst.role.md'), ` 用户分析师` ) jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => { return class MockPackageProtocol { async getPackageRoot() { return projectDir } } }) delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')] const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand') const mockedCommand = new MockedHelloCommand() // 模拟冲突处理逻辑(用户角色优先) mockedCommand.discoverLocalRoles = async function() { const PackageProtocol = require('../../lib/core/resource/protocols/PackageProtocol') const packageProtocol = new PackageProtocol() const discoveredRoles = {} try { const packageRoot = await packageProtocol.getPackageRoot() // 先扫描系统角色,再扫描用户角色(用户角色会覆盖同名系统角色) const scanPaths = [ { path: path.join(packageRoot, 'prompt', 'domain'), prefix: '@package://', source: 'local-discovery' }, { path: path.join(packageRoot, '.promptx', 'user-roles'), prefix: '@project://', source: 'user-generated' } ] for (const { path: scanPath, prefix, source } of scanPaths) { if (await fs.pathExists(scanPath)) { const domains = await fs.readdir(scanPath) for (const domain of domains) { const domainDir = path.join(scanPath, domain) const stat = await fs.stat(domainDir) if (stat.isDirectory()) { const roleFile = path.join(domainDir, `${domain}.role.md`) if (await fs.pathExists(roleFile)) { const content = await fs.readFile(roleFile, 'utf-8') const relativePath = path.relative(packageRoot, roleFile) let name = `🎭 ${domain}` let description = '本地发现的角色' const nameMatch = content.match(/name:\s*(.+?)(?:\n|$)/i) if (nameMatch) { name = nameMatch[1].trim() } const descMatch = content.match(/description:\s*(.+?)(?:\n|$)/i) if (descMatch) { description = descMatch[1].trim() } // 用户角色会覆盖系统角色 discoveredRoles[domain] = { file: `${prefix}${relativePath}`, name, description, source } } } } } } return discoveredRoles } catch (error) { return {} } } const discoveredRoles = await mockedCommand.discoverLocalRoles() // 验证用户角色优先级更高 expect(discoveredRoles).toHaveProperty('analyst') expect(discoveredRoles.analyst.name).toBe('👤 用户分析师') expect(discoveredRoles.analyst.description).toBe('用户自定义分析师') expect(discoveredRoles.analyst.source).toBe('user-generated') expect(discoveredRoles.analyst.file).toContain('@project://') jest.unmock('../../lib/core/resource/protocols/PackageProtocol') }) }) })