LAUTO 收口正式启用后自动化巡检

这个提交包含在:
2026-05-17 18:08:07 +08:00
父节点 0922f006d1
当前提交 70fdd1d2ed
共修改 8 个文件,包含 633 行新增和 16 行删除
@@ -96,6 +96,9 @@ const reportDir = process.env.P2_FIN_LIVE_REPORT_DIR?.trim() ||
path.resolve(process.cwd(), "..", "artifacts/qa-flow/p2-fin-live-local");
const summaryPath = path.join(reportDir, "live-result.json");
const finalFlagState = process.env.P2_FIN_LIVE_FINAL_STATE?.trim() === "force_on" ? "force_on" : "force_off";
const alreadyOnMode =
process.env.P2_FIN_LIVE_ALREADY_ON === "1" ||
process.env.LAUNCH_AUTO_PAYMENT_BATCH_ALREADY_ON === "1";
function nowMarker() {
return `${markerPrefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
@@ -406,8 +409,13 @@ test.describe("公网付款批次受控验收", () => {
summary.stages.preflight = {
paymentFlagBefore: snapshotFlag(paymentFlagBefore),
erpSyncEffectiveEnabled: erpFlagBefore.effectiveEnabled,
alreadyOnMode,
};
expect(paymentFlagBefore.effectiveEnabled, "payment_batch_v1 should be off before public live window").toBe(false);
if (alreadyOnMode) {
expect(paymentFlagBefore.effectiveEnabled, "payment_batch_v1 should already be on").toBe(true);
} else {
expect(paymentFlagBefore.effectiveEnabled, "payment_batch_v1 should be off before public live window").toBe(false);
}
expect(erpFlagBefore.effectiveEnabled, "erp_sync must remain off for this test").toBe(false);
const reimbursement = await createTestReimbursement(employee.api, marker);
@@ -428,12 +436,14 @@ test.describe("公网付款批次受控验收", () => {
financeTaskApproved: Boolean(approval.financeTaskId),
};
const paymentFlagOn = await updateFlag(
admin.api,
"payment_batch_v1",
"force_on",
`${issueId} short window ${marker}`,
);
const paymentFlagOn = alreadyOnMode
? paymentFlagBefore
: await updateFlag(
admin.api,
"payment_batch_v1",
"force_on",
`${issueId} short window ${marker}`,
);
summary.stages.flagOn = { paymentFlagOn: snapshotFlag(paymentFlagOn) };
expect(paymentFlagOn.effectiveEnabled).toBe(true);
const erpFlagAfterPaymentOn = await loadFlag(admin.api, "erp_sync");
@@ -512,10 +522,12 @@ test.describe("公网付款批次受控验收", () => {
});
expect(exportResponse.status(), "finance AP export should succeed while flag is on").toBe(200);
await expectAuditAction(admin.api, "FEATURE_FLAG_UPDATED", (item) =>
item.extra?.flagKey === "payment_batch_v1" &&
(item.extra?.to as Record<string, unknown> | undefined)?.overrideMode === "force_on",
);
if (!alreadyOnMode) {
await expectAuditAction(admin.api, "FEATURE_FLAG_UPDATED", (item) =>
item.extra?.flagKey === "payment_batch_v1" &&
(item.extra?.to as Record<string, unknown> | undefined)?.overrideMode === "force_on",
);
}
await expectAuditAction(admin.api, "PAYMENT_BATCH_CREATED", (item) => item.targetId === batch.id);
await expectAuditAction(admin.api, "PAYMENT_BATCH_REVIEWED", (item) => item.targetId === batch.id);
await expectAuditAction(admin.api, "PAYMENT_BATCH_RECEIPT_UPLOADED", (item) =>
@@ -546,7 +558,19 @@ test.describe("公网付款批次受控验收", () => {
} finally {
if (admin) {
try {
if (finalFlagState === "force_on" && !caughtError) {
if (alreadyOnMode) {
const finalPaymentFlag = await loadFlag(admin.api, "payment_batch_v1");
const finalErpFlag = await loadFlag(admin.api, "erp_sync");
summary.stages.finalState = {
paymentFlag: snapshotFlag(finalPaymentFlag),
erpSyncEffectiveEnabled: finalErpFlag.effectiveEnabled,
alreadyOnMode,
};
expect(finalPaymentFlag.effectiveEnabled).toBe(true);
expect(finalPaymentFlag.overrideMode).toBe("force_on");
expect(finalFlagState).toBe("force_on");
expect(finalErpFlag.effectiveEnabled).toBe(false);
} else if (finalFlagState === "force_on" && !caughtError) {
const paymentFlag = await loadFlag(admin.api, "payment_batch_v1");
const finalPaymentFlag =
paymentFlag.overrideMode === "force_on" && paymentFlag.effectiveEnabled
+3
查看文件
@@ -35,6 +35,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
- 2026-05-17 LAUNCH-OPS-V1 已固定首周正式使用运营口径:`wangshunli` 为上线拍板、管理员复核和回滚决策人,`wangzhiling` 为出纳/付款执行和代账资料准备人;首周问题登记、每日巡检、暂停条件、代账交接节奏和 UX8/可选功能冻结口径已回写现有手册。后续新增功能只按真实反馈和业务频次另建 Issue CSV。
- 2026-05-17 REQ-COMPLETE-AUDIT 已完成需求完成度复核和考勤边界校正:首批正式能力保持 `GO_TO_USE` 结论不变;企业微信打卡作为原始采集和风控底座,OA 不重造定位、人脸、Wi-Fi、排班、设备和防作弊。
- 2026-05-17 ATTV1 已完成考勤统一入口实现并部署到公网:历史 OA 自建打卡 `attendance_v1` 关闭;OA 新增企微打卡摘要同步、补卡/外出/加班修正审批和月度考勤解释;所有审批继续统一在 OA,企业微信只负责打卡原始采集和风控。公网复检显示旧打卡 API 返回 409、统一考勤月度汇总和修正列表可读、移动/企业微信矩阵 14 passed;企微 live 同步依赖 `WECOM_CHECKIN_SECRET` 和企微打卡 API 权限,缺配置时记 `SKIP`。
- 2026-05-17 LAUNCH-AUTO-OPS-V1 已完成正式启用后自动化收口脚本和证据刷新:本地 Prisma/node check/focused 单测、前端 typecheck/build、公网 readiness、PDF live OCR、统一考勤 live 检查、公网移动矩阵和企业微信 UA/渲染矩阵均通过;显式企微打卡同步检查返回 `SKIP wecom_checkin_sync_disabled`;但只读 flag 检查发现当前公网 `payment_batch_v1=force_off`,与 FIN-PROD-ENABLE 记录的正式常开口径不一致,结论为 `PAUSE_FOR_FIX`。runner 已跳过付款批次写入,未切换 flag、未部署、不接真实支付、不接 ERP。
## 关键证据索引
@@ -77,6 +78,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
| LAUNCH-OPS-V1 | 已收口:首周运营保障口径固定;支持人、问题登记、每日巡检、暂停条件、代账交接节奏和可选功能冻结口径已回写手册 | `issues/2026-05-17_12-55-50-launch-ops-v1.csv`、`docs/管理员手册.md`、`docs/员工快速上手.md`、`docs/试运行与自动化验证手册.md` |
| REQ-COMPLETE-AUDIT | 已收口:需求完成度复核和考勤边界校正完成;企微打卡与 OA 考勤解释边界已纠偏,补卡/外出/加班缺口已分级 | `issues/2026-05-17_13-32-26-requirements-completion-audit.csv`、`docs/海音达OA-MVP需求规格说明书.md`、`docs/需求-请假考勤修正规约.md`、`docs/需求-企业微信基础设施规约.md` |
| ATTV1 | 已收口:考勤统一入口与企微打卡同步 V1 已部署公网;关闭 OA 自建打卡,新增企微打卡摘要同步、OA 补卡/外出/加班审批和月度解释;公网 readiness、移动/企微矩阵和考勤关键接口复检通过 | `issues/2026-05-17_14-59-28-attendance-unified-wecom-sync-v1.csv`、`artifacts/qa-flow/live-smoke-readiness-20260517-171535/summary.json`、`docs/需求-请假考勤修正规约.md`、`docs/需求-企业微信基础设施规约.md` |
| LAUNCH-AUTO-OPS-V1 | 阶段收口:自动化证据完成,结论 `PAUSE_FOR_FIX`;除付款批次正式 flag 漂移外,本地门禁、公网 readiness/PDF live OCR、统一考勤、公网移动和企业微信 UA/渲染矩阵均通过;付款批次写入因前置失败被跳过 | `issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv`、`artifacts/qa-flow/launch-auto-ops-20260517-175548/summary.json`、`artifacts/qa-flow/live-smoke-readiness-20260517-175755/summary.json` |
## 当前门禁
@@ -109,6 +111,7 @@ DCL7 已删除本文件中 R70-R91 的长流水历史,避免旧环境、旧开
| LAUNCH-OPS-V1 首周运营保障 | 通过,无新增业务功能 | 管理员手册、员工快速上手和试运行手册已更新正式启用口径;首周问题登记、每日巡检、暂停条件、回滚联系人、代账交接节奏和 UX8/冻结项处理方式已固定。 |
| REQ-COMPLETE-AUDIT 需求完成度复核 | 通过,无运行时变更 | 完成首批能力和考勤边界复核;企业微信打卡为原始采集,OA 不重造打卡风控。 |
| ATTV1 考勤统一入口 | 通过,已部署 | 部署提交 `916cc3b` + 移动入口修正 `b2b4447`;历史 OA 自建打卡关闭,补卡、外出、加班已进入 OA 统一审批和月度解释;公网 `/ready`、live smoke、PDF live OCR、移动/企业微信矩阵 14 passed;企微 live 同步缺配置时按 `SKIP` 记录。 |
| LAUNCH-AUTO-OPS-V1 正式启用后自动化收口 | 暂停,结论 `PAUSE_FOR_FIX` | `run-launch-auto-ops.sh` 报告 `artifacts/qa-flow/launch-auto-ops-20260517-175548/`;本地门禁、live readiness、PDF live OCR、考勤统一入口、公网移动矩阵 6 passed、企业微信渲染矩阵 8 passed;只读 flag 检查失败:`payment_batch_v1=force_off`,期望为正式常开 `force_on`,因此付款批次写入回归被安全跳过。 |
## R7EP / OPS8 / QAF8 / UX8 / P2T 预检推进
+4 -3
查看文件
@@ -56,6 +56,7 @@
| `LAUNCH-OPS-V1` | `issues/2026-05-17_12-55-50-launch-ops-v1.csv` | `EVIDENCE` | 首周正式使用运营保障已收口;支持人、问题登记、每日巡检、暂停条件、回滚联系人、代账交接节奏和冻结项口径已固定 |
| `REQ-COMPLETE-AUDIT` | `issues/2026-05-17_13-32-26-requirements-completion-audit.csv` | `EVIDENCE` | 需求完成度复核和考勤边界校正已收口;首批能力可交付不变,企业微信打卡作为原始采集和风控底座,OA 不重造定位/人脸/Wi-Fi/排班/防作弊;员工补卡/外出/加班审批和企微打卡 API 同步需后续合同 |
| `ATTV1` | `issues/2026-05-17_14-59-28-attendance-unified-wecom-sync-v1.csv` | `EVIDENCE` | 考勤统一入口与企微打卡同步 V1 已收口:提交 `916cc3b` 和 `b2b4447` 已部署到公网;历史 OA 自建打卡关闭,补卡/外出/加班 OA 审批和月度解释可用;公网 readiness、PDF live OCR、移动/企业微信矩阵 14 passed,旧打卡 API 返回 409,企微打卡同步缺配置时记录 `SKIP` |
| `LAUNCH-AUTO-OPS-V1` | `issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv` | `EVIDENCE` | 正式启用后自动化收口已阶段完成,结论 `PAUSE_FOR_FIX`:本地门禁、公网 readiness/PDF live OCR、考勤统一入口、移动矩阵和企业微信 UA/渲染矩阵通过;企微打卡同步显式检查为 `SKIP wecom_checkin_sync_disabled`;公网 `payment_batch_v1=force_off` 与正式常开口径不一致,付款批次写入已安全跳过 |
旧 Issue CSV 保留为历史审计证据:不删除、不批量回写、不移动到 `docs/archive/` 或其他归档目录。新连续合同如替代旧合同,只在新合同 `Notes` 或文档索引中说明 supersedes。
@@ -63,8 +64,8 @@
| Doc_ID | Path | Category | Authority | Status | Owner | Related_Issues | Evidence | Supersedes | Last_Audited | Audit_Method | Risk | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1` | `rg` | none | 2026-05-17 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 |
| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1` | `audit-docs` | none | 2026-05-17 | `audit-docs --check-registry` | medium | 判断文档权威性 |
| `IDX` | `docs/文档索引.md` | 入口索引 | 最高文档入口 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1` | `rg` | none | 2026-05-17 | `node scripts/qa/audit-docs.mjs --check-registry` | medium | 判断当前该读什么 |
| `REG` | `docs/文档事实源状态矩阵.md` | 文档台账 | 文档分类和状态事实源 | `CURRENT` | product/dev | `GOV8,QAOPS9,P2D,P2-ORG,P2-FIN-DECISION,P2-FIN-V1,P2-OCR-SAMPLE,P2Q,P2AUTO,P2AF,P2-FIN-LIVE-DECISION,P2-FIN-LIVE,PRELAUNCH,CASHIER-VOUCHER-V1,LAUNCH-DEPLOY-FINANCE,FIN-PROD-ENABLE,ACCOUNTING-HANDOFF-V1,SYSTEM-COMPLETE-GATE,LAUNCH-OPS-V1,REQ-COMPLETE-AUDIT,ATTV1,LAUNCH-AUTO-OPS-V1` | `audit-docs` | none | 2026-05-17 | `audit-docs --check-registry` | medium | 判断文档权威性 |
| `PRD-MVP` | `docs/海音达OA-MVP需求规格说明书.md` | 需求事实源 | PRD 主事实源 | `CURRENT` | product | `REQ8,P2T,GOV8` | Issue CSV | `docs/requirements-specification.md` | 2026-05-13 | `rg` | high | 定义做什么、不做什么和验收口径 |
| `PRODUCT-SCOPE` | `docs/产品口径.md` | 需求事实源 | 产品边界事实源 | `CURRENT` | product | `REQ8,P2T` | `rg` | none | 2026-05-13 | `rg` | high | 记录已拍板产品边界 |
| `DESIGN-UI` | `docs/产品设计规则.md` | 设计事实源 | PC/移动/企业微信设计事实源 | `CURRENT` | design/frontend | `MIA7,MIF7,GOV8,DG8,DG9` | Playwright/视觉审查/DG8/DG9 | none | 2026-05-13 | `rg` | high | 页面改动必须读取;DG8 后飞书为主 UI/布局参考;DG9 后按页面分层和样板选型 |
@@ -169,4 +170,4 @@
- `ME6-3` 代表性企业微信真机门禁已关闭。
- MIF7-1..7 与 MIF7T 已完成本地整改、公网部署、公网匿名复检和带凭据 live smoke/OCR 复检。
- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核和 ATTV1 考勤统一入口均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同;当前公网 `payment_batch_v1=force_on`,`erp_sync=force_off`,正式支付接口、ERP、图片 OCR 和银行流水导入继续冻结。ATTV1 已部署:历史 OA 自建打卡 `attendance_v1` 关闭,OA 补卡/外出/加班审批和月度考勤解释已纳入统一考勤主线;企微打卡 live 同步能力依赖 `WECOM_CHECKIN_SECRET` 和企微 API 权限,缺配置时记录 `SKIP`。
- R7E 真实扩大试运行阶段验收、P2 排序、R7E 收口、QAOPS8 上线硬化阶段收口、QAOPS9 上线硬化连续合同、PRELAUNCH 正式启用前公网预回归、CASHIER-VOUCHER-V1 财务闭环与银行回单附件、LAUNCH-DEPLOY-FINANCE 部署复检、FIN-PROD-ENABLE 正式常开、ACCOUNTING-HANDOFF-V1 代账交接 SOP、SYSTEM-COMPLETE-GATE 系统完成门禁、LAUNCH-OPS-V1 首周运营保障、REQ-COMPLETE-AUDIT 需求完成度复核和 ATTV1 考勤统一入口均已完成,当前 P0/P1 阻断为 0;P2D 已完成决策分流,P2-ORG 只读巡检已完成且无需写修复合同。LAUNCH-AUTO-OPS-V1 发现当前公网 `payment_batch_v1=force_off`,与 FIN-PROD-ENABLE 的正式常开口径不一致,自动化结论为 `PAUSE_FOR_FIX`;`erp_sync=force_off`,正式支付接口、ERP、图片 OCR 和银行流水导入继续冻结。ATTV1 已部署:历史 OA 自建打卡 `attendance_v1` 关闭,OA 补卡/外出/加班审批和月度考勤解释已纳入统一考勤主线;企微打卡 live 同步能力依赖 `WECOM_CHECKIN_SECRET` 和企微 API 权限,缺配置时记录 `SKIP`。
+3 -1
查看文件
@@ -20,7 +20,7 @@
## 最近事实源链
R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1。
R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> MIF7/MIF7T(公网部署与带凭据公网复检已收口) -> DG8 -> DG9 -> R7E -> FBA -> QAOPS9/P2D -> P2-ORG -> P2-FIN-DECISION -> P2-FIN-V1 -> P2-OCR-SAMPLE -> P2Q -> P2AUTO -> P2AF -> P2-FIN-LIVE-DECISION -> P2-FIN-LIVE -> PRELAUNCH -> CASHIER-VOUCHER-V1 -> LAUNCH-DEPLOY-FINANCE -> FIN-PROD-ENABLE -> ACCOUNTING-HANDOFF-V1 -> SYSTEM-COMPLETE-GATE -> LAUNCH-OPS-V1 -> REQ-COMPLETE-AUDIT -> ATTV1 -> LAUNCH-AUTO-OPS-V1。
- R6、PC6、PA6、FL6、AP6、RI6、LP6 已完成并部署。
- ME6 自动化、公网视觉矩阵和 `ME6-3` 代表性企业微信真机门禁已完成。
@@ -50,6 +50,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
- LAUNCH-OPS-V1 是正式使用首周运营保障合同:固定 `wangshunli` 为上线拍板、管理员复核和回滚决策人,`wangzhiling` 为出纳/付款执行和代账资料准备人;首周问题登记、每日巡检、暂停条件、代账交接节奏和可选功能冻结口径已回写现有手册。
- REQ-COMPLETE-AUDIT 是系统完成门禁后的需求完成度复核合同:确认首批正式能力仍为报销、PDF OCR、审批、请假、采购、通知、移动/企业微信入口、财务台账、付款批次、银行回单附件和代账交接;考勤边界纠偏为“企业微信打卡做原始采集和风控,OA 不重造定位/人脸/Wi-Fi/排班/防作弊”。
- ATTV1 是 REQ-COMPLETE-AUDIT 后的考勤统一入口实现合同:已部署到公网,关闭历史 OA 自建打卡 `attendance_v1`;新增 `attendance_unified_v1`、企微打卡摘要同步批次、补卡/外出/加班 OA 审批和月度考勤解释;公网复检 `/ready`、live smoke、PDF live OCR、移动/企业微信矩阵和考勤关键接口均通过,企微 live 同步缺 Secret 或 API 权限时记 `SKIP`,不使用企微原生审批,不保存完整位置、人脸、设备明文或原始 payload。
- LAUNCH-AUTO-OPS-V1 是 ATTV1 后的正式启用自动化收口合同:新增当前正式状态 QA-only runner、live feature flag 只读检查和统一考勤 live 检查;本地门禁、公网 readiness、PDF live OCR、统一考勤检查、移动矩阵和企业微信 UA/渲染矩阵均通过。显式企微打卡同步检查返回 `SKIP wecom_checkin_sync_disabled`。实际公网 `payment_batch_v1=force_off`,与 FIN-PROD-ENABLE 记录的正式常开口径不一致,因此结论为 `PAUSE_FOR_FIX`;runner 已跳过付款批次写入,未切换 flag、未部署、未接真实支付或 ERP。
## 当前活跃治理流
@@ -86,6 +87,7 @@ R6 -> PC6 -> PA6 -> FL6 -> AP6 -> RI6 -> LP6 -> ME6/QG6 -> DG7/MRF7 -> MIA7 -> M
| `LAUNCH-OPS-V1` | `issues/2026-05-17_12-55-50-launch-ops-v1.csv` | 已收口:首周运营保障口径固定 | 首周支持人、问题登记、每日巡检、暂停条件、回滚联系人、代账月度交接节奏、UX8 真实反馈池和冻结项 | 不做真实恢复演练、不重置数据库、不主动开发可选功能 |
| `REQ-COMPLETE-AUDIT` | `issues/2026-05-17_13-32-26-requirements-completion-audit.csv` | 已收口:需求完成度复核和考勤边界校正完成 | 首批能力完成度、企微打卡边界、请假/补卡/外出/加班缺口、`attendance_v1` 处理建议 | 不改业务代码、不部署、不切换公网开关;后续考勤同步或修正审批必须另建合同 |
| `ATTV1` | `issues/2026-05-17_14-59-28-attendance-unified-wecom-sync-v1.csv` | 已收口:考勤统一入口与企微打卡同步 V1 已部署并复检通过 | 关闭历史 OA 自建打卡;新增企微打卡摘要同步、补卡/外出/加班 OA 修正审批、月度解释页和移动入口;公网移动/企微矩阵 14 passed | 不用企业微信原生审批;不重造定位/人脸/Wi-Fi/设备风控;不保存完整位置、原始 payload 或人脸信息 |
| `LAUNCH-AUTO-OPS-V1` | `issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv` | 阶段收口:自动化完成,结论 `PAUSE_FOR_FIX` | 正式启用后自动 runner、live flag 只读校验、考勤统一入口 live 检查、移动/企微矩阵和证据回填;报告 `artifacts/qa-flow/launch-auto-ops-20260517-175548/` | 不切换正式 flag、不部署、不接真实支付、不接 ERP;`payment_batch_v1` 当前 force_off 漂移需单独决策恢复 |
旧 Issue CSV 不删除、不批量回写、不移动到归档目录。若新治理流替代旧合同,只在新 CSV 的 `Notes` 或本索引说明 supersedes,旧文件保留历史审计语义。
@@ -0,0 +1,9 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"LAUTO-0","合同建档与边界冻结","建立正式启用后自动化收口合同,锁定本轮只做自动化巡检、正式 flag 状态校验、考勤统一入口公网校验、付款批次常开回归、移动/企业微信 UA 矩阵和证据回填;不扩展业务能力。","1) Issue CSV 校验通过;2) 明确不新增业务 API/schema/UI/feature flag;3) 明确旧 run-p2-safe-regression.sh 不再作为正式主 runner;4) 明确不能自动完成项只记 SKIP/BLOCKED。","受影响流程:正式启用后自动化收口 -> 执行合同 -> 后续 QA-only 实施;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv。","python3|manual","DONE","DONE","DONE","issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv","[]","CSV 校验通过;回滚路径:删除本 CSV 或回退本轮提交;不涉及运行时。"
"LAUTO-1","当前正式基线复核","只读核对 git、最新 Issue、事实源矩阵、公网 readiness 和正式 feature flag 状态:payment_batch_v1=force_on、erp_sync=force_off、attendance_v1 关闭、attendance_unified_v1 开启。","1) 工作区初始状态已核对;2) live smoke env strict 通过;3) live feature flag 检查通过;4) 不打印 token、cookie、远端 env 或 secret。","受影响流程:正式启用环境 -> flag 状态 -> 自动巡检基线;验证步骤:set -a && source ~/.hyb-oa/live-smoke.env && set +a;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-smoke-env.mjs --strict;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-feature-flags.mjs --expect payment_batch_v1=force_on --expect erp_sync=force_off --expect attendance_v1=disabled --expect attendance_unified_v1=enabled。","node|bash","DONE","DONE","TODO","scripts/qa/check-live-feature-flags.mjs","[""LAUTO-0""]","PAUSE_FOR_FIX:live-smoke-env strict PASS,但公网实际 payment_batch_v1=force_off/effective=false,与本合同 force_on 基线不一致;erp_sync force_off、attendance_v1 disabled、attendance_unified_v1 enabled 均通过;未打印 token/cookie/env/secret。"
"LAUTO-2","正式启用状态自动 runner","新增 scripts/qa/run-launch-auto-ops.sh,串联本地静态门禁、公网 readiness、正式 flag 检查、考勤统一入口检查、付款批次常开回归和移动/企微矩阵;默认不切换正式 flag。","1) bash -n 通过;2) 默认需要 LAUNCH_AUTO_ALLOW_PUBLIC_WRITES=1 才允许公网写测试数据;3) 产出 artifacts/qa-flow/launch-auto-ops-* 报告;4) 报告不包含敏感字段。","受影响流程:自动收口 runner -> 本地/公网门禁 -> 证据报告;验证步骤:bash -n scripts/qa/run-launch-auto-ops.sh;LAUNCH_AUTO_ALLOW_PUBLIC_WRITES=1 QA_ISSUE_ID=LAUTO LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-launch-auto-ops.sh。","bash|node|playwright","DONE","DONE","DONE","scripts/qa/run-launch-auto-ops.sh","[""LAUTO-1""]","bash -n 通过;报告 artifacts/qa-flow/launch-auto-ops-20260517-175548/ 已生成,状态 FAIL 原因是 LAUTO-1 flag drift;runner 在前置失败后跳过付款批次写入,未切换 flag。"
"LAUTO-3","考勤统一入口 live 检查","新增 scripts/qa/check-live-attendance-unified.mjs,验证旧 OA 打卡 API 返回 409,月度汇总可读,补卡/外出/加班列表可读,企微同步按配置返回 PASS 或 SKIP。","1) 旧 /api/attendance/clock-in 返回 409 attendance_v1_disabled;2) /api/attendance/monthly 返回 200;3) /api/attendance-corrections 返回 200;4) 企微同步缺配置时记录 SKIP,不伪造成 PASS。","受影响流程:员工考勤查看 -> OA 修正列表 -> 管理员企微同步;验证步骤:LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-attendance-unified.mjs --json;可选 LAUNCH_AUTO_RUN_WECOM_SYNC=1 触发短日期窗口同步。","node","DONE","DONE","DONE","scripts/qa/check-live-attendance-unified.mjs","[""LAUTO-1""]","公网检查 PASS:legacy clock-in 409 attendance_v1_disabled,monthly 200,attendance-corrections 200,sync-runs 200;显式 LAUNCH_AUTO_RUN_WECOM_SYNC=1 后 wecom-checkin-sync 返回 SKIP code=wecom_checkin_sync_disabled,employeeCount=12 boundUserCount=5 recordCount=0。"
"LAUTO-4","付款批次常开回归适配","扩展既有公网付款批次 Playwright spec 支持正式已开启模式:不要求初始 flag off,不执行 flag on/off 切换,仍覆盖创建测试报销、审批、批次、复核、回单、执行、AP 导出和审计。","1) 正式开启模式下不调用 feature flag 更新;2) 测试数据 marker 为 LAUNCHAUTO;3) 报销 paid、票据 archived、AP 台账 paid、回单下载 200、AP 导出 200、关键审计可查;4) erp_sync 保持关闭。","受影响流程:报销 -> 审批 -> 付款批次 -> 银行回单 -> OA 已付款 -> AP 导出 -> 审计;验证步骤:cd client && PLAYWRIGHT_MANUAL=1 E2E_MODE=live P2_FIN_LIVE_APPROVED=1 P2_FIN_LIVE_ALREADY_ON=1 P2_FIN_LIVE_FINAL_STATE=force_on QA_ISSUE_ID=LAUTO P2_FIN_LIVE_MARKER_PREFIX=LAUNCHAUTO LIVE_SMOKE_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --workers=1 --reporter=list。","playwright|node","DONE","DONE","TODO","client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts","[""LAUTO-2""]","Spec 已支持 P2_FIN_LIVE_ALREADY_ON=1,失败路径不再回滚关闭正式 flag;公网付款闭环因 LAUTO-1 检出 payment_batch_v1=force_off 被 runner 安全跳过,未创建 LAUNCHAUTO 付款测试数据。"
"LAUTO-5","本地自动化门禁","执行后端 prisma、node --check、focused 单测和前端 typecheck/build,确认 QA-only 变更不破坏当前代码基线。","1) prisma validate 通过;2) 指定后端 node --check 通过;3) focused 单测通过;4) client typecheck/build 通过。","受影响流程:QA-only 变更 -> 本地代码基线;验证步骤:cd server && npm run prisma:validate;cd server && node --check src/routes/attendance.js src/routes/attendanceCorrections.js src/routes/paymentBatches.js src/routes/exports.js src/routes/wecomAuth.js;cd server && npm test -- tests/unit/routes/attendance.test.js tests/unit/routes/attendanceCorrections.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/exports.apOperations.test.js tests/unit/routes/wecomAuth.test.js tests/unit/routes/notifications.test.js;cd client && npm run typecheck && npm run build。","npm|node|prisma","DONE","DONE","DONE","server/src/routes/attendance.js|server/src/routes/attendanceCorrections.js|server/src/routes/paymentBatches.js|server/src/routes/exports.js|server/src/routes/wecomAuth.js|client","[""LAUTO-2"",""LAUTO-3"",""LAUTO-4""]","本地门禁 PASS:prisma validate;node --check;server focused 6 files/42 tests passed;client typecheck/build passed。"
"LAUTO-6","公网 readiness 与移动企微矩阵","跑公网 readiness、PDF live OCR、正式 flag 检查、考勤统一入口检查、移动信息架构矩阵和企业微信 UA/渲染矩阵,形成正式启用后自动化证据。","1) live readiness PASS,允许 pending OCR 非阻断 SKIP;2) PDF live OCR PASS;3) 公网移动矩阵通过;4) 企业微信 UA/渲染矩阵通过。","受影响流程:公网正式环境 -> 员工/移动/企业微信入口 -> 自动化证据;验证步骤:QA_ISSUE_ID=LAUTO 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 tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。","bash|playwright","DONE","DONE","DONE","scripts/qa/run-live-smoke-readiness.sh|client/tests/e2e","[""LAUTO-5""]","公网 readiness PASS:live smoke 3 passed、PDF live OCR 1 passed、pending OCR 非阻断 SKIP;公网移动矩阵 6 passed;企业微信 UA/渲染矩阵 8 passed。"
"LAUTO-7","证据回填与提交","回填本 Issue CSV、文档索引、事实源矩阵和人工试运行报告,记录 GO_TO_OPERATE、PAUSE_FOR_FIX 或 BLOCKED_BY_EXTERNAL_CONFIG 结论并中文提交。","1) CSV 状态按实测回填;2) 文档索引、事实源矩阵和人工试运行报告记录本轮证据;3) audit-docs 通过;4) git diff --check 通过;5) 中文 commit 引用 LAUTO。","受影响流程:自动化证据 -> 事实源 -> 后续运营判断;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;rg -n 'LAUTO|LAUNCH-AUTO-OPS|GO_TO_OPERATE|payment_batch_v1|attendance_unified_v1|launch-auto-ops' docs issues scripts client/tests;git diff --check;git status --short。","node|rg|git","DONE","DONE","DONE","docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md|issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv","[""LAUTO-6""]","完成:docs 和 CSV 已回填;validate PASS,audit-docs errors=0 warnings=1 为既有 unregistered docs warning;rg 关键词核对完成;git diff --check PASS;本轮总体结论 PAUSE_FOR_FIX,原因是公网 payment_batch_v1 当前 force_off 与正式常开口径不一致。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 LAUTO-0 合同建档与边界冻结 建立正式启用后自动化收口合同,锁定本轮只做自动化巡检、正式 flag 状态校验、考勤统一入口公网校验、付款批次常开回归、移动/企业微信 UA 矩阵和证据回填;不扩展业务能力。 1) Issue CSV 校验通过;2) 明确不新增业务 API/schema/UI/feature flag;3) 明确旧 run-p2-safe-regression.sh 不再作为正式主 runner;4) 明确不能自动完成项只记 SKIP/BLOCKED。 受影响流程:正式启用后自动化收口 -> 执行合同 -> 后续 QA-only 实施;验证步骤:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv。 python3|manual DONE DONE DONE issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv [] CSV 校验通过;回滚路径:删除本 CSV 或回退本轮提交;不涉及运行时。
3 LAUTO-1 当前正式基线复核 只读核对 git、最新 Issue、事实源矩阵、公网 readiness 和正式 feature flag 状态:payment_batch_v1=force_on、erp_sync=force_off、attendance_v1 关闭、attendance_unified_v1 开启。 1) 工作区初始状态已核对;2) live smoke env strict 通过;3) live feature flag 检查通过;4) 不打印 token、cookie、远端 env 或 secret。 受影响流程:正式启用环境 -> flag 状态 -> 自动巡检基线;验证步骤:set -a && source ~/.hyb-oa/live-smoke.env && set +a;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-smoke-env.mjs --strict;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-feature-flags.mjs --expect payment_batch_v1=force_on --expect erp_sync=force_off --expect attendance_v1=disabled --expect attendance_unified_v1=enabled。 node|bash DONE DONE TODO scripts/qa/check-live-feature-flags.mjs ["LAUTO-0"] PAUSE_FOR_FIX:live-smoke-env strict PASS,但公网实际 payment_batch_v1=force_off/effective=false,与本合同 force_on 基线不一致;erp_sync force_off、attendance_v1 disabled、attendance_unified_v1 enabled 均通过;未打印 token/cookie/env/secret。
4 LAUTO-2 正式启用状态自动 runner 新增 scripts/qa/run-launch-auto-ops.sh,串联本地静态门禁、公网 readiness、正式 flag 检查、考勤统一入口检查、付款批次常开回归和移动/企微矩阵;默认不切换正式 flag。 1) bash -n 通过;2) 默认需要 LAUNCH_AUTO_ALLOW_PUBLIC_WRITES=1 才允许公网写测试数据;3) 产出 artifacts/qa-flow/launch-auto-ops-* 报告;4) 报告不包含敏感字段。 受影响流程:自动收口 runner -> 本地/公网门禁 -> 证据报告;验证步骤:bash -n scripts/qa/run-launch-auto-ops.sh;LAUNCH_AUTO_ALLOW_PUBLIC_WRITES=1 QA_ISSUE_ID=LAUTO LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-launch-auto-ops.sh。 bash|node|playwright DONE DONE DONE scripts/qa/run-launch-auto-ops.sh ["LAUTO-1"] bash -n 通过;报告 artifacts/qa-flow/launch-auto-ops-20260517-175548/ 已生成,状态 FAIL 原因是 LAUTO-1 flag drift;runner 在前置失败后跳过付款批次写入,未切换 flag。
5 LAUTO-3 考勤统一入口 live 检查 新增 scripts/qa/check-live-attendance-unified.mjs,验证旧 OA 打卡 API 返回 409,月度汇总可读,补卡/外出/加班列表可读,企微同步按配置返回 PASS 或 SKIP。 1) 旧 /api/attendance/clock-in 返回 409 attendance_v1_disabled;2) /api/attendance/monthly 返回 200;3) /api/attendance-corrections 返回 200;4) 企微同步缺配置时记录 SKIP,不伪造成 PASS。 受影响流程:员工考勤查看 -> OA 修正列表 -> 管理员企微同步;验证步骤:LIVE_SMOKE_BASE_URL=https://oa.hyinda.com node scripts/qa/check-live-attendance-unified.mjs --json;可选 LAUNCH_AUTO_RUN_WECOM_SYNC=1 触发短日期窗口同步。 node DONE DONE DONE scripts/qa/check-live-attendance-unified.mjs ["LAUTO-1"] 公网检查 PASS:legacy clock-in 409 attendance_v1_disabled,monthly 200,attendance-corrections 200,sync-runs 200;显式 LAUNCH_AUTO_RUN_WECOM_SYNC=1 后 wecom-checkin-sync 返回 SKIP code=wecom_checkin_sync_disabled,employeeCount=12 boundUserCount=5 recordCount=0。
6 LAUTO-4 付款批次常开回归适配 扩展既有公网付款批次 Playwright spec 支持正式已开启模式:不要求初始 flag off,不执行 flag on/off 切换,仍覆盖创建测试报销、审批、批次、复核、回单、执行、AP 导出和审计。 1) 正式开启模式下不调用 feature flag 更新;2) 测试数据 marker 为 LAUNCHAUTO;3) 报销 paid、票据 archived、AP 台账 paid、回单下载 200、AP 导出 200、关键审计可查;4) erp_sync 保持关闭。 受影响流程:报销 -> 审批 -> 付款批次 -> 银行回单 -> OA 已付款 -> AP 导出 -> 审计;验证步骤:cd client && PLAYWRIGHT_MANUAL=1 E2E_MODE=live P2_FIN_LIVE_APPROVED=1 P2_FIN_LIVE_ALREADY_ON=1 P2_FIN_LIVE_FINAL_STATE=force_on QA_ISSUE_ID=LAUTO P2_FIN_LIVE_MARKER_PREFIX=LAUNCHAUTO LIVE_SMOKE_BASE_URL=https://oa.hyinda.com npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --workers=1 --reporter=list。 playwright|node DONE DONE TODO client/tests/e2e/manual/p2-fin-live-payment-batch.spec.ts ["LAUTO-2"] Spec 已支持 P2_FIN_LIVE_ALREADY_ON=1,失败路径不再回滚关闭正式 flag;公网付款闭环因 LAUTO-1 检出 payment_batch_v1=force_off 被 runner 安全跳过,未创建 LAUNCHAUTO 付款测试数据。
7 LAUTO-5 本地自动化门禁 执行后端 prisma、node --check、focused 单测和前端 typecheck/build,确认 QA-only 变更不破坏当前代码基线。 1) prisma validate 通过;2) 指定后端 node --check 通过;3) focused 单测通过;4) client typecheck/build 通过。 受影响流程:QA-only 变更 -> 本地代码基线;验证步骤:cd server && npm run prisma:validate;cd server && node --check src/routes/attendance.js src/routes/attendanceCorrections.js src/routes/paymentBatches.js src/routes/exports.js src/routes/wecomAuth.js;cd server && npm test -- tests/unit/routes/attendance.test.js tests/unit/routes/attendanceCorrections.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/exports.apOperations.test.js tests/unit/routes/wecomAuth.test.js tests/unit/routes/notifications.test.js;cd client && npm run typecheck && npm run build。 npm|node|prisma DONE DONE DONE server/src/routes/attendance.js|server/src/routes/attendanceCorrections.js|server/src/routes/paymentBatches.js|server/src/routes/exports.js|server/src/routes/wecomAuth.js|client ["LAUTO-2","LAUTO-3","LAUTO-4"] 本地门禁 PASS:prisma validate;node --check;server focused 6 files/42 tests passed;client typecheck/build passed。
8 LAUTO-6 公网 readiness 与移动企微矩阵 跑公网 readiness、PDF live OCR、正式 flag 检查、考勤统一入口检查、移动信息架构矩阵和企业微信 UA/渲染矩阵,形成正式启用后自动化证据。 1) live readiness PASS,允许 pending OCR 非阻断 SKIP;2) PDF live OCR PASS;3) 公网移动矩阵通过;4) 企业微信 UA/渲染矩阵通过。 受影响流程:公网正式环境 -> 员工/移动/企业微信入口 -> 自动化证据;验证步骤:QA_ISSUE_ID=LAUTO 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 tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。 bash|playwright DONE DONE DONE scripts/qa/run-live-smoke-readiness.sh|client/tests/e2e ["LAUTO-5"] 公网 readiness PASS:live smoke 3 passed、PDF live OCR 1 passed、pending OCR 非阻断 SKIP;公网移动矩阵 6 passed;企业微信 UA/渲染矩阵 8 passed。
9 LAUTO-7 证据回填与提交 回填本 Issue CSV、文档索引、事实源矩阵和人工试运行报告,记录 GO_TO_OPERATE、PAUSE_FOR_FIX 或 BLOCKED_BY_EXTERNAL_CONFIG 结论并中文提交。 1) CSV 状态按实测回填;2) 文档索引、事实源矩阵和人工试运行报告记录本轮证据;3) audit-docs 通过;4) git diff --check 通过;5) 中文 commit 引用 LAUTO。 受影响流程:自动化证据 -> 事实源 -> 后续运营判断;验证步骤:node scripts/qa/audit-docs.mjs --check-registry;rg -n 'LAUTO|LAUNCH-AUTO-OPS|GO_TO_OPERATE|payment_batch_v1|attendance_unified_v1|launch-auto-ops' docs issues scripts client/tests;git diff --check;git status --short。 node|rg|git DONE DONE DONE docs/文档索引.md|docs/文档事实源状态矩阵.md|docs/trialrun/人工试运行报告.md|issues/2026-05-17_17-47-45-launch-auto-ops-v1.csv ["LAUTO-6"] 完成:docs 和 CSV 已回填;validate PASS,audit-docs errors=0 warnings=1 为既有 unregistered docs warning;rg 关键词核对完成;git diff --check PASS;本轮总体结论 PAUSE_FOR_FIX,原因是公网 payment_batch_v1 当前 force_off 与正式常开口径不一致。
@@ -0,0 +1,275 @@
#!/usr/bin/env node
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
function parseArgs(argv) {
const options = {
baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL,
json: false,
month: new Date().toISOString().slice(0, 7),
};
for (const arg of argv) {
if (arg === "--json") {
options.json = true;
} else if (arg.startsWith("--base-url=")) {
options.baseUrl = arg.slice("--base-url=".length);
} else if (arg.startsWith("--month=")) {
options.month = arg.slice("--month=".length);
}
}
options.baseUrl = options.baseUrl.trim().replace(/\/$/, "");
if (!/^\d{4}-\d{2}$/.test(options.month)) {
throw new Error(`invalid month: ${options.month}`);
}
return options;
}
function today() {
return new Date().toISOString().slice(0, 10);
}
function getCredential(role) {
const prefixByRole = {
admin: "LIVE_SMOKE_ADMIN",
employee: "LIVE_SMOKE_EMPLOYEE",
};
const prefix = prefixByRole[role];
const identifier = process.env[`${prefix}_IDENTIFIER`]?.trim();
const password = process.env[`${prefix}_PASSWORD`]?.trim();
if (!identifier || !password) {
return null;
}
return { identifier, password };
}
async function fetchJson(url, init = {}) {
const response = await fetch(url, {
...init,
signal: AbortSignal.timeout(25_000),
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, json };
}
function errorSummary(json) {
const error = json?.error ?? {};
const details = error.details ?? {};
return {
code: typeof error.code === "string" ? error.code : null,
featureFlag: typeof details.featureFlag === "string" ? details.featureFlag : null,
reason: typeof details.reason === "string" ? details.reason : null,
};
}
async function login(baseUrl, role) {
const credential = getCredential(role);
if (!credential) {
return { ok: false, status: "SKIP", role, reason: "missing_credentials" };
}
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-device-fingerprint": `launch-auto-attendance-${role}`,
},
body: JSON.stringify({
identifier: credential.identifier,
password: credential.password,
}),
});
const accessToken = result.json?.data?.accessToken;
if (!result.ok || typeof accessToken !== "string" || !accessToken) {
return {
ok: false,
status: "FAIL",
role,
httpStatus: result.status,
error: errorSummary(result.json),
};
}
return { ok: true, status: "PASS", role, accessToken };
}
async function callApi(baseUrl, token, method, pathName, body) {
return fetchJson(`${baseUrl}${pathName}`, {
method,
headers: {
authorization: `Bearer ${token}`,
accept: "application/json",
...(body ? { "content-type": "application/json" } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
});
}
function passStep(name, details = {}) {
return { name, status: "PASS", ...details };
}
function failStep(name, details = {}) {
return { name, status: "FAIL", ...details };
}
function skipStep(name, details = {}) {
return { name, status: "SKIP", ...details };
}
function printHuman(summary) {
console.log(`[live-attendance-unified] status=${summary.status} base=${summary.baseUrl} month=${summary.month}`);
for (const step of summary.steps) {
const detail = step.httpStatus ? ` http=${step.httpStatus}` : "";
const code = step.code ? ` code=${step.code}` : "";
console.log(`[live-attendance-unified] ${step.name} status=${step.status}${detail}${code}`);
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const employee = await login(options.baseUrl, "employee");
const admin = await login(options.baseUrl, "admin");
const steps = [];
if (!employee.ok) {
steps.push(employee.status === "SKIP" ? skipStep("employee-login", employee) : failStep("employee-login", employee));
} else {
steps.push(passStep("employee-login"));
const clockIn = await callApi(options.baseUrl, employee.accessToken, "POST", "/api/attendance/clock-in", {});
const clockError = errorSummary(clockIn.json);
if (clockIn.status === 409 && clockError.featureFlag === "attendance_v1_disabled") {
steps.push(passStep("legacy-clock-in-disabled", {
httpStatus: clockIn.status,
featureFlag: clockError.featureFlag,
reason: clockError.reason,
}));
} else {
steps.push(failStep("legacy-clock-in-disabled", {
httpStatus: clockIn.status,
error: clockError,
}));
}
const monthly = await callApi(
options.baseUrl,
employee.accessToken,
"GET",
`/api/attendance/monthly?month=${encodeURIComponent(options.month)}`,
);
if (monthly.ok) {
const data = monthly.json?.data ?? {};
steps.push(passStep("attendance-monthly", {
httpStatus: monthly.status,
employeeCount: data.summary?.employeeCount ?? null,
workDays: data.summary?.workDays ?? null,
checkinDays: data.summary?.checkinDays ?? null,
missingDays: data.summary?.missingDays ?? null,
}));
} else {
steps.push(failStep("attendance-monthly", {
httpStatus: monthly.status,
error: errorSummary(monthly.json),
}));
}
const corrections = await callApi(
options.baseUrl,
employee.accessToken,
"GET",
`/api/attendance-corrections?month=${encodeURIComponent(options.month)}&page=1&pageSize=5`,
);
if (corrections.ok) {
const data = corrections.json?.data ?? {};
steps.push(passStep("attendance-corrections-list", {
httpStatus: corrections.status,
total: data.total ?? null,
itemCount: Array.isArray(data.items) ? data.items.length : null,
}));
} else {
steps.push(failStep("attendance-corrections-list", {
httpStatus: corrections.status,
error: errorSummary(corrections.json),
}));
}
}
if (!admin.ok) {
steps.push(admin.status === "SKIP" ? skipStep("admin-login", admin) : failStep("admin-login", admin));
} else {
steps.push(passStep("admin-login"));
const syncRuns = await callApi(options.baseUrl, admin.accessToken, "GET", "/api/attendance/wecom/sync-runs?page=1&pageSize=5");
if (syncRuns.ok) {
const data = syncRuns.json?.data ?? {};
steps.push(passStep("wecom-sync-runs", {
httpStatus: syncRuns.status,
total: data.total ?? null,
itemCount: Array.isArray(data.items) ? data.items.length : null,
}));
} else {
steps.push(failStep("wecom-sync-runs", {
httpStatus: syncRuns.status,
error: errorSummary(syncRuns.json),
}));
}
if (process.env.LAUNCH_AUTO_RUN_WECOM_SYNC === "1") {
const sync = await callApi(options.baseUrl, admin.accessToken, "POST", "/api/attendance/wecom/sync", {
startDate: today(),
endDate: today(),
});
if (sync.ok && sync.json?.data?.skipped) {
steps.push(skipStep("wecom-checkin-sync", {
httpStatus: sync.status,
code: sync.json?.data?.code ?? null,
employeeCount: sync.json?.data?.employeeCount ?? null,
boundUserCount: sync.json?.data?.boundUserCount ?? null,
recordCount: sync.json?.data?.recordCount ?? null,
}));
} else if (sync.ok) {
steps.push(passStep("wecom-checkin-sync", {
httpStatus: sync.status,
code: sync.json?.data?.code ?? null,
employeeCount: sync.json?.data?.employeeCount ?? null,
boundUserCount: sync.json?.data?.boundUserCount ?? null,
recordCount: sync.json?.data?.recordCount ?? null,
}));
} else {
steps.push(failStep("wecom-checkin-sync", {
httpStatus: sync.status,
error: errorSummary(sync.json),
}));
}
} else {
steps.push(skipStep("wecom-checkin-sync", { reason: "LAUNCH_AUTO_RUN_WECOM_SYNC not set" }));
}
}
const failed = steps.some((step) => step.status === "FAIL");
const summary = {
checkedAt: new Date().toISOString(),
baseUrl: options.baseUrl,
month: options.month,
status: failed ? "FAIL" : "PASS",
steps,
};
if (options.json) {
console.log(JSON.stringify(summary, null, 2));
} else {
printHuman(summary);
}
if (failed) {
process.exit(1);
}
}
main().catch((error) => {
console.error(`[live-attendance-unified][ERROR] ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
+196
查看文件
@@ -0,0 +1,196 @@
#!/usr/bin/env node
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
function parseArgs(argv) {
const options = {
baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL,
json: false,
expectations: [],
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--json") {
options.json = true;
} else if (arg === "--expect") {
const value = argv[index + 1];
if (!value) {
throw new Error("--expect requires key=value");
}
options.expectations.push(value);
index += 1;
} else if (arg.startsWith("--expect=")) {
options.expectations.push(arg.slice("--expect=".length));
} else if (arg.startsWith("--base-url=")) {
options.baseUrl = arg.slice("--base-url=".length);
}
}
options.baseUrl = options.baseUrl.trim().replace(/\/$/, "");
return options;
}
function parseExpectation(value) {
const [flagKey, expectedState] = value.split("=");
if (!flagKey || !expectedState) {
throw new Error(`invalid expectation: ${value}`);
}
const allowed = new Set(["force_on", "force_off", "enabled", "disabled"]);
if (!allowed.has(expectedState)) {
throw new Error(`invalid expected state for ${flagKey}: ${expectedState}`);
}
return { flagKey, expectedState };
}
function getAdminCredential() {
const identifier = process.env.LIVE_SMOKE_ADMIN_IDENTIFIER?.trim();
const password = process.env.LIVE_SMOKE_ADMIN_PASSWORD?.trim();
if (!identifier || !password) {
return null;
}
return { identifier, password };
}
async function fetchJson(url, init = {}) {
const response = await fetch(url, {
...init,
signal: AbortSignal.timeout(20_000),
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return { ok: response.ok, status: response.status, json };
}
function summarizeError(json) {
const error = json?.error ?? {};
return {
code: typeof error.code === "string" ? error.code : null,
message: typeof error.message === "string" ? error.message.slice(0, 120) : null,
};
}
async function login(baseUrl, credential) {
const result = await fetchJson(`${baseUrl}/api/auth/login`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-device-fingerprint": "launch-auto-feature-flags",
},
body: JSON.stringify({
identifier: credential.identifier,
password: credential.password,
}),
});
const accessToken = result.json?.data?.accessToken;
if (!result.ok || typeof accessToken !== "string" || !accessToken) {
throw new Error(`admin login failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim());
}
return accessToken;
}
function checkExpectation(item, expectedState) {
if (!item) {
return false;
}
if (expectedState === "force_on") {
return item.overrideMode === "force_on" && item.effectiveEnabled === true;
}
if (expectedState === "force_off") {
return item.overrideMode === "force_off" && item.effectiveEnabled === false;
}
if (expectedState === "enabled") {
return item.effectiveEnabled === true;
}
return item.effectiveEnabled === false;
}
function buildFlagSummary(item) {
if (!item) {
return null;
}
return {
flagKey: item.flagKey,
overrideMode: item.overrideMode,
rolloutType: item.rolloutType,
version: item.version,
envEnabled: item.envEnabled,
effectiveEnabled: item.effectiveEnabled,
effectiveReason: item.effectiveReason,
};
}
function printHuman(summary) {
console.log(`[live-feature-flags] status=${summary.status} base=${summary.baseUrl}`);
for (const result of summary.results) {
const item = result.item;
const detail = item
? `override=${item.overrideMode} effective=${item.effectiveEnabled} reason=${item.effectiveReason}`
: "missing";
console.log(
`[live-feature-flags] ${result.flagKey} expected=${result.expectedState} status=${result.status} ${detail}`,
);
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const expectations = options.expectations.map(parseExpectation);
if (!expectations.length) {
throw new Error("at least one --expect key=value is required");
}
const credential = getAdminCredential();
if (!credential) {
throw new Error("missing LIVE_SMOKE_ADMIN_* credentials");
}
const accessToken = await login(options.baseUrl, credential);
const flags = await fetchJson(`${options.baseUrl}/api/system/feature-flags`, {
headers: {
authorization: `Bearer ${accessToken}`,
accept: "application/json",
},
});
if (!flags.ok) {
throw new Error(`feature flag list failed: HTTP ${flags.status} ${summarizeError(flags.json).code ?? ""}`.trim());
}
const items = flags.json?.data?.items ?? [];
const results = expectations.map((expectation) => {
const item = items.find((candidate) => candidate.flagKey === expectation.flagKey);
const passed = checkExpectation(item, expectation.expectedState);
return {
...expectation,
status: passed ? "PASS" : "FAIL",
item: buildFlagSummary(item),
};
});
const status = results.every((result) => result.status === "PASS") ? "PASS" : "FAIL";
const summary = {
checkedAt: new Date().toISOString(),
baseUrl: options.baseUrl,
status,
readOnly: true,
results,
};
if (options.json) {
console.log(JSON.stringify(summary, null, 2));
} else {
printHuman(summary);
}
if (status !== "PASS") {
process.exit(1);
}
}
main().catch((error) => {
console.error(`[live-feature-flags][ERROR] ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
+107
查看文件
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -u -o pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CLIENT_DIR="$ROOT_DIR/client"
SERVER_DIR="$ROOT_DIR/server"
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-https://oa.hyinda.com}"
REPORT_DIR="${LAUNCH_AUTO_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/launch-auto-ops-$(date +%Y%m%d-%H%M%S)}"
SUMMARY_TXT="$REPORT_DIR/summary.txt"
STATUS=0
mkdir -p "$REPORT_DIR"
: >"$SUMMARY_TXT"
load_default_env() {
local env_file="${LAUNCH_AUTO_ENV_FILE:-$HOME/.hyb-oa/live-smoke.env}"
if [[ "${LAUNCH_AUTO_SKIP_ENV_FILE:-0}" == "1" || ! -f "$env_file" ]]; then
return 0
fi
set -a
# shellcheck disable=SC1090
source "$env_file"
set +a
PUBLIC_BASE_URL="${LIVE_SMOKE_BASE_URL:-$PUBLIC_BASE_URL}"
echo "[launch-auto-ops] loaded masked env file" | tee -a "$SUMMARY_TXT"
}
run_step() {
local name="$1"
shift
local log_file="$REPORT_DIR/${name}.log"
echo ">>> [launch-auto-ops] $name" | tee -a "$SUMMARY_TXT"
if "$@" > >(tee "$log_file") 2>&1; then
echo "[launch-auto-ops][PASS] $name" | tee -a "$SUMMARY_TXT"
else
local code="$?"
echo "[launch-auto-ops][FAIL] $name exit=$code" | tee -a "$SUMMARY_TXT"
STATUS=1
fi
}
run_shell_step() {
local name="$1"
local command="$2"
run_step "$name" bash -lc "$command"
}
write_summary_json() {
local qa_status="PASS"
local notes="LAUTO GO_TO_OPERATE:正式启用后自动化巡检通过;payment_batch_v1 保持 force_on,erp_sync 保持 force_off;企微打卡同步缺外部配置时按 SKIP 记录。"
if [[ "$STATUS" -ne 0 ]]; then
qa_status="FAIL"
notes="LAUTO PAUSE_FOR_FIX:正式启用后自动化巡检存在失败;未执行生产恢复、数据库重置、真实支付或 ERP 操作。"
fi
QA_REPORT_DIR="$REPORT_DIR" \
QA_ISSUE_ID="${QA_ISSUE_ID:-LAUTO}" \
QA_BASE_URL="$PUBLIC_BASE_URL" \
QA_MODE="launch-auto-ops" \
QA_STATUS="$qa_status" \
QA_SUMMARY_NOTES="$notes" \
node "$ROOT_DIR/scripts/qa/write-qa-summary.mjs" >/dev/null 2>&1 || true
}
finish() {
write_summary_json
echo "[launch-auto-ops] report=$REPORT_DIR" | tee -a "$SUMMARY_TXT"
exit "$STATUS"
}
trap finish EXIT
load_default_env
run_shell_step "backend-prisma" "cd '$SERVER_DIR' && npm run prisma:validate"
run_shell_step "backend-node-check" "cd '$SERVER_DIR' && node --check src/routes/attendance.js src/routes/attendanceCorrections.js src/routes/paymentBatches.js src/routes/exports.js src/routes/wecomAuth.js"
run_shell_step "backend-focused-tests" "cd '$SERVER_DIR' && npm test -- tests/unit/routes/attendance.test.js tests/unit/routes/attendanceCorrections.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/routes/exports.apOperations.test.js tests/unit/routes/wecomAuth.test.js tests/unit/routes/notifications.test.js"
if [[ "${LAUNCH_AUTO_SKIP_BUILD:-0}" == "1" ]]; then
echo "[launch-auto-ops][SKIP] frontend-build LAUNCH_AUTO_SKIP_BUILD=1" | tee -a "$SUMMARY_TXT"
else
run_shell_step "frontend-typecheck" "cd '$CLIENT_DIR' && npm run typecheck"
run_shell_step "frontend-build" "cd '$CLIENT_DIR' && npm run build"
fi
if [[ "${LAUNCH_AUTO_SKIP_LIVE:-0}" == "1" ]]; then
echo "[launch-auto-ops][SKIP] live checks LAUNCH_AUTO_SKIP_LIVE=1" | tee -a "$SUMMARY_TXT"
else
run_shell_step "live-env-strict" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-smoke-env.mjs' --strict"
run_shell_step "live-readiness" "QA_ISSUE_ID='${QA_ISSUE_ID:-LAUTO}' LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' bash '$ROOT_DIR/scripts/qa/run-live-smoke-readiness.sh'"
run_shell_step "live-feature-flags" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-feature-flags.mjs' --expect payment_batch_v1=force_on --expect erp_sync=force_off --expect attendance_v1=disabled --expect attendance_unified_v1=enabled"
run_shell_step "live-attendance-unified" "LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' node '$ROOT_DIR/scripts/qa/check-live-attendance-unified.mjs' --json"
if [[ "$STATUS" -ne 0 ]]; then
echo "[launch-auto-ops][SKIP] payment-batch-force-on-flow skipped because previous live gate failed" | tee -a "$SUMMARY_TXT"
elif [[ "${LAUNCH_AUTO_ALLOW_PUBLIC_WRITES:-0}" != "1" ]]; then
echo "[launch-auto-ops][FAIL] payment-batch-live-write requires LAUNCH_AUTO_ALLOW_PUBLIC_WRITES=1" | tee -a "$SUMMARY_TXT"
STATUS=1
else
run_shell_step "payment-batch-force-on-flow" "cd '$CLIENT_DIR' && PLAYWRIGHT_MANUAL=1 E2E_MODE=live P2_FIN_LIVE_APPROVED=1 P2_FIN_LIVE_ALREADY_ON=1 P2_FIN_LIVE_FINAL_STATE=force_on QA_ISSUE_ID='${QA_ISSUE_ID:-LAUTO}' P2_FIN_LIVE_MARKER_PREFIX='${LAUNCH_AUTO_MARKER_PREFIX:-LAUNCHAUTO}' P2_FIN_LIVE_REPORT_DIR='$REPORT_DIR' LIVE_SMOKE_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/manual/p2-fin-live-payment-batch.spec.ts --workers=1 --reporter=list"
fi
run_shell_step "public-mobile-matrix" "cd '$CLIENT_DIR' && E2E_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/dg7-mobile-information-dedupe.spec.ts tests/e2e/r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list"
run_shell_step "wecom-rendering-matrix" "cd '$CLIENT_DIR' && E2E_BASE_URL='$PUBLIC_BASE_URL' npx playwright test tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list"
fi