mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 迁移
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 失败
169 lines
5.8 KiB
Go
169 lines
5.8 KiB
Go
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()
|
||
}
|