diff --git a/issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv b/issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv index 59a24c57..b38d8143 100644 --- a/issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv +++ b/issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv @@ -5,6 +5,6 @@ "WREMOTE1-3","准备 WREAL1 临时 env 模板","在 /tmp/hyb-oa-wreal1.env 准备真实试用临时配置占位,供用户后续填写真实员工和审批人/观察人,不进仓库。","1) 文件只在 /tmp;2) 只包含注释和空值占位;3) 不输出真实员工号;4) 后续只校验非空和脱敏。","受影响流程:WREAL1 启动前人工配置 -> 系统脱敏读取;验证步骤:test -f /tmp/hyb-oa-wreal1.env;awk -F= '/^WREAL1_/ {print $1""=""($2==""""?""EMPTY"":""SET"")}' /tmp/hyb-oa-wreal1.env 确认只输出 key 和 SET/EMPTY,不输出真实员工号。","bash","DONE","DONE","DONE","/tmp/hyb-oa-wreal1.env","[""WREMOTE1-2""]","回滚/清理:删除 /tmp/hyb-oa-wreal1.env;该文件不纳入 git。" "WREMOTE1-4","只读 reviewer 审查远端与正式模板边界","涉及审批规则、权限和真实试用准入,实施后由只读 reviewer 检查是否可能误用本地结果、误授权企微写入或跳过人工门禁。","1) reviewer 只读;2) 审查 WFULLSMOKE2 本地默认阻断;3) 审查 WEXT1-FORMAL 不创建模板、不自动提交审批;4) 审查 WREAL1 信息不入仓库;5) blocker 必须修复。","受影响流程:脚本/文档修正 -> 独立评审 -> 门禁回填;验证步骤:reviewer subagent 输出无 blocker 或 blocker 已处理。","subagent|manual","DONE","DONE","DONE","N/A","[""WREMOTE1-3""]","回滚/清理:reviewer 不修改文件;如发现风险,暂停部署和远端执行。" "WREMOTE1-5","本地静态门禁复跑","复跑合同、process gate、脚本语法、文档审计、skill 校验、diff 检查和最小回归。","1) validate_issues_csv 通过;2) baseline 和 issue-specific process gate 通过;3) bash -n 通过;4) audit-docs、quick_validate、git diff --check 通过;5) bash scripts/check-minimal.sh 通过。","受影响流程:本地静态门禁 -> 提交 -> 部署;验证步骤:validate_issues_csv.py ;node scripts/qa/check-process-contract.mjs --baseline --json;node scripts/qa/check-process-contract.mjs --issue --profile wecom --json;bash -n scripts/qa/run-three-day-full-goal-smoke.sh scripts/qa/run-wecom-test-account-final-closeout.sh scripts/qa/collect-wecom-controlled-trial-evidence.sh;bash scripts/check-minimal.sh。","python3|node|bash|git","DONE","DONE","DONE","scripts/qa/run-three-day-full-goal-smoke.sh|scripts/qa/run-wecom-test-account-final-closeout.sh|scripts/qa/collect-wecom-controlled-trial-evidence.sh|docs/*|.agents/skills/hyb-oa-delivery/SKILL.md","[""WREMOTE1-4""]","回滚/清理:若门禁失败,保留 TODO 并暂停部署。" -"WREMOTE1-6","部署并远端执行 WTCLOSE2","本地门禁通过并提交后,使用 SYNC_ENV=0 部署/同步,然后从部署服务器可信 IP 执行 WTCLOSE2 远端收口。","1) SYNC_ENV=0 部署成功;2) /ready 正常;3) bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote 生成远端报告;4) 报告不泄露完整 sp_no/template_id/userid/token/Secret/表单值/原始 payload。","受影响流程:部署 -> 远端 WTCLOSE2 -> WEXT1-FORMAL 问答;验证步骤:SYNC_ENV=0 bash deploy/trialrun/deploy.sh;curl -fsS https://oa.hyinda.com/ready;bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote。","bash|ssh|curl","TODO","TODO","TODO","deploy/trialrun/*|scripts/qa/run-wecom-test-account-final-closeout.sh","[""WREMOTE1-5""]","回滚/清理:若部署或远端 WTCLOSE2 阻断,记录真实 SKIP/BLOCKED_BY_EXTERNAL_CONFIG;不继续正式模板测试。" -"WREMOTE1-7","远端执行 WFULLSMOKE2 全域 smoke","远端运行 WFULLSMOKE2,允许创建 TEST-SMOKE-WFULL 标记的 OA 主办域测试数据,验证全域主路径。","1) 远端执行全域 smoke;2) 只创建 TEST-SMOKE-WFULL 标记 OA 测试业务数据;3) 不写企业微信、不真实付款、不接支付、不写 ERP、不导入银行流水;4) 输出 PASS_FULL_GOAL_SMOKE_READY、PASS_WITH_FINDINGS、PAUSED_MANUAL_CONFIRMATION 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。","受影响流程:OA 报销 -> 采购 -> 合同/项目 -> 付款批次 -> 移动/企微入口;验证步骤:WFULLSMOKE_ALLOW_PUBLIC_WRITES=1 bash scripts/qa/run-three-day-full-goal-smoke.sh --remote;核对 summary.json 和测试标记。","bash|ssh|playwright","TODO","TODO","TODO","scripts/qa/run-three-day-full-goal-smoke.sh","[""WREMOTE1-6""]","回滚/清理:测试数据保留审计,不自动清理;若凭据缺失,记录 PASS_WITH_FINDINGS 或 PAUSED_MANUAL_CONFIRMATION。" -"WREMOTE1-8","提交并报告后续人工门禁","回填 WREMOTE1 状态并中文提交;远端 WTCLOSE2/WFULLSMOKE2 结果决定是否进入正式模板外出/加班人工提交问答。","1) CSV 状态按真实验证回填;2) 中文提交包含 WREMOTE1;3) 最终说明是否已部署、远端报告路径和下一步人工操作;4) 若未远端执行,明确阻断原因。","受影响流程:提交 -> 人工 WEXT1-FORMAL 问答 -> WREAL1;验证步骤:git status --short;git log -1 --oneline。","git","TODO","TODO","TODO","issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv|scripts/qa/run-three-day-full-goal-smoke.sh|docs/*|.agents/skills/hyb-oa-delivery/SKILL.md","[""WREMOTE1-7""]","回滚/清理:revert 本提交;WEXT1-FORMAL 和 WREAL1 仍需后续人工确认。" +"WREMOTE1-6","部署并远端执行 WTCLOSE2","本地门禁通过并提交后,使用 SYNC_ENV=0 部署/同步,然后从部署服务器可信 IP 执行 WTCLOSE2 远端收口。","1) SYNC_ENV=0 部署成功;2) /ready 正常;3) bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote 生成远端报告;4) 报告不泄露完整 sp_no/template_id/userid/token/Secret/表单值/原始 payload。","受影响流程:部署 -> 远端 WTCLOSE2 -> WEXT1-FORMAL 问答;验证步骤:SYNC_ENV=0 bash deploy/trialrun/deploy.sh;curl -fsS https://oa.hyinda.com/ready;bash scripts/qa/run-wecom-test-account-final-closeout.sh --remote。","bash|ssh|curl","DONE","DONE","DONE","deploy/trialrun/*|scripts/qa/run-wecom-test-account-final-closeout.sh|server/src/services/integrations/wecomApprovalArchive.js|server/tests/unit/services/wecomApprovalArchive.test.js","[""WREMOTE1-5""]","远端部署通过,/ready OK;WTCLOSE2 报告 /app/artifacts/qa-flow/wecom-test-account-final-closeout-20260522-055840/summary.json,结论 SKIP/BLOCKED_BY_EXTERNAL_CONFIG:新补偿归档中请假/补卡各 1 条已可 WRULE1 PASS_RULE_MATCH;历史 callback-only 旧行缺 applicant/approver,外出/加班尚无正式模板归档,需 WEXT1-FORMAL 人工提交后复核;隐私扫描通过。回滚/清理:恢复脚本和解析修正;不写企微。" +"WREMOTE1-7","远端执行 WFULLSMOKE2 全域 smoke","远端运行 WFULLSMOKE2,允许创建 TEST-SMOKE-WFULL 标记的 OA 主办域测试数据,验证全域主路径。","1) 远端执行全域 smoke;2) 只创建 TEST-SMOKE-WFULL 标记 OA 测试业务数据;3) 不写企业微信、不真实付款、不接支付、不写 ERP、不导入银行流水;4) 输出 PASS_FULL_GOAL_SMOKE_READY、PASS_WITH_FINDINGS、PAUSED_MANUAL_CONFIRMATION 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。","受影响流程:OA 报销 -> 采购 -> 合同/项目 -> 付款批次 -> 移动/企微入口;验证步骤:WFULLSMOKE_ALLOW_PUBLIC_WRITES=1 bash scripts/qa/run-three-day-full-goal-smoke.sh --remote;核对 summary.json 和测试标记。","bash|ssh|playwright","DONE","DONE","DONE","scripts/qa/run-three-day-full-goal-smoke.sh","[""WREMOTE1-6""]","WFULLSMOKE2 远端报告 /app/artifacts/qa-flow/three-day-full-goal-smoke-20260522-055953/summary.json,结论 PASS_WITH_FINDINGS;/ready、WARCH3、WAUTO1、隐私扫描通过;OA public write flows 因缺 LIVE_SMOKE_* 业务凭据未创建测试数据;移动/企微入口 spec 未打入运行镜像,记录 finding;未写企业微信。" +"WREMOTE1-8","提交并报告后续人工门禁","回填 WREMOTE1 状态并中文提交;远端 WTCLOSE2/WFULLSMOKE2 结果决定是否进入正式模板外出/加班人工提交问答。","1) CSV 状态按真实验证回填;2) 中文提交包含 WREMOTE1;3) 最终说明是否已部署、远端报告路径和下一步人工操作;4) 若未远端执行,明确阻断原因。","受影响流程:提交 -> 人工 WEXT1-FORMAL 问答 -> WREAL1;验证步骤:git status --short;git log -1 --oneline。","git","DONE","DONE","DONE","issues/2026-05-22_12-34-41-wecom-remote-closeout-and-full-smoke-v1.csv|scripts/qa/run-three-day-full-goal-smoke.sh|scripts/qa/run-wecom-test-account-final-closeout.sh|server/src/services/integrations/wecomApprovalArchive.js|server/tests/unit/services/wecomApprovalArchive.test.js","[""WREMOTE1-7""]","本轮回填远端报告路径和真实结论;WEXT1-FORMAL 不自动开始,需用户按 A/B/C 选择是否由 SMOKE002 人工提交正式模板外出+加班测试单。回滚/清理:恢复本轮 CSV 状态和脚本解析/报告口径修正;不删除远端报告,正式模板人工测试另行授权。" diff --git a/scripts/qa/run-three-day-full-goal-smoke.sh b/scripts/qa/run-three-day-full-goal-smoke.sh index 47db2923..4802379c 100755 --- a/scripts/qa/run-three-day-full-goal-smoke.sh +++ b/scripts/qa/run-three-day-full-goal-smoke.sh @@ -305,8 +305,8 @@ write_manual_checklist() { ## Day 2 - SMOKE002:继续按企微规则打卡。 -- HYD000006:检查请假、补卡、外出、加班四类模板审批规则。 -- WAPPR1 选项:A 审批人自动带出且申请人不能任意选择;B 默认带出但申请人仍可改人;C 必须申请人手选;D 当前看不到或模板未配置。只有 A 放行。 +- HYD000006:已在 2026-05-21 真机确认请假、补卡、外出、加班四类模板均为 A。 +- WAPPR1 当前结论:A 审批人自动带出且申请人不能任意选择;申请人不能删除、改人或新增审批人。若后续模板配置变化,B/C/D 均重新阻断 WEXT1-FORMAL 和 WREAL1。 - 小团队规则:公司不到 10 人,默认一层直属主管或指定审批角色自动带出审批人;主管链缺失、审批人不可见/停用/未绑定或命中自审时,由管理员兜底或阻断。 ## Day 3 @@ -437,9 +437,9 @@ const manualGates = [ }, { id: "WAPPR1", - status: "PENDING_MANUAL_CONFIRMATION", + status: "PASS_APPROVER_RULES_READY", owner: "HYD000006", - requiredEvidence: "四类模板均为 A:一层主管/指定审批角色自动带出,申请人不能任意选择;缺主管/不可用/自审时管理员兜底或阻断", + requiredEvidence: "2026-05-21 用户真机确认请假、补卡、外出、加班均为 A:审批人自动带出,申请人不能删除、改人或新增审批人", }, { id: "WEXT1-FORMAL", @@ -466,6 +466,7 @@ if (Number(statusInput || 0) !== 0 || findings.some((item) => item.severity === const summary = { issueId: "WFULLSMOKE1", + routeIssueId: "WFULLSMOKE2", mode: "three-day-full-goal-smoke", status, conclusion: status, diff --git a/scripts/qa/run-wecom-test-account-final-closeout.sh b/scripts/qa/run-wecom-test-account-final-closeout.sh index ece935f8..4276b567 100755 --- a/scripts/qa/run-wecom-test-account-final-closeout.sh +++ b/scripts/qa/run-wecom-test-account-final-closeout.sh @@ -213,8 +213,9 @@ run_wrule1() { run_privacy_smoke() { local sensitive_pattern='(["'\'']?(WECOM_[A-Z0-9_]*(SECRET|TOKEN)|[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|ENCODING_AES_KEY|ENCODINGAESKEY|CORP_SECRET|ACCESS_TOKEN)[A-Z0-9_]*)["'\'']?[[:space:]]*[:=][[:space:]]*["'\'']?[A-Za-z0-9._:/+=-]{8,}["'\'']?)|Authorization:[[:space:]]*Bearer[[:space:]]+[A-Za-z0-9._-]{8,}|access_token=[A-Za-z0-9._-]{8,}|1[3-9][0-9]{9}|[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}|-----BEGIN|BEGIN [A-Z ]*KEY|"[^"]*(token|secret|password|encodingAESKey|encoding_aes_key|sp_no|spNo|template_id|templateId|userid|userId|external_userid|externalUserId|external_user_id)[^"]*"[[:space:]]*:[[:space:]]*"[^"]{8,}"|\b20[0-9]{12,}\b|"(raw|rawPayload|raw_payload|payload|xml|ciphertext|Encrypt|formValue|formValues|attachments|comments)"[[:space:]]*:' local safe_metadata_pattern='"[^"]*(secret|token)[^"]*"[[:space:]]*:[[:space:]]*"WECOM_[A-Z0-9_]*(SECRET|TOKEN)([[:space:]]|["]|$)|"[^"]*(secret|token)[^"]*"[[:space:]]*:[[:space:]]*"WECOM_[A-Z0-9_]*(SECRET|TOKEN)[^"]*(source|path|read paths|fallback)' + local safe_redacted_prefix_pattern='"[^"]*(sp_no|spNo|template_id|templateId|userid|userId|external_userid|externalUserId|external_user_id)[^"]*Prefixes?"[[:space:]]*:[[:space:]]*"[^"]{1,16}\*\*\*[^"]{0,8}"' local matches - matches="$(grep -RInEi "$sensitive_pattern" "$REPORT_DIR" | grep -vE 'HRWDTEST-[0-9]{14}' | grep -vEi "$safe_metadata_pattern" || true)" + matches="$(grep -RInEi "$sensitive_pattern" "$REPORT_DIR" | grep -vE 'HRWDTEST-[0-9]{14}' | grep -vEi "$safe_metadata_pattern" | grep -vEi "$safe_redacted_prefix_pattern" || true)" if [[ -n "$matches" ]]; then printf '%s\n' "$matches" log "[wtclose2][FAIL] report-privacy-smoke matched sensitive pattern" diff --git a/server/src/services/integrations/wecomApprovalArchive.js b/server/src/services/integrations/wecomApprovalArchive.js index e57b7faf..d304d93b 100644 --- a/server/src/services/integrations/wecomApprovalArchive.js +++ b/server/src/services/integrations/wecomApprovalArchive.js @@ -164,10 +164,20 @@ function normalizeFields(info) { .filter((item) => item.field_key); } -function extractUserId(value) { +function extractUserId(value, depth = 0) { if (!value) return ""; if (typeof value === "string") return toSafeString(value, 128); - return pickText(value, ["userid", "user_id", "UserId", "UserID", "open_userid", "OpenUserId"], 128); + if (depth > 3 || typeof value !== "object") return ""; + + const direct = pickText(value, ["userid", "user_id", "UserId", "UserID", "open_userid", "OpenUserId"], 128); + if (direct) return direct; + + for (const key of ["approver", "user", "member", "operator", "handler", "applyer", "applicant", "creator"]) { + const nested = extractUserId(value[key], depth + 1); + if (nested) return nested; + } + + return ""; } function normalizeParticipants(info) { diff --git a/server/tests/unit/services/wecomApprovalArchive.test.js b/server/tests/unit/services/wecomApprovalArchive.test.js index 4256996e..579dba0f 100644 --- a/server/tests/unit/services/wecomApprovalArchive.test.js +++ b/server/tests/unit/services/wecomApprovalArchive.test.js @@ -111,6 +111,36 @@ describe("wecomApprovalArchive", () => { expect(detail.detail_digest).toMatch(/^[0-9a-f]{64}$/); }); + it("normalizes nested approval record approvers", () => { + const nestedApprover = "nested_approver_full_userid_should_not_leak"; + const detail = normalizeApprovalDetailPayload(approvalPayload({ + sp_record: [{ + details: [{ + approver: { userid: nestedApprover }, + sp_status: "2", + speech_time: "1779149800", + speech: "同意", + }], + }], + }), { + source: "compensation_pull", + target: "leave", + }); + + expect(detail.participants).toEqual(expect.arrayContaining([ + expect.objectContaining({ + role: "approver", + external_user_id: nestedApprover, + }), + ])); + expect(detail.actions).toEqual(expect.arrayContaining([ + expect.objectContaining({ + action_type: "approve", + operator_external_user_id: nestedApprover, + }), + ])); + }); + it("returns only masked public archive summaries", () => { const summary = publicArchiveSummary({ id: "archive-1",