Files
OmniRoute/tests/unit/connection-reorder-by-availability.test.ts
Diego Rodrigues de Sa e Souza eb529cfa12 feat(dashboard): add reorder connections by availability button (#7211)
* feat(dashboard): add reorder-by-availability button to provider connections

Adds a "Reorder" action to the provider detail Connections toolbar that
sorts a provider's connections so available ones float to the top and
unavailable ones sink to the bottom, then persists the new order via the
existing per-connection priority PUT endpoint (same pattern already used
by handleSwapPriority).

Availability is computed with OmniRoute's own resilience model rather than
upstream's `modelLock_*` convention: a connection counts as available when
its effective status (testStatus, adjusted for the lazy connection-cooldown
window via rateLimitedUntil) is active/success — mirroring the exact logic
ConnectionRow already uses for its status badge, so the button and the row
badges never disagree. The sort is a stable Array.prototype.sort, so
connections keep their relative order within each availability group.

New pure helpers (sortConnectionsByAvailability, isConnectionAvailable,
getConnectionEffectiveStatus) live in connectionRowHelpers.ts and are
covered by a dedicated unit test, including the cooldown-lazy-recovery
edge case. i18n keys added to all 43 locales.

Co-authored-by: Fazril Syaveral Hillaby <fazriloke18@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2558

* chore(changelog): fragment for #7211

* fix(dashboard): extract reorder-by-availability into its own hook (file-size ratchet)

The reorder-by-availability feature pushed useProviderConnections.ts to 974
lines, past its frozen file-size cap (954). Extract the handler + its state
into a dedicated useReorderByAvailability hook, following the same pattern
already used for useModelVisibilityHandlers/useModelImportHandlers — no
behavior change, same tests still cover the sort logic in
connectionRowHelpers.ts.

* fix(dashboard): type the reorder hook's notifier explicitly (dashboard-typecheck TS2339)

ReturnType<typeof useNotificationStore> resolves to unknown under the
dashboard-scoped tsconfig gate (#7203), so notify.error tripped TS2339.
The hook only needs error(), so declare that minimal surface directly.

---------

Co-authored-by: Fazril Syaveral Hillaby <fazriloke18@gmail.com>
2026-07-17 10:39:50 -03:00

88 lines
3.1 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
sortConnectionsByAvailability,
isConnectionAvailable,
getConnectionEffectiveStatus,
} from "../../src/app/(dashboard)/dashboard/providers/[id]/components/connectionRowHelpers";
// Reorder-by-availability — upstream 9router PR #2558 ported to OmniRoute's
// resilience model (rateLimitedUntil cooldown + testStatus), not the
// upstream `modelLock_*` field convention. See CLAUDE.md "Resilience Runtime
// State" → Connection Cooldown.
test("sortConnectionsByAvailability moves available connections to the top", () => {
const connections = [
{ id: "a", testStatus: "error" },
{ id: "b", testStatus: "active" },
{ id: "c", testStatus: "success" },
{ id: "d", testStatus: "expired" },
];
const sorted = sortConnectionsByAvailability(connections);
assert.deepEqual(
sorted.map((c) => c.id),
["b", "c", "a", "d"]
);
});
test("sortConnectionsByAvailability is a stable sort (preserves relative order within each group)", () => {
const connections = [
{ id: "1", testStatus: "error" },
{ id: "2", testStatus: "active" },
{ id: "3", testStatus: "error" },
{ id: "4", testStatus: "success" },
{ id: "5", testStatus: "unknown" },
];
const sorted = sortConnectionsByAvailability(connections);
// Available group (2, 4) keeps its original relative order, then the
// unavailable group (1, 3, 5) keeps its original relative order.
assert.deepEqual(
sorted.map((c) => c.id),
["2", "4", "1", "3", "5"]
);
});
test("sortConnectionsByAvailability does not mutate the input array", () => {
const connections = [{ id: "a", testStatus: "error" }, { id: "b", testStatus: "active" }];
const original = [...connections];
sortConnectionsByAvailability(connections);
assert.deepEqual(connections, original);
});
test("a testStatus: 'unavailable' connection past its cooldown counts as available (lazy recovery)", () => {
const pastCooldown = new Date(Date.now() - 60_000).toISOString();
const connection = { testStatus: "unavailable", rateLimitedUntil: pastCooldown };
assert.equal(getConnectionEffectiveStatus(connection), "active");
assert.equal(isConnectionAvailable(connection), true);
});
test("a testStatus: 'unavailable' connection still within cooldown stays unavailable", () => {
const futureCooldown = new Date(Date.now() + 60_000).toISOString();
const connection = { testStatus: "unavailable", rateLimitedUntil: futureCooldown };
assert.equal(getConnectionEffectiveStatus(connection), "unavailable");
assert.equal(isConnectionAvailable(connection), false);
});
test("sortConnectionsByAvailability treats an active cooldown as unavailable even ahead of a hard error", () => {
const futureCooldown = new Date(Date.now() + 60_000).toISOString();
const connections = [
{ id: "cooling", testStatus: "unavailable", rateLimitedUntil: futureCooldown },
{ id: "recovered", testStatus: "active" },
];
const sorted = sortConnectionsByAvailability(connections);
assert.deepEqual(
sorted.map((c) => c.id),
["recovered", "cooling"]
);
});