diff --git a/apps/daemon/tests/chat-route.test.ts b/apps/daemon/tests/chat-route.test.ts index 2df4b60e0..614f5044f 100644 --- a/apps/daemon/tests/chat-route.test.ts +++ b/apps/daemon/tests/chat-route.test.ts @@ -394,6 +394,76 @@ child.on('exit', (code, signal) => { } }); + it('allows plugin authoring to succeed when the requested generated-plugin artifacts exist before close', async () => { + const projectId = `proj-plugin-authoring-success-${randomUUID()}`; + + const createProjectResponse = await fetch(`${baseUrl}/api/projects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: projectId, + name: 'Plugin authoring artifact success fixture', + skillId: null, + designSystemId: null, + }), + }); + expect(createProjectResponse.status).toBe(200); + const conversationsResponse = await fetch(`${baseUrl}/api/projects/${projectId}/conversations`); + expect(conversationsResponse.status).toBe(200); + const conversationsBody = await conversationsResponse.json() as { + conversations: Array<{ id: string }>; + }; + const conversationId = conversationsBody.conversations[0]?.id; + expect(conversationId).toBeTruthy(); + + await withFakeAgent( + 'opencode', + ` +const fs = require('node:fs'); +const path = require('node:path'); +process.stdin.resume(); +process.stdin.on('end', () => { + const pluginDir = path.join(process.cwd(), 'generated-plugin'); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync(path.join(pluginDir, 'open-design.json'), JSON.stringify({ name: 'generated-plugin' }, null, 2)); + fs.writeFileSync(path.join(pluginDir, 'SKILL.md'), '# Generated plugin\\n'); + console.log(JSON.stringify({ type: 'step_start' })); + console.log(JSON.stringify({ type: 'text', part: { text: '我来帮你创建一个通用的 Open Design 插件脚手架。先读取文档规范,再生成插件文件。' } })); + console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } })); + process.exit(0); +}); +`, + async () => { + const createResponse = await fetch(`${baseUrl}/api/runs`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + agentId: 'opencode', + projectId, + conversationId, + pluginId: 'od-plugin-authoring', + message: '请创建一个可刷新、可审计、由 API 驱动的 Open Design 插件脚手架。', + }), + }); + expect(createResponse.status).toBe(202); + const { runId } = await createResponse.json() as { runId: string }; + + const eventsResponse = await fetch(`${baseUrl}/api/runs/${runId}/events`); + const eventsBody = await readSseUntil(eventsResponse, 'event: final'); + const statusBody = await waitForRunStatus(baseUrl, runId); + + expect(eventsBody).toContain('先读取文档规范,再生成插件文件'); + expect(statusBody.status).toBe('succeeded'); + + const filesResponse = await fetch(`${baseUrl}/api/projects/${projectId}/files`); + expect(filesResponse.status).toBe(200); + const filesBody = await filesResponse.json() as { files: Array<{ name: string }> }; + expect(filesBody.files.some((file) => file.name === 'generated-plugin/open-design.json')).toBe(true); + expect(filesBody.files.some((file) => file.name === 'generated-plugin/SKILL.md')).toBe(true); + }, + ); + }); + it('does not report plugin authoring as succeeded when the agent only emits planning text without artifacts', async () => { const projectId = `proj-plugin-authoring-${randomUUID()}`; diff --git a/docs/testing/launch-review-e2e-regressions.zh-CN.md b/docs/testing/launch-review-e2e-regressions.zh-CN.md index 279c28531..f7cab5e8d 100644 --- a/docs/testing/launch-review-e2e-regressions.zh-CN.md +++ b/docs/testing/launch-review-e2e-regressions.zh-CN.md @@ -392,6 +392,10 @@ pnpm exec playwright test -c playwright.config.ts ui/design-systems-manager.test - `generated-plugin/SKILL.md` - daemon 会把本轮转成 `failed`,而不是错误地保留 `succeeded` +2. `allows plugin authoring to succeed when the requested generated-plugin artifacts exist before close` + - 覆盖同一条 guard 的对称路径:只要关键插件产物已经落地,就不会被误伤成失败 + - 锁住 daemon 的判断是“缺少目标产物才失败”,而不是“只要文本看起来像计划句就失败” + ### 6. Grok Build prompt inline argv 契约