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