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) } }