import { describe, it, expect, vi } from "vitest"; import { licenseMachinesReducer, initialLicenseMachinesState, selectShowEmptyState, selectCanDeactivate, selectCanActivate, shouldLoadOnOpen, performActivate, performDeactivate, loadMachines, loadActivationStatus, type LicenseMachinesAction, type LicenseMachinesDeps, type LicenseMachinesState, } from "./useLicenseMachines"; import { parseLicenseError, type ActivationStatus, type LicenseError, type LicenseErrorCode, type LicenseResult, type MachineInfo, } from "../services/licenseService"; const err = (code: LicenseErrorCode, extra: Record = {}): LicenseError => parseLicenseError(JSON.stringify({ code, ...extra })); const machine = (id: string): MachineInfo => ({ machine_id: id, machine_name: `host-${id}`, activated_at: "2026-09-01T00:00:00Z", last_seen_at: "2026-09-20T00:00:00Z", }); const reduce = ( actions: LicenseMachinesAction[], from: LicenseMachinesState = initialLicenseMachinesState, ): LicenseMachinesState => actions.reduce(licenseMachinesReducer, from); const limitHit = reduce([ { type: "ACTIVATE_START" }, { type: "ACTIVATE_ERROR", error: err("machine_limit_reached") }, ]); describe("licenseMachinesReducer — machine limit (D2)", () => { it("opens the list and keeps the message on machine_limit_reached", () => { expect(limitHit.activationError?.code).toBe("machine_limit_reached"); expect(limitHit.listOpen).toBe(true); expect(limitHit.activating).toBe(false); }); it("does not open the list for other activation refusals", () => { const s = reduce([ { type: "ACTIVATE_START" }, { type: "ACTIVATE_ERROR", error: err("license_revoked") }, ]); expect(s.activationError?.code).toBe("license_revoked"); expect(s.listOpen).toBe(false); }); it("keeps the activation error through the list load it triggers", () => { const loading = reduce([{ type: "LIST_LOAD_START" }], limitHit); expect(loading.activationError?.code).toBe("machine_limit_reached"); const loaded = reduce( [{ type: "LIST_LOAD_DONE", machines: [machine("a"), machine("b"), machine("c")] }], loading, ); expect(loaded.activationError?.code).toBe("machine_limit_reached"); expect(loaded.machines).toHaveLength(3); expect(loaded.listLoaded).toBe(true); }); it("keeps the activation error when the list load itself fails (e.g. a 429 right after the 409)", () => { const s = reduce( [{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_ERROR", error: err("rate_limit", { retry_after_s: 30 }) }], limitHit, ); expect(s.activationError?.code).toBe("machine_limit_reached"); expect(s.listError?.code).toBe("rate_limit"); expect(s.listLoaded).toBe(false); }); it("clears only a machine-limit activation error once a slot is freed", () => { const withList = reduce( [{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [machine("a"), machine("b")] }], limitHit, ); const freed = reduce( [{ type: "DEACTIVATE_START", machineId: "a" }, { type: "DEACTIVATE_DONE", machineId: "a" }], withList, ); expect(freed.activationError).toBeNull(); expect(freed.machines.map((m) => m.machine_id)).toEqual(["b"]); expect(freed.deactivatingId).toBeNull(); const networkFailure = reduce([ { type: "ACTIVATE_START" }, { type: "ACTIVATE_ERROR", error: err("network") }, { type: "DEACTIVATE_START", machineId: "a" }, { type: "DEACTIVATE_DONE", machineId: "a" }, ]); expect(networkFailure.activationError?.code).toBe("network"); }); }); describe("licenseMachinesReducer — who may clear the activation error", () => { // Only ACTIVATE_START and a DEACTIVATE_DONE after a machine-limit refusal // clear it. Everything else (list loads in particular) must leave it alone. const preserving: LicenseMachinesAction[] = [ { type: "ACTIVATION_STATUS", activation: { is_activated: false, machine_id: "x" } }, { type: "TOGGLE_LIST" }, { type: "TOGGLE_LIST" }, { type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [] }, { type: "LIST_LOAD_ERROR", error: err("server", { status: 502 }) }, { type: "DEACTIVATE_START", machineId: "a" }, { type: "DEACTIVATE_ERROR", error: err("network") }, { type: "ACTIVATE_DONE" }, ]; // ACTIVATE_DONE is listed too: it is not a clearing path of its own, since // ACTIVATE_START always clears the error before a successful activation. it.each(preserving.map((a) => [a.type, a] as const))("%s keeps it", (_type, action) => { expect(licenseMachinesReducer(limitHit, action).activationError?.code).toBe( "machine_limit_reached", ); }); it("ACTIVATE_START clears it", () => { expect(licenseMachinesReducer(limitHit, { type: "ACTIVATE_START" }).activationError).toBeNull(); }); }); describe("licenseMachinesReducer — list lifecycle", () => { it("DEACTIVATE_START clears a stale list error", () => { const s = reduce([ { type: "DEACTIVATE_START", machineId: "a" }, { type: "DEACTIVATE_ERROR", error: err("rate_limit", { retry_after_s: 20 }) }, { type: "DEACTIVATE_START", machineId: "a" }, ]); expect(s.listError).toBeNull(); expect(s.deactivatingId).toBe("a"); }); it("closes and invalidates the list after a successful activation", () => { const s = reduce( [ { type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [machine("a")] }, { type: "ACTIVATE_START" }, { type: "ACTIVATE_DONE" }, ], { ...initialLicenseMachinesState, listOpen: true }, ); expect(s.listOpen).toBe(false); expect(s.listLoaded).toBe(false); expect(s.activating).toBe(false); }); }); describe("selectShowEmptyState", () => { it("is false before the first load", () => { expect(selectShowEmptyState(initialLicenseMachinesState)).toBe(false); }); it("is true once a load returns an empty list without error", () => { expect(selectShowEmptyState(reduce([{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [] }]))).toBe(true); }); it("is false while loading, on a list error, or with machines", () => { expect(selectShowEmptyState(reduce([{ type: "LIST_LOAD_START" }]))).toBe(false); expect( selectShowEmptyState( reduce([{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_ERROR", error: err("license_revoked") }]), ), ).toBe(false); expect( selectShowEmptyState(reduce([{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [machine("a")] }])), ).toBe(false); }); it("is false when a deactivation fails on a list emptied locally", () => { const s = reduce([ { type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [] }, { type: "DEACTIVATE_START", machineId: "a" }, { type: "DEACTIVATE_ERROR", error: err("network") }, ]); expect(selectShowEmptyState(s)).toBe(false); }); }); describe("selectCanDeactivate", () => { const loaded = reduce([ { type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [machine("a"), machine("b")] }, ]); it("allows a deactivation on an idle, loaded list", () => { expect(selectCanDeactivate(loaded)).toBe(true); }); it("blocks every button while one deactivation is running", () => { expect(selectCanDeactivate(reduce([{ type: "DEACTIVATE_START", machineId: "a" }], loaded))).toBe(false); }); it("blocks the buttons while a 409 reloads an already loaded list", () => { // The reload answers with the list as of its request: a machine deactivated // before it lands would reappear. const reloading = reduce( [ { type: "ACTIVATE_START" }, { type: "ACTIVATE_ERROR", error: err("machine_limit_reached") }, { type: "LIST_LOAD_START" }, ], loaded, ); expect(reloading.machines).toHaveLength(2); expect(selectCanDeactivate(reloading)).toBe(false); }); }); describe("selectCanActivate", () => { it("allows an activation when nothing is running", () => { expect(selectCanActivate(initialLicenseMachinesState)).toBe(true); }); it("blocks the button while an activation is running", () => { expect(selectCanActivate(reduce([{ type: "ACTIVATE_START" }]))).toBe(false); }); it("blocks the button while the list loads", () => { // Otherwise a verify answer landing after a successful activation would // mark as fresh a list that no longer includes this machine. const loading = reduce([{ type: "TOGGLE_LIST" }, { type: "LIST_LOAD_START" }]); expect(selectCanActivate(loading)).toBe(false); }); }); describe("shouldLoadOnOpen", () => { const loaded = reduce([{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_DONE", machines: [] }]); it("loads the first time the list opens", () => { expect(shouldLoadOnOpen(initialLicenseMachinesState)).toBe(true); }); it("does not reload a list already loaded without error", () => { expect(shouldLoadOnOpen(loaded)).toBe(false); }); it("retries after a list error", () => { expect( shouldLoadOnOpen(reduce([{ type: "LIST_LOAD_START" }, { type: "LIST_LOAD_ERROR", error: err("network") }])), ).toBe(true); }); it("never loads when closing the list or while a load is running", () => { expect(shouldLoadOnOpen({ ...initialLicenseMachinesState, listOpen: true })).toBe(false); expect(shouldLoadOnOpen(reduce([{ type: "LIST_LOAD_START" }]))).toBe(false); }); }); // Orchestration: the effects are plain functions with injected dependencies, // so the D2 path can be replayed without React or Tauri. Every dispatched // action is folded through the real reducer. function harness(overrides: Partial = {}) { let state = initialLicenseMachinesState; const deps: LicenseMachinesDeps = { activateMachine: vi.fn(async (): Promise> => ({ ok: true, value: undefined })), deactivateMachine: vi.fn(async (): Promise> => ({ ok: true, value: undefined })), listActivatedMachines: vi.fn( async (): Promise> => ({ ok: true, value: [] }), ), getActivationStatus: vi.fn(async () => ({ is_activated: true, machine_id: "this" })), refresh: vi.fn(async () => {}), dispatch: (action) => { state = licenseMachinesReducer(state, action); }, ...overrides, }; const networkCalls = () => vi.mocked(deps.activateMachine).mock.calls.length + vi.mocked(deps.deactivateMachine).mock.calls.length + vi.mocked(deps.listActivatedMachines).mock.calls.length; return { deps, getState: () => state, networkCalls }; } describe("machine-limit path (D2) end to end", () => { it("fits in 4 license-server calls and keeps the message until a slot is freed", async () => { const activateMachine = vi .fn<() => Promise>>() .mockResolvedValueOnce({ ok: false, error: err("machine_limit_reached") }) .mockResolvedValueOnce({ ok: true, value: undefined }); const { deps, getState, networkCalls } = harness({ activateMachine, listActivatedMachines: vi.fn(async () => ({ ok: true as const, value: [machine("a"), machine("b"), machine("c")], })), }); await performActivate(deps); let s = getState(); expect(s.activationError?.code).toBe("machine_limit_reached"); expect(s.listOpen).toBe(true); expect(s.listLoaded).toBe(true); expect(s.machines).toHaveLength(3); expect(deps.refresh).not.toHaveBeenCalled(); await performDeactivate(deps, "b"); s = getState(); expect(s.machines.map((m) => m.machine_id)).toEqual(["a", "c"]); expect(s.activationError).toBeNull(); await performActivate(deps); s = getState(); expect(s.activationError).toBeNull(); // activate (409) + verify + deactivate + activate: under the server's // 5 requests per minute and per IP. expect(networkCalls()).toBe(4); expect(deps.listActivatedMachines).toHaveBeenCalledTimes(1); expect(deps.refresh).toHaveBeenCalledTimes(2); }); }); describe("performActivate", () => { it("refreshes the edition after a successful activation, without reloading the list", async () => { const { deps, getState } = harness(); await performActivate(deps); expect(deps.refresh).toHaveBeenCalledTimes(1); expect(deps.getActivationStatus).toHaveBeenCalledTimes(1); expect(deps.listActivatedMachines).not.toHaveBeenCalled(); expect(getState().activation?.is_activated).toBe(true); }); it("still refreshes when the local activation status cannot be read", async () => { const { deps } = harness({ getActivationStatus: vi.fn(async () => { throw new Error("machine id unreadable"); }), }); await expect(performActivate(deps)).resolves.toBeUndefined(); expect(deps.refresh).toHaveBeenCalledTimes(1); }); it("neither loads the list nor refreshes on other refusals", async () => { const { deps, getState } = harness({ activateMachine: vi.fn(async () => ({ ok: false as const, error: err("license_revoked") })), }); await performActivate(deps); expect(getState().activationError?.code).toBe("license_revoked"); expect(deps.listActivatedMachines).not.toHaveBeenCalled(); expect(deps.refresh).not.toHaveBeenCalled(); }); }); describe("performDeactivate", () => { it("removes the machine locally and refreshes, without calling verify", async () => { const { deps, getState } = harness(); await loadMachines({ ...deps, listActivatedMachines: vi.fn(async () => ({ ok: true as const, value: [machine("a"), machine("b")] })), }); await performDeactivate(deps, "a"); expect(getState().machines.map((m) => m.machine_id)).toEqual(["b"]); expect(deps.listActivatedMachines).not.toHaveBeenCalled(); expect(deps.refresh).toHaveBeenCalledTimes(1); }); it("re-reads the local status, so deactivating this machine brings back Activate", async () => { const getActivationStatus = vi .fn<() => Promise>() .mockResolvedValueOnce({ is_activated: true, machine_id: "this" }) .mockResolvedValueOnce({ is_activated: false, machine_id: "this" }); const { deps, getState } = harness({ getActivationStatus }); await loadActivationStatus(deps); await loadMachines({ ...deps, listActivatedMachines: vi.fn(async () => ({ ok: true as const, value: [machine("this"), machine("b")], })), }); expect(getState().activation?.is_activated).toBe(true); // Rust deletes this machine's activation token on success; only the // re-read status turns the card back from "Activated" to "Activate". await performDeactivate(deps, "this"); expect(getState().activation?.is_activated).toBe(false); expect(getActivationStatus).toHaveBeenCalledTimes(2); }); it("shows a refusal on the list and releases the busy state", async () => { const { deps, getState } = harness({ deactivateMachine: vi.fn(async () => ({ ok: false as const, error: err("rate_limit", { retry_after_s: 25 }), })), }); await performDeactivate(deps, "a"); const s = getState(); expect(s.listError?.code).toBe("rate_limit"); expect(s.listError?.params).toEqual({ count: 25 }); expect(s.deactivatingId).toBeNull(); expect(deps.refresh).not.toHaveBeenCalled(); }); });