aps-agent/docs/development/e2e.md

214 lines
13 KiB
Markdown
Raw Permalink Normal View History

# 前端 E2E 冒烟 · Sidecar 本地冒烟 · CI 构建矩阵
> 对应验收矩阵(`docs/product/plan-completion-matrix.md`):
> - 第 118 行「黄金测试是发布门禁」剩余项——完整前端 E2E、干净离线机安装冒烟、CI 构建矩阵;
> - 第 99 行「自包含 Python Sidecar」剩余本地可测项——20 次冷启动、端口冲突、崩溃重启、中文/空格路径。
>
> 本轮交付的本地可测部分全部落地并跑通;**签名 / SBOM 上链 / Defender / 干净离线机安装** 属外部环境项,
> 需签名证书、Defender 门户与离线验证机,见文末「剩余项与门禁说明」。
---
## 1. 前端 E2E 冒烟(Playwright)
### 1.1 运行前提
- Node ≥ 22(仓库根 `package.json` engines);需要 JMS 的用例必须通过环境变量提供隔离测试账号,禁止把账号密码写入仓库。
- 首次运行先装浏览器二进制(约 150 MB,需网络):
```powershell
cd apps/web
npm run test:e2e:install # npx playwright install chromium
```
### 1.2 一键运行(本地门禁)
```powershell
cd apps/web
npm run test:e2e # = npm run build && playwright test(preview 模式,推荐)
```
- 默认 **preview 模式**:`npm run build` 产出 `dist/` 后由 Playwright 起 `vite preview`(独立端口 43117),
并通过 `VITE_API_TARGET=http://127.0.0.1:8003` 把 `/api` 代理到本地后端。
- **为什么用 preview 而不是 dev**:preview 走生产构建,行为最接近发布包,作为 E2E 默认门禁。
round-41 发现的 dev 模式 StrictMode 缺陷(`refreshWorld` 结果捕获 + `alive` 存活标记被 StrictMode
模拟卸载永久置 false,导致 `world`/`resultStore` 更新被吞、视口不出现)已在 **round-42 修复**:
① `alive` effect 体内复位(mount→cleanup→mount 后保持 true);② `captureSessionResult` 移出
`setWorld` 更新器(纯函数化,经 `worldRef` 计算合并世界)。dev 模式(E2E_MODE=dev)已作为附加
冒烟验证通过;E2E 默认仍为 preview,CI 亦然。
- `E2E_MODE=dev` 仅作本地调试用:Playwright 会自动复用已运行的 5173(`reuseExistingServer`)。
### 1.3 覆盖清单(中文断言)
| 用例 | 断言 |
| --- | --- |
| 访客冒烟·品牌与登录入口 | `.sidebar-brand` 含「工业智核」;登录触发按钮可用 |
| 访客冒烟·登录弹窗 | 弹窗标题「登录工业智核」;企业/账号/密码三字段;Esc 可关闭 |
| 核心路径(JMS) | 使用 `E2E_JMS_*` 注入的测试账号登录 → 输入「跑一版柔性排产」→ 智能体中文回复非空 → `.flex-block-title` 含「柔性排产」→ 结果入口可用 → 视口打开(页签含「世界对比」,点击后 `.world-diff` 可见)→ 右侧「柔性工作台」打开 `.flex-bench` → Ctrl+K 命令面板:中文过滤「世界」命中「两个世界对比」、过滤「柔性」命中「柔性工作台」,Esc 关闭 |
### 1.4 常用变量
| 环境变量 | 默认 | 说明 |
| --- | --- | --- |
| `E2E_MODE` | `preview` | `preview`(build 后 vite preview,默认门禁)或 `dev`(vite dev;round-42 起 dev StrictMode 缺陷已修复,可作附加冒烟) |
| `E2E_FRONTEND_PORT` | `43117`(preview)/ `5173`(dev) | 前端端口(dev 模式复用已运行 5173) |
| `E2E_API_TARGET` | `http://127.0.0.1:8003` | `/api` 代理目标(CI 指向矩阵自起后端端口) |
| `E2E_BASE_URL` | `http://localhost:<port>` | 覆盖前端地址 |
| `E2E_OUTPUT_DIR` | 系统临时目录 | 截图/追踪产物目录(默认不污染仓库) |
| `APS_E2E_SKIP_JMS` | 空 | `1` 时跳过核心路径(无外网 JMS 场景),只跑离线访客冒烟 |
| `E2E_JMS_TENANT/USER/PASSWORD` | 无默认值,必须由本地安全环境或 CI Secret/Variable 注入 | JMS 登录参数 |
### 1.5 离线/无外网回退
- 若运行器无法访问 `JMS_AUTH_BASE_URL` 指向的 JMS 服务,设 `APS_E2E_SKIP_JMS=1`:
核心路径自动 skip,访客冒烟(2 例,不依赖后端)仍作为最小离线冒烟门禁。
- 浏览器二进制无法下载时:仍需已装 Chromium 才能跑访客冒烟;Chromium 二进制下载域名 `cdn.playwright.dev` 需可达。
---
## 2. Sidecar 本地冒烟(矩阵 99 本地部分)
```powershell
node scripts/smoke-sidecar-local.mjs
```
只读调用 `server/sidecar.py`(`python -m server.sidecar`)与桌面 Sidecar 生命周期语义,**不修改后端逻辑**;
每次启动都使用隔离的临时 `APS_HOME / APS_DB_PATH / APS_KNOWLEDGE_PATH / APS_APPROVAL_PATH / APS_WORLD_PATH`,
不触碰仓库 `server/data` 或 `~/.aps`。任一检查失败退出码非 0。
| 检查 | 预期 |
| --- | --- |
| 20 次冷启动 | 随机端口 + 随机 nonce 起/停,健康探针(nonce 校验 200 + 回显头)通过 |
| 端口冲突 | 预占端口后启动 → 显式失败(退出码非 0,stderr 含 WinError 10048 / address in use) |
| 崩溃重启 | kill sidecar 子进程后父进程(脚本模拟)以新端口+新 nonce 拉起新实例 |
| 父进程看门狗 | 父进程退出后 sidecar 由守护线程自行退出(exit 0) |
| 中文/空格路径 | 在含中文+空格的 cwd / APS_HOME / APS_UI_DIR 下启动,健康 + UI 均正常,无 Traceback |
可选 `APS_SIDECAR_PYTHON=<python>` 指定解释器;默认 `.sidecar-venv` → `.venv` → PATH。
「干净 Win10/11 离线机安装冒烟」需外部验证机,不在本地冒烟范围。
---
## 3. CI 构建矩阵(`.github/workflows/ci.yml`)
`push` / `pull_request` 触发,5 个 job:
| Job | 平台 | 内容 |
| --- | --- | --- |
| `lint` | Ubuntu | `ruff check server tests scripts`;**informational**(基线 55 项存量告警,round-41 记录;清零后改硬门禁) |
| `frontend-build` | Windows + Ubuntu | `apps/web`:`npm ci` → `npm run build`(tsc + vite) |
| `backend-tests` | Windows + Ubuntu | `pip install -r requirements.txt pytest` → `pytest tests/golden -q`;`APS_KNOWLEDGE_PATH` 等全部隔离到 `runner.temp` |
| `e2e` | Ubuntu | 起后端(独立端口 8100、隔离数据、jms)→ `npm ci` + build → `playwright install --with-deps chromium` → `npx playwright test`(preview 模式);失败自动上传 `aps-e2e-artifacts` |
| `sbom` | Ubuntu | `pip install uv` 提供 `uvx` → `npm run sbom` → 校验 `dist/sbom/*.cdx.json` 与 `release-manifest.json` |
CI 变量/密钥:
- `APS_E2E_SKIP_JMS`(仓库变量,可选):`1` 时 E2E 只跑离线访客冒烟。
- `APS_JMS_SESSION_SECRET`(仓库密钥,可选):JMS 会话签名密钥;未配置时用内置 CI 回退值。
- JMS 登录依赖运行器可访问配置的认证服务;若企业侧 IP 白名单限制 GitHub 出口,按上条降级。
---
## 3.5 数据隔离(C-P0 fail-closed)
核心路径 E2E 会在目标后端创建真实会话(会话标题=首条消息,如「柔性排产」)。
`apps/web/playwright.config.ts` 因此默认拒绝 `127.0.0.1:8003` / `localhost:8003`:该端口是现场开发后端,
直接复用会污染当前用户的项目、会话和排产结果。
### 唯一支持的本地启动方式
在仓库根目录新开 PowerShell,完整执行下面这一段;不要删减路径变量,也不要复用已经运行的 8003:
```powershell
$root = Join-Path ([System.IO.Path]::GetTempPath()) ("aps-e2e-" + [guid]::NewGuid().ToString("N"))
$home = Join-Path $root "home"
$data = Join-Path $home "data"
New-Item -ItemType Directory -Force -Path $data | Out-Null
$env:APS_HOME = $home
$env:APS_DATA_DIR = $data
$env:APS_DB_PATH = Join-Path $data "master.db"
$env:APS_WORLD_PATH = Join-Path $data "world.json"
$env:APS_PROJECTS_PATH = Join-Path $home "sessions\workspace.json"
$env:APS_KNOWLEDGE_PATH = Join-Path $data "knowledge.json"
$env:APS_CHECKPOINT_PATH = Join-Path $data "checkpoints.json"
$env:APS_PREFERENCE_PATH = Join-Path $data "preferences.json"
$env:APS_EMBEDDINGS_PATH = Join-Path $data "embeddings.json"
$env:APS_APPROVAL_PATH = Join-Path $data "approvals.json"
$env:APS_APPROVAL_BACKEND = "file"
$env:APS_AUDIT_LEDGER_DIR = Join-Path $data "audit-ledger"
$env:APS_AUDIT_MIRROR_DIR = Join-Path $data "audit-mirror"
$env:APS_AUTOMATION_STATE_PATH = Join-Path $data "automation.json"
$env:APS_MCP_BUS_PATH = Join-Path $data "mcp-bus.json"
$env:APS_BRANCH_PATH = Join-Path $data "branches.json"
$env:APS_BRANCH_DIR = Join-Path $data "branches"
$env:APS_GOLDEN_CACHE = Join-Path $data "golden-tests.json"
Remove-Item Env:APS_DATABASE_URL -ErrorAction SilentlyContinue
# R71.1 图纸 E2E 要求显式开启测试认证,避免仓库 .env 免登录配置让登录按钮永不启用
$env:APS_AUTH_ENABLED = "1"
.\.venv\Scripts\python.exe -X utf8 -m uvicorn server.main:app --host 127.0.0.1 --port 8100
```
后端健康后,在第二个 PowerShell 中运行:
```powershell
cd apps/web
$env:E2E_API_TARGET = "http://127.0.0.1:8100"
Remove-Item Env:E2E_ALLOW_LIVE_BACKEND -ErrorAction SilentlyContinue
npm run test:e2e
```
CI 同样使用独立端口 8100 和 `runner.temp` 持久化路径。
### 现场 8003 的显式例外
只有在操作者明确接受“向当前现场工作区创建真实会话”时,才允许:
```powershell
$env:E2E_API_TARGET = "http://127.0.0.1:8003"
$env:E2E_ALLOW_LIVE_BACKEND = "1"
npm run test:e2e
```
未设置 `E2E_ALLOW_LIVE_BACKEND=1` 时,Playwright 在加载配置阶段直接失败,不会启动浏览器或发送请求。
## 4. 门禁达成与剩余项
**已达成(本地门禁,均实测通过)**
- 按 §3.5 启动 8100 隔离后端后,`cd apps/web && npm run test:e2e`:3/3 通过(登录 → 排产 → 结果块 → 视口/工作台 → 命令面板;中文断言)。
- `node scripts/smoke-sidecar-local.mjs`:5/5 通过,退出码 0。
- CI 矩阵定义落地(Windows+Ubuntu 后端黄金测试与前端构建、E2E、SBOM)。
**剩余(需外部环境,非本轮可测)**
1. **干净离线 Win10/11 安装冒烟**——需无网验证机执行 NSIS 安装包安装/卸载/首次启动(矩阵 99/118)。
2. **代码签名**——需受信任代码签名证书;当前 `release-manifest.json` 明确 `signed: false`,仅为本地完整性清单。
3. **Defender 扫描入 CI 门禁**——需 Defender for Endpoint 门户/API 与提交策略。
4. **SBOM 上链/签名 attestation**——当前 `npm run sbom` 产出 CycloneDX 1.6 + SHA-256 清单,但未做签名与供应链上链。
**前端缺陷状态(round-42 已修复)**:dev 模式 StrictMode 下 `refreshWorld` / `captureSessionResult`
状态更新被吞的问题已修复——根因 ① `alive` ref 存活标记:effect 只写 cleanup 置 false,StrictMode
mount→cleanup→mount 双调用后永久为 false,`refreshWorld` 的全部提交被跳过;修复为 effect 体内复位 true。
根因 ② `captureSessionResult` 在 `setWorld` 更新器内调用(更新器非纯函数、dev 双调用重复执行);修复为
更新器外经 `worldRef` 计算 merged 后提交。dev 模式核心路径已 E2E 冒烟通过,preview 默认门禁保持 3/3。
## 5. CP RHS 参数重算浏览器验收
1. 打开“设置 -> 算法 Skill -> 异步任务”,选择开始日期。
2. 保留 C8=60 分钟、班组/工装容量=1,费率留空并提交 `CP RHS 增量重算`。
3. 轮询完成后打开结果:C8 显示全部待排订单;C12 仅显示真实激活资源的 code/id;未接线参数显示 inactive,不启动变体重解。
4. 无费率时每行只显示“未配置”,不得出现 0、CNY 或估算成本。再显式填写费率提交一次,确认成本按对应增量计算,同时页面声明目标改善与货币成本不可直接相减。
5. OPTIMAL 行显示精确改善;FEASIBLE 行只显示区间。C8 若出现“恢复可行”必须作为可行性异常,而不是收益。
6. 验收记录必须包含 `/api/jobs` 请求/轮询结果、浏览器 console warning/error、新增任务取消、8003/5173 或隔离端口健康状态;不得在浏览器验收中使用生产业务数据做写操作。
### 5.1 C7 line/day 实例
1. 进入异步任务后,先选开始日期,再选择 ACTIVE 产线和 C7 工作日;输入增加分钟,费率留空提交。
2. 结果必须显示具体 `lineCode/#lineId + bucketDate`、`baselineRhs → perturbedRhs`,并声明开工日整笔记账、非物化班次模型。
3. 无费率时 C7 货币字段全 null/显示未配置。仅填写 C7 实例费率后重算,成本只能出现在该 line/day 行,单位为每产线/工作日增量分钟。
4. DatePicker 必须禁用所选产线的非工作日;伪造非工作日、未启用产线、越界日期或非法实例时仍须在创建线程前 422,任务总数不增加。
5. 浏览器网络中不得出现发布/下发请求;最终 console warning/error 为 0。