- B3:截图落盘后**自动**并入本轮视图(能收图 ⇒ 真 image 部件挂 user 消息;收不了 ⇒ vision/OCR 文字;
大图先经 image-crop 裁切;取走即消费、每会话 ≤4 张、TTL 10min、跨会话隔离)
- B4:browser 面板**声明**(配置表单 + 运行状态/指纹自检/最近截图三张只读表 + 拍一张/重新自检)
+ 六个动作走 PLG-2 的通用动作端点(**不开新路由**)+ 保存事务(不可用则回滚 409)+ 热重载
- 读面**不启动浏览器**(计数型假 dialer 证明 0 次拨号);行数据不含路径/URL/内容
- 3 个变异(短路主路/行与列 key 脱钩/保存不回滚)
⚠️ **两行接线未落**(不在我文件面):wire_runtime.go 的 TransformContext + wire_frontend.go 的
wireBrowserPanel —— 落地前 B3/B4 只是「组件就绪」,跑着的后端里不存在(Y4 已写盯梢用例)
146 lines
5.2 KiB
Go
146 lines
5.2 KiB
Go
package browser
|
||
|
||
// 本文件钉住 B4 面板的**驱动层事实面**(design/28 §8 B4;形状见 state.go):
|
||
// 取数**不启动浏览器**、最近截图统计有界且最新在前、指纹缓存是深拷。
|
||
//
|
||
// 「面板取数无副作用」这条是硬口径:面板的只读区块连 viewer 都能看(design/09 §5.4.1),
|
||
// 让「打开页面看一眼」顺手起一个 Chrome 是明确的越界。因此这里用**计数型假 dialer**
|
||
// 证明 `State()` 一次都没碰过启动路径(比「看起来没起」强)。
|
||
|
||
import (
|
||
"context"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/browsercfg"
|
||
)
|
||
|
||
// countingDialer 记录 Dial 调用次数:任何一次 `State()` 都不该让它 +1。
|
||
type countingDialer struct {
|
||
dials atomic.Int32
|
||
}
|
||
|
||
func (d *countingDialer) Dial(context.Context) (string, func(), error) {
|
||
d.dials.Add(1)
|
||
return "", func() {}, nil
|
||
}
|
||
|
||
func (d *countingDialer) Describe() string { return "counting-dialer" }
|
||
func (d *countingDialer) Local() bool { return false }
|
||
|
||
// newStateFixture 造一个带可用 dialer 的管理器(配置走默认值)。
|
||
func newStateFixture(t *testing.T) (*Manager, *countingDialer) {
|
||
t.Helper()
|
||
m := NewManager(Options{
|
||
Availability: browsercfg.Availability{
|
||
Usable: true, Source: browsercfg.SourceLocalChrome, Detail: "fixture", Headless: true,
|
||
},
|
||
Config: browsercfg.Defaults(`C:\data`),
|
||
DataDir: `C:\data`,
|
||
})
|
||
t.Cleanup(func() { _ = m.Close() })
|
||
d := &countingDialer{}
|
||
m.d = d
|
||
return m, d
|
||
}
|
||
|
||
// TestManagerState_ReadsFactsWithoutLaunching 钉住「取数无副作用」:State() 只读字段,
|
||
// 一次都不碰启动路径。
|
||
func TestManagerState_ReadsFactsWithoutLaunching(t *testing.T) {
|
||
t.Parallel()
|
||
m, d := newStateFixture(t)
|
||
for i := 0; i < 3; i++ {
|
||
snap := m.State()
|
||
if !snap.Available {
|
||
t.Fatalf("可用来源下 Available 应为 true:%+v", snap)
|
||
}
|
||
if snap.SessionLive {
|
||
t.Fatalf("State() 建出了会话:%+v", snap)
|
||
}
|
||
if snap.Headless != browsercfg.DefaultHeadless {
|
||
t.Errorf("Headless = %v, want %v", snap.Headless, browsercfg.DefaultHeadless)
|
||
}
|
||
if snap.ViewportWidth != browsercfg.DefaultViewportWidth || snap.ViewportHeight != browsercfg.DefaultViewportHeight {
|
||
t.Errorf("视口 = %dx%d", snap.ViewportWidth, snap.ViewportHeight)
|
||
}
|
||
if snap.Concurrency != browsercfg.DefaultConcurrency {
|
||
t.Errorf("并发上限 = %d, want %d(profile 锁)", snap.Concurrency, browsercfg.DefaultConcurrency)
|
||
}
|
||
if snap.Shots != nil {
|
||
t.Errorf("没有截图时 Shots 应为空:%+v", snap.Shots)
|
||
}
|
||
}
|
||
if got := d.dials.Load(); got != 0 {
|
||
t.Fatalf("State() 触发了 %d 次拨号(面板取数必须无进程外副作用)", got)
|
||
}
|
||
}
|
||
|
||
// TestManagerState_ShotRingIsBoundedAndNewestFirst 钉住最近截图统计的形状:
|
||
// 有界(不随运行时间无界增长)且最新在前(面板第一行是「刚才那一下」)。
|
||
func TestManagerState_ShotRingIsBoundedAndNewestFirst(t *testing.T) {
|
||
t.Parallel()
|
||
m, _ := newStateFixture(t)
|
||
base := time.Date(2026, 10, 6, 21, 0, 0, 0, time.UTC)
|
||
for i := 0; i < maxShotRecords+5; i++ {
|
||
m.mu.Lock()
|
||
m.recordShotLocked(Shot{Bytes: i + 1, Width: 8, Height: 8, DimensionsKnown: true, Format: "png"}, base.Add(time.Duration(i)*time.Second))
|
||
m.mu.Unlock()
|
||
}
|
||
shots := m.State().Shots
|
||
if len(shots) != maxShotRecords {
|
||
t.Fatalf("保留条数 = %d, want %d", len(shots), maxShotRecords)
|
||
}
|
||
if shots[0].Bytes != maxShotRecords+5 {
|
||
t.Fatalf("最新的一条不在最前:首条字节 = %d", shots[0].Bytes)
|
||
}
|
||
for i := 1; i < len(shots); i++ {
|
||
if !shots[i-1].At.After(shots[i].At) {
|
||
t.Fatalf("行序不是最新在前:%d 与 %d", i-1, i)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestManagerState_FingerprintIsCachedByValue 钉住指纹缓存是**深拷**:调用方改了返回的
|
||
// 读数不该改到管理器里的那一份(否则面板显示会被别人的改动悄悄污染)。
|
||
func TestManagerState_FingerprintIsCachedByValue(t *testing.T) {
|
||
t.Parallel()
|
||
m, _ := newStateFixture(t)
|
||
m.recordFingerprint(Fingerprint{Webdriver: false, Languages: []string{"zh-CN", "en"}, Timezone: "Asia/Shanghai"})
|
||
first := m.State()
|
||
if first.Fingerprint == nil || first.FingerprintAt.IsZero() {
|
||
t.Fatalf("指纹没被记下来:%+v", first)
|
||
}
|
||
first.Fingerprint.Languages[0] = "mutated"
|
||
if got := m.State().Fingerprint.Languages[0]; got != "zh-CN" {
|
||
t.Fatalf("指纹缓存被外部改动污染了:%q", got)
|
||
}
|
||
}
|
||
|
||
// TestManagerState_ClosedIsReported 钉住「已关闭」如实上报(面板据此显示「已关闭」而不是
|
||
// 假装还可用 —— 关闭之后任何动作都会报 ErrNoSession)。
|
||
func TestManagerState_ClosedIsReported(t *testing.T) {
|
||
t.Parallel()
|
||
m, _ := newStateFixture(t)
|
||
if err := m.Close(); err != nil {
|
||
t.Fatalf("Close: %v", err)
|
||
}
|
||
snap := m.State()
|
||
if !snap.Closed {
|
||
t.Fatalf("关闭后 Closed = false:%+v", snap)
|
||
}
|
||
if snap.SessionLive {
|
||
t.Error("关闭后不该报会话活着")
|
||
}
|
||
}
|
||
|
||
// TestManagerState_NilManagerIsSafe 钉住 nil 管理器的兜底(面板在「工具没注册」那条路上
|
||
// 仍要取事实)。
|
||
func TestManagerState_NilManagerIsSafe(t *testing.T) {
|
||
t.Parallel()
|
||
var m *Manager
|
||
if snap := m.State(); snap.Available || snap.Closed {
|
||
t.Fatalf("nil 管理器的快照 = %+v", snap)
|
||
}
|
||
}
|