summaryrefslogtreecommitdiffhomepage
path: root/src/features/concurrency/logic/view-model.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/concurrency/logic/view-model.test.ts')
-rw-r--r--src/features/concurrency/logic/view-model.test.ts670
1 files changed, 670 insertions, 0 deletions
diff --git a/src/features/concurrency/logic/view-model.test.ts b/src/features/concurrency/logic/view-model.test.ts
new file mode 100644
index 0000000..6e6b770
--- /dev/null
+++ b/src/features/concurrency/logic/view-model.test.ts
@@ -0,0 +1,670 @@
+import type { ConcurrencyStatusEntry } from "@dispatch/transport-contract";
+import { describe, expect, it } from "vitest";
+import {
+ autoReduceNotices,
+ cooldownLabel,
+ DEFAULT_COOLDOWN_MS,
+ formatPauseDuration,
+ normalizeConcurrencyCooldown,
+ normalizeConcurrencyLimit,
+ normalizeConcurrencyLimits,
+ normalizeConcurrencyStatus,
+ parseCooldownInput,
+ parseLimitInput,
+ pauseLabel,
+ providerFromModel,
+ providerOptions,
+ statusLabel,
+ summarizeLimits,
+ summarizeStatus,
+ viewAutoReduce,
+ viewConcurrencyLimit,
+ viewConcurrencyLimits,
+ viewConcurrencyStatus,
+ viewConcurrencyStatuses,
+} from "./view-model";
+
+const status = (over: Partial<ConcurrencyStatusEntry> = {}): ConcurrencyStatusEntry => ({
+ providerId: "umans",
+ limit: 4,
+ inFlight: 2,
+ queued: 0,
+ paused: false,
+ cooldownMs: 350,
+ autoReduced: false,
+ ...over,
+});
+
+// ── parseLimitInput ───────────────────────────────────────────────────────────
+
+describe("parseLimitInput", () => {
+ it("accepts positive integers", () => {
+ expect(parseLimitInput("4")).toBe(4);
+ expect(parseLimitInput(" 12 ")).toBe(12);
+ expect(parseLimitInput("1")).toBe(1);
+ });
+
+ it("rejects zero, negatives, non-integers, and garbage", () => {
+ expect(parseLimitInput("0")).toBeNull();
+ expect(parseLimitInput("-1")).toBeNull();
+ expect(parseLimitInput("4.5")).toBeNull();
+ expect(parseLimitInput("")).toBeNull();
+ expect(parseLimitInput(" ")).toBeNull();
+ expect(parseLimitInput("abc")).toBeNull();
+ expect(parseLimitInput("4abc")).toBeNull();
+ });
+});
+
+// ── parseCooldownInput (non-negative integer — 0 is valid, unlike the limit) ──
+
+describe("parseCooldownInput", () => {
+ it("accepts zero + positive integers", () => {
+ expect(parseCooldownInput("0")).toBe(0);
+ expect(parseCooldownInput("350")).toBe(350);
+ expect(parseCooldownInput(" 100 ")).toBe(100);
+ });
+
+ it("rejects negatives, non-integers, and garbage", () => {
+ expect(parseCooldownInput("-1")).toBeNull();
+ expect(parseCooldownInput("4.5")).toBeNull();
+ expect(parseCooldownInput("")).toBeNull();
+ expect(parseCooldownInput("abc")).toBeNull();
+ expect(parseCooldownInput("100ms")).toBeNull();
+ });
+});
+
+// ── cooldownLabel ─────────────────────────────────────────────────────────────
+
+describe("cooldownLabel", () => {
+ it("0 → off label", () => {
+ expect(cooldownLabel(0)).toBe("0ms (off)");
+ });
+ it("sub-second → ms", () => {
+ expect(cooldownLabel(350)).toBe("350ms");
+ expect(cooldownLabel(999)).toBe("999ms");
+ });
+ it("≥1s → seconds (trims trailing .0)", () => {
+ expect(cooldownLabel(1000)).toBe("1s");
+ expect(cooldownLabel(1500)).toBe("1.5s");
+ expect(cooldownLabel(60_000)).toBe("60s");
+ });
+});
+
+// ── providerFromModel / providerOptions ───────────────────────────────────────
+
+describe("providerFromModel", () => {
+ it("takes the part before the first slash", () => {
+ expect(providerFromModel("openai/gpt-4o")).toBe("openai");
+ expect(providerFromModel("openai-compat/gpt-4o-mini")).toBe("openai-compat");
+ });
+ it("returns the whole string when there is no slash", () => {
+ expect(providerFromModel("umans")).toBe("umans");
+ });
+});
+
+describe("providerOptions", () => {
+ it("derives distinct provider ids from models, first-seen order", () => {
+ expect(
+ providerOptions(["openai/gpt-4o", "umans/umans-glm-5.2", "openai/gpt-4o-mini"], []),
+ ).toEqual(["openai", "umans"]);
+ });
+ it("unions with providers already carrying a configured limit", () => {
+ expect(providerOptions(["openai/gpt-4o"], [{ providerId: "anthropic", limit: 4 }])).toEqual([
+ "openai",
+ "anthropic",
+ ]);
+ });
+ it("does not duplicate a provider present in both models and limits", () => {
+ expect(providerOptions(["openai/gpt-4o"], [{ providerId: "openai", limit: 4 }])).toEqual([
+ "openai",
+ ]);
+ });
+ it("ignores models whose provider prefix is empty", () => {
+ expect(providerOptions(["/model-only", "umans/x"], [])).toEqual(["umans"]);
+ });
+ it("returns [] when there are no models and no limits", () => {
+ expect(providerOptions([], [])).toEqual([]);
+ });
+});
+
+// ── pauseLabel + formatPauseDuration ───────────────────────────────────────────
+
+describe("formatPauseDuration", () => {
+ it("formats seconds / minutes+seconds / hours+minutes", () => {
+ expect(formatPauseDuration(30_000)).toBe("30s");
+ expect(formatPauseDuration(65_000)).toBe("1m 05s");
+ expect(formatPauseDuration(3_660_000)).toBe("1h 01m");
+ });
+
+ it("non-positive → resuming", () => {
+ expect(formatPauseDuration(0)).toBe("resuming");
+ expect(formatPauseDuration(-5_000)).toBe("resuming");
+ });
+});
+
+describe("pauseLabel", () => {
+ it("null when not paused", () => {
+ expect(pauseLabel(false, undefined, 0)).toBeNull();
+ expect(pauseLabel(false, 10_000, 0)).toBeNull();
+ });
+
+ it("'paused' (bare) when paused without a usable timestamp", () => {
+ expect(pauseLabel(true, undefined, 0)).toBe("paused");
+ expect(pauseLabel(true, null, 0)).toBe("paused");
+ expect(pauseLabel(true, Number.NaN, 0)).toBe("paused");
+ });
+
+ it("countdown when paused with a future timestamp", () => {
+ const now = 1_000_000;
+ expect(pauseLabel(true, now + 30_000, now)).toBe("paused — resumes in 30s");
+ expect(pauseLabel(true, now + 65_000, now)).toBe("paused — resumes in 1m 05s");
+ });
+
+ it("'paused' (bare) when the timestamp is missing, non-finite, or expired", () => {
+ expect(pauseLabel(true, 0, 1_000)).toBe("paused");
+ expect(pauseLabel(true, 1_000, 2_000)).toBe("paused");
+ expect(pauseLabel(true, Number.NaN, 0)).toBe("paused");
+ // The countdown prefix only appears with a FUTURE timestamp:
+ expect(pauseLabel(true, 2_000, 1_000)).toBe("paused — resumes in 1s");
+ });
+});
+
+// ── viewConcurrencyStatus ──────────────────────────────────────────────────────
+
+describe("viewConcurrencyStatus", () => {
+ it("serving under capacity → success badge, in-flight label, no queue", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 2, limit: 4, queued: 0 }), 0);
+ expect(v.inFlightLabel).toBe("2/4");
+ expect(v.queuedLabel).toBe("no queue");
+ expect(v.pausedLabel).toBeNull();
+ expect(v.badge).toBe("success");
+ expect(v.busy).toBe(false);
+ });
+
+ it("at capacity with a queue → warning badge + busy (spinner)", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 3 }), 0);
+ expect(v.inFlightLabel).toBe("4/4");
+ expect(v.queuedLabel).toBe("3 queued");
+ expect(v.badge).toBe("warning");
+ expect(v.busy).toBe(true);
+ });
+
+ it("idle (no in-flight) → neutral badge, not busy", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 0, limit: 4, queued: 0 }), 0);
+ expect(v.badge).toBe("neutral");
+ expect(v.busy).toBe(false);
+ expect(v.inFlightLabel).toBe("0/4");
+ });
+
+ it("paused → warning badge + pause countdown label", () => {
+ const now = 1_000_000;
+ const v = viewConcurrencyStatus(
+ status({ paused: true, pausedUntil: now + 30_000, inFlight: 4, limit: 4, queued: 3 }),
+ now,
+ );
+ expect(v.paused).toBe(true);
+ expect(v.pausedLabel).toBe("paused — resumes in 30s");
+ expect(v.badge).toBe("warning");
+ expect(v.busy).toBe(true);
+ });
+
+ it("at capacity but no queue → success (busy only when queuing)", () => {
+ const v = viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 0 }), 0);
+ expect(v.badge).toBe("success");
+ expect(v.busy).toBe(false);
+ });
+
+ it("normalizes garbage counts to 0 and a malformed limit to 1", () => {
+ const v = viewConcurrencyStatus(
+ {
+ providerId: "x",
+ limit: -3,
+ inFlight: Number.NaN,
+ queued: "oops" as unknown as number,
+ paused: false,
+ cooldownMs: Number.NaN,
+ autoReduced: false,
+ },
+ 0,
+ );
+ expect(v.limit).toBe(1);
+ expect(v.inFlight).toBe(0);
+ expect(v.queued).toBe(0);
+ expect(v.inFlightLabel).toBe("0/1");
+ expect(v.cooldownMs).toBe(DEFAULT_COOLDOWN_MS);
+ });
+
+ it("viewConcurrencyStatuses maps a list preserving order", () => {
+ const views = viewConcurrencyStatuses(
+ [status({ providerId: "a" }), status({ providerId: "b" })],
+ 0,
+ );
+ expect(views.map((v) => v.providerId)).toEqual(["a", "b"]);
+ });
+
+ it("carries cooldownMs + label + autoReduced fields onto the view", () => {
+ const v = viewConcurrencyStatus(status({ cooldownMs: 1500 }), 0);
+ expect(v.cooldownMs).toBe(1500);
+ expect(v.cooldownLabel).toBe("1.5s");
+ expect(v.autoReduced).toBe(false);
+ expect(v.autoReducedFrom).toBeNull();
+ });
+
+ it("auto-reduced → warning badge (not busy) + autoReducedFrom carried", () => {
+ const v = viewConcurrencyStatus(
+ status({ limit: 3, autoReduced: true, autoReducedFrom: 4, inFlight: 0 }),
+ 0,
+ );
+ expect(v.autoReduced).toBe(true);
+ expect(v.autoReducedFrom).toBe(4);
+ expect(v.badge).toBe("warning");
+ // autoReduced alone does NOT flip busy (a reduced limit still admits agents).
+ expect(v.busy).toBe(false);
+ });
+});
+
+// ── statusLabel (the row's status badge word) ──────────────────────────────────
+
+describe("statusLabel", () => {
+ it("idle (no in-flight) → Idle", () => {
+ expect(
+ statusLabel(viewConcurrencyStatus(status({ inFlight: 0, limit: 4, queued: 0 }), 0)),
+ ).toBe("Idle");
+ });
+
+ it("serving under capacity → Active", () => {
+ expect(
+ statusLabel(viewConcurrencyStatus(status({ inFlight: 2, limit: 4, queued: 0 }), 0)),
+ ).toBe("Active");
+ });
+
+ it("at capacity with a queue → At capacity", () => {
+ expect(
+ statusLabel(viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 3 }), 0)),
+ ).toBe("At capacity");
+ });
+
+ it("at capacity but no queue → Active (not At capacity)", () => {
+ expect(
+ statusLabel(viewConcurrencyStatus(status({ inFlight: 4, limit: 4, queued: 0 }), 0)),
+ ).toBe("Active");
+ });
+
+ it("paused → Paused (regardless of in-flight/queue)", () => {
+ expect(
+ statusLabel(
+ viewConcurrencyStatus(status({ paused: true, inFlight: 4, limit: 4, queued: 3 }), 0),
+ ),
+ ).toBe("Paused");
+ });
+});
+
+// ── viewAutoReduce / autoReduceNotices (the auto-reduce banner view) ───────────
+
+describe("viewAutoReduce", () => {
+ it("returns null when not auto-reduced", () => {
+ expect(viewAutoReduce(status({ autoReduced: false }))).toBeNull();
+ });
+
+ it("uses the backend notice verbatim + carries from/current limits", () => {
+ const notice = viewAutoReduce(
+ status({
+ limit: 3,
+ autoReduced: true,
+ autoReducedFrom: 4,
+ notice: "Concurrency limit auto-reduced to 3 after a 429.",
+ }),
+ );
+ expect(notice).toEqual({
+ providerId: "umans",
+ message: "Concurrency limit auto-reduced to 3 after a 429.",
+ fromLimit: 4,
+ currentLimit: 3,
+ });
+ });
+
+ it("synthesizes a fallback notice when the backend notice is absent/empty", () => {
+ expect(viewAutoReduce(status({ limit: 3, autoReduced: true, autoReducedFrom: 4 }))).toEqual({
+ providerId: "umans",
+ message: "Concurrency limit auto-reduced to 3 after a 429 — restore manually when ready.",
+ fromLimit: 4,
+ currentLimit: 3,
+ });
+ expect(
+ viewAutoReduce(status({ limit: 3, autoReduced: true, autoReducedFrom: 4, notice: "" })),
+ ).not.toBeNull();
+ });
+
+ it("falls back to currentLimit+1 when autoReducedFrom is missing/garbage", () => {
+ const notice = viewAutoReduce(status({ limit: 3, autoReduced: true }));
+ expect(notice?.fromLimit).toBe(4); // 3 + 1
+ });
+});
+
+describe("autoReduceNotices", () => {
+ it("collects one banner per auto-reduced provider (input order), empty when none", () => {
+ expect(autoReduceNotices([status({ providerId: "a" })])).toEqual([]);
+ const out = autoReduceNotices([
+ status({ providerId: "a", autoReduced: true, autoReducedFrom: 4, limit: 3 }),
+ status({ providerId: "b" }),
+ status({ providerId: "c", autoReduced: true, autoReducedFrom: 2, limit: 1 }),
+ ]);
+ expect(out.map((n) => n.providerId)).toEqual(["a", "c"]);
+ expect(out[1]?.fromLimit).toBe(2);
+ });
+});
+
+// ── viewConcurrencyLimit ───────────────────────────────────────────────────────
+
+describe("viewConcurrencyLimit", () => {
+ it("passes through id + normalizes the limit", () => {
+ const v = viewConcurrencyLimit({ providerId: "umans", limit: 4 });
+ expect(v.providerId).toBe("umans");
+ expect(v.limit).toBe(4);
+ });
+
+ it("clamps a malformed limit to 1", () => {
+ expect(viewConcurrencyLimit({ providerId: "x", limit: 0 }).limit).toBe(1);
+ expect(viewConcurrencyLimit({ providerId: "x", limit: -2 }).limit).toBe(1);
+ expect(viewConcurrencyLimit({ providerId: "x", limit: 2.9 }).limit).toBe(2);
+ });
+
+ it("viewConcurrencyLimits maps a list preserving order", () => {
+ const views = viewConcurrencyLimits([
+ { providerId: "a", limit: 1 },
+ { providerId: "b", limit: 2 },
+ ]);
+ expect(views.map((v) => v.providerId)).toEqual(["a", "b"]);
+ });
+});
+
+// ── summarizeLimits / summarizeStatus ──────────────────────────────────────────
+
+describe("summarizeLimits", () => {
+ it("empty → No limits configured", () => {
+ expect(summarizeLimits([])).toBe("No limits configured");
+ });
+ it("counts limits (singular/plural)", () => {
+ expect(summarizeLimits([{ providerId: "a", limit: 1 }])).toBe("1 limit configured");
+ expect(
+ summarizeLimits([
+ { providerId: "a", limit: 1 },
+ { providerId: "b", limit: 2 },
+ ]),
+ ).toBe("2 limits configured");
+ });
+});
+
+describe("summarizeStatus", () => {
+ it("empty → No limits configured", () => {
+ expect(summarizeStatus([], 0)).toBe("No limits configured");
+ });
+ it("aggregates providers + in-flight totals", () => {
+ const s = summarizeStatus(
+ [
+ status({ providerId: "a", limit: 4, inFlight: 2 }),
+ status({ providerId: "b", limit: 6, inFlight: 3 }),
+ ],
+ 0,
+ );
+ expect(s).toBe("2 providers · 5/10 in flight");
+ });
+ it("includes queued + paused fragments only when non-zero", () => {
+ const s = summarizeStatus(
+ [
+ status({ providerId: "a", limit: 4, inFlight: 4, queued: 2 }),
+ status({
+ providerId: "b",
+ limit: 4,
+ inFlight: 1,
+ queued: 0,
+ paused: true,
+ pausedUntil: 1000,
+ }),
+ ],
+ 0,
+ );
+ expect(s).toBe("2 providers · 5/8 in flight · 2 queued · 1 paused");
+ });
+ it("singular provider", () => {
+ expect(summarizeStatus([status({ providerId: "a", limit: 4, inFlight: 1 })], 0)).toBe(
+ "1 provider · 1/4 in flight",
+ );
+ });
+ it("includes an auto-reduced fragment only when non-zero", () => {
+ const s = summarizeStatus(
+ [
+ status({ providerId: "a", limit: 3, inFlight: 1, autoReduced: true, autoReducedFrom: 4 }),
+ status({ providerId: "b", limit: 4, inFlight: 1 }),
+ ],
+ 0,
+ );
+ expect(s).toBe("2 providers · 2/7 in flight · 1 auto-reduced");
+ });
+});
+
+// ── Network-seam normalizers ───────────────────────────────────────────────────
+
+describe("normalizeConcurrencyLimits", () => {
+ it("coerces a well-formed body", () => {
+ const limits = normalizeConcurrencyLimits({
+ limits: [
+ { providerId: "umans", limit: 4 },
+ { providerId: "openai-compat", limit: 5 },
+ ],
+ });
+ expect(limits).toEqual([
+ { providerId: "umans", limit: 4 },
+ { providerId: "openai-compat", limit: 5 },
+ ]);
+ });
+
+ it("non-array / missing limits → []", () => {
+ expect(normalizeConcurrencyLimits({})).toEqual([]);
+ expect(normalizeConcurrencyLimits({ limits: "nope" })).toEqual([]);
+ expect(normalizeConcurrencyLimits(null)).toEqual([]);
+ expect(normalizeConcurrencyLimits(undefined)).toEqual([]);
+ });
+
+ it("drops entries without a provider id + clamps limits", () => {
+ const limits = normalizeConcurrencyLimits({
+ limits: [
+ { providerId: "umans", limit: 4 },
+ { providerId: "", limit: 9 },
+ { providerId: 123, limit: 1 },
+ { limit: 2 },
+ { providerId: "anthropic", limit: -5 },
+ ],
+ });
+ expect(limits).toEqual([
+ { providerId: "umans", limit: 4 },
+ { providerId: "anthropic", limit: 1 },
+ ]);
+ });
+});
+
+describe("normalizeConcurrencyLimit", () => {
+ it("coerces a well-formed single response", () => {
+ expect(normalizeConcurrencyLimit({ providerId: "umans", limit: 4 })).toEqual({
+ providerId: "umans",
+ limit: 4,
+ });
+ });
+ it("null when the provider id is missing/non-string", () => {
+ expect(normalizeConcurrencyLimit({ limit: 4 })).toBeNull();
+ expect(normalizeConcurrencyLimit({ providerId: "", limit: 4 })).toBeNull();
+ expect(normalizeConcurrencyLimit(null)).toBeNull();
+ });
+ it("clamps a malformed limit to 1", () => {
+ expect(normalizeConcurrencyLimit({ providerId: "x", limit: 0 })).toEqual({
+ providerId: "x",
+ limit: 1,
+ });
+ });
+});
+
+describe("normalizeConcurrencyStatus", () => {
+ it("coerces a well-formed body, preserving pausedUntil only when present", () => {
+ const now = Date.now();
+ const providers = normalizeConcurrencyStatus({
+ providers: [
+ { providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false },
+ {
+ providerId: "openai-compat",
+ limit: 5,
+ inFlight: 5,
+ queued: 3,
+ paused: true,
+ pausedUntil: now,
+ },
+ ],
+ });
+ expect(providers).toHaveLength(2);
+ const [first, second] = providers;
+ expect(first).toEqual({
+ providerId: "umans",
+ limit: 4,
+ inFlight: 2,
+ queued: 1,
+ paused: false,
+ cooldownMs: 350,
+ autoReduced: false,
+ });
+ expect(first !== undefined && !("pausedUntil" in first)).toBe(true);
+ expect(second).toEqual({
+ providerId: "openai-compat",
+ limit: 5,
+ inFlight: 5,
+ queued: 3,
+ paused: true,
+ pausedUntil: now,
+ cooldownMs: 350,
+ autoReduced: false,
+ });
+ });
+
+ it("non-array / missing providers → []", () => {
+ expect(normalizeConcurrencyStatus({})).toEqual([]);
+ expect(normalizeConcurrencyStatus({ providers: 42 })).toEqual([]);
+ expect(normalizeConcurrencyStatus(null)).toEqual([]);
+ });
+
+ it("drops entries without a provider id + clamps counts", () => {
+ const providers = normalizeConcurrencyStatus({
+ providers: [
+ { providerId: "umans", limit: 4, inFlight: 2, queued: 1, paused: false },
+ { providerId: "", inFlight: 1 },
+ { limit: 2 },
+ { providerId: 9, inFlight: 0 },
+ { providerId: "x", limit: -1, inFlight: "bad", queued: null, paused: "yes" },
+ ],
+ });
+ expect(providers).toEqual([
+ {
+ providerId: "umans",
+ limit: 4,
+ inFlight: 2,
+ queued: 1,
+ paused: false,
+ cooldownMs: 350,
+ autoReduced: false,
+ },
+ {
+ providerId: "x",
+ limit: 1,
+ inFlight: 0,
+ queued: 0,
+ paused: false,
+ cooldownMs: 350,
+ autoReduced: false,
+ },
+ ]);
+ });
+
+ it("omits pausedUntil when it is not a finite number", () => {
+ const providers = normalizeConcurrencyStatus({
+ providers: [
+ { providerId: "a", limit: 1, inFlight: 0, queued: 0, paused: true, pausedUntil: "x" },
+ { providerId: "b", limit: 1, inFlight: 0, queued: 0, paused: true, pausedUntil: null },
+ ],
+ });
+ for (const p of providers) expect("pausedUntil" in p).toBe(false);
+ });
+
+ it("coerces cooldownMs (default 350) + carries auto-reduce fields only when true", () => {
+ const [reduced, healthy] = normalizeConcurrencyStatus({
+ providers: [
+ {
+ providerId: "umans",
+ limit: 3,
+ inFlight: 1,
+ queued: 0,
+ paused: false,
+ cooldownMs: 500,
+ autoReduced: true,
+ autoReducedFrom: 4,
+ notice: "auto-reduced to 3 after a 429.",
+ },
+ { providerId: "openai", limit: 4, inFlight: 0, queued: 0, paused: false },
+ ],
+ });
+ expect(reduced?.cooldownMs).toBe(500);
+ expect(reduced?.autoReduced).toBe(true);
+ expect(reduced?.autoReducedFrom).toBe(4);
+ expect(reduced?.notice).toBe("auto-reduced to 3 after a 429.");
+ // Healthy entry: cooldownMs defaults to 350 when absent; auto-reduce fields
+ // are NOT present (they are only included when autoReduced===true).
+ expect(healthy?.cooldownMs).toBe(DEFAULT_COOLDOWN_MS);
+ expect(healthy?.autoReduced).toBe(false);
+ expect(healthy && "autoReducedFrom" in healthy).toBe(false);
+ expect(healthy && "notice" in healthy).toBe(false);
+ });
+
+ it("drops autoReducedFrom/notice when autoReduced is false (even if present in JSON)", () => {
+ const [p] = normalizeConcurrencyStatus({
+ providers: [
+ {
+ providerId: "x",
+ limit: 4,
+ inFlight: 0,
+ queued: 0,
+ paused: false,
+ autoReduced: false,
+ autoReducedFrom: 9,
+ notice: "stale",
+ },
+ ],
+ });
+ expect(p?.autoReduced).toBe(false);
+ expect(p && "autoReducedFrom" in p).toBe(false);
+ expect(p && "notice" in p).toBe(false);
+ });
+});
+
+// ── normalizeConcurrencyCooldown ───────────────────────────────────────────────
+
+describe("normalizeConcurrencyCooldown", () => {
+ it("coerces a well-formed body", () => {
+ expect(normalizeConcurrencyCooldown({ providerId: "umans", cooldownMs: 500 })).toEqual({
+ providerId: "umans",
+ cooldownMs: 500,
+ });
+ });
+
+ it("defaults a malformed/absent cooldownMs to 350", () => {
+ expect(normalizeConcurrencyCooldown({ providerId: "x", cooldownMs: -1 })?.cooldownMs).toBe(
+ DEFAULT_COOLDOWN_MS,
+ );
+ expect(normalizeConcurrencyCooldown({ providerId: "x" })?.cooldownMs).toBe(DEFAULT_COOLDOWN_MS);
+ expect(normalizeConcurrencyCooldown({ providerId: "x", cooldownMs: "fast" })?.cooldownMs).toBe(
+ DEFAULT_COOLDOWN_MS,
+ );
+ });
+
+ it("returns null for a missing/malformed providerId", () => {
+ expect(normalizeConcurrencyCooldown({ cooldownMs: 350 })).toBeNull();
+ expect(normalizeConcurrencyCooldown({ providerId: "", cooldownMs: 350 })).toBeNull();
+ expect(normalizeConcurrencyCooldown(null)).toBeNull();
+ expect(normalizeConcurrencyCooldown({})).toBeNull();
+ });
+});