WREAD1 准备企微审批可读性门禁

这个提交包含在:
2026-05-19 21:09:12 +08:00
父节点 509a6bed62
当前提交 09bb202ce2
共修改 13 个文件,包含 742 行新增和 22 行删除
+1
查看文件
@@ -32,6 +32,7 @@
"check:wecom-approval-fixtures": "node src/scripts/check-wecom-approval-fixtures.js",
"check:wecom-approval-callback-poc": "node src/scripts/check-wecom-approval-callback-poc.js",
"check:wecom-approval-compensation-poc": "node src/scripts/check-wecom-approval-compensation-poc.js",
"check:wecom-approval-readability-gate": "node src/scripts/check-wecom-approval-readability-gate.js",
"check:wecom-approval-poc": "node src/scripts/check-wecom-approval-poc.js",
"check:wecom-runtime-ready": "node src/scripts/check-wecom-runtime-ready.js",
"resolve:wecom-oa-users": "node src/scripts/resolve-wecom-oa-user-consistency.js",
@@ -0,0 +1,487 @@
#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const {
APPROVAL_POC_TARGETS,
maskApprovalIdentifier,
} = require("../services/integrations/wecomApproval");
const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow");
const READABILITY_STATUSES = new Set([
"PASS_READABLE",
"PASS_WITH_TEMPLATE_ADJUSTMENTS",
"FAIL_KEEP_OA_PRIMARY_FOR_THIS_TEMPLATE",
]);
const REPORT_PRIVACY =
"readability status, manual checklist, evidence statuses and masked prefixes only; no phone/email/token/secret/full userid/full template_id/full sp_no/form values/attachments/comments/raw payload";
function timestamp() {
return new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
}
function usage() {
return `Usage: node src/scripts/check-wecom-approval-readability-gate.js [options]
QA-only 企业微信审批可读性与误审批防护门禁。
默认只汇总现有 OA-POC 证据,不写 OA/企微数据,不创建模板或审批单。
Options:
--format=text|json 输出格式,默认 text。
--artifact-root=<path> QA 报告根目录,默认 artifacts/qa-flow。
--report-dir=<path> 输出目录,默认 artifacts/qa-flow/wecom-approval-readability-gate-<timestamp>。
--fixtures-report=<path> 指定 WAPC2FLIVE summary.json 或目录。
--callback-report=<path> 指定 WCBK1 summary.json 或目录。
--compensation-report=<path> 指定 WACP3R summary.json 或目录。
--manual-result=<target>:<status> 录入人工结论,target 为 leave 或 missed_punch。
--manual-note=<target>:<text> 录入脱敏人工备注,不写表单值、附件、评论或完整 ID。
--confirmed-by=<employeeNo> 人工确认人,仅记录员工号。
--help 显示帮助。
人工结论:
PASS_READABLE
PASS_WITH_TEMPLATE_ADJUSTMENTS
FAIL_KEEP_OA_PRIMARY_FOR_THIS_TEMPLATE
`;
}
function toSafeString(value, maxLength = 500) {
if (value == null) return "";
return String(value).trim().slice(0, maxLength);
}
function safeMessage(value) {
return toSafeString(value, 500)
.replace(/access_token=[^&\s]+/gi, "access_token=***")
.replace(/[A-Za-z0-9_-]{24,}/g, "***")
.slice(0, 240);
}
function targetKeys() {
return APPROVAL_POC_TARGETS.map((item) => item.key);
}
function parseTargetPair(value, flagName) {
const raw = toSafeString(value, 700);
const separatorIndex = raw.indexOf(":");
if (separatorIndex <= 0) {
throw new Error(`${flagName} requires <target>:<value>`);
}
const target = raw.slice(0, separatorIndex).trim();
const itemValue = raw.slice(separatorIndex + 1).trim();
if (!targetKeys().includes(target)) {
throw new Error(`${flagName} unsupported target: ${target}`);
}
if (!itemValue) {
throw new Error(`${flagName} requires non-empty value`);
}
return { target, value: itemValue };
}
function parseArgs(argv = process.argv.slice(2), env = process.env) {
const options = {
format: "text",
artifactRoot: env.WECOM_APPROVAL_READABILITY_ARTIFACT_ROOT || DEFAULT_REPORT_ROOT,
reportDir: env.WECOM_APPROVAL_READABILITY_REPORT_DIR
|| path.join(DEFAULT_REPORT_ROOT, `wecom-approval-readability-gate-${timestamp()}`),
reports: {
fixtures: "",
callback: "",
compensation: "",
},
manualResults: {},
manualNotes: {},
confirmedBy: "",
help: false,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--help" || arg === "-h") {
options.help = true;
} else if (arg === "--format") {
options.format = argv[index + 1] || options.format;
index += 1;
} else if (arg.startsWith("--format=")) {
options.format = arg.slice("--format=".length);
} else if (arg === "--artifact-root") {
options.artifactRoot = argv[index + 1] || options.artifactRoot;
index += 1;
} else if (arg.startsWith("--artifact-root=")) {
options.artifactRoot = arg.slice("--artifact-root=".length);
} else if (arg === "--report-dir") {
options.reportDir = argv[index + 1] || options.reportDir;
index += 1;
} else if (arg.startsWith("--report-dir=")) {
options.reportDir = arg.slice("--report-dir=".length);
} else if (arg === "--fixtures-report") {
options.reports.fixtures = argv[index + 1] || "";
index += 1;
} else if (arg.startsWith("--fixtures-report=")) {
options.reports.fixtures = arg.slice("--fixtures-report=".length);
} else if (arg === "--callback-report") {
options.reports.callback = argv[index + 1] || "";
index += 1;
} else if (arg.startsWith("--callback-report=")) {
options.reports.callback = arg.slice("--callback-report=".length);
} else if (arg === "--compensation-report") {
options.reports.compensation = argv[index + 1] || "";
index += 1;
} else if (arg.startsWith("--compensation-report=")) {
options.reports.compensation = arg.slice("--compensation-report=".length);
} else if (arg === "--manual-result") {
const pair = parseTargetPair(argv[index + 1], "--manual-result");
options.manualResults[pair.target] = pair.value;
index += 1;
} else if (arg.startsWith("--manual-result=")) {
const pair = parseTargetPair(arg.slice("--manual-result=".length), "--manual-result");
options.manualResults[pair.target] = pair.value;
} else if (arg === "--manual-note") {
const pair = parseTargetPair(argv[index + 1], "--manual-note");
options.manualNotes[pair.target] = pair.value;
index += 1;
} else if (arg.startsWith("--manual-note=")) {
const pair = parseTargetPair(arg.slice("--manual-note=".length), "--manual-note");
options.manualNotes[pair.target] = pair.value;
} else if (arg === "--confirmed-by") {
options.confirmedBy = toSafeString(argv[index + 1], 80);
index += 1;
} else if (arg.startsWith("--confirmed-by=")) {
options.confirmedBy = toSafeString(arg.slice("--confirmed-by=".length), 80);
} else {
throw new Error(`Unsupported argument: ${arg}`);
}
}
if (!["text", "json"].includes(options.format)) {
throw new Error(`Unsupported --format: ${options.format}`);
}
for (const [target, value] of Object.entries(options.manualResults)) {
if (!READABILITY_STATUSES.has(value)) {
throw new Error(`Unsupported manual result for ${target}: ${value}`);
}
}
return options;
}
function resolveSummaryPath(inputPath) {
const value = toSafeString(inputPath, 1000);
if (!value) return "";
if (!fs.existsSync(value)) return "";
const stat = fs.statSync(value);
if (stat.isDirectory()) {
const summaryPath = path.join(value, "summary.json");
return fs.existsSync(summaryPath) ? summaryPath : "";
}
return value;
}
function findLatestSummary(artifactRoot, prefix) {
const root = toSafeString(artifactRoot, 1000);
if (!root || !fs.existsSync(root)) return "";
const entries = fs.readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
.map((entry) => path.join(root, entry.name, "summary.json"))
.filter((summaryPath) => fs.existsSync(summaryPath))
.sort();
return entries[entries.length - 1] || "";
}
function readJsonFile(filePath) {
const resolved = resolveSummaryPath(filePath);
if (!resolved) return { ok: false, path: "", reason: "report_not_found", data: null };
try {
const data = JSON.parse(fs.readFileSync(resolved, "utf8"));
return { ok: true, path: resolved, reason: null, data };
} catch (error) {
return { ok: false, path: resolved, reason: "report_parse_failed", message: safeMessage(error?.message || error), data: null };
}
}
function loadEvidenceReports(options, deps = {}) {
const findLatestSummaryImpl = deps.findLatestSummaryImpl || findLatestSummary;
const readJsonFileImpl = deps.readJsonFileImpl || readJsonFile;
const paths = {
fixtures: resolveSummaryPath(options.reports.fixtures)
|| findLatestSummaryImpl(options.artifactRoot, "wecom-approval-fixtures-"),
callback: resolveSummaryPath(options.reports.callback)
|| findLatestSummaryImpl(options.artifactRoot, "wecom-approval-callback-poc-"),
compensation: resolveSummaryPath(options.reports.compensation)
|| findLatestSummaryImpl(options.artifactRoot, "wecom-approval-compensation-poc-"),
};
return {
fixtures: readJsonFileImpl(paths.fixtures),
callback: readJsonFileImpl(paths.callback),
compensation: readJsonFileImpl(paths.compensation),
};
}
function sanitizePath(filePath) {
return toSafeString(filePath, 1000) || null;
}
function targetLabel(target) {
return APPROVAL_POC_TARGETS.find((item) => item.key === target)?.label || target;
}
function maskEvidencePrefix(value) {
const raw = toSafeString(value, 160);
if (!raw) return null;
return raw.endsWith("***") ? raw : maskApprovalIdentifier(raw);
}
function summarizeFixtureEvidence(readResult) {
const report = readResult.data || {};
const liveWrite = report.checks?.liveWrite || {};
const autoRerun = report.checks?.autoRerun || {};
const createdTemplatePrefixes = liveWrite.createdTemplatePrefixes || autoRerun.templateIdPrefixes || {};
const submittedApprovalPrefixes = liveWrite.submittedApprovalPrefixes || autoRerun.spNoPrefixes || {};
const targets = targetKeys().map((target) => ({
target,
label: targetLabel(target),
templateIdPrefix: maskEvidencePrefix(createdTemplatePrefixes[target]),
spNoPrefix: maskEvidencePrefix(submittedApprovalPrefixes[target]),
templateAvailable: Boolean(createdTemplatePrefixes[target]),
approvalAvailable: Boolean(submittedApprovalPrefixes[target]),
}));
const ok = readResult.ok
&& report.status === "PASS_FIXTURE_CREATED"
&& autoRerun.status === "OK"
&& targets.every((item) => item.templateAvailable && item.approvalAvailable);
return {
id: "wapc2flive_fixture_evidence",
status: ok ? "OK" : (readResult.ok ? "PARTIAL" : "MISSING"),
reportPath: sanitizePath(readResult.path),
sourceStatus: report.status || null,
sourceConclusion: report.conclusion || null,
autoRerunStatus: autoRerun.status || null,
targets,
reason: readResult.ok ? null : readResult.reason,
};
}
function summarizeCallbackEvidence(readResult) {
const report = readResult.data || {};
const eventEvidence = report.checks?.eventEvidence || {};
const ok = readResult.ok && report.status === "PASS_CALLBACK_EVENT_CONFIRMED";
return {
id: "wcbk1_callback_evidence",
status: ok ? "OK" : (readResult.ok ? "PARTIAL" : "MISSING"),
reportPath: sanitizePath(readResult.path),
sourceStatus: report.status || null,
sourceConclusion: report.conclusion || null,
eventCount: Number(eventEvidence.eventCount || 0),
lastEvent: eventEvidence.lastEvent ? {
approvalType: eventEvidence.lastEvent.approvalType || null,
statusLabel: eventEvidence.lastEvent.statusLabel || null,
spNoPrefix: eventEvidence.lastEvent.spNoPrefix || null,
templateIdPrefix: eventEvidence.lastEvent.templateIdPrefix || null,
recordedAt: eventEvidence.lastEvent.recordedAt || null,
} : null,
reason: readResult.ok ? null : readResult.reason,
};
}
function summarizeCompensationEvidence(readResult) {
const report = readResult.data || {};
const targetChecks = Array.isArray(report.checks?.targets) ? report.checks.targets : [];
const targets = targetKeys().map((target) => {
const check = targetChecks.find((item) => item.target === target || item.key === target) || {};
const details = Array.isArray(check.details) ? check.details : [];
return {
target,
label: targetLabel(target),
status: check.status || null,
detailCount: Number(check.detailCount || details.length || 0),
statusSummary: check.statusSummary || null,
detailSpNoPrefixes: details.map((item) => item.spNoPrefix).filter(Boolean).slice(0, 5),
};
});
const ok = readResult.ok
&& ["PASS_COMPENSATION_POC", "PASS_WITH_FINDINGS"].includes(report.status)
&& targets.every((item) => item.status === "OK" && item.detailCount > 0);
return {
id: "wacp3r_compensation_evidence",
status: ok ? "OK" : (readResult.ok ? "PARTIAL" : "MISSING"),
reportPath: sanitizePath(readResult.path),
sourceStatus: report.status || null,
sourceConclusion: report.conclusion || null,
templateDiscoveryStatus: report.checks?.templateDiscovery?.status || null,
findings: (report.checks?.callbackCompare?.findings || []).map((item) => ({
id: item.id,
target: item.target || null,
spNoPrefix: item.spNoPrefix || null,
})),
targets,
reason: readResult.ok ? null : readResult.reason,
};
}
function buildManualChecklist() {
return targetKeys().map((target) => ({
target,
label: targetLabel(target),
questions: [
"企微消息卡片是否能看出申请类型、申请人、时间和状态?",
"企微审批列表是否能区分请假/补卡?",
"详情页字段是否足够审批人判断?",
"快捷同意/驳回是否容易误点?",
"是否需要模板字段调整或回退 OA 主办?",
],
}));
}
function sanitizeManualNote(value) {
return safeMessage(value)
.replace(/SP[A-Za-z0-9_-]{8,}/g, "SP***")
.replace(/[A-Za-z0-9_-]{16,}/g, "***");
}
function buildManualResults(options) {
return targetKeys().map((target) => ({
target,
label: targetLabel(target),
status: options.manualResults[target] || "PENDING_MANUAL_CONFIRMATION",
note: options.manualNotes[target] ? sanitizeManualNote(options.manualNotes[target]) : null,
}));
}
function evaluateConclusion({ evidence, manualResults }) {
const sampleEvidenceOk = evidence.compensation.status === "OK";
if (!sampleEvidenceOk) {
return {
status: "PARTIAL_READABILITY_NEEDS_FRESH_FIXTURE",
conclusion: "PARTIAL_READABILITY_NEEDS_FRESH_FIXTURE",
};
}
if (manualResults.some((item) => item.status === "PENDING_MANUAL_CONFIRMATION")) {
return {
status: "PAUSED_MANUAL_CONFIRMATION",
conclusion: "PAUSED_MANUAL_CONFIRMATION",
};
}
if (manualResults.some((item) => item.status === "FAIL_KEEP_OA_PRIMARY_FOR_THIS_TEMPLATE")) {
return {
status: "FAIL_KEEP_OA_PRIMARY_FOR_THIS_TEMPLATE",
conclusion: "FAIL_KEEP_OA_PRIMARY_FOR_THIS_TEMPLATE",
};
}
if (manualResults.some((item) => item.status === "PASS_WITH_TEMPLATE_ADJUSTMENTS")) {
return {
status: "PASS_WITH_TEMPLATE_ADJUSTMENTS",
conclusion: "PASS_WITH_TEMPLATE_ADJUSTMENTS",
};
}
return {
status: "PASS_READABLE",
conclusion: "PASS_READABLE",
};
}
async function executeReadabilityGate(options, deps = {}) {
const generatedAt = deps.generatedAt || new Date().toISOString();
const reports = loadEvidenceReports(options, deps);
const evidence = {
fixtures: summarizeFixtureEvidence(reports.fixtures),
callback: summarizeCallbackEvidence(reports.callback),
compensation: summarizeCompensationEvidence(reports.compensation),
};
const manualResults = buildManualResults(options);
const conclusion = evaluateConclusion({ evidence, manualResults });
const sampleEvidenceOk = evidence.compensation.status === "OK";
return {
mode: "qa_only",
generatedAt,
status: conclusion.status,
conclusion: conclusion.conclusion,
privacy: REPORT_PRIVACY,
scope: {
issue: "WREAD1",
targets: targetKeys(),
samplePolicy: "reuse existing OA-POC leave/missed_punch samples first; do not create new WeCom templates or approvals",
writeBehavior: "read-only evidence summary only; no OA writes, no WeCom writes, no callback setup, no mirror import",
deepMirror: "not_started",
nextStage: conclusion.status === "PASS_READABLE" || conclusion.status === "PASS_WITH_TEMPLATE_ADJUSTMENTS"
? "WADM1"
: "manual_confirmation_or_fresh_fixture_decision",
},
checks: {
evidence,
sampleEvidence: {
status: sampleEvidenceOk ? "OK" : "PARTIAL",
reason: sampleEvidenceOk
? "wacp3r_compensation_detail_evidence_available"
: "existing_oa_poc_compensation_detail_evidence_not_complete",
fixtureEvidenceStatus: evidence.fixtures.status,
callbackEvidenceStatus: evidence.callback.status,
},
manualChecklist: buildManualChecklist(),
manualResults,
},
confirmedBy: options.confirmedBy || null,
configGaps: sampleEvidenceOk ? [] : [{
id: "readability_existing_sample",
status: "NEEDS_CONFIG",
reason: "existing_compensation_detail_evidence_not_complete_or_not_found",
}],
mainBlockers: [],
};
}
function ensureReportDir(reportDir) {
fs.mkdirSync(reportDir, { recursive: true });
}
function writeReport(reportDir, report) {
ensureReportDir(reportDir);
const filePath = path.join(reportDir, "summary.json");
fs.writeFileSync(filePath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
return filePath;
}
function printText(report, reportPath) {
process.stdout.write(`[wecom-approval-readability-gate] status=${report.status} conclusion=${report.conclusion}\n`);
process.stdout.write(`[wecom-approval-readability-gate] report=${reportPath}\n`);
process.stdout.write(`[wecom-approval-readability-gate] privacy=${report.privacy}\n`);
process.stdout.write(`[wecom-approval-readability-gate] targets=${report.scope.targets.join(",")}\n`);
if (report.configGaps.length) {
process.stdout.write(`[wecom-approval-readability-gate] configGaps=${JSON.stringify(report.configGaps)}\n`);
}
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
process.stdout.write(usage());
return;
}
const report = await executeReadabilityGate(options);
const reportPath = writeReport(options.reportDir, report);
if (options.format === "json") {
process.stdout.write(`${JSON.stringify({ ...report, reportPath }, null, 2)}\n`);
} else {
printText(report, reportPath);
}
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(`[wecom-approval-readability-gate][FAIL] ${safeMessage(error?.message || error)}\n`);
process.exitCode = 1;
});
}
module.exports = {
buildManualChecklist,
buildManualResults,
evaluateConclusion,
executeReadabilityGate,
findLatestSummary,
parseArgs,
summarizeCompensationEvidence,
summarizeFixtureEvidence,
writeReport,
};
@@ -0,0 +1,220 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
executeReadabilityGate,
parseArgs,
} = require("../../../src/scripts/check-wecom-approval-readability-gate");
const SENSITIVE_TEMPLATE_ID = "tpl_readability_full_identifier_must_not_leak";
const SENSITIVE_SP_NO = "SPREADABILITYFULLIDENTIFIERMUSTNOTLEAK";
const SENSITIVE_USER_ID = "readability_userid_full_must_not_leak";
const SENSITIVE_FORM_VALUE = "这里是表单值,不应出现在报告里";
function tempDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), "wread1-test-"));
}
function writeSummary(root, dirname, payload) {
const dir = path.join(root, dirname);
fs.mkdirSync(dir, { recursive: true });
const filePath = path.join(dir, "summary.json");
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
return filePath;
}
function writeOkEvidence(root) {
writeSummary(root, "wecom-approval-fixtures-20260519T053400Z", {
status: "PASS_FIXTURE_CREATED",
conclusion: "PASS_FIXTURE_CREATED",
checks: {
liveWrite: {
createdTemplatePrefixes: {
leave: "tpl-leav***",
missed_punch: "tpl-miss***",
},
submittedApprovalPrefixes: {
leave: "SPLEAVE***",
missed_punch: "SPMISS***",
},
},
autoRerun: {
status: "OK",
},
},
rawShouldNotCopy: SENSITIVE_TEMPLATE_ID,
});
writeSummary(root, "wecom-approval-callback-poc-20260519T092648Z", {
status: "PASS_CALLBACK_EVENT_CONFIRMED",
conclusion: "PASS_CALLBACK_EVENT_CONFIRMED",
checks: {
eventEvidence: {
eventCount: 1,
lastEvent: {
approvalType: "missed_punch",
statusLabel: "已通过",
spNoPrefix: "SPMISS***",
templateIdPrefix: "tpl-miss***",
recordedAt: "2026-05-19T09:26:48.000Z",
rawShouldNotCopy: SENSITIVE_SP_NO,
},
},
},
});
writeSummary(root, "wecom-approval-compensation-poc-20260519T115944Z", {
status: "PASS_WITH_FINDINGS",
conclusion: "PASS_WITH_FINDINGS",
checks: {
templateDiscovery: { status: "OK", discovered: { leave: SENSITIVE_TEMPLATE_ID } },
targets: [
{
target: "leave",
status: "OK",
detailCount: 1,
statusSummary: { "审批中": 1 },
details: [{ spNoPrefix: "SPLEAVE***", rawShouldNotCopy: SENSITIVE_FORM_VALUE }],
},
{
target: "missed_punch",
status: "OK",
detailCount: 1,
statusSummary: { "已通过": 1 },
details: [{ spNoPrefix: "SPMISS***" }],
},
],
callbackCompare: {
findings: [
{ id: "callback_missing_for_pulled_detail", target: "leave", spNoPrefix: "SPLEAVE***" },
],
},
},
rawUserId: SENSITIVE_USER_ID,
});
}
describe("check-wecom-approval-readability-gate", () => {
it("parses manual results and report options", () => {
const options = parseArgs([
"--format=json",
"--artifact-root=/tmp/wread1-artifacts",
"--manual-result=leave:PASS_READABLE",
"--manual-result=missed_punch:PASS_WITH_TEMPLATE_ADJUSTMENTS",
"--manual-note=leave:卡片可读",
"--confirmed-by=wangshunli",
]);
expect(options.format).toBe("json");
expect(options.artifactRoot).toBe("/tmp/wread1-artifacts");
expect(options.manualResults).toEqual({
leave: "PASS_READABLE",
missed_punch: "PASS_WITH_TEMPLATE_ADJUSTMENTS",
});
expect(options.manualNotes.leave).toBe("卡片可读");
expect(options.confirmedBy).toBe("wangshunli");
expect(() => parseArgs(["--manual-result=leave:PASS"])).toThrow(/Unsupported manual result/);
});
it("pauses for manual confirmation when existing evidence is available", async () => {
const root = tempDir();
writeOkEvidence(root);
const report = await executeReadabilityGate({
...parseArgs(["--artifact-root", root]),
reportDir: path.join(root, "out"),
}, { generatedAt: "2026-05-20T00:00:00.000Z" });
expect(report.status).toBe("PAUSED_MANUAL_CONFIRMATION");
expect(report.checks.sampleEvidence.status).toBe("OK");
expect(report.checks.evidence.fixtures.status).toBe("OK");
expect(report.checks.evidence.compensation.status).toBe("OK");
expect(report.checks.manualResults.map((item) => item.status)).toEqual([
"PENDING_MANUAL_CONFIRMATION",
"PENDING_MANUAL_CONFIRMATION",
]);
});
it("accepts WACP3R compensation detail evidence when historical fixture artifacts are not persisted", async () => {
const root = tempDir();
writeSummary(root, "wecom-approval-compensation-poc-20260519T115944Z", {
status: "PASS_WITH_FINDINGS",
conclusion: "PASS_WITH_FINDINGS",
checks: {
templateDiscovery: { status: "OK" },
targets: [
{
target: "leave",
status: "OK",
details: [{ spNoPrefix: "SPLEAVE***" }],
},
{
target: "missed_punch",
status: "OK",
details: [{ spNoPrefix: "SPMISS***" }],
},
],
callbackCompare: { findings: [] },
},
});
const report = await executeReadabilityGate(parseArgs(["--artifact-root", root]), {
generatedAt: "2026-05-20T00:00:00.000Z",
});
expect(report.status).toBe("PAUSED_MANUAL_CONFIRMATION");
expect(report.checks.sampleEvidence.status).toBe("OK");
expect(report.checks.sampleEvidence.reason).toBe("wacp3r_compensation_detail_evidence_available");
expect(report.checks.evidence.fixtures.status).toBe("MISSING");
});
it("returns PASS_WITH_TEMPLATE_ADJUSTMENTS when one template needs changes", async () => {
const root = tempDir();
writeOkEvidence(root);
const report = await executeReadabilityGate(parseArgs([
"--artifact-root", root,
"--manual-result=leave:PASS_READABLE",
"--manual-result=missed_punch:PASS_WITH_TEMPLATE_ADJUSTMENTS",
"--manual-note=missed_punch:快捷按钮容易误点,需要提醒审批人点详情",
"--confirmed-by=wangshunli",
]), { generatedAt: "2026-05-20T00:00:00.000Z" });
expect(report.status).toBe("PASS_WITH_TEMPLATE_ADJUSTMENTS");
expect(report.confirmedBy).toBe("wangshunli");
expect(report.checks.manualResults.find((item) => item.target === "missed_punch").note).toContain("快捷按钮");
});
it("asks for a fresh fixture when existing sample evidence is missing", async () => {
const root = tempDir();
writeSummary(root, "wecom-approval-compensation-poc-20260519T115944Z", {
status: "PASS_WITH_FINDINGS",
checks: { targets: [] },
});
const report = await executeReadabilityGate(parseArgs(["--artifact-root", root]), {
generatedAt: "2026-05-20T00:00:00.000Z",
});
expect(report.status).toBe("PARTIAL_READABILITY_NEEDS_FRESH_FIXTURE");
expect(report.configGaps.map((item) => item.id)).toContain("readability_existing_sample");
});
it("does not leak full template, sp_no, userid or form values", async () => {
const root = tempDir();
writeOkEvidence(root);
const report = await executeReadabilityGate(parseArgs([
"--artifact-root", root,
"--manual-result=leave:PASS_READABLE",
"--manual-result=missed_punch:PASS_READABLE",
"--manual-note=leave:不要泄露 SPREADABILITYFULLIDENTIFIERMUSTNOTLEAK",
]), { generatedAt: "2026-05-20T00:00:00.000Z" });
const serialized = JSON.stringify(report);
expect(report.status).toBe("PASS_READABLE");
expect(serialized).not.toContain(SENSITIVE_TEMPLATE_ID);
expect(serialized).not.toContain(SENSITIVE_SP_NO);
expect(serialized).not.toContain(SENSITIVE_USER_ID);
expect(serialized).not.toContain(SENSITIVE_FORM_VALUE);
});
});