diff --git a/issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv b/issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv index 038c28ce..d60bbc28 100644 --- a/issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv +++ b/issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv @@ -1,18 +1,18 @@ "ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes" "OAAUTO1-0","建立自动化与人工收口拆分合同","基于 OASMALL1、OANEXT2、OALAUNCH1 和 OAOPS1 当前状态,建立启用前下一步执行拆分:我能自动完成的工作、必须人工完成的工作、自动化不能替代的边界和推荐顺序。","Issue CSV 校验通过;明确本合同只规划和细化步骤,不写公网业务数据、不部署、不修改业务代码;所有执行项需用户后续采纳后再进入对应合同。","受影响流程:启用前收口 -> 自动化/人工分工 -> 用户采纳 -> 后续执行;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv。","python3|manual","DONE","DONE","DONE","issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv","[]","MAGI 审视:自动化已覆盖 OCR、权限、TEST/SMOKE 技术闭环和每日巡检;执行:拆分剩余自动/人工工作;提升:自动先扫风险,人工只做真实业务和决策。回滚/清理:本合同只新增 Issue CSV,若不采纳可删除或后续标记 superseded,不影响公网、数据库或业务数据。" "OAAUTO1-1","当前剩余门禁基线","核对当前最终启用仍未完成的门禁:真实业务闭环、备份恢复演练、数据治理/培训、7 天稳定化、Day 7 Go/No-Go;权限矩阵、OCR 小样本、TEST/SMOKE 业务闭环已具备自动证据。","形成明确基线:自动证据不等于正式启用;OALAUNCH1 仍需真实业务、恢复和人工确认。","受影响流程:Issue 状态读取 -> 证据基线 -> 后续顺序;验证步骤:读取 OALAUNCH1-3/4/6/7、OAOPS1-3/4/5/6、OANEXT2-6/7/8/9/10 当前状态。","python3|rg|manual","DONE","DONE","DONE","issues/2026-05-23_16-49-39-oa-final-launch-readiness-v1.csv;issues/2026-05-22_20-09-22-oa-real-use-stabilization-v1.csv;issues/2026-05-23_20-47-20-oa-next-adoption-detailed-roadmap-v2.csv","[""OAAUTO1-0""]","当前证据:权限矩阵 PASS_PERMISSION_MATRIX_READY;TEST/SMOKE 技术闭环 PASS_TEST_SMOKE_BUSINESS_LOOP_READY;OCR 小样本 coverage_satisfied;OALAUNCH1 summary 仍 PAUSED_MANUAL_CONFIRMATION。" -"OAAUTO1-2","自动化第 1 步:公网只读日巡检","我可以先自动运行 OAOPS1 公网只读巡检,复核 /ready、账号 readiness、feature flags、报销/OCR 入口、采购/合同/付款/AP 入口、移动/企微入口和隐私扫描。","生成 artifacts/qa-flow/oa-real-use-daily-/summary.json;结论为 PASS_OA_REAL_USE_DAILY_READY 或可解释 finding;报告不含密码、Secret、完整账号、票据原文或合同正文。","受影响流程:公网健康 -> 入口/权限/只读探针 -> 日收口;验证步骤:LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-oa-real-use-daily.sh。","bash|node|playwright","TODO","TODO","TODO","scripts/qa/run-oa-real-use-daily.sh;artifacts/qa-flow/oa-real-use-daily-*","[""OAAUTO1-1""]","可自动完成;需要公网网络和 live smoke env,但不创建业务数据、不付款、不写企微。若遇到 429 或外部依赖失败,按 finding 记录。" -"OAAUTO1-3","自动化第 2 步:最终启用汇总刷新","我可以用最新权限矩阵和巡检证据自动刷新 OALAUNCH1 summary,明确还缺哪些人工或运维门禁。","生成 artifacts/qa-flow/oa-final-launch-readiness-/summary.json;open gates 清晰列出;在真实业务、备份和 Go/No-Go 未完成前不得输出 PASS_OA_LAUNCH_READY。","受影响流程:证据汇总 -> open gates -> Go/No-Go 前状态;验证步骤:OALAUNCH_PERMISSION_EVIDENCE=artifacts/qa-flow/oa-permission-matrix-20260523-magi/summary.json bash scripts/qa/run-oa-final-launch-readiness.sh。","bash|manual","TODO","TODO","TODO","scripts/qa/run-oa-final-launch-readiness.sh;artifacts/qa-flow/oa-final-launch-readiness-*","[""OAAUTO1-2""]","可自动完成;只读汇总,不能替代用户决策。" -"OAAUTO1-4","自动化第 3 步:权限负例补充冻结项与搜索","在现有权限矩阵基础上,我可以扩展只读自动化,覆盖冻结项、全局搜索、薪酬绩效差旅等高敏/后置模块直连负例。","普通员工无法通过导航、搜索、直连 API 访问冻结/高敏模块;授权角色必要入口可用;报告脱敏并可作为 OALAUNCH_PERMISSION_EVIDENCE 的补充。","受影响流程:多角色登录 -> 导航/搜索/API 负例 -> 脱敏报告;验证步骤:扩展或复用 scripts/qa/check-oa-permission-matrix.mjs 并运行。","node|playwright","TODO","TODO","TODO","scripts/qa/check-oa-permission-matrix.mjs;server/src/routes/search.js;client/src/features/navigation/schema.ts;server/src/routes/featureFlags.js","[""OAAUTO1-1""]","可自动完成但属于脚本增强;若修改 QA 脚本,需要先进入实现合同或更新本合同采纳状态。若发现真实权限漏洞,需新建修复合同并 reviewer 评审。" -"OAAUTO1-5","自动化第 4 步:OCR 代表样本复核","我可以按够用原则复跑 2-3 张代表 PDF 发票 OCR 小样本,验证秒级 OCR、重复/已用终态和脱敏报告;不跑 105 张全量,除非单独批准压力/覆盖专项。","summary 只含 alias、状态、耗时、字段命中布尔值和可解释失败分类;不提交报销、不审批、不付款,不输出票号、销售方、金额、完整路径或原始 payload。","受影响流程:PDF 样本 -> OCR -> 脱敏统计;验证步骤:PLAYWRIGHT_OCR_BATCH_MAX_SAMPLES=3 PLAYWRIGHT_OCR_BATCH_STOP_WHEN_COVERED=1 运行 live invoice OCR batch。","playwright|bash","TODO","TODO","TODO","client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;artifacts/qa-flow/oinv1-*","[""OAAUTO1-2""]","可自动完成;用户已明确够用即可,不把 105 张全量作为启用前门禁。" +"OAAUTO1-2","自动化第 1 步:公网只读日巡检","我可以先自动运行 OAOPS1 公网只读巡检,复核 /ready、账号 readiness、feature flags、报销/OCR 入口、采购/合同/付款/AP 入口、移动/企微入口和隐私扫描。","生成 artifacts/qa-flow/oa-real-use-daily-/summary.json;结论为 PASS_OA_REAL_USE_DAILY_READY 或可解释 finding;报告不含密码、Secret、完整账号、票据原文或合同正文。","受影响流程:公网健康 -> 入口/权限/只读探针 -> 日收口;验证步骤:LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-oa-real-use-daily.sh。","bash|node|playwright","DONE","DONE","DONE","scripts/qa/run-oa-real-use-daily.sh;artifacts/qa-flow/oa-real-use-daily-*","[""OAAUTO1-1""]","已完成:artifacts/qa-flow/oa-real-use-daily-20260523-231300/summary.json 为 PASS_OA_REAL_USE_DAILY_READY,findingCount=0;覆盖 /ready、live readiness、OA-only readiness、feature flags、认证只读探针、移动入口、企微渲染和 wecom-entry;不创建业务数据、不付款、不写企微。" +"OAAUTO1-3","自动化第 2 步:最终启用汇总刷新","我可以用最新权限矩阵和巡检证据自动刷新 OALAUNCH1 summary,明确还缺哪些人工或运维门禁。","生成 artifacts/qa-flow/oa-final-launch-readiness-/summary.json;open gates 清晰列出;在真实业务、备份和 Go/No-Go 未完成前不得输出 PASS_OA_LAUNCH_READY。","受影响流程:证据汇总 -> open gates -> Go/No-Go 前状态;验证步骤:OALAUNCH_PERMISSION_EVIDENCE=artifacts/qa-flow/oa-permission-matrix-20260523-magi/summary.json bash scripts/qa/run-oa-final-launch-readiness.sh。","bash|manual","DONE","DONE","DONE","scripts/qa/run-oa-final-launch-readiness.sh;artifacts/qa-flow/oa-final-launch-readiness-*","[""OAAUTO1-2""]","已完成:artifacts/qa-flow/oa-final-launch-readiness-20260523-231746/summary.json 为 PAUSED_MANUAL_CONFIRMATION,openCount=12,无新增 finding;仍缺真实业务、备份恢复、7 天稳定化和 Day 7 Go/No-Go。" +"OAAUTO1-4","自动化第 3 步:权限负例补充冻结项与搜索","在现有权限矩阵基础上,我可以扩展只读自动化,覆盖冻结项、全局搜索、薪酬绩效差旅等高敏/后置模块直连负例。","普通员工无法通过导航、搜索、直连 API 访问冻结/高敏模块;授权角色必要入口可用;报告脱敏并可作为 OALAUNCH_PERMISSION_EVIDENCE 的补充。","受影响流程:多角色登录 -> 导航/搜索/API 负例 -> 脱敏报告;验证步骤:扩展或复用 scripts/qa/check-oa-permission-matrix.mjs 并运行。","node|playwright","TODO","TODO","TODO","scripts/qa/check-oa-permission-matrix.mjs;server/src/routes/search.js;client/src/features/navigation/schema.ts;server/src/routes/featureFlags.js","[""OAAUTO1-1""]","已增强 QA 脚本并运行 artifacts/qa-flow/oa-permission-matrix-20260523-oauto1/summary.json,结论 SKIP/BLOCKED_BY_EXTERNAL_CONFIG:差旅列表对 employee/manager/finance 返回 200 空列表,HR 可访问 payroll/performance,manager 搜索可见合同台账摘要。未改生产权限;需用户决定这是既有授权口径还是上线前冻结项修复。回滚/清理:若不采纳增强检查,可回退本 QA 脚本改动或保留为非阻断报告;若后续修生产权限,需独立 Issue、reviewer 和 feature flag/权限回滚方案。" +"OAAUTO1-5","自动化第 4 步:OCR 代表样本复核","我可以按够用原则复跑 2-3 张代表 PDF 发票 OCR 小样本,验证秒级 OCR、重复/已用终态和脱敏报告;不跑 105 张全量,除非单独批准压力/覆盖专项。","summary 只含 alias、状态、耗时、字段命中布尔值和可解释失败分类;不提交报销、不审批、不付款,不输出票号、销售方、金额、完整路径或原始 payload。","受影响流程:PDF 样本 -> OCR -> 脱敏统计;验证步骤:PLAYWRIGHT_OCR_BATCH_MAX_SAMPLES=3 PLAYWRIGHT_OCR_BATCH_STOP_WHEN_COVERED=1 运行 live invoice OCR batch。","playwright|bash","DONE","DONE","DONE","client/tests/e2e/manual/live-invoice-ocr-batch.spec.ts;client/tests/e2e/helpers/liveInvoiceOcr.ts;artifacts/qa-flow/oinv1-*","[""OAAUTO1-2""]","已完成:artifacts/qa-flow/oinv1-redacted-batch-oauto1-20260523-2318/summary.json,plannedSamples=3,available=3,字段命中 3/3,耗时约 0.2-2.4 秒;报告只含别名/状态/布尔命中,不提交报销、不审批、不付款。" "OAAUTO1-6","自动化第 5 步:TEST/SMOKE 技术闭环复跑","如需回归,我可以复跑 TEST/SMOKE/OANEXT2 业务闭环,覆盖项目、合同台账、报销审批、付款批次、合成回单、已付款标记、AP 导出、采购审批和关联。","仅创建带 TEST/SMOKE 标记的数据;不执行真实银行付款、不接支付、不写 ERP、不导入银行流水、不上传真实票面原图。","受影响流程:TEST/SMOKE 报销 -> 审批 -> 付款/AP;采购 -> 合同/项目;验证步骤:OA_TEST_SMOKE_BUSINESS_WRITE=1 LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/run-oa-test-smoke-business-loop.mjs。","node|bash","TODO","TODO","TODO","scripts/qa/run-oa-test-smoke-business-loop.mjs;artifacts/qa-flow/oa-test-smoke-business-loop-*","[""OAAUTO1-2""]","可自动完成但会写 TEST/SMOKE 公网测试数据;每次复跑前需确认是否允许继续保留审计测试数据。" -"OAAUTO1-7","自动化第 6 步:人工证据包模板与脱敏汇总","我可以准备真实业务证据包模板和脱敏汇总方式,让人工只记录业务编号别名、状态、责任人、日期和证据路径,不暴露真实票据、回单、合同正文或银行资料。","形成可填写的 evidence 结构或 summary 输入清单;run-oa-final-launch-readiness 可读取或人工引用;不要求上传敏感原文。","受影响流程:人工真实样本 -> 脱敏 evidence -> OALAUNCH 汇总;验证步骤:检查证据包字段不含密码、完整账号、真实票号、合同正文、银行流水或原始附件。","manual|bash","TODO","TODO","TODO","artifacts/qa-flow;docs/trialrun/人工试运行报告.md;scripts/qa/run-oa-final-launch-readiness.sh","[""OAAUTO1-3""]","可自动辅助;若需要写文档模板,优先更新现有事实源,不新建孤立文档。" +"OAAUTO1-7","自动化第 6 步:人工证据包模板与脱敏汇总","我可以准备真实业务证据包模板和脱敏汇总方式,让人工只记录业务编号别名、状态、责任人、日期和证据路径,不暴露真实票据、回单、合同正文或银行资料。","形成可填写的 evidence 结构或 summary 输入清单;run-oa-final-launch-readiness 可读取或人工引用;不要求上传敏感原文。","受影响流程:人工真实样本 -> 脱敏 evidence -> OALAUNCH 汇总;验证步骤:检查证据包字段不含密码、完整账号、真实票号、合同正文、银行流水或原始附件。","manual|bash","DONE","DONE","DONE","artifacts/qa-flow;docs/trialrun/人工试运行报告.md;scripts/qa/run-oa-final-launch-readiness.sh","[""OAAUTO1-3""]","已完成:artifacts/qa-flow/oa-launch-manual-evidence-oauto1-20260523-2324/summary.json 和 manual-evidence-template.md,覆盖真实报销、采购/费用、合同台账和每日收口;只记录脱敏别名、状态、角色和证据路径。" "OAAUTO1-8","人工第 1 步:真实报销闭环操作","员工在公网 OA 提交 1 条真实报销并上传 PDF 发票;审批人查看详情后审批;财务创建付款批次、线下银行付款、上传回单、标记已付款并导出 AP/报销/票据资料。","报销真实业务闭环完成;OA 内状态、审批记录、付款批次、回单附件、已付款标记和 AP 导出均可追溯。","受影响流程:员工提交报销 -> 审批 -> 财务付款批次 -> 回单 -> 已付款 -> AP 导出;验证步骤:人工操作后运行 OAAUTO1-3 汇总,或在 evidence 包记录脱敏状态。","manual","TODO","TODO","TODO","external|public-oa","[""OAAUTO1-7""]","人工步骤:1 登录员工账号;2 进入报销/发票上传;3 上传真实 PDF 发票;4 核对 OCR 结果;5 填金额/项目/费用类型;6 提交;7 审批人打开详情审批;8 财务建付款批次;9 在线下银行付款;10 上传回单;11 标记已付款;12 导出 AP/报销/票据。不要把票据原图、票号、银行流水或回单原文发到聊天/仓库。" "OAAUTO1-9","人工第 2 步:真实采购或费用申请操作","员工在公网 OA 提交 1 条真实采购/费用申请,尽量关联项目或合同;审批人查看详情后通过或退回;管理员确认审计记录和项目/合同摘要可追溯。","至少 1 条真实采购/费用流程完成;有关联项目或合同;审批、退回或通过记录可查。","受影响流程:采购/费用申请 -> 审批 -> 项目/合同关联 -> 审计;验证步骤:人工操作后记录脱敏业务别名、状态和关联对象,最终汇总到 OALAUNCH。","manual","TODO","TODO","TODO","external|public-oa","[""OAAUTO1-7""]","人工步骤:1 登录员工或负责人账号;2 进入采购/费用申请;3 填申请事由、金额区间、供应商摘要和用途;4 选择项目或合同关联;5 提交;6 审批人打开详情处理;7 管理员查看列表、详情和审计记录;8 若退回,记录原因并验证可重提。敏感报价和成本明细只按公司边界上传或线下留存。" "OAAUTO1-10","人工第 3 步:真实合同台账和归档位置操作","负责人在公网 OA 建 1 条真实合同台账,填写合同编号、合同摘要、负责人、关联项目/采购/付款、线下归档位置和敏感级别;不上传涉密正文。","至少 1 条真实合同台账可查;归档位置明确;权限检查通过;敏感正文不进入公网 OA 或自动报告。","受影响流程:合同登记 -> 关联项目/采购/付款 -> 归档位置 -> 权限抽查;验证步骤:管理员和普通员工分别查看,确认授权可见、越权不可见或敏感字段脱敏。","manual","TODO","TODO","TODO","external|public-oa","[""OAAUTO1-7""]","人工步骤:1 登录负责人或管理员账号;2 进入合同台账;3 新建合同记录;4 填编号/名称摘要/签约方摘要/负责人/日期/金额区间;5 关联项目、采购或付款;6 填线下归档位置;7 设置敏感级别;8 保存;9 用普通员工账号确认不能越权查看敏感附件或详情。涉密合同正文、敏感报价、成本细节不上传。" "OAAUTO1-11","人工第 4 步:每日问题登记和阻断处理","7 天稳定化期间,管理员每天查看自动巡检和人工业务反馈,把问题分为 blocker、finding、feedback;blocker 当天修复、绕行或暂停流程。","每日有收口记录;blocker 有修复合同/绕行责任人/暂停决定;非阻断有 owner 和处理日期或接受理由。","受影响流程:日巡检 -> 人工反馈 -> 分类 -> 修复/绕行/暂停;验证步骤:Day 7 汇总检查 7 天记录和 unresolved blocker 数量。","manual","TODO","TODO","TODO","docs/trialrun/人工试运行报告.md;issues","[""OAAUTO1-2"",""OAAUTO1-8"",""OAAUTO1-9"",""OAAUTO1-10""]","人工步骤:1 每天固定时间查看 OAOPS1 summary;2 汇总员工/审批人/财务反馈;3 标记 blocker/finding/feedback;4 blocker 当天新建修复合同或暂停流程;5 人工绕行必须记录风险和补录责任人;6 非阻断进入反馈池。" -"OAAUTO1-12","半自动第 1 步:备份恢复演练准备","我可以准备影子库恢复检查清单、命令草案和证据模板;实际恢复范围、时间窗口和服务器操作必须由你批准,且不得影响公网生产库。","备份位置、恢复目标、验证表、附件抽样、RTO/RPO、负责人和清理路径明确;未批准前不执行恢复。","受影响流程:备份检查 -> 影子恢复 -> 抽样验证 -> 证据汇总;验证步骤:审批后按 OPS runbook 执行,OALAUNCH_BACKUP_EVIDENCE 指向恢复证据。","manual|ops|bash","TODO","TODO","TODO","docs/trialrun/OPS7运维交接Runbook.md;docs/ops/非功能基线.md","[""OAAUTO1-3""]","半自动。人工批准点:A 做影子库恢复;B 先只核对备份存在;C 暂停。禁止 destructive restore、migrate reset、覆盖公网生产库。清理只删除影子库/临时目录/临时凭据。" +"OAAUTO1-12","半自动第 1 步:备份恢复演练准备","我可以准备影子库恢复检查清单、命令草案和证据模板;实际恢复范围、时间窗口和服务器操作必须由你批准,且不得影响公网生产库。","备份位置、恢复目标、验证表、附件抽样、RTO/RPO、负责人和清理路径明确;未批准前不执行恢复。","受影响流程:备份检查 -> 影子恢复 -> 抽样验证 -> 证据汇总;验证步骤:审批后按 OPS runbook 执行,OALAUNCH_BACKUP_EVIDENCE 指向恢复证据。","manual|ops|bash","TODO","TODO","TODO","docs/trialrun/OPS7运维交接Runbook.md;docs/ops/非功能基线.md","[""OAAUTO1-3""]","已准备检查单 artifacts/qa-flow/oa-launch-manual-evidence-oauto1-20260523-2324/backup-restore-drill-checklist.md;未执行恢复,仍需人工选择 A 影子库恢复、B 只核对备份存在、C 暂停。禁止 destructive restore、migrate reset、覆盖公网生产库。" "OAAUTO1-13","自动与人工结合:7 天稳定化执行节奏","在用户确认起始日期后,我每天自动跑 OAOPS1 和 OALAUNCH 汇总;你每天做人工问题收口和必要真实业务确认。","连续 7 天报告齐全;无不可解释 blocker;Day 7 可进入正式启用决策。","受影响流程:每日自动巡检 -> 人工日收口 -> Day 7 汇总;验证步骤:每天运行 run-oa-real-use-daily.sh;Day 7 运行 run-oa-final-launch-readiness.sh。","bash|manual","TODO","TODO","TODO","scripts/qa/run-oa-real-use-daily.sh;scripts/qa/run-oa-final-launch-readiness.sh;artifacts/qa-flow/oa-real-use-daily-*","[""OAAUTO1-8"",""OAAUTO1-9"",""OAAUTO1-10"",""OAAUTO1-12""]","建议顺序:先完成 1 条真实业务样本和备份恢复演练,再启动 7 天;不能用旧报告凑连续 7 天。" "OAAUTO1-14","人工最终决策:Day 7 Go/No-Go","第 7 天根据最终启用汇总、真实业务证据、备份恢复、权限矩阵、SOP 和 7 天问题记录,由用户决定正式启用、带 finding 启用或暂停整改。","用户明确选择 A/B/C;A 为 PASS_OA_LAUNCH_READY 后启用;B 为 PASS_WITH_FINDINGS 并指定 owner/日期;C 为暂停启用并进入整改合同。","受影响流程:最终汇总 -> 用户决策 -> 正式启用/带 finding/暂停;验证步骤:设置 OALAUNCH_MANUAL_ACCEPT 或人工记录决策后复跑 run-oa-final-launch-readiness.sh。","manual|bash","TODO","TODO","TODO","scripts/qa/run-oa-final-launch-readiness.sh;issues/2026-05-23_16-49-39-oa-final-launch-readiness-v1.csv","[""OAAUTO1-13""]","不能自动替用户决策。选项:A 正式启用;B 带 finding 启用但指定 owner 和日期;C 暂停,先整改。" -"OAAUTO1-15","自动化替代边界说明","明确哪些可以用自动化测试替代,哪些不能:自动化可替代访问性、权限负例、OCR 可用性、技术闭环、脱敏扫描和汇总;不能替代真实付款、业务合理性、审批人判断、合同归档真实性、恢复演练接受和 Go/No-Go。","边界在后续执行中被遵守;任何报告不得把自动化 PASS 描述成真实业务已完成或正式启用已批准。","受影响流程:自动化测试 -> 人工业务 -> 启用决策;验证步骤:审查最终回复、Issue Notes 和 OALAUNCH summary 结论是否避免伪 PASS。","manual","TODO","TODO","TODO","issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv","[""OAAUTO1-1""]","原则:技术可用性可自动化,业务真实性和责任承担不可自动化。" +"OAAUTO1-15","自动化替代边界说明","明确哪些可以用自动化测试替代,哪些不能:自动化可替代访问性、权限负例、OCR 可用性、技术闭环、脱敏扫描和汇总;不能替代真实付款、业务合理性、审批人判断、合同归档真实性、恢复演练接受和 Go/No-Go。","边界在后续执行中被遵守;任何报告不得把自动化 PASS 描述成真实业务已完成或正式启用已批准。","受影响流程:自动化测试 -> 人工业务 -> 启用决策;验证步骤:审查最终回复、Issue Notes 和 OALAUNCH summary 结论是否避免伪 PASS。","manual","DONE","DONE","DONE","issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv","[""OAAUTO1-1""]","已执行边界:自动项只覆盖只读巡检、汇总、权限/搜索负例和 OCR 小样本;未执行真实付款、真实业务审批决策、备份恢复、TEST/SMOKE 复跑写入或 Go/No-Go。" "OAAUTO1-16","本合同校验与停止点","完成本 CSV 校验、process gate 和 diff 检查;提交后等待用户决定是否开始 OAAUTO1-2 的自动只读巡检或先执行人工样本。","validate_issues_csv.py、check-process-contract general、git diff --check 通过;本轮不部署、不写公网、不修改业务代码。","受影响流程:规划 -> 校验 -> 用户审查 -> 后续执行;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py ;node scripts/qa/check-process-contract.mjs --issue --profile general --json;git diff --check。","python3|node|git","DONE","DONE","DONE","issues/2026-05-23_23-01-55-oa-automation-manual-cutover-v1.csv","[""OAAUTO1-1""]","本轮只规划和细化,不执行公网巡检、不提交真实业务、不恢复备份;已通过 validate_issues_csv.py、check-process-contract general、git diff --check。" diff --git a/scripts/qa/check-oa-permission-matrix.mjs b/scripts/qa/check-oa-permission-matrix.mjs index 4c0d8827..4aa8d61b 100644 --- a/scripts/qa/check-oa-permission-matrix.mjs +++ b/scripts/qa/check-oa-permission-matrix.mjs @@ -49,6 +49,18 @@ const ROLE_TARGETS = [ { id: "notification-deliveries", path: "/api/notifications/deliveries?limit=5", statuses: [403] }, { id: "system-feature-flags", path: "/api/system/feature-flags", statuses: [403] }, { id: "erp-status", path: "/api/integrations/erp/master-data/status", statuses: [403] }, + { id: "payroll-items", path: "/api/payroll/items", statuses: [403] }, + { id: "performance-cycles", path: "/api/performance/cycles", statuses: [403, 409] }, + { id: "travel-requests", path: "/api/travel-requests?page=1&pageSize=5", statuses: [403, 409] }, + { + id: "global-search-sensitive", + path: "/api/search?q=TEST&limit=10", + statuses: [200, 409], + searchExpectations: { + forbiddenEntityTypes: ["contract_registry", "confidential_doc_ref", "travel_request"], + forbiddenHrefPrefixes: ["/contracts-registry", "/travel-requests"], + }, + }, ], }, { @@ -86,6 +98,18 @@ const ROLE_TARGETS = [ { id: "payment-batches", path: "/api/payment-batches?pageSize=5", statuses: [403, 409] }, { id: "audit-logs", path: "/api/audit-logs?pageSize=5", statuses: [403] }, { id: "erp-status", path: "/api/integrations/erp/master-data/status", statuses: [403] }, + { id: "payroll-items", path: "/api/payroll/items", statuses: [403] }, + { id: "performance-cycles", path: "/api/performance/cycles", statuses: [403, 409] }, + { id: "travel-requests", path: "/api/travel-requests?page=1&pageSize=5", statuses: [403, 409] }, + { + id: "global-search-sensitive", + path: "/api/search?q=TEST&limit=10", + statuses: [200, 409], + searchExpectations: { + forbiddenEntityTypes: ["contract_registry", "confidential_doc_ref", "travel_request"], + forbiddenHrefPrefixes: ["/contracts-registry", "/travel-requests"], + }, + }, ], }, { @@ -124,6 +148,18 @@ const ROLE_TARGETS = [ { id: "notification-deliveries", path: "/api/notifications/deliveries?limit=5", statuses: [403] }, { id: "system-feature-flags", path: "/api/system/feature-flags", statuses: [403] }, { id: "erp-status", path: "/api/integrations/erp/master-data/status", statuses: [403] }, + { id: "payroll-items", path: "/api/payroll/items", statuses: [403] }, + { id: "performance-cycles", path: "/api/performance/cycles", statuses: [403, 409] }, + { id: "travel-requests", path: "/api/travel-requests?page=1&pageSize=5", statuses: [403, 409] }, + { + id: "global-search-sensitive", + path: "/api/search?q=TEST&limit=10", + statuses: [200, 409], + searchExpectations: { + forbiddenEntityTypes: ["contract_registry", "confidential_doc_ref", "travel_request"], + forbiddenHrefPrefixes: ["/contracts-registry", "/travel-requests"], + }, + }, ], }, { @@ -197,6 +233,17 @@ const ROLE_TARGETS = [ { id: "payment-batches", path: "/api/payment-batches?pageSize=5", statuses: [403, 409] }, { id: "audit-logs", path: "/api/audit-logs?pageSize=5", statuses: [403] }, { id: "system-feature-flags", path: "/api/system/feature-flags", statuses: [403] }, + { id: "payroll-items", path: "/api/payroll/items", statuses: [403] }, + { id: "performance-cycles", path: "/api/performance/cycles", statuses: [403, 409] }, + { + id: "global-search-sensitive", + path: "/api/search?q=TEST&limit=10", + statuses: [200, 409], + searchExpectations: { + forbiddenEntityTypes: ["contract_registry", "confidential_doc_ref", "travel_request"], + forbiddenHrefPrefixes: ["/contracts-registry", "/travel-requests"], + }, + }, ], }, { @@ -231,6 +278,17 @@ const ROLE_TARGETS = [ negative: [ { id: "system-feature-flags", path: "/api/system/feature-flags", statuses: [403] }, { id: "notification-deliveries", path: "/api/notifications/deliveries?limit=5", statuses: [403] }, + { id: "payroll-items", path: "/api/payroll/items", statuses: [403] }, + { id: "performance-cycles", path: "/api/performance/cycles", statuses: [403, 409] }, + { + id: "global-search-sensitive", + path: "/api/search?q=TEST&limit=10", + statuses: [200, 409], + searchExpectations: { + forbiddenEntityTypes: ["contract_registry", "confidential_doc_ref", "travel_request"], + forbiddenHrefPrefixes: ["/contracts-registry", "/travel-requests"], + }, + }, ], }, { @@ -255,6 +313,9 @@ const ROLE_TARGETS = [ { id: "audit-logs", path: "/api/audit-logs?pageSize=5", statuses: [200] }, { id: "notification-deliveries", path: "/api/notifications/deliveries?limit=5", statuses: [200] }, { id: "system-feature-flags", path: "/api/system/feature-flags", statuses: [200] }, + { id: "payroll-items", path: "/api/payroll/items", statuses: [200] }, + { id: "performance-cycles", path: "/api/performance/cycles", statuses: [200, 409] }, + { id: "global-search", path: "/api/search?q=TEST&limit=10", statuses: [200, 409] }, { id: "assets", path: "/api/assets?pageSize=5", statuses: [200] }, { id: "navigation", @@ -384,6 +445,33 @@ function evaluateNavigationExpectations(json, expectations) { return [...visible, ...hidden]; } +function evaluateSearchExpectations(json, expectations) { + if (!expectations || !Array.isArray(json?.data?.items)) return []; + const items = json.data.items.filter((item) => item && typeof item === "object"); + const forbiddenEntityTypes = new Set(expectations.forbiddenEntityTypes || []); + const forbiddenHrefPrefixes = expectations.forbiddenHrefPrefixes || []; + const results = []; + for (const entityType of forbiddenEntityTypes) { + const matchedCount = items.filter((item) => item.entityType === entityType).length; + results.push({ + type: "forbidden_entity_type", + value: entityType, + status: matchedCount === 0 ? "PASS" : "FAIL", + matchedCount, + }); + } + for (const hrefPrefix of forbiddenHrefPrefixes) { + const matchedCount = items.filter((item) => typeof item.href === "string" && item.href.startsWith(hrefPrefix)).length; + results.push({ + type: "forbidden_href_prefix", + value: hrefPrefix, + status: matchedCount === 0 ? "PASS" : "FAIL", + matchedCount, + }); + } + return results; +} + function collectSensitiveKeyHits(value, currentPath = "data", hits = []) { if (hits.length >= 12) return hits; if (Array.isArray(value)) { @@ -471,17 +559,20 @@ async function probe(accessToken, probe, kind) { const expected = probe.statuses.includes(response.status); const dataExpectations = response.ok ? evaluateDataExpectations(response.json, probe.dataExpectations) : []; const navigationExpectations = response.ok ? evaluateNavigationExpectations(response.json, probe.navigationExpectations) : []; + const searchExpectations = response.ok ? evaluateSearchExpectations(response.json, probe.searchExpectations) : []; const dataExpectationFailed = dataExpectations.some((item) => item.status === "FAIL"); const navigationExpectationFailed = navigationExpectations.some((item) => item.status === "FAIL"); + const searchExpectationFailed = searchExpectations.some((item) => item.status === "FAIL"); const sensitiveKeyHits = response.ok ? collectSensitiveKeyHits(response.json?.data).slice(0, 12) : []; return { id: probe.id, kind, - status: expected && !dataExpectationFailed && !navigationExpectationFailed && sensitiveKeyHits.length === 0 ? "PASS" : "FAIL", + status: expected && !dataExpectationFailed && !navigationExpectationFailed && !searchExpectationFailed && sensitiveKeyHits.length === 0 ? "PASS" : "FAIL", httpStatus: response.status, expectedStatuses: probe.statuses, dataExpectations, navigationExpectations, + searchExpectations, sensitiveKeyHits, summary: response.ok ? summarizeData(response.json) : { code: response.json?.error?.code ?? null, @@ -544,6 +635,7 @@ function collectIssues(results) { code: probeResult.summary?.code ?? null, failedDataExpectations: probeResult.dataExpectations?.filter((item) => item.status === "FAIL") || [], failedNavigationExpectations: probeResult.navigationExpectations?.filter((item) => item.status === "FAIL") || [], + failedSearchExpectations: probeResult.searchExpectations?.filter((item) => item.status === "FAIL") || [], sensitiveKeyHits: probeResult.sensitiveKeyHits || [], }); }