import { beforeEach, describe, expect, it } from "vitest"; import { chooseLocalAdaptiveAction, clearLocalAdaptiveEngine, getLocalAdaptiveStats, LOCAL_ADAPTIVE_ENGINE_KEY, recordLocalAdaptiveChoice, recordLocalAdaptiveOutcome, } from "../../lib/local-adaptive-engine"; describe("local adaptive engine", () => { beforeEach(clearLocalAdaptiveEngine); it("returns an explainable fallback until evidence is confident", () => { recordLocalAdaptiveChoice("surface", "files", 1_000); recordLocalAdaptiveChoice("surface", "files", 1_001); expect( chooseLocalAdaptiveAction({ scope: "surface", candidates: ["terminal", "files"], fallback: "terminal", now: 1_002, ttlMs: 100, }), ).toEqual({ action: "terminal", confidence: expect.any(Number), reason: "insufficient-evidence", expiresAt: 1_102, }); recordLocalAdaptiveChoice("surface", "files", 1_003); expect( chooseLocalAdaptiveAction({ scope: "surface", candidates: ["terminal", "files"], fallback: "terminal", now: 1_004, }), ).toMatchObject({ action: "files", confidence: 1, reason: "learned" }); }); it("aggregates outcomes and latency with an EWMA", () => { recordLocalAdaptiveOutcome( "transfer:large", "direct", { success: true, latencyMs: 100 }, 1_000, ); recordLocalAdaptiveOutcome( "transfer:large", "direct", { success: false, fallback: true, latencyMs: 200 }, 1_001, ); expect(getLocalAdaptiveStats("transfer:large").direct).toMatchObject({ weight: 0, observations: 2, successes: 1, cancellations: 0, fallbacks: 1, latencyEwmaMs: 125, }); }); it("decays old choices and excludes unavailable candidates", () => { const month = 30 * 24 * 60 * 60 * 1000; for (let i = 0; i < 8; i++) { recordLocalAdaptiveChoice("surface", "docker", i); } for (let i = 0; i < 4; i++) { recordLocalAdaptiveChoice("surface", "files", month + i); } expect( chooseLocalAdaptiveAction({ scope: "surface", candidates: ["terminal", "files"], fallback: "terminal", now: month + 10, }).action, ).toBe("files"); }); it("bounds local storage and recovers from malformed data", () => { for (let i = 0; i < 140; i++) { recordLocalAdaptiveChoice(`scope:${i}`, "action", i); } const stored = JSON.parse( localStorage.getItem(LOCAL_ADAPTIVE_ENGINE_KEY) ?? "{}", ); expect(Object.keys(stored.scopes)).toHaveLength(128); localStorage.setItem(LOCAL_ADAPTIVE_ENGINE_KEY, "broken"); expect( chooseLocalAdaptiveAction({ scope: "surface", candidates: ["terminal", "files"], fallback: "terminal", }), ).toMatchObject({ action: "terminal", confidence: 0, reason: "fallback" }); }); it("keeps recent outcome-only actions when a scope reaches its limit", () => { for (let i = 0; i < 13; i++) { recordLocalAdaptiveOutcome( "resource-budget:module", `module:${i}`, { success: true }, i, ); } const stats = getLocalAdaptiveStats("resource-budget:module"); expect(Object.keys(stats)).toHaveLength(12); expect(stats["module:0"]).toBeUndefined(); expect(stats["module:12"]).toBeDefined(); }); });