diff --git a/src/__tests__/project-wiki-command.spec.ts b/src/__tests__/project-wiki-command.spec.ts index e88740a72bc9..c31438f635b3 100644 --- a/src/__tests__/project-wiki-command.spec.ts +++ b/src/__tests__/project-wiki-command.spec.ts @@ -1,19 +1,19 @@ import { getCommands, getCommand } from "../services/command/commands" -import { ensureProjectWikiCommandExists } from "../core/costrict/wiki/projectWikiHelpers" +import { ensureProjectWikiSubtasksExists } from "../core/costrict/wiki/projectWikiHelpers" import { projectWikiCommandName } from "../core/costrict/wiki/projectWikiHelpers" describe("Project Wiki Command Integration", () => { const testCwd = process.cwd() - describe("动态命令初始化", () => { - it("应该能够初始化 project-wiki 命令而不抛出错误", async () => { - // 测试 ensureProjectWikiCommandExists 函数 - await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow() + describe("子任务文件初始化", () => { + it("应该能够初始化 project-wiki 子任务文件而不抛出错误", async () => { + // 测试 ensureProjectWikiSubtasksExists 函数 + await expect(ensureProjectWikiSubtasksExists()).resolves.not.toThrow() }) it("getCommands() 应该包含 project-wiki 命令", async () => { - // 确保命令已初始化 - await ensureProjectWikiCommandExists() + // 确保子任务文件已初始化 + await ensureProjectWikiSubtasksExists() // 获取所有命令 const commands = await getCommands(testCwd) @@ -29,16 +29,16 @@ describe("Project Wiki Command Integration", () => { if (projectWikiCommand) { expect(projectWikiCommand.name).toBe(projectWikiCommandName) - expect(projectWikiCommand.source).toBe("global") + // 命令来源可能是 built-in 或 global,取决于是否存在全局文件 + expect(["built-in", "global"]).toContain(projectWikiCommand.source) expect(typeof projectWikiCommand.content).toBe("string") expect(projectWikiCommand.content.length).toBeGreaterThan(0) - expect(projectWikiCommand.filePath).toContain("project-wiki.md") } }) it("getCommand() 应该能够获取 project-wiki 命令", async () => { - // 确保命令已初始化 - await ensureProjectWikiCommandExists() + // 确保子任务文件已初始化 + await ensureProjectWikiSubtasksExists() // 获取特定命令 const command = await getCommand(testCwd, projectWikiCommandName) @@ -46,33 +46,34 @@ describe("Project Wiki Command Integration", () => { // 验证命令存在且正确 expect(command).toBeDefined() expect(command?.name).toBe(projectWikiCommandName) - expect(command?.source).toBe("global") + // 命令来源可能是 built-in 或 global,取决于是否存在全局文件 + expect(["built-in", "global"]).toContain(command?.source) expect(typeof command?.content).toBe("string") expect(command?.content.length).toBeGreaterThan(0) }) }) describe("错误处理机制", () => { - it("即使 ensureProjectWikiCommandExists 失败,getCommands 也应该正常工作", async () => { + it("即使 ensureProjectWikiSubtasksExists 失败,getCommands 也应该正常工作", async () => { // 这个测试验证错误隔离机制 - // 即使动态命令初始化失败,其他命令仍应正常工作 + // 即使子任务文件初始化失败,其他命令仍应正常工作 const commands = await getCommands(testCwd) // 应该返回数组(可能为空,但不应该抛出错误) expect(Array.isArray(commands)).toBe(true) }) - it("应该能够处理重复的命令初始化调用", async () => { + it("应该能够处理重复的子任务文件初始化调用", async () => { // 多次调用应该不会出错 - await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow() - await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow() - await expect(ensureProjectWikiCommandExists()).resolves.not.toThrow() + await expect(ensureProjectWikiSubtasksExists()).resolves.not.toThrow() + await expect(ensureProjectWikiSubtasksExists()).resolves.not.toThrow() + await expect(ensureProjectWikiSubtasksExists()).resolves.not.toThrow() }) }) describe("命令内容验证", () => { it("project-wiki 命令应该包含预期的内容结构", async () => { - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() const command = await getCommand(testCwd, projectWikiCommandName) expect(command).toBeDefined() diff --git a/src/api/providers/zgsm.ts b/src/api/providers/zgsm.ts index bb26a29cac04..ee0dcef8fcb7 100644 --- a/src/api/providers/zgsm.ts +++ b/src/api/providers/zgsm.ts @@ -535,7 +535,7 @@ export class ZgsmAiHandler extends BaseProvider implements SingleCompletionHandl requestId, cachedClientId, cachedWorkspacePath, - "system", + "user", ), }, timeout: 20000, diff --git a/src/core/costrict/activate.ts b/src/core/costrict/activate.ts index 08278a7f5eaa..5b3565c64c66 100644 --- a/src/core/costrict/activate.ts +++ b/src/core/costrict/activate.ts @@ -38,7 +38,7 @@ import { writeCostrictAccessToken } from "./codebase-index/utils" import { getPanel } from "../../activate/registerCommands" import { t } from "../../i18n" import prettyBytes from "pretty-bytes" -import { ensureProjectWikiCommandExists } from "./wiki/projectWikiHelpers" +import { ensureProjectWikiSubtasksExists } from "./wiki/projectWikiHelpers" const HISTORY_WARN_SIZE = 1000 * 1000 * 1000 * 3 @@ -212,8 +212,8 @@ export async function activate( setTimeout(() => { loginTip() - // init project-wiki command. - ensureProjectWikiCommandExists() + // init project-wiki subtasks. + ensureProjectWikiSubtasksExists() }, 2000) } diff --git a/src/core/costrict/wiki/__tests__/projectWikiHelpers.spec.ts b/src/core/costrict/wiki/__tests__/projectWikiHelpers.spec.ts index 6f0e7bec6dc0..81ba4b940da6 100644 --- a/src/core/costrict/wiki/__tests__/projectWikiHelpers.spec.ts +++ b/src/core/costrict/wiki/__tests__/projectWikiHelpers.spec.ts @@ -61,7 +61,7 @@ vi.mock("../wiki-prompts/subtasks/constants", () => ({ vi.mock("../wiki-prompts/project_wiki", () => ({ projectWikiVersion: "v1.0.1", - PROJECT_WIKI_TEMPLATE: "---\ndescription: \"项目深度分析与知识文档生成\"\nversion: v1.0.1\n---\n# 测试模板", + PROJECT_WIKI_TEMPLATE: '---\ndescription: "项目深度分析与知识文档生成"\nversion: v1.0.1\n---\n# 测试模板', })) vi.mock("../wiki-prompts/subtasks/01_Project_Overview_Analysis", () => ({ @@ -110,7 +110,7 @@ vi.mock("../wiki-prompts/subtasks/11_Project_Rules_Generation", () => ({ // 导入模块 import * as fs from "fs" -import { ensureProjectWikiCommandExists, setLogger } from "../projectWikiHelpers" +import { ensureProjectWikiSubtasksExists, setLogger } from "../projectWikiHelpers" describe("projectWikiHelpers", () => { beforeEach(() => { @@ -119,55 +119,47 @@ describe("projectWikiHelpers", () => { setLogger(mockLogger) }) - describe("ensureProjectWikiCommandExists", () => { - it("应该在文件不存在时创建项目wiki命令", async () => { - // Mock fs.access 抛出错误表示文件不存在 - vi.mocked(fs.promises.access).mockRejectedValue(new Error("File not found")) + describe("ensureProjectWikiSubtasksExists", () => { + it("应该在子任务目录不存在时创建子任务文件", async () => { + // Mock fs.stat 抛出错误表示目录不存在 vi.mocked(fs.promises.stat).mockRejectedValue(new Error("Directory not found")) vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined) vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined) vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() - expect(fs.promises.mkdir).toHaveBeenCalledWith("/home/user/.roo/commands", { recursive: true }) + expect(fs.promises.mkdir).toHaveBeenCalledWith("/home/user/.roo/commands/project-wiki-tasks/", { + recursive: true, + }) expect(fs.promises.writeFile).toHaveBeenCalled() - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...") + expect(mockLogger.info).toHaveBeenCalledWith( + "[projectWikiHelpers] Starting ensureProjectWikiSubtasksExists...", + ) }) - it("应该在版本不匹配时重新创建文件", async () => { - // Mock 文件存在但版本不匹配 - vi.mocked(fs.promises.access).mockResolvedValue(undefined) - vi.mocked(fs.promises.readFile).mockResolvedValue(`--- -description: "项目深度分析与知识文档生成" -version: "v1.0.0" ---- -# 旧版本模板`) + it("应该在子任务文件不完整时重新创建", async () => { + // Mock 子任务目录存在但文件不完整 vi.mocked(fs.promises.stat).mockResolvedValue({ isDirectory: () => true, } as any) vi.mocked(fs.promises.readdir).mockResolvedValue([ "01_Project_Overview_Analysis.md", "02_Overall_Architecture_Analysis.md", + // 缺少其他文件 ] as any) vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined) vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined) vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() expect(fs.promises.rm).toHaveBeenCalled() expect(fs.promises.writeFile).toHaveBeenCalled() }) - it("应该正确处理文件存在的情况", async () => { - // Mock 文件存在的情况 - vi.mocked(fs.promises.access).mockResolvedValue(undefined) - vi.mocked(fs.promises.readFile).mockResolvedValue(`--- -description: "项目深度分析与知识文档生成" -version: "v1.0.1" ---- -# 当前版本模板`) + it("应该正确处理子任务文件完整的情况", async () => { + // Mock 子任务文件完整的情况 vi.mocked(fs.promises.stat).mockResolvedValue({ isDirectory: () => true, } as any) @@ -188,112 +180,70 @@ version: "v1.0.1" vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined) vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() // 验证启动日志被调用 - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...") - // 验证设置命令的日志被调用 - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Setting up project-wiki command...") - // 验证写文件操作被调用 - expect(fs.promises.writeFile).toHaveBeenCalled() - }) - - it("应该在子任务目录缺少文件时重新创建", async () => { - // Mock 主文件存在但子任务目录缺少文件 - vi.mocked(fs.promises.access).mockResolvedValue(undefined) - vi.mocked(fs.promises.readFile).mockResolvedValue(`--- -description: "项目深度分析与知识文档生成" -version: "v1.0.1" ---- -# 当前版本模板`) - vi.mocked(fs.promises.stat).mockResolvedValue({ - isDirectory: () => true, - } as any) - vi.mocked(fs.promises.readdir).mockResolvedValue([ - "01_Project_Overview_Analysis.md", - // 缺少其他文件 - ] as any) - vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined) - vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined) - vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - - await ensureProjectWikiCommandExists() - - expect(fs.promises.rm).toHaveBeenCalled() - expect(fs.promises.writeFile).toHaveBeenCalled() + expect(mockLogger.info).toHaveBeenCalledWith( + "[projectWikiHelpers] Starting ensureProjectWikiSubtasksExists...", + ) + // 验证子任务已存在的日志被调用 + expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] project-wiki subtasks already exist") + // 验证不需要重新生成文件 + expect(fs.promises.writeFile).not.toHaveBeenCalled() }) it("应该处理错误情况", async () => { const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) - + // Mock mkdir 抛出错误 vi.mocked(fs.promises.mkdir).mockRejectedValue(new Error("Permission denied")) - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() expect(consoleSpy).toHaveBeenCalledWith( - "[commands] Failed to initialize project-wiki command:", - expect.stringContaining("Permission denied") + "[commands] Failed to initialize project-wiki subtasks:", + expect.stringContaining("Permission denied"), ) consoleSpy.mockRestore() }) - it("应该正确处理前置matter解析错误", async () => { - // Mock 文件存在但前置matter格式错误 - vi.mocked(fs.promises.access).mockResolvedValue(undefined) - vi.mocked(fs.promises.readFile).mockResolvedValue(`# 没有前置matter的文件`) - vi.mocked(fs.promises.stat).mockResolvedValue({ - isDirectory: () => true, - } as any) - vi.mocked(fs.promises.readdir).mockResolvedValue([] as any) - vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined) - vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined) - vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - - await ensureProjectWikiCommandExists() - - // 验证启动日志被调用 - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...") - // 验证设置命令的日志被调用 - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Setting up project-wiki command...") - // 验证写文件操作被调用 - expect(fs.promises.writeFile).toHaveBeenCalled() - }) - it("应该正确处理部分子任务文件生成失败的情况", async () => { - // Mock 文件不存在,需要创建 - vi.mocked(fs.promises.access).mockRejectedValue(new Error("File not found")) + // Mock 子任务目录不存在,需要创建 vi.mocked(fs.promises.stat).mockRejectedValue(new Error("Directory not found")) vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined) vi.mocked(fs.promises.writeFile) - .mockResolvedValueOnce(undefined) // 主文件成功 .mockRejectedValueOnce(new Error("Write failed")) // 第一个子任务文件失败 .mockResolvedValue(undefined) // 其他文件成功 vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() // 验证启动和设置日志被调用 - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...") - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Setting up project-wiki command...") + expect(mockLogger.info).toHaveBeenCalledWith( + "[projectWikiHelpers] Starting ensureProjectWikiSubtasksExists...", + ) + expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Setting up project-wiki subtasks...") // 验证警告日志被调用(部分文件生成失败) expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Failed to generate")) }) it("应该正确处理Promise.allSettled的混合结果", async () => { // Mock 混合的成功和失败情况 - vi.mocked(fs.promises.access).mockRejectedValue(new Error("File not found")) vi.mocked(fs.promises.stat).mockRejectedValue(new Error("Directory not found")) vi.mocked(fs.promises.mkdir).mockResolvedValue(undefined) vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined) vi.mocked(fs.promises.rm).mockResolvedValue(undefined) - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() // 验证基本流程被执行 - expect(mockLogger.info).toHaveBeenCalledWith("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...") - expect(fs.promises.mkdir).toHaveBeenCalledWith("/home/user/.roo/commands", { recursive: true }) + expect(mockLogger.info).toHaveBeenCalledWith( + "[projectWikiHelpers] Starting ensureProjectWikiSubtasksExists...", + ) + expect(fs.promises.mkdir).toHaveBeenCalledWith("/home/user/.roo/commands/project-wiki-tasks/", { + recursive: true, + }) }) }) @@ -311,4 +261,4 @@ version: "v1.0.1" expect(() => setLogger(testLogger)).not.toThrow() }) }) -}) \ No newline at end of file +}) diff --git a/src/core/costrict/wiki/projectWikiHelpers.ts b/src/core/costrict/wiki/projectWikiHelpers.ts index 2d794fccbd8f..31cbb992441f 100644 --- a/src/core/costrict/wiki/projectWikiHelpers.ts +++ b/src/core/costrict/wiki/projectWikiHelpers.ts @@ -1,8 +1,6 @@ import { promises as fs } from "fs" import * as path from "path" -import { formatError, getGlobalCommandsDir, subtaskDir } from "./wiki-prompts/subtasks/constants" -import { PROJECT_WIKI_TEMPLATE, projectWikiVersion } from "./wiki-prompts/project_wiki" -import { SUBTASK_FILENAMES, MAIN_WIKI_FILENAME } from "./wiki-prompts/subtasks/constants" +import { formatError, subtaskDir, SUBTASK_FILENAMES } from "./wiki-prompts/subtasks/constants" import { PROJECT_OVERVIEW_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/01_Project_Overview_Analysis" import { OVERALL_ARCHITECTURE_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/02_Overall_Architecture_Analysis" import { SERVICE_DEPENDENCIES_ANALYSIS_TEMPLATE } from "./wiki-prompts/subtasks/03_Service_Dependencies_Analysis" @@ -27,9 +25,8 @@ export function setLogger(testLogger: ILogger): void { logger = testLogger } -// Template data mapping -const TEMPLATES = { - [MAIN_WIKI_FILENAME]: PROJECT_WIKI_TEMPLATE, +// Template data mapping for subtasks only +const SUBTASK_TEMPLATES = { [SUBTASK_FILENAMES.PROJECT_OVERVIEW_TASK_FILE]: PROJECT_OVERVIEW_ANALYSIS_TEMPLATE, [SUBTASK_FILENAMES.OVERALL_ARCHITECTURE_TASK_FILE]: OVERALL_ARCHITECTURE_ANALYSIS_TEMPLATE, [SUBTASK_FILENAMES.SERVICE_DEPENDENCIES_TASK_FILE]: SERVICE_DEPENDENCIES_ANALYSIS_TEMPLATE, @@ -43,75 +40,34 @@ const TEMPLATES = { [SUBTASK_FILENAMES.PROJECT_RULES_TASK_FILE]: PROJECT_RULES_GENERATION_TEMPLATE, } -export async function ensureProjectWikiCommandExists() { +export async function ensureProjectWikiSubtasksExists() { const startTime = Date.now() - logger.info("[projectWikiHelpers] Starting ensureProjectWikiCommandExists...") + logger.info("[projectWikiHelpers] Starting ensureProjectWikiSubtasksExists...") try { - const globalCommandsDir = getGlobalCommandsDir() - await fs.mkdir(globalCommandsDir, { recursive: true }) + // Ensure subtask directory exists + await fs.mkdir(subtaskDir, { recursive: true }) - const projectWikiFile = path.join(globalCommandsDir, `${projectWikiCommandName}.md`) - - // Check if setup is needed - const needsSetup = await checkIfSetupNeeded(projectWikiFile, subtaskDir) + // Check if subtask setup is needed + const needsSetup = await checkIfSubtaskSetupNeeded(subtaskDir) if (!needsSetup) { - logger.info("[projectWikiHelpers] project-wiki command already exists") + logger.info("[projectWikiHelpers] project-wiki subtasks already exist") return } - logger.info("[projectWikiHelpers] Setting up project-wiki command...") + logger.info("[projectWikiHelpers] Setting up project-wiki subtasks...") - // Clean up existing files - await Promise.allSettled([ - fs.rm(projectWikiFile, { force: true }), - fs.rm(subtaskDir, { recursive: true, force: true }), - ]) + // Clean up existing subtask directory + await fs.rm(subtaskDir, { recursive: true, force: true }) - // Generate Wiki files - await generateWikiCommandFiles(projectWikiFile, subtaskDir) + // Generate subtask files + await generateSubtaskFiles(subtaskDir) const duration = Date.now() - startTime - logger.info(`[projectWikiHelpers] project-wiki command setup completed in ${duration}ms`) + logger.info(`[projectWikiHelpers] project-wiki subtasks setup completed in ${duration}ms`) } catch (error) { const errorMsg = formatError(error) - console.error("[commands] Failed to initialize project-wiki command:", errorMsg) - } -} - -// Check if file version matches current version -async function checkFileVersion(projectWikiFile: string): Promise { - try { - const existingContent = await fs.readFile(projectWikiFile, "utf-8") - - // Extract front matter section (between --- and ---) - const frontMatterMatch = existingContent.match(/^---\s*\n([\s\S]*?)\n---/) - if (!frontMatterMatch) { - logger.info("[projectWikiHelpers] No valid front matter found in existing file") - return false - } - - // Parse version from front matter - const frontMatterContent = frontMatterMatch[1] - const versionMatch = frontMatterContent.match(/^version:\s*"([^"]+)"/m) - if (!versionMatch) { - logger.info("[projectWikiHelpers] Version field not found in front matter") - return false - } - - const existingVersion = versionMatch[1].trim() - if (existingVersion !== projectWikiVersion) { - logger.info( - `[projectWikiHelpers] Version mismatch. Current: ${existingVersion}, Expected: ${projectWikiVersion}`, - ) - return false - } - - logger.info(`[projectWikiHelpers] Version check passed: ${existingVersion}`) - return true - } catch (error) { - logger.info("[projectWikiHelpers] Failed to read or parse existing file version:", formatError(error)) - return false + console.error("[commands] Failed to initialize project-wiki subtasks:", errorMsg) } } @@ -130,7 +86,7 @@ async function checkSubtaskDirectory(subTaskDir: string): Promise { const mdFiles = subTaskFiles.filter((file) => file.endsWith(".md")) // subtask file check. - const subTaskFileNames = Object.keys(TEMPLATES).filter((file) => file !== MAIN_WIKI_FILENAME) + const subTaskFileNames = Object.keys(SUBTASK_TEMPLATES) const missingSubTaskFiles = subTaskFileNames.filter((fileName) => !mdFiles.includes(fileName)) if (missingSubTaskFiles.length > 0) { @@ -145,60 +101,28 @@ async function checkSubtaskDirectory(subTaskDir: string): Promise { } } -// Optimized file checking logic, using Promise.allSettled to improve performance -async function checkIfSetupNeeded(projectWikiFile: string, subTaskDir: string): Promise { +// Check if subtask setup is needed +async function checkIfSubtaskSetupNeeded(subTaskDir: string): Promise { try { - const [mainFileResult, subDirResult] = await Promise.allSettled([ - fs.access(projectWikiFile, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK), - fs.stat(subTaskDir), - ]) - - // If main file doesn't exist, setup is needed - if (mainFileResult.status === "rejected") { - logger.info("[projectWikiHelpers] projectWikiFile not accessible:", formatError(mainFileResult.reason)) - return true - } - - // Check version in existing file - const isVersionValid = await checkFileVersion(projectWikiFile) - if (!isVersionValid) { - return true - } - - // If subtask directory doesn't exist or is not valid, setup is needed - if (subDirResult.status === "rejected") { - logger.info("[projectWikiHelpers] subTaskDir not accessible:", formatError(subDirResult.reason)) - return true - } - const isSubtaskDirValid = await checkSubtaskDirectory(subTaskDir) return !isSubtaskDirValid } catch (error) { - logger.error("[projectWikiHelpers] Error checking setup status:", formatError(error)) + logger.info("[projectWikiHelpers] subTaskDir not accessible:", formatError(error)) return true } } -// Generate Wiki files -async function generateWikiCommandFiles(projectWikiFile: string, subTaskDir: string): Promise { +// Generate subtask files +async function generateSubtaskFiles(subTaskDir: string): Promise { try { - // Generate main file - const mainTemplate = TEMPLATES[MAIN_WIKI_FILENAME] - if (!mainTemplate) { - throw new Error("Main template not found") - } - - await fs.writeFile(projectWikiFile, mainTemplate, "utf-8") - logger.info(`[projectWikiHelpers] Generated main wiki file: ${projectWikiFile}`) - // Create subtask directory await fs.mkdir(subTaskDir, { recursive: true }) // Generate subtask files - const subTaskFiles = Object.keys(TEMPLATES).filter((file) => file !== MAIN_WIKI_FILENAME) + const subTaskFiles = Object.keys(SUBTASK_TEMPLATES) const generateResults = await Promise.allSettled( subTaskFiles.map(async (file) => { - const template = TEMPLATES[file as keyof typeof TEMPLATES] + const template = SUBTASK_TEMPLATES[file as keyof typeof SUBTASK_TEMPLATES] if (!template) { throw new Error(`Template not found for file: ${file}`) } @@ -225,6 +149,6 @@ async function generateWikiCommandFiles(projectWikiFile: string, subTaskDir: str } } catch (error) { const errorMsg = formatError(error) - throw new Error(`Failed to generate wiki files: ${errorMsg}`) + throw new Error(`Failed to generate subtask files: ${errorMsg}`) } } diff --git a/src/core/costrict/workflow/builtIn-commands/tdd.md b/src/core/costrict/workflow/builtIn-commands/tdd.md new file mode 100644 index 000000000000..5f0495549799 --- /dev/null +++ b/src/core/costrict/workflow/builtIn-commands/tdd.md @@ -0,0 +1,9 @@ +--- +description: "测试驱动开发" +--- + +请严格遵守以下规则: + +1. 查看 .cospec/TEST_GUIDE.md 文件是否存在,若文件不存在,则使用 ask_followup_question 工具告知用户如何创建测试指导文档:“未找到测试指导文档,请通过触发首页的《测试方案》功能来生成。确定并退出跳过测试步骤”,然后可忽略后续测试要求;若文件存在,则读取该文件作为测试方法的唯一真相来源 (Single Source of Truth)。 +2. 确保所有测试用例 100% 执行通过 +3. 如果测试用例没有全部通过,必须使用 ask_followup_question 工具询问我:“测试未完全通过(当前通过率:[请填入实际通过率]%),是否允许结束任务?”。只有我给出肯定答复,才可以使用 attempt_completion 工具 diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 019a1b776784..755d3d4d062b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -71,7 +71,7 @@ import { ErrorCodeManager } from "../costrict/error-code" import { writeCostrictAccessToken } from "../costrict/codebase-index/utils" import { workspaceEventMonitor } from "../costrict/codebase-index/workspace-event-monitor" import { fetchZgsmQuotaInfo } from "../../api/providers/fetchers/zgsm" -import { ensureProjectWikiCommandExists } from "../costrict/wiki/projectWikiHelpers" +import { ensureProjectWikiSubtasksExists } from "../costrict/wiki/projectWikiHelpers" export const webviewMessageHandler = async ( provider: ClineProvider, @@ -558,7 +558,7 @@ export const webviewMessageHandler = async ( // task. This essentially creates a fresh slate for the new task. try { if (message.values?.checkProjectWiki) { - await ensureProjectWikiCommandExists() + await ensureProjectWikiSubtasksExists() } await provider.createTask(message.text, message.images) // Task created successfully - notify the UI to reset diff --git a/src/services/command/__tests__/built-in-commands.spec.ts b/src/services/command/__tests__/built-in-commands.spec.ts index d1685f542052..86166f78840a 100644 --- a/src/services/command/__tests__/built-in-commands.spec.ts +++ b/src/services/command/__tests__/built-in-commands.spec.ts @@ -5,7 +5,7 @@ describe("Built-in Commands", () => { it("should return all built-in commands", async () => { const commands = await getBuiltInCommands() - expect(commands).toHaveLength(2) + expect(commands).toHaveLength(4) expect(commands.map((cmd) => cmd.name)).toEqual(expect.arrayContaining(["init", "test-guide"])) // Verify all commands have required properties @@ -63,10 +63,10 @@ describe("Built-in Commands", () => { it("should return all built-in command names", async () => { const names = await getBuiltInCommandNames() - expect(names).toHaveLength(2) - expect(names).toEqual(expect.arrayContaining(["init", "test-guide"])) + expect(names).toHaveLength(4) + expect(names).toEqual(expect.arrayContaining(["init", "project-wiki", "tdd", "test-guide"])) // Order doesn't matter since it's based on filesystem order - expect(names.sort()).toEqual(["init", "test-guide"]) + expect(names.sort()).toEqual(["init", "project-wiki", "tdd", "test-guide"]) }) it("should return array of strings", async () => { diff --git a/src/services/command/built-in-commands.ts b/src/services/command/built-in-commands.ts index f1b700c9784e..a5e218ad25a4 100644 --- a/src/services/command/built-in-commands.ts +++ b/src/services/command/built-in-commands.ts @@ -1,4 +1,5 @@ import { Command } from "./commands" +import { PROJECT_WIKI_TEMPLATE } from "../../core/costrict/wiki/wiki-prompts/project_wiki" interface BuiltInCommandDefinition { name: string @@ -283,6 +284,19 @@ Please analyze this codebase and create an AGENTS.md file containing: Remember: The goal is to create documentation that enables AI assistants to be immediately productive in this codebase, focusing on project-specific knowledge that isn't obvious from the code structure alone.`, + }, + tdd: { + name: "tdd", + description: "Test driven development", + content: `--- +description: "测试驱动开发" +--- + +请严格遵守以下规则: + +1. 查看 .cospec/TEST_GUIDE.md 文件是否存在,若文件不存在,则使用 ask_followup_question 工具告知用户如何创建测试指导文档:“未找到测试指导文档,请通过触发首页的《测试方案》功能来生成。确定并退出跳过测试步骤”,然后可忽略后续测试要求;若文件存在,则读取该文件作为测试方法的唯一真相来源 (Single Source of Truth)。 +2. 确保所有测试用例 100% 执行通过 +3. 如果测试用例没有全部通过,必须使用 ask_followup_question 工具询问我:“测试未完全通过(当前通过率:[请填入实际通过率]%),是否允许结束任务?”。只有我给出肯定答复,才可以使用 attempt_completion 工具`, }, "test-guide": { name: "test-guide", @@ -310,19 +324,19 @@ description: "生成项目测试指导文档" 这是所有操作的第一步,也是决策的关键。 1. **扫描项目**: - * **任务**:全面分析项目结构,重点检查是否存在用于自动化测试的配置文件或脚本。 - * **检查重点**:\`build.sh\`, \`Makefile\`, \`package.json\` (中的 \`scripts\` 部分), \`scripts/\` 目录, \`test/\` 或 \`tests/\` 目录等。 - * **判断标准**:确定项目中是否存在一个**已定义的、可运行的** API 测试命令或流程。 + + - **任务**:全面分析项目结构,重点检查是否存在用于自动化测试的配置文件或脚本。 + - **检查重点**:\`build.sh\`, \`Makefile\`, \`package.json\` (中的 \`scripts\` 部分), \`scripts/\` 目录, \`test/\` 或 \`tests/\` 目录等。 + - **判断标准**:确定项目中是否存在一个**已定义的、可运行的** API 测试命令或流程。 2. **决策与沟通**: - * **如果找到现有测试机制**:直接进入 **[路径A:复用与验证]**。 - * **如果未找到或机制不完整**:必须使用 \`ask_followup_question\` 工具询问用户,并提供明确选项: + - **如果找到现有测试机制**:直接进入 **[路径A:复用与验证]**。 + - **如果未找到或机制不完整**:必须使用 \`ask_followup_question\` 工具询问用户,并提供明确选项: > "检测到当前项目缺少一套完整的本地化API测试机制。是否授权我为您创建一套? > - > 是,请帮我创建 - > 否,暂时不需要" - * 若用户选择“是”,则进入 **[路径B:创建与验证]**。 - * 若用户选择“否”,则使用 \`attempt_completion\` 礼貌地终止任务。 + > 是,请帮我创建 > 否,暂时不需要" + - 若用户选择“是”,则进入 **[路径B:创建与验证]**。 + - 若用户选择“否”,则使用 \`attempt_completion\` 礼貌地终止任务。 --- @@ -336,8 +350,8 @@ description: "生成项目测试指导文档" ##### 进度跟踪 -* **任务开始时的第一步**: 使用 \`todo_list\` 工具列出任务清单,此操作必须在其它任何动作之前。 -* 通过任务清单的勾选状态跟踪实现进度。 +- **任务开始时的第一步**: 使用 \`todo_list\` 工具列出任务清单,此操作必须在其它任何动作之前。 +- 通过任务清单的勾选状态跟踪实现进度。 ##### 任务清单内容 @@ -345,9 +359,9 @@ description: "生成项目测试指导文档" 1. **提炼流程**:总结并提炼出现有测试脚本的完整执行流程(例如:如何安装依赖、启动服务、运行特定测试、清理环境)。 2. **委托脚本验证与修复**: - * **必须**使用 \`new_task\` 工具,切换到 \`Code\` 模式,将**验证并修复现有测试脚本**的任务委托给子任务。 - * 传递给子任务的指令**必须**使用 **《附录A:委托子任务指令模板》**,其中 \`{{任务说明}}\` 应描述为“请验证并确保以下现有测试脚本能够成功执行”,\`{{测试方案}}\` 部分留空。 - * **子任务的核心职责**:必须在当前环境中**尝试执行**现有脚本。如果执行失败,必须遵循模板中的修复流程进行**循环修复**,直到脚本可以成功**跑完所有测试用例**为止。 + - **必须**使用 \`new_task\` 工具,切换到 \`Code\` 模式,将**验证并修复现有测试脚本**的任务委托给子任务。 + - 传递给子任务的指令**必须**使用 **《附录A:委托子任务指令模板》**,其中 \`{{任务说明}}\` 应描述为“请验证并确保以下现有测试脚本能够成功执行”,\`{{测试方案}}\` 部分留空。 + - **子任务的核心职责**:必须在当前环境中**尝试执行**现有脚本。如果执行失败,必须遵循模板中的修复流程进行**循环修复**,直到脚本可以成功**跑完所有测试用例**为止。 3. **生成指导文档**:严格依据**验证后**的测试机制,在 \`.cospec/\` 目录下创建 \`TEST_GUIDE.md\`。文档需遵循 **《附录B》** 和 **《附录C》** 的要求。 4. **寻求确认**:生成文档后,使用 \`ask_followup_question\` 工具寻求用户反馈: > "已根据项目现有测试流程生成指导文档 \`TEST_GUIDE.md\`。请您审阅,如需修改可直接提出,确认无误后请点击‘继续’。 @@ -361,8 +375,8 @@ description: "生成项目测试指导文档" ##### 进度跟踪 -* **任务开始时的第一步**: 使用 \`todo_list\` 工具列出任务清单,此操作必须在其它任何动作之前。 -* 通过任务清单的勾选状态跟踪实现进度。 +- **任务开始时的第一步**: 使用 \`todo_list\` 工具列出任务清单,此操作必须在其它任何动作之前。 +- 通过任务清单的勾选状态跟踪实现进度。 ##### 任务清单内容 @@ -371,9 +385,9 @@ description: "生成项目测试指导文档" 1. **规划方案**:严格遵循 **《附录A》** 中模板内的“测试方案要求”,规划一套新的测试方案,并向用户简要陈述设计思路。 2. **确认脚本创建**:使用 \`ask_followup_question\` 工具与用户确认测试脚本的创建。优先推荐在项目中能够找到的常见脚本形式(bash, bat, powershell等),否则默认使用bash。 3. **委托脚本生成与验证**: - * **必须**使用 \`new_task\` 工具,切换到 \`Code\` 模式,将**创建、验证并修复测试脚本**的任务委托给子任务。 - * 传递给子任务的指令**必须**使用 **《附录A:委托子任务指令模板》**,其中 \`{{任务说明}}\` 应描述为“请根据测试方案生成新的测试脚本并验证”,\`{{测试方案}}\` 部分填写第1步规划的方案。 - * **子任务的核心职责**:生成脚本后,必须在当前环境中**尝试执行**以验证其可用性。如果执行失败,必须遵循模板中的修复流程进行**循环修复**,直到脚本可以成功**跑完所有测试用例**为止。 + - **必须**使用 \`new_task\` 工具,切换到 \`Code\` 模式,将**创建、验证并修复测试脚本**的任务委托给子任务。 + - 传递给子任务的指令**必须**使用 **《附录A:委托子任务指令模板》**,其中 \`{{任务说明}}\` 应描述为“请根据测试方案生成新的测试脚本并验证”,\`{{测试方案}}\` 部分填写第1步规划的方案。 + - **子任务的核心职责**:生成脚本后,必须在当前环境中**尝试执行**以验证其可用性。如果执行失败,必须遵循模板中的修复流程进行**循环修复**,直到脚本可以成功**跑完所有测试用例**为止。 4. **编写指导文档**:在子任务成功返回后,在 \`.cospec/\` 目录下创建 \`TEST_GUIDE.md\`,详细说明这套**全新**测试机制的使用方法。文档结构需遵循 **《附录B》**。 5. **内部审查文档**:对照 **《附录C:测试指导文档要求》**,自我检查生成的文档是否符合精简、易于扩展的原则。 6. **寻求用户确认**:完成所有生成和审查后,使用 \`ask_followup_question\` 工具寻求用户反馈: @@ -454,19 +468,25 @@ description: "生成项目测试指导文档" ### **附录C:测试指导文档要求** -- **聚焦机制**:只需写明测试机制本身,便于指引 AI 和开发者查找正确信息,减少无用信息。 -- **忽略覆盖率**:无需关注测试覆盖率问题。 -- **展示扩展性**:不要枚举所有支持的功能点、模块。应只介绍功能点、模块的**声明机制**,提供如何**扩展**和**使用**的示例即可。 +- **聚焦机制**:只需写明测试机制本身,便于指引 AI 和开发者查找正确信息,减少无用信息。 +- **忽略覆盖率**:无需关注测试覆盖率问题。 +- **展示扩展性**:不要枚举所有支持的功能点、模块。应只介绍功能点、模块的**声明机制**,提供如何**扩展**和**使用**的示例即可。 ## 重要约束清单 ### 禁止行为 -- 禁止在未经用户同意的情况下修改用户业务代码,需说明清楚为什么这么修改。 +- 禁止在未经用户同意的情况下修改用户业务代码,需说明清楚为什么这么修改。 ### 必须行为 -- 严格按照本 Prompt 定义的工作流(即对应路径下的任务清单)推进任务。`, +- 严格按照本 Prompt 定义的工作流(即对应路径下的任务清单)推进任务。 +`, + }, + "project-wiki": { + name: "project-wiki", + description: "Perform an in-depth analysis of the project and create a comprehensive project wiki.", + content: PROJECT_WIKI_TEMPLATE, }, } diff --git a/webview-ui/src/components/welcome/RooTips.tsx b/webview-ui/src/components/welcome/RooTips.tsx index 7695bcda3863..6ba39559c1ab 100644 --- a/webview-ui/src/components/welcome/RooTips.tsx +++ b/webview-ui/src/components/welcome/RooTips.tsx @@ -12,19 +12,6 @@ import { StandardTooltip } from "../ui" import { Button } from "@/components/ui" const tips = [ - { - icon: "codicon-debug-all", - click: (e?: any) => { - e?.preventDefault() - vscode.postMessage({ - type: "mode", - text: "debug", - }) - }, - disabled: true, - titleKey: "rooTips.debug.title", - descriptionKey: "rooTips.debug.description", - }, { icon: "codicon-book", click: (e: any) => { @@ -33,17 +20,16 @@ const tips = [ type: "mode", text: "code", }) - // 调用 project-wiki 自定义指令 vscode.postMessage({ type: "newTask", - text: "/test-guide", + text: "/project-wiki", values: { checkProjectWiki: true, }, }) }, - titleKey: "rooTips.testGuide.title", - descriptionKey: "rooTips.testGuide.description", + titleKey: "rooTips.projectWiki.title", + descriptionKey: "rooTips.projectWiki.description", }, { icon: "codicon-book", @@ -53,17 +39,29 @@ const tips = [ type: "mode", text: "code", }) - // 调用 project-wiki 自定义指令 vscode.postMessage({ type: "newTask", - text: "/project-wiki", + text: "/test-guide", values: { checkProjectWiki: true, }, }) }, - titleKey: "rooTips.projectWiki.title", - descriptionKey: "rooTips.projectWiki.description", + titleKey: "rooTips.testGuide.title", + descriptionKey: "rooTips.testGuide.description", + }, + { + icon: "codicon-debug-all", + click: (e?: any) => { + e?.preventDefault() + vscode.postMessage({ + type: "mode", + text: "debug", + }) + }, + disabled: true, + titleKey: "rooTips.debug.title", + descriptionKey: "rooTips.debug.description", }, ] as { icon: string diff --git a/webview-ui/src/components/welcome/__tests__/RooTips.spec.tsx b/webview-ui/src/components/welcome/__tests__/RooTips.spec.tsx index 922c2f85b65e..918f0565b705 100644 --- a/webview-ui/src/components/welcome/__tests__/RooTips.spec.tsx +++ b/webview-ui/src/components/welcome/__tests__/RooTips.spec.tsx @@ -94,9 +94,9 @@ describe("RooTips Component", () => { }) test("renders tip cards", () => { - expect(screen.getByRole("button", { name: "rooTips.debug.title" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "rooTips.projectWiki.title" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "rooTips.testGuide.title" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "rooTips.debug.title" })).toBeInTheDocument() }) test("uses VSCode theme colors correctly", () => { diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index a335ed16ff22..68f0ac843cc9 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -61,7 +61,7 @@ "tooltip": "批准此操作" }, "runCommand": { - "title": "运行命令", + "title": "运行", "tooltip": "执行此命令" }, "proceedWhileRunning": { diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 06226479d02c..51d7af7f8c4a 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -69,7 +69,7 @@ } }, "runCommand": { - "title": "執行命令", + "title": "執行", "tooltip": "執行此命令" }, "proceedWhileRunning": {