Files
HomeAgent/internal/plugins/webui/test/mascot_asset_test.go
JianFeeeee b8ec1e84d2 refactor(webui): 再迁 3 个判据到 test/ —— 去重辅助函数
## 迁移

    dashboard_js_scope_test.go  → webui/test/
    embed_read_test.go          → webui/test/
    mascot_asset_test.go        → webui/test/

这三个原先都直接读未导出的 `dashboardFS`(`go:embed` 的变量)。
上一批已经加了导出入口 `FrontendAsset(name)`,所以只需换调用即可,
不涉及行为改动。

## 迁移暴露的三处重复(一并去重)

迁过去之后编译报了三组重复定义:

    stripJSComments      helpers_test.go 与 dashboard_js_scope_test.go 各一份
    dashboardJSSource    helpers_test.go 与 dashboard_js_scope_test.go 各一份
    mustReadEmbedded     embed_read_test.go 里定义,kernel_buttons_render_test.go 在用

前两个是**迁移造成的镜像**:原本包内的辅助函数在迁到 test/ 时被复制了一份。
逐字比较确认两份 `stripJSComments` 实现完全相同,删掉包内那份、保留
`test/helpers_test.go` 里的共享版本。

第三个不同:`mustReadEmbedded` 只有包内的 `kernel_buttons_render_test.go` 在用。
把它改成直接调导出的 `FrontendAsset`,不再需要这个辅助函数 ——
包内测试从此也不再依赖"哪个变量装着 embed 资源"这种实现细节。

## 顺带清掉一批未使用导入

批量迁移+脚本改包名之后,多个文件留着已经不用的 `webui` 导入
(有的只在注释里提到 `webui.FrontendAsset`)。用 `go vet` 的输出逐个清理,
最后 `go vet ./internal/plugins/webui/...` 干净。

## 判据未失效(变异验证)

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

    --- FAIL: TestChatAckTimeoutIsGenerous
        chat_ack_timeout_test.go:59: ★ 受理超时只有 30000 ms —— 太小。

恢复后全绿。

## 进度

    webui/        180 条(原 186,迁走 6 条)
    webui/test/    22 条
    合计          202 条,与迁移前一致(+ 之前新增的契约判据)

`webui/` 剩下 20 个测试文件,全部依赖 `handleXxx` / `newTestHandler` /
`testSDK` 等未导出 handler 与装配辅助,需要先有导出入口才能迁。
其中 `proxy_test.go`(45 处)和 `handler_test.go`(90 处)是最大两块,
要迁必须先给 webui 导出"起一个带路由的 handler"的测试入口。

## 验证

    go build ./...                          PASS
    go vet ./internal/plugins/webui/...     PASS
    gofmt -l                                干净
    go test ./... -count=1                  65 包全过,0 FAIL
    webui 连跑 3 次                          0 失败
2026-11-04 20:23:34 +08:00

169 lines
5.8 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 (
"bytes"
"encoding/binary"
"errors"
"github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
"testing"
)
// 这个小测试钉住一件很具体、也很容易被无声破坏的事:
// **看板娘素材必须自带真 alpha,而不是「白底图」或「棋盘格假透明」。**
//
// 为什么值得一个判据:
//
// 历史上 mascot.webp 是不透明 WebP(白底 + 蓝紫渐变外框)。放在深色面板的
// 30px 圆形头像里,就是一个白方块;site/README.md 里记着因为「没法抠图」
// 最终干脆不放立绘。后来换素材时又出现过「棋盘格 JPEG」—— 那种图**看起来**
// 是透明的,实际像素是灰白格子,直接塞进仓库依然会露出格子。
//
// 这类退化在肉眼 review 里几乎抓不住(缩到 30px 就是一团浅色),但用户每天
// 都在看。所以用容器格式本身来判定:判据看的是 VP8X 的 Alpha 标志位与
// ALPH chunk 是否**一致存在**,而不是「渲染出来像不像透明」。
func TestMascotAssetHasRealAlpha(t *testing.T) {
data, err := webui.FrontendAsset("mascot.webp")
if err != nil {
t.Fatalf("读 mascot.webp: %v", err)
}
info, err := parseWebPInfo(data)
if err != nil {
t.Fatalf("mascot.webp 不是合法 WebP: %v", err)
}
if !info.hasAlphaFlag {
t.Errorf("mascot.webp 的 VP8X Alpha 标志位未置位(flags=0x%02x)——"+
"说明这张图没有真 alpha 通道,深色面板下会露出白底/棋盘格", info.vp8xFlags)
}
if !info.hasALPHChunk {
t.Errorf("mascot.webp 缺 ALPH chunk —— alpha 标志位与实际数据不一致")
}
// 尺寸:30px 圆形头像在 2x 屏上需要 60px;留够余量但别把大图塞进二进制。
if info.width != info.height {
t.Errorf("mascot.webp 应为正方形(圆形头像裁剪依赖它),实际 %dx%d", info.width, info.height)
}
if info.width < 128 {
t.Errorf("mascot.webp 只有 %dpx,2x/3x 屏上会糊(要求 >= 128)", info.width)
}
if len(data) > 128<<10 {
t.Errorf("mascot.webp 体积 %.1fKB 超过 128KB —— 它进二进制、每次请求都发,"+
"头像用途不需要这个量级", float64(len(data))/1024)
}
}
// 反向验证:把真实素材「退化」成无 alpha 的容器,确认判据会报红。
//
// 这条来自实测教训:形状类判据如果只喂真实数据,它可能对任何输入都绿。
// 这里不依赖外部工具,直接构造一个带 VP8X 但不置 Alpha 位的最小容器。
func TestMascotAssetHasRealAlpha_RejectsOpaqueContainer(t *testing.T) {
var chunks bytes.Buffer
payload := make([]byte, 10) // flags=0x00:Alpha 位为 0
payload[4], payload[5] = 0x7f, 0x00 // 宽 = 0x7f+1 = 128
payload[7], payload[8] = 0x7f, 0x00 // 高 = 0x7f+1 = 128
writeChunk(&chunks, "VP8X", payload)
info, err := parseWebPInfo(buildRIFF(chunks.Bytes()))
if err != nil {
t.Fatalf("构造的容器应能被解析: %v", err)
}
if info.hasAlphaFlag {
t.Fatal("容器本来就声明不透明,解析器却报了 Alpha —— 判据会对退化素材失效")
}
if info.hasALPHChunk {
t.Fatal("构造的容器里没有 ALPH chunk,解析器却说有")
}
}
type webpInfo struct {
width, height int
vp8xFlags byte
hasAlphaFlag bool
hasALPHChunk bool
}
var errNotWebP = errors.New("不是 RIFF/WEBP 容器")
// parseWebPInfo 只解析容器头,不解码像素 —— 判据要看的是「格式声明」,
// 不是「看起来透明」。纯标准库即可,不给依赖加负担。
func parseWebPInfo(data []byte) (*webpInfo, error) {
if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
return nil, errNotWebP
}
info := &webpInfo{}
// 遍历 RIFF chunk:4 字节 ID + 4 字节 LE 长度 + 负载(按 2 字节对齐)。
for off := 12; off+8 <= len(data); {
id := string(data[off : off+4])
size := int(binary.LittleEndian.Uint32(data[off+4 : off+8]))
payload := off + 8
if payload+size > len(data) {
break
}
switch id {
case "VP8X":
if size >= 10 {
info.vp8xFlags = data[payload]
info.hasAlphaFlag = info.vp8xFlags&0x10 != 0 // bit 4 = Alpha
// 画布尺寸存的是「实际值 - 1」,各 3 字节 LE。
info.width = int(readUint24LE(data[payload+4:])) + 1
info.height = int(readUint24LE(data[payload+7:])) + 1
}
case "ALPH":
info.hasALPHChunk = true
case "VP8 ":
// 无损/有损的「简单格式」(无 VP8X):宽高在帧头里。
// 这种文件必然没有 alpha —— 正是判据要拒的那种。
if size >= 10 && data[payload+3] == 0x9d && data[payload+4] == 0x01 && data[payload+5] == 0x2a {
info.width = int(binary.LittleEndian.Uint16(data[payload+6:]) & 0x3FFF)
info.height = int(binary.LittleEndian.Uint16(data[payload+8:]) & 0x3FFF)
}
case "VP8L":
if size >= 5 {
// 无损格式:signature 0x2f + 14bit 宽 + 14bit 高 + alpha_is_used 位
b := data[payload+1 : payload+5]
info.width = int(binary.LittleEndian.Uint16(b)&0x3FFF) + 1
info.height = int(binary.LittleEndian.Uint16(b[2:])&0x3FFF) + 1
if data[payload+4]&0x10 != 0 {
info.hasAlphaFlag = true
}
}
}
adv := payload + size
if size%2 == 1 {
adv++ // RIFF 要求 chunk 负载补齐到偶数
}
off = adv
}
if info.width == 0 || info.height == 0 {
return nil, errNotWebP
}
return info, nil
}
func readUint24LE(b []byte) int {
return int(b[0]) | int(b[1])<<8 | int(b[2])<<16
}
func writeChunk(buf *bytes.Buffer, id string, payload []byte) {
buf.WriteString(id)
var sz [4]byte
binary.LittleEndian.PutUint32(sz[:], uint32(len(payload)))
buf.Write(sz[:])
buf.Write(payload)
if len(payload)%2 == 1 {
buf.WriteByte(0)
}
}
func buildRIFF(chunks []byte) []byte {
var out bytes.Buffer
out.WriteString("RIFF")
var sz [4]byte
binary.LittleEndian.PutUint32(sz[:], uint32(4+len(chunks)))
out.Write(sz[:])
out.WriteString("WEBP")
out.Write(chunks)
return out.Bytes()
}