更新资源管理器和命令逻辑:新增角色创建和生成相关功能,优化资源加载流程,支持用户自定义资源的发现与合并,同时增强错误处理和描述提取逻辑,提升系统的灵活性和用户体验。
This commit is contained in:
@ -85,10 +85,16 @@ ${COMMANDS.HELLO}
|
||||
*/
|
||||
async analyzeRoleDependencies (roleInfo) {
|
||||
try {
|
||||
// 处理文件路径,将@package://前缀替换为实际路径
|
||||
// 处理文件路径,将@package://和@project://前缀替换为实际路径
|
||||
let filePath = roleInfo.file
|
||||
if (filePath.startsWith('@package://')) {
|
||||
filePath = filePath.replace('@package://', '')
|
||||
} else if (filePath.startsWith('@project://')) {
|
||||
// 对于@project://路径,使用当前工作目录作为基础路径
|
||||
const ProjectProtocol = require('../../resource/protocols/ProjectProtocol')
|
||||
const projectProtocol = new ProjectProtocol()
|
||||
const relativePath = filePath.replace('@project://', '')
|
||||
filePath = path.join(process.cwd(), relativePath)
|
||||
}
|
||||
|
||||
// 读取角色文件内容
|
||||
|
||||
@ -26,23 +26,25 @@ class HelloCommand extends BasePouchCommand {
|
||||
}
|
||||
|
||||
try {
|
||||
// 从ResourceManager获取统一注册表
|
||||
// 使用新的ResourceManager架构
|
||||
const ResourceManager = require('../../resource/resourceManager')
|
||||
const resourceManager = new ResourceManager()
|
||||
await resourceManager.initialize() // 确保初始化完成
|
||||
|
||||
let registeredRoles = {}
|
||||
if (resourceManager.registry && resourceManager.registry.protocols && resourceManager.registry.protocols.role && resourceManager.registry.protocols.role.registry) {
|
||||
registeredRoles = resourceManager.registry.protocols.role.registry
|
||||
}
|
||||
|
||||
// 动态发现本地角色并合并
|
||||
const discoveredRoles = await this.discoverLocalRoles()
|
||||
|
||||
// 合并注册表中的角色和动态发现的角色
|
||||
this.roleRegistry = {
|
||||
...registeredRoles,
|
||||
...discoveredRoles
|
||||
// 加载统一注册表(包含系统+用户资源)
|
||||
const unifiedRegistry = await resourceManager.loadUnifiedRegistry()
|
||||
|
||||
// 提取角色数据
|
||||
const roleData = unifiedRegistry.role || {}
|
||||
|
||||
// 转换为HelloCommand期望的格式
|
||||
this.roleRegistry = {}
|
||||
for (const [roleId, roleInfo] of Object.entries(roleData)) {
|
||||
this.roleRegistry[roleId] = {
|
||||
file: roleInfo.file,
|
||||
name: roleInfo.name || roleId,
|
||||
description: this.extractDescription(roleInfo) || `${roleInfo.name || roleId}专业角色`,
|
||||
source: roleInfo.source || 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有任何角色,使用基础角色
|
||||
@ -51,31 +53,21 @@ class HelloCommand extends BasePouchCommand {
|
||||
assistant: {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持'
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'fallback'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('角色注册表加载失败,尝试动态发现:', error.message)
|
||||
console.warn('角色注册表加载失败,使用基础角色:', error.message)
|
||||
|
||||
// fallback到动态发现
|
||||
try {
|
||||
const discoveredRoles = await this.discoverLocalRoles()
|
||||
this.roleRegistry = Object.keys(discoveredRoles).length > 0 ? discoveredRoles : {
|
||||
assistant: {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持'
|
||||
}
|
||||
}
|
||||
} catch (discoveryError) {
|
||||
console.warn('动态角色发现也失败了:', discoveryError.message)
|
||||
this.roleRegistry = {
|
||||
assistant: {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持'
|
||||
}
|
||||
// 使用基础角色作为fallback
|
||||
this.roleRegistry = {
|
||||
assistant: {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'fallback'
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,6 +75,21 @@ class HelloCommand extends BasePouchCommand {
|
||||
return this.roleRegistry
|
||||
}
|
||||
|
||||
/**
|
||||
* 从角色信息中提取描述
|
||||
* @param {Object} roleInfo - 角色信息对象
|
||||
* @returns {string} 角色描述
|
||||
*/
|
||||
extractDescription(roleInfo) {
|
||||
// 尝试从不同字段提取描述
|
||||
if (roleInfo.description) {
|
||||
return roleInfo.description
|
||||
}
|
||||
|
||||
// 如果有更多元数据,可以在这里扩展提取逻辑
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有角色列表(转换为数组格式)
|
||||
*/
|
||||
@ -92,10 +99,29 @@ class HelloCommand extends BasePouchCommand {
|
||||
id,
|
||||
name: roleInfo.name,
|
||||
description: roleInfo.description,
|
||||
file: roleInfo.file
|
||||
file: roleInfo.file,
|
||||
source: roleInfo.source
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取来源标签
|
||||
* @param {string} source - 资源来源
|
||||
* @returns {string} 来源标签
|
||||
*/
|
||||
getSourceLabel(source) {
|
||||
switch (source) {
|
||||
case 'user-generated':
|
||||
return '(用户生成)'
|
||||
case 'system':
|
||||
return '(系统角色)'
|
||||
case 'fallback':
|
||||
return '(默认角色)'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async getContent (args) {
|
||||
await this.loadRoleRegistry()
|
||||
const allRoles = await this.getAllRoles()
|
||||
@ -111,7 +137,8 @@ class HelloCommand extends BasePouchCommand {
|
||||
|
||||
// 清楚显示角色ID和激活命令
|
||||
allRoles.forEach((role, index) => {
|
||||
content += `### ${index + 1}. ${role.name}
|
||||
const sourceLabel = this.getSourceLabel(role.source)
|
||||
content += `### ${index + 1}. ${role.name} ${sourceLabel}
|
||||
**角色ID**: \`${role.id}\`
|
||||
**专业能力**: ${role.description}
|
||||
**激活命令**: \`${buildCommand.action(role.id)}\`
|
||||
|
||||
@ -11,6 +11,16 @@ const ProjectProtocol = require('./protocols/ProjectProtocol')
|
||||
const UserProtocol = require('./protocols/UserProtocol')
|
||||
const PromptProtocol = require('./protocols/PromptProtocol')
|
||||
|
||||
// 常量定义
|
||||
const USER_RESOURCE_DIR = '.promptx'
|
||||
const RESOURCE_DOMAIN_PATH = ['resource', 'domain']
|
||||
const SUPPORTED_RESOURCE_TYPES = ['role', 'thought', 'execution']
|
||||
const DPML_TAGS = {
|
||||
role: { start: '<role>', end: '</role>' },
|
||||
thought: { start: '<thought>', end: '</thought>' },
|
||||
execution: { start: '<execution>', end: '</execution>' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源管理器 - 统一管理各种协议的资源加载
|
||||
*/
|
||||
@ -41,17 +51,95 @@ class ResourceManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载统一资源注册表
|
||||
* 加载统一资源注册表(合并系统和用户资源)
|
||||
*/
|
||||
async loadUnifiedRegistry () {
|
||||
const registryPath = path.resolve(__dirname, '../../../resource.registry.json')
|
||||
try {
|
||||
// 加载系统资源注册表
|
||||
const registryPath = path.resolve(__dirname, '../../../resource.registry.json')
|
||||
|
||||
if (!await fs.pathExists(registryPath)) {
|
||||
throw new Error(`统一资源注册表文件不存在: ${registryPath}`)
|
||||
if (!await fs.pathExists(registryPath)) {
|
||||
throw new Error(`统一资源注册表文件不存在: ${registryPath}`)
|
||||
}
|
||||
|
||||
const systemRegistry = await fs.readJSON(registryPath)
|
||||
|
||||
// 发现用户资源
|
||||
const userResources = await this.discoverUserResources()
|
||||
|
||||
// 从系统注册表中提取资源数据
|
||||
const extractedSystemResources = {}
|
||||
for (const resourceType of SUPPORTED_RESOURCE_TYPES) {
|
||||
const protocolConfig = systemRegistry.protocols[resourceType]
|
||||
if (protocolConfig && protocolConfig.registry) {
|
||||
extractedSystemResources[resourceType] = protocolConfig.registry
|
||||
}
|
||||
}
|
||||
|
||||
// 合并资源,用户资源覆盖系统资源
|
||||
const mergedRegistry = { ...systemRegistry }
|
||||
|
||||
// 合并各种资源类型
|
||||
for (const resourceType of SUPPORTED_RESOURCE_TYPES) {
|
||||
// 确保有基础结构
|
||||
if (!mergedRegistry[resourceType]) {
|
||||
mergedRegistry[resourceType] = {}
|
||||
}
|
||||
|
||||
// 先添加系统资源
|
||||
if (extractedSystemResources[resourceType]) {
|
||||
if (!mergedRegistry[resourceType]) mergedRegistry[resourceType] = {}
|
||||
for (const [id, resourceInfo] of Object.entries(extractedSystemResources[resourceType])) {
|
||||
mergedRegistry[resourceType][id] = {
|
||||
...resourceInfo,
|
||||
source: 'system'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 再添加用户资源(覆盖同名的系统资源)
|
||||
if (userResources[resourceType]) {
|
||||
for (const [id, resourceInfo] of Object.entries(userResources[resourceType])) {
|
||||
let filePath = resourceInfo.file || resourceInfo
|
||||
|
||||
// 将绝对路径转换为@project://相对路径格式
|
||||
if (path.isAbsolute(filePath)) {
|
||||
// 简单的路径转换:去掉项目根目录前缀
|
||||
const projectRoot = process.cwd()
|
||||
if (filePath.startsWith(projectRoot)) {
|
||||
const relativePath = path.relative(projectRoot, filePath)
|
||||
filePath = `@project://${relativePath}`
|
||||
}
|
||||
}
|
||||
|
||||
// 对于role资源类型,需要保持对象格式以包含name和description
|
||||
if (resourceType === 'role') {
|
||||
mergedRegistry[resourceType][id] = {
|
||||
file: filePath,
|
||||
name: resourceInfo.name || id,
|
||||
description: resourceInfo.description || `${resourceInfo.name || id}专业角色`,
|
||||
source: 'user-generated',
|
||||
format: resourceInfo.format,
|
||||
type: resourceInfo.type
|
||||
}
|
||||
} else {
|
||||
// 对于thought和execution,协议处理器期望的是文件路径字符串
|
||||
if (!mergedRegistry[resourceType]) mergedRegistry[resourceType] = {}
|
||||
mergedRegistry[resourceType][id] = filePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.registry = mergedRegistry
|
||||
return mergedRegistry
|
||||
} catch (error) {
|
||||
// 如果加载失败,至少返回一个基本结构
|
||||
logger.warn(`加载统一注册表失败: ${error.message}`)
|
||||
const fallbackRegistry = { role: {} }
|
||||
this.registry = fallbackRegistry
|
||||
return fallbackRegistry
|
||||
}
|
||||
|
||||
const registryContent = await fs.readJSON(registryPath)
|
||||
this.registry = registryContent
|
||||
}
|
||||
|
||||
/**
|
||||
@ -204,6 +292,178 @@ class ResourceManager {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 发现用户资源
|
||||
* @returns {Promise<Object>} 用户资源注册表
|
||||
*/
|
||||
async discoverUserResources() {
|
||||
try {
|
||||
const PackageProtocol = require('./protocols/PackageProtocol')
|
||||
const packageProtocol = new PackageProtocol()
|
||||
const packageRoot = await packageProtocol.getPackageRoot()
|
||||
|
||||
const userResourcePath = path.join(packageRoot, USER_RESOURCE_DIR, ...RESOURCE_DOMAIN_PATH)
|
||||
|
||||
// 检查用户资源目录是否存在
|
||||
if (!await fs.pathExists(userResourcePath)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return await this.scanResourceDirectory(userResourcePath)
|
||||
} catch (error) {
|
||||
// 出错时返回空对象,不抛出异常
|
||||
logger.warn(`用户资源发现失败: ${error.message}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描资源目录
|
||||
* @param {string} basePath - 基础路径
|
||||
* @returns {Promise<Object>} 发现的资源
|
||||
*/
|
||||
async scanResourceDirectory(basePath) {
|
||||
const resources = {}
|
||||
|
||||
try {
|
||||
const directories = await fs.readdir(basePath)
|
||||
|
||||
for (const roleDir of directories) {
|
||||
const rolePath = path.join(basePath, roleDir)
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(rolePath)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// 扫描角色文件
|
||||
await this.scanRoleResources(rolePath, roleDir, resources)
|
||||
|
||||
// 扫描其他资源类型(thought, execution)
|
||||
await this.scanOtherResources(rolePath, roleDir, resources)
|
||||
}
|
||||
} catch (dirError) {
|
||||
// 跳过无法访问的目录
|
||||
logger.debug(`跳过目录 ${roleDir}: ${dirError.message}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`扫描资源目录失败 ${basePath}: ${error.message}`)
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描角色资源
|
||||
* @param {string} rolePath - 角色目录路径
|
||||
* @param {string} roleId - 角色ID
|
||||
* @param {Object} resources - 资源容器
|
||||
*/
|
||||
async scanRoleResources(rolePath, roleId, resources) {
|
||||
const roleFile = path.join(rolePath, `${roleId}.role.md`)
|
||||
|
||||
if (await fs.pathExists(roleFile)) {
|
||||
try {
|
||||
const content = await fs.readFile(roleFile, 'utf8')
|
||||
|
||||
// 验证DPML格式
|
||||
if (this.validateDPMLFormat(content, 'role')) {
|
||||
const name = this.extractRoleName(content)
|
||||
|
||||
if (!resources.role) resources.role = {}
|
||||
resources.role[roleId] = {
|
||||
file: roleFile,
|
||||
name: name || roleId,
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略单个文件的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描其他资源类型
|
||||
* @param {string} rolePath - 角色目录路径
|
||||
* @param {string} roleId - 角色ID
|
||||
* @param {Object} resources - 资源容器
|
||||
*/
|
||||
async scanOtherResources(rolePath, roleId, resources) {
|
||||
for (const resourceType of SUPPORTED_RESOURCE_TYPES.filter(type => type !== 'role')) {
|
||||
const resourceDir = path.join(rolePath, resourceType)
|
||||
|
||||
if (await fs.pathExists(resourceDir)) {
|
||||
try {
|
||||
const files = await fs.readdir(resourceDir)
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith(`.${resourceType}.md`)) {
|
||||
const resourceName = file.replace(`.${resourceType}.md`, '')
|
||||
const filePath = path.join(resourceDir, file)
|
||||
const content = await fs.readFile(filePath, 'utf8')
|
||||
|
||||
if (this.validateDPMLFormat(content, resourceType)) {
|
||||
if (!resources[resourceType]) resources[resourceType] = {}
|
||||
resources[resourceType][resourceName] = {
|
||||
file: filePath,
|
||||
name: resourceName,
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: resourceType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`扫描${resourceType}资源失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证DPML格式
|
||||
* @param {string} content - 文件内容
|
||||
* @param {string} type - 资源类型
|
||||
* @returns {boolean} 是否为有效格式
|
||||
*/
|
||||
validateDPMLFormat(content, type) {
|
||||
const tags = DPML_TAGS[type]
|
||||
if (!tags) {
|
||||
return false
|
||||
}
|
||||
|
||||
return content.includes(tags.start) && content.includes(tags.end)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从角色内容中提取名称
|
||||
* @param {string} content - 角色文件内容
|
||||
* @returns {string} 角色名称
|
||||
*/
|
||||
extractRoleName(content) {
|
||||
// 简单的名称提取逻辑
|
||||
const match = content.match(/#\s*([^\n]+)/)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载系统资源注册表(兼容现有方法)
|
||||
* @returns {Promise<Object>} 系统资源注册表
|
||||
*/
|
||||
async loadSystemRegistry() {
|
||||
const registryPath = path.resolve(__dirname, '../../../resource.registry.json')
|
||||
|
||||
if (!await fs.pathExists(registryPath)) {
|
||||
throw new Error(`统一资源注册表文件不存在: ${registryPath}`)
|
||||
}
|
||||
|
||||
return await fs.readJSON(registryPath)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceManager
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"assistant": "@package://prompt/domain/assistant/thought/assistant.thought.md",
|
||||
"remember": "@package://prompt/core/thought/remember.thought.md",
|
||||
"recall": "@package://prompt/core/thought/recall.thought.md",
|
||||
"role-creation": "@package://prompt/core/thought/role-creation.thought.md",
|
||||
"product-manager": "@package://prompt/domain/product-manager/thought/product-manager.thought.md",
|
||||
"java-backend-developer": "@package://prompt/domain/java-backend-developer/thought/java-backend-developer.thought.md"
|
||||
}
|
||||
@ -33,7 +34,14 @@
|
||||
"system-architecture": "@package://prompt/domain/java-backend-developer/execution/system-architecture.execution.md",
|
||||
"spring-ecosystem": "@package://prompt/domain/java-backend-developer/execution/spring-ecosystem.execution.md",
|
||||
"code-quality": "@package://prompt/domain/java-backend-developer/execution/code-quality.execution.md",
|
||||
"database-design": "@package://prompt/domain/java-backend-developer/execution/database-design.execution.md"
|
||||
"database-design": "@package://prompt/domain/java-backend-developer/execution/database-design.execution.md",
|
||||
"role-generation": "@package://prompt/core/execution/role-generation.execution.md",
|
||||
"execution-authoring": "@package://prompt/core/execution/execution-authoring.execution.md",
|
||||
"thought-authoring": "@package://prompt/core/execution/thought-authoring.execution.md",
|
||||
"role-authoring": "@package://prompt/core/execution/role-authoring.execution.md",
|
||||
"resource-authoring": "@package://prompt/core/execution/resource-authoring.execution.md",
|
||||
|
||||
"wechat-miniprogram-development": "@package://prompt/domain/frontend-developer/execution/wechat-miniprogram-development.execution.md"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
@ -78,6 +86,11 @@
|
||||
"name": "☕ Java后端开发者",
|
||||
"description": "专业Java后端开发专家,精通Spring生态系统、微服务架构和系统设计"
|
||||
},
|
||||
"nuwa": {
|
||||
"file": "@package://prompt/core/nuwa/nuwa.role.md",
|
||||
"name": "🎨 女娲",
|
||||
"description": "专业角色创造顾问,通过对话收集需求,为用户量身定制AI助手角色"
|
||||
},
|
||||
"test-role": {
|
||||
"file": "@package://prompt/domain/test-role/test-role.role.md",
|
||||
"name": "🧪 测试角色",
|
||||
|
||||
221
src/tests/commands/CrossPlatformDiscovery.unit.test.js
Normal file
221
src/tests/commands/CrossPlatformDiscovery.unit.test.js
Normal file
@ -0,0 +1,221 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
|
||||
describe('跨平台角色发现兼容性测试', () => {
|
||||
let tempDir
|
||||
let projectDir
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cross-platform-test-'))
|
||||
projectDir = path.join(tempDir, 'test-project')
|
||||
|
||||
await fs.ensureDir(path.join(projectDir, 'prompt', 'domain'))
|
||||
await fs.ensureDir(path.join(projectDir, '.promptx', 'user-roles'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
})
|
||||
|
||||
describe('Node.js 原生 API 替代 glob', () => {
|
||||
test('应该能使用 fs.readdir 代替 glob.sync', async () => {
|
||||
// 创建测试角色文件
|
||||
const roleDir = path.join(projectDir, 'prompt', 'domain', 'test-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'test-role.role.md'),
|
||||
'<role><personality>测试</personality></role>'
|
||||
)
|
||||
|
||||
// 使用Node.js原生API实现角色发现(替代glob)
|
||||
async function discoverRolesWithNativeAPI(scanPath) {
|
||||
const discoveredRoles = {}
|
||||
|
||||
try {
|
||||
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')
|
||||
|
||||
discoveredRoles[domain] = {
|
||||
file: roleFile,
|
||||
name: `🎭 ${domain}`,
|
||||
description: '原生API发现的角色',
|
||||
source: 'native-api'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return discoveredRoles
|
||||
} catch (error) {
|
||||
console.warn('原生API角色发现失败:', error.message)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const domainPath = path.join(projectDir, 'prompt', 'domain')
|
||||
const discoveredRoles = await discoverRolesWithNativeAPI(domainPath)
|
||||
|
||||
expect(discoveredRoles).toHaveProperty('test-role')
|
||||
expect(discoveredRoles['test-role'].source).toBe('native-api')
|
||||
})
|
||||
|
||||
test('应该能处理不同平台的路径分隔符', () => {
|
||||
const unixPath = 'prompt/domain/role/role.role.md'
|
||||
const windowsPath = 'prompt\\domain\\role\\role.role.md'
|
||||
|
||||
// 使用path.join确保跨平台兼容性
|
||||
const normalizedPath = path.join('prompt', 'domain', 'role', 'role.role.md')
|
||||
|
||||
// 在当前平台上验证路径处理
|
||||
if (process.platform === 'win32') {
|
||||
expect(normalizedPath).toContain('\\')
|
||||
} else {
|
||||
expect(normalizedPath).toContain('/')
|
||||
}
|
||||
|
||||
// path.relative应该也能正常工作
|
||||
const relativePath = path.relative(projectDir, path.join(projectDir, normalizedPath))
|
||||
expect(relativePath).toBe(normalizedPath)
|
||||
})
|
||||
|
||||
test('应该处理路径中的特殊字符', async () => {
|
||||
// 创建包含特殊字符的角色名(但符合文件系统要求)
|
||||
const specialRoleName = 'role-with_special.chars'
|
||||
const roleDir = path.join(projectDir, 'prompt', 'domain', specialRoleName)
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const roleFile = path.join(roleDir, `${specialRoleName}.role.md`)
|
||||
await fs.writeFile(roleFile, '<role><personality>特殊角色</personality></role>')
|
||||
|
||||
// 验证能正确处理特殊字符的文件名
|
||||
expect(await fs.pathExists(roleFile)).toBe(true)
|
||||
|
||||
const content = await fs.readFile(roleFile, 'utf-8')
|
||||
expect(content).toContain('特殊角色')
|
||||
})
|
||||
})
|
||||
|
||||
describe('文件系统权限处理', () => {
|
||||
test('应该优雅处理无权限访问的目录', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows权限测试较为复杂,跳过
|
||||
expect(true).toBe(true)
|
||||
return
|
||||
}
|
||||
|
||||
const restrictedDir = path.join(projectDir, 'restricted')
|
||||
await fs.ensureDir(restrictedDir)
|
||||
|
||||
// 移除读权限
|
||||
await fs.chmod(restrictedDir, 0o000)
|
||||
|
||||
// 角色发现应该不会因为权限问题而崩溃
|
||||
async function safeDiscoverRoles(scanPath) {
|
||||
try {
|
||||
if (await fs.pathExists(scanPath)) {
|
||||
const domains = await fs.readdir(scanPath)
|
||||
return domains
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
// 应该优雅处理权限错误
|
||||
console.warn('权限不足,跳过目录:', scanPath)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const result = await safeDiscoverRoles(restrictedDir)
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
|
||||
// 恢复权限以便清理
|
||||
await fs.chmod(restrictedDir, 0o755)
|
||||
})
|
||||
})
|
||||
|
||||
describe('错误恢复机制', () => {
|
||||
test('应该在部分文件失败时继续处理其他文件', async () => {
|
||||
// 创建多个角色,其中一个有问题
|
||||
const goodRoleDir = path.join(projectDir, 'prompt', 'domain', 'good-role')
|
||||
await fs.ensureDir(goodRoleDir)
|
||||
await fs.writeFile(
|
||||
path.join(goodRoleDir, 'good-role.role.md'),
|
||||
'<role><personality>正常角色</personality></role>'
|
||||
)
|
||||
|
||||
const badRoleDir = path.join(projectDir, 'prompt', 'domain', 'bad-role')
|
||||
await fs.ensureDir(badRoleDir)
|
||||
await fs.writeFile(
|
||||
path.join(badRoleDir, 'bad-role.role.md'),
|
||||
'无效内容'
|
||||
)
|
||||
|
||||
// 模拟容错的角色发现实现
|
||||
async function resilientDiscoverRoles(scanPath) {
|
||||
const discoveredRoles = {}
|
||||
const errors = []
|
||||
|
||||
try {
|
||||
if (await fs.pathExists(scanPath)) {
|
||||
const domains = await fs.readdir(scanPath)
|
||||
|
||||
for (const domain of domains) {
|
||||
try {
|
||||
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')
|
||||
|
||||
// 简单验证内容
|
||||
if (content.includes('<role>')) {
|
||||
discoveredRoles[domain] = {
|
||||
file: roleFile,
|
||||
name: `🎭 ${domain}`,
|
||||
description: '容错发现的角色',
|
||||
source: 'resilient-discovery'
|
||||
}
|
||||
} else {
|
||||
throw new Error('无效的角色文件格式')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 记录错误但继续处理其他文件
|
||||
errors.push({ domain, error: error.message })
|
||||
console.warn(`跳过无效角色 ${domain}:`, error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('角色发现过程中出错:', error.message)
|
||||
}
|
||||
|
||||
return { discoveredRoles, errors }
|
||||
}
|
||||
|
||||
const domainPath = path.join(projectDir, 'prompt', 'domain')
|
||||
const result = await resilientDiscoverRoles(domainPath)
|
||||
|
||||
// 应该发现正常角色,跳过问题角色
|
||||
expect(result.discoveredRoles).toHaveProperty('good-role')
|
||||
expect(result.discoveredRoles).not.toHaveProperty('bad-role')
|
||||
expect(result.errors).toHaveLength(1)
|
||||
expect(result.errors[0].domain).toBe('bad-role')
|
||||
})
|
||||
})
|
||||
})
|
||||
231
src/tests/commands/HelloCommand.integration.test.js
Normal file
231
src/tests/commands/HelloCommand.integration.test.js
Normal file
@ -0,0 +1,231 @@
|
||||
const HelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const ResourceManager = require('../../lib/core/resource/resourceManager')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
describe('HelloCommand - ResourceManager集成', () => {
|
||||
let helloCommand
|
||||
let tempDir
|
||||
let mockPackageRoot
|
||||
|
||||
beforeEach(async () => {
|
||||
// 创建临时测试目录
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-hello-test-'))
|
||||
mockPackageRoot = tempDir
|
||||
|
||||
// 模拟用户资源目录结构
|
||||
await fs.ensureDir(path.join(tempDir, '.promptx', 'resource', 'domain'))
|
||||
|
||||
helloCommand = new HelloCommand()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// 清理临时目录
|
||||
await fs.remove(tempDir)
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('用户角色发现集成', () => {
|
||||
it('应该显示用户创建的角色', async () => {
|
||||
// 创建测试用户角色
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'sales-expert')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const roleContent = `<role>
|
||||
<personality>
|
||||
# 销售专家思维模式
|
||||
## 核心特征
|
||||
- **客户导向思维**:始终以客户需求为出发点
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
# 销售专家行为原则
|
||||
## 核心原则
|
||||
- **诚信为本**:建立长期客户关系
|
||||
</principle>
|
||||
|
||||
<knowledge>
|
||||
# 销售专业知识体系
|
||||
## 销售技巧
|
||||
- **需求挖掘**:深度了解客户真实需求
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(path.join(roleDir, 'sales-expert.role.md'), roleContent)
|
||||
|
||||
// Mock ResourceManager的loadUnifiedRegistry方法
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
.mockResolvedValue({
|
||||
role: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
},
|
||||
'sales-expert': {
|
||||
file: path.join(roleDir, 'sales-expert.role.md'),
|
||||
name: '销售专家思维模式',
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟执行hello命令
|
||||
const result = await helloCommand.execute([])
|
||||
|
||||
// 验证用户角色在输出中显示
|
||||
const allOutput = result.content || ''
|
||||
|
||||
expect(allOutput).toContain('sales-expert')
|
||||
expect(allOutput).toContain('销售专家')
|
||||
expect(allOutput).toContain('(用户生成)')
|
||||
})
|
||||
|
||||
it('应该允许用户角色覆盖系统角色', async () => {
|
||||
// 创建与系统角色同名的用户角色
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'assistant')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const customAssistantContent = `<role>
|
||||
<personality>
|
||||
# 定制智能助手
|
||||
## 个性化特征
|
||||
- **专业导向**:专注于技术问题解决
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
# 定制助手原则
|
||||
## 核心原则
|
||||
- **精准回答**:提供准确的技术解决方案
|
||||
</principle>
|
||||
|
||||
<knowledge>
|
||||
# 定制助手知识体系
|
||||
## 技术领域
|
||||
- **编程语言**:多种编程语言的深度理解
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(path.join(roleDir, 'assistant.role.md'), customAssistantContent)
|
||||
|
||||
// Mock ResourceManager返回用户覆盖的角色
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
.mockResolvedValue({
|
||||
role: {
|
||||
'assistant': {
|
||||
file: path.join(roleDir, 'assistant.role.md'),
|
||||
name: '定制智能助手',
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = await helloCommand.execute([])
|
||||
|
||||
const allOutput = result.content || ''
|
||||
|
||||
// 验证显示的是用户版本
|
||||
expect(allOutput).toContain('定制智能助手')
|
||||
expect(allOutput).toContain('(用户生成)')
|
||||
expect(allOutput).not.toContain('🙋 智能助手')
|
||||
})
|
||||
|
||||
it('应该同时显示系统角色和用户角色', async () => {
|
||||
// 创建用户角色
|
||||
const userRoleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'data-analyst')
|
||||
await fs.ensureDir(userRoleDir)
|
||||
|
||||
const userRoleContent = `<role>
|
||||
<personality>
|
||||
# 数据分析师
|
||||
## 分析思维
|
||||
- **逻辑思维**:系统性分析数据模式
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
# 分析原则
|
||||
## 核心原则
|
||||
- **数据驱动**:基于数据做决策
|
||||
</principle>
|
||||
|
||||
<knowledge>
|
||||
# 分析知识
|
||||
## 统计学
|
||||
- **描述统计**:数据的基本特征分析
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(path.join(userRoleDir, 'data-analyst.role.md'), userRoleContent)
|
||||
|
||||
// Mock ResourceManager返回系统和用户角色
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
.mockResolvedValue({
|
||||
role: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
},
|
||||
'java-backend-developer': {
|
||||
file: '@package://prompt/domain/java-backend-developer/java-backend-developer.role.md',
|
||||
name: '☕ Java后端开发专家',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
},
|
||||
'data-analyst': {
|
||||
file: path.join(userRoleDir, 'data-analyst.role.md'),
|
||||
name: '数据分析师',
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result = await helloCommand.execute([])
|
||||
|
||||
const allOutput = result.content || ''
|
||||
|
||||
// 验证系统角色和用户角色都显示
|
||||
expect(allOutput).toContain('智能助手')
|
||||
expect(allOutput).toContain('Java后端开发专家')
|
||||
expect(allOutput).toContain('数据分析师')
|
||||
expect(allOutput).toContain('data-analyst')
|
||||
})
|
||||
})
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该优雅处理资源发现失败', async () => {
|
||||
// 模拟ResourceManager错误
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
.mockRejectedValue(new Error('资源发现失败'))
|
||||
|
||||
// 应该不抛出异常
|
||||
const result = await helloCommand.execute([])
|
||||
|
||||
// 应该显示基础角色(fallback)
|
||||
expect(result.content).toContain('智能助手')
|
||||
})
|
||||
|
||||
it('应该处理空的资源注册表', async () => {
|
||||
// Mock空的资源注册表
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
.mockResolvedValue({ role: {} })
|
||||
|
||||
const result = await helloCommand.execute([])
|
||||
|
||||
// 应该显示基础角色(fallback)
|
||||
expect(result.content).toContain('智能助手')
|
||||
})
|
||||
})
|
||||
})
|
||||
323
src/tests/commands/HelloCommand.unit.test.js
Normal file
323
src/tests/commands/HelloCommand.unit.test.js
Normal file
@ -0,0 +1,323 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
const HelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
|
||||
describe('HelloCommand 单元测试', () => {
|
||||
let helloCommand
|
||||
let tempDir
|
||||
let tempProjectDir
|
||||
|
||||
beforeEach(async () => {
|
||||
helloCommand = new HelloCommand()
|
||||
|
||||
// 创建临时目录模拟项目结构
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hello-command-test-'))
|
||||
tempProjectDir = path.join(tempDir, 'test-project')
|
||||
|
||||
// 创建基础目录结构
|
||||
await fs.ensureDir(path.join(tempProjectDir, 'prompt', 'domain'))
|
||||
await fs.ensureDir(path.join(tempProjectDir, '.promptx', 'user-roles'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
// 清理缓存
|
||||
if (helloCommand.roleRegistry) {
|
||||
helloCommand.roleRegistry = null
|
||||
}
|
||||
})
|
||||
|
||||
describe('基础功能测试', () => {
|
||||
test('应该能实例化HelloCommand', () => {
|
||||
expect(helloCommand).toBeInstanceOf(HelloCommand)
|
||||
expect(typeof helloCommand.discoverLocalRoles).toBe('function')
|
||||
expect(typeof helloCommand.loadRoleRegistry).toBe('function')
|
||||
})
|
||||
|
||||
test('getPurpose应该返回正确的目的描述', () => {
|
||||
const purpose = helloCommand.getPurpose()
|
||||
expect(purpose).toContain('AI')
|
||||
expect(purpose).toContain('角色')
|
||||
})
|
||||
})
|
||||
|
||||
describe('discoverLocalRoles 功能测试', () => {
|
||||
test('应该能发现系统内置角色', async () => {
|
||||
// 创建模拟的系统角色文件
|
||||
const assistantDir = path.join(tempProjectDir, 'prompt', 'domain', 'assistant')
|
||||
await fs.ensureDir(assistantDir)
|
||||
|
||||
const roleFileContent = `<!--
|
||||
name: 🙋 智能助手
|
||||
description: 通用助理角色,提供基础的助理服务和记忆支持
|
||||
-->
|
||||
|
||||
<role>
|
||||
<personality>
|
||||
@!thought://remember
|
||||
@!thought://recall
|
||||
@!thought://assistant
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
@!execution://assistant
|
||||
</principle>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(assistantDir, 'assistant.role.md'),
|
||||
roleFileContent
|
||||
)
|
||||
|
||||
// Mock PackageProtocol.getPackageRoot 返回临时目录
|
||||
const originalRequire = require
|
||||
jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return tempProjectDir
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 重新加载HelloCommand使用mock
|
||||
delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')]
|
||||
const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const mockedCommand = new MockedHelloCommand()
|
||||
|
||||
const discoveredRoles = await mockedCommand.discoverLocalRoles()
|
||||
|
||||
expect(discoveredRoles).toHaveProperty('assistant')
|
||||
expect(discoveredRoles.assistant.name).toContain('智能助手')
|
||||
expect(discoveredRoles.assistant.description).toContain('通用助理角色')
|
||||
expect(discoveredRoles.assistant.source).toBe('local-discovery')
|
||||
|
||||
// 恢复原始require
|
||||
jest.unmock('../../lib/core/resource/protocols/PackageProtocol')
|
||||
})
|
||||
|
||||
test('应该处理空的角色目录', async () => {
|
||||
// Mock PackageProtocol.getPackageRoot 返回空目录
|
||||
jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return tempProjectDir
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')]
|
||||
const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const mockedCommand = new MockedHelloCommand()
|
||||
|
||||
const discoveredRoles = await mockedCommand.discoverLocalRoles()
|
||||
expect(discoveredRoles).toEqual({})
|
||||
|
||||
jest.unmock('../../lib/core/resource/protocols/PackageProtocol')
|
||||
})
|
||||
|
||||
test('应该优雅处理文件读取错误', async () => {
|
||||
// 创建无效的角色文件(权限问题)
|
||||
const invalidRoleDir = path.join(tempProjectDir, 'prompt', 'domain', 'invalid')
|
||||
await fs.ensureDir(invalidRoleDir)
|
||||
|
||||
const invalidRoleFile = path.join(invalidRoleDir, 'invalid.role.md')
|
||||
await fs.writeFile(invalidRoleFile, 'invalid content')
|
||||
|
||||
// 修改文件权限使其不可读(仅在Unix系统上有效)
|
||||
if (process.platform !== 'win32') {
|
||||
await fs.chmod(invalidRoleFile, 0o000)
|
||||
}
|
||||
|
||||
jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return tempProjectDir
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')]
|
||||
const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const mockedCommand = new MockedHelloCommand()
|
||||
|
||||
// 应该不抛出异常,而是记录警告并跳过无效文件
|
||||
const discoveredRoles = await mockedCommand.discoverLocalRoles()
|
||||
expect(typeof discoveredRoles).toBe('object')
|
||||
|
||||
// 恢复文件权限
|
||||
if (process.platform !== 'win32') {
|
||||
await fs.chmod(invalidRoleFile, 0o644)
|
||||
}
|
||||
|
||||
jest.unmock('../../lib/core/resource/protocols/PackageProtocol')
|
||||
})
|
||||
})
|
||||
|
||||
describe('元数据提取测试', () => {
|
||||
test('应该正确提取角色名称和描述', async () => {
|
||||
const testRoleDir = path.join(tempProjectDir, 'prompt', 'domain', 'test-role')
|
||||
await fs.ensureDir(testRoleDir)
|
||||
|
||||
const roleContent = `<!--
|
||||
name: 🧪 测试角色
|
||||
description: 这是一个测试用的角色
|
||||
-->
|
||||
|
||||
<role>
|
||||
<personality>
|
||||
测试思维模式
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
测试行为原则
|
||||
</principle>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testRoleDir, 'test-role.role.md'),
|
||||
roleContent
|
||||
)
|
||||
|
||||
jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return tempProjectDir
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')]
|
||||
const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const mockedCommand = new MockedHelloCommand()
|
||||
|
||||
const discoveredRoles = await mockedCommand.discoverLocalRoles()
|
||||
|
||||
expect(discoveredRoles).toHaveProperty('test-role')
|
||||
expect(discoveredRoles['test-role'].name).toBe('🧪 测试角色')
|
||||
expect(discoveredRoles['test-role'].description).toBe('这是一个测试用的角色')
|
||||
|
||||
jest.unmock('../../lib/core/resource/protocols/PackageProtocol')
|
||||
})
|
||||
|
||||
test('应该处理缺少元数据的角色文件', async () => {
|
||||
const testRoleDir = path.join(tempProjectDir, 'prompt', 'domain', 'no-meta')
|
||||
await fs.ensureDir(testRoleDir)
|
||||
|
||||
const roleContent = `<role>
|
||||
<personality>
|
||||
基础角色内容
|
||||
</personality>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testRoleDir, 'no-meta.role.md'),
|
||||
roleContent
|
||||
)
|
||||
|
||||
jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return tempProjectDir
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')]
|
||||
const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const mockedCommand = new MockedHelloCommand()
|
||||
|
||||
const discoveredRoles = await mockedCommand.discoverLocalRoles()
|
||||
|
||||
expect(discoveredRoles).toHaveProperty('no-meta')
|
||||
expect(discoveredRoles['no-meta'].name).toBe('🎭 no-meta') // 默认格式
|
||||
expect(discoveredRoles['no-meta'].description).toBe('本地发现的角色') // 默认描述
|
||||
|
||||
jest.unmock('../../lib/core/resource/protocols/PackageProtocol')
|
||||
})
|
||||
})
|
||||
|
||||
describe('角色注册表加载测试', () => {
|
||||
test('应该能加载角色注册表', async () => {
|
||||
const result = await helloCommand.loadRoleRegistry()
|
||||
|
||||
expect(typeof result).toBe('object')
|
||||
expect(helloCommand.roleRegistry).toBe(result)
|
||||
})
|
||||
|
||||
test('应该在失败时返回默认assistant角色', async () => {
|
||||
// Mock ResourceManager抛出异常
|
||||
jest.doMock('../../lib/core/resource/resourceManager', () => {
|
||||
return class MockResourceManager {
|
||||
async initialize() {
|
||||
throw new Error('Mock initialization failure')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Mock discoverLocalRoles也失败
|
||||
jest.spyOn(helloCommand, 'discoverLocalRoles').mockRejectedValue(new Error('Mock discovery failure'))
|
||||
|
||||
delete require.cache[require.resolve('../../lib/core/pouch/commands/HelloCommand')]
|
||||
const MockedHelloCommand = require('../../lib/core/pouch/commands/HelloCommand')
|
||||
const mockedCommand = new MockedHelloCommand()
|
||||
|
||||
const result = await mockedCommand.loadRoleRegistry()
|
||||
|
||||
expect(result).toHaveProperty('assistant')
|
||||
expect(result.assistant.name).toContain('智能助手')
|
||||
|
||||
jest.unmock('../../lib/core/resource/resourceManager')
|
||||
helloCommand.discoverLocalRoles.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('角色信息获取测试', () => {
|
||||
test('getRoleInfo应该返回正确的角色信息', async () => {
|
||||
// Mock注册表
|
||||
helloCommand.roleRegistry = {
|
||||
'test-role': {
|
||||
name: '测试角色',
|
||||
description: '测试描述',
|
||||
file: '@package://test/path'
|
||||
}
|
||||
}
|
||||
|
||||
const roleInfo = await helloCommand.getRoleInfo('test-role')
|
||||
|
||||
expect(roleInfo).toEqual({
|
||||
id: 'test-role',
|
||||
name: '测试角色',
|
||||
description: '测试描述',
|
||||
file: '@package://test/path'
|
||||
})
|
||||
})
|
||||
|
||||
test('getRoleInfo对不存在的角色应该返回null', async () => {
|
||||
helloCommand.roleRegistry = {}
|
||||
|
||||
const roleInfo = await helloCommand.getRoleInfo('non-existent')
|
||||
expect(roleInfo).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAllRoles测试', () => {
|
||||
test('应该返回角色数组格式', async () => {
|
||||
helloCommand.roleRegistry = {
|
||||
'role1': { name: '角色1', description: '描述1', file: 'file1' },
|
||||
'role2': { name: '角色2', description: '描述2', file: 'file2' }
|
||||
}
|
||||
|
||||
const allRoles = await helloCommand.getAllRoles()
|
||||
|
||||
expect(Array.isArray(allRoles)).toBe(true)
|
||||
expect(allRoles).toHaveLength(2)
|
||||
expect(allRoles[0]).toHaveProperty('id')
|
||||
expect(allRoles[0]).toHaveProperty('name')
|
||||
expect(allRoles[0]).toHaveProperty('description')
|
||||
expect(allRoles[0]).toHaveProperty('file')
|
||||
})
|
||||
})
|
||||
})
|
||||
525
src/tests/commands/UserRoleDiscovery.integration.test.js
Normal file
525
src/tests/commands/UserRoleDiscovery.integration.test.js
Normal file
@ -0,0 +1,525 @@
|
||||
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 = `<!--
|
||||
name: 📊 自定义分析师
|
||||
description: 用户定制的数据分析专家
|
||||
-->
|
||||
|
||||
<role>
|
||||
<personality>
|
||||
# 数据分析思维
|
||||
我是一个专注于数据洞察的分析师,善于从复杂数据中发现业务价值。
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
# 分析原则
|
||||
- 数据驱动决策
|
||||
- 业务价值导向
|
||||
- 简洁清晰表达
|
||||
</principle>
|
||||
|
||||
<knowledge>
|
||||
# 专业技能
|
||||
- 统计分析方法
|
||||
- 数据可视化技能
|
||||
- 业务理解能力
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
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'),
|
||||
`<!--
|
||||
name: 🤖 系统助手
|
||||
description: 系统内置助手
|
||||
-->
|
||||
|
||||
<role>
|
||||
<personality>系统助手思维</personality>
|
||||
</role>`
|
||||
)
|
||||
|
||||
// 创建用户角色
|
||||
const userRoleDir = path.join(projectDir, '.promptx', 'user-roles', 'my-role')
|
||||
await fs.ensureDir(userRoleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(userRoleDir, 'my-role.role.md'),
|
||||
`<!--
|
||||
name: 👤 我的角色
|
||||
description: 用户自定义角色
|
||||
-->
|
||||
|
||||
<role>
|
||||
<personality>用户自定义思维</personality>
|
||||
</role>`
|
||||
)
|
||||
|
||||
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 = `<role>
|
||||
<personality>
|
||||
# 数据分析师思维模式
|
||||
|
||||
## 核心思维特征
|
||||
- **数据敏感性思维**:善于从数字中发现故事和趋势模式
|
||||
- **逻辑分析思维**:系统性地分解复杂数据问题,追求因果关系
|
||||
- **结果导向思维**:专注于为业务决策提供可行洞察和建议
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
# 数据分析师行为原则
|
||||
|
||||
## 核心工作原则
|
||||
- **数据驱动决策**:所有分析建议必须有可靠数据支撑
|
||||
- **简洁清晰表达**:复杂分析结果要用简单易懂的方式呈现
|
||||
- **业务价值优先**:分析要紧密围绕业务目标和价值创造
|
||||
</principle>
|
||||
|
||||
<knowledge>
|
||||
# 数据分析专业知识体系
|
||||
|
||||
## 数据处理技能
|
||||
- **数据清洗方法**:缺失值处理、异常值识别、数据标准化
|
||||
- **数据整合技巧**:多源数据合并、关联分析、数据建模
|
||||
- **质量控制流程**:数据校验、一致性检查、完整性验证
|
||||
|
||||
## 分析方法论
|
||||
- **描述性分析**:趋势分析、对比分析、分布分析
|
||||
- **诊断性分析**:钻取分析、根因分析、相关性分析
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
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) {
|
||||
// 从<personality>标签中提取角色名称
|
||||
const personalityMatch = content.match(/<personality[^>]*>([\s\S]*?)<\/personality>/i)
|
||||
const roleNameFromPersonality = personalityMatch
|
||||
? personalityMatch[1].split('\n')[0].replace(/^#\s*/, '').trim()
|
||||
: null
|
||||
|
||||
// 从<knowledge>标签中提取专业能力描述
|
||||
const knowledgeMatch = content.match(/<knowledge[^>]*>([\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'),
|
||||
`<role><personality>助手</personality></role>`
|
||||
)
|
||||
|
||||
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'),
|
||||
`<!--
|
||||
name: 📊 系统分析师
|
||||
description: 系统内置分析师
|
||||
-->
|
||||
<role><personality>系统分析师</personality></role>`
|
||||
)
|
||||
|
||||
const userRoleDir = path.join(projectDir, '.promptx', 'user-roles', 'analyst')
|
||||
await fs.ensureDir(userRoleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(userRoleDir, 'analyst.role.md'),
|
||||
`<!--
|
||||
name: 👤 用户分析师
|
||||
description: 用户自定义分析师
|
||||
-->
|
||||
<role><personality>用户分析师</personality></role>`
|
||||
)
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
220
src/tests/core/resource/ResourceManager.unit.test.js
Normal file
220
src/tests/core/resource/ResourceManager.unit.test.js
Normal file
@ -0,0 +1,220 @@
|
||||
const ResourceManager = require('../../../lib/core/resource/resourceManager')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
describe('ResourceManager - 用户资源发现', () => {
|
||||
let resourceManager
|
||||
let tempDir
|
||||
let mockPackageRoot
|
||||
|
||||
beforeEach(async () => {
|
||||
// 创建临时测试目录
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-test-'))
|
||||
mockPackageRoot = tempDir
|
||||
|
||||
// 模拟用户资源目录结构
|
||||
await fs.ensureDir(path.join(tempDir, '.promptx', 'resource', 'domain'))
|
||||
|
||||
resourceManager = new ResourceManager()
|
||||
|
||||
// Mock packageProtocol module
|
||||
jest.doMock('../../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return mockPackageRoot
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// 清理临时目录
|
||||
await fs.remove(tempDir)
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('discoverUserResources', () => {
|
||||
it('应该返回空对象当用户资源目录不存在时', async () => {
|
||||
// 删除用户资源目录
|
||||
await fs.remove(path.join(tempDir, '.promptx'))
|
||||
|
||||
const result = await resourceManager.discoverUserResources()
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('应该发现用户创建的角色文件', async () => {
|
||||
// 创建测试角色文件
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'test-sales-analyst')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const roleContent = `<role>
|
||||
<personality>
|
||||
# 销售数据分析师思维模式
|
||||
## 核心思维特征
|
||||
- **数据敏感性思维**:善于从数字中发现故事和趋势模式
|
||||
</personality>
|
||||
|
||||
<principle>
|
||||
# 销售数据分析师行为原则
|
||||
## 核心工作原则
|
||||
- **数据驱动决策**:所有分析建议必须有可靠数据支撑
|
||||
</principle>
|
||||
|
||||
<knowledge>
|
||||
# 销售数据分析专业知识体系
|
||||
## 数据处理技能
|
||||
- **数据清洗方法**:缺失值处理、异常值识别
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
await fs.writeFile(path.join(roleDir, 'test-sales-analyst.role.md'), roleContent)
|
||||
|
||||
const result = await resourceManager.discoverUserResources()
|
||||
|
||||
expect(result).toHaveProperty('role')
|
||||
expect(result.role).toHaveProperty('test-sales-analyst')
|
||||
expect(result.role['test-sales-analyst']).toMatchObject({
|
||||
file: expect.stringContaining('test-sales-analyst.role.md'),
|
||||
name: expect.stringContaining('销售数据分析师'),
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
})
|
||||
})
|
||||
|
||||
it('应该支持多种资源类型发现', async () => {
|
||||
// 创建角色和相关资源
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'test-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.ensureDir(path.join(roleDir, 'thought'))
|
||||
await fs.ensureDir(path.join(roleDir, 'execution'))
|
||||
|
||||
// 创建角色文件
|
||||
await fs.writeFile(path.join(roleDir, 'test-role.role.md'), '<role><personality>Test</personality><principle>Test</principle><knowledge>Test</knowledge></role>')
|
||||
|
||||
// 创建思维文件
|
||||
await fs.writeFile(path.join(roleDir, 'thought', 'test.thought.md'), '<thought><exploration>Test exploration</exploration><reasoning>Test reasoning</reasoning></thought>')
|
||||
|
||||
// 创建执行文件
|
||||
await fs.writeFile(path.join(roleDir, 'execution', 'test.execution.md'), '<execution><constraint>Test constraint</constraint></execution>')
|
||||
|
||||
const result = await resourceManager.discoverUserResources()
|
||||
|
||||
expect(result).toHaveProperty('role')
|
||||
expect(result).toHaveProperty('thought')
|
||||
expect(result).toHaveProperty('execution')
|
||||
expect(result.role).toHaveProperty('test-role')
|
||||
expect(result.thought).toHaveProperty('test')
|
||||
expect(result.execution).toHaveProperty('test')
|
||||
})
|
||||
|
||||
it('应该处理DPML格式错误的文件', async () => {
|
||||
// 创建格式错误的角色文件
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'invalid-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const invalidContent = `这不是有效的DPML格式`
|
||||
await fs.writeFile(path.join(roleDir, 'invalid-role.role.md'), invalidContent)
|
||||
|
||||
const result = await resourceManager.discoverUserResources()
|
||||
|
||||
// 应该跳过格式错误的文件,但不应该抛出错误
|
||||
expect(result.role || {}).not.toHaveProperty('invalid-role')
|
||||
})
|
||||
|
||||
it('应该跨平台正确处理路径', async () => {
|
||||
// 在不同平台上创建角色文件
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'cross-platform-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const roleContent = '<role><personality>Test</personality><principle>Test</principle><knowledge>Test</knowledge></role>'
|
||||
await fs.writeFile(path.join(roleDir, 'cross-platform-role.role.md'), roleContent)
|
||||
|
||||
const result = await resourceManager.discoverUserResources()
|
||||
|
||||
expect(result.role).toHaveProperty('cross-platform-role')
|
||||
|
||||
// 验证文件路径使用正确的分隔符
|
||||
const roleInfo = result.role['cross-platform-role']
|
||||
expect(roleInfo.file).toBe(path.normalize(roleInfo.file))
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadUnifiedRegistry', () => {
|
||||
it('应该合并系统资源和用户资源', async () => {
|
||||
// 模拟系统资源
|
||||
const mockSystemResources = {
|
||||
role: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock fs.readJSON for system registry
|
||||
jest.spyOn(fs, 'readJSON')
|
||||
.mockImplementation((filePath) => {
|
||||
if (filePath.includes('resource.registry.json')) {
|
||||
return Promise.resolve(mockSystemResources)
|
||||
}
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
// 创建用户资源
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'user-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'user-role.role.md'),
|
||||
'<role><personality>User</personality><principle>User</principle><knowledge>User</knowledge></role>'
|
||||
)
|
||||
|
||||
const result = await resourceManager.loadUnifiedRegistry()
|
||||
|
||||
expect(result.role).toHaveProperty('assistant') // 系统资源
|
||||
expect(result.role).toHaveProperty('user-role') // 用户资源
|
||||
})
|
||||
|
||||
it('应该让用户资源覆盖同名系统资源', async () => {
|
||||
// 模拟系统资源
|
||||
const mockSystemResources = {
|
||||
role: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock fs.readJSON for system registry
|
||||
jest.spyOn(fs, 'readJSON')
|
||||
.mockImplementation((filePath) => {
|
||||
if (filePath.includes('resource.registry.json')) {
|
||||
return Promise.resolve(mockSystemResources)
|
||||
}
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
// 创建同名的用户资源
|
||||
const roleDir = path.join(tempDir, '.promptx', 'resource', 'domain', 'assistant')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'assistant.role.md'),
|
||||
'<role><personality># 自定义助手\n用户定制的助手</personality><principle>Custom</principle><knowledge>Custom</knowledge></role>'
|
||||
)
|
||||
|
||||
const result = await resourceManager.loadUnifiedRegistry()
|
||||
|
||||
expect(result.role.assistant.source).toBe('user-generated')
|
||||
expect(result.role.assistant.name).toContain('自定义助手')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user