From 9bf95343535c8c51732f05b6fdb778e1542000fe Mon Sep 17 00:00:00 2001 From: "junlei.zhang" <1036735371@qq.com> Date: Tue, 27 Oct 2026 12:48:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?chore(gitignore):=20=E5=BF=BD=E7=95=A5=20.c?= =?UTF-8?q?odebuddy=20=E6=9C=AC=E5=9C=B0=E8=BF=87=E7=A8=8B=E4=BA=A7?= =?UTF-8?q?=E7=89=A9=E4=B8=8E=20Playwright=20CLI=20=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .codebuddy/plans(方案草稿)、.codebuddy/preview(设计预览图)、 .codebuddy/tmp(临时排查脚本)与 .playwright-cli(CLI 会话日志) 均为本机过程产物,不应入库。 --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index e71476e53..06aeaa5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,11 @@ ai/agents/AiTaskPlatform/memory/data/ # ===== CodeBuddy 本地规则(个人纪律/项目约定,仅本地,不入库;用户规则在用户目录本就不跟踪)===== .codebuddy/rules/ +# ===== CodeBuddy 本地过程产物(方案草稿/设计预览图/临时排查脚本,仅本地,不入库)===== +.codebuddy/plans/ +.codebuddy/preview/ +.codebuddy/tmp/ + # ===== WorkBuddy 本地记忆/配置(跨会话记忆与自动化配置,仅本地,不入库)===== .workbuddy/ @@ -174,6 +179,7 @@ ai/kb/kb_structure_cache.json # ===== Playwright MCP 浏览器工具产物(每次工具调用自动落盘,不入库)===== .playwright-mcp/ +.playwright-cli/ # ===== 本地临时排查脚本(不入库)===== _tmp_*.sql From c9ed252eb5ca118cb720d0d13a7a86c85d51b3f0 Mon Sep 17 00:00:00 2001 From: "junlei.zhang" <1036735371@qq.com> Date: Tue, 27 Oct 2026 12:48:47 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(frontend):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=AE=A8=E8=AE=BA=E5=8C=BA=E8=BE=93=E5=85=A5=E6=A1=86=E5=8F=96?= =?UTF-8?q?=E5=80=BC=E4=B8=8D=E8=A2=AB=E6=94=B9=E5=86=99=E7=9A=84=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 锁定四点:含反斜杠的输入逐字一致、IME 组合输入不累积字符、 发送正文与输入框内容一致、超 500 字整段替换为 #clipboard(N行) 记号 (即用户反馈的「输入时闪一下」的真实机制)。 --- .../__tests__/DiscussionPanel.input.test.tsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 frontend/src/shared/components/__tests__/DiscussionPanel.input.test.tsx diff --git a/frontend/src/shared/components/__tests__/DiscussionPanel.input.test.tsx b/frontend/src/shared/components/__tests__/DiscussionPanel.input.test.tsx new file mode 100644 index 000000000..04ef2e4b7 --- /dev/null +++ b/frontend/src/shared/components/__tests__/DiscussionPanel.input.test.tsx @@ -0,0 +1,102 @@ +// 讨论区输入框「自验」用例:确认输入值在组件内不被任何转义/改写逻辑篡改。 +// +// 背景(用户反馈:输入评论时先「闪一下」,随后冒出一长串反斜杠): +// 怀疑点是 DiscussionPanel 的受控 textarea(value={commentText} + onChange) +// 在输入过程中对内容做了转义/拼接,或 IME 组合输入期间被重置。 +// 本用例锁定「输入框不会自己造出反斜杠」这一事实,为定位到底是代码 bug +// 还是用户粘贴/输入法侧带入提供判据。 +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, act } from '@testing-library/react'; + +// ---- auth store 替身:无权限(不拉 USP 环境列表),头像/名字为空 ---- +vi.mock('@/stores/auth', () => ({ + useAuthStore: Object.assign( + () => ({ username: 'tester', name: '测试', avatarResourceId: null, hasPermission: () => false }), + { getState: () => ({ username: 'tester', name: '测试', avatarResourceId: null, hasPermission: () => false }) }, + ), +})); + +// ---- 只读名单接口不触网 ---- +vi.mock('@/api/taskRead', () => ({ + fetchCommentReadList: vi.fn().mockResolvedValue([]), + reportCommentRead: vi.fn().mockResolvedValue(undefined), +})); + +import DiscussionPanel from '../DiscussionPanel'; + +const getInput = () => screen.getByRole('textbox') as HTMLTextAreaElement; + +describe('DiscussionPanel 输入框:值不被改写', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('输入含反斜杠的文本:textarea 值与用户输入逐字一致(不增不减反斜杠)', () => { + const onSend = vi.fn().mockResolvedValue(true); + render(); + const input = getInput(); + + // 典型场景:正文 + 一串反斜杠 + const typed = '会议结论:库位空调逻辑需要增加预设逻辑,对于库位状态需要加读写锁,\\\\\\\\\\\\'; + fireEvent.change(input, { target: { value: typed } }); + + expect(input.value).toBe(typed); + expect(input.value.match(/\\/g)?.length).toBe(typed.match(/\\/g)?.length); + }); + + it('连续多次输入(含 IME 组合事件):不会累积出额外字符', () => { + const onSend = vi.fn().mockResolvedValue(true); + render(); + const input = getInput(); + + fireEvent.compositionStart(input); + fireEvent.change(input, { target: { value: '会议结论' } }); + fireEvent.compositionUpdate(input, { data: '会议结论' }); + fireEvent.compositionEnd(input, { data: '会议结论' }); + expect(input.value).toBe('会议结论'); + + fireEvent.change(input, { target: { value: '会议结论:库位空调逻辑需要增加预设逻辑' } }); + expect(input.value).toBe('会议结论:库位空调逻辑需要增加预设逻辑'); + + fireEvent.change(input, { target: { value: '会议结论:库位空调逻辑需要增加预设逻辑,\\\\' } }); + expect(input.value).toBe('会议结论:库位空调逻辑需要增加预设逻辑,\\\\'); + expect(input.value.match(/\\/g)?.length).toBe(2); + }); + + it('发送出去的正文与输入框内容逐字一致(后端 JSON 编码不体现在界面文本上)', async () => { + const onSend = vi.fn().mockResolvedValue(true); + render(); + const input = getInput(); + + const typed = '路径 C:\\logs\\a.txt,反斜杠 \\\\'; + fireEvent.change(input, { target: { value: typed } }); + + await act(async () => { + // tdesign Button 不透传 aria-label,这里按发送按钮的类名定位 + const sendBtn = document.querySelector('.detail-chat-send') as HTMLButtonElement; + fireEvent.click(sendBtn); + }); + + expect(onSend).toHaveBeenCalledTimes(1); + const sentText = onSend.mock.calls[0][0] as string; + expect(sentText).toBe(typed); + expect(sentText.match(/\\/g)?.length).toBe(4); + }); + + it('「闪一下」的代码机制:超 500 字时输入框内容被整段替换为 #clipboard(N行) 记号', () => { + const onSend = vi.fn().mockResolvedValue(true); + render(); + const input = getInput(); + + // 600 字、无换行 → 记号按「行数」命名,故为 1 行 + const long = '库'.repeat(600); + fireEvent.change(input, { target: { value: long } }); + + // 输入框被换成记号(= 用户肉眼看到的「闪一下、内容突变」) + expect(input.value).toBe('#clipboard(1行)'); + // 原文进了待发送附件,不丢内容(输入框 + 附件芯片各一处同名记号) + expect(screen.getAllByText('#clipboard(1行)').length).toBe(2); + // 超限提示可见 + expect(screen.getByText(/超出 500 字限制/)).toBeInTheDocument(); + }); +});