Skip to content

Commit 0056c42

Browse files
committed
refactor: remove unsafe overload casts in parallel probe
parallel-probe used forced casts to distinguish overload inputs and to normalize probe failures. Replace those with explicit type guards and error normalization so the module keeps the same behavior without leaning on unsafe assertions.
1 parent a6da983 commit 0056c42

2 files changed

Lines changed: 67 additions & 3 deletions

File tree

lib/parallel-probe.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ export interface GetTopCandidatesParams {
3838
maxCandidates: number;
3939
}
4040

41+
function isAccountManager(value: unknown): value is AccountManager {
42+
return (
43+
typeof value === "object" &&
44+
value !== null &&
45+
"getAccountsSnapshot" in value &&
46+
typeof value.getAccountsSnapshot === "function"
47+
);
48+
}
49+
50+
function isGetTopCandidatesParams(value: unknown): value is GetTopCandidatesParams {
51+
return (
52+
typeof value === "object" &&
53+
value !== null &&
54+
"accountManager" in value &&
55+
isAccountManager(value.accountManager) &&
56+
"modelFamily" in value &&
57+
typeof value.modelFamily === "string" &&
58+
"model" in value &&
59+
(typeof value.model === "string" || value.model === null) &&
60+
"maxCandidates" in value &&
61+
typeof value.maxCandidates === "number"
62+
);
63+
}
64+
65+
function toProbeError(error: unknown): Error {
66+
return error instanceof Error ? error : new Error(String(error));
67+
}
68+
4169
/**
4270
* Get top N candidates ranked by hybrid score WITHOUT mutating AccountManager state.
4371
* Uses getAccountsSnapshot() and ranks by health + tokens + freshness.
@@ -64,13 +92,19 @@ export function getTopCandidates(
6492
let resolvedMaxCandidates: number | undefined;
6593

6694
if (useNamedParams) {
67-
const namedParams = accountManagerOrParams as GetTopCandidatesParams;
95+
if (!isGetTopCandidatesParams(accountManagerOrParams)) {
96+
throw new TypeError("getTopCandidates requires accountManager");
97+
}
98+
const namedParams = accountManagerOrParams;
6899
resolvedAccountManager = namedParams.accountManager;
69100
resolvedModelFamily = namedParams.modelFamily;
70101
resolvedModel = namedParams.model;
71102
resolvedMaxCandidates = namedParams.maxCandidates;
72103
} else {
73-
resolvedAccountManager = accountManagerOrParams as AccountManager;
104+
if (!isAccountManager(accountManagerOrParams)) {
105+
throw new TypeError("getTopCandidates requires accountManager");
106+
}
107+
resolvedAccountManager = accountManagerOrParams;
74108
resolvedModelFamily = modelFamily;
75109
resolvedModel = model;
76110
resolvedMaxCandidates = maxCandidates;
@@ -158,7 +192,7 @@ export async function probeAccountsInParallel<T>(
158192
const response = await probeFn(account, controller.signal);
159193
return { type: "success", account, response };
160194
} catch (error) {
161-
return { type: "failure", account, error: error as Error };
195+
return { type: "failure", account, error: toProbeError(error) };
162196
}
163197
}
164198

test/parallel-probe.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ describe("parallel-probe", () => {
7777
expect(result?.error?.message).toBe("network error");
7878
});
7979

80+
it("normalizes non-Error probe failures for single candidates", async () => {
81+
const account = createMockAccount(0);
82+
const candidates = createProbeCandidates([account]);
83+
84+
const result = await probeAccountsInParallel(candidates, async () => {
85+
throw "string failure";
86+
});
87+
88+
expect(result?.type).toBe("failure");
89+
expect(result?.error).toBeInstanceOf(Error);
90+
expect(result?.error?.message).toBe("string failure");
91+
});
92+
8093
it("returns first success in parallel probing", async () => {
8194
const accounts = [createMockAccount(0), createMockAccount(1), createMockAccount(2)];
8295
const candidates = createProbeCandidates(accounts);
@@ -174,6 +187,23 @@ describe("parallel-probe", () => {
174187
});
175188

176189
describe("getTopCandidates", () => {
190+
it("accepts named params without overload casts", () => {
191+
const accounts = [createMockAccount(0), createMockAccount(1)];
192+
const mockManager = {
193+
getAccountsSnapshot: vi.fn().mockReturnValue(accounts),
194+
};
195+
196+
const candidates = getTopCandidates({
197+
accountManager: mockManager as AccountManager,
198+
modelFamily: "codex",
199+
model: null,
200+
maxCandidates: 1,
201+
});
202+
203+
expect(candidates).toHaveLength(1);
204+
expect(mockManager.getAccountsSnapshot).toHaveBeenCalledTimes(1);
205+
});
206+
177207
it("returns empty array when no accounts available", () => {
178208
const mockManager = {
179209
getAccountsSnapshot: vi.fn().mockReturnValue([]),

0 commit comments

Comments
 (0)