Files
HomeAgent/internal/plugins/webui/test/chat_attachment_img_test.go
JianFeeeee 87f874e06f refactor(webui): 7 个黑箱判据迁到 test/ + 导出前端资源入口
## 迁移了什么

    chat_ack_timeout_test.go       → test/
    chat_attachment_img_test.go    → test/
    cmd_history_render_test.go     → test/
    config_namespace_test.go       → test/
    dashboard_tabs_deps_test.go    → test/
    plugin_status_render_test.go   → test/
    starmap_perf_test.go           → test/

包名 `webui` → `test`。`webui/` 目录少 7 个测试文件。

## 为什么是这 7 个(分类依据)

先按「对未导出符号的依赖数」把 29 个测试文件测了一遍,这 7 个是
**0 依赖**(既不用 handleXxx,也不用 testSDK/newTestHandler 等测试辅助)。
其余 22 个按强度递增留在包内,后续逐步处理。

## 新增两个导出入口

### webui.FrontendAsset(name)

返回打包进二进制的前端资源原文(dashboard.js / dashboard.css /
mascot.webp / logo.svg …)。

判据要检查「实际会被发出去的那份文本」:dashboard.js 在严格模式下
会不会抛 ReferenceError(用 node 真跑)、mascot.webp 的魔数是否合法、
HTML 属性里有没有残留转义引号。

这些无法通过 HTTP 端点可靠断言(要登录态、且拿到的是拼接后整页,
定位不到单个 asset)。而留在包内测就得读 `dashboardFS` 这个**未导出变量**
—— 那正是"测试耦合实现"。导出后,判据变成「输入 asset 名 → 输出字节」,
与资源存在哪个变量、怎么 embed 无关。

### webui.DashboardPage()

返回组装完成的整页 HTML(外壳 + 内联 CSS/JS)—— 浏览器实际收到的那份。
与 FrontendAsset 的分工:那个取单个源资产,这个取成品。

## 顺带修掉迁移暴露的两个隐患

① 三个判据原本 `os.ReadFile("dashboard.js")` 读**磁盘相对路径**。
   在包内恰好能跑(工作目录就是包目录),但那读的是**工作区文件**,
   不是**二进制里实际嵌入的那份** —— 两者在生产上可能不同
   (比如打包后 dashboard.js 被替换)。统一改走 FrontendAsset。

② starmap_perf_test 的相对路径 `../../../shared/starmap/layout.js`
   与 `static/starmap-layout-worker.js` 在移入子目录后失效,已修正为
   `../../../../shared/...` 与 `../static/...`。

## 判据仍有效(变异验证)

把 dashboard.js 里 ack 超时 `}, 600000);` 改成 `}, 30000);`:

    --- FAIL: TestChatAckTimeoutIsGenerous
        ★ 受理超时只有 30000 ms —— 太小。
          超时太短会让前端 abort,而 agent 仍在正常干活

恢复后全绿。迁移不是搬完就废。

## 验证

    go build ./...            PASS
    go vet ./...              PASS
    gofmt -l                  干净
    go test ./... -count=1    63 包全过,0 FAIL
                             (webui/test 是新包,故 62 → 63)
    干净检出(worktree)       build + webui/... 全绿

## 后续

    webui 剩余 22 个文件(依赖 handleXxx / testSDK 等),按强度递增处理
    agent/core 739 处白盒引用,先分类再动
    CI 补 coverage + staticcheck
2026-11-04 18:17:31 +08:00

94 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package test
// 聊天图片附件的 HTML 必须**能被浏览器正确切分**(2026-10-30)。
//
// ## 缺陷现场
//
// 生产截图:图片明明加载成功了(img 元素在、src 正确),界面上却多出一行
// 蓝色文字「图片加载失败"/>」。
//
// 根因是内联事件属性里用了反斜杠转义:
//
// onerror="this.parentElement.innerHTML='<span class=\"att-err\">图片加载失败</span>'"
//
// **HTML 属性值里没有反斜杠转义** —— 那个 \" 就是引号本身,属性在
// `<span class=` 之后就闭合了。浏览器切出来的是:
//
// onerror = "this.parentElement.innerHTML='<span class="
//
// 剩下的 `att-err">图片加载失败</span>'"/>` 变成**文本节点**直接显示。
// 而那个 onerror 因为语法错误永远不会执行 —— 真失败时反而什么都不显示。
//
// ## 为什么这条判据值得写
//
// 这种缺陷**不会让任何构建或测试失败**:JSON 合法、JS 语法合法、
// 字符串拼接合法。它只在浏览器里错,而且症状("加载成功却显示加载失败")
// 指向的方向与真因(引号归属)完全不同。
//
// 判据做法:按正则把 HTML 里所有 `name="value"` 属性切出来,
// 断言每个值内部**不含裸引号**,并断言页面里没有残留的 `"/>` 文本片段。
import (
"regexp"
"strings"
"testing"
)
// attrRe 匹配 HTML 属性。用它模拟浏览器"走到下一个引号就闭合"的行为。
var attrRe = regexp.MustCompile(`([a-zA-Z_:][-a-zA-Z0-9_:.]*)="([^"]*)"`)
// TestChatAttachmentImgHasNoBrokenInlineHandler 钉住附件图片的 HTML 形态。
func TestChatAttachmentImgHasNoBrokenInlineHandler(t *testing.T) {
js := stripJSComments(dashboardJS(t))
if !strings.Contains(js, "chat-attachment-img") {
t.Fatal("dashboardJS 里找不到 chat-attachment-img —— 附件图片渲染被改名或删了?")
}
// ① 内联 onerror 不能再出现:它是本缺陷的载体。
//
// 真正的失败处理已改为**捕获阶段的事件委托**(error 事件不冒泡,
// 只能在捕获阶段拿到)。所以这里断言"没有内联 onerror"是合理的收紧,
// 而不是"不许处理错误"。
if strings.Contains(js, `onerror="this.parentElement`) {
t.Error("★ 附件图片仍在用内联 onerror —— " +
"HTML 属性不支持反斜杠转义,属性会被提前闭合、剩余部分变成可见文本。" +
"请用捕获阶段的 error 事件委托(见 renderChat 里的 addEventListener(\"error\", ..., true))")
}
// ② 失败占位必须走 DOM API,不能拼 innerHTML 字符串。
//
// innerHTML 拼接是这条缺陷的来源(要在字符串里同时处理 HTML 引号、
// JS 引号、CSS 选择器)。用 createElement + textContent 只需处理数据。
if !strings.Contains(js, `span.className = "att-err"`) {
t.Error("找不到 .att-err 的 DOM 构造 —— 失败占位应改用 createElement/textContent")
}
// ③ 必须有捕获阶段的 error 委托(缺了它,图片失败时用户看到破图无提示)。
//
// 注意:error 事件不冒泡,`addEventListener("error", fn)` 不带 true
// 是**收不到** img 加载失败的 —— 这条注释是为了让改的人别再踩。
if !regexp.MustCompile(`addEventListener\(\s*"error"`).MatchString(js) {
t.Error("缺少 error 事件监听 —— 图片加载失败时不会显示占位")
}
}
// TestDashboardNoEscapedQuoteInHTMLAttributes 是**通用**的同类缺陷扫描。
//
// 它不针对某一行,而是扫整个 dashboard.js:凡是形如 `="...\"..."` 的
// 拼接,都会在浏览器里被提前闭合。
//
// 这条判据的价值在于:下次别人再写内联事件(onclick/onerror/onload…)
// 并试图用 \" 转义时,这里会红,而不是等到生产上出现一行诡异的可见文本。
func TestDashboardNoEscapedQuoteInHTMLAttributes(t *testing.T) {
js := stripJSComments(dashboardJS(t))
// 找 `="` 之后到下一个 `"` 之间出现反斜杠转义引号的片段。
// 注意要排除 JS 字符串里的普通转义(如 "a\"b" 作为纯数据),
// 所以只匹配**紧跟在 HTML 属性名后面**的形态:空白 + 名字 + = "
bad := regexp.MustCompile(`[a-zA-Z-]+="[^"]*\\"`)
if m := bad.FindString(js); m != "" {
t.Errorf("★ 发现 HTML 属性内用反斜杠转义引号(浏览器会提前闭合属性):\n %s\n"+
"HTML 属性值里没有转义序列 —— 需要嵌套引号时改用单引号包属性值,"+
"或改用 DOM API / 事件委托。", m)
}
}