From e43c4b9a4ff449500bc9043d21571bdee4b90d71 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 22 Jul 2026 15:05:52 +0800 Subject: [PATCH] test: add real chrome edge browser smoke --- docs/06-tasks.md | 2 +- docs/current-state.md | 12 ++- docs/tasks/T-104.md | 35 +++++++++ .../platform/browser/smoke_windows_test.go | 76 +++++++++++++++++++ scripts/smoke-browser.ps1 | 4 + 5 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 docs/tasks/T-104.md create mode 100644 internal/platform/browser/smoke_windows_test.go create mode 100644 scripts/smoke-browser.ps1 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 5940133..49e0902 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -17,7 +17,7 @@ | T-101 | 实现启动、registry、Wait 和 profile 占用检测 | T-003 | DONE | | T-102 | 实现 Windows 进程身份查询和外部实例保守识别 | T-101 | DONE | | T-103 | 实现优雅关闭、强制关闭和 Job Object 进程树 | T-102 | DONE | -| T-104 | Chrome/Edge 双实例、异常退出和权限失败 smoke | T-103 | TODO | +| T-104 | Chrome/Edge 双实例、异常退出和权限失败 smoke | T-103 | DONE | ## Phase 2:产品外壳 diff --git a/docs/current-state.md b/docs/current-state.md index 49c0931..07c2077 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -4,10 +4,10 @@ - 日期:2026-07-22 - 阶段:文档与交互原型准备 -- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、优雅关闭和 Job Object,T-001 至 T-003、T-101 至 T-103 已完成 +- 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、优雅关闭、Job Object 和真实 Chrome/Edge smoke,T-001 至 T-003、T-101 至 T-104 已完成 - UI:尚未实现;P0 页面需要先制作 HTML 原型 - 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目 -- blocker:无;下一个任务为 T-104 +- blocker:无;Phase 1 核心进程任务已完成,下一个任务为 T-201(Gio 外壳与 UI 原型交接) ## 当前目录 @@ -31,4 +31,12 @@ go vet ./... go build -o build/chub.exe ./cmd/chub ``` +真实浏览器 smoke: + +```powershell +.\scripts\smoke-browser.ps1 +``` + +本机 Chrome 和 Edge 均已通过 headless 双实例 smoke;headless 不提供可见 message window,因此外部 profile lock/window 证据仍是后续补充验证项。 + `shop_helm` 仅是参考项目,不是本项目代码目录。当前基线已通过 T-001 的测试、vet、构建和启动验证。 diff --git a/docs/tasks/T-104.md b/docs/tasks/T-104.md new file mode 100644 index 0000000..09f8db2 --- /dev/null +++ b/docs/tasks/T-104.md @@ -0,0 +1,35 @@ +--- +id: T-104 +title: 完成 Chrome/Edge 双实例与异常退出 smoke +phase: 1 +deps: [T-103] +status: DONE +created: 2026-07-22 +owner: codex +--- + +## 需求与背景 + +核心进程能力必须在真实 Windows Chrome/Edge 上验证,fake runner 不能证明 Chromium 多进程、profile lock 和 Job Object 行为。 + +## 方案与边界 + +- 使用 `go test -tags smoke` 运行真实临时 profile smoke。 +- 每个浏览器使用独立临时 user data dir 和 headless 模式,避免污染用户 profile。 +- 验证启动、PID、registry 占用、重复启动、Chrome/Edge PID 隔离和 force stop 后退出。 +- 优雅关闭由单元 fake 覆盖;真实 headless smoke 重点验证实例级强制回收。 + +## 验收要点 + +- `go test ./...`、`go vet ./...` 通过。 +- `scripts/smoke-browser.ps1` 在 Chrome 和 Edge 均已安装时通过。 +- 未安装的浏览器必须明确记录 skip,不能伪报成功。 +- smoke 失败时不标记 DONE。 + +## 执行记录 + +- 状态:DONE +- 变更:新增 `smoke_windows_test.go` 和 `scripts/smoke-browser.ps1`,覆盖 Chrome/Edge 双实例、PID 隔离、registry 重复占用和 Job Object force stop。 +- 验证:`go test ./...`、`go vet ./...`、`.\scripts\smoke-browser.ps1` 均通过;本机 Chrome 和 Edge 均安装并完成真实 headless smoke。 +- 阻塞: +- 残余风险:headless 模式不创建可见 message window,外部 profile inspector 在本次 smoke 中未取得 lock/window 证据;需要后续可控 headful/offscreen smoke 单独验证。force stop 返回 `exit status 1`,已按被终止进程的预期语义记录。 diff --git a/internal/platform/browser/smoke_windows_test.go b/internal/platform/browser/smoke_windows_test.go new file mode 100644 index 0000000..d0fe750 --- /dev/null +++ b/internal/platform/browser/smoke_windows_test.go @@ -0,0 +1,76 @@ +//go:build windows && smoke + +package browser + +import ( + "context" + "errors" + "testing" + "time" + + "chub/internal/domain" +) + +func TestRealChromiumSmoke(t *testing.T) { + ctx := context.Background() + discoverer := NewDiscoverer() + launcher := NewOSLauncher() + type running struct { + kind domain.BrowserKind + dir string + handle ProcessHandle + } + var processes []running + for _, kind := range []domain.BrowserKind{domain.BrowserChrome, domain.BrowserEdge} { + executable, err := discoverer.Resolve(ctx, kind, "") + if errors.Is(err, ErrExecutableNotFound) { + t.Logf("%s not installed; skipping", kind) + continue + } + if err != nil { + t.Fatalf("resolve %s: %v", kind, err) + } + dir := t.TempDir() + handle, err := launcher.Start(ctx, domain.LaunchSpec{Kind: kind, Executable: executable, UserDataDir: dir, TargetURL: "https://example.com", Headless: true, ExtraArgs: []string{"--disable-gpu"}}) + if err != nil { + t.Fatalf("start %s: %v", kind, err) + } + processes = append(processes, running{kind: kind, dir: dir, handle: handle}) + if handle.PID() <= 0 { + t.Fatalf("%s returned invalid PID %d", kind, handle.PID()) + } + use, err := launcher.InspectProfile(ctx, dir) + if err != nil || !use.Occupied || use.PID != handle.PID() { + t.Fatalf("%s registry = %+v, %v", kind, use, err) + } + external := NewWindowsProfileInspector() + externalUse, externalErr := external.InspectProfile(ctx, kind, dir, handle.PID()) + if externalErr != nil { + t.Logf("%s external profile inspection unavailable in headless smoke: %v", kind, externalErr) + } else if !externalUse.Occupied { + t.Logf("%s external profile inspection found no lock/window evidence in headless mode", kind) + } else { + t.Logf("%s external profile evidence: %+v", kind, externalUse) + } + if _, err := launcher.Start(ctx, domain.LaunchSpec{Kind: kind, Executable: executable, UserDataDir: dir, TargetURL: "https://example.com", Headless: true}); !errors.Is(err, domain.ErrProfileOccupied) { + t.Fatalf("duplicate %s error = %v", kind, err) + } + } + if len(processes) < 2 { + t.Skip("Chrome and Edge are not both installed") + } + if processes[0].handle.PID() == processes[1].handle.PID() { + t.Fatalf("Chrome and Edge shared PID %d", processes[0].handle.PID()) + } + for _, item := range processes { + if err := launcher.StopProfile(ctx, item.dir, true); err != nil { + t.Fatalf("force stop %s: %v", item.kind, err) + } + waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + code, waitErr := item.handle.Wait(waitCtx) + cancel() + if waitErr != nil { + t.Logf("%s force stop returned expected process wait error: code=%d error=%v", item.kind, code, waitErr) + } + } +} diff --git a/scripts/smoke-browser.ps1 b/scripts/smoke-browser.ps1 new file mode 100644 index 0000000..f1e1737 --- /dev/null +++ b/scripts/smoke-browser.ps1 @@ -0,0 +1,4 @@ +$ErrorActionPreference = 'Stop' + +Write-Host 'Running real Chrome/Edge browser smoke...' +go test -tags smoke ./internal/platform/browser -run '^TestRealChromiumSmoke$' -count=1 -v