Files
knowledge-governance/web/src/SystemSettings.test.tsx
T
姚章浩 f0b12412a1
build / build (push) Successful in 1m10s
fix(settings): 仅展示只读DWS凭证状态
2026-10-27 09:56:19 +08:00

643 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { SystemSettingsView, ppSourceConnections, visibleSaveMessage } from './SystemSettings'
import { AuthContext } from './auth'
import type { MonitorOverview } from './monitor'
import type { CatalogSource } from './sourceCatalog'
import type { UISettings } from './uiSettings'
import type { SourceConnectionListItem } from './sourceConnections'
const monitor: MonitorOverview = {
mode: 'simulation',
simulated: true,
observed_at: '2026-09-08T12:00:00Z',
host: {
cpu_percent: 42,
memory_used_bytes: 1,
memory_total_bytes: 2,
disk_used_bytes: 1,
disk_total_bytes: 2,
gpu_state: 'unknown',
gpu_detail: '未接入真实GPU监控',
simulated: true,
},
services: [
{
key: 'mineru',
name: 'MinerU',
state: 'healthy',
detail: '模拟',
latency_ms: 1,
observed_at: '2026-09-08T12:00:00Z',
simulated: true,
},
],
knowledge_bases: [],
health: [
{
key: 'mineru',
name: 'MinerU',
liveness: 'up',
readiness: 'blocked',
reason: '服务可达但容量已满(429),暂时不可接单',
latency_ms: 30,
observed_at: '2026-09-08T12:00:00Z',
last_success_at: '2026-09-08T11:59:00Z',
consecutive_failures: 2,
configured: true,
applicable: true,
},
],
}
const sources: CatalogSource[] = [
{
connection_id: 'ding-1',
source_type: 'dingtalk',
name: '公司级技术支持知识库',
root_node_id: 'root-1',
folder_count: 1079,
file_count: 3444,
observed_at: '2026-09-09T09:30:32Z',
snapshot_only: true,
snapshot_status: 'ready',
last_refresh_duration_ms: 0,
},
]
const connections: SourceConnectionListItem[] = [
{
id: 'conn-ding',
source_type: 'dingtalk',
name: '公司级技术支持知识库',
enabled: true,
schedule_configured: true,
health_status: 'healthy',
item_count: 3444,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: '2026-09-12T10:00:00Z',
},
{
id: 'conn-man',
source_type: 'manual',
name: '本地手工验收',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 5,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
]
const settings: UISettings = {
id: 'settings-default',
state: 'active',
document_refresh_seconds: 15,
resource_refresh_seconds: 30,
stalled_after_seconds: 1800,
finalizing_after_seconds: 7200,
ingestion_concurrency: 2,
test_weknora_base_url: '',
production_weknora_base_url: '',
production_tenant_id: '',
production_knowledge_base_id: '',
updated_by: 'system:migration',
created_at: '2026-09-10T08:00:00Z',
activated_at: '2026-09-10T08:00:00Z',
retired_at: null,
error_code: '',
}
const authValue = (role: 'admin' | 'engineer') => ({
identity: { id: `${role}-1`, username: role, display_name: role, role, must_change_password: false },
ready: true,
login: async () => null,
logout: async () => {},
changePassword: async () => null,
signedOut: 0,
notifySignedOut: () => {},
})
describe('SystemSettingsView service configuration', () => {
it('lets an administrator edit exactly the four service fields in the web console', () => {
const configured: UISettings = {
...settings,
test_weknora_base_url: 'https://weknora.test.example',
production_weknora_base_url: 'https://weknora.prod.example',
production_tenant_id: '10000',
production_knowledge_base_id: 'kb-abc123',
}
const view = (role: 'admin' | 'engineer') =>
renderToStaticMarkup(
<AuthContext.Provider value={authValue(role)}>
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={configured}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="services"
/>
</AuthContext.Provider>
)
const adminHtml = view('admin')
for (const text of [
'测试地址',
'生产地址',
'生产 ID',
'知识库 ID',
'保存并启用服务配置',
'https://weknora.test.example',
'https://weknora.prod.example',
'10000',
'kb-abc123',
'四项留空即表示未配置',
])
expect(adminHtml).toContain(text)
expect(adminHtml).not.toContain('仅管理员可修改')
// 界面只剩这四项:依赖探活矩阵与对内/对外绑定下拉不再出现在服务配置页。
for (const removed of ['MinerU', 'Embedding', 'Redis / MQ', 'PostgreSQL', '测试知识库映射'])
expect(adminHtml).not.toContain(removed)
const engineerHtml = view('engineer')
expect(engineerHtml).toContain('仅管理员可修改')
expect(engineerHtml).not.toContain('保存并启用服务配置')
})
})
describe('SystemSettingsView', () => {
it('separates document-engineer settings from locked administrator and production controls', () => {
const html = renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage="刷新与告警参数已验证并启用:文档15秒、资源30秒、停滞30分钟、finalizing 2小时"
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
/>
)
for (const text of ['系统设置', '数据源配置', '服务配置', '存储与备份', '刷新策略']) expect(html).toContain(text)
// 主人 2026-10-26 要求去掉的噪声:页头副标题、刷新状态、权限条、活动版本横幅。
for (const removed of [
'查看内容源、服务与备份状态',
'刷新状态',
'文档工程师可调整',
'管理员设置已锁定',
'设置服务已启用',
])
expect(html).not.toContain(removed)
// 需求(2026-10-14 收敛):数据源配置只剩两个入口 —— dws 导入(按当前钉钉账号发现)
// 与 PP 导入;AList/静态钉钉知识库等来源类型不再出现在这里(也没有折叠开关兜着)。
expect(html).toContain('公司级技术支持知识库')
expect(html).not.toContain('dws 共享授权')
for (const hidden of ['专用浏览器扫码会话', '显示 AList/钉钉知识库来源']) expect(html).not.toContain(hidden)
// 统一入口后不再按类型分标题:PP 与钉钉共用同一张清单与同一个添加向导。
expect(html).not.toContain('PP 导入')
// 需求(2026-10-10):数据源配置不再做文件格式筛选(格式由目录级标注决定)。
for (const removed of ['文件格式筛选', '未勾选的一律不取件', '保存保留格式', '恢复继承上级规则'])
expect(html).not.toContain(removed)
// 需求(2026-10-10):新增入口只剩「可加入的来源」勾选,粘贴链接的按钮已下线。
for (const removed of ['+ 新增钉钉知识库', '新增钉钉知识库']) expect(html).not.toContain(removed)
const servicesHtml = renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="services"
/>
)
for (const text of ['测试 WeKnora', '生产 WeKnora', '测试地址', '生产地址', '生产 ID', '知识库 ID'])
expect(servicesHtml).toContain(text)
const storageHtml = renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="storage"
/>
)
// 存储与备份分区已换成真实功能(整库逻辑备份的定时计划),静态占位文案不再存在。
expect(storageHtml).toContain('存储与备份')
expect(storageHtml).toContain('正在读取备份计划…')
expect(storageHtml).not.toContain('仅保存凭据引用,不可回读')
const refreshHtml = renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage="刷新与告警参数已验证并启用:文档15秒、资源30秒、停滞30分钟、finalizing 2小时"
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="refresh"
/>
)
for (const text of [
'刷新与告警参数已验证并启用',
'停滞30分钟',
'finalizing 2小时',
'文档状态刷新间隔',
'15秒',
'解析停滞判定',
'30分钟',
'保存并应用刷新与告警参数',
])
expect(refreshHtml).toContain(text)
expect(visibleSaveMessage('已保存', true)).toBeNull()
expect(visibleSaveMessage('已保存', false)).toBe('已保存')
expect(html).not.toContain('本地模拟')
expect(html).not.toContain('保存模拟设置')
for (const forbidden of ['DATABASE_URL', 'RELEASE_CONFIG_FILE', 'API Key明文', 'secret_ref'])
expect(html).not.toContain(forbidden)
// 两个环境卡仍由真实探测驱动(依赖矩阵搬到总览页,本页不再渲染)。
expect(servicesHtml).toContain('解析队列 对内 已配置 · 对外 已配置')
expect(servicesHtml).not.toContain('PostgreSQL')
expect(servicesHtml).toContain('端点探测:暂无真实探测结果')
// V1: left-nav section controls, per-source schedule editing, danger modal & history affordances.
expect(html).toContain('settings-nav')
for (const kept of ['更多', '自动刷新中']) expect(html).toContain(kept)
for (const hidden of ['查看修订历史', '保存并启用调度', '停用来源']) expect(html).not.toContain(hidden)
expect(html).not.toContain('本页只列 PP 来源')
expect(html).not.toContain('本地手工导入来源不支持定时刷新')
// dead buttons removed
expect(html).not.toContain('添加内容源')
expect(html).not.toContain('管理员编辑')
})
it('lets an administrator bind the internal/external import knowledge bases from 系统设置', () => {
// 组件一直在(RoutingBindingsSection),但从未挂到任何页面 —— 导入目标库只能靠
// 部署 env 钉死,换库必须重建容器。
const view = (section: 'sources' | 'bindings' | 'production-bindings') =>
renderToStaticMarkup(
<AuthContext.Provider value={authValue('admin')}>
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection={section}
/>
</AuthContext.Provider>
)
expect(view('sources')).toContain('测试知识库绑定')
expect(view('sources')).toContain('生产知识库绑定')
const bound = view('bindings')
expect(bound).toContain('id="settings-routing-bindings"')
expect(bound).toContain('分发到对应的测试 WeKnora 知识库')
// 未读到绑定数据时(SSR 不跑 effect)面板也要如实存在,而不是空白。
expect(bound).toContain('读取中…')
// 发布目标(源库 → 目标库)是独立分区,与测试端收件库分开。
const target = view('production-bindings')
expect(target).toContain('id="settings-production-bindings"')
expect(target).toContain('新发布的对内 / 对外内容按这里的绑定写入生产 WeKnora 的对应知识库')
})
it('does not show healthy simulation when the settings data failed to load', () => {
const html = renderToStaticMarkup(
<SystemSettingsView
sources={[]}
connections={[]}
monitor={null}
settings={null}
productionEnabled={false}
routingCapabilities={{ internal_enabled: false, external_enabled: false }}
loading={false}
saving={false}
error="加载失败"
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
/>
)
expect(html).toContain('加载失败')
expect(html).not.toContain('模拟运行中')
})
it('renders the WeKnora cards and production channel from live probe and the real production gate', () => {
const probed: MonitorOverview = {
...monitor,
health: [
{
key: 'weknora',
name: 'WeKnora API',
liveness: 'up',
readiness: 'ready',
reason: '',
latency_ms: 86,
observed_at: '2026-09-08T12:00:00Z',
last_success_at: '2026-09-08T11:59:00Z',
consecutive_failures: 0,
configured: true,
applicable: true,
},
{
key: 'weknora-production',
name: '生产 WeKnora',
liveness: 'up',
readiness: 'ready',
reason: '',
latency_ms: 41,
observed_at: '2026-09-08T12:00:00Z',
last_success_at: '2026-09-08T11:59:00Z',
consecutive_failures: 0,
configured: true,
applicable: true,
},
{
key: 'embedding',
name: 'Embedding',
liveness: 'up',
readiness: 'not_applicable',
reason: '未部署:本平台不使用该依赖',
latency_ms: 0,
observed_at: '2026-09-08T12:00:00Z',
last_success_at: null,
consecutive_failures: 0,
configured: false,
applicable: false,
},
],
}
const view = (enabled: boolean, verified: boolean) =>
renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={probed}
settings={settings}
productionEnabled={enabled}
productionBackupRestoreVerified={verified}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="services"
/>
)
const on = view(true, true)
expect(on).toContain('测试 WeKnora')
expect(on).toContain('可达 · 可接单')
expect(on).toContain('延迟 86ms')
expect(on).toContain('已启用 · 可提交生产请求')
expect(on).toContain('生产写入:开启')
expect(on).toContain('发布目标与凭据:管理员受限配置')
const off = view(false, false)
expect(off).toContain('未启用')
expect(off).toContain('生产写入:关闭')
// 卡片状态由真实探测/门禁驱动,写死的“已连接 / 未连接 / 生产映射:未配置”必须消失。
for (const removed of ['本地真实连接', '生产映射:未配置']) expect(off).not.toContain(removed)
// 生产端点可达性来自真实探活,且只说真话:探活不等于验证过接单能力。
expect(on).toContain('端点探测:端点可达 · 未做接单验证')
const blocked = renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={monitor}
settings={settings}
productionEnabled={true}
productionBackupRestoreVerified={false}
routingCapabilities={{ internal_enabled: false, external_enabled: false }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="services"
/>
)
expect(blocked).toContain('已启用 · 被阻断')
// 未配置生产端点时必须显式说「未配置」,不得靠文案含糊过去。
expect(blocked).toContain('端点探测:暂无真实探测结果')
const unconfigured = renderToStaticMarkup(
<SystemSettingsView
sources={sources}
connections={connections}
monitor={{
...monitor,
health: [
{
key: 'weknora-production',
name: '生产 WeKnora',
liveness: 'unknown',
readiness: 'not_configured',
reason: '已部署但未配置探测参数',
latency_ms: 0,
observed_at: '2026-09-08T12:00:00Z',
last_success_at: null,
consecutive_failures: 0,
configured: false,
applicable: false,
},
],
}}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: false, external_enabled: false }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
initialSection="services"
/>
)
expect(unconfigured).toContain('端点探测:未配置')
expect(unconfigured).not.toContain('端点可达')
})
})
describe('SystemSettingsView 数据源配置的默认清单', () => {
const withPP: SourceConnectionListItem[] = [
{
id: 'conn-pp',
source_type: 'pp',
name: 'PP资料下载中心',
enabled: true,
schedule_configured: true,
health_status: 'healthy',
item_count: 613,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
{
id: 'conn-alist',
source_type: 'alist',
name: '国内工业',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 3027,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
{
id: 'conn-ding2',
source_type: 'dingtalk',
name: 'ATOM知识库',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 109,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
]
const view = (role: 'admin' | 'engineer' = 'admin') =>
renderToStaticMarkup(
<AuthContext.Provider value={authValue(role)}>
<SystemSettingsView
sources={[]}
connections={withPP}
monitor={monitor}
settings={settings}
productionEnabled={false}
routingCapabilities={{ internal_enabled: true, external_enabled: true }}
loading={false}
saving={false}
error={null}
saveMessage={null}
onSave={vi.fn()}
onSourcesChanged={vi.fn()}
onRefresh={vi.fn()}
/>
</AuthContext.Provider>
)
it('统一来源清单显示已接入的 PP 和钉钉来源,不依赖目录快照', () => {
const html = view()
expect(html).toContain('PP资料下载中心')
expect(html).toContain('PP 资料下载中心')
for (const name of ['国内工业', 'ATOM知识库']) expect(html).toContain(name)
expect(html).not.toContain('显示 AList')
// 统一入口:一个「+ 添加数据源」向导,不再有按类型分家的 dws 导入面板标题。
expect(html).toContain('+ 添加数据源')
expect(html).not.toContain('PP 导入')
expect(html).not.toContain('去掉来源')
for (const removed of ['端点与凭据', '认证方式', '查看修订历史', '保存并启用调度', '停用来源', '自动刷新周期'])
expect(html).not.toContain(removed)
expect(html).toContain('更多')
expect(html).toContain('aria-expanded="false"')
expect(html).toContain('仅手动刷新')
expect(html).toContain('数据源设置审计')
expect((html.match(/DWS 凭证 ·/g) ?? []).length).toBe(1)
for (const removed of ['+ 新增钉钉知识库', '文件格式筛选']) expect(html).not.toContain(removed)
// 文档工程师看不到添加向导与「去掉来源」:两者都是管理员动作。
const engineer = view('engineer')
expect(engineer).not.toContain('+ 添加数据源')
expect(engineer).not.toContain('去掉来源')
// PP 与钉钉共用同一张清单:工程师看得到卡,只是没有添加与归档。
expect(engineer).toContain('PP资料下载中心')
})
})
describe('ppSourceConnections(数据源配置的清单规则)', () => {
const rows: SourceConnectionListItem[] = [
{
id: 'a',
source_type: 'pp',
name: 'PP资料下载中心',
enabled: true,
schedule_configured: true,
health_status: 'healthy',
item_count: 1,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
{
id: 'b',
source_type: 'alist',
name: '国内工业',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 2,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
{
id: 'c',
source_type: 'dingtalk',
name: 'ATOM知识库',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 3,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
{
id: 'd',
source_type: 'manual',
name: '本地手工验收',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 4,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
{
id: 'e',
source_type: 'pp',
name: 'PP legacy',
enabled: false,
schedule_configured: false,
health_status: 'unknown',
item_count: 5,
updated_at: '2026-09-09T09:30:32Z',
next_refresh_at: null,
},
]
it('只放行 PP 来源,顺序不变,其余类型一条都不放行', () => {
expect(ppSourceConnections(rows).map((item) => item.id)).toEqual(['a', 'e'])
expect(ppSourceConnections([])).toEqual([])
})
})