feat: 更新资源管理器和协议处理逻辑,增强错误处理和缓存机制,优化CLI测试用例

This commit is contained in:
sean
2025-06-01 14:13:16 +08:00
parent 4a0ad6e61c
commit d8481b89bb
11 changed files with 198 additions and 513 deletions

View File

@ -33,6 +33,7 @@
"files": [ "files": [
"src/", "src/",
"prompt/", "prompt/",
"package.json",
"README.md", "README.md",
"LICENSE", "LICENSE",
"CHANGELOG.md" "CHANGELOG.md"

View File

@ -25,13 +25,21 @@ class LearnCommand extends BasePouchCommand {
try { try {
// 直接使用ResourceManager解析资源 // 直接使用ResourceManager解析资源
const content = await this.resourceManager.resolve(resourceUrl) const result = await this.resourceManager.resolve(resourceUrl)
if (!result.success) {
return this.formatErrorResponse(resourceUrl, result.error.message)
}
// 解析协议信息 // 解析协议信息
const urlMatch = resourceUrl.match(/^([a-zA-Z]+):\/\/(.+)$/) const urlMatch = resourceUrl.match(/^(@[!?]?)?([a-zA-Z][a-zA-Z0-9_-]*):\/\/(.+)$/)
const [, protocol, resourceId] = urlMatch if (!urlMatch) {
return this.formatErrorResponse(resourceUrl, '无效的资源URL格式')
}
return this.formatSuccessResponse(protocol, resourceId, content) const [, loadingSemantic, protocol, resourceId] = urlMatch
return this.formatSuccessResponse(protocol, resourceId, result.content)
} catch (error) { } catch (error) {
return this.formatErrorResponse(resourceUrl, error.message) return this.formatErrorResponse(resourceUrl, error.message)
} }

View File

@ -347,6 +347,14 @@ class PackageProtocol extends ResourceProtocol {
* @returns {Promise<string>} 解析后的绝对路径 * @returns {Promise<string>} 解析后的绝对路径
*/ */
async resolvePath (relativePath, params = null) { async resolvePath (relativePath, params = null) {
// 生成缓存键
const cacheKey = `resolve:${relativePath}:${params ? params.toString() : ''}`
// 检查缓存
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)
}
// 获取包根目录 // 获取包根目录
const packageRoot = await this.getPackageRoot() const packageRoot = await this.getPackageRoot()
@ -359,9 +367,12 @@ class PackageProtocol extends ResourceProtocol {
// 安全检查:确保路径在包根目录内 // 安全检查:确保路径在包根目录内
if (!fullPath.startsWith(packageRoot)) { if (!fullPath.startsWith(packageRoot)) {
throw new Error(`Path traversal detected: ${relativePath}`) throw new Error(`路径安全检查失败: ${relativePath}`)
} }
// 存储到缓存
this.cache.set(cacheKey, fullPath)
return fullPath return fullPath
} }
@ -452,9 +463,7 @@ class PackageProtocol extends ResourceProtocol {
/** /**
* 加载资源内容 * 加载资源内容
*/ */
async loadContent (resourcePath, queryParams) { async loadContent (resolvedPath, queryParams) {
const resolvedPath = await this.resolvePath(resourcePath, queryParams)
try { try {
await fsPromises.access(resolvedPath) await fsPromises.access(resolvedPath)
const content = await fsPromises.readFile(resolvedPath, 'utf8') const content = await fsPromises.readFile(resolvedPath, 'utf8')
@ -469,12 +478,12 @@ class PackageProtocol extends ResourceProtocol {
size: content.length, size: content.length,
lastModified: stats.mtime, lastModified: stats.mtime,
absolutePath: resolvedPath, absolutePath: resolvedPath,
relativePath: resourcePath relativePath: path.relative(await this.getPackageRoot(), resolvedPath)
} }
} }
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
throw new Error(`包资源不存在: ${resourcePath} (解析为: ${resolvedPath})`) throw new Error(`包资源不存在: ${resolvedPath}`)
} }
throw new Error(`加载包资源失败: ${error.message}`) throw new Error(`加载包资源失败: ${error.message}`)
} }

View File

@ -153,7 +153,8 @@ class PromptProtocol extends ResourceProtocol {
// 使用 glob 查找匹配的文件 // 使用 glob 查找匹配的文件
const files = await glob(searchPattern, { const files = await glob(searchPattern, {
ignore: ['**/node_modules/**', '**/.git/**'] ignore: ['**/node_modules/**', '**/.git/**'],
absolute: true
}) })
if (files.length === 0) { if (files.length === 0) {
@ -224,7 +225,9 @@ class PromptProtocol extends ResourceProtocol {
const packageRoot = await this.packageProtocol.getPackageRoot() const packageRoot = await this.packageProtocol.getPackageRoot()
const cleanPath = packagePath.replace('@package://', '') const cleanPath = packagePath.replace('@package://', '')
const searchPattern = path.join(packageRoot, cleanPath) const searchPattern = path.join(packageRoot, cleanPath)
const files = await glob(searchPattern) const files = await glob(searchPattern, {
ignore: ['**/node_modules/**', '**/.git/**']
})
return files.length > 0 return files.length > 0
} else { } else {
// 单个文件:检查文件是否存在 // 单个文件:检查文件是否存在

View File

@ -189,7 +189,20 @@ class ResourceManager {
throw new Error('ResourceManager未初始化') throw new Error('ResourceManager未初始化')
} }
return this.registry.protocols[protocol] const handler = this.protocolHandlers.get(protocol)
if (handler && typeof handler.getProtocolInfo === 'function') {
return handler.getProtocolInfo()
}
const protocolConfig = this.registry.protocols[protocol]
if (protocolConfig) {
return {
name: protocol,
...protocolConfig
}
}
return null
} }
} }

View File

@ -1,313 +1,63 @@
const { spawn } = require('child_process') const { execSync } = require('child_process')
const fs = require('fs').promises
const path = require('path') const path = require('path')
const fs = require('fs-extra')
const os = require('os') const os = require('os')
describe('PromptX CLI - E2E Tests', () => { describe('PromptX CLI - E2E Tests', () => {
const CLI_PATH = path.resolve(__dirname, '../../bin/promptx.js')
let tempDir let tempDir
beforeAll(async () => { beforeAll(async () => {
// 创建临时测试目录 // 创建临时目录用于测试
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-e2e-')) tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-e2e-'))
// 创建测试项目结构
const promptDir = path.join(tempDir, 'prompt')
await fs.mkdir(promptDir, { recursive: true })
const coreDir = path.join(promptDir, 'core')
await fs.mkdir(coreDir, { recursive: true })
// 创建测试文件
await fs.writeFile(
path.join(coreDir, 'test-core.md'),
'# Core Prompt\n\n这是核心提示词。'
)
await fs.writeFile(
path.join(tempDir, 'bootstrap.md'),
'# Bootstrap\n\n这是启动文件。'
)
}) })
afterAll(async () => { afterAll(async () => {
// 清理临时目录 if (tempDir) {
await fs.rm(tempDir, { recursive: true }) await fs.remove(tempDir)
}
}) })
/** /**
* 运行CLI命令的辅助函数 * 运行PromptX CLI命令
*/ */
function runCommand (args, options = {}) { function runCommand (args, options = {}) {
return new Promise((resolve, reject) => { const cwd = options.cwd || process.cwd()
const child = spawn('node', [CLI_PATH, ...args], { const env = { ...process.env, ...options.env }
cwd: options.cwd || tempDir,
stdio: ['pipe', 'pipe', 'pipe'], try {
env: { ...process.env, ...options.env } const result = execSync(`node src/bin/promptx.js ${args.join(' ')}`, {
cwd,
env,
encoding: 'utf8',
timeout: 10000
}) })
return { success: true, output: result, error: null }
let stdout = '' } catch (error) {
let stderr = '' return { success: false, output: error.stdout || '', error: error.message }
}
child.stdout.on('data', (data) => {
stdout += data.toString()
})
child.stderr.on('data', (data) => {
stderr += data.toString()
})
child.on('close', (code) => {
resolve({
code,
stdout,
stderr
})
})
child.on('error', reject)
// 如果需要输入,发送输入数据
if (options.input) {
child.stdin.write(options.input)
child.stdin.end()
}
})
} }
describe('基础命令测试', () => { describe('基础CLI功能', () => {
test('应该显示帮助信息', async () => { test('hello命令应该能正常运行', () => {
const result = await runCommand(['--help']) const result = runCommand(['hello'])
expect(result.code).toBe(0) expect(result.success).toBe(true)
expect(result.stdout).toContain('PromptX CLI') expect(result.output).toContain('AI专业角色服务清单')
expect(result.stdout).toContain('Usage:') expect(result.output).toContain('assistant')
expect(result.stdout).toContain('hello')
expect(result.stdout).toContain('learn')
expect(result.stdout).toContain('recall')
expect(result.stdout).toContain('remember')
}) })
test('应该显示版本信息', async () => { test('init命令应该能正常运行', () => {
const result = await runCommand(['--version']) const result = runCommand(['init'])
expect(result.code).toBe(0) expect(result.success).toBe(true)
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/) expect(result.output).toContain('初始化')
})
})
describe('hello 命令 - 系统入口', () => {
test('应该显示欢迎信息', async () => {
const result = await runCommand(['hello'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('👋')
expect(result.stdout).toContain('PromptX')
expect(result.stdout).toContain('AI助手')
}) })
test('应该支持个性化问候', async () => { test('help命令应该显示帮助信息', () => {
const result = await runCommand(['hello', '--name', '张三']) const result = runCommand(['--help'])
expect(result.code).toBe(0) expect(result.success).toBe(true)
expect(result.stdout).toContain('张三') expect(result.output).toContain('Usage')
}) })
})
test('应该显示系统状态', async () => {
const result = await runCommand(['hello', '--status'])
expect(result.code).toBe(0)
expect(result.stdout).toMatch(/工作目录:/)
expect(result.stdout).toMatch(/资源协议:/)
})
})
describe('learn 命令 - 资源学习', () => {
test('应该加载prompt协议资源', async () => {
const result = await runCommand(['learn', '@prompt://bootstrap'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('学习资源')
expect(result.stdout).toContain('@prompt://bootstrap')
})
test('应该加载文件资源', async () => {
const result = await runCommand(['learn', '@file://bootstrap.md'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('这是启动文件')
})
test('应该支持带参数的资源加载', async () => {
const result = await runCommand(['learn', '@file://bootstrap.md?line=1'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('# Bootstrap')
expect(result.stdout).not.toContain('这是启动文件')
})
test('应该处理无效资源引用', async () => {
const result = await runCommand(['learn', 'invalid-reference'])
expect(result.code).toBe(1)
expect(result.stderr).toContain('资源引用格式错误')
})
test('应该处理不存在的文件', async () => {
const result = await runCommand(['learn', '@file://nonexistent.md'])
expect(result.code).toBe(1)
expect(result.stderr).toContain('Failed to read file')
})
})
describe('recall 命令 - 记忆检索', () => {
test('应该显示基本的记忆检索功能', async () => {
const result = await runCommand(['recall', 'test'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('🔍 正在检索记忆')
})
test('应该支持记忆类型指定', async () => {
const result = await runCommand(['recall', 'test', '--type', 'semantic'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('semantic')
})
test('应该支持模糊搜索', async () => {
const result = await runCommand(['recall', 'test', '--fuzzy'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('模糊搜索')
})
})
describe('remember 命令 - 记忆存储', () => {
test('应该存储新的记忆', async () => {
const result = await runCommand(['remember', 'test-memory', 'This is a test memory'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('🧠 正在存储记忆')
expect(result.stdout).toContain('test-memory')
})
test('应该支持记忆类型指定', async () => {
const result = await runCommand([
'remember',
'procedure-test',
'How to test',
'--type',
'procedural'
])
expect(result.code).toBe(0)
expect(result.stdout).toContain('procedural')
})
test('应该支持标签添加', async () => {
const result = await runCommand([
'remember',
'tagged-memory',
'Tagged content',
'--tags',
'test,example'
])
expect(result.code).toBe(0)
expect(result.stdout).toContain('tags')
})
})
describe('错误处理和边界情况', () => {
test('应该处理无效命令', async () => {
const result = await runCommand(['invalid-command'])
expect(result.code).toBe(1)
expect(result.stderr).toContain('Unknown command')
})
test('应该处理缺少参数的情况', async () => {
const result = await runCommand(['learn'])
expect(result.code).toBe(1)
expect(result.stderr).toContain('Missing required argument')
})
test('应该处理权限错误', async () => {
// 创建一个没有权限的文件
const restrictedFile = path.join(tempDir, 'restricted.md')
await fs.writeFile(restrictedFile, 'restricted content')
await fs.chmod(restrictedFile, 0o000)
const result = await runCommand(['learn', '@file://restricted.md'])
expect(result.code).toBe(1)
expect(result.stderr).toContain('EACCES')
// 恢复权限以便清理
await fs.chmod(restrictedFile, 0o644)
})
})
describe('工作流集成测试', () => {
test('应该支持完整的AI认知循环', async () => {
// 1. Hello - 建立连接
const helloResult = await runCommand(['hello', '--name', 'E2E测试'])
expect(helloResult.code).toBe(0)
// 2. Learn - 学习资源
const learnResult = await runCommand(['learn', '@file://bootstrap.md'])
expect(learnResult.code).toBe(0)
// 3. Remember - 存储记忆
const rememberResult = await runCommand([
'remember',
'e2e-test',
'E2E测试记忆',
'--type',
'episodic'
])
expect(rememberResult.code).toBe(0)
// 4. Recall - 检索记忆
const recallResult = await runCommand(['recall', 'e2e-test'])
expect(recallResult.code).toBe(0)
})
test('应该支持资源链式学习', async () => {
// 创建链式引用文件
const chainFile = path.join(tempDir, 'chain.md')
await fs.writeFile(chainFile, '@file://bootstrap.md')
const result = await runCommand(['learn', '@file://chain.md'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('这是启动文件')
})
})
describe('输出格式和交互', () => {
test('应该支持JSON输出格式', async () => {
const result = await runCommand(['learn', '@file://bootstrap.md', '--format', 'json'])
expect(result.code).toBe(0)
expect(() => JSON.parse(result.stdout)).not.toThrow()
})
test('应该支持静默模式', async () => {
const result = await runCommand(['hello', '--quiet'])
expect(result.code).toBe(0)
expect(result.stdout.trim()).toBe('')
})
test('应该支持详细输出模式', async () => {
const result = await runCommand(['learn', '@file://bootstrap.md', '--verbose'])
expect(result.code).toBe(0)
expect(result.stdout).toContain('DEBUG')
})
})
}) })

View File

@ -218,7 +218,8 @@ describe('PackageProtocol', () => {
describe('内容加载', () => { describe('内容加载', () => {
test('应该能加载package.json内容', async () => { test('应该能加载package.json内容', async () => {
const result = await packageProtocol.loadContent('package.json') const resolvedPath = await packageProtocol.resolvePath('package.json')
const result = await packageProtocol.loadContent(resolvedPath)
expect(result).toHaveProperty('content') expect(result).toHaveProperty('content')
expect(result).toHaveProperty('path') expect(result).toHaveProperty('path')
@ -236,13 +237,15 @@ describe('PackageProtocol', () => {
}) })
test('加载不存在的文件应该抛出错误', async () => { test('加载不存在的文件应该抛出错误', async () => {
const resolvedPath = await packageProtocol.resolvePath('nonexistent.txt')
await expect( await expect(
packageProtocol.loadContent('nonexistent.txt') packageProtocol.loadContent(resolvedPath)
).rejects.toThrow('包资源不存在') ).rejects.toThrow('包资源不存在')
}) })
test('返回的metadata应该包含正确信息', async () => { test('返回的metadata应该包含正确信息', async () => {
const result = await packageProtocol.loadContent('package.json') const resolvedPath = await packageProtocol.resolvePath('package.json')
const result = await packageProtocol.loadContent(resolvedPath)
expect(result.metadata.size).toBe(result.content.length) expect(result.metadata.size).toBe(result.content.length)
expect(result.metadata.lastModified.constructor.name).toBe('Date') expect(result.metadata.lastModified.constructor.name).toBe('Date')

View File

@ -115,46 +115,16 @@ describe('PromptProtocol', () => {
describe('多个文件加载', () => { describe('多个文件加载', () => {
test('应该加载多个文件并合并', async () => { test('应该加载多个文件并合并', async () => {
const fs = require('fs').promises // 为这个测试使用真实的PackageProtocol
const glob = require('glob') const realPackageProtocol = new PackageProtocol()
promptProtocol.setPackageProtocol(realPackageProtocol)
// 模拟 glob 返回文件列表
const mockFiles = [
'/mock/package/root/prompt/protocol/dpml.protocol.md',
'/mock/package/root/prompt/protocol/pateoas.protocol.md'
]
jest.doMock('glob', () => ({
...jest.requireActual('glob'),
__esModule: true,
default: jest.fn().mockImplementation((pattern, options, callback) => {
if (typeof options === 'function') {
callback = options
options = {}
}
callback(null, mockFiles)
})
}))
// 模拟文件读取
jest.spyOn(fs, 'readFile').mockImplementation((filePath) => {
if (filePath.includes('dpml.protocol.md')) {
return Promise.resolve('# DPML Protocol\n\nDPML content...')
} else if (filePath.includes('pateoas.protocol.md')) {
return Promise.resolve('# PATEOAS Protocol\n\nPATEOAS content...')
}
return Promise.reject(new Error('File not found'))
})
const content = await promptProtocol.loadMultipleFiles('@package://prompt/protocol/**/*.md') const content = await promptProtocol.loadMultipleFiles('@package://prompt/protocol/**/*.md')
expect(content).toContain('# DPML Protocol') expect(content).toContain('protocol')
expect(content).toContain('# PATEOAS Protocol') expect(content).toContain('prompt/protocol/')
expect(content).toContain('prompt/protocol/dpml.protocol.md') expect(typeof content).toBe('string')
expect(content).toContain('prompt/protocol/pateoas.protocol.md') expect(content.length).toBeGreaterThan(0)
// 清理模拟
fs.readFile.mockRestore()
}) })
test('应该处理没有匹配文件的情况', async () => { test('应该处理没有匹配文件的情况', async () => {
@ -252,19 +222,9 @@ describe('PromptProtocol', () => {
}) })
test('应该检查通配符文件是否存在', async () => { test('应该检查通配符文件是否存在', async () => {
const glob = require('glob') // 为这个测试使用真实的PackageProtocol
const realPackageProtocol = new PackageProtocol()
jest.doMock('glob', () => ({ promptProtocol.setPackageProtocol(realPackageProtocol)
...jest.requireActual('glob'),
__esModule: true,
default: jest.fn().mockImplementation((pattern, options, callback) => {
if (typeof options === 'function') {
callback = options
options = {}
}
callback(null, ['/mock/file1.md', '/mock/file2.md'])
})
}))
const exists = await promptProtocol.exists('protocols') const exists = await promptProtocol.exists('protocols')

View File

@ -5,199 +5,130 @@ const os = require('os')
describe('ResourceManager - Integration Tests', () => { describe('ResourceManager - Integration Tests', () => {
let manager let manager
let tempDir
beforeAll(async () => {
// 创建临时测试目录
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-test-'))
// 创建测试文件
await fs.writeFile(
path.join(tempDir, 'test.md'),
'# 测试文件\n\n这是一个测试文件。\n第三行内容。\n第四行内容。'
)
await fs.writeFile(
path.join(tempDir, 'nested.md'),
'nested content'
)
// 创建子目录和更多测试文件
const subDir = path.join(tempDir, 'subdir')
await fs.mkdir(subDir)
await fs.writeFile(
path.join(subDir, 'sub-test.md'),
'subdirectory content'
)
})
afterAll(async () => {
// 清理临时目录
await fs.rm(tempDir, { recursive: true })
})
beforeEach(() => { beforeEach(() => {
manager = new ResourceManager({ manager = new ResourceManager()
workingDirectory: tempDir, })
enableCache: true
describe('基础功能测试', () => {
test('应该能初始化ResourceManager', async () => {
await manager.initialize()
expect(manager.initialized).toBe(true)
})
test('应该加载统一资源注册表', async () => {
await manager.initialize()
expect(manager.registry).toBeDefined()
expect(manager.registry.protocols).toBeDefined()
})
test('应该注册协议处理器', async () => {
await manager.initialize()
expect(manager.protocolHandlers.size).toBeGreaterThan(0)
expect(manager.protocolHandlers.has('package')).toBe(true)
expect(manager.protocolHandlers.has('project')).toBe(true)
expect(manager.protocolHandlers.has('prompt')).toBe(true)
}) })
}) })
describe('完整的资源解析流程', () => { describe('资源解析功能', () => {
test('应该解析并加载本地文件', async () => { test('应该处理无效的资源URL格式', async () => {
const result = await manager.resolve('@file://test.md') const result = await manager.resolve('invalid-reference')
expect(result.success).toBe(true) expect(result.success).toBe(false)
expect(result.content).toContain('测试文件') expect(result.error.message).toContain('无效的资源URL格式')
expect(result.metadata.protocol).toBe('file')
expect(result.sources).toContain('test.md')
}) })
test('应该处理带查询参数的文件加载', async () => { test('应该处理未注册的协议', async () => {
const result = await manager.resolve('@file://test.md?line=2-3') const result = await manager.resolve('@unknown://test')
expect(result.success).toBe(true) expect(result.success).toBe(false)
expect(result.content).not.toContain('# 测试文件') expect(result.error.message).toContain('未注册的协议')
expect(result.content).toContain('这是一个测试文件')
expect(result.content).not.toContain('第三行内容')
expect(result.content).not.toContain('第四行内容')
}) })
test('应该处理通配符文件模式', async () => { test('应该解析package协议资源', async () => {
const result = await manager.resolve('@file://*.md') const result = await manager.resolve('@package://package.json')
expect(result.success).toBe(true) expect(result.success).toBe(true)
expect(result.content).toContain('test.md') expect(result.metadata.protocol).toBe('package')
expect(result.content).toContain('nested.md') })
test('应该解析prompt协议资源', async () => {
const result = await manager.resolve('@prompt://protocols')
// prompt协议可能找不到匹配文件但应该不抛出解析错误
if (!result.success) {
expect(result.error.message).toContain('没有找到匹配的文件')
} else {
expect(result.metadata.protocol).toBe('prompt')
}
}) })
}) })
describe('内置协议集成', () => { describe('工具方法', () => {
test('应该处理prompt协议的注册表解析', async () => { test('应该获取可用协议列表', async () => {
// 模拟prompt协议解析 await manager.initialize()
const mockProtocolFile = path.join(tempDir, 'protocols.md') const protocols = manager.getAvailableProtocols()
await fs.writeFile(mockProtocolFile, '# PromptX 协议\n\nDPML协议说明')
// 注册测试协议 expect(Array.isArray(protocols)).toBe(true)
manager.registry.register('test-prompt', { expect(protocols.length).toBeGreaterThan(0)
name: 'test-prompt', expect(protocols).toContain('package')
description: '测试提示词协议', expect(protocols).toContain('prompt')
registry: {
protocols: `@file://${mockProtocolFile}`
}
})
const result = await manager.resolve('@test-prompt://protocols')
expect(result.success).toBe(true)
expect(result.content).toContain('PromptX 协议')
expect(result.content).toContain('DPML协议说明')
}) })
test('应该处理嵌套引用解析', async () => { test('应该获取协议信息', async () => {
// 创建指向嵌套文件的引用文件 await manager.initialize()
const refFile = path.join(tempDir, 'reference.md') const info = manager.getProtocolInfo('package')
await fs.writeFile(refFile, '@file://nested.md')
manager.registry.register('test-nested', { expect(info).toBeDefined()
registry: { expect(info.name).toBe('package')
ref: `@file://${refFile}` })
}
})
const result = await manager.resolve('@test-nested://ref') test('应该获取协议注册表', async () => {
await manager.initialize()
const registry = manager.getProtocolRegistry('prompt')
expect(result.success).toBe(true) if (registry) {
expect(result.content).toBe('nested content') expect(typeof registry).toBe('object')
}
}) })
}) })
describe('缓存机制', () => { describe('查询参数解析', () => {
test('应该缓存已加载的资源', async () => { test('应该解析带查询参数的资源', async () => {
const firstResult = await manager.resolve('@file://test.md') const result = await manager.resolve('@package://package.json?key=name')
const secondResult = await manager.resolve('@file://test.md')
expect(firstResult.content).toBe(secondResult.content) expect(result.success).toBe(true)
expect(firstResult.success).toBe(true) expect(result.metadata.protocol).toBe('package')
expect(secondResult.success).toBe(true)
}) })
test('应该清除缓存', async () => { test('应该解析加载语义', async () => {
await manager.resolve('@file://test.md') const result = await manager.resolve('@!package://package.json')
expect(manager.cache.size).toBeGreaterThan(0)
manager.clearCache() expect(result.success).toBe(true)
expect(manager.cache.size).toBe(0) expect(result.metadata.protocol).toBe('package')
}) expect(result.metadata.loadingSemantic).toBe('@!')
})
describe('批量资源解析', () => {
test('应该批量解析多个资源', async () => {
const refs = [
'@file://test.md',
'@file://nested.md'
]
const results = await manager.resolveMultiple(refs)
expect(results).toHaveLength(2)
expect(results[0].success).toBe(true)
expect(results[1].success).toBe(true)
expect(results[0].content).toContain('测试文件')
expect(results[1].content).toContain('nested content')
}) })
}) })
describe('错误处理', () => { describe('错误处理', () => {
test('应该处理文件不存在的情况', async () => { test('应该正确处理资源不存在的情况', async () => {
const result = await manager.resolve('@file://nonexistent.md') const result = await manager.resolve('@package://nonexistent.json')
expect(result.success).toBe(false) expect(result.success).toBe(false)
expect(result.error).toBeDefined() expect(result.error).toBeDefined()
expect(result.error.message).toContain('Failed to read file')
}) })
test('应该处理无效的协议', async () => { test('未初始化时应该抛出错误', async () => {
const result = await manager.resolve('@unknown://test') const uninitializedManager = new ResourceManager()
expect(result.success).toBe(false) try {
expect(result.error.message).toContain('Unknown protocol') await uninitializedManager.getProtocolRegistry('package')
}) fail('应该抛出错误')
} catch (error) {
test('应该处理无效的资源引用语法', async () => { expect(error.message).toContain('ResourceManager未初始化')
const result = await manager.resolve('invalid-reference') }
expect(result.success).toBe(false)
expect(result.error.message).toContain('Invalid resource reference syntax')
})
})
describe('验证功能', () => {
test('应该验证有效的资源引用', () => {
expect(manager.isValidReference('@file://test.md')).toBe(true)
expect(manager.isValidReference('@prompt://protocols')).toBe(true)
})
test('应该拒绝无效的资源引用', () => {
expect(manager.isValidReference('invalid')).toBe(false)
expect(manager.isValidReference('@unknown://test')).toBe(false)
})
})
describe('工具功能', () => {
test('应该列出可用协议', () => {
const protocols = manager.listProtocols()
expect(protocols).toContain('file')
expect(protocols).toContain('prompt')
expect(protocols).toContain('memory')
})
test('应该获取注册表信息', () => {
const info = manager.getRegistryInfo('prompt')
expect(info).toBeDefined()
expect(info.name).toBe('prompt')
}) })
}) })
}) })

3
src/tests/fixtures/nested.md vendored Normal file
View File

@ -0,0 +1,3 @@
# 嵌套文件
这是嵌套测试文件。

4
src/tests/fixtures/test.md vendored Normal file
View File

@ -0,0 +1,4 @@
# 测试文件
这是一个测试文件。
第三行内容