文件
hyb-oa/server/tests/unit/scripts/feature-flag-apply-uncertainty.test.js

124 行
4.7 KiB
JavaScript

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();
});
});