From d072db21e6527eac01edab4cbef78eee101ea2d8 Mon Sep 17 00:00:00 2001 From: Ankit <177378174+anki1kr@users.noreply.github.com> Date: Sat, 4 Jul 2026 04:14:12 +0530 Subject: [PATCH] fix(combos): expand OpenCode/MiMo fingerprint accounts in combo builder (#6087) (#6092) Expand OpenCode/MiMo fingerprint accounts in combo builder (#6087). Integrated into release/v3.8.44. --- src/lib/combos/builderOptions.ts | 52 ++++++++++- ...ombo-builder-fingerprint-expansion.test.ts | 87 +++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 tests/unit/combo-builder-fingerprint-expansion.test.ts diff --git a/src/lib/combos/builderOptions.ts b/src/lib/combos/builderOptions.ts index b1dee3c1e9..4ce26daa93 100644 --- a/src/lib/combos/builderOptions.ts +++ b/src/lib/combos/builderOptions.ts @@ -65,6 +65,7 @@ type ProviderConnectionLike = { lastTested?: string | null; updatedAt?: string | null; testStatus?: string | null; + providerSpecificData?: Record | null; }; type ProviderNodeLike = { @@ -223,6 +224,50 @@ function deriveConnectionStatus(connection: ProviderConnectionLike): BuilderConn return "active"; } +/** + * Expand a list of raw DB connection rows into `ComboBuilderConnectionOption` items. + * + * No-auth account providers (opencode, mimocode) pack multiple "accounts" as UUIDs in + * `providerSpecificData.fingerprints` of a single DB row (#6087). This helper inflates + * them: each fingerprint becomes a separate option labeled "Account N" with an id of + * `${rowId}|fp|${fingerprint}` so the combo step can pin a specific account. + * Rows without fingerprints are converted 1:1 via buildConnectionOption as before. + */ +export function expandConnectionOptions( + connections: ProviderConnectionLike[] +): ComboBuilderConnectionOption[] { + const result: ComboBuilderConnectionOption[] = []; + for (const connection of connections) { + const fingerprints = Array.isArray(connection.providerSpecificData?.fingerprints) + ? (connection.providerSpecificData.fingerprints as unknown[]).filter( + (fp): fp is string => typeof fp === "string" && fp.length > 0 + ) + : []; + if (fingerprints.length > 0) { + const baseStatus = deriveConnectionStatus(connection); + const basePriority = typeof connection.priority === "number" ? connection.priority : 0; + for (let i = 0; i < fingerprints.length; i++) { + result.push({ + id: `${toStringOrNull(connection.id) || ""}|fp|${fingerprints[i]}`, + label: `Account ${i + 1}`, + type: toStringOrNull(connection.authType) || "unknown", + status: baseStatus, + priority: basePriority, + isActive: connection.isActive !== false, + defaultModel: toStringOrNull(connection.defaultModel), + rateLimitedUntil: toNumberOrNull(connection.rateLimitedUntil), + lastError: toStringOrNull(connection.lastError), + lastTested: toStringOrNull(connection.lastTested), + }); + } + } else { + const opt = buildConnectionOption(connection); + if (opt) result.push(opt); + } + } + return result; +} + function buildConnectionOption( connection: ProviderConnectionLike ): ComboBuilderConnectionOption | null { @@ -509,10 +554,9 @@ export async function getComboBuilderOptions(): Promise buildConnectionOption(connection)) - .filter((connection): connection is ComboBuilderConnectionOption => Boolean(connection)) - .sort(compareConnections); + const normalizedConnections = expandConnectionOptions(providerConnections).sort( + compareConnections + ); const activeConnectionCount = normalizedConnections.filter( (connection) => connection.isActive diff --git a/tests/unit/combo-builder-fingerprint-expansion.test.ts b/tests/unit/combo-builder-fingerprint-expansion.test.ts new file mode 100644 index 0000000000..20a96c041f --- /dev/null +++ b/tests/unit/combo-builder-fingerprint-expansion.test.ts @@ -0,0 +1,87 @@ +/** + * Tests for combo builder fingerprint expansion (#6087). + * + * No-auth account providers (opencode, mimocode) pack multiple accounts as UUIDs inside + * providerSpecificData.fingerprints of a SINGLE provider_connections row. The combo + * builder must expand them into one selectable option per account. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { expandConnectionOptions } from "../../src/lib/combos/builderOptions.ts"; + +function makeConn(overrides: Record = {}) { + return { + id: "conn-1", + provider: "opencode", + authType: "noauth", + name: "OpenCode Account 1", + priority: 0, + isActive: true, + testStatus: "active", + ...overrides, + }; +} + +describe("expandConnectionOptions — fingerprint expansion", () => { + it("expands 3 fingerprints into 3 separate connection options", () => { + const result = expandConnectionOptions([ + makeConn({ providerSpecificData: { fingerprints: ["fp-a", "fp-b", "fp-c"] } }), + ]); + assert.equal(result.length, 3); + assert.equal(result[0].label, "Account 1"); + assert.equal(result[1].label, "Account 2"); + assert.equal(result[2].label, "Account 3"); + }); + + it("encodes fingerprint into the connection id for pinning", () => { + const result = expandConnectionOptions([ + makeConn({ id: "row-abc", providerSpecificData: { fingerprints: ["fp-x", "fp-y"] } }), + ]); + assert.ok(result[0].id.includes("fp-x"), `id should contain fp-x, got: ${result[0].id}`); + assert.ok(result[1].id.includes("fp-y"), `id should contain fp-y, got: ${result[1].id}`); + assert.ok(result[0].id.includes("row-abc")); + }); + + it("falls back to the row itself when there are no fingerprints", () => { + const result = expandConnectionOptions([makeConn({ name: "OpenCode Account 1" })]); + assert.equal(result.length, 1); + assert.equal(result[0].id, "conn-1"); + assert.equal(result[0].label, "OpenCode Account 1"); + }); + + it("treats an empty fingerprints array the same as no fingerprints", () => { + const result = expandConnectionOptions([ + makeConn({ providerSpecificData: { fingerprints: [] } }), + ]); + assert.equal(result.length, 1); + assert.equal(result[0].id, "conn-1"); + }); + + it("skips non-string entries in fingerprints array", () => { + const result = expandConnectionOptions([ + makeConn({ providerSpecificData: { fingerprints: ["fp-1", null, 42, "fp-2"] } }), + ]); + assert.equal(result.length, 2); + assert.equal(result[0].label, "Account 1"); + assert.equal(result[1].label, "Account 2"); + }); + + it("expands fingerprints across multiple rows, accumulating all accounts", () => { + const result = expandConnectionOptions([ + makeConn({ id: "row-1", providerSpecificData: { fingerprints: ["fp-1", "fp-2"] } }), + makeConn({ id: "row-2", providerSpecificData: { fingerprints: ["fp-3"] } }), + ]); + assert.equal(result.length, 3); + const ids = result.map((r) => r.id); + assert.ok(ids.some((id) => id.includes("fp-1"))); + assert.ok(ids.some((id) => id.includes("fp-2"))); + assert.ok(ids.some((id) => id.includes("fp-3"))); + }); + + it("inherits isActive from the parent connection row", () => { + const result = expandConnectionOptions([ + makeConn({ isActive: false, providerSpecificData: { fingerprints: ["fp-inactive"] } }), + ]); + assert.equal(result[0].isActive, false); + }); +});