fix(dashboard): proxy modal stops pre-filling new scopes with an unrelated proxy (#4312)

The proxy assignments list returned by /api/settings/proxies/assignments is
global, so its first entry belongs to some other scope. ProxyConfigModal picked
`items.find(matchingScope) || items[0]`, so opening the proxy config for a freshly
created provider/key (which has no assignment of its own) fell back to items[0]
and pre-filled host/port/user/password from an unrelated proxy plus set
hasOwnProxy=true — users reported a new provider already carried a proxy they
never configured.

Extracted the scope helpers into proxyAssignment.ts and added selectScopeAssignment
which returns null (never items[0]) when the current scope has no assignment. The
modal then shows the empty/custom state for new scopes. Both call sites now use it.

TDD: src/shared/components/proxyAssignment.test.tsx (no-match -> null red->green for
provider/key/global scopes; matching-scope + empty-list regression guards). Existing
ProxyConfigModal component test stays green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-19 21:31:39 -03:00
committed by GitHub
parent 466d3cf6eb
commit 5193a595bf
3 changed files with 89 additions and 23 deletions

View File

@@ -4,6 +4,12 @@ import { useState, useEffect, useMemo } from "react";
import { useTranslations } from "next-intl";
import Modal from "./Modal";
import Button from "./Button";
import {
type ProxyAssignmentItem,
normalizeScopeId,
isSameScopeAssignment,
selectScopeAssignment,
} from "./proxyAssignment";
const ALL_PROXY_TYPES = [
{ value: "http", label: "HTTP" },
@@ -33,12 +39,6 @@ type ProxyRegistryItem = {
source?: string | null;
};
type ProxyAssignmentItem = {
proxyId?: string | null;
scope?: string | null;
scopeId?: string | null;
};
type ProxyConfigModalProps = {
isOpen: boolean;
onClose: () => void;
@@ -59,20 +59,6 @@ function getAssignmentScopeId(level: ProxyConfigLevel, levelId?: string) {
return level === "global" ? null : levelId || null;
}
function normalizeScopeId(scopeId?: string | null) {
return !scopeId || scopeId === "__global__" ? null : scopeId;
}
function isSameScopeAssignment(
assignment: ProxyAssignmentItem,
scope: string,
scopeId: string | null
) {
return (
assignment.scope === scope && normalizeScopeId(assignment.scopeId) === normalizeScopeId(scopeId)
);
}
function getCustomProxyName(level: ProxyConfigLevel, levelId?: string, levelLabel?: string) {
const label = levelLabel || levelId || "";
const suffix = label ? ` (${label})` : "";
@@ -95,7 +81,7 @@ async function fetchAssignmentForScope(scope: string, scopeId: string | null) {
const payload = await readJson(res);
const items: ProxyAssignmentItem[] = Array.isArray(payload?.items) ? payload.items : [];
return items.find((item) => isSameScopeAssignment(item, scope, scopeId)) || items[0] || null;
return selectScopeAssignment(items, scope, scopeId);
}
async function fetchRegistryProxy(proxyId: string, cachedProxies: ProxyRegistryItem[]) {
@@ -198,8 +184,7 @@ export default function ProxyConfigModal({
if (assignmentRes.ok) {
const assignmentPayload = await assignmentRes.json();
const items = Array.isArray(assignmentPayload?.items) ? assignmentPayload.items : [];
const target =
items.find((item) => isSameScopeAssignment(item, scope, scopeId)) || items[0];
const target = selectScopeAssignment(items, scope, scopeId);
if (target?.proxyId) {
setSelectedProxyId(target.proxyId);
setHasOwnProxy(true);

View File

@@ -0,0 +1,41 @@
import { describe, it, expect } from "vitest";
import { selectScopeAssignment } from "./proxyAssignment";
// Regression guard for the escalated bug "when I create a new provider it already
// comes with a proxy I never configured". The proxy assignments list is global, so
// its first entry belongs to some other scope. selectScopeAssignment must return null
// when the current scope has no assignment of its own — never fall back to items[0],
// which used to pre-fill freshly created providers/keys with an unrelated proxy.
const ASSIGNMENTS = [
{ proxyId: "socks5-acct", scope: "key", scopeId: "3c0031c1-existing-account" },
{ proxyId: "http-claude", scope: "provider", scopeId: "claude" },
];
describe("selectScopeAssignment", () => {
it("returns null (not items[0]) for a scope with no assignment of its own", () => {
// A brand-new provider with no proxy assignment.
const result = selectScopeAssignment(ASSIGNMENTS, "provider", "newprovider");
expect(result).toBeNull();
});
it("returns null for a new account/key scope even when other proxies exist", () => {
const result = selectScopeAssignment(ASSIGNMENTS, "key", "brand-new-key");
expect(result).toBeNull();
});
it("returns the matching assignment when the scope owns one", () => {
const result = selectScopeAssignment(ASSIGNMENTS, "provider", "claude");
expect(result?.proxyId).toBe("http-claude");
});
it("treats the global scope (null scopeId) distinctly", () => {
const withGlobal = [{ proxyId: "g", scope: "global", scopeId: null }, ...ASSIGNMENTS];
expect(selectScopeAssignment(withGlobal, "global", null)?.proxyId).toBe("g");
// a provider scope still must not borrow the global assignment
expect(selectScopeAssignment(withGlobal, "provider", "unconfigured")).toBeNull();
});
it("returns null for an empty assignments list", () => {
expect(selectScopeAssignment([], "provider", "anything")).toBeNull();
});
});

View File

@@ -0,0 +1,40 @@
/**
* Proxy-assignment scope helpers, extracted from ProxyConfigModal so the pure
* selection logic can be unit-tested without rendering the React component.
*/
export type ProxyAssignmentItem = {
proxyId?: string | null;
scope?: string | null;
scopeId?: string | null;
};
export function normalizeScopeId(scopeId?: string | null) {
return !scopeId || scopeId === "__global__" ? null : scopeId;
}
export function isSameScopeAssignment(
assignment: ProxyAssignmentItem,
scope: string,
scopeId: string | null
) {
return (
assignment.scope === scope && normalizeScopeId(assignment.scopeId) === normalizeScopeId(scopeId)
);
}
/**
* Pick the proxy assignment that belongs to *this* scope, or `null` when none does.
*
* Must NOT fall back to `items[0]`: the assignments list is global, so the first
* entry belongs to some other scope (e.g. another account's proxy). Returning it
* for a scope with no assignment of its own made a freshly created provider/key
* appear pre-filled with an unrelated proxy the user never configured. (escalated bug)
*/
export function selectScopeAssignment(
items: ProxyAssignmentItem[],
scope: string,
scopeId: string | null
): ProxyAssignmentItem | null {
return items.find((item) => isSameScopeAssignment(item, scope, scopeId)) ?? null;
}