package browser // 本文件只放「失败怎么分类/怎么报」这一类判据(实现见 launch.go 的 budgetLaunchErr / // portTimeoutErr 与 lift 里的三条出口)。它们与 launch_test.go 的其余 Dial 用例分开, // 一是为了每个文件都不超 AGENTS §10 的 500 行上限,二是让「三类失败必须一眼可分」这一组 // 判据集中在一处可读。 import ( "context" "errors" "log/slog" "strings" "testing" "time" ) // TestDial_CallerDeadlineIsReportedAsBudget 钉住**真机那条路**:等待期间调用方的截止 // 时间到了(这一轮的工具预算烧完),错误必须读成「预算耗尽、浏览器没坏」,而不是 // 「等待调试端口时被取消」这种既像浏览器故障、又说不清谁取消的文案。 func TestDial_CallerDeadlineIsReportedAsBudget(t *testing.T) { t.Parallel() log, buf := newCapturedLog(slog.LevelDebug) l := newFakeChrome(t, fakeBrowserScript{Stderr: []string{"浏览器在等,但没人给它时间"}, SleepMS: 5000}, 10*time.Second, log) ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() _, _, err := l.Dial(ctx) if err == nil { t.Fatal("调用方截止时间到之后 Dial 必须报错") } if !errors.Is(err, ErrLaunch) { t.Fatalf("错误必须仍然包住 ErrLaunch: %v", err) } if !strings.Contains(err.Error(), launchMarkerBudget) { t.Errorf("错误必须读成「预算耗尽」,实得: %v", err) } if strings.Contains(err.Error(), launchMarkerPortTimeout) { t.Errorf("调用方预算先到时不许报成我们自己的端口等待超时: %v", err) } if !containsInLog(buf.String(), l.profileDir) { t.Errorf("这条失败也要带 argv(否则现场没法复现):\n%s", buf.String()) } } // TestDial_ThreeLaunchFailuresAreDistinguishable 钉住「一眼分辨」:三条出口 —— // ① 进程立刻退出 / ② 我们自己的端口等待超时 / ③ 调用方预算耗尽 —— 各自的错误文案里 // 必须带**互不相同**的标记,谁也别冒充谁。 func TestDial_ThreeLaunchFailuresAreDistinguishable(t *testing.T) { t.Parallel() cases := []struct { name string script fakeBrowserScript timeout time.Duration ctx func() (context.Context, context.CancelFunc) wantMarker string forbid string }{ { name: "①进程立刻退出", script: fakeBrowserScript{Stderr: []string{"参数被拒,进程退出"}, ExitCode: 1}, timeout: 5 * time.Second, ctx: func() (context.Context, context.CancelFunc) { return context.WithCancel(context.Background()) }, wantMarker: launchMarkerProcessExited, forbid: launchMarkerBudget, }, { name: "②等端口就绪超时", script: fakeBrowserScript{Stderr: []string{"起来了但从不答 CDP"}, SleepMS: 5000}, timeout: 300 * time.Millisecond, ctx: func() (context.Context, context.CancelFunc) { return context.WithCancel(context.Background()) }, wantMarker: launchMarkerPortTimeout, forbid: launchMarkerBudget, }, { name: "③调用方预算耗尽", script: fakeBrowserScript{Stderr: []string{"还没就绪预算就没了"}, SleepMS: 5000}, timeout: 10 * time.Second, ctx: func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 200*time.Millisecond) }, wantMarker: launchMarkerBudget, forbid: launchMarkerPortTimeout, }, } seen := map[string]string{} for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { l := newFakeChrome(t, tc.script, tc.timeout, discardLog()) ctx, cancel := tc.ctx() defer cancel() _, _, err := l.Dial(ctx) if err == nil { t.Fatal("这一档必须报错") } msg := err.Error() t.Logf("%s 实测错误:%v", tc.name, err) if !strings.Contains(msg, tc.wantMarker) { t.Errorf("错误里应带标记 %q:%v", tc.wantMarker, err) } if strings.Contains(msg, tc.forbid) { t.Errorf("错误里不该出现 %q(冒充了另一类失败):%v", tc.forbid, err) } if prev, dup := seen[tc.wantMarker]; dup { t.Errorf("标记 %q 与 %q 撞了 —— 三种情形必须可分", tc.wantMarker, prev) } seen[tc.wantMarker] = tc.name }) } }