125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { selectAccountP2C, selectAccount } =
|
|
await import("../../open-sse/services/accountSelector.ts");
|
|
|
|
// ─── selectAccountP2C ───────────────────────────────────────────────────────
|
|
|
|
test("selectAccountP2C: returns null for empty array", () => {
|
|
assert.equal(selectAccountP2C([]), null);
|
|
assert.equal(selectAccountP2C(null), null);
|
|
});
|
|
|
|
test("selectAccountP2C: returns single account", () => {
|
|
const acct = { id: "a1" };
|
|
assert.equal(selectAccountP2C([acct]), acct);
|
|
});
|
|
|
|
test("selectAccountP2C: returns one of the candidates", () => {
|
|
const accounts = [{ id: "a1" }, { id: "a2" }, { id: "a3" }];
|
|
const selected = selectAccountP2C(accounts);
|
|
assert.ok(accounts.includes(selected));
|
|
});
|
|
|
|
test("selectAccountP2C: prefers healthier account over many runs", () => {
|
|
// Account with error should be selected less often
|
|
const healthy = { id: "healthy" };
|
|
const degraded = { id: "degraded", error: true, rateLimited: true, backoffLevel: 3 };
|
|
const accounts = [healthy, degraded];
|
|
|
|
let healthyCount = 0;
|
|
for (let i = 0; i < 100; i++) {
|
|
const selected = selectAccountP2C(accounts);
|
|
if (selected.id === "healthy") healthyCount++;
|
|
}
|
|
// Should strongly prefer healthy account
|
|
assert.ok(healthyCount > 60, `Expected healthy to win >60/100 times, got ${healthyCount}`);
|
|
});
|
|
|
|
// ─── selectAccount strategies ───────────────────────────────────────────────
|
|
|
|
test("selectAccount: fill-first returns first", () => {
|
|
const accounts = [{ id: "first" }, { id: "second" }];
|
|
const { account } = selectAccount(accounts, "fill-first");
|
|
assert.equal(account.id, "first");
|
|
});
|
|
|
|
test("selectAccount: round-robin cycles", () => {
|
|
const accounts = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
let state = {};
|
|
const results = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
const { account, state: newState } = selectAccount(accounts, "round-robin", state);
|
|
results.push(account.id);
|
|
state = newState;
|
|
}
|
|
assert.deepEqual(results, ["a", "b", "c", "a", "b", "c"]);
|
|
});
|
|
|
|
test("selectAccount: random returns valid account", () => {
|
|
const accounts = [{ id: "x" }, { id: "y" }];
|
|
const { account } = selectAccount(accounts, "random");
|
|
assert.ok(accounts.includes(account));
|
|
});
|
|
|
|
test("selectAccount: p2c returns valid account", () => {
|
|
const accounts = [{ id: "p1" }, { id: "p2" }, { id: "p3" }];
|
|
const { account } = selectAccount(accounts, "p2c");
|
|
assert.ok(accounts.includes(account));
|
|
});
|
|
|
|
test("selectAccount: empty accounts returns null", () => {
|
|
const { account } = selectAccount([], "fill-first");
|
|
assert.equal(account, null);
|
|
});
|
|
|
|
// ─── Round-robin fallback scenario (Issue #340) ─────────────────────────────
|
|
|
|
test("selectAccount: round-robin with excludeConnectionId skips excluded", () => {
|
|
const accounts = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
|
|
// Simulate: first request picks 'a', then it fails
|
|
// Second request should exclude 'a' and pick 'b'
|
|
let state = {};
|
|
|
|
// First request - no exclusion
|
|
const { account: acc1 } = selectAccount(accounts, "round-robin", state);
|
|
state = { lastIndex: 0 }; // Simulate 'a' was picked
|
|
|
|
// 'a' fails, exclude it
|
|
const { account: acc2 } = selectAccount(
|
|
accounts.filter((a) => a.id !== "a"),
|
|
"round-robin",
|
|
state
|
|
);
|
|
|
|
// Should pick 'b' or 'c', not 'a'
|
|
assert.notEqual(acc2.id, "a", "Should not pick excluded account");
|
|
assert.ok(["b", "c"].includes(acc2.id), "Should pick from remaining accounts");
|
|
});
|
|
|
|
test("selectAccount: round-robin respects state across calls", () => {
|
|
const accounts = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
let state = { lastIndex: -1 };
|
|
|
|
// First call should pick index 0
|
|
const { account: acc1, state: state1 } = selectAccount(accounts, "round-robin", state);
|
|
assert.equal(acc1.id, "a");
|
|
assert.equal(state1.lastIndex, 0);
|
|
|
|
// Second call should pick index 1
|
|
const { account: acc2, state: state2 } = selectAccount(accounts, "round-robin", state1);
|
|
assert.equal(acc2.id, "b");
|
|
assert.equal(state2.lastIndex, 1);
|
|
|
|
// Third call should pick index 2
|
|
const { account: acc3, state: state3 } = selectAccount(accounts, "round-robin", state2);
|
|
assert.equal(acc3.id, "c");
|
|
assert.equal(state3.lastIndex, 2);
|
|
|
|
// Fourth call should wrap to index 0
|
|
const { account: acc4 } = selectAccount(accounts, "round-robin", state3);
|
|
assert.equal(acc4.id, "a");
|
|
});
|