Files
HomeAgent/internal/plugins/webui/test/plugin_status_render_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

73 lines
2.6 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
import (
"strings"
"testing"
)
// 插件列表「启动状态」渲染的回归判据(2026-10-20 生产反馈)。
//
// ## 原始问题
//
// 面板只拿内核 status 的 loaded 一个布尔来判断,于是三种完全不同的形态
// 被显示成同一个「未加载」:
//
// · auto_start=false 且未运行 → **按需插件正常待命**(不是故障)
// · auto_start=true 且未运行 → 该起没起(需要关注)
// · 运行态 plugin_stop 之后 → 用户自己停的(重启会恢复)
//
// 用户看到一屏「未加载」,以为插件全坏了 —— 实际它们只是在等 plugin_start。
//
// ## 判据思路
//
// 与 cmd_history_render_test.go 同款:不执行 JS,只守住源码结构 ——
// 渲染函数必须消费 auto_start / runtime_stopped / alive 三个字段,
// 并给出「按需待命」这一独立分支;否则回归立刻红。
func TestPluginStatusUsesRuntimeFields(t *testing.T) {
src := dashboardJSSource(t)
start := strings.Index(src, "function renderPlugins()")
if start < 0 {
t.Fatal("找不到 renderPlugins —— 函数被重命名或删除,请同步更新本判据")
}
end := strings.Index(src[start+1:], "\nfunction ")
var body string
if end < 0 {
body = src[start:]
} else {
body = src[start : start+1+end]
}
for _, want := range []string{
"auto_start", // 区分自启/按需
"runtime_stopped", // 区分运行态停止与持久禁用
"alive", // 区分「已加载」与「进程丢失」
"按需待命", // 按需插件待命是独立状态,不能混进「未运行」
} {
if !strings.Contains(body, want) {
t.Fatalf("renderPlugins 未引用 %q —— 状态判定退化,"+
"按需插件会被误显示成故障。见本文件头部说明", want)
}
}
}
// 「启动」按钮必须走运行态接口 /start,不能复用持久态的 /enable ——
// 后者会清掉持久禁用标记,让「点一下启动」变成「以后都自启」。
func TestPluginStartUsesRuntimeEndpoint(t *testing.T) {
src := dashboardJSSource(t)
i := strings.Index(src, "async function startPlugin(")
if i < 0 {
t.Fatal("找不到 startPlugin")
}
body := src[i:]
if j := strings.Index(body[1:], "\nasync function "); j > 0 {
body = body[:j+1]
}
if !strings.Contains(body, `"/plugins/" + encodeURIComponent(name) + "/start"`) {
t.Fatal("startPlugin 必须调用 /plugins/<name>/start(运行态)," +
"不得复用 /enable(持久态会改动开机行为)")
}
if strings.Contains(body, "\"/enable\"") {
t.Fatal("startPlugin 里出现了 /enable —— 运行态启动不得动持久禁用清单")
}
}