From 997292206736412b4261da78faa019422aa4582c Mon Sep 17 00:00:00 2001 From: sean Date: Mon, 16 Jun 2025 12:18:32 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84ActionCommand=E5=92=8CLearnCo?= =?UTF-8?q?mmand=EF=BC=8C=E6=9B=B4=E6=96=B0DPMLContentParser=E5=92=8CSeman?= =?UTF-8?q?ticRenderer=E7=9A=84=E5=AF=BC=E5=85=A5=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=A8=A1=E5=9D=97=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E6=80=A7=E3=80=82=E5=88=A0=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84DPMLContentParser=E5=92=8CS?= =?UTF-8?q?emanticRenderer=E6=96=87=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/resource-protocol-system.md | 563 ++++++++++++++++++ .../{resource => dpml}/DPMLContentParser.js | 0 .../{resource => dpml}/SemanticRenderer.js | 0 src/lib/core/dpml/index.js | 34 ++ src/lib/core/pouch/commands/ActionCommand.js | 4 +- src/lib/core/pouch/commands/LearnCommand.js | 4 +- .../pouch/commands/LearnCommand.js.backup | 4 +- src/tests/README.md | 237 -------- .../adapters/mcp-output-adapter.unit.test.js | 172 ------ ...ActionCommand.dpml-fix.integration.test.js | 180 ------ .../commands/HelloCommand.integration.test.js | 219 ------- src/tests/commands/HelloCommand.unit.test.js | 218 ------- ...PStreamableHttpCommand.integration.test.js | 311 ---------- .../MCPStreamableHttpCommand.unit.test.js | 178 ------ src/tests/commands/mcp-server.unit.test.js | 308 ---------- src/tests/commands/promptxCli.e2e.test.js | 63 -- .../DPMLContentParser.integration.test.js | 106 ---- .../DPMLContentParser.position.unit.test.js | 174 ------ .../resource/DPMLContentParser.unit.test.js | 236 -------- .../EnhancedResourceRegistry.unit.test.js | 420 ------------- .../resource/ProtocolResolver.unit.test.js | 192 ------ .../resource/ResourceManager.unit.test.js | 288 --------- .../resource/SemanticRenderer.unit.test.js | 223 ------- .../SemanticRendering.integration.test.js | 250 -------- .../discovery/BaseDiscovery.unit.test.js | 99 --- .../DiscoveryManager.registry-merge.test.js | 186 ------ .../discovery/DiscoveryManager.unit.test.js | 185 ------ .../discovery/ProjectDiscovery.unit.test.js | 227 ------- .../protocols/PackageProtocol.unit.test.js | 360 ----------- .../protocols/ProjectProtocol.unit.test.js | 263 -------- .../protocols/PromptProtocol.unit.test.js | 298 --------- .../protocols/UserProtocol.unit.test.js | 232 -------- .../resourceManager.integration.test.js | 244 -------- .../resourceProtocolParser.unit.test.js | 133 ----- src/tests/fixtures/nested.md | 3 - src/tests/fixtures/test.md | 4 - src/tests/fixtures/testResources.js | 301 ---------- src/tests/issues/README.md | 103 ---- ...issue-31-windows-path-parsing.unit.test.js | 190 ------ src/tests/issues/platform-folders.e2e.test.js | 276 --------- .../issues/protocol-path-warning.e2e.test.js | 363 ----------- src/tests/setup.js | 123 ---- .../DirectoryService.integration.test.js | 282 --------- 43 files changed, 603 insertions(+), 7653 deletions(-) create mode 100644 docs/architecture/resource-protocol-system.md rename src/lib/core/{resource => dpml}/DPMLContentParser.js (100%) rename src/lib/core/{resource => dpml}/SemanticRenderer.js (100%) create mode 100644 src/lib/core/dpml/index.js delete mode 100644 src/tests/README.md delete mode 100644 src/tests/adapters/mcp-output-adapter.unit.test.js delete mode 100644 src/tests/commands/ActionCommand.dpml-fix.integration.test.js delete mode 100644 src/tests/commands/HelloCommand.integration.test.js delete mode 100644 src/tests/commands/HelloCommand.unit.test.js delete mode 100644 src/tests/commands/MCPStreamableHttpCommand.integration.test.js delete mode 100644 src/tests/commands/MCPStreamableHttpCommand.unit.test.js delete mode 100644 src/tests/commands/mcp-server.unit.test.js delete mode 100644 src/tests/commands/promptxCli.e2e.test.js delete mode 100644 src/tests/core/resource/DPMLContentParser.integration.test.js delete mode 100644 src/tests/core/resource/DPMLContentParser.position.unit.test.js delete mode 100644 src/tests/core/resource/DPMLContentParser.unit.test.js delete mode 100644 src/tests/core/resource/EnhancedResourceRegistry.unit.test.js delete mode 100644 src/tests/core/resource/ProtocolResolver.unit.test.js delete mode 100644 src/tests/core/resource/ResourceManager.unit.test.js delete mode 100644 src/tests/core/resource/SemanticRenderer.unit.test.js delete mode 100644 src/tests/core/resource/SemanticRendering.integration.test.js delete mode 100644 src/tests/core/resource/discovery/BaseDiscovery.unit.test.js delete mode 100644 src/tests/core/resource/discovery/DiscoveryManager.registry-merge.test.js delete mode 100644 src/tests/core/resource/discovery/DiscoveryManager.unit.test.js delete mode 100644 src/tests/core/resource/discovery/ProjectDiscovery.unit.test.js delete mode 100644 src/tests/core/resource/protocols/PackageProtocol.unit.test.js delete mode 100644 src/tests/core/resource/protocols/ProjectProtocol.unit.test.js delete mode 100644 src/tests/core/resource/protocols/PromptProtocol.unit.test.js delete mode 100644 src/tests/core/resource/protocols/UserProtocol.unit.test.js delete mode 100644 src/tests/core/resource/resourceManager.integration.test.js delete mode 100644 src/tests/core/resource/resourceProtocolParser.unit.test.js delete mode 100644 src/tests/fixtures/nested.md delete mode 100644 src/tests/fixtures/test.md delete mode 100644 src/tests/fixtures/testResources.js delete mode 100644 src/tests/issues/README.md delete mode 100644 src/tests/issues/issue-31-windows-path-parsing.unit.test.js delete mode 100644 src/tests/issues/platform-folders.e2e.test.js delete mode 100644 src/tests/issues/protocol-path-warning.e2e.test.js delete mode 100644 src/tests/setup.js delete mode 100644 src/tests/utils/DirectoryService.integration.test.js diff --git a/docs/architecture/resource-protocol-system.md b/docs/architecture/resource-protocol-system.md new file mode 100644 index 0000000..eb6cdec --- /dev/null +++ b/docs/architecture/resource-protocol-system.md @@ -0,0 +1,563 @@ +# PromptX 资源协议系统架构设计 + +## 📋 概述 + +PromptX 资源协议系统采用**三层装饰器模式**架构,实现了统一的资源访问协议体系。系统遵循**奥卡姆剃刀原理**、**单一职责原则**和**约定大于配置**的设计理念,提供简洁、高效、跨平台的资源管理能力。 + +## 🏗️ 架构设计理念 + +### 核心设计原则 +- **🔪 奥卡姆剃刀原理**:追求最简洁有效的解决方案,去除不必要的复杂性 +- **🎯 单一职责原则**:每个组件只负责一个明确的职责,避免功能混杂 +- **⚙️ 约定大于配置**:优先使用智能约定减少用户配置,提供零配置体验 +- **🔄 装饰器模式**:层次化装饰,功能逐步增强,灵活可组合 + +### MVP 设计策略 +- **专注核心功能**:去除缓存、复杂验证等非核心功能 +- **渐进式扩展**:架构支持后续功能的平滑增加 +- **跨平台优先**:统一处理 Windows、macOS、Linux 平台差异 + +## 🎭 三层协议体系 + +### 1. 语义层协议 (AI功能协议) +负责AI功能的语义抽象,通过注册表查找实际资源路径。 + +| 协议 | 描述 | 示例 | +|------|------|------| +| `@role://` | AI角色定义协议 | `@role://product-manager` | +| `@thought://` | 思维模式协议 | `@thought://creativity` | +| `@execution://` | 执行原则协议 | `@execution://best-practice` | +| `@knowledge://` | 知识体系协议 | `@knowledge://javascript` | + +### 2. 路径层协议 (路径抽象协议) +提供跨平台的路径抽象,直接进行路径转换。 + +| 协议 | 描述 | 示例 | +|------|------|------| +| `@user://` | 用户路径协议 | `@user://config/settings.json` | +| `@project://` | 项目路径协议 | `@project://src/lib/core.js` | +| `@package://` | 包路径协议 | `@package://lodash/index.js` | + +### 3. 传输层协议 (物理资源协议) +直接访问物理资源或网络资源。 + +| 协议 | 描述 | 示例 | +|------|------|------| +| `@file://` | 文件系统协议 | `@file:///absolute/path/file.txt` | +| `@http://` | HTTP协议 | `@http://api.example.com/data` | +| `@https://` | HTTPS协议 | `@https://secure.api.com/data` | + +## 📊 系统架构类图 + +```mermaid +classDiagram + %% === Layer 1: Protocol Definition 协议定义层 === + class IResourceProtocol { + <> + +name: string + +version: string + +pathPattern: RegExp + +description: string + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class ResourceProtocol { + <> + +name: string + +version: string + +constructor(name: string, version?: string) + +validate(path: string): boolean + +toString(): string + } + + %% === 语义层协议 === + class RoleProtocol { + +pathPattern: RegExp + +description: "AI角色定义协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class ThoughtProtocol { + +pathPattern: RegExp + +description: "思维模式协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class ExecutionProtocol { + +pathPattern: RegExp + +description: "执行原则协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class KnowledgeProtocol { + +pathPattern: RegExp + +description: "知识体系协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + %% === 路径层协议 === + class UserProtocol { + +pathPattern: RegExp + +description: "用户路径协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class ProjectProtocol { + +pathPattern: RegExp + +description: "项目路径协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class PackageProtocol { + +pathPattern: RegExp + +description: "包路径协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + %% === 传输层协议 === + class FileProtocol { + +pathPattern: RegExp + +description: "文件系统协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + class HttpProtocol { + +pathPattern: RegExp + +description: "HTTP协议" + +validatePath(path: string): boolean + +getExamples(): string[] + } + + %% === Layer 2: Resolution 协议解析层 === + class IResourceResolver { + <> + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + } + + class ResourceResolver { + <> + +platformPath: PlatformPath + +constructor(platformPath: PlatformPath) + +normalizePath(path: string): string + +expandEnvironmentVars(path: string): string + +validatePath(path: string): boolean + } + + %% === 语义层解析器 === + class RoleResolver { + +registryManager: RegistryManager + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + -findRoleInRegistry(roleName: string): string + } + + class ThoughtResolver { + +registryManager: RegistryManager + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + -findThoughtInRegistry(thoughtName: string): string + } + + %% === 路径层解析器 === + class UserResolver { + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + +getUserHome(): string + -resolveUserPath(path: string): string + } + + class ProjectResolver { + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + +getProjectRoot(): string + -resolveProjectPath(path: string): string + } + + class PackageResolver { + +packageManager: PackageManager + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + -findPackagePath(packageName: string): string + } + + %% === 传输层解析器 === + class FileResolver { + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + -resolveAbsolutePath(path: string): string + } + + class HttpResolver { + +resolve(protocolPath: string): Promise~string~ + +canResolve(protocolPath: string): boolean + -validateUrl(url: string): boolean + } + + %% === Layer 3: Loading 内容加载层 === + class IResourceLoader { + <> + +load(filePath: string): Promise~string~ + +canLoad(filePath: string): boolean + +getSupportedExtensions(): string[] + } + + class ResourceLoader { + <> + +encoding: string + +constructor(encoding?: string) + +readFile(filePath: string): Promise~Buffer~ + +detectEncoding(buffer: Buffer): string + +handleError(error: Error, filePath: string): never + } + + class TextLoader { + +load(filePath: string): Promise~string~ + +canLoad(filePath: string): boolean + +getSupportedExtensions(): string[] + -parseTextContent(buffer: Buffer): string + } + + class MarkdownLoader { + +load(filePath: string): Promise~string~ + +canLoad(filePath: string): boolean + +getSupportedExtensions(): string[] + -parseMarkdownContent(buffer: Buffer): string + } + + class JsonLoader { + +load(filePath: string): Promise~string~ + +canLoad(filePath: string): boolean + +getSupportedExtensions(): string[] + -parseJsonContent(buffer: Buffer): string + } + + class HttpLoader { + +load(url: string): Promise~string~ + +canLoad(url: string): boolean + +getSupportedProtocols(): string[] + -fetchContent(url: string): Promise~string~ + } + + %% === Supporting Classes 支持类 === + class PlatformPath { + +platform: string + +separator: string + +homeDir: string + +constructor() + +join(...paths: string[]): string + +resolve(path: string): string + +normalize(path: string): string + +getHomeDirectory(): string + +getEnvironmentVariable(name: string): string + } + + class RegistryManager { + +registryPath: string + +constructor(registryPath: string) + +findResource(type: string, name: string): string + +registerResource(type: string, name: string, path: string): void + +loadRegistry(): Map~string, string~ + } + + class PackageManager { + +packagePaths: string[] + +constructor(packagePaths: string[]) + +findPackage(packageName: string): string + +resolvePackageResource(packageName: string, resourcePath: string): string + } + + %% === Inheritance Relations 继承关系 === + IResourceProtocol <|-- ResourceProtocol + ResourceProtocol <|-- RoleProtocol + ResourceProtocol <|-- ThoughtProtocol + ResourceProtocol <|-- ExecutionProtocol + ResourceProtocol <|-- KnowledgeProtocol + ResourceProtocol <|-- UserProtocol + ResourceProtocol <|-- ProjectProtocol + ResourceProtocol <|-- PackageProtocol + ResourceProtocol <|-- FileProtocol + ResourceProtocol <|-- HttpProtocol + + IResourceResolver <|-- ResourceResolver + ResourceResolver <|-- RoleResolver + ResourceResolver <|-- ThoughtResolver + ResourceResolver <|-- UserResolver + ResourceResolver <|-- ProjectResolver + ResourceResolver <|-- PackageResolver + ResourceResolver <|-- FileResolver + ResourceResolver <|-- HttpResolver + + IResourceLoader <|-- ResourceLoader + ResourceLoader <|-- TextLoader + ResourceLoader <|-- MarkdownLoader + ResourceLoader <|-- JsonLoader + ResourceLoader <|-- HttpLoader + + %% === Composition Relations 组合关系 === + ResourceResolver --> PlatformPath + RoleResolver --> RegistryManager + ThoughtResolver --> RegistryManager + PackageResolver --> PackageManager +``` + +## 🔍 注册表协议引用机制 + +### 注册表结构说明 + +PromptX 的注册表本身也使用协议引用,而不是直接存储物理路径: + +```json +{ + "id": "promptx-architect", + "source": "project", + "protocol": "role", + "name": "Promptx Architect 角色", + "reference": "@project://.promptx/resource/domain/promptx-architect/promptx-architect.role.md" +} +``` + +### 二次协议解析流程 + +语义层协议的解析需要经过两个步骤: + +1. **第一次解析**:`@role://promptx-architect` → 查找注册表 → `@project://...` +2. **第二次解析**:`@project://...` → 路径层解析器 → 物理文件路径 + +这种设计的优势: +- **🔄 协议一致性**:注册表也遵循统一的协议语法 +- **🎯 灵活性**:资源可以存储在不同的位置(用户、项目、包等) +- **🔧 可维护性**:修改资源位置只需更新注册表,不影响引用方 +- **📈 扩展性**:支持跨项目、跨用户的资源引用 + +## 🔄 系统交互序列图 + +### 语义层协议解析流程 + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant RP as RoleProtocol + participant RR as RoleResolver + participant RM as RegistryManager + participant PR as ProjectResolver + participant TL as TextLoader + participant FS as 文件系统 + + Note over Client, FS: 语义层协议解析: @role://promptx-architect + + Client->>RP: validatePath("@role://promptx-architect") + RP-->>Client: true (验证通过) + + Client->>RR: resolve("@role://promptx-architect") + RR->>RM: findResource("role", "promptx-architect") + RM->>FS: 读取注册表文件 + FS-->>RM: 注册表JSON数据 + RM-->>RR: "@project://.promptx/resource/domain/promptx-architect/promptx-architect.role.md" + + Note over RR, PR: 二次协议解析:路径层协议 + RR->>PR: resolve("@project://.promptx/resource/domain/promptx-architect/promptx-architect.role.md") + PR->>PR: getProjectRoot() + 相对路径 + PR-->>RR: "/absolute/project/path/.promptx/resource/domain/promptx-architect/promptx-architect.role.md" + RR-->>Client: 最终解析的文件路径 + + Client->>TL: load("/absolute/project/path/.promptx/resource/domain/promptx-architect/promptx-architect.role.md") + TL->>FS: readFile(filePath) + FS-->>TL: 文件内容Buffer + TL->>TL: parseTextContent(buffer) + TL-->>Client: 角色定义内容 +``` + +### 路径层协议解析流程 + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant UP as UserProtocol + participant UR as UserResolver + participant PP as PlatformPath + participant TL as TextLoader + participant FS as 文件系统 + + Note over Client, FS: 路径层协议解析: @user://config/settings.json + + Client->>UP: validatePath("@user://config/settings.json") + UP-->>Client: true (验证通过) + + Client->>UR: resolve("@user://config/settings.json") + UR->>PP: getHomeDirectory() + PP-->>UR: "/Users/username" + UR->>PP: join(homeDir, "config/settings.json") + PP-->>UR: "/Users/username/config/settings.json" + UR-->>Client: 解析后的绝对路径 + + Client->>TL: load("/Users/username/config/settings.json") + TL->>FS: readFile(filePath) + FS-->>TL: 文件内容Buffer + TL->>TL: parseTextContent(buffer) + TL-->>Client: 配置文件内容 +``` + +### 传输层协议解析流程 + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant FP as FileProtocol + participant FR as FileResolver + participant PP as PlatformPath + participant TL as TextLoader + participant FS as 文件系统 + + Note over Client, FS: 传输层协议解析: @file:///absolute/path/file.txt + + Client->>FP: validatePath("@file:///absolute/path/file.txt") + FP-->>Client: true (验证通过) + + Client->>FR: resolve("@file:///absolute/path/file.txt") + FR->>PP: normalize("/absolute/path/file.txt") + PP-->>FR: "/absolute/path/file.txt" + FR-->>Client: 标准化的绝对路径 + + Client->>TL: load("/absolute/path/file.txt") + TL->>FS: readFile(filePath) + FS-->>TL: 文件内容Buffer + TL->>TL: parseTextContent(buffer) + TL-->>Client: 文件内容 +``` + +## 🔧 跨平台支持 + +### PlatformPath 跨平台抽象 + +```typescript +class PlatformPath { + constructor() { + this.platform = process.platform + this.separator = path.sep + this.homeDir = os.homedir() + } + + // 统一路径拼接 + join(...paths: string[]): string { + return path.join(...paths) + } + + // 统一路径解析 + resolve(inputPath: string): string { + return path.resolve(inputPath) + } + + // 统一路径标准化 + normalize(inputPath: string): string { + return path.normalize(inputPath) + } + + // 统一环境变量获取 + getEnvironmentVariable(name: string): string { + return process.env[name] || '' + } +} +``` + +### 平台差异处理 + +| 平台 | 用户目录 | 路径分隔符 | 配置目录 | +|------|----------|------------|----------| +| Windows | `C:\Users\username` | `\` | `%APPDATA%` | +| macOS | `/Users/username` | `/` | `~/Library` | +| Linux | `/home/username` | `/` | `~/.config` | + +## 📈 扩展性设计 + +### 新协议添加流程 + +1. **定义协议类**:继承 `ResourceProtocol` +2. **实现解析器**:继承 `ResourceResolver` +3. **注册协议**:添加到协议注册表 +4. **测试验证**:编写单元测试 + +### 新加载器添加流程 + +1. **定义加载器类**:继承 `ResourceLoader` +2. **实现加载逻辑**:重写 `load()` 方法 +3. **注册加载器**:添加到加载器工厂 +4. **测试验证**:编写单元测试 + +## 🎯 使用示例 + +### 基础用法 + +```typescript +// 语义层协议使用 +const roleContent = await resourceSystem.load('@role://product-manager') +const thoughtContent = await resourceSystem.load('@thought://creativity') + +// 路径层协议使用 +const userConfig = await resourceSystem.load('@user://config/settings.json') +const projectFile = await resourceSystem.load('@project://src/index.js') + +// 传输层协议使用 +const localFile = await resourceSystem.load('@file:///path/to/file.txt') +const remoteData = await resourceSystem.load('@https://api.example.com/data') +``` + +### 高级用法 + +```typescript +// 协议验证 +const isValid = RoleProtocol.validatePath('@role://invalid-name') + +// 自定义解析器 +class CustomResolver extends ResourceResolver { + async resolve(protocolPath: string): Promise { + // 自定义解析逻辑 + return this.customResolveLogic(protocolPath) + } +} + +// 自定义加载器 +class XmlLoader extends ResourceLoader { + async load(filePath: string): Promise { + const buffer = await this.readFile(filePath) + return this.parseXmlContent(buffer) + } +} +``` + +## 🚀 性能优化 + +### MVP 阶段优化策略 + +1. **延迟加载**:按需加载协议解析器和加载器 +2. **路径缓存**:缓存已解析的路径映射关系 +3. **并发处理**:支持多个资源的并发加载 +4. **错误恢复**:优雅的错误处理和重试机制 + +### 未来扩展优化 + +1. **内容缓存**:添加智能内容缓存系统 +2. **预加载**:预测性资源预加载 +3. **压缩传输**:网络资源的压缩传输 +4. **增量更新**:支持资源的增量更新 + +## 📝 总结 + +PromptX 资源协议系统通过三层装饰器架构,实现了: + +- **🎯 统一的资源访问接口**:所有资源通过统一的 `@protocol://` 语法访问 +- **🔄 灵活的扩展机制**:支持新协议和新加载器的平滑添加 +- **🌍 完整的跨平台支持**:统一处理不同操作系统的差异 +- **⚡ 高效的解析性能**:MVP 设计专注核心功能,性能优异 +- **🛠️ 简洁的使用体验**:零配置开箱即用,符合约定大于配置理念 + +这个架构为 PromptX 系统提供了坚实的资源管理基础,支持未来功能的持续演进和扩展。 \ No newline at end of file diff --git a/src/lib/core/resource/DPMLContentParser.js b/src/lib/core/dpml/DPMLContentParser.js similarity index 100% rename from src/lib/core/resource/DPMLContentParser.js rename to src/lib/core/dpml/DPMLContentParser.js diff --git a/src/lib/core/resource/SemanticRenderer.js b/src/lib/core/dpml/SemanticRenderer.js similarity index 100% rename from src/lib/core/resource/SemanticRenderer.js rename to src/lib/core/dpml/SemanticRenderer.js diff --git a/src/lib/core/dpml/index.js b/src/lib/core/dpml/index.js new file mode 100644 index 0000000..17e91ee --- /dev/null +++ b/src/lib/core/dpml/index.js @@ -0,0 +1,34 @@ +/** + * PromptX DPML Module + * DPML协议解析和内容处理模块 + * + * 提供DPML语法解析、标签处理、语义结构构建功能 + */ + +const DPMLContentParser = require('./DPMLContentParser') + +module.exports = { + // 核心解析器 + DPMLContentParser, + + // 便捷方法 - 创建解析器实例 + createParser: () => new DPMLContentParser(), + + // 便捷方法 - 快速解析标签内容 + parseTagContent: (content, tagName) => { + const parser = new DPMLContentParser() + return parser.parseTagContent(content, tagName) + }, + + // 便捷方法 - 快速解析角色文档 + parseRoleDocument: (roleContent) => { + const parser = new DPMLContentParser() + return parser.parseRoleDocument(roleContent) + }, + + // 便捷方法 - 提取引用 + extractReferences: (content) => { + const parser = new DPMLContentParser() + return parser.extractReferences(content) + } +} \ No newline at end of file diff --git a/src/lib/core/pouch/commands/ActionCommand.js b/src/lib/core/pouch/commands/ActionCommand.js index 81aaad9..d7cbdfa 100644 --- a/src/lib/core/pouch/commands/ActionCommand.js +++ b/src/lib/core/pouch/commands/ActionCommand.js @@ -3,8 +3,8 @@ const fs = require('fs-extra') const path = require('path') const { COMMANDS } = require('../../../../constants') const { getGlobalResourceManager } = require('../../resource') -const DPMLContentParser = require('../../resource/DPMLContentParser') -const SemanticRenderer = require('../../resource/SemanticRenderer') +const DPMLContentParser = require('../../dpml/DPMLContentParser') +const SemanticRenderer = require('../../dpml/SemanticRenderer') const logger = require('../../../utils/logger') /** diff --git a/src/lib/core/pouch/commands/LearnCommand.js b/src/lib/core/pouch/commands/LearnCommand.js index 1cb7ef3..292f3ef 100644 --- a/src/lib/core/pouch/commands/LearnCommand.js +++ b/src/lib/core/pouch/commands/LearnCommand.js @@ -1,7 +1,7 @@ const BasePouchCommand = require('../BasePouchCommand') const { getGlobalResourceManager } = require('../../resource') -const DPMLContentParser = require('../../resource/DPMLContentParser') -const SemanticRenderer = require('../../resource/SemanticRenderer') +const DPMLContentParser = require('../../dpml/DPMLContentParser') +const SemanticRenderer = require('../../dpml/SemanticRenderer') const { COMMANDS } = require('../../../../constants') /** diff --git a/src/lib/core/pouch/commands/LearnCommand.js.backup b/src/lib/core/pouch/commands/LearnCommand.js.backup index 08a8955..c6528e2 100644 --- a/src/lib/core/pouch/commands/LearnCommand.js.backup +++ b/src/lib/core/pouch/commands/LearnCommand.js.backup @@ -1,7 +1,7 @@ const BasePouchCommand = require('../BasePouchCommand') const ResourceManager = require('../../resource/resourceManager') -const DPMLContentParser = require('../../resource/DPMLContentParser') -const SemanticRenderer = require('../../resource/SemanticRenderer') +const DPMLContentParser = require('../../dpml/DPMLContentParser') +const SemanticRenderer = require('../../dpml/SemanticRenderer') const { COMMANDS } = require('../../../../constants') /** diff --git a/src/tests/README.md b/src/tests/README.md deleted file mode 100644 index c95294a..0000000 --- a/src/tests/README.md +++ /dev/null @@ -1,237 +0,0 @@ -# PromptX 测试指南 - -本文档介绍 PromptX 项目的测试规范、命名规则和执行方式。 - -## 测试文件命名规范 - -### 命名格式 -所有测试文件必须使用 **驼峰命名法(camelCase)** 并明确标识测试类型: - -``` -{模块名}.{测试类型}.test.js -``` - -### 测试类型 -- **unit**: 单元测试 - 测试单个函数或类的功能 -- **integration**: 集成测试 - 测试多个组件之间的协作 -- **e2e**: 端到端测试 - 测试完整的用户工作流 - -### 示例 -``` -resourceProtocolParser.unit.test.js -resourceRegistry.unit.test.js -resourceManager.integration.test.js -promptxCli.e2e.test.js -``` - -## 测试目录结构 - -``` -src/tests/ -├── setup.js # 全局测试配置 -├── fixtures/ # 测试固定数据 -│ └── testResources.js # 测试资源工厂 -├── __mocks__/ # 模拟对象 -├── core/ -│ └── resource/ -│ ├── resourceProtocolParser.unit.test.js -│ ├── resourceRegistry.unit.test.js -│ └── resourceManager.integration.test.js -└── commands/ - └── promptxCli.e2e.test.js -``` - -## 执行测试 - -### 运行所有测试 -```bash -npm test -``` - -### 分类运行测试 -```bash -# 单元测试 -npm run test:unit - -# 集成测试 -npm run test:integration - -# 端到端测试 -npm run test:e2e -``` - -### 开发模式 -```bash -# 监听模式运行所有测试 -npm run test:watch - -# 监听模式运行单元测试 -npm run test:watchUnit - -# 监听模式运行集成测试 -npm run test:watchIntegration -``` - -### 覆盖率测试 -```bash -# 生成覆盖率报告 -npm run test:coverage - -# 分类生成覆盖率报告 -npm run test:coverageUnit -npm run test:coverageIntegration -npm run test:coverageE2e -``` - -### CI/CD 测试 -```bash -# 持续集成环境测试 -npm run test:ci -``` - -### 调试测试 -```bash -# 调试模式运行测试 -npm run test:debug -``` - -## 测试编写规范 - -### 单元测试 (*.unit.test.js) -- 测试单个函数、类或模块 -- 使用模拟(mock)隔离外部依赖 -- 快速执行,无外部资源依赖 -- 覆盖边界条件和错误场景 - -```javascript -describe('ResourceProtocolParser - Unit Tests', () => { - let parser; - - beforeEach(() => { - parser = new ResourceProtocolParser(); - }); - - describe('基础语法解析', () => { - test('应该解析基本的资源引用', () => { - const result = parser.parse('@promptx://protocols'); - expect(result.protocol).toBe('promptx'); - expect(result.path).toBe('protocols'); - }); - }); -}); -``` - -### 集成测试 (*.integration.test.js) -- 测试多个组件之间的协作 -- 可使用真实的文件系统和临时资源 -- 测试完整的数据流和业务逻辑 -- 关注组件间接口和数据传递 - -```javascript -describe('ResourceManager - Integration Tests', () => { - let manager; - let tempDir; - - beforeAll(async () => { - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-test-')); - }); - - afterAll(async () => { - await fs.rmdir(tempDir, { recursive: true }); - }); - - test('应该解析并加载本地文件', async () => { - const result = await manager.resolve('@file://test.md'); - expect(result.success).toBe(true); - }); -}); -``` - -### 端到端测试 (*.e2e.test.js) -- 测试完整的用户工作流 -- 通过CLI接口测试实际使用场景 -- 模拟真实的用户交互 -- 验证系统的整体行为 - -```javascript -describe('PromptX CLI - E2E Tests', () => { - function runCommand(args) { - return new Promise((resolve, reject) => { - const child = spawn('node', [CLI_PATH, ...args]); - // ... 实现命令执行逻辑 - }); - } - - test('应该支持完整的AI认知循环', async () => { - const helloResult = await runCommand(['hello']); - expect(helloResult.code).toBe(0); - - const learnResult = await runCommand(['learn', '@file://bootstrap.md']); - expect(learnResult.code).toBe(0); - }); -}); -``` - -## 测试工具和辅助函数 - -### 全局测试工具 -在 `setup.js` 中定义的全局工具: - -```javascript -// 等待函数 -await testUtils.sleep(1000); - -// 延迟Promise -const result = await testUtils.delayed('value', 100); - -// 延迟拒绝 -await expect(testUtils.delayedReject(new Error('test'), 100)).rejects.toThrow(); -``` - -### 自定义断言 -```javascript -// 验证DPML资源引用 -expect('@promptx://protocols').toBeValidDpmlReference(); - -// 验证对象属性 -expect(result).toHaveRequiredProperties(['protocol', 'path']); -``` - -### 测试资源工厂 -使用 `TestResourceFactory` 创建测试数据: - -```javascript -const { createTestFactory } = require('../fixtures/testResources'); - -const factory = createTestFactory(); -const tempDir = await factory.createTempDir(); -const { structure, files } = await factory.createPromptXStructure(tempDir); -``` - -## 覆盖率要求 - -项目设置了以下覆盖率阈值: -- 分支覆盖率: 80% -- 函数覆盖率: 80% -- 行覆盖率: 80% -- 语句覆盖率: 80% - -## 最佳实践 - -1. **命名清晰**: 测试名称应清楚描述测试的功能 -2. **独立性**: 每个测试应该独立运行,不依赖其他测试 -3. **快速执行**: 单元测试应该快速执行 -4. **完整清理**: 集成测试和E2E测试应清理临时资源 -5. **错误场景**: 不仅测试正常情况,也要测试错误和边界情况 -6. **文档化**: 复杂的测试逻辑应有适当的注释说明 - -## 持续集成 - -在 CI/CD 环境中,测试按以下顺序执行: -1. 代码格式检查 (`npm run lint`) -2. 单元测试 (`npm run test:unit`) -3. 集成测试 (`npm run test:integration`) -4. 端到端测试 (`npm run test:e2e`) -5. 覆盖率检查 - -只有所有测试通过且覆盖率达标,才能合并代码或发布版本。 \ No newline at end of file diff --git a/src/tests/adapters/mcp-output-adapter.unit.test.js b/src/tests/adapters/mcp-output-adapter.unit.test.js deleted file mode 100644 index 286ae6b..0000000 --- a/src/tests/adapters/mcp-output-adapter.unit.test.js +++ /dev/null @@ -1,172 +0,0 @@ -const { MCPOutputAdapter } = require('../../lib/adapters/MCPOutputAdapter'); - -describe('MCPOutputAdapter 单元测试', () => { - let adapter; - - beforeEach(() => { - adapter = new MCPOutputAdapter(); - }); - - describe('基础功能测试', () => { - test('MCPOutputAdapter类应该能创建', () => { - expect(adapter).toBeDefined(); - expect(adapter).toBeInstanceOf(MCPOutputAdapter); - }); - - test('应该有convertToMCPFormat方法', () => { - expect(typeof adapter.convertToMCPFormat).toBe('function'); - }); - - test('应该有sanitizeText方法', () => { - expect(typeof adapter.sanitizeText).toBe('function'); - }); - - test('应该有handleError方法', () => { - expect(typeof adapter.handleError).toBe('function'); - }); - }); - - describe('文本转换测试', () => { - test('应该保留emoji和中文字符', () => { - const input = '🎯 PromptX 系统初始化完成!'; - const result = adapter.convertToMCPFormat(input); - - expect(result.content).toBeDefined(); - expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain('🎯'); - expect(result.content[0].text).toContain('PromptX'); - }); - - test('应该保留markdown格式', () => { - const input = '## 🎯 角色激活总结\n✅ **assistant 角色已完全激活!**'; - const result = adapter.convertToMCPFormat(input); - - expect(result.content[0].text).toContain('##'); - expect(result.content[0].text).toContain('**'); - expect(result.content[0].text).toContain('✅'); - }); - - test('应该处理复杂的PromptX输出格式', () => { - const input = `============================================================ -🎯 锦囊目的:激活特定AI角色,分析并生成具体的思维模式、行为模式和知识学习计划 -============================================================ - -📜 锦囊内容: -🎭 **角色激活完成:assistant** - 所有技能已自动加载`; - - const result = adapter.convertToMCPFormat(input); - - expect(result.content[0].text).toContain('🎯'); - expect(result.content[0].text).toContain('📜'); - expect(result.content[0].text).toContain('🎭'); - expect(result.content[0].text).toContain('===='); - }); - - test('应该处理多行内容', () => { - const input = `行1\n行2\n行3`; - const result = adapter.convertToMCPFormat(input); - - expect(result.content[0].text).toContain('行1'); - expect(result.content[0].text).toContain('行2'); - expect(result.content[0].text).toContain('行3'); - }); - }); - - describe('对象输入处理测试', () => { - test('应该处理PouchOutput对象', () => { - const mockPouchOutput = { - toString: () => '🎯 模拟的PouchOutput输出' - }; - - const result = adapter.convertToMCPFormat(mockPouchOutput); - expect(result.content[0].text).toBe('🎯 模拟的PouchOutput输出'); - }); - - test('应该处理普通对象', () => { - const input = { message: '测试消息', status: 'success' }; - const result = adapter.convertToMCPFormat(input); - - expect(result.content[0].text).toContain('message'); - expect(result.content[0].text).toContain('测试消息'); - }); - - test('应该处理null和undefined', () => { - const nullResult = adapter.convertToMCPFormat(null); - const undefinedResult = adapter.convertToMCPFormat(undefined); - - expect(nullResult.content[0].text).toBe('null'); - expect(undefinedResult.content[0].text).toBe('undefined'); - }); - }); - - describe('错误处理测试', () => { - test('应该处理转换错误', () => { - const result = adapter.handleError(new Error('测试错误')); - - expect(result.content[0].text).toContain('❌'); - expect(result.content[0].text).toContain('测试错误'); - expect(result.isError).toBe(true); - }); - - test('应该处理未知错误', () => { - const result = adapter.handleError('字符串错误'); - - expect(result.content[0].text).toContain('❌'); - expect(result.content[0].text).toContain('字符串错误'); - expect(result.isError).toBe(true); - }); - - test('错误输出应该符合MCP格式', () => { - const result = adapter.handleError(new Error('测试')); - - expect(result.content).toBeDefined(); - expect(Array.isArray(result.content)).toBe(true); - expect(result.content[0].type).toBe('text'); - expect(typeof result.content[0].text).toBe('string'); - }); - }); - - describe('边界情况测试', () => { - test('应该处理空字符串', () => { - const result = adapter.convertToMCPFormat(''); - expect(result.content[0].text).toBe(''); - }); - - test('应该处理非常长的文本', () => { - const longText = 'a'.repeat(10000); - const result = adapter.convertToMCPFormat(longText); - expect(result.content[0].text).toBe(longText); - }); - - test('应该处理特殊字符', () => { - const specialChars = '\\n\\r\\t"\'{|}[]()'; - const result = adapter.convertToMCPFormat(specialChars); - expect(result.content[0].text).toContain(specialChars); - }); - }); - - describe('输出格式验证测试', () => { - test('输出应该始终符合MCP content格式', () => { - const inputs = [ - 'simple text', - '🎯 emoji text', - { object: 'data' }, - ['array', 'data'], - null, - undefined - ]; - - inputs.forEach(input => { - const result = adapter.convertToMCPFormat(input); - - // 验证MCP标准格式 - expect(result).toHaveProperty('content'); - expect(Array.isArray(result.content)).toBe(true); - expect(result.content).toHaveLength(1); - expect(result.content[0]).toHaveProperty('type', 'text'); - expect(result.content[0]).toHaveProperty('text'); - expect(typeof result.content[0].text).toBe('string'); - }); - }); - }); -}); \ No newline at end of file diff --git a/src/tests/commands/ActionCommand.dpml-fix.integration.test.js b/src/tests/commands/ActionCommand.dpml-fix.integration.test.js deleted file mode 100644 index 90fde3e..0000000 --- a/src/tests/commands/ActionCommand.dpml-fix.integration.test.js +++ /dev/null @@ -1,180 +0,0 @@ -const ActionCommand = require('../../lib/core/pouch/commands/ActionCommand') -const path = require('path') -const fs = require('fs-extra') - -describe('ActionCommand DPML修复验证测试', () => { - let actionCommand - - beforeEach(() => { - actionCommand = new ActionCommand() - }) - - describe('角色内容解析修复验证', () => { - test('应该完整解析internet-debater角色的直接内容', async () => { - // 模拟角色信息 - const mockRoleInfo = { - id: 'internet-debater', - name: '互联网杠精', - file: '.promptx/resource/domain/internet-debater/internet-debater.role.md' - } - - // 检查角色文件是否存在 - const roleFilePath = path.join(process.cwd(), mockRoleInfo.file) - const exists = await fs.pathExists(roleFilePath) - - if (!exists) { - console.log('跳过测试:internet-debater角色文件不存在') - return - } - - // 分析角色依赖 - const dependencies = await actionCommand.analyzeRoleDependencies(mockRoleInfo) - - // 验证新的语义结构存在 - expect(dependencies).toHaveProperty('roleSemantics') - expect(dependencies.roleSemantics).toHaveProperty('personality') - expect(dependencies.roleSemantics).toHaveProperty('principle') - expect(dependencies.roleSemantics).toHaveProperty('knowledge') - - // 验证personality直接内容 - const personality = dependencies.roleSemantics.personality - expect(personality).toBeTruthy() - expect(personality.directContent).toContain('网络杠精思维模式') - expect(personality.directContent).toContain('挑刺思维') - expect(personality.directContent).toContain('抬杠本能') - expect(personality.directContent.length).toBeGreaterThan(400) - - // 验证principle直接内容 - const principle = dependencies.roleSemantics.principle - expect(principle).toBeTruthy() - expect(principle.directContent).toContain('网络杠精行为原则') - expect(principle.directContent).toContain('逢言必反') - expect(principle.directContent).toContain('抠字眼优先') - expect(principle.directContent.length).toBeGreaterThan(500) - - // 验证knowledge直接内容 - const knowledge = dependencies.roleSemantics.knowledge - expect(knowledge).toBeTruthy() - expect(knowledge.directContent).toContain('网络杠精专业知识体系') - expect(knowledge.directContent).toContain('逻辑谬误大全') - expect(knowledge.directContent).toContain('稻草人谬误') - expect(knowledge.directContent.length).toBeGreaterThan(800) - - console.log('✅ internet-debater角色直接内容解析成功') - console.log(` - personality: ${personality.directContent.length} 字符`) - console.log(` - principle: ${principle.directContent.length} 字符`) - console.log(` - knowledge: ${knowledge.directContent.length} 字符`) - console.log(` - 总内容: ${personality.directContent.length + principle.directContent.length + knowledge.directContent.length} 字符`) - }) - - test('应该生成包含完整内容的学习计划', async () => { - const mockRoleInfo = { - id: 'internet-debater', - name: '互联网杠精', - file: '.promptx/resource/domain/internet-debater/internet-debater.role.md' - } - - const roleFilePath = path.join(process.cwd(), mockRoleInfo.file) - const exists = await fs.pathExists(roleFilePath) - - if (!exists) { - console.log('跳过测试:internet-debater角色文件不存在') - return - } - - // 分析依赖并生成学习计划 - const dependencies = await actionCommand.analyzeRoleDependencies(mockRoleInfo) - - // Mock executeRecall 方法避免实际调用 - actionCommand.executeRecall = jest.fn().mockResolvedValue('---\n## 🧠 自动记忆检索结果\n模拟记忆内容\n') - - const learningPlan = await actionCommand.generateLearningPlan(mockRoleInfo.id, dependencies) - - // 验证学习计划包含直接内容 - expect(learningPlan).toContain('角色激活完成:internet-debater') - expect(learningPlan).toContain('网络杠精思维模式') - expect(learningPlan).toContain('挑刺思维') - expect(learningPlan).toContain('网络杠精行为原则') - expect(learningPlan).toContain('逢言必反') - expect(learningPlan).toContain('网络杠精专业知识体系') - expect(learningPlan).toContain('逻辑谬误大全') - - // 验证角色组件信息 - expect(learningPlan).toContain('🎭 角色组件:👤 人格特征, ⚖️ 行为原则, 📚 专业知识') - - console.log('✅ 学习计划包含完整的角色内容') - console.log(` 学习计划长度: ${learningPlan.length} 字符`) - }) - - test('修复前后对比:应该展示语义完整性的提升', async () => { - // 创建混合内容测试 - const testContent = ` - - @!thought://remember - @!thought://recall - - # 杠精思维特征 - - 挑刺思维:看到任何观点都先找问题 - - 抬杠本能:天生反对派 - - - @!execution://assistant - - # 杠精行为原则 - - 逢言必反:对任何观点都要找反对角度 - - 抠字眼优先:从用词表述找问题 - -` - - // 使用新的DPMLContentParser解析 - const roleSemantics = actionCommand.dpmlParser.parseRoleDocument(testContent) - - // 验证混合内容解析 - expect(roleSemantics.personality.references).toHaveLength(2) - expect(roleSemantics.personality.references.map(r => r.resource)).toEqual(['remember', 'recall']) - expect(roleSemantics.personality.directContent).toContain('杠精思维特征') - expect(roleSemantics.personality.directContent).toContain('挑刺思维') - - expect(roleSemantics.principle.references).toHaveLength(1) - expect(roleSemantics.principle.references[0].resource).toBe('assistant') - expect(roleSemantics.principle.directContent).toContain('杠精行为原则') - expect(roleSemantics.principle.directContent).toContain('逢言必反') - - console.log('📊 修复验证结果:') - console.log(` personality: ${roleSemantics.personality.references.length}个引用 + ${roleSemantics.personality.directContent.length}字符直接内容`) - console.log(` principle: ${roleSemantics.principle.references.length}个引用 + ${roleSemantics.principle.directContent.length}字符直接内容`) - console.log(` 🎯 混合内容解析成功:引用和直接内容都被完整保留`) - }) - }) - - describe('向下兼容性验证', () => { - test('应该兼容纯@引用的系统角色', () => { - const testContent = ` - - @!thought://remember - @!thought://recall - @!thought://assistant - - - @!execution://assistant - -` - - const roleSemantics = actionCommand.dpmlParser.parseRoleDocument(testContent) - - // 验证引用解析正常 - expect(roleSemantics.personality.references).toHaveLength(3) - expect(roleSemantics.principle.references).toHaveLength(1) - - // 验证没有直接内容 - expect(roleSemantics.personality.directContent).toBe('') - expect(roleSemantics.principle.directContent).toBe('') - - // 验证内容类型 - expect(roleSemantics.personality.metadata.contentType).toBe('references-only') - expect(roleSemantics.principle.metadata.contentType).toBe('references-only') - - console.log('✅ 向下兼容性验证通过:纯@引用角色正常解析') - }) - }) -}) \ No newline at end of file diff --git a/src/tests/commands/HelloCommand.integration.test.js b/src/tests/commands/HelloCommand.integration.test.js deleted file mode 100644 index 9884400..0000000 --- a/src/tests/commands/HelloCommand.integration.test.js +++ /dev/null @@ -1,219 +0,0 @@ -const path = require('path') -const fs = require('fs-extra') -const os = require('os') -const HelloCommand = require('../../lib/core/pouch/commands/HelloCommand') - -/** - * HelloCommand集成测试 - * - * 测试HelloCommand与ResourceManager的集成,包括: - * 1. 用户角色发现 - * 2. 系统角色与用户角色的合并 - * 3. 错误处理 - */ -describe('HelloCommand - ResourceManager集成', () => { - let helloCommand - let tempDir - let userRoleDir - - beforeEach(async () => { - helloCommand = new HelloCommand() - - // 创建临时测试环境 - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hello-command-integration-')) - userRoleDir = path.join(tempDir, 'user-roles') - await fs.ensureDir(userRoleDir) - }) - - afterEach(async () => { - if (tempDir) { - await fs.remove(tempDir) - } - jest.clearAllMocks() - }) - - describe('用户角色发现集成', () => { - test('应该显示用户创建的角色', async () => { - // 创建模拟用户角色文件 - const customRoleDir = path.join(userRoleDir, 'custom-role') - await fs.ensureDir(customRoleDir) - await fs.writeFile( - path.join(customRoleDir, 'custom-role.role.md'), - `# 自定义专家 -> 这是一个用户自定义的专业角色 - - -## 角色定义 -专业的自定义角色,具备特定的技能和知识。 -` - ) - - // 直接模拟loadRoleRegistry方法返回期望的角色注册表 - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - 'assistant': { - file: '@package://prompt/domain/assistant/assistant.role.md', - name: '🙋 智能助手', - description: '通用助理角色,提供基础的助理服务和记忆支持', - source: 'system' - }, - 'custom-role': { - file: path.join(customRoleDir, 'custom-role.role.md'), - name: '自定义专家', - description: '这是一个用户自定义的专业角色', - source: 'user-generated' - } - }) - - const content = await helloCommand.getContent([]) - - expect(content).toContain('自定义专家') - expect(content).toContain('智能助手') - expect(content).toContain('custom-role') - expect(content).toContain('assistant') - }) - - test('应该允许用户角色覆盖系统角色', async () => { - // 创建用户自定义的assistant角色 - const assistantRoleDir = path.join(userRoleDir, 'assistant') - await fs.ensureDir(assistantRoleDir) - await fs.writeFile( - path.join(assistantRoleDir, 'assistant.role.md'), - `# 🚀 增强助手 -> 用户自定义的增强版智能助手 - - -## 角色定义 -增强版的智能助手,具备更多专业能力。 -` - ) - - // 直接模拟loadRoleRegistry方法返回用户覆盖的角色 - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - 'assistant': { - file: path.join(assistantRoleDir, 'assistant.role.md'), - name: '🚀 增强助手', - description: '用户自定义的增强版智能助手', - source: 'user-generated' - } - }) - - const content = await helloCommand.getContent([]) - - expect(content).toContain('🚀 增强助手') - expect(content).toContain('用户自定义') - expect(content).not.toContain('🙋 智能助手') // 不应该包含原始系统角色 - }) - - test('应该同时显示系统角色和用户角色', async () => { - // 创建用户角色目录和文件 - const webDevRoleDir = path.join(userRoleDir, 'web-developer') - await fs.ensureDir(webDevRoleDir) - await fs.writeFile( - path.join(webDevRoleDir, 'web-developer.role.md'), - `# 前端开发专家 -> 专业的前端开发工程师 - - -## 角色定义 -精通HTML、CSS、JavaScript的前端开发专家。 -` - ) - - // 直接模拟loadRoleRegistry方法返回系统和用户角色 - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - 'assistant': { - file: '@package://prompt/domain/assistant/assistant.role.md', - name: '🙋 智能助手', - description: '通用助理角色,提供基础的助理服务和记忆支持', - source: 'system' - }, - 'web-developer': { - file: path.join(webDevRoleDir, 'web-developer.role.md'), - name: '前端开发专家', - description: '专业的前端开发工程师', - source: 'user-generated' - } - }) - - const content = await helloCommand.getContent([]) - - expect(content).toContain('智能助手') - expect(content).toContain('前端开发专家') - expect(content).toContain('assistant') - expect(content).toContain('web-developer') - }) - }) - - describe('错误处理', () => { - test('应该优雅处理资源发现失败', async () => { - // 这里不能直接模拟loadRoleRegistry抛出错误,因为会绕过内部的try-catch - // 相反,我们模拟loadRoleRegistry返回fallback角色(表示内部发生了错误) - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - assistant: { - file: '@package://prompt/domain/assistant/assistant.role.md', - name: '🙋 智能助手', - description: '通用助理角色,提供基础的助理服务和记忆支持', - source: 'fallback' - } - }) - - // 应该不抛出异常 - const result = await helloCommand.execute([]) - - expect(result).toBeDefined() - expect(result.content).toContain('智能助手') // 应该fallback到默认角色 - expect(result.content).toContain('(默认角色)') // 应该显示fallback标签 - }) - - test('应该处理空的资源注册表', async () => { - // 模拟空的资源注册表时,loadRoleRegistry会自动添加fallback角色 - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - assistant: { - file: '@package://prompt/domain/assistant/assistant.role.md', - name: '🙋 智能助手', - description: '通用助理角色,提供基础的助理服务和记忆支持', - source: 'fallback' - } - }) - - const result = await helloCommand.execute([]) - - expect(result).toBeDefined() - expect(result.content).toContain('智能助手') - expect(result.content).toContain('(默认角色)') // 应该标注为fallback角色 - }) - }) - - describe('HATEOAS支持', () => { - test('应该返回正确的可用状态转换', async () => { - const hateoas = await helloCommand.getPATEOAS([]) - - expect(hateoas.currentState).toBe('role_discovery') - expect(hateoas.availableTransitions).toContain('action') - expect(hateoas.nextActions).toBeDefined() - expect(Array.isArray(hateoas.nextActions)).toBe(true) - }) - }) - - describe('命令执行集成', () => { - test('应该成功执行完整的角色发现流程', async () => { - // 模拟基础系统角色 - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - 'assistant': { - file: '@package://prompt/domain/assistant/assistant.role.md', - name: '🙋 智能助手', - description: '通用助理角色,提供基础的助理服务和记忆支持', - source: 'system' - } - }) - - const result = await helloCommand.execute([]) - - expect(result).toBeDefined() - expect(result.purpose).toContain('为AI提供可用角色信息') - expect(result.content).toContain('AI专业角色服务清单') - expect(result.content).toContain('激活命令') - expect(result.pateoas).toBeDefined() - }) - }) -}) \ No newline at end of file diff --git a/src/tests/commands/HelloCommand.unit.test.js b/src/tests/commands/HelloCommand.unit.test.js deleted file mode 100644 index 79e04cf..0000000 --- a/src/tests/commands/HelloCommand.unit.test.js +++ /dev/null @@ -1,218 +0,0 @@ -const path = require('path') -const fs = require('fs-extra') -const os = require('os') -const HelloCommand = require('../../lib/core/pouch/commands/HelloCommand') - -describe('HelloCommand 单元测试', () => { - let helloCommand - let tempDir - let tempProjectDir - - beforeEach(async () => { - helloCommand = new HelloCommand() - - // 创建临时目录模拟项目结构 - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hello-command-test-')) - tempProjectDir = path.join(tempDir, 'test-project') - - // 创建基础目录结构 - await fs.ensureDir(path.join(tempProjectDir, 'prompt', 'domain')) - await fs.ensureDir(path.join(tempProjectDir, '.promptx', 'user-roles')) - }) - - afterEach(async () => { - if (tempDir) { - await fs.remove(tempDir) - } - // 清理 mock - jest.clearAllMocks() - }) - - describe('基础功能测试', () => { - test('应该能实例化HelloCommand', () => { - expect(helloCommand).toBeInstanceOf(HelloCommand) - expect(typeof helloCommand.loadRoleRegistry).toBe('function') - expect(helloCommand.resourceManager).toBeDefined() - }) - - test('getPurpose应该返回正确的目的描述', () => { - const purpose = helloCommand.getPurpose() - expect(purpose).toContain('AI') - expect(purpose).toContain('角色') - }) - }) - - describe('ResourceManager 集成测试', () => { - test('应该能发现系统内置角色', async () => { - // Mock ResourceManager的initializeWithNewArchitecture和registry - const mockRegistry = new Map([ - ['role:assistant', '@package://prompt/domain/assistant/assistant.role.md'] - ]) - mockRegistry.index = mockRegistry // 向后兼容 - - helloCommand.resourceManager.initializeWithNewArchitecture = jest.fn().mockResolvedValue() - helloCommand.resourceManager.registry = { index: mockRegistry } - helloCommand.resourceManager.loadResource = jest.fn().mockResolvedValue({ - success: true, - content: '# 🙋 智能助手\n> 通用助理角色,提供基础的助理服务和记忆支持' - }) - - const roleRegistry = await helloCommand.loadRoleRegistry() - - expect(roleRegistry).toHaveProperty('assistant') - expect(roleRegistry.assistant.name).toContain('智能助手') - expect(roleRegistry.assistant.description).toContain('助理') - expect(roleRegistry.assistant.source).toBe('system') - }) - - test('应该处理空的角色目录', async () => { - // Mock ResourceManager返回空注册表 - const mockRegistry = new Map() - mockRegistry.index = mockRegistry - - helloCommand.resourceManager.initializeWithNewArchitecture = jest.fn().mockResolvedValue() - helloCommand.resourceManager.registry = { index: mockRegistry } - - const roleRegistry = await helloCommand.loadRoleRegistry() - - // 应该返回fallback assistant角色 - expect(roleRegistry).toHaveProperty('assistant') - expect(roleRegistry.assistant.source).toBe('fallback') - }) - - test('应该使用ResourceManager处理错误', async () => { - const mockedCommand = new HelloCommand() - - // Mock ResourceManager to throw an error - mockedCommand.resourceManager.initializeWithNewArchitecture = jest.fn().mockRejectedValue(new Error('Mock error')) - - // 应该fallback到默认assistant角色 - const roleRegistry = await mockedCommand.loadRoleRegistry() - expect(roleRegistry).toHaveProperty('assistant') - expect(roleRegistry.assistant.source).toBe('fallback') - }) - }) - - describe('元数据提取测试', () => { - test('应该正确提取角色名称', () => { - const content = '# 测试角色\n> 这是一个测试角色的描述' - const name = helloCommand.extractRoleNameFromContent(content) - expect(name).toBe('测试角色') - }) - - test('应该正确提取角色描述', () => { - const content = '# 测试角色\n> 这是一个测试角色的描述' - const description = helloCommand.extractDescriptionFromContent(content) - expect(description).toBe('这是一个测试角色的描述') - }) - - test('应该处理无效内容', () => { - expect(helloCommand.extractRoleNameFromContent('')).toBeNull() - expect(helloCommand.extractDescriptionFromContent(null)).toBeNull() - }) - - test('应该正确提取角色描述(向后兼容)', () => { - const roleInfo = { description: '这是一个测试用的角色' } - const extracted = helloCommand.extractDescription(roleInfo) - expect(extracted).toBe('这是一个测试用的角色') - }) - - test('应该处理缺少元数据的角色文件', () => { - const roleInfo = { name: 'test-role' } - const extracted = helloCommand.extractDescription(roleInfo) - expect(extracted).toBeNull() - }) - }) - - describe('角色注册表加载测试', () => { - test('应该能加载角色注册表', async () => { - const result = await helloCommand.loadRoleRegistry() - - expect(typeof result).toBe('object') - expect(result).toBeDefined() - }) - - test('应该在失败时返回默认assistant角色', async () => { - const mockedCommand = new HelloCommand() - - // Mock ResourceManager to throw an error - mockedCommand.resourceManager.initializeWithNewArchitecture = jest.fn().mockRejectedValue(new Error('Mock error')) - - const result = await mockedCommand.loadRoleRegistry() - - expect(result).toHaveProperty('assistant') - expect(result.assistant.name).toContain('智能助手') - expect(result.assistant.source).toBe('fallback') - }) - }) - - describe('角色信息获取测试', () => { - test('getRoleInfo应该返回正确的角色信息', async () => { - // Mock loadRoleRegistry 方法 - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({ - 'test-role': { - name: '测试角色', - description: '测试描述', - file: '@package://test/path' - } - }) - - const roleInfo = await helloCommand.getRoleInfo('test-role') - - expect(roleInfo).toEqual({ - id: 'test-role', - name: '测试角色', - description: '测试描述', - file: '@package://test/path' - }) - }) - - test('getRoleInfo对不存在的角色应该返回null', async () => { - helloCommand.loadRoleRegistry = jest.fn().mockResolvedValue({}) - - const roleInfo = await helloCommand.getRoleInfo('non-existent') - expect(roleInfo).toBeNull() - }) - }) - - describe('getAllRoles测试', () => { - test('应该返回角色数组格式', async () => { - // 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 roles = await helloCommand.getAllRoles() - - 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' - }) - }) - }) -}) \ No newline at end of file diff --git a/src/tests/commands/MCPStreamableHttpCommand.integration.test.js b/src/tests/commands/MCPStreamableHttpCommand.integration.test.js deleted file mode 100644 index 3b0d449..0000000 --- a/src/tests/commands/MCPStreamableHttpCommand.integration.test.js +++ /dev/null @@ -1,311 +0,0 @@ -const { MCPStreamableHttpCommand } = require('../../lib/commands/MCPStreamableHttpCommand'); -const http = require('http'); - -describe('MCPStreamableHttpCommand Integration Tests', () => { - let command; - let server; - let port; - - beforeEach(() => { - command = new MCPStreamableHttpCommand(); - port = 3001 + Math.floor(Math.random() * 1000); // 随机端口避免冲突 - }); - - afterEach(async () => { - if (server && server.close) { - await new Promise((resolve) => { - server.close(() => { - server = null; - resolve(); - }); - }); - } - // 清理命令实例 - if (command && command.server) { - command.server = null; - } - }); - - describe('Streamable HTTP Server', () => { - it('should start server and respond to health check', async () => { - // 启动服务器 - server = await command.execute({ - transport: 'http', - port, - host: 'localhost' - }); - - // 等待服务器启动 - await new Promise(resolve => setTimeout(resolve, 100)); - - // 发送健康检查请求 - const response = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/health', - method: 'GET' - }); - - expect(response.statusCode).toBe(200); - }, 5000); - - it('should handle MCP initialize request', async () => { - // 启动服务器 - server = await command.execute({ - transport: 'http', - port, - host: 'localhost' - }); - - // 等待服务器启动 - await new Promise(resolve => setTimeout(resolve, 100)); - - // 发送初始化请求 - const initRequest = { - jsonrpc: '2.0', - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { - name: 'test-client', - version: '1.0.0' - } - }, - id: 1 - }; - - const response = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream' - } - }, JSON.stringify(initRequest)); - - expect(response.statusCode).toBe(200); - const responseData = JSON.parse(response.data); - expect(responseData.jsonrpc).toBe('2.0'); - expect(responseData.id).toBe(1); - }, 5000); - - it('should handle tools/list request', async () => { - // 启动服务器 - server = await command.execute({ - transport: 'http', - port, - host: 'localhost' - }); - - // 等待服务器启动 - await new Promise(resolve => setTimeout(resolve, 100)); - - // 先初始化 - const initRequest = { - jsonrpc: '2.0', - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { name: 'test-client', version: '1.0.0' } - }, - id: 1 - }; - - const initResponse = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream' - } - }, JSON.stringify(initRequest)); - - const initResponseData = JSON.parse(initResponse.data); - const sessionId = initResponse.headers['mcp-session-id']; - - if (!sessionId) { - throw new Error('Session ID not found in initialization response headers. Headers: ' + JSON.stringify(initResponse.headers) + ', Body: ' + JSON.stringify(initResponseData)); - } - - // 发送工具列表请求 - const toolsRequest = { - jsonrpc: '2.0', - method: 'tools/list', - params: {}, - id: 2 - }; - - const response = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream', - 'mcp-session-id': sessionId - } - }, JSON.stringify(toolsRequest)); - - expect(response.statusCode).toBe(200); - const responseData = JSON.parse(response.data); - expect(responseData.result.tools).toBeDefined(); - expect(Array.isArray(responseData.result.tools)).toBe(true); - expect(responseData.result.tools.length).toBe(6); - }, 5000); - - it('should handle tool call request', async () => { - // 启动服务器 - server = await command.execute({ - transport: 'http', - port, - host: 'localhost' - }); - - // 等待服务器启动 - await new Promise(resolve => setTimeout(resolve, 100)); - - // 先初始化 - const initRequest = { - jsonrpc: '2.0', - method: 'initialize', - params: { - protocolVersion: '2024-11-05', - capabilities: {}, - clientInfo: { name: 'test-client', version: '1.0.0' } - }, - id: 1 - }; - - const initResponse = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream' - } - }, JSON.stringify(initRequest)); - - const initResponseData = JSON.parse(initResponse.data); - const sessionId = initResponse.headers['mcp-session-id']; - - if (!sessionId) { - throw new Error('Session ID not found in initialization response headers. Headers: ' + JSON.stringify(initResponse.headers)); - } - - // 发送工具调用请求 - const toolCallRequest = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'promptx_hello', - arguments: {} - }, - id: 3 - }; - - const response = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream', - 'mcp-session-id': sessionId - } - }, JSON.stringify(toolCallRequest)); - - expect(response.statusCode).toBe(200); - const responseData = JSON.parse(response.data); - expect(responseData.result).toBeDefined(); - }, 5000); - }); - - describe('Error Handling', () => { - it('should handle invalid JSON requests', async () => { - await command.execute({ transport: 'http', port, host: 'localhost' }); - await new Promise(resolve => setTimeout(resolve, 100)); - - const response = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream' - } - }, 'invalid json'); - - expect(response.statusCode).toBe(400); - }, 5000); - - it('should handle missing session ID for non-initialize requests', async () => { - await command.execute({ transport: 'http', port, host: 'localhost' }); - await new Promise(resolve => setTimeout(resolve, 100)); - - const request = { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'promptx_hello', - arguments: {} - }, - id: 1 - }; - - const response = await makeHttpRequest({ - hostname: 'localhost', - port, - path: '/mcp', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json, text/event-stream' - } - }, JSON.stringify(request)); - - expect(response.statusCode).toBe(400); - }, 5000); - }); -}); - -// Helper function to make HTTP requests -function makeHttpRequest(options, data = null) { - return new Promise((resolve, reject) => { - // 如果有数据,添加Content-Length header - if (data && options.headers) { - options.headers['Content-Length'] = Buffer.byteLength(data); - } - - const req = http.request(options, (res) => { - let responseData = ''; - res.on('data', (chunk) => { - responseData += chunk; - }); - res.on('end', () => { - resolve({ - statusCode: res.statusCode, - headers: res.headers, - data: responseData - }); - }); - }); - - req.on('error', reject); - - if (data) { - req.write(data); - } - req.end(); - }); -} \ No newline at end of file diff --git a/src/tests/commands/MCPStreamableHttpCommand.unit.test.js b/src/tests/commands/MCPStreamableHttpCommand.unit.test.js deleted file mode 100644 index 6b532eb..0000000 --- a/src/tests/commands/MCPStreamableHttpCommand.unit.test.js +++ /dev/null @@ -1,178 +0,0 @@ -const { MCPStreamableHttpCommand } = require('../../lib/commands/MCPStreamableHttpCommand'); - -describe('MCPStreamableHttpCommand', () => { - let command; - - beforeEach(() => { - command = new MCPStreamableHttpCommand(); - }); - - describe('constructor', () => { - it('should initialize with correct name and version', () => { - expect(command.name).toBe('promptx-mcp-streamable-http-server'); - expect(command.version).toBe('1.0.0'); - }); - - it('should have default configuration', () => { - expect(command.transport).toBe('http'); - expect(command.port).toBe(3000); - expect(command.host).toBe('localhost'); - }); - }); - - describe('execute', () => { - it('should throw error when transport type is unsupported', async () => { - await expect(command.execute({ transport: 'unsupported' })) - .rejects - .toThrow('Unsupported transport: unsupported'); - }); - - it('should start Streamable HTTP server with default options', async () => { - const mockStartStreamableHttpServer = jest.fn().mockResolvedValue(); - command.startStreamableHttpServer = mockStartStreamableHttpServer; - - await command.execute(); - - expect(mockStartStreamableHttpServer).toHaveBeenCalledWith(3000, 'localhost'); - }); - - it('should start Streamable HTTP server with custom options', async () => { - const mockStartStreamableHttpServer = jest.fn().mockResolvedValue(); - command.startStreamableHttpServer = mockStartStreamableHttpServer; - - await command.execute({ transport: 'http', port: 4000, host: '0.0.0.0' }); - - expect(mockStartStreamableHttpServer).toHaveBeenCalledWith(4000, '0.0.0.0'); - }); - - it('should start SSE server when transport is sse', async () => { - const mockStartSSEServer = jest.fn().mockResolvedValue(); - command.startSSEServer = mockStartSSEServer; - - await command.execute({ transport: 'sse', port: 3001 }); - - expect(mockStartSSEServer).toHaveBeenCalledWith(3001, 'localhost'); - }); - }); - - describe('startStreamableHttpServer', () => { - it('should create Express app and listen on specified port', async () => { - // Mock Express - const mockApp = { - use: jest.fn(), - post: jest.fn(), - get: jest.fn(), - delete: jest.fn(), - listen: jest.fn((port, callback) => callback()) - }; - const mockExpress = jest.fn(() => mockApp); - mockExpress.json = jest.fn(); - - // Mock the method to avoid actual server startup - const originalMethod = command.startStreamableHttpServer; - command.startStreamableHttpServer = jest.fn().mockImplementation(async (port, host) => { - expect(port).toBe(3000); - expect(host).toBe('localhost'); - return Promise.resolve(); - }); - - await command.startStreamableHttpServer(3000, 'localhost'); - - expect(command.startStreamableHttpServer).toHaveBeenCalledWith(3000, 'localhost'); - }); - }); - - describe('startSSEServer', () => { - it('should create Express app with dual endpoints', async () => { - // Mock the method to avoid actual server startup - command.startSSEServer = jest.fn().mockImplementation(async (port, host) => { - expect(port).toBe(3000); - expect(host).toBe('localhost'); - return Promise.resolve(); - }); - - await command.startSSEServer(3000, 'localhost'); - - expect(command.startSSEServer).toHaveBeenCalledWith(3000, 'localhost'); - }); - }); - - describe('setupMCPServer', () => { - it('should create MCP server with correct configuration', () => { - const server = command.setupMCPServer(); - - expect(server).toBeDefined(); - // We'll verify the server has the correct tools in integration tests - }); - }); - - describe('getToolDefinitions', () => { - it('should return all PromptX tools', () => { - const tools = command.getToolDefinitions(); - - expect(Array.isArray(tools)).toBe(true); - expect(tools.length).toBe(6); // All PromptX tools - - const toolNames = tools.map(tool => tool.name); - expect(toolNames).toContain('promptx_init'); - expect(toolNames).toContain('promptx_hello'); - expect(toolNames).toContain('promptx_action'); - expect(toolNames).toContain('promptx_learn'); - expect(toolNames).toContain('promptx_recall'); - expect(toolNames).toContain('promptx_remember'); - }); - }); - - describe('handleMCPRequest', () => { - it('should handle tool calls correctly', async () => { - const mockReq = { - body: { - jsonrpc: '2.0', - method: 'tools/call', - params: { - name: 'promptx_hello', - arguments: {} - }, - id: 1 - }, - headers: {} - }; - - const mockRes = { - json: jest.fn(), - status: jest.fn().mockReturnThis(), - headersSent: false - }; - - // Mock CLI execution - const mockCli = { - execute: jest.fn().mockResolvedValue('Hello response') - }; - - command.cli = mockCli; - command.handleMCPRequest = jest.fn().mockImplementation(async (req, res) => { - expect(req.body.method).toBe('tools/call'); - res.json({ result: 'success' }); - }); - - await command.handleMCPRequest(mockReq, mockRes); - - expect(command.handleMCPRequest).toHaveBeenCalledWith(mockReq, mockRes); - }); - }); - - describe('configuration validation', () => { - it('should validate port number', () => { - expect(() => command.validatePort(3000)).not.toThrow(); - expect(() => command.validatePort('invalid')).toThrow('Port must be a number'); - expect(() => command.validatePort(70000)).toThrow('Port must be between 1 and 65535'); - }); - - it('should validate host address', () => { - expect(() => command.validateHost('localhost')).not.toThrow(); - expect(() => command.validateHost('0.0.0.0')).not.toThrow(); - expect(() => command.validateHost('192.168.1.1')).not.toThrow(); - expect(() => command.validateHost('')).toThrow('Host cannot be empty'); - }); - }); -}); \ No newline at end of file diff --git a/src/tests/commands/mcp-server.unit.test.js b/src/tests/commands/mcp-server.unit.test.js deleted file mode 100644 index 9f76404..0000000 --- a/src/tests/commands/mcp-server.unit.test.js +++ /dev/null @@ -1,308 +0,0 @@ -const { exec } = require('child_process'); -const { promisify } = require('util'); -const fs = require('fs'); -const path = require('path'); - -const execAsync = promisify(exec); - -// 测试辅助函数 -function normalizeOutput(output) { - return output - .replace(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g, 'TIMESTAMP') - .replace(/\[\d+ms\]/g, '[TIME]') - .replace(/PS [^>]+>/g, '') - .trim(); -} - -describe('MCP Server 项目结构验证', () => { - test('现有CLI入口文件存在', () => { - expect(fs.existsSync('src/bin/promptx.js')).toBe(true); - }); - - test('commands目录已创建', () => { - expect(fs.existsSync('src/lib/commands')).toBe(true); - }); - - test('MCP SDK依赖已安装', () => { - const pkg = require('../../../package.json'); - expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined(); - }); -}); - -describe('CLI函数调用基线测试', () => { - let cli; - - beforeEach(() => { - // 重新导入以确保清洁状态 - delete require.cache[require.resolve('../../lib/core/pouch')]; - cli = require('../../lib/core/pouch').cli; - }); - - test('cli.execute函数可用性', () => { - expect(typeof cli.execute).toBe('function'); - }); - - test('init命令函数调用', async () => { - const result = await cli.execute('init', []); - expect(result).toBeDefined(); - expect(result.toString()).toContain('🎯'); - }, 10000); - - test('hello命令函数调用', async () => { - const result = await cli.execute('hello', []); - expect(result).toBeDefined(); - expect(result.toString()).toContain('🎯'); - }, 10000); - - test('action命令函数调用', async () => { - const result = await cli.execute('action', ['assistant']); - expect(result).toBeDefined(); - expect(result.toString()).toContain('🎭'); - }, 10000); -}); - -describe('MCP适配器单元测试', () => { - let mcpServer; - - beforeEach(() => { - try { - const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand'); - mcpServer = new MCPServerCommand(); - } catch (error) { - mcpServer = null; - } - }); - - describe('基础结构测试', () => { - test('MCPServerCommand类应该能导入', () => { - expect(() => { - require('../../lib/commands/MCPServerCommand'); - }).not.toThrow(); - }); - - test('MCPServerCommand应该有必要方法', () => { - if (!mcpServer) { - expect(true).toBe(true); // 跳过测试如果类还没实现 - return; - } - - expect(typeof mcpServer.execute).toBe('function'); - expect(typeof mcpServer.getToolDefinitions).toBe('function'); - expect(typeof mcpServer.convertMCPToCliParams).toBe('function'); - expect(typeof mcpServer.callTool).toBe('function'); - expect(typeof mcpServer.log).toBe('function'); - }); - - test('调试模式应该可配置', () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - expect(typeof mcpServer.debug).toBe('boolean'); - expect(typeof mcpServer.log).toBe('function'); - }); - }); - - describe('参数转换测试', () => { - test('promptx_init参数转换', () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = mcpServer.convertMCPToCliParams('promptx_init', {}); - expect(result).toEqual([]); - }); - - test('promptx_action参数转换', () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = mcpServer.convertMCPToCliParams('promptx_action', { - role: 'product-manager' - }); - expect(result).toEqual(['product-manager']); - }); - - test('promptx_learn参数转换', () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = mcpServer.convertMCPToCliParams('promptx_learn', { - resource: 'thought://creativity' - }); - expect(result).toEqual(['thought://creativity']); - }); - - test('promptx_remember参数转换', () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = mcpServer.convertMCPToCliParams('promptx_remember', { - content: '测试内容', - tags: '测试 标签' - }); - expect(result).toEqual(['测试内容', '--tags', '测试 标签']); - }); - }); - - describe('工具调用测试', () => { - test('init工具调用', async () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = await mcpServer.callTool('promptx_init', {}); - expect(result.content).toBeDefined(); - expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain('初始化'); - }, 15000); - - test('hello工具调用', async () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = await mcpServer.callTool('promptx_hello', {}); - expect(result.content).toBeDefined(); - expect(result.content[0].text).toContain('角色'); - }, 15000); - - test('action工具调用', async () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = await mcpServer.callTool('promptx_action', { - role: 'assistant' - }); - expect(result.content).toBeDefined(); - expect(result.content[0].text).toContain('激活'); - }, 15000); - }); - - describe('错误处理测试', () => { - test('无效工具名处理', async () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = await mcpServer.callTool('invalid_tool', {}); - expect(result.content[0].text).toContain('❌'); - expect(result.isError).toBe(true); - }); - - test('缺少必需参数处理', async () => { - if (!mcpServer) { - expect(true).toBe(true); - return; - } - - const result = await mcpServer.callTool('promptx_action', {}); - expect(result.content[0].text).toContain('❌'); - }); - }); -}); - -describe('MCP vs CLI 一致性测试', () => { - let mcpServer; - let cli; - - beforeEach(() => { - try { - const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand'); - mcpServer = new MCPServerCommand(); - cli = require('../../lib/core/pouch').cli; - } catch (error) { - mcpServer = null; - cli = null; - } - }); - - test('init: MCP vs CLI 输出一致性', async () => { - if (!mcpServer || !cli) { - expect(true).toBe(true); - return; - } - - // 通过MCP调用 - const mcpResult = await mcpServer.callTool('promptx_init', {}); - const mcpOutput = normalizeOutput(mcpResult.content[0].text); - - // 直接CLI函数调用 - const cliResult = await cli.execute('init', []); - const cliOutput = normalizeOutput(cliResult.toString()); - - // 验证输出一致性 - expect(mcpOutput).toBe(cliOutput); - }, 15000); - - test('action: MCP vs CLI 输出一致性', async () => { - if (!mcpServer || !cli) { - expect(true).toBe(true); - return; - } - - const role = 'assistant'; - - const mcpResult = await mcpServer.callTool('promptx_action', { role }); - const mcpOutput = normalizeOutput(mcpResult.content[0].text); - - const cliResult = await cli.execute('action', [role]); - const cliOutput = normalizeOutput(cliResult.toString()); - - expect(mcpOutput).toBe(cliOutput); - }, 15000); -}); - -describe('MCP协议通信测试', () => { - test('工具定义获取', () => { - let mcpServer; - try { - const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand'); - mcpServer = new MCPServerCommand(); - } catch (error) { - expect(true).toBe(true); // 跳过如果还没实现 - return; - } - - const tools = mcpServer.getToolDefinitions(); - expect(tools).toHaveLength(6); - - const toolNames = tools.map(t => t.name); - expect(toolNames).toContain('promptx_init'); - expect(toolNames).toContain('promptx_hello'); - expect(toolNames).toContain('promptx_action'); - expect(toolNames).toContain('promptx_learn'); - expect(toolNames).toContain('promptx_recall'); - expect(toolNames).toContain('promptx_remember'); - }); - - test('工具Schema验证', () => { - let mcpServer; - try { - const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand'); - mcpServer = new MCPServerCommand(); - } catch (error) { - expect(true).toBe(true); - return; - } - - const tools = mcpServer.getToolDefinitions(); - const actionTool = tools.find(t => t.name === 'promptx_action'); - - expect(actionTool.inputSchema.properties.role).toBeDefined(); - expect(actionTool.inputSchema.required).toContain('role'); - }); -}); \ No newline at end of file diff --git a/src/tests/commands/promptxCli.e2e.test.js b/src/tests/commands/promptxCli.e2e.test.js deleted file mode 100644 index af4f953..0000000 --- a/src/tests/commands/promptxCli.e2e.test.js +++ /dev/null @@ -1,63 +0,0 @@ -const { execSync } = require('child_process') -const path = require('path') -const fs = require('fs-extra') -const os = require('os') - -describe('PromptX CLI - E2E Tests', () => { - let tempDir - - beforeAll(async () => { - // 创建临时目录用于测试 - tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptx-e2e-')) - }) - - afterAll(async () => { - if (tempDir) { - await fs.remove(tempDir) - } - }) - - /** - * 运行PromptX CLI命令 - */ - function runCommand (args, options = {}) { - const cwd = options.cwd || process.cwd() - const env = { ...process.env, ...options.env } - - try { - const result = execSync(`node src/bin/promptx.js ${args.join(' ')}`, { - cwd, - env, - encoding: 'utf8', - timeout: 10000 - }) - return { success: true, output: result, error: null } - } catch (error) { - return { success: false, output: error.stdout || '', error: error.message } - } - } - - describe('基础CLI功能', () => { - test('hello命令应该能正常运行', () => { - const result = runCommand(['hello']) - - expect(result.success).toBe(true) - expect(result.output).toContain('AI专业角色服务清单') - expect(result.output).toContain('assistant') - }) - - test('init命令应该能正常运行', () => { - const result = runCommand(['init']) - - expect(result.success).toBe(true) - expect(result.output).toContain('初始化') - }) - - test('help命令应该显示帮助信息', () => { - const result = runCommand(['--help']) - - expect(result.success).toBe(true) - expect(result.output).toContain('Usage') - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/DPMLContentParser.integration.test.js b/src/tests/core/resource/DPMLContentParser.integration.test.js deleted file mode 100644 index a47d044..0000000 --- a/src/tests/core/resource/DPMLContentParser.integration.test.js +++ /dev/null @@ -1,106 +0,0 @@ -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)}%`) - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/DPMLContentParser.position.unit.test.js b/src/tests/core/resource/DPMLContentParser.position.unit.test.js deleted file mode 100644 index 56d1eba..0000000 --- a/src/tests/core/resource/DPMLContentParser.position.unit.test.js +++ /dev/null @@ -1,174 +0,0 @@ -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) - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/DPMLContentParser.unit.test.js b/src/tests/core/resource/DPMLContentParser.unit.test.js deleted file mode 100644 index cb6bd19..0000000 --- a/src/tests/core/resource/DPMLContentParser.unit.test.js +++ /dev/null @@ -1,236 +0,0 @@ -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 = ` - - @!thought://remember - # 个性特征 - - - @!execution://assistant - # 行为原则 - -` - - 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 = ` - - @!thought://remember - # 杠精思维特征 - - - @!execution://assistant - # 抬杠行为原则 - - - # 逻辑谬误知识体系 - -` - - 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内容') - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/EnhancedResourceRegistry.unit.test.js b/src/tests/core/resource/EnhancedResourceRegistry.unit.test.js deleted file mode 100644 index e8b7c83..0000000 --- a/src/tests/core/resource/EnhancedResourceRegistry.unit.test.js +++ /dev/null @@ -1,420 +0,0 @@ -const EnhancedResourceRegistry = require('../../../lib/core/resource/EnhancedResourceRegistry') - -describe('EnhancedResourceRegistry', () => { - let registry - - beforeEach(() => { - registry = new EnhancedResourceRegistry() - }) - - describe('constructor', () => { - test('should initialize with empty registry', () => { - expect(registry.size()).toBe(0) - expect(registry.list()).toEqual([]) - }) - }) - - describe('register', () => { - test('should register resource with metadata', () => { - const resource = { - id: 'role:test', - reference: '@package://test.md', - metadata: { - source: 'PACKAGE', - priority: 1, - timestamp: new Date() - } - } - - registry.register(resource) - - expect(registry.has('role:test')).toBe(true) - expect(registry.resolve('role:test')).toBe('@package://test.md') - }) - - test('should throw error for invalid resource', () => { - expect(() => { - registry.register({ id: 'test' }) // missing reference - }).toThrow('Resource must have id and reference') - }) - - test('should throw error for missing metadata', () => { - expect(() => { - registry.register({ - id: 'role:test', - reference: '@package://test.md' - // missing metadata - }) - }).toThrow('Resource must have metadata with source and priority') - }) - }) - - describe('registerBatch', () => { - test('should register multiple resources at once', () => { - const resources = [ - { - id: 'role:test1', - reference: '@package://test1.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'role:test2', - reference: '@project://test2.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() } - } - ] - - registry.registerBatch(resources) - - expect(registry.size()).toBe(2) - expect(registry.has('role:test1')).toBe(true) - expect(registry.has('role:test2')).toBe(true) - }) - - test('should handle batch registration failures gracefully', () => { - const resources = [ - { - id: 'role:valid', - reference: '@package://valid.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'role:invalid' - // missing reference and metadata - } - ] - - // Should register valid resources and skip invalid ones - registry.registerBatch(resources) - - expect(registry.size()).toBe(1) - expect(registry.has('role:valid')).toBe(true) - expect(registry.has('role:invalid')).toBe(false) - }) - }) - - describe('merge', () => { - test('should merge with another registry using priority rules', () => { - // Setup first registry with package resources - const resource1 = { - id: 'role:test', - reference: '@package://test.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date('2023-01-01') } - } - registry.register(resource1) - - // Create second registry with project resources (higher priority) - const otherRegistry = new EnhancedResourceRegistry() - const resource2 = { - id: 'role:test', // same ID, should override - reference: '@project://test.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date('2023-01-02') } - } - otherRegistry.register(resource2) - - // Merge - PROJECT should override PACKAGE due to higher priority - registry.merge(otherRegistry) - - expect(registry.resolve('role:test')).toBe('@project://test.md') - const metadata = registry.getMetadata('role:test') - expect(metadata.source).toBe('PROJECT') - }) - - test('should handle same priority by timestamp (newer wins)', () => { - const older = new Date('2023-01-01') - const newer = new Date('2023-01-02') - - // Setup first registry - const resource1 = { - id: 'role:test', - reference: '@package://old.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: older } - } - registry.register(resource1) - - // Create second registry with same priority but newer timestamp - const otherRegistry = new EnhancedResourceRegistry() - const resource2 = { - id: 'role:test', - reference: '@package://new.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: newer } - } - otherRegistry.register(resource2) - - registry.merge(otherRegistry) - - expect(registry.resolve('role:test')).toBe('@package://new.md') - }) - - test('should handle discovery source priority correctly', () => { - // Test priority order: USER > PROJECT > PACKAGE > INTERNET - const resources = [ - { - id: 'role:test', - reference: '@internet://remote.md', - metadata: { source: 'INTERNET', priority: 4, timestamp: new Date() } - }, - { - id: 'role:test', - reference: '@package://builtin.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'role:test', - reference: '@project://project.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() } - }, - { - id: 'role:test', - reference: '@user://custom.md', - metadata: { source: 'USER', priority: 3, timestamp: new Date() } - } - ] - - // Register in random order - registry.register(resources[0]) // INTERNET - registry.register(resources[1]) // PACKAGE (should override INTERNET) - registry.register(resources[2]) // PROJECT (should override PACKAGE) - registry.register(resources[3]) // USER (should override PROJECT) - - expect(registry.resolve('role:test')).toBe('@user://custom.md') - }) - }) - - describe('resolve', () => { - test('should resolve resource by exact ID', () => { - const resource = { - id: 'role:test', - reference: '@package://test.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - } - registry.register(resource) - - expect(registry.resolve('role:test')).toBe('@package://test.md') - }) - - test('should support backwards compatibility lookup', () => { - const resource = { - id: 'role:java-developer', - reference: '@package://java.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - } - registry.register(resource) - - // Should find by bare name if no exact match - expect(registry.resolve('java-developer')).toBe('@package://java.md') - }) - - test('should throw error if resource not found', () => { - expect(() => { - registry.resolve('non-existent') - }).toThrow('Resource \'non-existent\' not found') - }) - }) - - describe('list', () => { - test('should list all resources', () => { - const resources = [ - { - id: 'role:test1', - reference: '@package://test1.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'execution:test2', - reference: '@project://test2.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() } - } - ] - - registry.registerBatch(resources) - - const list = registry.list() - expect(list).toHaveLength(2) - expect(list).toContain('role:test1') - expect(list).toContain('execution:test2') - }) - - test('should filter by protocol', () => { - const resources = [ - { - id: 'role:test1', - reference: '@package://test1.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'execution:test2', - reference: '@project://test2.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() } - }, - { - id: 'role:test3', - reference: '@user://test3.md', - metadata: { source: 'USER', priority: 3, timestamp: new Date() } - } - ] - - registry.registerBatch(resources) - - const roleList = registry.list('role') - expect(roleList).toHaveLength(2) - expect(roleList).toContain('role:test1') - expect(roleList).toContain('role:test3') - - const executionList = registry.list('execution') - expect(executionList).toHaveLength(1) - expect(executionList).toContain('execution:test2') - }) - }) - - describe('getMetadata', () => { - test('should return resource metadata', () => { - const timestamp = new Date() - const resource = { - id: 'role:test', - reference: '@package://test.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: timestamp } - } - registry.register(resource) - - const metadata = registry.getMetadata('role:test') - expect(metadata).toEqual({ - source: 'PACKAGE', - priority: 1, - timestamp: timestamp - }) - }) - - test('should return null for non-existent resource', () => { - const metadata = registry.getMetadata('non-existent') - expect(metadata).toBeNull() - }) - }) - - describe('clear', () => { - test('should clear all resources', () => { - const resource = { - id: 'role:test', - reference: '@package://test.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - } - registry.register(resource) - - expect(registry.size()).toBe(1) - - registry.clear() - - expect(registry.size()).toBe(0) - expect(registry.list()).toEqual([]) - }) - }) - - describe('size', () => { - test('should return number of registered resources', () => { - expect(registry.size()).toBe(0) - - const resources = [ - { - id: 'role:test1', - reference: '@package://test1.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'role:test2', - reference: '@project://test2.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() } - } - ] - - registry.registerBatch(resources) - - expect(registry.size()).toBe(2) - }) - }) - - describe('has', () => { - test('should check if resource exists', () => { - const resource = { - id: 'role:test', - reference: '@package://test.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - } - registry.register(resource) - - expect(registry.has('role:test')).toBe(true) - expect(registry.has('non-existent')).toBe(false) - }) - }) - - describe('remove', () => { - test('should remove resource', () => { - const resource = { - id: 'role:test', - reference: '@package://test.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - } - registry.register(resource) - - expect(registry.has('role:test')).toBe(true) - - registry.remove('role:test') - - expect(registry.has('role:test')).toBe(false) - expect(registry.size()).toBe(0) - }) - - test('should do nothing if resource does not exist', () => { - registry.remove('non-existent') // Should not throw - expect(registry.size()).toBe(0) - }) - }) - - describe('loadFromDiscoveryResults', () => { - test('should load resources from discovery manager results', () => { - const discoveryResults = [ - { - id: 'role:test1', - reference: '@package://test1.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'role:test2', - reference: '@project://test2.md', - metadata: { source: 'PROJECT', priority: 2, timestamp: new Date() } - } - ] - - registry.loadFromDiscoveryResults(discoveryResults) - - expect(registry.size()).toBe(2) - expect(registry.resolve('role:test1')).toBe('@package://test1.md') - expect(registry.resolve('role:test2')).toBe('@project://test2.md') - }) - - test('should handle empty discovery results', () => { - registry.loadFromDiscoveryResults([]) - expect(registry.size()).toBe(0) - }) - - test('should handle invalid discovery results gracefully', () => { - const discoveryResults = [ - { - id: 'role:valid', - reference: '@package://valid.md', - metadata: { source: 'PACKAGE', priority: 1, timestamp: new Date() } - }, - { - id: 'role:invalid' - // missing reference and metadata - }, - null, - undefined - ] - - registry.loadFromDiscoveryResults(discoveryResults) - - expect(registry.size()).toBe(1) - expect(registry.has('role:valid')).toBe(true) - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/ProtocolResolver.unit.test.js b/src/tests/core/resource/ProtocolResolver.unit.test.js deleted file mode 100644 index e29389b..0000000 --- a/src/tests/core/resource/ProtocolResolver.unit.test.js +++ /dev/null @@ -1,192 +0,0 @@ -const path = require('path') -const fs = require('fs') -const ProtocolResolver = require('../../../lib/core/resource/ProtocolResolver') - -describe('ProtocolResolver', () => { - let resolver - - beforeEach(() => { - resolver = new ProtocolResolver() - }) - - describe('parseReference', () => { - test('should parse valid @package:// reference', () => { - const result = resolver.parseReference('@package://prompt/core/role.md') - - expect(result.protocol).toBe('package') - expect(result.resourcePath).toBe('prompt/core/role.md') - expect(result.loadingSemantic).toBe('') - expect(result.fullReference).toBe('@package://prompt/core/role.md') - }) - - test('should parse valid @project:// reference', () => { - const result = resolver.parseReference('@project://.promptx/custom.role.md') - - expect(result.protocol).toBe('project') - expect(result.resourcePath).toBe('.promptx/custom.role.md') - expect(result.loadingSemantic).toBe('') - }) - - test('should parse valid @file:// reference', () => { - const result = resolver.parseReference('@file:///absolute/path/to/file.md') - - expect(result.protocol).toBe('file') - expect(result.resourcePath).toBe('/absolute/path/to/file.md') - expect(result.loadingSemantic).toBe('') - }) - - test('should parse @! hot loading semantic', () => { - const result = resolver.parseReference('@!package://prompt/core/role.md') - - expect(result.protocol).toBe('package') - expect(result.resourcePath).toBe('prompt/core/role.md') - expect(result.loadingSemantic).toBe('!') - expect(result.fullReference).toBe('@!package://prompt/core/role.md') - }) - - test('should parse @? lazy loading semantic', () => { - const result = resolver.parseReference('@?file://large-dataset.csv') - - expect(result.protocol).toBe('file') - expect(result.resourcePath).toBe('large-dataset.csv') - expect(result.loadingSemantic).toBe('?') - expect(result.fullReference).toBe('@?file://large-dataset.csv') - }) - - test('should throw error for invalid reference format', () => { - expect(() => { - resolver.parseReference('invalid-reference') - }).toThrow('Invalid reference format: invalid-reference') - }) - - test('should throw error for missing protocol', () => { - expect(() => { - resolver.parseReference('://no-protocol') - }).toThrow('Invalid reference format: ://no-protocol') - }) - - test('should throw error for invalid loading semantic', () => { - expect(() => { - resolver.parseReference('@#package://invalid-semantic') - }).toThrow('Invalid reference format: @#package://invalid-semantic') - }) - }) - - describe('resolve', () => { - test('should resolve @package:// reference to absolute path', async () => { - // Mock the package root finding - jest.spyOn(resolver, 'findPackageRoot').mockResolvedValue('/mock/package/root') - - const result = await resolver.resolve('@package://prompt/core/role.md') - - expect(result).toBe(path.resolve('/mock/package/root', 'prompt/core/role.md')) - }) - - test('should resolve @project:// reference to project relative path', async () => { - const result = await resolver.resolve('@project://.promptx/custom.role.md') - - expect(result).toBe(path.resolve(process.cwd(), '.promptx/custom.role.md')) - }) - - test('should resolve @file:// reference with absolute path', async () => { - const result = await resolver.resolve('@file:///absolute/path/to/file.md') - - expect(result).toBe('/absolute/path/to/file.md') - }) - - test('should resolve @file:// reference with relative path', async () => { - const result = await resolver.resolve('@file://relative/path/to/file.md') - - expect(result).toBe(path.resolve(process.cwd(), 'relative/path/to/file.md')) - }) - - test('should throw error for unsupported protocol', async () => { - await expect(resolver.resolve('@unsupported://some/path')).rejects.toThrow('Unsupported protocol: unsupported') - }) - }) - - describe('findPackageRoot', () => { - test('should find package root with promptx package.json', async () => { - // Mock file system operations - const originalExistsSync = fs.existsSync - const originalReadFileSync = fs.readFileSync - - fs.existsSync = jest.fn() - fs.readFileSync = jest.fn() - - // Mock directory structure - const mockDirname = '/some/deep/nested/path' - resolver.__dirname = mockDirname - - // Mock package.json exists in parent directory - fs.existsSync - .mockReturnValueOnce(false) // /some/deep/nested/path/package.json - .mockReturnValueOnce(false) // /some/deep/nested/package.json - .mockReturnValueOnce(false) // /some/deep/package.json - .mockReturnValueOnce(true) // /some/package.json - - fs.readFileSync.mockReturnValue(JSON.stringify({ name: 'promptx' })) - - // Mock path operations - jest.spyOn(path, 'dirname') - .mockReturnValueOnce('/some/deep/nested') - .mockReturnValueOnce('/some/deep') - .mockReturnValueOnce('/some') - - const result = await resolver.findPackageRoot() - - expect(result).toBe('/some') - - // Restore - fs.existsSync = originalExistsSync - fs.readFileSync = originalReadFileSync - }) - - test('should throw error when package root not found', async () => { - // Mock file system operations - const originalExistsSync = fs.existsSync - fs.existsSync = jest.fn().mockReturnValue(false) - - // Mock reaching root directory - jest.spyOn(path, 'parse').mockReturnValue({ root: '/' }) - - await expect(resolver.findPackageRoot()).rejects.toThrow('PromptX package root not found') - - // Restore - fs.existsSync = originalExistsSync - }) - }) - - describe('caching behavior', () => { - test('should cache package root after first lookup', async () => { - const mockRoot = '/mock/package/root' - jest.spyOn(resolver, 'findPackageRoot').mockResolvedValue(mockRoot) - - // First call - await resolver.resolve('@package://prompt/core/role.md') - expect(resolver.findPackageRoot).toHaveBeenCalledTimes(1) - - // Second call should use cached value - await resolver.resolve('@package://prompt/domain/java.role.md') - expect(resolver.findPackageRoot).toHaveBeenCalledTimes(1) // Still only called once - }) - }) - - describe('cross-platform compatibility', () => { - test('should handle Windows-style paths correctly', async () => { - jest.spyOn(resolver, 'findPackageRoot').mockResolvedValue('C:\\mock\\package\\root') - - const result = await resolver.resolve('@package://prompt\\core\\role.md') - - expect(result).toBe(path.resolve('C:\\mock\\package\\root', 'prompt\\core\\role.md')) - }) - - test('should handle Unix-style paths correctly', async () => { - jest.spyOn(resolver, 'findPackageRoot').mockResolvedValue('/mock/package/root') - - const result = await resolver.resolve('@package://prompt/core/role.md') - - expect(result).toBe(path.resolve('/mock/package/root', 'prompt/core/role.md')) - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/ResourceManager.unit.test.js b/src/tests/core/resource/ResourceManager.unit.test.js deleted file mode 100644 index 35cbc4c..0000000 --- a/src/tests/core/resource/ResourceManager.unit.test.js +++ /dev/null @@ -1,288 +0,0 @@ -const ResourceManager = require('../../../lib/core/resource/resourceManager') -const ResourceRegistry = require('../../../lib/core/resource/resourceRegistry') -const ProtocolResolver = require('../../../lib/core/resource/ProtocolResolver') - -// Mock所有依赖项 -jest.mock('../../../lib/core/resource/resourceRegistry') -jest.mock('../../../lib/core/resource/ProtocolResolver') -jest.mock('../../../lib/core/resource/discovery/DiscoveryManager') - -describe('ResourceManager - New Architecture Unit Tests', () => { - let manager - let mockRegistry - let mockProtocolParser - - beforeEach(() => { - // 清除所有模拟 - jest.clearAllMocks() - - // 创建模拟对象 - mockRegistry = { - get: jest.fn(), - has: jest.fn(), - size: 0, - register: jest.fn(), - clear: jest.fn(), - keys: jest.fn(), - entries: jest.fn(), - printAll: jest.fn(), - groupByProtocol: jest.fn(), - getStats: jest.fn(), - search: jest.fn(), - toJSON: jest.fn() - } - - mockProtocolParser = { - parse: jest.fn(), - loadResource: jest.fn() - } - - // 设置模拟构造函数 - ResourceRegistry.mockImplementation(() => mockRegistry) - ProtocolResolver.mockImplementation(() => mockProtocolParser) - - // 创建管理器实例 - manager = new ResourceManager() - }) - - describe('初始化和构造', () => { - test('应该创建ResourceManager实例', () => { - expect(manager).toBeInstanceOf(ResourceManager) - expect(manager.registry).toBeDefined() - expect(manager.protocolParser).toBeDefined() - }) - - test('应该注册所有协议处理器', () => { - expect(manager.protocols.size).toBe(6) // 6个协议 (包括knowledge) - expect(manager.protocols.has('package')).toBe(true) - expect(manager.protocols.has('project')).toBe(true) - expect(manager.protocols.has('role')).toBe(true) - expect(manager.protocols.has('execution')).toBe(true) - expect(manager.protocols.has('thought')).toBe(true) - expect(manager.protocols.has('knowledge')).toBe(true) - }) - - test('应该初始化发现管理器', () => { - expect(manager.discoveryManager).toBeDefined() - }) - }) - - describe('资源加载 - loadResource方法', () => { - test('应该处理DPML格式资源引用', async () => { - const resourceId = '@!role://java-developer' - const mockReference = { - id: 'role:java-developer', - path: '/path/to/role', - protocol: 'role' - } - const mockContent = 'Role content...' - - // Set registry size to non-zero to avoid auto-initialization - manager.registry.register('dummy', {id: 'dummy'}) - - // Replace the real protocolParser with mock - manager.protocolParser = mockProtocolParser - manager.registry = mockRegistry - - mockProtocolParser.parse.mockReturnValue({ protocol: 'role', path: 'java-developer' }) - mockRegistry.get.mockReturnValue(mockReference) - - // Mock loadResourceByProtocol instead of protocolParser.loadResource - manager.loadResourceByProtocol = jest.fn().mockResolvedValue(mockContent) - - const result = await manager.loadResource(resourceId) - - expect(mockProtocolParser.parse).toHaveBeenCalledWith(resourceId) - expect(mockRegistry.get).toHaveBeenCalledWith('role:java-developer') - expect(manager.loadResourceByProtocol).toHaveBeenCalledWith(mockReference) - expect(result).toEqual({ - success: true, - content: mockContent, - resourceId, - reference: mockReference - }) - }) - - test('应该处理传统格式资源ID', async () => { - const resourceId = '@package://java-developer.role.md' - const mockReference = { id: resourceId, protocol: 'package', path: 'java-developer.role.md' } - const mockContent = 'Package content...' - - // Replace the real registry with mock - manager.registry = mockRegistry - // Set registry size to non-zero to avoid auto-initialization - mockRegistry.size = 1 - - mockRegistry.get.mockReturnValue(mockReference) - - // Mock loadResourceByProtocol instead of protocolParser.loadResource - manager.loadResourceByProtocol = jest.fn().mockResolvedValue(mockContent) - - const result = await manager.loadResource(resourceId) - - expect(mockRegistry.get).toHaveBeenCalledWith(resourceId) - expect(manager.loadResourceByProtocol).toHaveBeenCalledWith(mockReference) - expect(result).toEqual({ - success: true, - content: mockContent, - resourceId, - reference: mockReference - }) - }) - - // FIXME: 这个测试用例太耗时,暂时注释掉 - // 原因:触发了真正的资源发现过程,涉及大量文件系统操作 - test.skip('应该在注册表为空时自动初始化', async () => { - const resourceId = 'role:test-role' - - // Ensure registry is empty to trigger initialization - manager.registry = new (require('../../../lib/core/resource/resourceRegistry.js'))() - - // 模拟空注册表 - mockRegistry.get.mockReturnValue(null) - mockRegistry.size = 0 - - // 模拟初始化成功 - const mockDiscoveryManager = { - discoverRegistries: jest.fn().mockResolvedValue() - } - manager.discoveryManager = mockDiscoveryManager - - const result = await manager.loadResource(resourceId) - - expect(mockDiscoveryManager.discoverRegistries).toHaveBeenCalled() - expect(result.success).toBe(false) // 因为资源仍然没找到 - }) - }) - - describe('向后兼容 - resolve方法', () => { - test('应该处理@package://格式引用', async () => { - const resourceUrl = '@package://test/file.md' - const mockContent = 'Package content...' - - // Set registry size to non-zero to avoid auto-initialization - manager.registry.register('dummy', {id: 'dummy'}) - - // Spy on the loadResourceByProtocol method which is what resolve() calls for @package:// URLs - const loadResourceByProtocolSpy = jest.spyOn(manager, 'loadResourceByProtocol').mockResolvedValue(mockContent) - - const result = await manager.resolve(resourceUrl) - - expect(loadResourceByProtocolSpy).toHaveBeenCalledWith(resourceUrl) - expect(result).toEqual({ - success: true, - content: mockContent, - path: resourceUrl, - reference: resourceUrl - }) - - loadResourceByProtocolSpy.mockRestore() - }) - - test('应该处理逻辑协议引用', async () => { - const resourceId = 'role:java-developer' - const mockContent = 'Role content...' - const mockReference = { id: resourceId, protocol: 'role', path: '/path/to/role' } - - // Mock the loadResource method which is what resolve() calls internally - manager.loadResource = jest.fn().mockResolvedValue({ - success: true, - content: mockContent, - resourceId, - reference: mockReference - }) - - const result = await manager.resolve(resourceId) - - expect(result.success).toBe(true) - expect(result.content).toBe(mockContent) - }) - - test('应该处理传统格式资源ID', async () => { - const resourceId = 'java-developer.role.md' - const mockContent = 'File content...' - - mockRegistry.get.mockReturnValue(null) - mockProtocolParser.loadResource.mockResolvedValue(mockContent) - - const result = await manager.resolve(resourceId) - - expect(result.success).toBe(false) // 找不到资源 - }) - }) - - describe('新架构集成', () => { - // FIXME: 这个测试可能耗时,暂时注释掉以提高测试速度 - test.skip('应该支持initializeWithNewArchitecture方法', async () => { - const mockDiscoveryManager = { - discoverRegistries: jest.fn().mockResolvedValue() - } - manager.discoveryManager = mockDiscoveryManager - - await manager.initializeWithNewArchitecture() - - expect(mockDiscoveryManager.discoverRegistries).toHaveBeenCalled() - expect(manager.initialized).toBe(true) - }) - - test('应该支持loadResourceByProtocol方法', async () => { - const protocolUrl = '@package://test.md' - const mockContent = 'Test content' - - // Replace the real protocolParser with mock - manager.protocolParser = mockProtocolParser - mockProtocolParser.parse.mockReturnValue({ protocol: 'package', path: 'test.md' }) - - // Mock the protocol's resolve method - const mockPackageProtocol = { - resolve: jest.fn().mockResolvedValue(mockContent) - } - manager.protocols.set('package', mockPackageProtocol) - - const result = await manager.loadResourceByProtocol(protocolUrl) - - expect(mockProtocolParser.parse).toHaveBeenCalledWith(protocolUrl) - expect(mockPackageProtocol.resolve).toHaveBeenCalledWith('test.md', undefined) - expect(result).toBe(mockContent) - }) - }) - - describe('协议管理', () => { - test('应该能获取所有已注册的协议', () => { - const protocols = manager.getAvailableProtocols() - expect(protocols).toEqual(['package', 'project', 'role', 'thought', 'execution', 'knowledge']) - }) - - test('应该能检查协议是否支持', () => { - expect(manager.supportsProtocol('package')).toBe(true) - expect(manager.supportsProtocol('role')).toBe(true) - expect(manager.supportsProtocol('unknown')).toBe(false) - }) - }) - - describe('错误处理', () => { - test('应该优雅处理资源不存在的情况', async () => { - const resourceId = 'non-existent-resource' - - mockRegistry.get.mockReturnValue(null) - - const result = await manager.loadResource(resourceId) - - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - - test('应该处理协议解析错误', async () => { - const resourceId = '@invalid://resource' - - mockProtocolParser.parse.mockImplementation(() => { - throw new Error('Invalid protocol') - }) - - const result = await manager.loadResource(resourceId) - - expect(result.success).toBe(false) - expect(result.error).toBeDefined() - }) - }) -}) \ No newline at end of file diff --git a/src/tests/core/resource/SemanticRenderer.unit.test.js b/src/tests/core/resource/SemanticRenderer.unit.test.js deleted file mode 100644 index 757a91d..0000000 --- a/src/tests/core/resource/SemanticRenderer.unit.test.js +++ /dev/null @@ -1,223 +0,0 @@ -const SemanticRenderer = require('../../../lib/core/resource/SemanticRenderer') - -describe('SemanticRenderer', () => { - let renderer - let mockResourceManager - - beforeEach(() => { - renderer = new SemanticRenderer() - mockResourceManager = { - resolve: jest.fn() - } - }) - - describe('renderSemanticContent', () => { - test('应该保持@引用的位置语义', async () => { - // Arrange - const tagSemantics = { - fullSemantics: 'intro @!thought://A middle @!thought://B end', - references: [ - { - fullMatch: '@!thought://A', - priority: '!', - protocol: 'thought', - resource: 'A', - position: 6, - isRequired: true, - isOptional: false - }, - { - fullMatch: '@!thought://B', - priority: '!', - protocol: 'thought', - resource: 'B', - position: 32, - isRequired: true, - isOptional: false - } - ] - } - - mockResourceManager.resolve - .mockResolvedValueOnce({ success: true, content: '[A的内容]' }) - .mockResolvedValueOnce({ success: true, content: '[B的内容]' }) - - // Act - const result = await renderer.renderSemanticContent(tagSemantics, mockResourceManager) - - // Assert - expect(result).toContain('[A的内容]') - expect(result).toContain('[B的内容]') - expect(mockResourceManager.resolve).toHaveBeenCalledTimes(2) - }) - - test('应该处理复杂的@引用布局', async () => { - // Arrange - const content = `# 标题 -@!thought://base - -## 子标题 -- 列表项1 -@!execution://action -- 列表项2` - - const tagSemantics = { - fullSemantics: content, - references: [ - { - fullMatch: '@!thought://base', - priority: '!', - protocol: 'thought', - resource: 'base', - position: 5, - isRequired: true, - isOptional: false - }, - { - fullMatch: '@!execution://action', - priority: '!', - protocol: 'execution', - resource: 'action', - position: 40, - isRequired: true, - isOptional: false - } - ] - } - - mockResourceManager.resolve - .mockResolvedValueOnce({ success: true, content: '基础思维框架内容' }) - .mockResolvedValueOnce({ success: true, content: '执行动作框架内容' }) - - // Act - const result = await renderer.renderSemanticContent(tagSemantics, mockResourceManager) - - // Assert - expect(result).toContain('基础思维框架内容') - expect(result).toContain('执行动作框架内容') - expect(result).toContain('# 标题') - expect(result).toContain('- 列表项1') - expect(result).toContain('- 列表项2') - }) - - test('应该优雅处理引用解析失败', async () => { - // Arrange - const tagSemantics = { - fullSemantics: 'content with @!thought://missing reference', - references: [ - { - fullMatch: '@!thought://missing', - priority: '!', - protocol: 'thought', - resource: 'missing', - position: 13, - isRequired: true, - isOptional: false - } - ] - } - - mockResourceManager.resolve.mockResolvedValueOnce({ success: false, error: new Error('Resource not found') }) - - // Act - const result = await renderer.renderSemanticContent(tagSemantics, mockResourceManager) - - // Assert - expect(result).toContain('content with