Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 0 additions & 131 deletions src/app/[locale]/settings/config/_components/system-settings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import {
MapPin,
Network,
Pencil,
Plus,
Radio,
Terminal,
Thermometer,
Trash2,
Wrench,
Zap,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import { GroupMultiSelect } from "@/app/[locale]/settings/request-filters/_components/group-multi-select";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { InlineWarning } from "@/components/ui/inline-warning";
Expand Down Expand Up @@ -668,30 +664,6 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
/>
</div>

{/* Enable OpenAI Responses WebSocket (Codex only) */}
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/5 flex items-center justify-between hover:bg-white/[0.04] transition-colors">
<div className="flex items-start gap-3">
<div className="w-8 h-8 flex items-center justify-center rounded-lg bg-cyan-500/10 text-cyan-400 shrink-0">
<Radio className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-medium text-foreground">
{t("enableOpenaiResponsesWebsocket")}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{t("enableOpenaiResponsesWebsocketDesc")}
</p>
</div>
</div>
<Switch
id="enable-openai-responses-websocket"
aria-label={t("enableOpenaiResponsesWebsocket")}
checked={enableOpenaiResponsesWebsocket}
onCheckedChange={(checked) => setEnableOpenaiResponsesWebsocket(checked)}
disabled={isPending}
/>
</div>

<div className="p-4 rounded-xl bg-white/[0.02] border border-white/5 flex items-center justify-between hover:bg-white/[0.04] transition-colors">
<div className="flex items-start gap-3">
<div className="w-8 h-8 flex items-center justify-center rounded-lg bg-red-500/10 text-red-400 shrink-0">
Expand Down Expand Up @@ -852,109 +824,6 @@ export function SystemSettingsForm({ initialSettings }: SystemSettingsFormProps)
/>
</div>

{/* Fake Streaming Whitelist */}
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/5 space-y-3">
<div className="flex items-start gap-3">
<div className="w-8 h-8 flex items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-400 shrink-0">
<Radio className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-medium text-foreground">{t("fakeStreaming.title")}</p>
<p className="text-xs text-muted-foreground mt-0.5">
{t("fakeStreaming.description")}
</p>
</div>
</div>

{fakeStreamingWhitelist.length === 0 ? (
<p className="text-xs text-muted-foreground italic px-1">
{t("fakeStreaming.emptyState")}
</p>
) : (
<div className="space-y-2">
{fakeStreamingWhitelist.map((entry, index) => (
<div
key={index}
className="rounded-lg border border-border bg-muted/30 p-3 space-y-2"
>
<div className="flex items-center gap-2">
<Label
htmlFor={`fake-streaming-model-${index}`}
className="text-xs font-medium text-muted-foreground w-20 shrink-0"
>
{t("fakeStreaming.modelLabel")}
</Label>
<Input
id={`fake-streaming-model-${index}`}
data-testid={`fake-streaming-model-${index}`}
value={entry.model}
onChange={(event) => {
const next = event.target.value;
setFakeStreamingWhitelist((prev) =>
prev.map((item, i) => (i === index ? { ...item, model: next } : item))
);
}}
placeholder={t("fakeStreaming.modelPlaceholder")}
disabled={isPending}
className={inputClassName}
/>
<Button
type="button"
variant="ghost"
size="icon"
data-testid={`fake-streaming-remove-${index}`}
onClick={() => {
setFakeStreamingWhitelist((prev) => prev.filter((_, i) => i !== index));
}}
disabled={isPending}
aria-label={t("fakeStreaming.remove")}
className="shrink-0 text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="flex items-start gap-2">
<Label className="text-xs font-medium text-muted-foreground w-20 shrink-0 pt-2">
{t("fakeStreaming.groupsLabel")}
</Label>
<div className="flex-1 space-y-1">
<GroupMultiSelect
selectedGroupTags={entry.groupTags}
onChange={(groupTags) => {
setFakeStreamingWhitelist((prev) =>
prev.map((item, i) => (i === index ? { ...item, groupTags } : item))
);
}}
disabled={isPending}
/>
{entry.groupTags.length === 0 ? (
<p className="text-[11px] text-muted-foreground">
{t("fakeStreaming.allGroupsHint")}
</p>
) : null}
</div>
</div>
</div>
))}
</div>
)}

<Button
type="button"
variant="outline"
size="sm"
data-testid="fake-streaming-add"
onClick={() => {
setFakeStreamingWhitelist((prev) => [...prev, { model: "", groupTags: [] }]);
}}
disabled={isPending}
className="w-full"
>
<Plus className="h-4 w-4 mr-2" />
{t("fakeStreaming.addModel")}
</Button>
</div>

{/* Enable Codex Session ID Completion */}
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/5 flex items-center justify-between hover:bg-white/[0.04] transition-colors">
<div className="flex items-start gap-3">
Expand Down
7 changes: 1 addition & 6 deletions src/types/system-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@ export interface FakeStreamingWhitelistEntry {

// Default whitelist used when system_settings has no persisted value (legacy
// upgrade path). A persisted empty array is preserved as explicit opt-out.
export const DEFAULT_FAKE_STREAMING_WHITELIST: ReadonlyArray<FakeStreamingWhitelistEntry> = [
{ model: "gpt-image-2", groupTags: [] },
{ model: "gpt-image-1.5", groupTags: [] },
{ model: "gemini-3.1-flash-image-preview", groupTags: [] },
{ model: "gemini-3-pro-image-preview", groupTags: [] },
];
export const DEFAULT_FAKE_STREAMING_WHITELIST: ReadonlyArray<FakeStreamingWhitelistEntry> = [];

export interface SystemSettings {
id: number;
Expand Down
11 changes: 3 additions & 8 deletions tests/unit/actions/system-config-fake-streaming-setting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,7 @@ vi.mock("@/lib/utils/timezone", () => ({
isValidIANATimezone: vi.fn(() => true),
}));

const DEFAULT_FAKE_STREAMING_MODELS = [
{ model: "gpt-image-2", groupTags: [] },
{ model: "gpt-image-1.5", groupTags: [] },
{ model: "gemini-3.1-flash-image-preview", groupTags: [] },
{ model: "gemini-3-pro-image-preview", groupTags: [] },
];
const DEFAULT_FAKE_STREAMING_MODELS: { model: string; groupTags: string[] }[] = [];

function createSettings(overrides: Record<string, unknown> = {}) {
return {
Expand Down Expand Up @@ -131,7 +126,7 @@ describe("fake streaming whitelist system setting", () => {
});

describe("transformer defaults", () => {
test("defaults missing fake streaming config to requested image models", async () => {
test("defaults missing fake streaming config to the empty default", async () => {
const { toSystemSettings } = await import("@/repository/_shared/transformers");

const fromUndefined = toSystemSettings(undefined);
Expand Down Expand Up @@ -180,7 +175,7 @@ describe("fake streaming whitelist system setting", () => {
expect(result.fakeStreamingWhitelist).toEqual(persisted);
});

test("repository fallback (table missing) defaults to image models", async () => {
test("repository fallback (table missing) defaults to the empty default", async () => {
vi.resetModules();
vi.doUnmock("@/repository/system-config");
vi.doMock("@/drizzle/db", () => ({
Expand Down
83 changes: 16 additions & 67 deletions tests/unit/settings/system-settings-form-fake-streaming.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,6 @@ async function submitForm() {
});
}

function findRemoveButtons(): HTMLButtonElement[] {
return Array.from(
document.querySelectorAll<HTMLButtonElement>('button[data-testid^="fake-streaming-remove-"]')
);
}

function findAddButton(): HTMLButtonElement | null {
return document.querySelector<HTMLButtonElement>('button[data-testid="fake-streaming-add"]');
}

function findModelInputs(): HTMLInputElement[] {
return Array.from(
document.querySelectorAll<HTMLInputElement>('input[data-testid^="fake-streaming-model-"]')
);
}

describe("SystemSettingsForm fake streaming whitelist", () => {
beforeEach(() => {
document.body.innerHTML = "";
Expand All @@ -185,55 +169,23 @@ describe("SystemSettingsForm fake streaming whitelist", () => {
unmount();
});

test("user can add a new model entry and saves it for all groups", async () => {
test("editor UI is no longer rendered", () => {
const { unmount } = render(<SystemSettingsForm initialSettings={baseSettings} />);

const addBtn = findAddButton();
if (!addBtn) throw new Error("未找到 fake-streaming 添加按钮");

act(() => {
addBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});

const inputs = findModelInputs();
expect(inputs.length).toBe(3);
const newRow = inputs[2];

act(() => {
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
setter?.call(newRow, "custom-model-x");
newRow.dispatchEvent(new Event("input", { bubbles: true }));
});

await submitForm();

expect(systemConfigActionMocks.saveSystemSettings).toHaveBeenCalledWith(
expect.objectContaining({
fakeStreamingWhitelist: [
{ model: "gpt-image-2", groupTags: [] },
{ model: "gemini-3.1-flash-image-preview", groupTags: [] },
{ model: "custom-model-x", groupTags: [] },
],
})
);
expect(document.querySelector('button[data-testid="fake-streaming-add"]')).toBeNull();
expect(document.querySelector('button[data-testid^="fake-streaming-remove-"]')).toBeNull();
expect(document.querySelector('input[data-testid^="fake-streaming-model-"]')).toBeNull();

unmount();
});

test("user can remove a model entry and the empty whitelist is preserved as opt-out", async () => {
const singleEntry = {
test("preserves an explicitly empty initial whitelist as opt-out", async () => {
const emptyInitial = {
...baseSettings,
fakeStreamingWhitelist: [{ model: "gpt-image-2", groupTags: [] }],
fakeStreamingWhitelist: [],
} satisfies typeof baseSettings;

const { unmount } = render(<SystemSettingsForm initialSettings={singleEntry} />);

const removeBtns = findRemoveButtons();
expect(removeBtns.length).toBe(1);

act(() => {
removeBtns[0].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const { unmount } = render(<SystemSettingsForm initialSettings={emptyInitial} />);

await submitForm();

Expand All @@ -247,18 +199,15 @@ describe("SystemSettingsForm fake streaming whitelist", () => {
});

test("trims whitespace and drops empty model entries before submitting", async () => {
const { unmount } = render(<SystemSettingsForm initialSettings={baseSettings} />);

const inputs = findModelInputs();
expect(inputs.length).toBe(2);
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
const initial = {
...baseSettings,
fakeStreamingWhitelist: [
{ model: " custom-image-model ", groupTags: [] },
{ model: " ", groupTags: [] },
],
} satisfies typeof baseSettings;

act(() => {
setter?.call(inputs[0], " custom-image-model ");
inputs[0].dispatchEvent(new Event("input", { bubbles: true }));
setter?.call(inputs[1], " ");
inputs[1].dispatchEvent(new Event("input", { bubbles: true }));
});
const { unmount } = render(<SystemSettingsForm initialSettings={initial} />);

await submitForm();

Expand Down
Loading