AUTOREM 收口剩余自动化运营门禁
这个提交包含在:
@@ -0,0 +1,200 @@
|
||||
#!/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,
|
||||
mode: "force_on",
|
||||
issueId: process.env.QA_ISSUE_ID || "AUTOREM",
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--mode") {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("--mode requires force_on or force_off");
|
||||
}
|
||||
options.mode = value;
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--mode=")) {
|
||||
options.mode = arg.slice("--mode=".length);
|
||||
} else if (arg.startsWith("--base-url=")) {
|
||||
options.baseUrl = arg.slice("--base-url=".length);
|
||||
}
|
||||
}
|
||||
|
||||
if (!["force_on", "force_off"].includes(options.mode)) {
|
||||
throw new Error(`invalid mode: ${options.mode}`);
|
||||
}
|
||||
options.baseUrl = options.baseUrl.trim().replace(/\/$/, "");
|
||||
options.issueId = options.issueId.trim() || "AUTOREM";
|
||||
return options;
|
||||
}
|
||||
|
||||
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": "autorem-payment-batch-flag-restore",
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
async function listFlags(baseUrl, accessToken) {
|
||||
const result = await fetchJson(`${baseUrl}/api/system/feature-flags`, {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(`feature flag list failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim());
|
||||
}
|
||||
return result.json?.data?.items ?? [];
|
||||
}
|
||||
|
||||
function findRequiredFlag(items, flagKey) {
|
||||
const item = items.find((candidate) => candidate.flagKey === flagKey);
|
||||
if (!item) {
|
||||
throw new Error(`missing feature flag: ${flagKey}`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function summarizeFlag(item) {
|
||||
return {
|
||||
flagKey: item.flagKey,
|
||||
overrideMode: item.overrideMode,
|
||||
rolloutType: item.rolloutType,
|
||||
version: item.version,
|
||||
effectiveEnabled: item.effectiveEnabled,
|
||||
effectiveReason: item.effectiveReason,
|
||||
};
|
||||
}
|
||||
|
||||
function printFlag(prefix, item) {
|
||||
const summary = summarizeFlag(item);
|
||||
console.log(
|
||||
`[restore-payment-batch-flag] ${prefix} ${summary.flagKey} override=${summary.overrideMode} effective=${summary.effectiveEnabled} reason=${summary.effectiveReason} version=${summary.version}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function updatePaymentFlag(baseUrl, accessToken, paymentFlag, mode, issueId) {
|
||||
const result = await fetchJson(`${baseUrl}/api/system/feature-flags/payment_batch_v1`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
overrideMode: mode,
|
||||
rolloutType: "global",
|
||||
departmentIds: [],
|
||||
note: `${issueId} restore formal payment batch flag to ${mode}`,
|
||||
expectedVersion: paymentFlag.version,
|
||||
}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
throw new Error(`payment_batch_v1 update failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim());
|
||||
}
|
||||
return result.json?.data?.item ?? null;
|
||||
}
|
||||
|
||||
function assertFinalState(paymentFlag, erpFlag, mode) {
|
||||
const paymentOk = mode === "force_on"
|
||||
? paymentFlag.overrideMode === "force_on" && paymentFlag.effectiveEnabled === true
|
||||
: paymentFlag.overrideMode === "force_off" && paymentFlag.effectiveEnabled === false;
|
||||
const erpOk = erpFlag.overrideMode === "force_off" && erpFlag.effectiveEnabled === false;
|
||||
if (!paymentOk) {
|
||||
throw new Error(`payment_batch_v1 final state mismatch: override=${paymentFlag.overrideMode} effective=${paymentFlag.effectiveEnabled}`);
|
||||
}
|
||||
if (!erpOk) {
|
||||
throw new Error(`erp_sync final state mismatch: override=${erpFlag.overrideMode} effective=${erpFlag.effectiveEnabled}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (process.env.AUTOREM_RESTORE_APPROVED !== "1") {
|
||||
throw new Error("AUTOREM_RESTORE_APPROVED=1 is required");
|
||||
}
|
||||
|
||||
const credential = getAdminCredential();
|
||||
if (!credential) {
|
||||
throw new Error("missing LIVE_SMOKE_ADMIN_* credentials");
|
||||
}
|
||||
|
||||
const accessToken = await login(options.baseUrl, credential);
|
||||
const beforeItems = await listFlags(options.baseUrl, accessToken);
|
||||
const beforePayment = findRequiredFlag(beforeItems, "payment_batch_v1");
|
||||
const beforeErp = findRequiredFlag(beforeItems, "erp_sync");
|
||||
printFlag("before", beforePayment);
|
||||
printFlag("before", beforeErp);
|
||||
|
||||
if (beforePayment.overrideMode === options.mode) {
|
||||
console.log(`[restore-payment-batch-flag] payment_batch_v1 already ${options.mode}; no update sent`);
|
||||
} else {
|
||||
const updated = await updatePaymentFlag(options.baseUrl, accessToken, beforePayment, options.mode, options.issueId);
|
||||
if (updated) {
|
||||
printFlag("updated", updated);
|
||||
}
|
||||
}
|
||||
|
||||
const afterItems = await listFlags(options.baseUrl, accessToken);
|
||||
const afterPayment = findRequiredFlag(afterItems, "payment_batch_v1");
|
||||
const afterErp = findRequiredFlag(afterItems, "erp_sync");
|
||||
assertFinalState(afterPayment, afterErp, options.mode);
|
||||
printFlag("after", afterPayment);
|
||||
printFlag("after", afterErp);
|
||||
console.log(`[restore-payment-batch-flag] status=PASS base=${options.baseUrl} mode=${options.mode}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`[restore-payment-batch-flag][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户