WEXT1F 增加正式模板外出加班取证门禁
这个提交包含在:
@@ -9,6 +9,7 @@ const { maskApprovalIdentifier } = require("../services/integrations/wecomApprov
|
||||
|
||||
const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow");
|
||||
const DEFAULT_TARGETS = Object.freeze(["leave", "missed_punch"]);
|
||||
const SUPPORTED_TARGETS = Object.freeze(["leave", "missed_punch", "outing", "overtime"]);
|
||||
const REPORT_PRIVACY =
|
||||
"OA DB archive counts/status summaries/masked sp_no/template/userid prefixes only; no phone/email/token/secret/full userid/full template_id/full sp_no/reason text/attachments/comments/raw payload";
|
||||
|
||||
@@ -24,6 +25,8 @@ Options:
|
||||
--report-dir=<path> 报告目录,默认 artifacts/qa-flow/wecom-approval-archive-<timestamp>。
|
||||
--template=leave 只检查请假留存。
|
||||
--template=missed_punch 只检查补卡留存。
|
||||
--template=outing 只检查外出留存。
|
||||
--template=overtime 只检查加班留存。
|
||||
--status=<status> 按规范化状态过滤。
|
||||
--limit=<1..100> 最大读取条数,默认 20。
|
||||
--help 显示帮助。
|
||||
@@ -102,7 +105,7 @@ function parseArgs(argv) {
|
||||
}
|
||||
const uniqueTemplates = Array.from(new Set(options.templates.filter(Boolean)));
|
||||
for (const template of uniqueTemplates) {
|
||||
if (!DEFAULT_TARGETS.includes(template)) {
|
||||
if (!SUPPORTED_TARGETS.includes(template)) {
|
||||
throw new Error(`Unsupported --template: ${template}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const { getConfig } = require("../config");
|
||||
const { createPrismaClient } = require("../prismaClient");
|
||||
const {
|
||||
APPROVAL_GET_INFO_ENDPOINT,
|
||||
APPROVAL_HR_ATTENDANCE_TARGETS,
|
||||
APPROVAL_POC_TARGETS,
|
||||
fetchWecomApprovalDetail,
|
||||
fetchWecomApprovalInfoList,
|
||||
@@ -35,7 +36,10 @@ const REPORT_PRIVACY =
|
||||
const TEMPLATE_ENV = Object.freeze({
|
||||
leave: "WECOM_APPROVAL_LEAVE_TEMPLATE_ID",
|
||||
missed_punch: "WECOM_APPROVAL_MISSED_PUNCH_TEMPLATE_ID",
|
||||
outing: "WECOM_APPROVAL_OUTING_TEMPLATE_ID",
|
||||
overtime: "WECOM_APPROVAL_OVERTIME_TEMPLATE_ID",
|
||||
});
|
||||
const DEFAULT_TARGET_KEYS = Object.freeze(APPROVAL_POC_TARGETS.map((item) => item.key));
|
||||
|
||||
function printHelp() {
|
||||
process.stdout.write(`Usage: node src/scripts/check-wecom-approval-compensation-poc.js [options]
|
||||
@@ -51,9 +55,15 @@ Options:
|
||||
--end-time=<iso|epoch> 窗口结束时间,默认当前时间。
|
||||
--template=leave:<id> 指定请假模板 ID,用于 template_id filter。
|
||||
--template=missed_punch:<id> 指定补卡模板 ID,用于 template_id filter。
|
||||
--template=outing:<id> 指定外出模板 ID,用于 WEXT1-FORMAL 正式模板只读证据。
|
||||
--template=overtime:<id> 指定加班模板 ID,用于 WEXT1-FORMAL 正式模板只读证据。
|
||||
--target=<key> 显式目标,可重复;默认 leave + missed_punch。
|
||||
--targets=<a,b> 显式目标列表;支持 leave, missed_punch, outing, overtime。
|
||||
--discover-template-ids 缺模板 ID 时,按申请人只读发现 OA-POC 模板 ID。
|
||||
--discover-applicant-employee-no=<no>
|
||||
自动发现使用的 OA 申请人员工号,默认 SMOKE002。
|
||||
--filter-creator-employee-no=<no>
|
||||
拉取模板详情时同时按创建人员工号过滤;正式模板证据必须设置。
|
||||
--size=<1..100> 单页审批单号数量,默认 100。
|
||||
--persist-to-db 写入 OA 自建留存表;还必须设置 WECOM_ARCHIVE_WRITE=1。
|
||||
--help 显示帮助。
|
||||
@@ -86,7 +96,26 @@ function safeMessage(value) {
|
||||
}
|
||||
|
||||
function supportedTargetKeys() {
|
||||
return APPROVAL_POC_TARGETS.map((item) => item.key);
|
||||
return APPROVAL_HR_ATTENDANCE_TARGETS.map((item) => item.key);
|
||||
}
|
||||
|
||||
function selectedTargetKeys(options) {
|
||||
const rawTargets = Array.isArray(options.targets) && options.targets.length
|
||||
? options.targets
|
||||
: DEFAULT_TARGET_KEYS;
|
||||
const uniqueTargets = Array.from(new Set(rawTargets.map((item) => toSafeString(item, 40)).filter(Boolean)));
|
||||
const supported = supportedTargetKeys();
|
||||
for (const target of uniqueTargets) {
|
||||
if (!supported.includes(target)) {
|
||||
throw new Error(`unsupported target: ${target}`);
|
||||
}
|
||||
}
|
||||
return uniqueTargets.length ? uniqueTargets : [...DEFAULT_TARGET_KEYS];
|
||||
}
|
||||
|
||||
function selectedTargetDescriptors(options) {
|
||||
const selected = new Set(selectedTargetKeys(options));
|
||||
return APPROVAL_HR_ATTENDANCE_TARGETS.filter((item) => selected.has(item.key));
|
||||
}
|
||||
|
||||
function parseTargetPair(value, flagName) {
|
||||
@@ -114,8 +143,10 @@ function parseArgs(argv) {
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
templates: {},
|
||||
targets: [],
|
||||
discoverTemplateIds: false,
|
||||
discoverApplicantEmployeeNo: DEFAULT_DISCOVERY_APPLICANT_EMPLOYEE_NO,
|
||||
filterCreatorEmployeeNo: "",
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
maxPages: DEFAULT_MAX_PAGES,
|
||||
persistToDb: false,
|
||||
@@ -157,6 +188,16 @@ function parseArgs(argv) {
|
||||
} else if (arg.startsWith("--template=")) {
|
||||
const pair = parseTargetPair(arg.slice("--template=".length), "--template");
|
||||
options.templates[pair.target] = pair.value;
|
||||
} else if (arg === "--target") {
|
||||
options.targets.push(toSafeString(argv[index + 1], 40));
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--target=")) {
|
||||
options.targets.push(toSafeString(arg.slice("--target=".length), 40));
|
||||
} else if (arg === "--targets") {
|
||||
options.targets.push(...toSafeString(argv[index + 1], 200).split(",").map((item) => item.trim()).filter(Boolean));
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--targets=")) {
|
||||
options.targets.push(...toSafeString(arg.slice("--targets=".length), 200).split(",").map((item) => item.trim()).filter(Boolean));
|
||||
} else if (arg === "--discover-template-ids") {
|
||||
options.discoverTemplateIds = true;
|
||||
} else if (arg === "--discover-applicant-employee-no") {
|
||||
@@ -164,6 +205,11 @@ function parseArgs(argv) {
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--discover-applicant-employee-no=")) {
|
||||
options.discoverApplicantEmployeeNo = toSafeString(arg.slice("--discover-applicant-employee-no=".length), 80);
|
||||
} else if (arg === "--filter-creator-employee-no") {
|
||||
options.filterCreatorEmployeeNo = toSafeString(argv[index + 1], 80);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--filter-creator-employee-no=")) {
|
||||
options.filterCreatorEmployeeNo = toSafeString(arg.slice("--filter-creator-employee-no=".length), 80);
|
||||
} else if (arg === "--size") {
|
||||
options.size = Number(argv[index + 1] || options.size);
|
||||
index += 1;
|
||||
@@ -187,6 +233,7 @@ function parseArgs(argv) {
|
||||
if (!options.discoverApplicantEmployeeNo) {
|
||||
throw new Error("--discover-applicant-employee-no requires non-empty value");
|
||||
}
|
||||
options.targets = selectedTargetKeys(options);
|
||||
options.size = Math.floor(options.size);
|
||||
return options;
|
||||
}
|
||||
@@ -226,7 +273,7 @@ function envValue(env, name) {
|
||||
}
|
||||
|
||||
function buildInputs(options, env = process.env) {
|
||||
return APPROVAL_POC_TARGETS.map((target) => {
|
||||
return selectedTargetDescriptors(options).map((target) => {
|
||||
const cliTemplateId = toSafeString(options.templates[target.key], 500);
|
||||
const envTemplateId = envValue(env, TEMPLATE_ENV[target.key]);
|
||||
const templateId = cliTemplateId || envTemplateId;
|
||||
@@ -327,6 +374,8 @@ function inferTargetFromApprovalDetail(detailResult) {
|
||||
].filter(Boolean).join(" ");
|
||||
if (/补卡|missed[_ -]?punch/i.test(text)) return "missed_punch";
|
||||
if (/请假|leave/i.test(text)) return "leave";
|
||||
if (/外出|outing|outgoing/i.test(text)) return "outing";
|
||||
if (/加班|overtime/i.test(text)) return "overtime";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -615,11 +664,98 @@ function buildDryRunTargetCheck(input) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildCreatorFilterNotReadyTargetCheck(input, creatorFilterCheck) {
|
||||
return {
|
||||
id: "approval_compensation_pull",
|
||||
target: input.key,
|
||||
label: input.label,
|
||||
endpoint: `POST ${APPROVAL_GET_INFO_ENDPOINT}`,
|
||||
source: "WECOM_AGENT_SECRET",
|
||||
status: "NEEDS_CONFIG",
|
||||
reason: creatorFilterCheck?.reason || "creator_filter_not_ready",
|
||||
missingEnv: creatorFilterCheck?.missing || [],
|
||||
templateIdPrefix: input.templateIdPrefix,
|
||||
configuredFrom: input.templateSource,
|
||||
creatorFilter: {
|
||||
requested: true,
|
||||
status: creatorFilterCheck?.status || "NEEDS_CONFIG",
|
||||
employeeNo: creatorFilterCheck?.employeeNo || null,
|
||||
userIdPrefix: creatorFilterCheck?.userIdPrefix || null,
|
||||
},
|
||||
pageCount: 0,
|
||||
spNoCount: 0,
|
||||
details: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildSkippedCreatorFilterCheck(reason) {
|
||||
return {
|
||||
id: "approval_creator_filter",
|
||||
status: "SKIPPED",
|
||||
reason,
|
||||
employeeNo: null,
|
||||
applicant: null,
|
||||
userIdPrefix: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildCreatorFilterCheck({ options, queryApplicantActorImpl }) {
|
||||
const employeeNo = toSafeString(options.filterCreatorEmployeeNo, 80);
|
||||
if (!employeeNo) return buildSkippedCreatorFilterCheck("creator_filter_not_requested");
|
||||
|
||||
let applicant;
|
||||
try {
|
||||
applicant = await queryApplicantActorImpl(employeeNo);
|
||||
} catch (error) {
|
||||
return {
|
||||
id: "approval_creator_filter",
|
||||
status: "NEEDS_CONFIG",
|
||||
reason: "creator_filter_lookup_failed",
|
||||
message: safeMessage(error?.message || error),
|
||||
employeeNo,
|
||||
applicant: null,
|
||||
userIdPrefix: null,
|
||||
missing: ["active_employee_wecom_binding"],
|
||||
};
|
||||
}
|
||||
|
||||
const gaps = [];
|
||||
if (!applicant) gaps.push("creator_employee_missing");
|
||||
if (applicant && !isOaActive(applicant)) gaps.push("creator_employee_inactive");
|
||||
if (applicant && !applicant.wecomExternalUserId) gaps.push("creator_employee_missing_wecom_binding");
|
||||
if (gaps.length) {
|
||||
return {
|
||||
id: "approval_creator_filter",
|
||||
status: "NEEDS_CONFIG",
|
||||
reason: "creator_filter_not_ready",
|
||||
employeeNo,
|
||||
applicant: sanitizeDiscoveryActor(applicant),
|
||||
userIdPrefix: null,
|
||||
gaps,
|
||||
missing: ["active_employee_wecom_binding"],
|
||||
};
|
||||
}
|
||||
|
||||
return attachHiddenValue({
|
||||
id: "approval_creator_filter",
|
||||
status: "OK",
|
||||
reason: null,
|
||||
employeeNo,
|
||||
applicant: sanitizeDiscoveryActor(applicant),
|
||||
userIdPrefix: maskApprovalIdentifier(applicant.wecomExternalUserId),
|
||||
filter: {
|
||||
key: "creator",
|
||||
useridPrefix: maskApprovalIdentifier(applicant.wecomExternalUserId),
|
||||
},
|
||||
}, "userid", applicant.wecomExternalUserId);
|
||||
}
|
||||
|
||||
async function runLiveTarget({
|
||||
config,
|
||||
input,
|
||||
timeWindow,
|
||||
options,
|
||||
creatorFilterCheck,
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
archivePersistence,
|
||||
@@ -632,13 +768,17 @@ async function runLiveTarget({
|
||||
|
||||
do {
|
||||
pageIndex += 1;
|
||||
const filters = [{ key: "template_id", value: input.templateId }];
|
||||
if (creatorFilterCheck?.status === "OK" && creatorFilterCheck.userid) {
|
||||
filters.push({ key: "creator", value: creatorFilterCheck.userid });
|
||||
}
|
||||
const result = await fetchApprovalInfoListImpl({
|
||||
config,
|
||||
startTime: timeWindow.startTime,
|
||||
endTime: timeWindow.endTime,
|
||||
size: options.size,
|
||||
nextCursor: cursor,
|
||||
filters: [{ key: "template_id", value: input.templateId }],
|
||||
filters,
|
||||
});
|
||||
pages.push({
|
||||
page: pageIndex,
|
||||
@@ -672,6 +812,10 @@ async function runLiveTarget({
|
||||
message: blocked.message ? safeMessage(blocked.message) : null,
|
||||
templateIdPrefix: input.templateIdPrefix,
|
||||
configuredFrom: input.templateSource,
|
||||
creatorFilter: creatorFilterCheck?.status === "OK" ? {
|
||||
employeeNo: creatorFilterCheck.employeeNo,
|
||||
userIdPrefix: creatorFilterCheck.userIdPrefix,
|
||||
} : null,
|
||||
pageCount: pages.length,
|
||||
spNoCount: 0,
|
||||
pages,
|
||||
@@ -727,6 +871,10 @@ async function runLiveTarget({
|
||||
reason: detailBlocked ? detailBlocked.reason : null,
|
||||
templateIdPrefix: input.templateIdPrefix,
|
||||
configuredFrom: input.templateSource,
|
||||
creatorFilter: creatorFilterCheck?.status === "OK" ? {
|
||||
employeeNo: creatorFilterCheck.employeeNo,
|
||||
userIdPrefix: creatorFilterCheck.userIdPrefix,
|
||||
} : null,
|
||||
pageCount: pages.length,
|
||||
spNoCount: uniqueSpNos.length,
|
||||
pages,
|
||||
@@ -740,6 +888,7 @@ async function runTargetChecks({
|
||||
inputs,
|
||||
timeWindow,
|
||||
options,
|
||||
creatorFilterCheck,
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
archivePersistence,
|
||||
@@ -748,6 +897,8 @@ async function runTargetChecks({
|
||||
for (const input of inputs) {
|
||||
if (!input.templateId) {
|
||||
targetChecks.push(buildMissingTemplateCheck(input));
|
||||
} else if (options.filterCreatorEmployeeNo && creatorFilterCheck?.status !== "OK") {
|
||||
targetChecks.push(buildCreatorFilterNotReadyTargetCheck(input, creatorFilterCheck));
|
||||
} else if (!options.live) {
|
||||
targetChecks.push(buildDryRunTargetCheck(input));
|
||||
} else {
|
||||
@@ -756,6 +907,7 @@ async function runTargetChecks({
|
||||
input,
|
||||
timeWindow,
|
||||
options,
|
||||
creatorFilterCheck,
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
archivePersistence,
|
||||
@@ -956,7 +1108,7 @@ function collectConfigGaps(targetChecks, verificationGaps, evidenceCheck, compar
|
||||
gaps.push({
|
||||
id: "approval_compensation_window_empty",
|
||||
status: "NEEDS_CONFIG",
|
||||
reason: "no_existing_oa_poc_approval_in_window",
|
||||
reason: "no_existing_approval_in_window_for_selected_targets",
|
||||
});
|
||||
}
|
||||
return gaps;
|
||||
@@ -1148,6 +1300,10 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
fetchApprovalDetailImpl: deps.fetchApprovalDetailImpl || fetchWecomApprovalDetail,
|
||||
queryApplicantActorImpl: deps.queryApplicantActorImpl || queryApplicantActorRowReadOnly,
|
||||
});
|
||||
const creatorFilterCheck = await buildCreatorFilterCheck({
|
||||
options,
|
||||
queryApplicantActorImpl: deps.queryApplicantActorImpl || queryApplicantActorRowReadOnly,
|
||||
});
|
||||
const resolvedInputs = applyDiscoveredTemplateIds(inputs, discoveryCheck);
|
||||
if (archivePersistenceCheck.status === "READY") {
|
||||
try {
|
||||
@@ -1183,6 +1339,7 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
inputs: resolvedInputs,
|
||||
timeWindow,
|
||||
options,
|
||||
creatorFilterCheck,
|
||||
fetchApprovalInfoListImpl: deps.fetchApprovalInfoListImpl || fetchWecomApprovalInfoList,
|
||||
fetchApprovalDetailImpl: deps.fetchApprovalDetailImpl || fetchWecomApprovalDetail,
|
||||
archivePersistence,
|
||||
@@ -1245,7 +1402,7 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
privacy: REPORT_PRIVACY,
|
||||
scope: {
|
||||
issue: "WACP3",
|
||||
targets: APPROVAL_POC_TARGETS.map((item) => item.key),
|
||||
targets: selectedTargetKeys(options),
|
||||
writeBehavior: options.persistToDb
|
||||
? "WeCom read-only; OA archive DB persistence requires --persist-to-db + WECOM_ARCHIVE_WRITE=1"
|
||||
: "read-only only; no OA writes, no WeCom writes, no template creation, no approval submission",
|
||||
@@ -1265,10 +1422,15 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
pageSize: options.size,
|
||||
maxPages: options.maxPages,
|
||||
discoveryApplicantEmployeeNo: options.discoverTemplateIds ? options.discoverApplicantEmployeeNo : null,
|
||||
filterCreatorEmployeeNo: options.filterCreatorEmployeeNo || null,
|
||||
},
|
||||
checks: {
|
||||
secretMatrix: secretCheck,
|
||||
templateDiscovery: sanitizeDiscoveryCheck(discoveryCheck),
|
||||
creatorFilter: {
|
||||
...creatorFilterCheck,
|
||||
userid: undefined,
|
||||
},
|
||||
targets: targetChecks,
|
||||
callbackEvidence: {
|
||||
...evidenceCheck,
|
||||
|
||||
@@ -9,6 +9,11 @@ const APPROVAL_POC_TARGETS = Object.freeze([
|
||||
{ key: "leave", label: "请假" },
|
||||
{ key: "missed_punch", label: "补卡" },
|
||||
]);
|
||||
const APPROVAL_HR_ATTENDANCE_TARGETS = Object.freeze([
|
||||
...APPROVAL_POC_TARGETS,
|
||||
{ key: "outing", label: "外出" },
|
||||
{ key: "overtime", label: "加班" },
|
||||
]);
|
||||
|
||||
class WecomApprovalPocError extends Error {
|
||||
constructor(code, message, details = {}) {
|
||||
@@ -650,6 +655,7 @@ async function fetchWecomApprovalApplyEvent({
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APPROVAL_HR_ATTENDANCE_TARGETS,
|
||||
APPROVAL_POC_TARGETS,
|
||||
APPROVAL_APPLY_EVENT_ENDPOINT,
|
||||
APPROVAL_CREATE_TEMPLATE_ENDPOINT,
|
||||
|
||||
@@ -77,7 +77,8 @@ describe("check-wecom-approval-archive", () => {
|
||||
status: "approved",
|
||||
limit: 50,
|
||||
});
|
||||
expect(() => parseArgs(["--template=outing"])).toThrow(/Unsupported --template/);
|
||||
expect(parseArgs(["--template=outing", "--template=overtime"]).templates).toEqual(["outing", "overtime"]);
|
||||
expect(() => parseArgs(["--template=unknown"])).toThrow(/Unsupported --template/);
|
||||
expect(() => parseArgs(["--limit=101"])).toThrow(/--limit/);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,8 +89,11 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
"--size=50",
|
||||
"--template=leave:tpl-leave",
|
||||
"--template=missed_punch:tpl-missed",
|
||||
"--target=leave",
|
||||
"--targets=missed_punch",
|
||||
"--discover-template-ids",
|
||||
"--discover-applicant-employee-no=SMOKE002",
|
||||
"--filter-creator-employee-no=SMOKE002",
|
||||
]);
|
||||
|
||||
expect(options.live).toBe(true);
|
||||
@@ -98,12 +101,16 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
expect(options.size).toBe(50);
|
||||
expect(options.discoverTemplateIds).toBe(true);
|
||||
expect(options.discoverApplicantEmployeeNo).toBe("SMOKE002");
|
||||
expect(options.filterCreatorEmployeeNo).toBe("SMOKE002");
|
||||
expect(options.persistToDb).toBe(false);
|
||||
expect(options.targets).toEqual(["leave", "missed_punch"]);
|
||||
expect(options.templates).toEqual({
|
||||
leave: "tpl-leave",
|
||||
missed_punch: "tpl-missed",
|
||||
});
|
||||
expect(() => parseArgs(["--template=outing:tpl"])).toThrow(/unsupported target/);
|
||||
expect(parseArgs(["--target=outing", "--targets=overtime"]).targets).toEqual(["outing", "overtime"]);
|
||||
expect(parseArgs(["--template=outing:tpl"]).templates).toMatchObject({ outing: "tpl" });
|
||||
expect(() => parseArgs(["--target=unknown"])).toThrow(/unsupported target/);
|
||||
expect(() => parseArgs(["--size=101"])).toThrow(/--size/);
|
||||
expect(parseArgs(["--persist-to-db"]).persistToDb).toBe(true);
|
||||
});
|
||||
@@ -133,6 +140,29 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("builds explicit WEXT1-FORMAL outing and overtime inputs without changing defaults", () => {
|
||||
const defaultInputs = buildInputs(baseOptions({ templates: {} }), {});
|
||||
expect(defaultInputs.map((item) => item.key)).toEqual(["leave", "missed_punch"]);
|
||||
|
||||
const inputs = buildInputs(baseOptions({
|
||||
targets: ["outing", "overtime"],
|
||||
templates: { outing: "cli-outing-template" },
|
||||
}), {
|
||||
WECOM_APPROVAL_OUTING_TEMPLATE_ID: "env-outing-template",
|
||||
WECOM_APPROVAL_OVERTIME_TEMPLATE_ID: "env-overtime-template",
|
||||
});
|
||||
|
||||
expect(inputs.map((item) => item.key)).toEqual(["outing", "overtime"]);
|
||||
expect(inputs.find((item) => item.key === "outing")).toMatchObject({
|
||||
templateSource: "cli",
|
||||
templateIdPrefix: maskApprovalIdentifier("cli-outing-template"),
|
||||
});
|
||||
expect(inputs.find((item) => item.key === "overtime")).toMatchObject({
|
||||
templateSource: "WECOM_APPROVAL_OVERTIME_TEMPLATE_ID",
|
||||
templateIdPrefix: maskApprovalIdentifier("env-overtime-template"),
|
||||
});
|
||||
});
|
||||
|
||||
it("documents WECOM_AGENT_SECRET as the only Secret source", () => {
|
||||
const check = buildSecretCheck(baseConfig());
|
||||
expect(check).toMatchObject({
|
||||
@@ -227,6 +257,8 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
it("auto-discovers missing template IDs by applicant creator without leaking full identifiers", async () => {
|
||||
const discoveredLeaveTemplateId = "tpllv-auto-discovered-leave-sensitive";
|
||||
const discoveredMissedTemplateId = "tplmp-auto-discovered-missed-sensitive";
|
||||
const discoveredOutingTemplateId = "tplout-auto-discovered-outing-sensitive";
|
||||
const discoveredOvertimeTemplateId = "tplot-auto-discovered-overtime-sensitive";
|
||||
const fetchApprovalInfoListImpl = vi.fn(async ({ filters }) => {
|
||||
if (filters[0].key === "creator") {
|
||||
return {
|
||||
@@ -234,10 +266,15 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
response: {
|
||||
errcode: 0,
|
||||
spNoCount: 2,
|
||||
spNoPrefixes: [maskApprovalIdentifier("sp-discovery-leave"), maskApprovalIdentifier("sp-discovery-missed")],
|
||||
spNoCount: 4,
|
||||
spNoPrefixes: [
|
||||
maskApprovalIdentifier("sp-discovery-leave"),
|
||||
maskApprovalIdentifier("sp-discovery-missed"),
|
||||
maskApprovalIdentifier("sp-discovery-outing"),
|
||||
maskApprovalIdentifier("sp-discovery-overtime"),
|
||||
],
|
||||
},
|
||||
spNos: ["sp-discovery-leave", "sp-discovery-missed"],
|
||||
spNos: ["sp-discovery-leave", "sp-discovery-missed", "sp-discovery-outing", "sp-discovery-overtime"],
|
||||
nextCursor: "",
|
||||
};
|
||||
}
|
||||
@@ -274,13 +311,37 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
response: { errcode: 0, spStatus: "2" },
|
||||
};
|
||||
}
|
||||
if (spNo === "sp-discovery-outing") {
|
||||
return {
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
templateId: discoveredOutingTemplateId,
|
||||
spName: "外出",
|
||||
summary: detailSummary(spNo, { templateIdPrefix: maskApprovalIdentifier(discoveredOutingTemplateId) }),
|
||||
response: { errcode: 0, spStatus: "2" },
|
||||
};
|
||||
}
|
||||
if (spNo === "sp-discovery-overtime") {
|
||||
return {
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
templateId: discoveredOvertimeTemplateId,
|
||||
spName: "加班",
|
||||
summary: detailSummary(spNo, { templateIdPrefix: maskApprovalIdentifier(discoveredOvertimeTemplateId) }),
|
||||
response: { errcode: 0, spStatus: "2" },
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: detailSummary(spNo, {
|
||||
templateIdPrefix: spNo.startsWith(discoveredLeaveTemplateId)
|
||||
? maskApprovalIdentifier(discoveredLeaveTemplateId)
|
||||
: maskApprovalIdentifier(discoveredMissedTemplateId),
|
||||
: spNo.startsWith(discoveredMissedTemplateId)
|
||||
? maskApprovalIdentifier(discoveredMissedTemplateId)
|
||||
: spNo.startsWith(discoveredOutingTemplateId)
|
||||
? maskApprovalIdentifier(discoveredOutingTemplateId)
|
||||
: maskApprovalIdentifier(discoveredOvertimeTemplateId),
|
||||
}),
|
||||
response: { errcode: 0, spStatus: "2" },
|
||||
};
|
||||
@@ -288,6 +349,7 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
templates: {},
|
||||
targets: ["leave", "missed_punch", "outing", "overtime"],
|
||||
discoverTemplateIds: true,
|
||||
discoverApplicantEmployeeNo: "SMOKE002",
|
||||
}), {
|
||||
@@ -315,6 +377,18 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(`${discoveredMissedTemplateId}-sp`),
|
||||
},
|
||||
{
|
||||
callbackKind: "approval_status_change",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(`${discoveredOutingTemplateId}-sp`),
|
||||
},
|
||||
{
|
||||
callbackKind: "approval_status_change",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(`${discoveredOvertimeTemplateId}-sp`),
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -328,13 +402,23 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
expect(fetchApprovalInfoListImpl.mock.calls[2][0].filters).toEqual([
|
||||
{ key: "template_id", value: discoveredMissedTemplateId },
|
||||
]);
|
||||
expect(fetchApprovalInfoListImpl.mock.calls[3][0].filters).toEqual([
|
||||
{ key: "template_id", value: discoveredOutingTemplateId },
|
||||
]);
|
||||
expect(fetchApprovalInfoListImpl.mock.calls[4][0].filters).toEqual([
|
||||
{ key: "template_id", value: discoveredOvertimeTemplateId },
|
||||
]);
|
||||
const serialized = JSON.stringify(report);
|
||||
expect(serialized).not.toContain(discoveredLeaveTemplateId);
|
||||
expect(serialized).not.toContain(discoveredMissedTemplateId);
|
||||
expect(serialized).not.toContain(discoveredOutingTemplateId);
|
||||
expect(serialized).not.toContain(discoveredOvertimeTemplateId);
|
||||
expect(serialized).not.toContain(SENSITIVE_USER_ID);
|
||||
expect(report.checks.templateDiscovery.discoveredTemplatePrefixes).toEqual({
|
||||
leave: maskApprovalIdentifier(discoveredLeaveTemplateId),
|
||||
missed_punch: maskApprovalIdentifier(discoveredMissedTemplateId),
|
||||
outing: maskApprovalIdentifier(discoveredOutingTemplateId),
|
||||
overtime: maskApprovalIdentifier(discoveredOvertimeTemplateId),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -402,6 +486,107 @@ describe("check-wecom-approval-compensation-poc", () => {
|
||||
expect(report.checks.archivePersistence.status).toBe("SKIPPED");
|
||||
});
|
||||
|
||||
it("filters WEXT1-FORMAL template pulls by creator employee to avoid broad formal-template reads", async () => {
|
||||
const creatorUserId = "formal_creator_userid_should_not_leak";
|
||||
const outingTemplateId = "outing-template-sensitive";
|
||||
const overtimeTemplateId = "overtime-template-sensitive";
|
||||
const fetchApprovalInfoListImpl = vi.fn(async ({ filters }) => {
|
||||
expect(filters).toHaveLength(2);
|
||||
expect(filters[0].key).toBe("template_id");
|
||||
expect(filters[1]).toEqual({ key: "creator", value: creatorUserId });
|
||||
return {
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
response: { errcode: 0, spNoCount: 1, spNoPrefixes: [maskApprovalIdentifier(`${filters[0].value}-sp`)] },
|
||||
spNos: [`${filters[0].value}-sp`],
|
||||
nextCursor: "",
|
||||
};
|
||||
});
|
||||
const fetchApprovalDetailImpl = vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: detailSummary(spNo, { spStatus: "2" }),
|
||||
response: { errcode: 0, spStatus: "2" },
|
||||
}));
|
||||
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
targets: ["outing", "overtime"],
|
||||
templates: {
|
||||
outing: outingTemplateId,
|
||||
overtime: overtimeTemplateId,
|
||||
},
|
||||
filterCreatorEmployeeNo: "SMOKE002",
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
queryApplicantActorImpl: vi.fn(async () => ({
|
||||
employeeNo: "SMOKE002",
|
||||
status: "active",
|
||||
archived: false,
|
||||
wecomExternalUserId: creatorUserId,
|
||||
})),
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
readCallbackEvidenceImpl: vi.fn(async () => [
|
||||
{
|
||||
callbackKind: "approval_status_change",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(`${outingTemplateId}-sp`),
|
||||
},
|
||||
{
|
||||
callbackKind: "approval_status_change",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(`${overtimeTemplateId}-sp`),
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PASS_COMPENSATION_POC");
|
||||
expect(report.checks.creatorFilter).toMatchObject({
|
||||
status: "OK",
|
||||
employeeNo: "SMOKE002",
|
||||
userIdPrefix: maskApprovalIdentifier(creatorUserId),
|
||||
});
|
||||
expect(fetchApprovalInfoListImpl).toHaveBeenCalledTimes(2);
|
||||
const serialized = JSON.stringify(report);
|
||||
expect(serialized).not.toContain(creatorUserId);
|
||||
expect(serialized).not.toContain(outingTemplateId);
|
||||
expect(serialized).not.toContain(overtimeTemplateId);
|
||||
});
|
||||
|
||||
it("does not pull formal templates when the requested creator filter cannot be resolved", async () => {
|
||||
const fetchApprovalInfoListImpl = vi.fn();
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
targets: ["outing"],
|
||||
templates: { outing: "formal-outing-template-sensitive" },
|
||||
filterCreatorEmployeeNo: "SMOKE002",
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
queryApplicantActorImpl: vi.fn(async () => ({
|
||||
employeeNo: "SMOKE002",
|
||||
status: "active",
|
||||
archived: false,
|
||||
wecomExternalUserId: "",
|
||||
})),
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl: vi.fn(),
|
||||
readCallbackEvidenceImpl: vi.fn(async () => []),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PARTIAL_COMPENSATION_NEEDS_CONFIG");
|
||||
expect(fetchApprovalInfoListImpl).not.toHaveBeenCalled();
|
||||
expect(report.checks.targets[0]).toMatchObject({
|
||||
target: "outing",
|
||||
status: "NEEDS_CONFIG",
|
||||
reason: "creator_filter_not_ready",
|
||||
});
|
||||
});
|
||||
|
||||
it("requires both --persist-to-db and WECOM_ARCHIVE_WRITE=1 before OA archive writes", async () => {
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
live: true,
|
||||
|
||||
在新工单中引用
屏蔽一个用户