优化角色注册,发现,nuwa 角色的提示词等
This commit is contained in:
@ -3,6 +3,9 @@ const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { COMMANDS, buildCommand } = require('../../../../constants')
|
||||
const ResourceManager = require('../../resource/resourceManager')
|
||||
const DPMLContentParser = require('../../resource/DPMLContentParser')
|
||||
const SemanticRenderer = require('../../resource/SemanticRenderer')
|
||||
const logger = require('../../../utils/logger')
|
||||
|
||||
/**
|
||||
* 角色激活锦囊命令
|
||||
@ -14,6 +17,8 @@ class ActionCommand extends BasePouchCommand {
|
||||
// 获取HelloCommand的角色注册表
|
||||
this.helloCommand = null
|
||||
this.resourceManager = new ResourceManager()
|
||||
this.dpmlParser = new DPMLContentParser()
|
||||
this.semanticRenderer = new SemanticRenderer()
|
||||
}
|
||||
|
||||
getPurpose () {
|
||||
@ -38,9 +43,14 @@ ${COMMANDS.HELLO}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(`[ActionCommand] 开始激活角色: ${roleId}`)
|
||||
|
||||
// 1. 获取角色信息
|
||||
const roleInfo = await this.getRoleInfo(roleId)
|
||||
logger.debug(`[ActionCommand] getRoleInfo结果:`, roleInfo)
|
||||
|
||||
if (!roleInfo) {
|
||||
logger.warn(`[ActionCommand] 角色 "${roleId}" 不存在!`)
|
||||
return `❌ 角色 "${roleId}" 不存在!
|
||||
|
||||
🔍 请使用以下命令查看可用角色:
|
||||
@ -71,17 +81,24 @@ ${COMMANDS.HELLO}
|
||||
* 获取角色信息(从HelloCommand)
|
||||
*/
|
||||
async getRoleInfo (roleId) {
|
||||
logger.debug(`[ActionCommand] getRoleInfo调用,角色ID: ${roleId}`)
|
||||
|
||||
// 懒加载HelloCommand实例
|
||||
if (!this.helloCommand) {
|
||||
logger.debug(`[ActionCommand] 创建新的HelloCommand实例`)
|
||||
const HelloCommand = require('./HelloCommand')
|
||||
this.helloCommand = new HelloCommand()
|
||||
} else {
|
||||
logger.debug(`[ActionCommand] 复用现有HelloCommand实例`)
|
||||
}
|
||||
|
||||
return await this.helloCommand.getRoleInfo(roleId)
|
||||
const result = await this.helloCommand.getRoleInfo(roleId)
|
||||
logger.debug(`[ActionCommand] HelloCommand.getRoleInfo返回:`, result)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析角色文件,提取thought和execution依赖
|
||||
* 分析角色文件,提取完整的角色语义(@引用 + 直接内容)
|
||||
*/
|
||||
async analyzeRoleDependencies (roleInfo) {
|
||||
try {
|
||||
@ -99,32 +116,47 @@ ${COMMANDS.HELLO}
|
||||
|
||||
// 读取角色文件内容
|
||||
const roleContent = await fs.readFile(filePath, 'utf-8')
|
||||
|
||||
// 提取所有资源引用
|
||||
const resourceRegex = /@([!?]?)([a-zA-Z][a-zA-Z0-9_-]*):\/\/([a-zA-Z0-9_\/.,-]+?)(?=[\s\)\],]|$)/g
|
||||
const matches = Array.from(roleContent.matchAll(resourceRegex))
|
||||
|
||||
const dependencies = {
|
||||
thoughts: new Set(),
|
||||
executions: new Set(),
|
||||
knowledge: [roleInfo.id] // 角色自身的knowledge
|
||||
}
|
||||
|
||||
// 分类依赖
|
||||
matches.forEach(match => {
|
||||
const [fullMatch, priority, protocol, resource] = match
|
||||
|
||||
if (protocol === 'thought') {
|
||||
dependencies.thoughts.add(resource)
|
||||
} else if (protocol === 'execution') {
|
||||
dependencies.executions.add(resource)
|
||||
|
||||
// 使用DPMLContentParser解析完整的角色语义
|
||||
const roleSemantics = this.dpmlParser.parseRoleDocument(roleContent)
|
||||
|
||||
// 提取@引用依赖(保持兼容性)
|
||||
// 注意:对于包含语义内容的角色,引用已在语义渲染中处理,无需重复加载
|
||||
const thoughts = new Set()
|
||||
const executions = new Set()
|
||||
|
||||
// 从所有标签中提取thought和execution引用
|
||||
// 但排除已在语义内容中处理的引用
|
||||
Object.values(roleSemantics).forEach(tagSemantics => {
|
||||
if (tagSemantics && tagSemantics.references) {
|
||||
tagSemantics.references.forEach(ref => {
|
||||
// 跳过已在语义内容中处理的引用
|
||||
if (tagSemantics.fullSemantics) {
|
||||
// 如果标签有完整语义内容,其引用将在语义渲染中处理,无需独立加载
|
||||
return
|
||||
}
|
||||
|
||||
if (ref.protocol === 'thought') {
|
||||
thoughts.add(ref.resource)
|
||||
} else if (ref.protocol === 'execution') {
|
||||
executions.add(ref.resource)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
thoughts: dependencies.thoughts,
|
||||
executions: dependencies.executions,
|
||||
knowledge: dependencies.knowledge
|
||||
// 保持原有结构(兼容性)
|
||||
thoughts,
|
||||
executions,
|
||||
knowledge: [roleInfo.id],
|
||||
|
||||
// 新增:完整的角色语义结构
|
||||
roleSemantics: {
|
||||
personality: roleSemantics.personality || null,
|
||||
principle: roleSemantics.principle || null,
|
||||
knowledge: roleSemantics.knowledge || null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error analyzing role dependencies:', error)
|
||||
@ -132,7 +164,12 @@ ${COMMANDS.HELLO}
|
||||
return {
|
||||
thoughts: [],
|
||||
executions: [],
|
||||
knowledge: [roleInfo.id]
|
||||
knowledge: [roleInfo.id],
|
||||
roleSemantics: {
|
||||
personality: null,
|
||||
principle: null,
|
||||
knowledge: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -267,37 +304,85 @@ ${result.content}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成学习计划并直接加载所有内容
|
||||
* 生成学习计划并直接加载所有内容(包含完整的角色语义)
|
||||
*/
|
||||
async generateLearningPlan (roleId, dependencies) {
|
||||
const { thoughts, executions } = dependencies
|
||||
const { thoughts, executions, roleSemantics } = dependencies
|
||||
|
||||
let content = `🎭 **角色激活完成:${roleId}** - 所有技能已自动加载\n`
|
||||
|
||||
// 加载思维模式
|
||||
// 加载思维模式技能(仅包含独立的thought引用)
|
||||
if (thoughts.size > 0) {
|
||||
content += `# 🧠 思维模式技能 (${thoughts.size}个)\n`
|
||||
|
||||
// 加载引用的思维资源
|
||||
for (const thought of Array.from(thoughts)) {
|
||||
content += await this.loadLearnContent(`thought://${thought}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载执行技能
|
||||
// 添加角色人格特征(支持@引用占位符语义渲染)
|
||||
if (roleSemantics.personality && roleSemantics.personality.fullSemantics) {
|
||||
content += `# 👤 角色人格特征\n`
|
||||
content += `## ✅ 👤 人格特征:${roleId}\n`
|
||||
const personalityContent = await this.semanticRenderer.renderSemanticContent(
|
||||
roleSemantics.personality,
|
||||
this.resourceManager
|
||||
)
|
||||
content += `${personalityContent}\n`
|
||||
content += `---\n`
|
||||
}
|
||||
|
||||
// 加载执行技能(仅包含独立的execution引用)
|
||||
if (executions.size > 0) {
|
||||
content += `# ⚡ 执行技能 (${executions.size}个)\n`
|
||||
|
||||
// 加载引用的执行资源
|
||||
for (const execution of Array.from(executions)) {
|
||||
content += await this.loadLearnContent(`execution://${execution}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加角色行为原则(支持@引用占位符语义渲染)
|
||||
if (roleSemantics.principle && roleSemantics.principle.fullSemantics) {
|
||||
content += `# ⚖️ 角色行为原则\n`
|
||||
content += `## ✅ ⚖️ 行为原则:${roleId}\n`
|
||||
const principleContent = await this.semanticRenderer.renderSemanticContent(
|
||||
roleSemantics.principle,
|
||||
this.resourceManager
|
||||
)
|
||||
content += `${principleContent}\n`
|
||||
content += `---\n`
|
||||
}
|
||||
|
||||
// 添加语义渲染的知识体系(支持@引用占位符)
|
||||
if (roleSemantics.knowledge && roleSemantics.knowledge.fullSemantics) {
|
||||
content += `# 📚 专业知识体系\n`
|
||||
content += `## ✅ 📚 知识体系:${roleId}-knowledge\n`
|
||||
const knowledgeContent = await this.semanticRenderer.renderSemanticContent(
|
||||
roleSemantics.knowledge,
|
||||
this.resourceManager
|
||||
)
|
||||
content += `${knowledgeContent}\n`
|
||||
content += `---\n`
|
||||
}
|
||||
|
||||
// 激活总结
|
||||
content += `# 🎯 角色激活总结\n`
|
||||
content += `✅ **${roleId} 角色已完全激活!**\n`
|
||||
content += `📋 **已获得能力**:\n`
|
||||
if (thoughts.size > 0) content += `- 🧠 思维模式:${Array.from(thoughts).join(', ')}\n`
|
||||
if (executions.size > 0) content += `- ⚡ 执行技能:${Array.from(executions).join(', ')}\n`
|
||||
|
||||
// 显示角色核心组件
|
||||
const roleComponents = []
|
||||
if (roleSemantics.personality?.fullSemantics) roleComponents.push('👤 人格特征')
|
||||
if (roleSemantics.principle?.fullSemantics) roleComponents.push('⚖️ 行为原则')
|
||||
if (roleSemantics.knowledge?.fullSemantics) roleComponents.push('📚 专业知识')
|
||||
if (roleComponents.length > 0) {
|
||||
content += `- 🎭 角色组件:${roleComponents.join(', ')}\n`
|
||||
}
|
||||
|
||||
content += `💡 **现在可以立即开始以 ${roleId} 身份提供专业服务!**\n`
|
||||
|
||||
// 自动执行 recall 命令
|
||||
|
||||
@ -2,6 +2,8 @@ const BasePouchCommand = require('../BasePouchCommand')
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { buildCommand } = require('../../../../constants')
|
||||
const SimplifiedRoleDiscovery = require('../../resource/SimplifiedRoleDiscovery')
|
||||
const logger = require('../../../utils/logger')
|
||||
|
||||
/**
|
||||
* 角色发现锦囊命令
|
||||
@ -10,7 +12,8 @@ const { buildCommand } = require('../../../../constants')
|
||||
class HelloCommand extends BasePouchCommand {
|
||||
constructor () {
|
||||
super()
|
||||
this.roleRegistry = null // 角色注册表将从资源系统动态加载
|
||||
// 移除roleRegistry缓存,改为每次实时扫描
|
||||
this.discovery = new SimplifiedRoleDiscovery()
|
||||
}
|
||||
|
||||
getPurpose () {
|
||||
@ -18,28 +21,21 @@ class HelloCommand extends BasePouchCommand {
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态加载角色注册表
|
||||
* 动态加载角色注册表 - 使用SimplifiedRoleDiscovery
|
||||
* 移除缓存机制,每次都实时扫描,确保角色发现的一致性
|
||||
*/
|
||||
async loadRoleRegistry () {
|
||||
if (this.roleRegistry) {
|
||||
return this.roleRegistry
|
||||
}
|
||||
|
||||
// 移除缓存检查,每次都实时扫描
|
||||
// 原因:1) 客户端应用,action频次不高 2) 避免新角色创建后的状态不一致问题
|
||||
|
||||
try {
|
||||
// 使用新的ResourceManager架构
|
||||
const ResourceManager = require('../../resource/resourceManager')
|
||||
const resourceManager = new ResourceManager()
|
||||
// 使用新的SimplifiedRoleDiscovery算法
|
||||
const allRoles = await this.discovery.discoverAllRoles()
|
||||
|
||||
// 加载统一注册表(包含系统+用户资源)
|
||||
const unifiedRegistry = await resourceManager.loadUnifiedRegistry()
|
||||
|
||||
// 提取角色数据
|
||||
const roleData = unifiedRegistry.role || {}
|
||||
|
||||
// 转换为HelloCommand期望的格式
|
||||
this.roleRegistry = {}
|
||||
for (const [roleId, roleInfo] of Object.entries(roleData)) {
|
||||
this.roleRegistry[roleId] = {
|
||||
// 转换为HelloCommand期望的格式,不缓存
|
||||
const roleRegistry = {}
|
||||
for (const [roleId, roleInfo] of Object.entries(allRoles)) {
|
||||
roleRegistry[roleId] = {
|
||||
file: roleInfo.file,
|
||||
name: roleInfo.name || roleId,
|
||||
description: this.extractDescription(roleInfo) || `${roleInfo.name || roleId}专业角色`,
|
||||
@ -48,21 +44,21 @@ class HelloCommand extends BasePouchCommand {
|
||||
}
|
||||
|
||||
// 如果没有任何角色,使用基础角色
|
||||
if (Object.keys(this.roleRegistry).length === 0) {
|
||||
this.roleRegistry = {
|
||||
assistant: {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'fallback'
|
||||
}
|
||||
if (Object.keys(roleRegistry).length === 0) {
|
||||
roleRegistry.assistant = {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'fallback'
|
||||
}
|
||||
}
|
||||
|
||||
return roleRegistry
|
||||
} catch (error) {
|
||||
console.warn('角色注册表加载失败,使用基础角色:', error.message)
|
||||
logger.warn('角色注册表加载失败,使用基础角色:', error.message)
|
||||
|
||||
// 使用基础角色作为fallback
|
||||
this.roleRegistry = {
|
||||
return {
|
||||
assistant: {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
@ -71,8 +67,6 @@ class HelloCommand extends BasePouchCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.roleRegistry
|
||||
}
|
||||
|
||||
/**
|
||||
@ -212,19 +206,28 @@ ${buildCommand.action(allRoles[0]?.id || 'assistant')}
|
||||
* 获取角色信息(提供给其他命令使用)
|
||||
*/
|
||||
async getRoleInfo (roleId) {
|
||||
logger.debug(`[HelloCommand] getRoleInfo调用,角色ID: ${roleId}`)
|
||||
|
||||
const registry = await this.loadRoleRegistry()
|
||||
logger.debug(`[HelloCommand] 注册表加载完成,包含角色:`, Object.keys(registry))
|
||||
|
||||
const roleData = registry[roleId]
|
||||
logger.debug(`[HelloCommand] 查找角色${roleId}结果:`, roleData ? '找到' : '未找到')
|
||||
|
||||
if (!roleData) {
|
||||
logger.debug(`[HelloCommand] 角色${roleId}在注册表中不存在`)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
const result = {
|
||||
id: roleId,
|
||||
name: roleData.name,
|
||||
description: roleData.description,
|
||||
file: roleData.file
|
||||
}
|
||||
|
||||
logger.debug(`[HelloCommand] 返回角色信息:`, result)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@ -238,62 +241,10 @@ ${buildCommand.action(allRoles[0]?.id || 'assistant')}
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态发现本地角色文件
|
||||
* 注意:原来的discoverLocalRoles方法已被移除
|
||||
* 现在使用SimplifiedRoleDiscovery.discoverAllRoles()替代
|
||||
* 这避免了glob依赖和跨平台兼容性问题
|
||||
*/
|
||||
async discoverLocalRoles () {
|
||||
const PackageProtocol = require('../../resource/protocols/PackageProtocol')
|
||||
const packageProtocol = new PackageProtocol()
|
||||
const glob = require('glob')
|
||||
const path = require('path')
|
||||
|
||||
try {
|
||||
const packageRoot = await packageProtocol.getPackageRoot()
|
||||
const domainPath = path.join(packageRoot, 'prompt', 'domain')
|
||||
|
||||
// 扫描所有角色目录
|
||||
const rolePattern = path.join(domainPath, '*', '*.role.md')
|
||||
const roleFiles = glob.sync(rolePattern)
|
||||
|
||||
const discoveredRoles = {}
|
||||
|
||||
for (const roleFile of roleFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(roleFile, 'utf-8')
|
||||
const relativePath = path.relative(packageRoot, roleFile)
|
||||
const roleName = path.basename(roleFile, '.role.md')
|
||||
|
||||
// 尝试从文件内容中提取角色信息
|
||||
let description = '本地发现的角色'
|
||||
let name = `🎭 ${roleName}`
|
||||
|
||||
// 简单的元数据提取(支持多行)
|
||||
const descMatch = content.match(/description:\s*(.+?)(?:\n|$)/i)
|
||||
if (descMatch) {
|
||||
description = descMatch[1].trim()
|
||||
}
|
||||
|
||||
const nameMatch = content.match(/name:\s*(.+?)(?:\n|$)/i)
|
||||
if (nameMatch) {
|
||||
name = nameMatch[1].trim()
|
||||
}
|
||||
|
||||
discoveredRoles[roleName] = {
|
||||
file: `@package://${relativePath}`,
|
||||
name,
|
||||
description,
|
||||
source: 'local-discovery'
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`跳过无效的角色文件: ${roleFile}`, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return discoveredRoles
|
||||
} catch (error) {
|
||||
console.warn('动态角色发现失败:', error.message)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HelloCommand
|
||||
|
||||
179
src/lib/core/resource/DPMLContentParser.js
Normal file
179
src/lib/core/resource/DPMLContentParser.js
Normal file
@ -0,0 +1,179 @@
|
||||
/**
|
||||
* DPML内容解析器
|
||||
* 统一处理DPML标签内的混合内容(@引用 + 直接内容)
|
||||
* 确保标签语义完整性
|
||||
*/
|
||||
class DPMLContentParser {
|
||||
/**
|
||||
* 解析DPML标签的完整语义内容
|
||||
* @param {string} content - 标签内的原始内容
|
||||
* @param {string} tagName - 标签名称
|
||||
* @returns {Object} 完整的语义结构
|
||||
*/
|
||||
parseTagContent(content, tagName) {
|
||||
if (!content || !content.trim()) {
|
||||
return {
|
||||
fullSemantics: '',
|
||||
references: [],
|
||||
directContent: '',
|
||||
metadata: {
|
||||
tagName,
|
||||
hasReferences: false,
|
||||
hasDirectContent: false,
|
||||
contentType: 'empty'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cleanContent = content.trim()
|
||||
const references = this.extractReferencesWithPosition(cleanContent)
|
||||
const directContent = this.extractDirectContent(cleanContent)
|
||||
|
||||
return {
|
||||
// 完整语义内容(用户看到的最终效果)
|
||||
fullSemantics: cleanContent,
|
||||
|
||||
// 引用部分(需要解析和加载的资源)
|
||||
references,
|
||||
|
||||
// 直接部分(用户原创内容)
|
||||
directContent,
|
||||
|
||||
// 元数据
|
||||
metadata: {
|
||||
tagName,
|
||||
hasReferences: references.length > 0,
|
||||
hasDirectContent: directContent.length > 0,
|
||||
contentType: this.determineContentType(cleanContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取所有@引用
|
||||
* @param {string} content - 内容
|
||||
* @returns {Array} 引用数组
|
||||
*/
|
||||
extractReferences(content) {
|
||||
// 使用新的位置信息方法,但保持向下兼容
|
||||
return this.extractReferencesWithPosition(content).map(ref => ({
|
||||
fullMatch: ref.fullMatch,
|
||||
priority: ref.priority,
|
||||
protocol: ref.protocol,
|
||||
resource: ref.resource,
|
||||
isRequired: ref.isRequired,
|
||||
isOptional: ref.isOptional
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:获取引用的位置信息
|
||||
* @param {string} content - 内容
|
||||
* @returns {Array} 包含位置信息的引用数组
|
||||
*/
|
||||
extractReferencesWithPosition(content) {
|
||||
if (!content) {
|
||||
return []
|
||||
}
|
||||
|
||||
const resourceRegex = /@([!?]?)([a-zA-Z][a-zA-Z0-9_-]*):\/\/([a-zA-Z0-9_\/.,-]+?)(?=[\s\)\],]|$)/g
|
||||
const matches = []
|
||||
let match
|
||||
|
||||
while ((match = resourceRegex.exec(content)) !== null) {
|
||||
matches.push({
|
||||
fullMatch: match[0],
|
||||
priority: match[1],
|
||||
protocol: match[2],
|
||||
resource: match[3],
|
||||
position: match.index, // 位置信息
|
||||
isRequired: match[1] === '!',
|
||||
isOptional: match[1] === '?'
|
||||
})
|
||||
}
|
||||
|
||||
return matches.sort((a, b) => a.position - b.position) // 按位置排序
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取直接内容(移除@引用后的剩余内容)
|
||||
* @param {string} content - 内容
|
||||
* @returns {string} 直接内容
|
||||
*/
|
||||
extractDirectContent(content) {
|
||||
// 移除所有@引用行,保留其他内容
|
||||
const withoutReferences = content.replace(/^.*@[!?]?[a-zA-Z][a-zA-Z0-9_-]*:\/\/.*$/gm, '')
|
||||
|
||||
// 清理多余的空行
|
||||
const cleaned = withoutReferences.replace(/\n{3,}/g, '\n\n').trim()
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含引用
|
||||
* @param {string} content - 内容
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasReferences(content) {
|
||||
return /@[!?]?[a-zA-Z][a-zA-Z0-9_-]*:\/\//.test(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含直接内容
|
||||
* @param {string} content - 内容
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasDirectContent(content) {
|
||||
const withoutReferences = this.extractDirectContent(content)
|
||||
return withoutReferences.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 确定内容类型
|
||||
* @param {string} content - 内容
|
||||
* @returns {string} 内容类型
|
||||
*/
|
||||
determineContentType(content) {
|
||||
const hasRefs = this.hasReferences(content)
|
||||
const hasDirect = this.hasDirectContent(content)
|
||||
|
||||
if (hasRefs && hasDirect) return 'mixed'
|
||||
if (hasRefs) return 'references-only'
|
||||
if (hasDirect) return 'direct-only'
|
||||
return 'empty'
|
||||
}
|
||||
|
||||
/**
|
||||
* 从DPML文档中提取指定标签的内容
|
||||
* @param {string} dpmlContent - 完整的DPML文档内容
|
||||
* @param {string} tagName - 标签名称
|
||||
* @returns {string} 标签内容
|
||||
*/
|
||||
extractTagContent(dpmlContent, tagName) {
|
||||
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`, 'i')
|
||||
const match = dpmlContent.match(regex)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析完整的DPML角色文档
|
||||
* @param {string} roleContent - 角色文档内容
|
||||
* @returns {Object} 解析后的角色语义结构
|
||||
*/
|
||||
parseRoleDocument(roleContent) {
|
||||
const dpmlTags = ['personality', 'principle', 'knowledge']
|
||||
const roleSemantics = {}
|
||||
|
||||
dpmlTags.forEach(tagName => {
|
||||
const tagContent = this.extractTagContent(roleContent, tagName)
|
||||
if (tagContent) {
|
||||
roleSemantics[tagName] = this.parseTagContent(tagContent, tagName)
|
||||
}
|
||||
})
|
||||
|
||||
return roleSemantics
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DPMLContentParser
|
||||
83
src/lib/core/resource/SemanticRenderer.js
Normal file
83
src/lib/core/resource/SemanticRenderer.js
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* SemanticRenderer - DPML语义渲染器
|
||||
*
|
||||
* 核心理念:@引用 = 语义占位符
|
||||
* 在标签的原始位置插入引用内容,保持完整的语义流程
|
||||
*/
|
||||
class SemanticRenderer {
|
||||
/**
|
||||
* 语义占位符渲染:将@引用替换为实际内容
|
||||
* @param {Object} tagSemantics - 标签语义结构
|
||||
* @param {string} tagSemantics.fullSemantics - 完整的语义内容
|
||||
* @param {Array} tagSemantics.references - 引用列表
|
||||
* @param {ResourceManager} resourceManager - 资源管理器
|
||||
* @returns {string} 完整融合的语义内容
|
||||
*/
|
||||
async renderSemanticContent(tagSemantics, resourceManager) {
|
||||
if (!tagSemantics || !tagSemantics.fullSemantics) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let content = tagSemantics.fullSemantics
|
||||
|
||||
if (!tagSemantics.references || tagSemantics.references.length === 0) {
|
||||
return content.trim()
|
||||
}
|
||||
|
||||
// 按出现顺序处理每个@引用(保持位置语义)
|
||||
// 需要按位置排序确保正确的替换顺序
|
||||
const sortedReferences = [...tagSemantics.references].sort((a, b) => a.position - b.position)
|
||||
|
||||
for (const ref of sortedReferences) {
|
||||
try {
|
||||
// 解析引用内容
|
||||
const result = await resourceManager.resolve(ref.fullMatch)
|
||||
|
||||
// 检查解析是否成功
|
||||
if (result.success) {
|
||||
// 提取标签内容(去掉外层DPML标签)
|
||||
const cleanContent = this.extractTagInnerContent(result.content, ref.protocol)
|
||||
// 用<reference>标签包装引用内容,标明这是占位符渲染
|
||||
const wrappedContent = `<reference protocol="${ref.protocol}" resource="${ref.resource}">\n${cleanContent}\n</reference>`
|
||||
// 在原始位置替换@引用为实际内容
|
||||
const refIndex = content.indexOf(ref.fullMatch)
|
||||
if (refIndex !== -1) {
|
||||
content = content.substring(0, refIndex) + wrappedContent + content.substring(refIndex + ref.fullMatch.length)
|
||||
} else {
|
||||
content = content.replace(ref.fullMatch, wrappedContent)
|
||||
}
|
||||
} else {
|
||||
// 解析失败时的优雅降级
|
||||
content = content.replace(ref.fullMatch, `<!-- 引用解析失败: ${ref.fullMatch} - ${result.error?.message || 'Unknown error'} -->`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 引用解析失败时的优雅降级
|
||||
content = content.replace(ref.fullMatch, `<!-- 引用解析失败: ${ref.fullMatch} - ${error.message} -->`)
|
||||
}
|
||||
}
|
||||
|
||||
return content.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取DPML标签内的内容
|
||||
* @param {string} content - 包含DPML标签的完整内容
|
||||
* @param {string} protocol - 协议名称(thought, execution等)
|
||||
* @returns {string} 标签内的纯内容
|
||||
*/
|
||||
extractTagInnerContent(content, protocol) {
|
||||
// 根据协议类型确定标签名
|
||||
const tagName = protocol
|
||||
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)</${tagName}>`, 'i')
|
||||
const match = content.match(regex)
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1].trim()
|
||||
}
|
||||
|
||||
// 如果没有匹配到标签,返回原内容(可能已经是纯内容)
|
||||
return content.trim()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SemanticRenderer
|
||||
285
src/lib/core/resource/SimplifiedRoleDiscovery.js
Normal file
285
src/lib/core/resource/SimplifiedRoleDiscovery.js
Normal file
@ -0,0 +1,285 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const logger = require('../../utils/logger')
|
||||
|
||||
/**
|
||||
* SimplifiedRoleDiscovery - 简化的角色发现算法
|
||||
*
|
||||
* 设计原则:
|
||||
* 1. 系统角色:完全依赖静态注册表,零动态扫描
|
||||
* 2. 用户角色:最小化文件系统操作,简单有效
|
||||
* 3. 统一接口:单一发现入口,无重复逻辑
|
||||
* 4. 跨平台安全:使用Node.js原生API,避免glob
|
||||
*/
|
||||
class SimplifiedRoleDiscovery {
|
||||
constructor() {
|
||||
this.USER_RESOURCE_DIR = '.promptx'
|
||||
this.RESOURCE_DOMAIN_PATH = ['resource', 'domain']
|
||||
}
|
||||
|
||||
/**
|
||||
* 发现所有角色(系统 + 用户)
|
||||
* @returns {Promise<Object>} 合并后的角色注册表
|
||||
*/
|
||||
async discoverAllRoles() {
|
||||
logger.debug('[SimplifiedRoleDiscovery] 开始发现所有角色...')
|
||||
try {
|
||||
// 并行加载,提升性能
|
||||
const [systemRoles, userRoles] = await Promise.all([
|
||||
this.loadSystemRoles(),
|
||||
this.discoverUserRoles()
|
||||
])
|
||||
|
||||
logger.debug('[SimplifiedRoleDiscovery] 系统角色数量:', Object.keys(systemRoles).length)
|
||||
logger.debug('[SimplifiedRoleDiscovery] 用户角色数量:', Object.keys(userRoles).length)
|
||||
logger.debug('[SimplifiedRoleDiscovery] 用户角色列表:', Object.keys(userRoles))
|
||||
|
||||
// 用户角色覆盖同名系统角色
|
||||
const mergedRoles = this.mergeRoles(systemRoles, userRoles)
|
||||
logger.debug('[SimplifiedRoleDiscovery] 合并后总角色数量:', Object.keys(mergedRoles).length)
|
||||
logger.debug('[SimplifiedRoleDiscovery] 最终角色列表:', Object.keys(mergedRoles))
|
||||
|
||||
return mergedRoles
|
||||
} catch (error) {
|
||||
logger.warn(`[SimplifiedRoleDiscovery] 角色发现失败: ${error.message}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载系统角色(零文件扫描)
|
||||
* @returns {Promise<Object>} 系统角色注册表
|
||||
*/
|
||||
async loadSystemRoles() {
|
||||
try {
|
||||
const registryPath = path.resolve(__dirname, '../../../resource.registry.json')
|
||||
|
||||
if (!await fs.pathExists(registryPath)) {
|
||||
console.warn('系统资源注册表文件不存在')
|
||||
return {}
|
||||
}
|
||||
|
||||
const registry = await fs.readJSON(registryPath)
|
||||
return registry.protocols?.role?.registry || {}
|
||||
} catch (error) {
|
||||
console.warn(`加载系统角色失败: ${error.message}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发现用户角色(最小化扫描)
|
||||
* @returns {Promise<Object>} 用户角色注册表
|
||||
*/
|
||||
async discoverUserRoles() {
|
||||
try {
|
||||
const userRolePath = await this.getUserRolePath()
|
||||
logger.debug('[SimplifiedRoleDiscovery] 用户角色路径:', userRolePath)
|
||||
|
||||
// 快速检查:目录不存在直接返回
|
||||
if (!await fs.pathExists(userRolePath)) {
|
||||
logger.debug('[SimplifiedRoleDiscovery] 用户角色目录不存在')
|
||||
return {}
|
||||
}
|
||||
|
||||
logger.debug('[SimplifiedRoleDiscovery] 开始扫描用户角色目录...')
|
||||
const result = await this.scanUserRolesOptimized(userRolePath)
|
||||
logger.debug('[SimplifiedRoleDiscovery] 用户角色扫描完成,发现角色:', Object.keys(result))
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.warn(`[SimplifiedRoleDiscovery] 用户角色发现失败: ${error.message}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化的用户角色扫描算法
|
||||
* @param {string} basePath - 用户角色基础路径
|
||||
* @returns {Promise<Object>} 发现的用户角色
|
||||
*/
|
||||
async scanUserRolesOptimized(basePath) {
|
||||
const roles = {}
|
||||
|
||||
try {
|
||||
// 使用withFileTypes提升性能,一次读取获得文件类型
|
||||
const entries = await fs.readdir(basePath, { withFileTypes: true })
|
||||
|
||||
// 只处理目录,跳过文件
|
||||
const directories = entries.filter(entry => entry.isDirectory())
|
||||
|
||||
// 并行检查所有角色目录(性能优化)
|
||||
const rolePromises = directories.map(dir =>
|
||||
this.checkRoleDirectory(basePath, dir.name)
|
||||
)
|
||||
|
||||
const roleResults = await Promise.allSettled(rolePromises)
|
||||
|
||||
// 收集成功的角色
|
||||
roleResults.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
const roleName = directories[index].name
|
||||
roles[roleName] = result.value
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`扫描用户角色目录失败: ${error.message}`)
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个角色目录
|
||||
* @param {string} basePath - 基础路径
|
||||
* @param {string} roleName - 角色名称
|
||||
* @returns {Promise<Object|null>} 角色信息或null
|
||||
*/
|
||||
async checkRoleDirectory(basePath, roleName) {
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 检查角色目录: ${roleName}`)
|
||||
try {
|
||||
const roleDir = path.join(basePath, roleName)
|
||||
const roleFile = path.join(roleDir, `${roleName}.role.md`)
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 角色文件路径: ${roleFile}`)
|
||||
|
||||
// 核心检查:主角色文件必须存在
|
||||
const fileExists = await fs.pathExists(roleFile)
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 角色文件${roleName}是否存在: ${fileExists}`)
|
||||
|
||||
if (!fileExists) {
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 角色${roleName}文件不存在,跳过`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 简化验证:只检查基础DPML标签
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 读取角色文件内容: ${roleName}`)
|
||||
const content = await fs.readFile(roleFile, 'utf8')
|
||||
const isValid = this.isValidRoleFile(content)
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 角色${roleName}内容验证: ${isValid}`)
|
||||
|
||||
if (!isValid) {
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 角色${roleName}内容格式无效,跳过`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 返回角色信息(简化元数据)
|
||||
const roleInfo = {
|
||||
file: roleFile,
|
||||
name: this.extractRoleName(content) || roleName,
|
||||
description: this.extractDescription(content) || `${roleName}专业角色`,
|
||||
source: 'user-generated'
|
||||
}
|
||||
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 角色${roleName}检查成功:`, roleInfo.name)
|
||||
return roleInfo
|
||||
|
||||
} catch (error) {
|
||||
// 单个角色失败不影响其他角色
|
||||
logger.warn(`[SimplifiedRoleDiscovery] 角色${roleName}检查失败: ${error.message}`)
|
||||
logger.debug(`[SimplifiedRoleDiscovery] 错误堆栈:`, error.stack)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的DPML验证(只检查关键标签)
|
||||
* @param {string} content - 文件内容
|
||||
* @returns {boolean} 是否为有效角色文件
|
||||
*/
|
||||
isValidRoleFile(content) {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
const trimmedContent = content.trim()
|
||||
if (trimmedContent.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return trimmedContent.includes('<role>') && trimmedContent.includes('</role>')
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的角色名称提取
|
||||
* @param {string} content - 文件内容
|
||||
* @returns {string|null} 提取的角色名称
|
||||
*/
|
||||
extractRoleName(content) {
|
||||
if (!content) return null
|
||||
|
||||
// 提取Markdown标题
|
||||
const match = content.match(/^#\s*(.+)$/m)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的描述提取
|
||||
* @param {string} content - 文件内容
|
||||
* @returns {string|null} 提取的描述
|
||||
*/
|
||||
extractDescription(content) {
|
||||
if (!content) return null
|
||||
|
||||
// 提取Markdown引用(描述)
|
||||
const match = content.match(/^>\s*(.+)$/m)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并角色(用户优先)
|
||||
* @param {Object} systemRoles - 系统角色
|
||||
* @param {Object} userRoles - 用户角色
|
||||
* @returns {Object} 合并后的角色注册表
|
||||
*/
|
||||
mergeRoles(systemRoles, userRoles) {
|
||||
if (!systemRoles || typeof systemRoles !== 'object') {
|
||||
systemRoles = {}
|
||||
}
|
||||
|
||||
if (!userRoles || typeof userRoles !== 'object') {
|
||||
userRoles = {}
|
||||
}
|
||||
|
||||
return {
|
||||
...systemRoles, // 系统角色作为基础
|
||||
...userRoles // 用户角色覆盖同名系统角色
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色路径
|
||||
* @returns {Promise<string>} 用户角色目录路径
|
||||
*/
|
||||
async getUserRolePath() {
|
||||
const projectRoot = await this.findProjectRoot()
|
||||
return path.join(projectRoot, this.USER_RESOURCE_DIR, ...this.RESOURCE_DOMAIN_PATH)
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化的项目根目录查找
|
||||
* @returns {Promise<string>} 项目根目录路径
|
||||
*/
|
||||
async findProjectRoot() {
|
||||
let currentDir = process.cwd()
|
||||
|
||||
// 向上查找包含package.json的目录
|
||||
while (currentDir !== path.dirname(currentDir)) {
|
||||
const packageJsonPath = path.join(currentDir, 'package.json')
|
||||
|
||||
try {
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
return currentDir
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略权限错误,继续向上查找
|
||||
}
|
||||
|
||||
currentDir = path.dirname(currentDir)
|
||||
}
|
||||
|
||||
// 如果没找到package.json,返回当前工作目录
|
||||
return process.cwd()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SimplifiedRoleDiscovery
|
||||
@ -2,7 +2,10 @@ const path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
const os = require('os')
|
||||
|
||||
describe('跨平台角色发现兼容性测试', () => {
|
||||
// Import the new SimplifiedRoleDiscovery for testing
|
||||
const SimplifiedRoleDiscovery = require('../../lib/core/resource/SimplifiedRoleDiscovery')
|
||||
|
||||
describe('跨平台角色发现兼容性测试 - 优化版', () => {
|
||||
let tempDir
|
||||
let projectDir
|
||||
|
||||
@ -11,65 +14,60 @@ describe('跨平台角色发现兼容性测试', () => {
|
||||
projectDir = path.join(tempDir, 'test-project')
|
||||
|
||||
await fs.ensureDir(path.join(projectDir, 'prompt', 'domain'))
|
||||
await fs.ensureDir(path.join(projectDir, '.promptx', 'user-roles'))
|
||||
await fs.ensureDir(path.join(projectDir, '.promptx', 'resource', 'domain'))
|
||||
await fs.writeFile(
|
||||
path.join(projectDir, 'package.json'),
|
||||
JSON.stringify({ name: 'test-project', version: '1.0.0' })
|
||||
)
|
||||
|
||||
// Mock process.cwd to point to our test project
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(projectDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Node.js 原生 API 替代 glob', () => {
|
||||
test('应该能使用 fs.readdir 代替 glob.sync', async () => {
|
||||
// 创建测试角色文件
|
||||
const roleDir = path.join(projectDir, 'prompt', 'domain', 'test-role')
|
||||
describe('SimplifiedRoleDiscovery 跨平台兼容性', () => {
|
||||
test('应该使用原生API替代glob发现用户角色', async () => {
|
||||
// 创建用户角色文件
|
||||
const roleDir = path.join(projectDir, '.promptx', 'resource', 'domain', 'test-user-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'test-role.role.md'),
|
||||
'<role><personality>测试</personality></role>'
|
||||
path.join(roleDir, 'test-user-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)
|
||||
const discovery = new SimplifiedRoleDiscovery()
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
expect(discoveredRoles).toHaveProperty('test-role')
|
||||
expect(discoveredRoles['test-role'].source).toBe('native-api')
|
||||
expect(userRoles).toHaveProperty('test-user-role')
|
||||
expect(userRoles['test-user-role'].source).toBe('user-generated')
|
||||
})
|
||||
|
||||
test('应该正确合并系统角色和用户角色', async () => {
|
||||
// 创建用户角色(与系统角色同名,应该覆盖)
|
||||
const roleDir = path.join(projectDir, '.promptx', 'resource', 'domain', 'assistant')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'assistant.role.md'),
|
||||
`# 自定义助手
|
||||
> 用户自定义的助手角色
|
||||
<role><personality>自定义助手</personality></role>`
|
||||
)
|
||||
|
||||
const discovery = new SimplifiedRoleDiscovery()
|
||||
const allRoles = await discovery.discoverAllRoles()
|
||||
|
||||
// 应该包含系统角色和用户角色
|
||||
expect(allRoles).toHaveProperty('assistant')
|
||||
|
||||
// 用户角色应该覆盖系统角色
|
||||
expect(allRoles.assistant.source).toBe('user-generated')
|
||||
expect(allRoles.assistant.name).toBe('自定义助手')
|
||||
})
|
||||
|
||||
test('应该能处理不同平台的路径分隔符', () => {
|
||||
|
||||
@ -54,24 +54,20 @@ describe('HelloCommand - ResourceManager集成', () => {
|
||||
|
||||
await fs.writeFile(path.join(roleDir, 'sales-expert.role.md'), roleContent)
|
||||
|
||||
// Mock ResourceManager的loadUnifiedRegistry方法
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
// Mock SimplifiedRoleDiscovery的discoverAllRoles方法
|
||||
jest.spyOn(helloCommand.discovery, 'discoverAllRoles')
|
||||
.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'
|
||||
}
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'system'
|
||||
},
|
||||
'sales-expert': {
|
||||
file: path.join(roleDir, 'sales-expert.role.md'),
|
||||
name: '销售专家',
|
||||
description: '专业销售角色,提供销售技巧和客户关系管理',
|
||||
source: 'user-generated'
|
||||
}
|
||||
})
|
||||
|
||||
@ -113,17 +109,14 @@ describe('HelloCommand - ResourceManager集成', () => {
|
||||
|
||||
await fs.writeFile(path.join(roleDir, 'assistant.role.md'), customAssistantContent)
|
||||
|
||||
// Mock ResourceManager返回用户覆盖的角色
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
// Mock SimplifiedRoleDiscovery返回用户覆盖的角色
|
||||
jest.spyOn(helloCommand.discovery, 'discoverAllRoles')
|
||||
.mockResolvedValue({
|
||||
role: {
|
||||
'assistant': {
|
||||
file: path.join(roleDir, 'assistant.role.md'),
|
||||
name: '定制智能助手',
|
||||
source: 'user-generated',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
}
|
||||
'assistant': {
|
||||
file: path.join(roleDir, 'assistant.role.md'),
|
||||
name: '定制智能助手',
|
||||
description: '专业技术助手,专注于编程和技术解决方案',
|
||||
source: 'user-generated'
|
||||
}
|
||||
})
|
||||
|
||||
@ -164,31 +157,26 @@ describe('HelloCommand - ResourceManager集成', () => {
|
||||
|
||||
await fs.writeFile(path.join(userRoleDir, 'data-analyst.role.md'), userRoleContent)
|
||||
|
||||
// Mock ResourceManager返回系统和用户角色
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
// Mock SimplifiedRoleDiscovery返回系统和用户角色
|
||||
jest.spyOn(helloCommand.discovery, 'discoverAllRoles')
|
||||
.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'
|
||||
}
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'system'
|
||||
},
|
||||
'java-backend-developer': {
|
||||
file: '@package://prompt/domain/java-backend-developer/java-backend-developer.role.md',
|
||||
name: '☕ Java后端开发专家',
|
||||
description: '专业Java后端开发专家,精通Spring生态系统、微服务架构和系统设计',
|
||||
source: 'system'
|
||||
},
|
||||
'data-analyst': {
|
||||
file: path.join(userRoleDir, 'data-analyst.role.md'),
|
||||
name: '数据分析师',
|
||||
description: '专业数据分析师,提供数据洞察和统计分析',
|
||||
source: 'user-generated'
|
||||
}
|
||||
})
|
||||
|
||||
@ -206,8 +194,8 @@ describe('HelloCommand - ResourceManager集成', () => {
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('应该优雅处理资源发现失败', async () => {
|
||||
// 模拟ResourceManager错误
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
// 模拟SimplifiedRoleDiscovery错误
|
||||
jest.spyOn(helloCommand.discovery, 'discoverAllRoles')
|
||||
.mockRejectedValue(new Error('资源发现失败'))
|
||||
|
||||
// 应该不抛出异常
|
||||
@ -219,8 +207,8 @@ describe('HelloCommand - ResourceManager集成', () => {
|
||||
|
||||
it('应该处理空的资源注册表', async () => {
|
||||
// Mock空的资源注册表
|
||||
jest.spyOn(ResourceManager.prototype, 'loadUnifiedRegistry')
|
||||
.mockResolvedValue({ role: {} })
|
||||
jest.spyOn(helloCommand.discovery, 'discoverAllRoles')
|
||||
.mockResolvedValue({})
|
||||
|
||||
const result = await helloCommand.execute([])
|
||||
|
||||
|
||||
@ -24,17 +24,15 @@ describe('HelloCommand 单元测试', () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
// 清理缓存
|
||||
if (helloCommand.roleRegistry) {
|
||||
helloCommand.roleRegistry = null
|
||||
}
|
||||
// 清理 mock
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('基础功能测试', () => {
|
||||
test('应该能实例化HelloCommand', () => {
|
||||
expect(helloCommand).toBeInstanceOf(HelloCommand)
|
||||
expect(typeof helloCommand.discoverLocalRoles).toBe('function')
|
||||
expect(typeof helloCommand.loadRoleRegistry).toBe('function')
|
||||
expect(helloCommand.discovery).toBeDefined()
|
||||
})
|
||||
|
||||
test('getPurpose应该返回正确的目的描述', () => {
|
||||
@ -44,198 +42,67 @@ describe('HelloCommand 单元测试', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('discoverLocalRoles 功能测试', () => {
|
||||
describe('SimplifiedRoleDiscovery 集成测试', () => {
|
||||
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
|
||||
// Mock SimplifiedRoleDiscovery.discoverAllRoles 返回系统角色
|
||||
const mockDiscovery = {
|
||||
discoverAllRoles: jest.fn().mockResolvedValue({
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持',
|
||||
source: 'system'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 重新加载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()
|
||||
helloCommand.discovery = mockDiscovery
|
||||
const roleRegistry = await helloCommand.loadRoleRegistry()
|
||||
|
||||
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')
|
||||
expect(roleRegistry).toHaveProperty('assistant')
|
||||
expect(roleRegistry.assistant.name).toContain('智能助手')
|
||||
expect(roleRegistry.assistant.description).toContain('助理')
|
||||
expect(roleRegistry.assistant.source).toBe('system')
|
||||
})
|
||||
|
||||
test('应该处理空的角色目录', async () => {
|
||||
// Mock PackageProtocol.getPackageRoot 返回空目录
|
||||
jest.doMock('../../lib/core/resource/protocols/PackageProtocol', () => {
|
||||
return class MockPackageProtocol {
|
||||
async getPackageRoot() {
|
||||
return tempProjectDir
|
||||
}
|
||||
}
|
||||
})
|
||||
// Mock SimplifiedRoleDiscovery.discoverAllRoles 返回空对象
|
||||
const mockDiscovery = {
|
||||
discoverAllRoles: jest.fn().mockResolvedValue({})
|
||||
}
|
||||
|
||||
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({})
|
||||
helloCommand.discovery = mockDiscovery
|
||||
const roleRegistry = await helloCommand.loadRoleRegistry()
|
||||
|
||||
jest.unmock('../../lib/core/resource/protocols/PackageProtocol')
|
||||
// 应该返回fallback assistant角色
|
||||
expect(roleRegistry).toHaveProperty('assistant')
|
||||
expect(roleRegistry.assistant.source).toBe('fallback')
|
||||
})
|
||||
|
||||
test('应该优雅处理文件读取错误', async () => {
|
||||
// 创建无效的角色文件(权限问题)
|
||||
const invalidRoleDir = path.join(tempProjectDir, 'prompt', 'domain', 'invalid')
|
||||
await fs.ensureDir(invalidRoleDir)
|
||||
test('应该使用SimplifiedRoleDiscovery处理错误', async () => {
|
||||
const mockedCommand = new HelloCommand()
|
||||
|
||||
const invalidRoleFile = path.join(invalidRoleDir, 'invalid.role.md')
|
||||
await fs.writeFile(invalidRoleFile, 'invalid content')
|
||||
// Mock discovery to throw an error
|
||||
mockedCommand.discovery.discoverAllRoles = jest.fn().mockRejectedValue(new Error('Mock error'))
|
||||
|
||||
// 修改文件权限使其不可读(仅在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')
|
||||
// 应该fallback到默认assistant角色
|
||||
const roleRegistry = await mockedCommand.loadRoleRegistry()
|
||||
expect(roleRegistry).toHaveProperty('assistant')
|
||||
expect(roleRegistry.assistant.source).toBe('fallback')
|
||||
})
|
||||
})
|
||||
|
||||
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('应该正确提取角色描述', () => {
|
||||
const roleInfo = { description: '这是一个测试用的角色' }
|
||||
const extracted = helloCommand.extractDescription(roleInfo)
|
||||
expect(extracted).toBe('这是一个测试用的角色')
|
||||
})
|
||||
|
||||
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')
|
||||
test('应该处理缺少元数据的角色文件', () => {
|
||||
const roleInfo = { name: 'test-role' }
|
||||
const extracted = helloCommand.extractDescription(roleInfo)
|
||||
expect(extracted).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@ -244,46 +111,33 @@ description: 这是一个测试用的角色
|
||||
const result = await helloCommand.loadRoleRegistry()
|
||||
|
||||
expect(typeof result).toBe('object')
|
||||
expect(helloCommand.roleRegistry).toBe(result)
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
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 mockedCommand = new HelloCommand()
|
||||
|
||||
// Mock discovery to throw an error
|
||||
mockedCommand.discovery.discoverAllRoles = jest.fn().mockRejectedValue(new Error('Mock error'))
|
||||
|
||||
const result = await mockedCommand.loadRoleRegistry()
|
||||
|
||||
expect(result).toHaveProperty('assistant')
|
||||
expect(result.assistant.name).toContain('智能助手')
|
||||
|
||||
jest.unmock('../../lib/core/resource/resourceManager')
|
||||
helloCommand.discoverLocalRoles.mockRestore()
|
||||
expect(result.assistant.source).toBe('fallback')
|
||||
})
|
||||
})
|
||||
|
||||
describe('角色信息获取测试', () => {
|
||||
test('getRoleInfo应该返回正确的角色信息', async () => {
|
||||
// Mock注册表
|
||||
helloCommand.roleRegistry = {
|
||||
// Mock loadRoleRegistry 方法
|
||||
helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({
|
||||
'test-role': {
|
||||
name: '测试角色',
|
||||
description: '测试描述',
|
||||
file: '@package://test/path'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const roleInfo = await helloCommand.getRoleInfo('test-role')
|
||||
|
||||
@ -296,7 +150,7 @@ description: 这是一个测试用的角色
|
||||
})
|
||||
|
||||
test('getRoleInfo对不存在的角色应该返回null', async () => {
|
||||
helloCommand.roleRegistry = {}
|
||||
helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({})
|
||||
|
||||
const roleInfo = await helloCommand.getRoleInfo('non-existent')
|
||||
expect(roleInfo).toBeNull()
|
||||
@ -305,19 +159,42 @@ description: 这是一个测试用的角色
|
||||
|
||||
describe('getAllRoles测试', () => {
|
||||
test('应该返回角色数组格式', async () => {
|
||||
helloCommand.roleRegistry = {
|
||||
'role1': { name: '角色1', description: '描述1', file: 'file1' },
|
||||
'role2': { name: '角色2', description: '描述2', file: 'file2' }
|
||||
}
|
||||
// Mock loadRoleRegistry 方法
|
||||
helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({
|
||||
'role1': {
|
||||
name: '角色1',
|
||||
description: '描述1',
|
||||
file: 'file1',
|
||||
source: 'system'
|
||||
},
|
||||
'role2': {
|
||||
name: '角色2',
|
||||
description: '描述2',
|
||||
file: 'file2',
|
||||
source: 'user-generated'
|
||||
}
|
||||
})
|
||||
|
||||
const allRoles = await helloCommand.getAllRoles()
|
||||
const roles = 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')
|
||||
expect(Array.isArray(roles)).toBe(true)
|
||||
expect(roles).toHaveLength(2)
|
||||
|
||||
expect(roles[0]).toEqual({
|
||||
id: 'role1',
|
||||
name: '角色1',
|
||||
description: '描述1',
|
||||
file: 'file1',
|
||||
source: 'system'
|
||||
})
|
||||
|
||||
expect(roles[1]).toEqual({
|
||||
id: 'role2',
|
||||
name: '角色2',
|
||||
description: '描述2',
|
||||
file: 'file2',
|
||||
source: 'user-generated'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,525 +0,0 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
106
src/tests/core/resource/DPMLContentParser.integration.test.js
Normal file
106
src/tests/core/resource/DPMLContentParser.integration.test.js
Normal file
@ -0,0 +1,106 @@
|
||||
const DPMLContentParser = require('../../../lib/core/resource/DPMLContentParser')
|
||||
const path = require('path')
|
||||
const fs = require('fs-extra')
|
||||
|
||||
describe('DPMLContentParser 集成测试', () => {
|
||||
let parser
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new DPMLContentParser()
|
||||
})
|
||||
|
||||
describe('真实角色文件测试', () => {
|
||||
test('应该正确解析internet-debater角色的完整内容', async () => {
|
||||
const roleFile = '/Users/sean/WorkSpaces/DeepracticeProjects/PromptX/.promptx/resource/domain/internet-debater/internet-debater.role.md'
|
||||
|
||||
// 检查文件是否存在
|
||||
const exists = await fs.pathExists(roleFile)
|
||||
if (!exists) {
|
||||
console.log('跳过测试:internet-debater角色文件不存在')
|
||||
return
|
||||
}
|
||||
|
||||
const roleContent = await fs.readFile(roleFile, 'utf-8')
|
||||
const roleSemantics = parser.parseRoleDocument(roleContent)
|
||||
|
||||
// 验证personality解析
|
||||
expect(roleSemantics).toHaveProperty('personality')
|
||||
expect(roleSemantics.personality.metadata.contentType).toBe('direct-only')
|
||||
expect(roleSemantics.personality.directContent).toContain('网络杠精思维模式')
|
||||
expect(roleSemantics.personality.directContent).toContain('挑刺思维')
|
||||
expect(roleSemantics.personality.directContent).toContain('抬杠本能')
|
||||
|
||||
// 验证principle解析
|
||||
expect(roleSemantics).toHaveProperty('principle')
|
||||
expect(roleSemantics.principle.metadata.contentType).toBe('direct-only')
|
||||
expect(roleSemantics.principle.directContent).toContain('网络杠精行为原则')
|
||||
expect(roleSemantics.principle.directContent).toContain('逢言必反')
|
||||
expect(roleSemantics.principle.directContent).toContain('抠字眼优先')
|
||||
|
||||
// 验证knowledge解析
|
||||
expect(roleSemantics).toHaveProperty('knowledge')
|
||||
expect(roleSemantics.knowledge.metadata.contentType).toBe('direct-only')
|
||||
expect(roleSemantics.knowledge.directContent).toContain('网络杠精专业知识体系')
|
||||
expect(roleSemantics.knowledge.directContent).toContain('逻辑谬误大全')
|
||||
expect(roleSemantics.knowledge.directContent).toContain('稻草人谬误')
|
||||
|
||||
console.log('✅ internet-debater角色内容完整解析成功')
|
||||
console.log(` - personality: ${roleSemantics.personality.directContent.length} 字符`)
|
||||
console.log(` - principle: ${roleSemantics.principle.directContent.length} 字符`)
|
||||
console.log(` - knowledge: ${roleSemantics.knowledge.directContent.length} 字符`)
|
||||
})
|
||||
|
||||
test('应该正确解析系统角色的@引用内容', async () => {
|
||||
const roleFile = '/Users/sean/WorkSpaces/DeepracticeProjects/PromptX/prompt/domain/assistant/assistant.role.md'
|
||||
|
||||
const exists = await fs.pathExists(roleFile)
|
||||
if (!exists) {
|
||||
console.log('跳过测试:assistant角色文件不存在')
|
||||
return
|
||||
}
|
||||
|
||||
const roleContent = await fs.readFile(roleFile, 'utf-8')
|
||||
const roleSemantics = parser.parseRoleDocument(roleContent)
|
||||
|
||||
// 验证personality有@引用
|
||||
if (roleSemantics.personality) {
|
||||
expect(roleSemantics.personality.references.length).toBeGreaterThan(0)
|
||||
console.log('✅ assistant角色@引用解析成功')
|
||||
console.log(` - personality引用数量: ${roleSemantics.personality.references.length}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('修复前后对比测试', () => {
|
||||
test('模拟ActionCommand当前解析vs新解析的差异', () => {
|
||||
const mixedContent = `@!thought://remember
|
||||
@!thought://recall
|
||||
|
||||
# 网络杠精思维模式
|
||||
## 核心思维特征
|
||||
- 挑刺思维:看到任何观点都先找问题和漏洞
|
||||
- 抬杠本能:天生反对派,习惯性质疑一切表述`
|
||||
|
||||
// 模拟当前ActionCommand的解析(只提取@引用)
|
||||
const currentParsing = {
|
||||
thoughtReferences: ['remember', 'recall'],
|
||||
directContent: '' // 完全丢失
|
||||
}
|
||||
|
||||
// 新的DPMLContentParser解析
|
||||
const newParsing = parser.parseTagContent(mixedContent, 'personality')
|
||||
|
||||
// 对比结果
|
||||
expect(newParsing.references).toHaveLength(2)
|
||||
expect(newParsing.references.map(r => r.resource)).toEqual(['remember', 'recall'])
|
||||
expect(newParsing.directContent).toContain('网络杠精思维模式')
|
||||
expect(newParsing.directContent).toContain('挑刺思维')
|
||||
expect(newParsing.directContent).toContain('抬杠本能')
|
||||
|
||||
console.log('📊 解析能力对比:')
|
||||
console.log(` 当前ActionCommand: 只解析${currentParsing.thoughtReferences.length}个引用,丢失${newParsing.directContent.length}字符直接内容`)
|
||||
console.log(` 新DPMLContentParser: 解析${newParsing.references.length}个引用 + ${newParsing.directContent.length}字符直接内容`)
|
||||
console.log(` 🎯 语义完整性提升: ${((newParsing.directContent.length / mixedContent.length) * 100).toFixed(1)}%`)
|
||||
})
|
||||
})
|
||||
})
|
||||
174
src/tests/core/resource/DPMLContentParser.position.unit.test.js
Normal file
174
src/tests/core/resource/DPMLContentParser.position.unit.test.js
Normal file
@ -0,0 +1,174 @@
|
||||
const DPMLContentParser = require('../../../lib/core/resource/DPMLContentParser')
|
||||
|
||||
describe('DPMLContentParser - Position Extension', () => {
|
||||
let parser
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new DPMLContentParser()
|
||||
})
|
||||
|
||||
describe('extractReferencesWithPosition', () => {
|
||||
test('应该提取引用的位置信息', () => {
|
||||
// Arrange
|
||||
const content = 'intro @!thought://A middle @!thought://B end'
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toHaveLength(2)
|
||||
expect(references[0]).toEqual({
|
||||
fullMatch: '@!thought://A',
|
||||
priority: '!',
|
||||
protocol: 'thought',
|
||||
resource: 'A',
|
||||
position: 6,
|
||||
isRequired: true,
|
||||
isOptional: false
|
||||
})
|
||||
expect(references[1]).toEqual({
|
||||
fullMatch: '@!thought://B',
|
||||
priority: '!',
|
||||
protocol: 'thought',
|
||||
resource: 'B',
|
||||
position: 27,
|
||||
isRequired: true,
|
||||
isOptional: false
|
||||
})
|
||||
})
|
||||
|
||||
test('应该按位置排序返回引用', () => {
|
||||
// Arrange
|
||||
const content = '@!thought://second @!thought://first'
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toHaveLength(2)
|
||||
expect(references[0].resource).toBe('second')
|
||||
expect(references[0].position).toBe(0)
|
||||
expect(references[1].resource).toBe('first')
|
||||
expect(references[1].position).toBe(19)
|
||||
})
|
||||
|
||||
test('应该处理复杂布局中的位置信息', () => {
|
||||
// Arrange
|
||||
const content = `# 标题
|
||||
@!thought://base
|
||||
|
||||
## 子标题
|
||||
- 列表项1
|
||||
@!execution://action
|
||||
- 列表项2`
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toHaveLength(2)
|
||||
|
||||
const baseRef = references.find(ref => ref.resource === 'base')
|
||||
const actionRef = references.find(ref => ref.resource === 'action')
|
||||
|
||||
expect(baseRef.position).toBe(5) // 在"# 标题\n"之后
|
||||
expect(actionRef.position).toBeGreaterThan(baseRef.position)
|
||||
})
|
||||
|
||||
test('应该处理可选引用', () => {
|
||||
// Arrange
|
||||
const content = 'content @?optional://resource more'
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toHaveLength(1)
|
||||
expect(references[0]).toEqual({
|
||||
fullMatch: '@?optional://resource',
|
||||
priority: '?',
|
||||
protocol: 'optional',
|
||||
resource: 'resource',
|
||||
position: 8,
|
||||
isRequired: false,
|
||||
isOptional: true
|
||||
})
|
||||
})
|
||||
|
||||
test('应该处理普通引用(无优先级标记)', () => {
|
||||
// Arrange
|
||||
const content = 'content @normal://resource more'
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toHaveLength(1)
|
||||
expect(references[0]).toEqual({
|
||||
fullMatch: '@normal://resource',
|
||||
priority: '',
|
||||
protocol: 'normal',
|
||||
resource: 'resource',
|
||||
position: 8,
|
||||
isRequired: false,
|
||||
isOptional: false
|
||||
})
|
||||
})
|
||||
|
||||
test('应该处理空内容', () => {
|
||||
// Arrange
|
||||
const content = ''
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toEqual([])
|
||||
})
|
||||
|
||||
test('应该处理没有引用的内容', () => {
|
||||
// Arrange
|
||||
const content = '这是一段没有任何引用的普通文本内容'
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toEqual([])
|
||||
})
|
||||
|
||||
test('应该处理多行内容中的引用位置', () => {
|
||||
// Arrange
|
||||
const content = `第一行内容
|
||||
@!thought://first
|
||||
第二行内容
|
||||
@!thought://second
|
||||
第三行内容`
|
||||
|
||||
// Act
|
||||
const references = parser.extractReferencesWithPosition(content)
|
||||
|
||||
// Assert
|
||||
expect(references).toHaveLength(2)
|
||||
expect(references[0].resource).toBe('first')
|
||||
expect(references[0].position).toBe(6) // 在"第一行内容\n"之后
|
||||
expect(references[1].resource).toBe('second')
|
||||
expect(references[1].position).toBeGreaterThan(references[0].position)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseTagContent - 位置信息集成', () => {
|
||||
test('应该在parseTagContent中包含位置信息', () => {
|
||||
// Arrange
|
||||
const content = 'intro @!thought://A middle @!thought://B end'
|
||||
|
||||
// Act
|
||||
const result = parser.parseTagContent(content, 'personality')
|
||||
|
||||
// Assert
|
||||
expect(result.references).toHaveLength(2)
|
||||
expect(result.references[0].position).toBe(6)
|
||||
expect(result.references[1].position).toBe(27)
|
||||
})
|
||||
})
|
||||
})
|
||||
236
src/tests/core/resource/DPMLContentParser.unit.test.js
Normal file
236
src/tests/core/resource/DPMLContentParser.unit.test.js
Normal file
@ -0,0 +1,236 @@
|
||||
const DPMLContentParser = require('../../../lib/core/resource/DPMLContentParser')
|
||||
|
||||
describe('DPMLContentParser 单元测试', () => {
|
||||
let parser
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new DPMLContentParser()
|
||||
})
|
||||
|
||||
describe('基础功能测试', () => {
|
||||
test('应该能实例化DPMLContentParser', () => {
|
||||
expect(parser).toBeInstanceOf(DPMLContentParser)
|
||||
expect(typeof parser.parseTagContent).toBe('function')
|
||||
expect(typeof parser.extractReferences).toBe('function')
|
||||
expect(typeof parser.extractDirectContent).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('引用解析测试', () => {
|
||||
test('应该正确解析单个@引用', () => {
|
||||
const content = '@!thought://remember'
|
||||
const references = parser.extractReferences(content)
|
||||
|
||||
expect(references).toHaveLength(1)
|
||||
expect(references[0]).toEqual({
|
||||
fullMatch: '@!thought://remember',
|
||||
priority: '!',
|
||||
protocol: 'thought',
|
||||
resource: 'remember',
|
||||
isRequired: true,
|
||||
isOptional: false
|
||||
})
|
||||
})
|
||||
|
||||
test('应该正确解析多个@引用', () => {
|
||||
const content = `@!thought://remember
|
||||
@?execution://assistant
|
||||
@thought://recall`
|
||||
const references = parser.extractReferences(content)
|
||||
|
||||
expect(references).toHaveLength(3)
|
||||
expect(references[0].resource).toBe('remember')
|
||||
expect(references[1].resource).toBe('assistant')
|
||||
expect(references[2].resource).toBe('recall')
|
||||
expect(references[0].isRequired).toBe(true)
|
||||
expect(references[1].isOptional).toBe(true)
|
||||
expect(references[2].isRequired).toBe(false)
|
||||
})
|
||||
|
||||
test('应该处理没有@引用的内容', () => {
|
||||
const content = '# 这是直接内容\n- 列表项目'
|
||||
const references = parser.extractReferences(content)
|
||||
|
||||
expect(references).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('直接内容提取测试', () => {
|
||||
test('应该正确提取纯直接内容', () => {
|
||||
const content = `# 网络杠精思维模式
|
||||
## 核心思维特征
|
||||
- 挑刺思维:看到任何观点都先找问题
|
||||
- 抬杠本能:天生反对派`
|
||||
|
||||
const directContent = parser.extractDirectContent(content)
|
||||
|
||||
expect(directContent).toContain('网络杠精思维模式')
|
||||
expect(directContent).toContain('挑刺思维')
|
||||
expect(directContent).toContain('抬杠本能')
|
||||
})
|
||||
|
||||
test('应该从混合内容中过滤掉@引用', () => {
|
||||
const content = `@!thought://remember
|
||||
|
||||
# 直接编写的个性特征
|
||||
- 特征1
|
||||
- 特征2
|
||||
|
||||
@!execution://assistant`
|
||||
|
||||
const directContent = parser.extractDirectContent(content)
|
||||
|
||||
expect(directContent).toContain('直接编写的个性特征')
|
||||
expect(directContent).toContain('特征1')
|
||||
expect(directContent).not.toContain('@!thought://remember')
|
||||
expect(directContent).not.toContain('@!execution://assistant')
|
||||
})
|
||||
|
||||
test('应该处理只有@引用没有直接内容的情况', () => {
|
||||
const content = '@!thought://remember\n@!execution://assistant'
|
||||
const directContent = parser.extractDirectContent(content)
|
||||
|
||||
expect(directContent).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('标签内容解析测试', () => {
|
||||
test('应该解析混合内容标签', () => {
|
||||
const content = `@!thought://remember
|
||||
@!thought://recall
|
||||
|
||||
# 网络杠精思维模式
|
||||
## 核心思维特征
|
||||
- 挑刺思维:看到任何观点都先找问题和漏洞
|
||||
- 抬杠本能:天生反对派,习惯性质疑一切表述`
|
||||
|
||||
const result = parser.parseTagContent(content, 'personality')
|
||||
|
||||
expect(result.fullSemantics).toBe(content.trim())
|
||||
expect(result.references).toHaveLength(2)
|
||||
expect(result.references[0].resource).toBe('remember')
|
||||
expect(result.references[1].resource).toBe('recall')
|
||||
expect(result.directContent).toContain('网络杠精思维模式')
|
||||
expect(result.directContent).toContain('挑刺思维')
|
||||
expect(result.metadata.tagName).toBe('personality')
|
||||
expect(result.metadata.hasReferences).toBe(true)
|
||||
expect(result.metadata.hasDirectContent).toBe(true)
|
||||
expect(result.metadata.contentType).toBe('mixed')
|
||||
})
|
||||
|
||||
test('应该解析纯@引用标签', () => {
|
||||
const content = `@!thought://remember
|
||||
@!thought://assistant
|
||||
@!execution://assistant`
|
||||
|
||||
const result = parser.parseTagContent(content, 'personality')
|
||||
|
||||
expect(result.references).toHaveLength(3)
|
||||
expect(result.directContent).toBe('')
|
||||
expect(result.metadata.contentType).toBe('references-only')
|
||||
})
|
||||
|
||||
test('应该解析纯直接内容标签', () => {
|
||||
const content = `# 网络杠精思维模式
|
||||
## 核心思维特征
|
||||
- 挑刺思维:看到任何观点都先找问题和漏洞`
|
||||
|
||||
const result = parser.parseTagContent(content, 'personality')
|
||||
|
||||
expect(result.references).toHaveLength(0)
|
||||
expect(result.directContent).toContain('网络杠精思维模式')
|
||||
expect(result.metadata.contentType).toBe('direct-only')
|
||||
})
|
||||
|
||||
test('应该处理空标签', () => {
|
||||
const result = parser.parseTagContent('', 'personality')
|
||||
|
||||
expect(result.fullSemantics).toBe('')
|
||||
expect(result.references).toHaveLength(0)
|
||||
expect(result.directContent).toBe('')
|
||||
expect(result.metadata.contentType).toBe('empty')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DPML文档解析测试', () => {
|
||||
test('应该从DPML文档中提取标签内容', () => {
|
||||
const dpmlContent = `<role>
|
||||
<personality>
|
||||
@!thought://remember
|
||||
# 个性特征
|
||||
</personality>
|
||||
<principle>
|
||||
@!execution://assistant
|
||||
# 行为原则
|
||||
</principle>
|
||||
</role>`
|
||||
|
||||
const personalityContent = parser.extractTagContent(dpmlContent, 'personality')
|
||||
const principleContent = parser.extractTagContent(dpmlContent, 'principle')
|
||||
|
||||
expect(personalityContent).toContain('@!thought://remember')
|
||||
expect(personalityContent).toContain('个性特征')
|
||||
expect(principleContent).toContain('@!execution://assistant')
|
||||
expect(principleContent).toContain('行为原则')
|
||||
})
|
||||
|
||||
test('应该解析完整的角色文档', () => {
|
||||
const roleContent = `<role>
|
||||
<personality>
|
||||
@!thought://remember
|
||||
# 杠精思维特征
|
||||
</personality>
|
||||
<principle>
|
||||
@!execution://assistant
|
||||
# 抬杠行为原则
|
||||
</principle>
|
||||
<knowledge>
|
||||
# 逻辑谬误知识体系
|
||||
</knowledge>
|
||||
</role>`
|
||||
|
||||
const roleSemantics = parser.parseRoleDocument(roleContent)
|
||||
|
||||
expect(roleSemantics).toHaveProperty('personality')
|
||||
expect(roleSemantics).toHaveProperty('principle')
|
||||
expect(roleSemantics).toHaveProperty('knowledge')
|
||||
|
||||
expect(roleSemantics.personality.metadata.contentType).toBe('mixed')
|
||||
expect(roleSemantics.principle.metadata.contentType).toBe('mixed')
|
||||
expect(roleSemantics.knowledge.metadata.contentType).toBe('direct-only')
|
||||
})
|
||||
})
|
||||
|
||||
describe('边界情况测试', () => {
|
||||
test('应该处理复杂的@引用格式', () => {
|
||||
const content = '@!protocol://complex-resource/with-path.execution'
|
||||
const references = parser.extractReferences(content)
|
||||
|
||||
expect(references).toHaveLength(1)
|
||||
expect(references[0].resource).toBe('complex-resource/with-path.execution')
|
||||
})
|
||||
|
||||
test('应该处理包含@符号但非引用的内容', () => {
|
||||
const content = '邮箱地址:user@example.com 不应该被识别为引用'
|
||||
const references = parser.extractReferences(content)
|
||||
|
||||
expect(references).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('应该正确清理多余的空行', () => {
|
||||
const content = `@!thought://remember
|
||||
|
||||
|
||||
|
||||
# 标题
|
||||
|
||||
|
||||
|
||||
内容`
|
||||
|
||||
const directContent = parser.extractDirectContent(content)
|
||||
|
||||
expect(directContent).toBe('# 标题\n\n内容')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -144,15 +144,17 @@ describe('ResourceManager - 用户资源发现', () => {
|
||||
|
||||
describe('loadUnifiedRegistry', () => {
|
||||
it('应该合并系统资源和用户资源', async () => {
|
||||
// 模拟系统资源
|
||||
// 模拟系统资源(使用正确的registry格式)
|
||||
const mockSystemResources = {
|
||||
role: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
protocols: {
|
||||
role: {
|
||||
registry: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,15 +183,17 @@ describe('ResourceManager - 用户资源发现', () => {
|
||||
})
|
||||
|
||||
it('应该让用户资源覆盖同名系统资源', async () => {
|
||||
// 模拟系统资源
|
||||
// 模拟系统资源(使用正确的registry格式)
|
||||
const mockSystemResources = {
|
||||
role: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
source: 'system',
|
||||
format: 'dpml',
|
||||
type: 'role'
|
||||
protocols: {
|
||||
role: {
|
||||
registry: {
|
||||
'assistant': {
|
||||
file: '@package://prompt/domain/assistant/assistant.role.md',
|
||||
name: '🙋 智能助手',
|
||||
description: '通用助理角色,提供基础的助理服务和记忆支持'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
344
src/tests/core/resource/RoleDiscoveryEdgeCases.unit.test.js
Normal file
344
src/tests/core/resource/RoleDiscoveryEdgeCases.unit.test.js
Normal file
@ -0,0 +1,344 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
const SimplifiedRoleDiscovery = require('../../../lib/core/resource/SimplifiedRoleDiscovery')
|
||||
|
||||
describe('Role Discovery Edge Cases', () => {
|
||||
let tempDir
|
||||
let testProjectDir
|
||||
let discovery
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'role-discovery-edge-'))
|
||||
testProjectDir = path.join(tempDir, 'edge-test-project')
|
||||
|
||||
await fs.ensureDir(path.join(testProjectDir, '.promptx', 'resource', 'domain'))
|
||||
await fs.writeFile(
|
||||
path.join(testProjectDir, 'package.json'),
|
||||
JSON.stringify({ name: 'edge-test-project', version: '1.0.0' })
|
||||
)
|
||||
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(testProjectDir)
|
||||
discovery = new SimplifiedRoleDiscovery()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Corrupted Role Files', () => {
|
||||
test('should handle role files with malformed DPML', async () => {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'malformed-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
// Create role file with malformed DPML
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'malformed-role.role.md'),
|
||||
`# Malformed Role
|
||||
<role>
|
||||
<personality>
|
||||
Unclosed tag here
|
||||
</personalit
|
||||
<principle>
|
||||
Normal content
|
||||
</principle>
|
||||
</role>`
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
// Should still discover the role (basic validation only checks for tags presence)
|
||||
expect(userRoles).toHaveProperty('malformed-role')
|
||||
})
|
||||
|
||||
test('should handle role files with missing required tags', async () => {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'missing-tags')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'missing-tags.role.md'),
|
||||
`# Missing Tags Role
|
||||
This file has no <role> tags at all.`
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('missing-tags')
|
||||
})
|
||||
|
||||
test('should handle empty role files', async () => {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'empty-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
await fs.writeFile(path.join(roleDir, 'empty-role.role.md'), '')
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('empty-role')
|
||||
})
|
||||
|
||||
test('should handle role files with only whitespace', async () => {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'whitespace-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'whitespace-role.role.md'),
|
||||
' \n\t \n '
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('whitespace-role')
|
||||
})
|
||||
})
|
||||
|
||||
describe('File System Edge Cases', () => {
|
||||
test('should handle permission denied errors gracefully', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
// Skip permission tests on Windows
|
||||
return
|
||||
}
|
||||
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'permission-denied')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const roleFile = path.join(roleDir, 'permission-denied.role.md')
|
||||
await fs.writeFile(roleFile, '<role>test</role>')
|
||||
|
||||
// Remove read permissions
|
||||
await fs.chmod(roleFile, 0o000)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('permission-denied')
|
||||
|
||||
// Restore permissions for cleanup
|
||||
await fs.chmod(roleFile, 0o644)
|
||||
})
|
||||
|
||||
test('should handle directory symlinks correctly', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
// Skip symlink tests on Windows (require admin privileges)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: SimplifiedRoleDiscovery intentionally doesn't support symlinks for security
|
||||
// This test documents the expected behavior rather than testing it
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
// SimplifiedRoleDiscovery doesn't follow symlinks by design
|
||||
expect(userRoles).toBeDefined()
|
||||
expect(typeof userRoles).toBe('object')
|
||||
})
|
||||
|
||||
test('should handle broken symlinks gracefully', async () => {
|
||||
if (process.platform === 'win32') {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a symlink to a non-existent directory
|
||||
const brokenSymlink = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'broken-symlink')
|
||||
const nonExistentTarget = path.join(testProjectDir, 'non-existent-target')
|
||||
|
||||
await fs.symlink(nonExistentTarget, brokenSymlink)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('broken-symlink')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Special Characters and Unicode', () => {
|
||||
test('should handle role names with special characters', async () => {
|
||||
const roleName = 'special-chars_123.test'
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', roleName)
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, `${roleName}.role.md`),
|
||||
'<role><personality>Special chars role</personality></role>'
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(Object.keys(userRoles)).toContain(roleName)
|
||||
expect(userRoles[roleName]).toBeDefined()
|
||||
})
|
||||
|
||||
test('should handle Unicode role names', async () => {
|
||||
const roleName = '测试角色'
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', roleName)
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, `${roleName}.role.md`),
|
||||
'<role><personality>Unicode role</personality></role>'
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).toHaveProperty(roleName)
|
||||
})
|
||||
|
||||
test('should handle roles with emoji in content', async () => {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'emoji-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'emoji-role.role.md'),
|
||||
`# 🎭 Emoji Role
|
||||
> A role with emojis 🚀✨
|
||||
|
||||
<role>
|
||||
<personality>
|
||||
I love using emojis! 😄🎉
|
||||
</personality>
|
||||
</role>`
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).toHaveProperty('emoji-role')
|
||||
expect(userRoles['emoji-role'].name).toBe('🎭 Emoji Role')
|
||||
expect(userRoles['emoji-role'].description).toBe('A role with emojis 🚀✨')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Concurrent Access', () => {
|
||||
test('should handle concurrent discovery calls safely', async () => {
|
||||
// Create test roles
|
||||
await createTestRole('concurrent-1')
|
||||
await createTestRole('concurrent-2')
|
||||
await createTestRole('concurrent-3')
|
||||
|
||||
// Start multiple discovery operations concurrently
|
||||
const discoveryPromises = [
|
||||
discovery.discoverUserRoles(),
|
||||
discovery.discoverUserRoles(),
|
||||
discovery.discoverUserRoles()
|
||||
]
|
||||
|
||||
const results = await Promise.all(discoveryPromises)
|
||||
|
||||
// All results should be consistent
|
||||
expect(results[0]).toEqual(results[1])
|
||||
expect(results[1]).toEqual(results[2])
|
||||
|
||||
// Should find all test roles
|
||||
expect(results[0]).toHaveProperty('concurrent-1')
|
||||
expect(results[0]).toHaveProperty('concurrent-2')
|
||||
expect(results[0]).toHaveProperty('concurrent-3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Large File Handling', () => {
|
||||
test('should handle very large role files', async () => {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'large-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
// Create a large role file (1MB of content)
|
||||
const largeContent = 'A'.repeat(1024 * 1024)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'large-role.role.md'),
|
||||
`<role><personality>${largeContent}</personality></role>`
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).toHaveProperty('large-role')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Directory Structure Edge Cases', () => {
|
||||
test('should handle nested subdirectories gracefully', async () => {
|
||||
// Create deeply nested structure (should be ignored)
|
||||
const nestedDir = path.join(
|
||||
testProjectDir, '.promptx', 'resource', 'domain', 'nested',
|
||||
'very', 'deep', 'structure'
|
||||
)
|
||||
await fs.ensureDir(nestedDir)
|
||||
await fs.writeFile(
|
||||
path.join(nestedDir, 'deep.role.md'),
|
||||
'<role>deep</role>'
|
||||
)
|
||||
|
||||
// Also create a valid role at the correct level
|
||||
await createTestRole('valid-role')
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
// Should find the valid role but ignore the deeply nested one
|
||||
expect(userRoles).toHaveProperty('valid-role')
|
||||
expect(userRoles).not.toHaveProperty('deep')
|
||||
})
|
||||
|
||||
test('should handle files instead of directories in domain folder', async () => {
|
||||
const domainPath = path.join(testProjectDir, '.promptx', 'resource', 'domain')
|
||||
|
||||
// Create a file directly in the domain folder (should be ignored)
|
||||
await fs.writeFile(
|
||||
path.join(domainPath, 'not-a-role-dir.md'),
|
||||
'<role>should be ignored</role>'
|
||||
)
|
||||
|
||||
// Create a valid role
|
||||
await createTestRole('valid-role')
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
expect(userRoles).toHaveProperty('valid-role')
|
||||
expect(Object.keys(userRoles)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Missing Registry File', () => {
|
||||
test('should handle missing system registry gracefully', async () => {
|
||||
// Mock fs.readJSON to simulate missing registry file
|
||||
const originalReadJSON = fs.readJSON
|
||||
fs.readJSON = jest.fn().mockRejectedValue(new Error('ENOENT: no such file'))
|
||||
|
||||
const systemRoles = await discovery.loadSystemRoles()
|
||||
expect(systemRoles).toEqual({})
|
||||
|
||||
// Restore original function
|
||||
fs.readJSON = originalReadJSON
|
||||
})
|
||||
|
||||
test('should handle corrupted registry file gracefully', async () => {
|
||||
const originalReadJSON = fs.readJSON
|
||||
fs.readJSON = jest.fn().mockRejectedValue(new Error('Unexpected token in JSON'))
|
||||
|
||||
const systemRoles = await discovery.loadSystemRoles()
|
||||
expect(systemRoles).toEqual({})
|
||||
|
||||
fs.readJSON = originalReadJSON
|
||||
})
|
||||
})
|
||||
|
||||
describe('Project Root Detection Edge Cases', () => {
|
||||
test('should handle projects without package.json', async () => {
|
||||
// Remove package.json
|
||||
await fs.remove(path.join(testProjectDir, 'package.json'))
|
||||
|
||||
// Should still work (fallback to current directory)
|
||||
await createTestRole('no-package-json')
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
expect(userRoles).toHaveProperty('no-package-json')
|
||||
})
|
||||
|
||||
test('should handle project root at filesystem root', async () => {
|
||||
// Mock process.cwd to return root directory
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(path.parse(process.cwd()).root)
|
||||
|
||||
// Should not crash
|
||||
const userPath = await discovery.getUserRolePath()
|
||||
expect(userPath).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// Helper function to create a test role
|
||||
async function createTestRole(roleName) {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', roleName)
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, `${roleName}.role.md`),
|
||||
`<role><personality>${roleName} personality</personality></role>`
|
||||
)
|
||||
}
|
||||
})
|
||||
222
src/tests/core/resource/RoleDiscoveryPerformance.unit.test.js
Normal file
222
src/tests/core/resource/RoleDiscoveryPerformance.unit.test.js
Normal file
@ -0,0 +1,222 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
const SimplifiedRoleDiscovery = require('../../../lib/core/resource/SimplifiedRoleDiscovery')
|
||||
|
||||
describe('Role Discovery Performance Benchmarks', () => {
|
||||
let tempDir
|
||||
let testProjectDir
|
||||
let discovery
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'role-discovery-perf-'))
|
||||
testProjectDir = path.join(tempDir, 'perf-test-project')
|
||||
|
||||
await fs.ensureDir(path.join(testProjectDir, '.promptx', 'resource', 'domain'))
|
||||
await fs.writeFile(
|
||||
path.join(testProjectDir, 'package.json'),
|
||||
JSON.stringify({ name: 'perf-test-project', version: '1.0.0' })
|
||||
)
|
||||
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(testProjectDir)
|
||||
discovery = new SimplifiedRoleDiscovery()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Scaling Performance Tests', () => {
|
||||
test('should discover 10 roles in under 50ms', async () => {
|
||||
await createMultipleTestRoles(10)
|
||||
|
||||
const startTime = process.hrtime.bigint()
|
||||
const roles = await discovery.discoverAllRoles()
|
||||
const endTime = process.hrtime.bigint()
|
||||
|
||||
const durationMs = Number(endTime - startTime) / 1000000
|
||||
|
||||
expect(Object.keys(roles).length).toBeGreaterThanOrEqual(10) // System + user roles
|
||||
expect(durationMs).toBeLessThan(50)
|
||||
})
|
||||
|
||||
test('should discover 50 roles in under 100ms', async () => {
|
||||
await createMultipleTestRoles(50)
|
||||
|
||||
const startTime = process.hrtime.bigint()
|
||||
const roles = await discovery.discoverAllRoles()
|
||||
const endTime = process.hrtime.bigint()
|
||||
|
||||
const durationMs = Number(endTime - startTime) / 1000000
|
||||
|
||||
expect(Object.keys(roles).length).toBeGreaterThanOrEqual(50)
|
||||
expect(durationMs).toBeLessThan(100)
|
||||
})
|
||||
|
||||
test('should discover 100 roles in under 150ms', async () => {
|
||||
await createMultipleTestRoles(100)
|
||||
|
||||
const startTime = process.hrtime.bigint()
|
||||
const roles = await discovery.discoverAllRoles()
|
||||
const endTime = process.hrtime.bigint()
|
||||
|
||||
const durationMs = Number(endTime - startTime) / 1000000
|
||||
|
||||
expect(Object.keys(roles).length).toBeGreaterThanOrEqual(100)
|
||||
expect(durationMs).toBeLessThan(150)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parallel vs Sequential Processing', () => {
|
||||
test('parallel discovery should be faster than sequential', async () => {
|
||||
const roleCount = 50 // 增加角色数量以放大差异
|
||||
await createMultipleTestRoles(roleCount)
|
||||
|
||||
// 多次运行取平均值,减少测试波动
|
||||
const runs = 3
|
||||
let parallelTotal = 0
|
||||
let sequentialTotal = 0
|
||||
|
||||
for (let i = 0; i < runs; i++) {
|
||||
// Test parallel discovery (our implementation)
|
||||
const parallelStart = process.hrtime.bigint()
|
||||
await discovery.discoverUserRoles()
|
||||
const parallelEnd = process.hrtime.bigint()
|
||||
parallelTotal += Number(parallelEnd - parallelStart) / 1000000
|
||||
|
||||
// Test sequential discovery (simulated)
|
||||
const sequentialStart = process.hrtime.bigint()
|
||||
await simulateSequentialDiscovery(roleCount)
|
||||
const sequentialEnd = process.hrtime.bigint()
|
||||
sequentialTotal += Number(sequentialEnd - sequentialStart) / 1000000
|
||||
}
|
||||
|
||||
const parallelAvg = parallelTotal / runs
|
||||
const sequentialAvg = sequentialTotal / runs
|
||||
|
||||
// 放宽条件:并行应该比串行快,或者至少不慢太多
|
||||
expect(parallelAvg).toBeLessThan(sequentialAvg * 1.2) // 允许20%的误差
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memory Usage Tests', () => {
|
||||
test('should not accumulate excessive memory with large role sets', async () => {
|
||||
const initialMemory = process.memoryUsage().heapUsed
|
||||
|
||||
// Create and discover many roles
|
||||
await createMultipleTestRoles(100)
|
||||
await discovery.discoverAllRoles()
|
||||
|
||||
const finalMemory = process.memoryUsage().heapUsed
|
||||
const memoryIncrease = finalMemory - initialMemory
|
||||
|
||||
// Memory increase should be reasonable (less than 50MB for 100 roles)
|
||||
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024)
|
||||
})
|
||||
})
|
||||
|
||||
describe('File System Optimization Tests', () => {
|
||||
test('should minimize file system calls', async () => {
|
||||
await createMultipleTestRoles(10)
|
||||
|
||||
// Spy on file system operations
|
||||
const statSpy = jest.spyOn(fs, 'stat')
|
||||
const pathExistsSpy = jest.spyOn(fs, 'pathExists')
|
||||
const readFileSpy = jest.spyOn(fs, 'readFile')
|
||||
const readdirSpy = jest.spyOn(fs, 'readdir')
|
||||
|
||||
await discovery.discoverUserRoles()
|
||||
|
||||
// Should use readdir with withFileTypes to minimize stat calls
|
||||
expect(readdirSpy).toHaveBeenCalled()
|
||||
|
||||
// Should minimize individual stat and pathExists calls through optimization
|
||||
const totalFsCalls = statSpy.mock.calls.length +
|
||||
pathExistsSpy.mock.calls.length +
|
||||
readFileSpy.mock.calls.length
|
||||
|
||||
expect(totalFsCalls).toBeLessThan(25) // Should be efficient with batch operations
|
||||
|
||||
statSpy.mockRestore()
|
||||
pathExistsSpy.mockRestore()
|
||||
readFileSpy.mockRestore()
|
||||
readdirSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Caching Performance (Future Enhancement)', () => {
|
||||
test('should be ready for caching implementation', async () => {
|
||||
await createMultipleTestRoles(20)
|
||||
|
||||
// First discovery
|
||||
const firstStart = process.hrtime.bigint()
|
||||
const firstResult = await discovery.discoverAllRoles()
|
||||
const firstEnd = process.hrtime.bigint()
|
||||
const firstDuration = Number(firstEnd - firstStart) / 1000000
|
||||
|
||||
// Second discovery (cache would help here)
|
||||
const secondStart = process.hrtime.bigint()
|
||||
const secondResult = await discovery.discoverAllRoles()
|
||||
const secondEnd = process.hrtime.bigint()
|
||||
const secondDuration = Number(secondEnd - secondStart) / 1000000
|
||||
|
||||
// Results should be consistent
|
||||
expect(Object.keys(firstResult)).toEqual(Object.keys(secondResult))
|
||||
|
||||
// Both should be reasonably fast (caching would make second faster)
|
||||
expect(firstDuration).toBeLessThan(100)
|
||||
expect(secondDuration).toBeLessThan(100)
|
||||
})
|
||||
})
|
||||
|
||||
// Helper function to create multiple test roles
|
||||
async function createMultipleTestRoles(count) {
|
||||
const promises = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const roleName = `perf-test-role-${i.toString().padStart(3, '0')}`
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', roleName)
|
||||
|
||||
promises.push(
|
||||
fs.ensureDir(roleDir).then(() =>
|
||||
fs.writeFile(
|
||||
path.join(roleDir, `${roleName}.role.md`),
|
||||
`# Performance Test Role ${i}
|
||||
> Role created for performance testing
|
||||
|
||||
<role>
|
||||
<personality>
|
||||
Performance test personality for role ${i}
|
||||
</personality>
|
||||
<principle>
|
||||
Performance test principle for role ${i}
|
||||
</principle>
|
||||
<knowledge>
|
||||
Performance test knowledge for role ${i}
|
||||
</knowledge>
|
||||
</role>`
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
// Simulate sequential discovery for comparison
|
||||
async function simulateSequentialDiscovery(count) {
|
||||
const userPath = path.join(testProjectDir, '.promptx', 'resource', 'domain')
|
||||
const directories = await fs.readdir(userPath)
|
||||
|
||||
for (const dir of directories) {
|
||||
const roleFile = path.join(userPath, dir, `${dir}.role.md`)
|
||||
if (await fs.pathExists(roleFile)) {
|
||||
await fs.readFile(roleFile, 'utf8')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
309
src/tests/core/resource/SimplifiedRoleDiscovery.unit.test.js
Normal file
309
src/tests/core/resource/SimplifiedRoleDiscovery.unit.test.js
Normal file
@ -0,0 +1,309 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
// This will be the implementation we're building towards
|
||||
const SimplifiedRoleDiscovery = require('../../../lib/core/resource/SimplifiedRoleDiscovery')
|
||||
|
||||
describe('SimplifiedRoleDiscovery - TDD Implementation', () => {
|
||||
let tempDir
|
||||
let testProjectDir
|
||||
let discovery
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temporary test environment
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'simplified-role-discovery-'))
|
||||
testProjectDir = path.join(tempDir, 'test-project')
|
||||
|
||||
// Create test project structure
|
||||
await fs.ensureDir(path.join(testProjectDir, '.promptx', 'resource', 'domain'))
|
||||
await fs.writeFile(
|
||||
path.join(testProjectDir, 'package.json'),
|
||||
JSON.stringify({ name: 'test-project', version: '1.0.0' })
|
||||
)
|
||||
|
||||
// Mock process.cwd to point to our test project
|
||||
jest.spyOn(process, 'cwd').mockReturnValue(testProjectDir)
|
||||
|
||||
discovery = new SimplifiedRoleDiscovery()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.remove(tempDir)
|
||||
}
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Core Algorithm API', () => {
|
||||
test('should expose discoverAllRoles method', () => {
|
||||
expect(typeof discovery.discoverAllRoles).toBe('function')
|
||||
})
|
||||
|
||||
test('should expose loadSystemRoles method', () => {
|
||||
expect(typeof discovery.loadSystemRoles).toBe('function')
|
||||
})
|
||||
|
||||
test('should expose discoverUserRoles method', () => {
|
||||
expect(typeof discovery.discoverUserRoles).toBe('function')
|
||||
})
|
||||
|
||||
test('should expose mergeRoles method', () => {
|
||||
expect(typeof discovery.mergeRoles).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('System Roles Loading', () => {
|
||||
test('should load system roles from static registry', async () => {
|
||||
const systemRoles = await discovery.loadSystemRoles()
|
||||
|
||||
expect(systemRoles).toBeDefined()
|
||||
expect(typeof systemRoles).toBe('object')
|
||||
|
||||
// Should contain known system roles
|
||||
expect(systemRoles).toHaveProperty('assistant')
|
||||
expect(systemRoles.assistant).toHaveProperty('name')
|
||||
expect(systemRoles.assistant).toHaveProperty('file')
|
||||
expect(systemRoles.assistant.name).toContain('智能助手')
|
||||
})
|
||||
|
||||
test('should handle missing registry file gracefully', async () => {
|
||||
// Mock missing registry file
|
||||
const originalReadJSON = fs.readJSON
|
||||
fs.readJSON = jest.fn().mockRejectedValue(new Error('File not found'))
|
||||
|
||||
const systemRoles = await discovery.loadSystemRoles()
|
||||
expect(systemRoles).toEqual({})
|
||||
|
||||
fs.readJSON = originalReadJSON
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Roles Discovery', () => {
|
||||
test('should return empty object when user directory does not exist', async () => {
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).toEqual({})
|
||||
})
|
||||
|
||||
test('should discover valid user role', async () => {
|
||||
// Create test user role
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'test-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'test-role.role.md'),
|
||||
`# Test Role
|
||||
> A test role for unit testing
|
||||
|
||||
<role>
|
||||
<personality>
|
||||
Test personality
|
||||
</personality>
|
||||
<principle>
|
||||
Test principle
|
||||
</principle>
|
||||
<knowledge>
|
||||
Test knowledge
|
||||
</knowledge>
|
||||
</role>`
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
|
||||
expect(userRoles).toHaveProperty('test-role')
|
||||
expect(userRoles['test-role']).toHaveProperty('name', 'Test Role')
|
||||
expect(userRoles['test-role']).toHaveProperty('description', 'A test role for unit testing')
|
||||
expect(userRoles['test-role']).toHaveProperty('source', 'user-generated')
|
||||
expect(userRoles['test-role']).toHaveProperty('file')
|
||||
})
|
||||
|
||||
test('should skip invalid role files', async () => {
|
||||
// Create invalid role file (missing <role> tags)
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'invalid-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'invalid-role.role.md'),
|
||||
'This is not a valid role file'
|
||||
)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('invalid-role')
|
||||
})
|
||||
|
||||
test('should handle missing role file gracefully', async () => {
|
||||
// Create directory but no role file
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'empty-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('empty-role')
|
||||
})
|
||||
|
||||
test('should handle file system errors gracefully', async () => {
|
||||
// Create a role directory with permission issues (Unix only)
|
||||
if (process.platform !== 'win32') {
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'restricted-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
const roleFile = path.join(roleDir, 'restricted-role.role.md')
|
||||
await fs.writeFile(roleFile, '<role>test</role>')
|
||||
await fs.chmod(roleFile, 0o000) // Remove all permissions
|
||||
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).not.toHaveProperty('restricted-role')
|
||||
|
||||
// Restore permissions for cleanup
|
||||
await fs.chmod(roleFile, 0o644)
|
||||
} else {
|
||||
// On Windows, just test that the method doesn't throw
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
expect(userRoles).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parallel Discovery Performance', () => {
|
||||
test('should process multiple user roles in parallel', async () => {
|
||||
const roleCount = 10
|
||||
const createRolePromises = []
|
||||
|
||||
// Create multiple test roles
|
||||
for (let i = 0; i < roleCount; i++) {
|
||||
const roleName = `test-role-${i}`
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', roleName)
|
||||
|
||||
createRolePromises.push(
|
||||
fs.ensureDir(roleDir).then(() =>
|
||||
fs.writeFile(
|
||||
path.join(roleDir, `${roleName}.role.md`),
|
||||
`<role><personality>Role ${i}</personality></role>`
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(createRolePromises)
|
||||
|
||||
const startTime = Date.now()
|
||||
const userRoles = await discovery.discoverUserRoles()
|
||||
const endTime = Date.now()
|
||||
|
||||
expect(Object.keys(userRoles)).toHaveLength(roleCount)
|
||||
expect(endTime - startTime).toBeLessThan(100) // Should be fast with parallel processing
|
||||
})
|
||||
})
|
||||
|
||||
describe('Role Merging', () => {
|
||||
test('should merge system and user roles correctly', () => {
|
||||
const systemRoles = {
|
||||
'assistant': { name: 'System Assistant', source: 'system' },
|
||||
'system-only': { name: 'System Only', source: 'system' }
|
||||
}
|
||||
|
||||
const userRoles = {
|
||||
'assistant': { name: 'User Assistant', source: 'user' },
|
||||
'user-only': { name: 'User Only', source: 'user' }
|
||||
}
|
||||
|
||||
const merged = discovery.mergeRoles(systemRoles, userRoles)
|
||||
|
||||
expect(merged).toHaveProperty('assistant')
|
||||
expect(merged).toHaveProperty('system-only')
|
||||
expect(merged).toHaveProperty('user-only')
|
||||
|
||||
// User role should override system role
|
||||
expect(merged.assistant.source).toBe('user')
|
||||
expect(merged.assistant.name).toBe('User Assistant')
|
||||
|
||||
// System-only role should remain
|
||||
expect(merged['system-only'].source).toBe('system')
|
||||
|
||||
// User-only role should be included
|
||||
expect(merged['user-only'].source).toBe('user')
|
||||
})
|
||||
|
||||
test('should handle empty input gracefully', () => {
|
||||
expect(discovery.mergeRoles({}, {})).toEqual({})
|
||||
expect(discovery.mergeRoles({ test: 'value' }, {})).toEqual({ test: 'value' })
|
||||
expect(discovery.mergeRoles({}, { test: 'value' })).toEqual({ test: 'value' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Complete Discovery Flow', () => {
|
||||
test('should discover all roles (system + user)', async () => {
|
||||
// Create a test user role
|
||||
const roleDir = path.join(testProjectDir, '.promptx', 'resource', 'domain', 'custom-role')
|
||||
await fs.ensureDir(roleDir)
|
||||
await fs.writeFile(
|
||||
path.join(roleDir, 'custom-role.role.md'),
|
||||
'<role><personality>Custom role</personality></role>'
|
||||
)
|
||||
|
||||
const allRoles = await discovery.discoverAllRoles()
|
||||
|
||||
expect(allRoles).toBeDefined()
|
||||
expect(typeof allRoles).toBe('object')
|
||||
|
||||
// Should contain system roles
|
||||
expect(allRoles).toHaveProperty('assistant')
|
||||
|
||||
// Should contain user role
|
||||
expect(allRoles).toHaveProperty('custom-role')
|
||||
expect(allRoles['custom-role'].source).toBe('user-generated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DPML Validation', () => {
|
||||
test('should validate basic DPML format', () => {
|
||||
const validContent = '<role><personality>test</personality></role>'
|
||||
const invalidContent = 'no role tags here'
|
||||
|
||||
expect(discovery.isValidRoleFile(validContent)).toBe(true)
|
||||
expect(discovery.isValidRoleFile(invalidContent)).toBe(false)
|
||||
})
|
||||
|
||||
test('should extract role name from markdown header', () => {
|
||||
const content = `# My Custom Role
|
||||
<role>content</role>`
|
||||
|
||||
expect(discovery.extractRoleName(content)).toBe('My Custom Role')
|
||||
})
|
||||
|
||||
test('should extract description from markdown quote', () => {
|
||||
const content = `# Role Name
|
||||
> This is the role description
|
||||
<role>content</role>`
|
||||
|
||||
expect(discovery.extractDescription(content)).toBe('This is the role description')
|
||||
})
|
||||
|
||||
test('should handle missing metadata gracefully', () => {
|
||||
const content = '<role>content</role>'
|
||||
|
||||
expect(discovery.extractRoleName(content)).toBeNull()
|
||||
expect(discovery.extractDescription(content)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cross-platform Path Handling', () => {
|
||||
test('should handle Windows paths correctly', () => {
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' })
|
||||
|
||||
// Test that path operations work correctly on Windows
|
||||
const userPath = discovery.getUserRolePath()
|
||||
expect(userPath).toBeDefined()
|
||||
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
})
|
||||
|
||||
test('should handle Unix paths correctly', () => {
|
||||
const originalPlatform = process.platform
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' })
|
||||
|
||||
// Test that path operations work correctly on Unix
|
||||
const userPath = discovery.getUserRolePath()
|
||||
expect(userPath).toBeDefined()
|
||||
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform })
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user