diff --git a/.trellis/spec/hyb-oa/deployment-trialrun.md b/.trellis/spec/hyb-oa/deployment-trialrun.md index fa4b6640..658a3cb8 100644 --- a/.trellis/spec/hyb-oa/deployment-trialrun.md +++ b/.trellis/spec/hyb-oa/deployment-trialrun.md @@ -110,6 +110,9 @@ SYNC_ENV=0 bash deploy/trialrun/deploy.sh `/portal` URL and the `今日任务` heading. - Navigation-label assertions for the workbench link must accept both `工作台` and `Portal` while older mocks still use English labels. +- Navigation assertions must be role-aware. Admin/manager smoke may assert + team/admin entries such as `团队`, but ordinary employee smoke must not + require links that `NavLinks` intentionally hides for that role. - Smoke tests must not require a copy string that is not a product contract for public availability. - If the product intentionally renames the primary workbench label again, update @@ -120,6 +123,8 @@ SYNC_ENV=0 bash deploy/trialrun/deploy.sh - Page reaches `/portal`, `今日任务` is visible, but nav label is `工作台` -> smoke should pass. +- Employee reaches `/portal`, role-aware navigation hides `团队`, and employee + pages such as `项目` remain reachable -> smoke should pass. - Page reaches `/portal`, but `今日任务` is missing -> smoke should fail. - Page stays on login, `/403`, error fallback or another route -> smoke should fail. @@ -130,10 +135,14 @@ SYNC_ENV=0 bash deploy/trialrun/deploy.sh - Good: `assertPortalLoaded` checks `/portal`, `今日任务`, and a workbench link matching either `工作台` or `Portal`. +- Good: `assertMainNavVisible(page, { expectTeam: false })` is used for + ordinary employee live smoke. - Base: mocked E2E fixtures may still emit `Portal`; live public navigation may emit `工作台`. - Bad: live smoke hard-codes only `Portal` and blocks deployment after a valid Chinese navigation rollout. +- Bad: employee live smoke requires the `团队` sidebar link after role-aware + navigation intentionally hides it. ### 6. Tests Required diff --git a/.trellis/tasks/05-30-deploy-mobile-public-audit/audit.md b/.trellis/tasks/05-30-deploy-mobile-public-audit/audit.md new file mode 100644 index 00000000..75e0b01e --- /dev/null +++ b/.trellis/tasks/05-30-deploy-mobile-public-audit/audit.md @@ -0,0 +1,41 @@ +# 部署与审计记录 + +## Deployment + +- 部署目标:`https://oa.hyinda.com` +- 部署代码基线:`df56d6f`,包含移动端工作提交 `ba7391e` +- 部署命令:`SYNC_ENV=0 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash deploy/trialrun/deploy.sh` +- 环境边界:未同步本地 `.env` +- 远端结果:Docker build、Prisma migrate status、seed config/trialrun 完成;无待应用 migration +- 注意项:rsync 提示一个历史 artifact 非空目录无法删除,但未阻断构建、启动和 smoke + +## Public Checks + +- `node deploy/trialrun/validate-env-contract.js`:PASS +- `curl -fsS https://oa.hyinda.com/ready`:PASS +- `node scripts/qa/check-public-bundle-copy.mjs https://oa.hyinda.com`:PASS,`findings=0` +- `LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh`:PASS +- live smoke:3 passed +- live OCR sample:PASS,可解释终态为重复票据类别 +- pending OCR 扫描:`SKIP missing DATABASE_URL` + +## Mobile Visual Checks + +- 公网资源移动专项:`E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list` +- 结果:6 passed +- 截图目录: + - `output/playwright/mobile-ia-flow-remediation/` + - `output/playwright/r124-mobile-entry-matrix/` +- 抽查页面:移动首页财务卡、待我处理 Tab、移动我的、移动报销。 + +## Code Audit Findings + +- `ba7391e` 的移动端改动符合已确认边界:请假只读额度、财务卡条件出现、搜索标签 `当前列表搜索`、审批文案 `待我处理`、移动我的去掉无条件财务入口。 +- `client/src` 中未发现旧用户文案 `待我审批`、`搜索报销`、`搜索采购` 或 `付款批次未启用` 仍作为当前 UI 输出。 +- 发现 live smoke 旧断言:员工 smoke 仍强制要求侧栏 `团队`,但当前 `NavLinks` 已按角色隐藏员工团队入口。 +- 已修正:`assertMainNavVisible` 支持 `expectTeam` 选项,管理员仍检查团队入口,员工断言团队入口不可见。 + +## Remaining Risk + +- 本轮自动化覆盖公网浏览器和企业微信 UA 近似视口,但没有替代真实手机企业微信真机确认。 +- `pending OCR` 只因本地缺 `DATABASE_URL` 记录 SKIP;不代表公网 OCR 队列异常。 diff --git a/.trellis/tasks/05-30-deploy-mobile-public-audit/check.jsonl b/.trellis/tasks/05-30-deploy-mobile-public-audit/check.jsonl new file mode 100644 index 00000000..e14b62af --- /dev/null +++ b/.trellis/tasks/05-30-deploy-mobile-public-audit/check.jsonl @@ -0,0 +1,3 @@ +{"file":".trellis/spec/hyb-oa/deployment-trialrun.md","reason":"检查部署证据、public smoke 和可测试声明是否满足公网门禁。"} +{"file":".trellis/spec/hyb-oa/frontend-react-vite-design.md","reason":"检查移动视觉和用户文案是否符合统一前端规则。"} +{"file":".trellis/spec/hyb-oa/security-privacy.md","reason":"检查报告和日志摘要不泄露敏感信息。"} diff --git a/.trellis/tasks/05-30-deploy-mobile-public-audit/implement.jsonl b/.trellis/tasks/05-30-deploy-mobile-public-audit/implement.jsonl new file mode 100644 index 00000000..ba20ce05 --- /dev/null +++ b/.trellis/tasks/05-30-deploy-mobile-public-audit/implement.jsonl @@ -0,0 +1,3 @@ +{"file":".trellis/spec/hyb-oa/deployment-trialrun.md","reason":"公网部署命令、SYNC_ENV=0 边界、部署后 ready/smoke/文案扫描验收。"} +{"file":".trellis/spec/hyb-oa/frontend-react-vite-design.md","reason":"移动端轻动作、视觉检查、条件卡片和当前列表搜索规则。"} +{"file":".trellis/spec/hyb-oa/security-privacy.md","reason":"公网验证和审计过程中避免输出敏感凭据、真实票据和原始 payload。"} diff --git a/.trellis/tasks/05-30-deploy-mobile-public-audit/implement.md b/.trellis/tasks/05-30-deploy-mobile-public-audit/implement.md new file mode 100644 index 00000000..93a150f8 --- /dev/null +++ b/.trellis/tasks/05-30-deploy-mobile-public-audit/implement.md @@ -0,0 +1,21 @@ +# Implementation Notes + +## Execution Plan + +1. 用 `SYNC_ENV=0` 执行既有公网部署脚本,不同步本地 `.env`。 +2. 以部署脚本内置远端门禁作为第一层验收:env contract、compose config、Docker build、migration status、`/ready`、external deps smoke、runtime smoke、live smoke、public bundle copy scan。 +3. 如部署脚本因非产品问题的 smoke 旧断言失败,先审计实际公网页面和断言来源;只修正测试口径,不改生产数据。 +4. 用公网资源运行移动端专项 Playwright,覆盖本轮移动页面视觉和响应式基线。 +5. 审计 `ba7391e` 的移动端页面、测试和 spec 变更,记录发现、修正和剩余风险。 + +## Boundaries + +- 不同步本地 `.env`,不输出 Secret、密码或完整账号。 +- 不做真实业务写入、真实付款、ERP 写入、银行流水导入或企业微信写入。 +- 公网自动化和企业微信 UA 近似视口只作为自动化证据,不替代真实手机企业微信验收。 + +## Fix Applied During Audit + +- live smoke 员工用例旧口径强制要求 `团队` 导航。 +- 当前产品导航已经角色化,员工截图显示工作台、报销、采购、项目、资产、请假记录、待办,隐藏团队入口。 +- 修正方式:`assertMainNavVisible` 增加 `expectTeam` 选项;管理员保留团队入口断言,员工断言团队入口不可见。 diff --git a/.trellis/tasks/05-30-deploy-mobile-public-audit/prd.md b/.trellis/tasks/05-30-deploy-mobile-public-audit/prd.md new file mode 100644 index 00000000..d7d827ae --- /dev/null +++ b/.trellis/tasks/05-30-deploy-mobile-public-audit/prd.md @@ -0,0 +1,46 @@ +# 部署移动端收口并公网复检审计 + +## Goal + +将已提交的移动端入口与文案收口改动部署到公网,并完成公网自动复检、代码审计和移动端视觉检查,给出是否可让用户在线测试的结论。 + +## Background + +- 用户已批准“开始部署 公网复检”,并要求随后审计代码和视觉效果。 +- 待部署工作提交为 `ba7391e 收口移动端入口与文案(05-30-mobile-confirmed-entry-copy-followups)`,归档提交为 `df56d6f chore(task): 归档移动端入口与文案收敛`。 +- 公网是准生产环境,必须使用 `SYNC_ENV=0` 部署,避免覆盖远端 `.env`。 + +## Scope + +- 执行当前 `main` 分支到公网的既有部署脚本。 +- 公网复检:`/ready`、部署脚本内 smoke、public bundle 文案扫描、可用凭据条件下的 live smoke。 +- 移动视觉检查:重点覆盖移动首页、移动请假、移动报销、移动采购、移动审批、移动我的。 +- 代码审计:复核本次提交对移动入口、文案、条件显示、测试和 spec 的影响,列出风险与建议。 + +## Out Of Scope + +- 不修改生产 `.env`。 +- 不做真实付款、银行流水导入、ERP 写入或企业微信写入。 +- 不新增业务功能或修复非本轮部署阻断之外的问题。 +- 不把自动化企业微信 UA 验证等同于真实手机企业微信验收。 + +## Acceptance Criteria + +- [ ] `SYNC_ENV=0 bash deploy/trialrun/deploy.sh` 完成或给出明确阻断原因。 +- [ ] 公网 `/ready` 通过。 +- [ ] 公网只读 smoke / readiness 按可用凭据执行,缺凭据时记录 `SKIP` 而非伪装通过。 +- [ ] 公网移动视觉截图或 Playwright 检查覆盖本轮变更页面。 +- [ ] 代码审计覆盖 `ba7391e` 的前端页面、测试和 spec 变更。 +- [ ] 给出是否可在线测试、剩余风险和下一步建议。 + +## Test Plan + +- `node deploy/trialrun/validate-env-contract.js` +- `SYNC_ENV=0 bash deploy/trialrun/deploy.sh` +- `curl -fsS https://oa.hyinda.com/ready` +- `LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh`(凭据存在时) +- `cd client && E2E_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list` +- 额外移动端截图或截图审查输出到 `output/playwright/05-30-deploy-mobile-public-audit/` +- `python3 ./.trellis/scripts/task.py validate 05-30-deploy-mobile-public-audit` +- `node scripts/qa/check-trellis-governance.mjs` +- `git diff --check` diff --git a/.trellis/tasks/05-30-deploy-mobile-public-audit/task.json b/.trellis/tasks/05-30-deploy-mobile-public-audit/task.json new file mode 100644 index 00000000..a80bb3c5 --- /dev/null +++ b/.trellis/tasks/05-30-deploy-mobile-public-audit/task.json @@ -0,0 +1,26 @@ +{ + "id": "deploy-mobile-public-audit", + "name": "deploy-mobile-public-audit", + "title": "部署移动端收口并公网复检审计", + "description": "部署已提交的移动端入口与文案收口改动,完成公网复检、代码审计和移动视觉检查。", + "status": "in_progress", + "dev_type": null, + "scope": "deployment-public-frontend-audit", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-05-30", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/client/tests/e2e/helpers/smokeAssertions.ts b/client/tests/e2e/helpers/smokeAssertions.ts index c93cbb77..1606da64 100644 --- a/client/tests/e2e/helpers/smokeAssertions.ts +++ b/client/tests/e2e/helpers/smokeAssertions.ts @@ -32,9 +32,14 @@ export async function assertForbiddenPage(page: Page) { await expect(page.getByRole("heading", { name: "无权限访问", exact: true })).toBeVisible(); } -export async function assertMainNavVisible(page: Page) { +export async function assertMainNavVisible(page: Page, options: { expectTeam?: boolean } = {}) { + const { expectTeam = true } = options; await expect(page.getByRole("link", { name: "工作台", exact: true }).first()).toBeVisible(); - await expect(page.getByRole("link", { name: "团队" }).first()).toBeVisible(); + if (expectTeam) { + await expect(page.getByRole("link", { name: "团队" }).first()).toBeVisible(); + } else { + await expect(page.getByRole("link", { name: "团队", exact: true })).toHaveCount(0); + } await expect(page.getByRole("link", { name: "项目" }).first()).toBeVisible(); } diff --git a/client/tests/e2e/live-smoke.spec.ts b/client/tests/e2e/live-smoke.spec.ts index 8e266831..1cc64896 100644 --- a/client/tests/e2e/live-smoke.spec.ts +++ b/client/tests/e2e/live-smoke.spec.ts @@ -115,10 +115,7 @@ test.describe("live smoke", () => { userName = employee.name; await assertPortalLoaded(page); - await assertMainNavVisible(page); - - await page.goto("/team"); - await expect(page.getByRole("heading", { name: "团队状态", exact: true })).toBeVisible(); + await assertMainNavVisible(page, { expectTeam: false }); await page.goto("/projects"); await expect(page.getByRole("heading", { name: "项目", exact: true })).toBeVisible();