import { describe, expect, it } from "vitest"; import { evaluateFlag, getFeatureFlagVariant, type StoredFeatureFlag } from "./evaluate.js"; function mkFlag(overrides: Partial = {}): StoredFeatureFlag { return { id: overrides.id ?? "flag_1", name: overrides.name ?? "test-flag", enabled: overrides.enabled ?? true, rules: overrides.rules ?? null, variants: overrides.variants ?? [], enabledFrom: overrides.enabledFrom ?? null, enabledUntil: overrides.enabledUntil ?? null, parentFlagId: overrides.parentFlagId ?? null, }; } function mkLoader(flags: StoredFeatureFlag[]) { const byKey = new Map(); for (const f of flags) { byKey.set(f.name, f); byKey.set(f.id, f); } return async (key: string) => byKey.get(key) ?? null; } describe("evaluateFlag — basics", () => { it("flag-not-found", async () => { const r = await evaluateFlag("missing", { flagName: "missing" }, { load: mkLoader([]) }); expect(r).toEqual({ enabled: false, reason: "flag-not-found" }); }); it("disabled", async () => { const r = await evaluateFlag( "test-flag", { flagName: "test-flag" }, { load: mkLoader([mkFlag({ enabled: false })]) }, ); expect(r.enabled).toBe(false); expect(r.reason).toBe("disabled"); }); it("enabled with no rules → matched", async () => { const r = await evaluateFlag( "test-flag", { flagName: "test-flag" }, { load: mkLoader([mkFlag({})]) }, ); expect(r).toEqual({ enabled: true, reason: "matched" }); }); }); describe("evaluateFlag — time window", () => { it("future enabledFrom → time-window-future", async () => { const r = await evaluateFlag( "test-flag", { flagName: "test-flag", now: new Date("2026-01-01T00:00:00Z") }, { load: mkLoader([mkFlag({ enabledFrom: new Date("2030-01-01T00:00:00Z") })]), }, ); expect(r.reason).toBe("time-window-future"); expect(r.enabled).toBe(false); }); it("past enabledUntil → time-window-expired", async () => { const r = await evaluateFlag( "test-flag", { flagName: "test-flag", now: new Date("2026-01-01T00:00:00Z") }, { load: mkLoader([mkFlag({ enabledUntil: new Date("2020-01-01T00:00:00Z") })]), }, ); expect(r.reason).toBe("time-window-expired"); }); }); describe("evaluateFlag — dependency resolution", () => { it("parent disabled → child returns parent-disabled", async () => { const parent = mkFlag({ id: "parent_1", name: "parent", enabled: false }); const child = mkFlag({ id: "child_1", name: "child", parentFlagId: "parent_1" }); const r = await evaluateFlag("child", { flagName: "child" }, { load: mkLoader([parent, child]) }); expect(r.reason).toBe("parent-disabled"); expect(r.enabled).toBe(false); }); it("parent missing → parent-not-found", async () => { const child = mkFlag({ id: "child_1", name: "child", parentFlagId: "ghost" }); const r = await evaluateFlag("child", { flagName: "child" }, { load: mkLoader([child]) }); expect(r.reason).toBe("parent-not-found"); }); it("parent rules fail → parent-disabled bubbles up", async () => { const parent = mkFlag({ id: "parent_1", name: "parent", rules: { type: "allowlist", userIds: ["other_user"] }, }); const child = mkFlag({ id: "child_1", name: "child", parentFlagId: "parent_1" }); const r = await evaluateFlag( "child", { flagName: "child", userId: "u_self" }, { load: mkLoader([parent, child]) }, ); expect(r.reason).toBe("parent-disabled"); }); it("parent enabled → child evaluates own rules", async () => { const parent = mkFlag({ id: "parent_1", name: "parent" }); const child = mkFlag({ id: "child_1", name: "child", parentFlagId: "parent_1" }); const r = await evaluateFlag("child", { flagName: "child" }, { load: mkLoader([parent, child]) }); expect(r.enabled).toBe(true); }); it("loop guard — child→parent→child returns parent-disabled, no infinite recursion", async () => { const a = mkFlag({ id: "a", name: "a", parentFlagId: "b" }); const b = mkFlag({ id: "b", name: "b", parentFlagId: "a" }); const r = await evaluateFlag("a", { flagName: "a" }, { load: mkLoader([a, b]) }); expect(r.reason).toBe("parent-disabled"); }); }); describe("evaluateFlag — rules", () => { it("rule mismatch → no-match", async () => { const r = await evaluateFlag( "test-flag", { flagName: "test-flag", planTier: "free" }, { load: mkLoader([ mkFlag({ rules: { type: "plan-tier", plans: ["enterprise"] } }), ]), }, ); expect(r.reason).toBe("no-match"); }); it("rule match → matched", async () => { const r = await evaluateFlag( "test-flag", { flagName: "test-flag", planTier: "enterprise" }, { load: mkLoader([ mkFlag({ rules: { type: "plan-tier", plans: ["enterprise"] } }), ]), }, ); expect(r.enabled).toBe(true); expect(r.reason).toBe("matched"); }); }); describe("evaluateFlag — variants", () => { it("single variant always wins", async () => { const r = await evaluateFlag( "ab", { flagName: "ab", userId: "u" }, { load: mkLoader([mkFlag({ name: "ab", variants: [{ key: "only" }] })]) }, ); expect(r.variant?.key).toBe("only"); }); it("multiple variants are sticky per user", async () => { const variants = [ { key: "A", weight: 1 }, { key: "B", weight: 1 }, { key: "C", weight: 1 }, ]; const flag = mkFlag({ name: "vv", variants }); const r1 = await evaluateFlag("vv", { flagName: "vv", userId: "u_42" }, { load: mkLoader([flag]) }); const r2 = await evaluateFlag("vv", { flagName: "vv", userId: "u_42" }, { load: mkLoader([flag]) }); expect(r1.variant?.key).toBe(r2.variant?.key); expect(r1.variant?.key).toMatch(/^[ABC]$/); }); it("getFeatureFlagVariant returns null when disabled", async () => { const v = await getFeatureFlagVariant( "x", { flagName: "x" }, { load: mkLoader([mkFlag({ name: "x", enabled: false, variants: [{ key: "A" }] })]) }, ); expect(v).toBeNull(); }); it("getFeatureFlagVariant returns the variant when matched", async () => { const v = await getFeatureFlagVariant( "x", { flagName: "x", userId: "u" }, { load: mkLoader([mkFlag({ name: "x", variants: [{ key: "A" }] })]) }, ); expect(v?.key).toBe("A"); }); });