mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-21 05:21:20 +00:00
Merge pull request #6643 from MarushiruDonato/fix/codex-manifest-reactive-vmodel
fix(frontend): 修复 Codex Manifest 编辑状态丢失
This commit is contained in:
@@ -3056,7 +3056,8 @@
|
||||
v-if="editForm.platform === 'openai' && editingGroup"
|
||||
ref="editCodexManifestRef"
|
||||
:group-id="editingGroup.id"
|
||||
v-model="editCodexManifestConfig"
|
||||
:model-value="editCodexManifestConfig"
|
||||
@update:model-value="Object.assign(editCodexManifestConfig, $event)"
|
||||
:account-names="editCodexManifestAccountNames"
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { defineComponent, h, type PropType } from "vue";
|
||||
import { flushPromises, mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AdminGroup, CodexModelsManifestConfig } from "@/types";
|
||||
import GroupsView from "@/views/admin/GroupsView.vue";
|
||||
|
||||
const {
|
||||
listGroups,
|
||||
getModelsListCandidates,
|
||||
getUsageSummary,
|
||||
getCapacitySummary,
|
||||
getLiveCapability,
|
||||
} = vi.hoisted(() => ({
|
||||
listGroups: vi.fn(),
|
||||
getModelsListCandidates: vi.fn(),
|
||||
getUsageSummary: vi.fn(),
|
||||
getCapacitySummary: vi.fn(),
|
||||
getLiveCapability: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/api/admin", () => ({
|
||||
adminAPI: {
|
||||
groups: {
|
||||
list: listGroups,
|
||||
getAll: vi.fn(),
|
||||
getModelsListCandidates,
|
||||
getUsageSummary,
|
||||
getCapacitySummary,
|
||||
getLiveCapability,
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
duplicate: vi.fn(),
|
||||
updateSortOrder: vi.fn(),
|
||||
},
|
||||
accounts: {
|
||||
list: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/app", () => ({
|
||||
useAppStore: () => ({
|
||||
showError: vi.fn(),
|
||||
showSuccess: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/onboarding", () => ({
|
||||
useOnboardingStore: () => ({
|
||||
isCurrentStep: vi.fn(() => false),
|
||||
nextStep: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("vue-i18n", async () => {
|
||||
const actual = await vi.importActual<typeof import("vue-i18n")>("vue-i18n");
|
||||
return {
|
||||
...actual,
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
};
|
||||
});
|
||||
|
||||
const sourceGroup = {
|
||||
id: 42,
|
||||
name: "OpenAI",
|
||||
description: null,
|
||||
platform: "openai",
|
||||
rate_multiplier: 1,
|
||||
rpm_limit: 0,
|
||||
is_exclusive: false,
|
||||
status: "active",
|
||||
subscription_type: "standard",
|
||||
daily_limit_usd: null,
|
||||
weekly_limit_usd: null,
|
||||
monthly_limit_usd: null,
|
||||
long_context_pricing_enabled: true,
|
||||
force_openai_fast: false,
|
||||
free_openai_fast: false,
|
||||
model_pricing: [],
|
||||
profit_control_enabled: false,
|
||||
profit_min_margin: 0,
|
||||
profit_safety_buffer: 0,
|
||||
allow_image_generation: false,
|
||||
allow_batch_image_generation: false,
|
||||
image_rate_independent: false,
|
||||
image_rate_multiplier: 1,
|
||||
batch_image_discount_multiplier: 0.5,
|
||||
batch_image_hold_multiplier: 0.6,
|
||||
image_price_1k: null,
|
||||
image_price_2k: null,
|
||||
image_price_4k: null,
|
||||
video_rate_independent: false,
|
||||
video_rate_multiplier: 1,
|
||||
video_price_480p: null,
|
||||
video_price_720p: null,
|
||||
video_price_1080p: null,
|
||||
web_search_price_per_call: null,
|
||||
search_price_per_1k: null,
|
||||
audio_realtime_price_per_min: null,
|
||||
audio_tts_price_per_million_chars: null,
|
||||
audio_stt_price_per_hour: null,
|
||||
peak_rate_enabled: false,
|
||||
peak_start: "",
|
||||
peak_end: "",
|
||||
peak_rate_multiplier: 1,
|
||||
claude_code_only: false,
|
||||
fallback_group_id: null,
|
||||
fallback_group_id_on_invalid_request: null,
|
||||
allow_messages_dispatch: false,
|
||||
allow_live: false,
|
||||
require_oauth_only: false,
|
||||
require_privacy_set: false,
|
||||
created_at: "2026-09-05T00:00:00Z",
|
||||
updated_at: "2026-09-05T00:00:00Z",
|
||||
model_routing: null,
|
||||
model_routing_enabled: false,
|
||||
mcp_xml_inject: true,
|
||||
supported_model_scopes: [],
|
||||
account_count: 1,
|
||||
active_account_count: 1,
|
||||
rate_limited_account_count: 0,
|
||||
models_list_config: undefined,
|
||||
codex_models_manifest_config: {
|
||||
enabled: false,
|
||||
account_ids: [],
|
||||
fallback_to_scheduler: false,
|
||||
},
|
||||
sort_order: 10,
|
||||
} satisfies AdminGroup;
|
||||
|
||||
const AppLayoutStub = defineComponent({
|
||||
template: "<main><slot /></main>",
|
||||
});
|
||||
|
||||
const TablePageLayoutStub = defineComponent({
|
||||
template: '<section><slot name="filters" /><slot name="table" /><slot name="pagination" /></section>',
|
||||
});
|
||||
|
||||
const DataTableStub = defineComponent({
|
||||
props: {
|
||||
data: { type: Array, default: () => [] },
|
||||
},
|
||||
template: '<div><div v-if="data.length"><slot name="cell-actions" :row="data[0]" /></div></div>',
|
||||
});
|
||||
|
||||
const BaseDialogStub = defineComponent({
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
},
|
||||
template: '<div v-if="show"><slot /><slot name="footer" /></div>',
|
||||
});
|
||||
|
||||
const CodexManifestAccountsFieldStub = defineComponent({
|
||||
name: "CodexManifestAccountsField",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as PropType<CodexModelsManifestConfig>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit, expose }) {
|
||||
expose({ validate: () => true, resetValidation: () => undefined });
|
||||
return () =>
|
||||
h("div", { "data-testid": "codex-manifest-field" }, [
|
||||
h(
|
||||
"output",
|
||||
{ "data-testid": "codex-manifest-value" },
|
||||
JSON.stringify(props.modelValue),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
"data-testid": "codex-manifest-enable",
|
||||
onClick: () =>
|
||||
emit("update:modelValue", {
|
||||
...props.modelValue,
|
||||
enabled: true,
|
||||
}),
|
||||
},
|
||||
"enable",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
"data-testid": "codex-manifest-select-account",
|
||||
onClick: () =>
|
||||
emit("update:modelValue", {
|
||||
...props.modelValue,
|
||||
account_ids: [17],
|
||||
}),
|
||||
},
|
||||
"select account",
|
||||
),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const mountView = () =>
|
||||
mount(GroupsView, {
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: AppLayoutStub,
|
||||
TablePageLayout: TablePageLayoutStub,
|
||||
DataTable: DataTableStub,
|
||||
Pagination: true,
|
||||
BaseDialog: BaseDialogStub,
|
||||
ConfirmDialog: true,
|
||||
EmptyState: true,
|
||||
Select: true,
|
||||
PlatformIcon: true,
|
||||
Icon: true,
|
||||
GroupCapacityBadge: true,
|
||||
GroupRateMultipliersModal: true,
|
||||
GroupRPMOverridesModal: true,
|
||||
ReasoningEffortPolicyFields: true,
|
||||
CodexManifestAccountsField: CodexManifestAccountsFieldStub,
|
||||
PricingEntryCard: true,
|
||||
VueDraggable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("GroupsView Codex manifest binding", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
listGroups.mockReset();
|
||||
getModelsListCandidates.mockReset();
|
||||
getUsageSummary.mockReset();
|
||||
getCapacitySummary.mockReset();
|
||||
getLiveCapability.mockReset();
|
||||
|
||||
listGroups.mockResolvedValue({
|
||||
items: [sourceGroup],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
pages: 1,
|
||||
});
|
||||
getModelsListCandidates.mockResolvedValue([]);
|
||||
getUsageSummary.mockResolvedValue([]);
|
||||
getCapacitySummary.mockResolvedValue([]);
|
||||
getLiveCapability.mockResolvedValue({ supported: false });
|
||||
});
|
||||
|
||||
it("preserves consecutive child updates on the reactive edit config", async () => {
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
const editButton = wrapper
|
||||
.findAll("button")
|
||||
.find((button) => button.text().includes("common.edit"));
|
||||
expect(editButton).toBeTruthy();
|
||||
await editButton!.trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('[data-testid="codex-manifest-value"]').text()).toBe(
|
||||
JSON.stringify(sourceGroup.codex_models_manifest_config),
|
||||
);
|
||||
|
||||
await wrapper.get('[data-testid="codex-manifest-enable"]').trigger("click");
|
||||
await flushPromises();
|
||||
expect(wrapper.get('[data-testid="codex-manifest-value"]').text()).toContain(
|
||||
'"enabled":true',
|
||||
);
|
||||
|
||||
await wrapper
|
||||
.get('[data-testid="codex-manifest-select-account"]')
|
||||
.trigger("click");
|
||||
await flushPromises();
|
||||
expect(wrapper.get('[data-testid="codex-manifest-value"]').text()).toBe(
|
||||
JSON.stringify({
|
||||
enabled: true,
|
||||
account_ids: [17],
|
||||
fallback_to_scheduler: false,
|
||||
}),
|
||||
);
|
||||
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user