import { expect, test } from "bun:test"; import { moduleGroupSource } from "./helpers/module-group-source"; function compactSource(source: string) { return source.replace(/\s+/g, " ").trim(); } function sourceSection(source: string, startMarker: string, endMarker: string) { const start = source.indexOf(startMarker); const end = source.indexOf(endMarker, start + startMarker.length); expect(start).toBeGreaterThanOrEqual(0); expect(end).toBeGreaterThan(start); return source.slice(start, end); } test("announcement editor preserves image and pinned fields through edit and save", async () => { const [panelSource, safetySource] = await Promise.all([ Promise.resolve(moduleGroupSource("pages/admin/components/admin-announcements-panel.tsx")), Bun.file(new URL("../src/pages/admin/components/admin-announcement-safety.ts", import.meta.url)).text(), ]); const panel = compactSource(panelSource); expect(panel).toContain("uploadAdminAnnouncementImage"); expect(panel).toContain("discardAdminAnnouncementImage"); expect(panel).toContain('imageResourceId: announcement.imageResourceId || ""'); expect(panel).toContain("pinned: announcement.pinned"); expect(panel).toContain('imageResourceId: values.imageResourceId?.trim() || ""'); expect(panel).toContain("pinned: Boolean(values.pinned)"); expect(panel).toContain('(announcement?.imageResourceId || "") === (expectedContent.imageResourceId || "")'); expect(panel).toContain('rootClassName="admin-modal-root admin-announcement-editor-modal"'); expect(panel).toContain("centered"); expect(panel).not.toContain(" { const [pluginSource, adminCss] = await Promise.all([Bun.file(new URL("../src/pages/plugins/plugin-documentation-modals.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text()]); const toggleCss = sourceSection(adminCss, ".admin-price-tier-toggle span {", ".admin-model-editor-add-tier.ant-btn {"); expect(pluginSource).toContain("event.preventDefault()"); expect(pluginSource).toContain("onDragOver={(event)"); expect(pluginSource).toContain("onDrop={handlePluginDrop}"); expect(pluginSource).toContain("点击选择插件文件,也可拖拽到此处"); expect(pluginSource).toContain("释放文件以上传插件"); expect(pluginSource).toContain("isDraggingPlugin"); expect(compactSource(adminCss)).toContain(".admin-price-tier-controls { margin-left: auto;"); expect(toggleCss).toContain("overflow-wrap: anywhere;"); expect(toggleCss).toContain("white-space: normal;"); expect(toggleCss).not.toContain("text-overflow: ellipsis;"); }); test("model reference limits use compact rows only inside the admin editor", async () => { const css = await Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(); const numberField = sourceSection(css, ".admin-model-editor-references .admin-capability-number-field {", ".admin-model-editor-references .admin-capability-boolean-field {"); expect(numberField).toContain("grid-template-columns: minmax(0, 1fr) 80px;"); expect(numberField).toContain("min-height: 32px;"); expect(numberField).toContain("align-items: center;"); const switches = sourceSection(css, ".admin-model-editor-references .admin-capability-boolean-field label {", "@media (min-width: 601px)"); expect(switches).toContain("display: flex;"); expect(compactSource(css)).toContain(".admin-model-editor-references .admin-capability-reference-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start;"); expect(compactSource(css)).toContain(".admin-model-editor-modal .admin-capability-reference-grid { grid-template-columns: minmax(0, 1fr);"); }); test("model editor presents protocols in a searchable inline radio browser instead of a dropdown", async () => { const [source, css] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/channel-model-editor.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text()]); const protocolSection = sourceSection(compactSource(source), ' { const [componentSource, apiSource, adminCssSource] = await Promise.all([ Bun.file(new URL("../src/pages/admin/components/channel-model-manager.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/wallet.ts", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(), ]); const component = compactSource(componentSource); expect(apiSource).toContain("http.post<{ models: string[] }>(`/admin/channels/${encodeURIComponent(channelId)}/models/fetch`)"); expect(apiSource).toContain("http.post<{ models: string[]; added: number }>(`/admin/channels/${encodeURIComponent(channelId)}/models/import`, { models })"); expect(component).toContain('title="选择要导入的模型"'); expect(component).toContain("默认已全选"); expect(component).toContain("setFetchPreviewOpen(true)"); expect(component).toContain("setSelectedFetchModels(result.models)"); expect(component).toContain("已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型"); expect(component).toContain("disabled={importing || allFetchModelsSelected}"); expect(component).toContain("onClick={() => setSelectedFetchModels(fetchPreviewModels)}"); expect(component).toContain("disabled={importing || selectedFetchModels.length === 0}"); expect(component).toContain("onClick={() => setSelectedFetchModels([])}"); expect(component).toContain("取消全选"); expect(component).toContain("disabled={importing}"); expect(component).toContain("importAdminChannelModels(channel.id, selectedFetchModels)"); expect(component).toContain("disabled={!selectedFetchModels.length}"); expect(component).not.toContain("disabled: alreadyExists"); expect(component).not.toContain("const result = await fetchAdminChannelModels(channel.id); await reload();"); expect(adminCssSource).toContain(".admin-model-import-modal .channel-model-import-picker .ant-checkbox-checked"); expect(adminCssSource).toContain("border-color: var(--control-check-fg) !important"); }); test("channel model manager supports bounded atomic batch deletion", async () => { const [componentSource, apiSource] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/channel-model-manager.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/wallet.ts", import.meta.url)).text()]); const component = compactSource(componentSource); expect(apiSource).toContain("http.post<{ deleted: number }>(`/admin/channels/${encodeURIComponent(channelId)}/models/batch-delete`, { modelIds })"); expect(component).toContain(" { const source = compactSource(await Bun.file(new URL("../src/pages/admin/components/analytics-panel.tsx", import.meta.url)).text()); expect(source).toContain('type RangePreset = "7d" | "30d" | "60d"'); expect(source).toContain('["60d", "60 天"]'); expect(source).toContain('next.set("rangePreset", rangePreset)'); expect(source).toContain("setRangePreset(undefined)"); expect(source).not.toContain("模型价格配置"); expect(source).not.toContain("listAdminModelPricings"); expect(source).toContain("finance.revenueMicrocredits"); expect(source).toContain("finance.profitMicrocredits"); expect(source).toContain("finance.costedOrders < finance.settledOrders"); expect(source).toContain("...analyticsFinanceColumns"); expect(source).toContain("后端未返回完整财务统计"); }); test("storage settings keep generic S3 controls and connection validation", async () => { const source = await Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx")); const compacted = compactSource(source); expect(compacted).toContain('{ mode: "s3", label: "S3 兼容存储"'); expect(compacted).toContain("testAdminOSSConnection(connectionInput(values))"); for (const field of ["s3Preset", "sessionToken", "pathStyle", "allowUserS3"]) { expect(compacted).toContain(`name="${field}"`); } expect(compacted).toContain('["aliyun", "tencent", "qiniu", "s3"].includes(setting.provider || "")'); }); test("admin navigation keeps the storage resource page reachable", async () => { const source = await Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text(); expect(source).toContain('path: "/admin/resources"'); }); test("nested admin pages return to their own parent entry", async () => { const source = await Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text(); const compacted = compactSource(source); expect(compacted).toContain("const currentItem = currentSection?.items.find"); expect(compacted).toContain("const sectionPath = back ? (currentItem?.path"); }); test("high impact feature shutdowns require confirmation", async () => { const source = await Bun.file(new URL("../src/pages/admin/components/feature-availability-panel.tsx", import.meta.url)).text(); expect(source).toContain('title: "关闭用户积分功能?"'); expect(source).toContain('title: "关闭前台模型功能?"'); expect(source).toContain("onChange={requestFeatureChange}"); }); test("admin settings use full-width summaries without selected-card side stripes", async () => { const [componentSource, cssSource] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/admin-ui.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text()]); expect(componentSource).not.toContain("lg:grid lg:grid-cols-4"); expect(cssSource).not.toContain('content: "配置摘要"'); expect(cssSource).not.toContain("grid-template-columns: minmax(0, 1fr) 344px"); expect(cssSource).not.toContain(".admin-storage-mode-choice::before"); const featureSelected = sourceSection(cssSource, ".admin-feature-board-row.is-selected {", ".admin-feature-board-row.is-dirty {"); const drawingSelected = sourceSection(cssSource, ".admin-drawing-engine-choice.is-selected {", ".admin-drawing-engine-choice.is-unavailable"); expect(featureSelected).not.toContain("inset 3px 0 0"); expect(drawingSelected).not.toContain("inset 3px 0 0"); }); test("task-first settings reveal dependent configuration only after the primary choice", async () => { const [storageSource, emailSource, accessSource, featureSource, appearanceSource, welcomeSource, drawingSource, arkSource, interceptionSource, thirdPartySource, cssSource] = await Promise.all([ Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx")), Bun.file(new URL("../src/pages/admin/components/email-settings-panel.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/access-settings-panel.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/feature-availability-panel.tsx", import.meta.url)).text(), Promise.resolve(moduleGroupSource("pages/admin/settings/appearance-settings-page.tsx")), Bun.file(new URL("../src/pages/admin/settings/components/welcome-setting.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/settings/drawing-engine-settings-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/settings/ark-private-assets-settings-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/settings/response-interception-settings-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/settings/libtv-settings-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(), ]); expect(storageSource).toContain('title="1. 选择新资源存储位置"'); expect(storageSource).toContain("选择后继续完成第 2 步并保存"); expect(sourceSection(storageSource, "const requestModeChange", "const save")).not.toContain("save(values)"); expect(emailSource).toContain("{draftEnabled ? ("); expect(emailSource).toContain('id="admin-email-smtp"'); expect(emailSource).toContain('title="1. 是否发送账户安全邮件"'); expect(emailSource).toContain('title="2. 配置 SMTP 连接与发件身份"'); expect(accessSource).toContain("{draftLinuxDOEnabled ? ("); expect(accessSource).toContain('title="1. 是否允许创建新账号"'); expect(accessSource).toContain('title="2. 是否开放 Linux.do 登录"'); expect(featureSource).toContain('title="1. 用户工作台入口"'); expect(featureSource).toContain('title="2. 插件开放范围"'); expect(featureSource).toContain('title="3. 用户模型来源"'); expect(appearanceSource).toContain(""); expect(welcomeSource).toContain("启用欢迎页"); expect(welcomeSource).toContain("updateAdminFeatureAvailability({ welcomeEnabled: value })"); expect(drawingSource).toContain('title="1. 选择新建绘图默认编辑器"'); expect(drawingSource).toContain('title="2. 配置 tldraw 授权(按需)"'); expect(sourceSection(drawingSource, "const selectEngine", "async function save")).not.toContain("save("); expect(arkSource).toContain('title="1. 配置方舟项目与 IAM 凭据"'); expect(arkSource).toContain('title="2. 是否启用可信素材同步"'); expect(arkSource).toContain("{prerequisitesReady || draftEnabled ? ("); expect(arkSource).toContain('aria-label="启用可信素材同步,保存修改后生效"'); expect(interceptionSource).toContain('title="1. 是否替换用户可见的上游错误"'); expect(interceptionSource).toContain('title="2. 配置替换规则与优先级"'); expect(interceptionSource).toContain('title="3. 本地预览用户最终文案"'); expect(interceptionSource).toContain("{enabled ? ("); expect(interceptionSource).not.toContain('className="admin-intercept-overview"'); expect(sourceSection(interceptionSource, "const changeEnabled", "if (loading")).not.toContain("save("); expect(thirdPartySource).toContain('title="1. 配置 LibTV 服务端访问凭据"'); expect(thirdPartySource).toContain('title="2. 是否开放用户导入 LibTV 画布"'); expect(thirdPartySource).toContain('title="3. 验证已保存的 LibTV 凭据"'); expect(thirdPartySource).toContain("{draftHasToken ? ("); expect(thirdPartySource).toContain("{setting.hasToken && !clearTokenDraft ? ("); expect(thirdPartySource).not.toContain('className="admin-third-party-overview"'); expect(sourceSection(thirdPartySource, "const changeEnabled", "const markTokenForRemoval")).not.toContain("save("); expect(compactSource(cssSource)).toContain(".admin-feature-board { width: 100%; max-width: none; grid-template-columns: minmax(0, 1fr);"); }); test("admin tables keep requested filters and actions in the intended positions", async () => { const [storageSource, creditSource] = await Promise.all([ Bun.file(new URL("../src/pages/admin/components/storage-resources-panel.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/credit-operations-panel.tsx", import.meta.url)).text(), ]); const storageToolbar = sourceSection(storageSource, "toolbar={", "toolbarActiveFilters="); expect(storageToolbar).toContain('className="admin-storage-resource-filters"'); expect(storageToolbar).toContain('placeholder="资源 ID 或对象路径"'); expect(storageToolbar).toContain('placeholder="用户名 / 昵称 / 邮箱 / 用户 ID"'); expect(storageSource).toContain("user: debouncedUserQuery || undefined"); expect(storageSource).toContain("userId: debouncedUserQuery || undefined"); expect(storageToolbar).toContain('aria-label="筛选资源类型"'); expect(storageToolbar).toContain('aria-label="筛选资源状态"'); expect(storageToolbar).toContain('aria-label="筛选存储类型"'); const operationColumn = sourceSection(creditSource, 'title: "操作"', "const hasFilters"); expect(operationColumn).toContain('fixed: "right"'); }); test("request logs display user credit billing independently from upstream cost", async () => { const [listSource, detailSource, apiSource] = await Promise.all([ Bun.file(new URL("../src/pages/admin/logs/logs-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/api-log-detail-drawer.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/auth.ts", import.meta.url)).text(), ]); const billingSummary = sourceSection(listSource, "function BillingSummary", "function MediaResult"); expect(listSource).toContain('title: "积分计算"'); expect(listSource).toContain('title: "请求阶段 / 状态"'); expect(listSource).toContain('description="模型生成与结果下载记录;仅计费调用扣除积分"'); expect(billingSummary).toContain("billingAmountMicrocredits"); expect(billingSummary).toContain("billingAvailable"); expect(billingSummary).toContain("!log.billable"); expect(billingSummary).toContain("未扣积分"); expect(billingSummary).not.toContain("costAvailable"); expect(detailSource).toContain('label="请求阶段" value={requestKindText(log.requestKind)}'); expect(detailSource).toContain('label="计费" value={billingText(log)}'); expect(detailSource).toContain('label="请求 ID" value={}'); expect(detailSource).toContain('label="供应商任务 ID" value={log.providerRequestId ? : "未记录"}'); expect(apiSource).toContain("billingAmountMicrocredits: number"); expect(apiSource).toContain("billingAvailable: boolean"); }); test("banner announcement editor keeps title styles through edit, save and status toggle", async () => { const [panelSource, editorSource, sliderSource, apiSource, contentSource, emojiPickerSource, noticeSource] = await Promise.all([ Bun.file(new URL("../src/pages/admin/components/admin-banner-announcements-panel.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/banner-title-editor.tsx", import.meta.url)).text(), Bun.file(new URL("../src/components/layout/banner-announcements-slider.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/announcements.ts", import.meta.url)).text(), Bun.file(new URL("../src/components/layout/banner-announcement-content.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/banner-notice-emoji-picker.tsx", import.meta.url)).text(), Bun.file(new URL("../src/lib/announcements/banner-notice.ts", import.meta.url)).text(), ]); const panel = compactSource(panelSource); const slider = compactSource(sliderSource); // 状态开关与保存都必须带上样式分段与通知类型:后端更新走显式字段表,漏传会清空它们。 expect(panel).toContain("titleRuns: banner.titleRuns"); expect(panel).toContain("titleRuns: runs"); expect(panel).toContain("noticeType: selectedNoticeType"); expect(panel).toContain("noticeType: normalizeBannerNoticeType(banner.noticeType)"); // 编辑入口不能被新建信号重置:父级 createOpen 消费后立即复位,编辑路径不再写回该标志。 expect(panel).toContain("onCreateOpenChange(false)"); expect(panel).toContain('setDialog({ mode: "edit", banner })'); expect(panel).toContain("banner.titleRuns?.length"); // AntD 6 用 destroyOnHidden,destroyOnClose 已废弃。 expect(panel).toContain("destroyOnHidden"); expect(panel).not.toContain("destroyOnClose"); // Modal 渲染在 body portal 里,必须挂 admin-modal-root 才能拿到弹窗作用域的强边框 / 分层 token, // 否则编辑器等自绘控件的边框回落到 :root 的 8% 透明度,肉眼不可见。 expect(panel).toContain('rootClassName="admin-modal-root"'); // 非表单控件不能放进 Form.Item(会被注入 value/onChange/ref)。 expect(panel).toContain(""); // 通知类型在后台要有独立入口;emoji 图标不设独立字段,经编辑器「图标」按钮插入。 expect(panel).toContain(" { const [tokens, shell, chrome, globals] = await Promise.all([ Bun.file(new URL("../src/pages/admin/theme/admin-tokens.css", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/theme/admin-chrome.css", import.meta.url)).text(), Bun.file(new URL("../src/styles/globals.css", import.meta.url)).text(), ]); expect(tokens).toContain("--admin-canvas: #f7f8fa;"); expect(tokens).toContain("--admin-canvas: #111317;"); expect(tokens).not.toContain("--admin-layer-0: var(--workspace-"); expect(tokens).not.toContain("--admin-layer-0: var(--skin-admin-"); expect(shell).toContain("data-admin-root"); expect(shell).toContain("getIsolatedAdminAntTheme"); expect(shell).not.toContain("WorkspacePage"); expect(shell).not.toContain("getAdminAntThemeConfig"); expect(shell).not.toContain("app-workspace-nav-link"); expect(chrome).toContain("[data-admin-root] .admin-nav-link"); expect(chrome).toContain("border-left: 0 !important"); expect(chrome).not.toContain("left: -8px"); expect(chrome).toContain(".admin-drawer .ant-drawer-content"); expect(globals).not.toContain("/* 管理端专用视觉收口:不覆盖创作端 workspace 的导航、状态和图表样式。 */"); const [overlays, userDetail, prompts, payments, modelEditor] = await Promise.all([ Bun.file(new URL("../src/pages/admin/ui/overlays.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/admin-user-detail-drawer.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/storyboard-prompts/storyboard-prompts-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/payments/payments-page.tsx", import.meta.url)).text(), Bun.file(new URL("../src/pages/admin/components/channel-model-editor.tsx", import.meta.url)).text(), ]); expect(overlays).toContain('rootClassName={cn("admin-drawer"'); expect(overlays).toContain('rootClassName={cn("admin-modal-root"'); expect(overlays).not.toContain("@/components/ui/product"); for (const source of [userDetail, prompts, payments, modelEditor]) { expect(source).not.toContain("@/components/ui/product"); expect(source).not.toContain("AppDrawer"); expect(source).not.toContain("AppModal"); } });