治理(08-11-internal-oa-finance-backoffice-positioning-and-navigation-reduction): 固化财务后台定位与写入定性
这个提交包含在:
@@ -0,0 +1,123 @@
|
||||
const {
|
||||
FEATURE_FLAG_APPLY_DECISION,
|
||||
executeFeatureFlagApplyAttempt,
|
||||
} = require("../../../../scripts/qa/feature-flag-apply-uncertainty.mjs");
|
||||
|
||||
const TARGETS = Object.freeze([
|
||||
{ flagKey: "announcements_v1", preVersion: 49, preOverrideMode: "force_on", postOverrideMode: "force_off" },
|
||||
{ flagKey: "todo_board_v1", preVersion: 49, preOverrideMode: "force_on", postOverrideMode: "force_off" },
|
||||
{ flagKey: "weekly_report_v1", preVersion: 49, preOverrideMode: "force_on", postOverrideMode: "force_off" },
|
||||
]);
|
||||
|
||||
function state(kind) {
|
||||
return TARGETS.map((target, index) => {
|
||||
if (kind === "partial" && index === TARGETS.length - 1) {
|
||||
return { flagKey: target.flagKey, version: target.preVersion, overrideMode: target.preOverrideMode };
|
||||
}
|
||||
if (kind === "pre") {
|
||||
return { flagKey: target.flagKey, version: target.preVersion, overrideMode: target.preOverrideMode };
|
||||
}
|
||||
return { flagKey: target.flagKey, version: target.preVersion + 1, overrideMode: target.postOverrideMode };
|
||||
});
|
||||
}
|
||||
|
||||
describe("feature flag apply uncertainty", () => {
|
||||
it("continues POST when APIRequestContext succeeds although the page observer sees zero", async () => {
|
||||
const sendBatch = vi.fn().mockResolvedValue({ count: 3 });
|
||||
const readCurrent = vi.fn().mockResolvedValue(state("post"));
|
||||
const rollback = vi.fn();
|
||||
|
||||
const result = await executeFeatureFlagApplyAttempt({
|
||||
targets: TARGETS,
|
||||
payload: { items: TARGETS },
|
||||
sendMutation: sendBatch,
|
||||
readCurrent,
|
||||
observedRequestCount: 0,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
decision: FEATURE_FLAG_APPLY_DECISION.CONTINUE_POST,
|
||||
classification: "APPLIED",
|
||||
sendAttemptCount: 1,
|
||||
responseKnown: true,
|
||||
response: { count: 3 },
|
||||
observedRequestCount: 0,
|
||||
observerMismatch: true,
|
||||
});
|
||||
expect(sendBatch).toHaveBeenCalledTimes(1);
|
||||
expect(readCurrent).toHaveBeenCalledTimes(1);
|
||||
expect(rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues POST when the response is missing but readback proves POST state", async () => {
|
||||
const rollback = vi.fn();
|
||||
const result = await executeFeatureFlagApplyAttempt({
|
||||
targets: TARGETS,
|
||||
payload: { items: TARGETS },
|
||||
sendMutation: vi.fn().mockRejectedValue(new Error("transport_result_missing")),
|
||||
readCurrent: vi.fn().mockResolvedValue(state("post")),
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
decision: FEATURE_FLAG_APPLY_DECISION.CONTINUE_POST,
|
||||
classification: "APPLIED",
|
||||
sendAttemptCount: 1,
|
||||
responseKnown: false,
|
||||
});
|
||||
expect(rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops without retry or rollback when the response is missing and readback remains PRE", async () => {
|
||||
const retry = vi.fn();
|
||||
const rollback = vi.fn();
|
||||
const result = await executeFeatureFlagApplyAttempt({
|
||||
targets: TARGETS,
|
||||
payload: { items: TARGETS },
|
||||
sendMutation: vi.fn().mockRejectedValue(new Error("transport_result_missing")),
|
||||
readCurrent: vi.fn().mockResolvedValue(state("pre")),
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
decision: FEATURE_FLAG_APPLY_DECISION.STOP_NOT_APPLIED,
|
||||
classification: "NOT_APPLIED",
|
||||
sendAttemptCount: 1,
|
||||
responseKnown: false,
|
||||
});
|
||||
expect(retry).not.toHaveBeenCalled();
|
||||
expect(rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops unknown with zero mutation when readback fails", async () => {
|
||||
const retry = vi.fn();
|
||||
const rollback = vi.fn();
|
||||
const result = await executeFeatureFlagApplyAttempt({
|
||||
targets: TARGETS,
|
||||
payload: { items: TARGETS },
|
||||
sendMutation: vi.fn().mockRejectedValue(new Error("transport_result_missing")),
|
||||
readCurrent: vi.fn().mockRejectedValue(new Error("readback_unavailable")),
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ decision: FEATURE_FLAG_APPLY_DECISION.STOP_UNKNOWN, classification: "READBACK_FAILED" });
|
||||
expect(retry).not.toHaveBeenCalled();
|
||||
expect(rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops for manual decision with zero mutation when readback is partial", async () => {
|
||||
const continuePost = vi.fn();
|
||||
const rollback = vi.fn();
|
||||
const result = await executeFeatureFlagApplyAttempt({
|
||||
targets: TARGETS,
|
||||
payload: { items: TARGETS },
|
||||
sendMutation: vi.fn().mockRejectedValue(new Error("transport_result_missing")),
|
||||
readCurrent: vi.fn().mockResolvedValue(state("partial")),
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
decision: FEATURE_FLAG_APPLY_DECISION.STOP_FOR_MANUAL_DECISION,
|
||||
classification: "PARTIAL_OR_UNEXPECTED",
|
||||
});
|
||||
expect(result.observations.map((item) => item.state)).toEqual(["POST", "POST", "PRE"]);
|
||||
expect(continuePost).not.toHaveBeenCalled();
|
||||
expect(rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户