@mzzsfy/dsh-bug-perf 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ // 会话列表瘦身:session/list 为每个会话下发完整投影快照,列表行只消费其中少数几项。
2
+ // 本模块在 remote 方法返回前按白名单裁剪并缓存结果。任何失败路径都降级为原行为:
3
+ // 未装载、形状不符、原方法抛错一律透传,不影响本插件的其它补丁。
4
+ // sessionController 经 ctx.inject 可选依赖取得,宿主缺该服务时静默等待。
5
+ // 缓存只服务 owner/本地调用:非 owner Remote 主体直接透传原方法,避免跨主体命中共享缓存。
6
+
7
+ // 列表行消费的投影;其余键不下发。被裁掉的投影仍由会话详情接口完整返回。
8
+ export const KEEP_PROJECTIONS = ['title', 'sessionListMetadata', 'inbox', 'modelSelection', 'tokenUsage']
9
+
10
+ // 缓存有效期:会话增删立即失效,TTL 只兜底标题与运行时状态的漂移。
11
+ export const TTL_MS = 15 * 1000
12
+
13
+ // 启动后预热延迟:让宿主先完成装载,再让出一次列表重建。
14
+ export const WARMUP_DELAY_MS = 10 * 1000
15
+
16
+ // 预热等待上限:宿主列表长期不返回时不无限占用共享构建。
17
+ export const WARMUP_TIMEOUT_MS = 2 * 60 * 1000
18
+
19
+ const LOG_PREFIX = '[dsh-bug-perf] 列表瘦身'
20
+ const INSTALL_MARKER = Symbol.for('@mzzsfy/dsh-bug-perf/list-slim')
21
+ const WRAPPED_MARKER = Symbol.for('@mzzsfy/dsh-bug-perf/list-slim/wrapped')
22
+
23
+ /**
24
+ * 当前 receiver 是否来自非 owner Remote 主体。
25
+ * 与宿主 remoteAccess.allows 的判定同源:owner 或非 Remote 调用没有 principal。
26
+ * @param receiver - 列表方法调用时的 this。
27
+ * @returns 非 owner 主体返回 true。
28
+ */
29
+ function isNonOwner(receiver) {
30
+ const principal = receiver?.ctx?.invocation?.peer?.principal
31
+ return principal !== undefined && principal.kind !== 'owner'
32
+ }
33
+
34
+ /**
35
+ * 让一个调用者按自己的 signal 等待共享构建,取消只影响本次等待。
36
+ * @param promise - 共享的列表构建。
37
+ * @param signal - 调用者的取消信号。
38
+ * @returns 本调用者的等待结果。
39
+ */
40
+ function raceWithSignal(promise, signal) {
41
+ if (signal === undefined) return promise
42
+ if (signal.aborted) return Promise.reject(signal.reason ?? new Error('aborted'))
43
+ return new Promise((resolve, reject) => {
44
+ const onAbort = () => reject(signal.reason ?? new Error('aborted'))
45
+ signal.addEventListener('abort', onAbort, { once: true })
46
+ promise.then(
47
+ (value) => {
48
+ signal.removeEventListener('abort', onAbort)
49
+ resolve(value)
50
+ },
51
+ (error) => {
52
+ signal.removeEventListener('abort', onAbort)
53
+ reject(error)
54
+ },
55
+ )
56
+ })
57
+ }
58
+
59
+ /**
60
+ * 裁剪一条列表项的投影,只保留白名单里的键。
61
+ * @param item - 宿主返回的列表项。
62
+ * @param keep - 保留的投影键。
63
+ * @returns 投影已裁剪的列表项;形状不符时原样返回。
64
+ */
65
+ export function slimItem(item, keep = KEEP_PROJECTIONS) {
66
+ const values = item?.projections?.values
67
+ if (values === null || typeof values !== 'object' || Array.isArray(values)) return item
68
+ const picked = {}
69
+ for (const key of keep) if (key in values) picked[key] = values[key]
70
+ return { ...item, projections: { ...item.projections, values: picked } }
71
+ }
72
+
73
+ /**
74
+ * 给 sessionController.list 装上裁剪与缓存。
75
+ * @param ctx - 宿主上下文。
76
+ * @param options - 开关、白名单、缓存有效期与预热延迟。
77
+ * @returns 是否提交了装载回调;sessionController 缺席时回调不执行,宿主行为不变。
78
+ */
79
+ export function installListSlim(ctx, options = {}) {
80
+ const {
81
+ isEnabled,
82
+ keep = KEEP_PROJECTIONS,
83
+ ttlMs = TTL_MS,
84
+ warmupDelayMs = WARMUP_DELAY_MS,
85
+ } = options
86
+ const warn = (message) => ctx.logger?.warn?.(LOG_PREFIX + ' ' + message)
87
+ try {
88
+ ctx.inject(['sessionController'], (scope) => {
89
+ try {
90
+ const controller = scope.sessionController
91
+ const original = controller?.list
92
+ if (typeof original !== 'function') {
93
+ warn('sessionController.list 不可用,跳过')
94
+ return
95
+ }
96
+ if (controller[INSTALL_MARKER] !== undefined && controller.list?.[WRAPPED_MARKER] === true) return
97
+ let cache
98
+ let cachedAt = 0
99
+ let inflight
100
+ let generation = 0
101
+ let active = true
102
+
103
+ const fresh = () => cache !== undefined && Date.now() - cachedAt < ttlMs
104
+ const invalidate = () => {
105
+ generation += 1
106
+ cachedAt = 0
107
+ }
108
+ scope.on('api-session/added', invalidate)
109
+ scope.on('api-session/removed', invalidate)
110
+
111
+ // 每个调用者独立等待共享构建;所有等待者都取消时中止构建,避免挂起被后续调用复用
112
+ const awaitBuild = (entry, signal) => {
113
+ const waiter = {}
114
+ entry.waiters.add(waiter)
115
+ const result = raceWithSignal(entry.promise, signal)
116
+ const settle = () => {
117
+ entry.waiters.delete(waiter)
118
+ if (entry.waiters.size === 0 && inflight === entry) {
119
+ entry.controller.abort()
120
+ inflight = undefined
121
+ }
122
+ }
123
+ result.then(settle, settle)
124
+ return result
125
+ }
126
+
127
+ // 缓存跨调用共享,按 TTL 与会话增删事件失效;非 owner 不读写缓存。
128
+ // 普通函数保留调用方 receiver:宿主在 list 内按 invocation 做逐项鉴权。
129
+ const wrapped = function list(request, signal) {
130
+ const receiver = this
131
+ // 卸载、关闭、已取消或非 owner:不裁剪、不缓存,原样交给宿主
132
+ if (!active || signal?.aborted || (isEnabled !== undefined && !isEnabled()) || isNonOwner(receiver)) {
133
+ return original.call(receiver, request, signal)
134
+ }
135
+ if (fresh()) return cache
136
+ // 共享构建不绑定任何调用者的 signal;调用者取消只结束自己的等待
137
+ if (inflight !== undefined && inflight.generation === generation) return awaitBuild(inflight, signal)
138
+ const current = generation
139
+ const buildController = new AbortController()
140
+ const entry = { promise: undefined, generation: current, controller: buildController, waiters: new Set() }
141
+ const created = Promise.resolve(original.call(receiver, request, buildController.signal))
142
+ .then((value) => {
143
+ if (!Array.isArray(value?.items)) return value
144
+ const slimmed = { items: value.items.map((item) => slimItem(item, keep)) }
145
+ // 在途期间发生增删失效时只返回本次结果,不把旧快照写回缓存
146
+ if (generation === current && inflight === entry) {
147
+ cache = slimmed
148
+ cachedAt = Date.now()
149
+ }
150
+ return slimmed
151
+ })
152
+ .finally(() => {
153
+ if (inflight === entry) inflight = undefined
154
+ })
155
+ entry.promise = created
156
+ inflight = entry
157
+ return awaitBuild(entry, signal)
158
+ }
159
+ Object.defineProperty(controller, INSTALL_MARKER, { value: wrapped, configurable: true })
160
+ Object.defineProperty(wrapped, WRAPPED_MARKER, { value: true })
161
+ try {
162
+ controller.list = wrapped
163
+ } catch (error) {
164
+ if (controller[INSTALL_MARKER] === wrapped) delete controller[INSTALL_MARKER]
165
+ throw error
166
+ }
167
+ let timer
168
+ if (warmupDelayMs >= 0) {
169
+ timer = setTimeout(() => {
170
+ if (isEnabled !== undefined && !isEnabled()) return
171
+ if (fresh() || inflight !== undefined) return
172
+ const timeout = AbortSignal.timeout?.(WARMUP_TIMEOUT_MS)
173
+ void wrapped.call(controller, {}, timeout).catch(() => {})
174
+ }, warmupDelayMs)
175
+ timer.unref?.()
176
+ }
177
+
178
+ // 卸载恢复原方法:按包装标记与归属判断,不摘掉后装或其它实例的包装;
179
+ // 已被后装引用时由 active 失活,链上继续透传;在途构建一并中止
180
+ const dispose = () => {
181
+ active = false
182
+ if (timer !== undefined) clearTimeout(timer)
183
+ if (inflight !== undefined) {
184
+ inflight.controller.abort()
185
+ inflight = undefined
186
+ }
187
+ if (controller[INSTALL_MARKER] === wrapped && controller.list?.[WRAPPED_MARKER] === true) controller.list = original
188
+ if (controller[INSTALL_MARKER] === wrapped) delete controller[INSTALL_MARKER]
189
+ }
190
+ try {
191
+ scope.effect(() => dispose)
192
+ } catch (error) {
193
+ // effect 注册失败时回滚包装与标记,避免留下无法卸载的包装
194
+ dispose()
195
+ throw error
196
+ }
197
+ ctx.logger?.info?.(`${LOG_PREFIX} 已装载 (keep=${keep.join(',')} ttl=${ttlMs}ms)`)
198
+ } catch (error) {
199
+ warn('装载失败,保持原行为: ' + String(error?.message ?? error))
200
+ }
201
+ })
202
+ return true
203
+ } catch (error) {
204
+ warn('未启用: ' + String(error?.message ?? error))
205
+ return false
206
+ }
207
+ }
@@ -0,0 +1,165 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ const source = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
6
+
7
+ function loadPlugin() {
8
+ let registered
9
+ const window = { __ModuleLoader__: { load: (entry) => { registered = entry } } }
10
+ new Function('window', source)(window)
11
+ return registered.factory(() => { throw new Error('no modules') })
12
+ }
13
+
14
+ function fakeClock() {
15
+ let now = 0
16
+ let nextId = 0
17
+ const intervals = new Map()
18
+ return {
19
+ now: () => now,
20
+ setInterval: (fn, ms) => { const id = ++nextId; intervals.set(id, { fn, every: ms, at: now + ms }); return id },
21
+ clearInterval: (id) => { intervals.delete(id) },
22
+ advance(ms) {
23
+ const target = now + ms
24
+ while (true) {
25
+ const due = [...intervals.values()].map((timer) => timer.at).filter((at) => at <= target)
26
+ if (due.length === 0) break
27
+ now = Math.min(...due)
28
+ for (const timer of intervals.values()) {
29
+ if (timer.at <= now) {
30
+ timer.at = now + timer.every
31
+ timer.fn()
32
+ }
33
+ }
34
+ }
35
+ now = target
36
+ },
37
+ intervalCount: () => intervals.size,
38
+ }
39
+ }
40
+
41
+ function fakeWindow(clock) {
42
+ const fetchCalls = []
43
+ const pendingProbes = []
44
+ const win = {
45
+ location: { pathname: '/chat' },
46
+ performance: { memory: { usedJSHeapSize: 64 * 1024 * 1024 } },
47
+ document: { querySelectorAll: () => ({ length: 42 }) },
48
+ fetch: (url, options) => {
49
+ fetchCalls.push({ url, options })
50
+ return Promise.resolve({ ok: true })
51
+ },
52
+ setTimeout: (fn) => { pendingProbes.push({ fn, startedAt: clock.now() }); return pendingProbes.length },
53
+ addEventListener: () => {},
54
+ }
55
+ return { win, fetchCalls, pendingProbes }
56
+ }
57
+
58
+ // Given 默认关闭, When 采样运行五分钟, Then 不发任何请求, 采样与阻塞探测仍工作
59
+ test('诊断_默认关闭_只采样不上报', () => {
60
+ const plugin = loadPlugin()
61
+ const clock = fakeClock()
62
+ const { win, fetchCalls, pendingProbes } = fakeWindow(clock)
63
+ const diag = plugin.__test.createDiag(clock)
64
+ plugin.__test.startDiagSampling({ win, clock, diag, doc: win.document })
65
+ const reporting = plugin.__test.startDiagReporting({
66
+ win,
67
+ clock,
68
+ diag,
69
+ options: { enabled: false, reportEvery: 12 },
70
+ })
71
+ clock.advance(5 * 60 * 1000)
72
+ assert.equal(reporting.enabled, false)
73
+ assert.equal(diag.enabled, false)
74
+ assert.equal(fetchCalls.length, 0)
75
+ assert.ok(diag.samples.length >= 60)
76
+ clock.advance(plugin.__test.DIAG_PROBE_MS)
77
+ const probe = pendingProbes.shift()
78
+ assert.equal(typeof probe?.fn, 'function')
79
+ clock.advance(plugin.__test.DIAG_BLOCK_MS + 1)
80
+ probe.fn()
81
+ assert.equal(typeof diag.blockedAt, 'number')
82
+ assert.ok(diag.blockedMs >= plugin.__test.DIAG_BLOCK_MS)
83
+ })
84
+
85
+ // Given 开启, When 推进一个上报周期, Then 恰好一次请求且 payload 字段完整
86
+ test('诊断_开启_按周期上报', () => {
87
+ const plugin = loadPlugin()
88
+ const clock = fakeClock()
89
+ const { win, fetchCalls } = fakeWindow(clock)
90
+ const diag = plugin.__test.createDiag(clock)
91
+ plugin.__test.startDiagSampling({ win, clock, diag, doc: win.document })
92
+ const reporting = plugin.__test.startDiagReporting({ win, clock, diag, options: { enabled: true, reportEvery: 12 } })
93
+ assert.equal(reporting.enabled, true)
94
+ assert.equal(diag.enabled, true)
95
+ clock.advance(12 * plugin.__test.DIAG_SAMPLE_MS - 1)
96
+ assert.equal(fetchCalls.length, 0)
97
+ clock.advance(1)
98
+ assert.equal(fetchCalls.length, 1)
99
+ const { url, options } = fetchCalls[0]
100
+ assert.ok(url.startsWith('/api/dsh-bug-perf/diag?d='))
101
+ assert.equal(options.keepalive, true)
102
+ const payload = JSON.parse(decodeURIComponent(url.slice(url.indexOf('?d=') + 3)))
103
+ assert.equal(typeof payload.at, 'number')
104
+ assert.equal(payload.path, '/chat')
105
+ assert.equal(typeof payload.totals.opened, 'number')
106
+ assert.ok(Array.isArray(payload.last))
107
+ assert.ok(Array.isArray(payload.longTasks))
108
+ assert.ok(Array.isArray(payload.errors))
109
+ assert.equal('blockedMs' in payload, true)
110
+ assert.ok(payload.last.length > 0)
111
+ })
112
+
113
+ // Given reportEvery 改小, When 推进两个周期, Then 上报次数随周期增加
114
+ test('诊断_上报间隔_随配置变化', () => {
115
+ const plugin = loadPlugin()
116
+ const clock = fakeClock()
117
+ const { win, fetchCalls } = fakeWindow(clock)
118
+ const diag = plugin.__test.createDiag(clock)
119
+ plugin.__test.startDiagSampling({ win, clock, diag, doc: win.document })
120
+ plugin.__test.startDiagReporting({ win, clock, diag, options: { enabled: true, reportEvery: 2 } })
121
+ clock.advance(2 * plugin.__test.DIAG_SAMPLE_MS)
122
+ assert.equal(fetchCalls.length, 1)
123
+ clock.advance(2 * plugin.__test.DIAG_SAMPLE_MS)
124
+ assert.equal(fetchCalls.length, 2)
125
+ })
126
+
127
+ // Given fetch 缺失或抛错, When 到达上报点, Then 静默跳过且不抛
128
+ test('诊断_上报失败_静默', () => {
129
+ const plugin = loadPlugin()
130
+ const clock = fakeClock()
131
+ const missing = fakeWindow(clock)
132
+ delete missing.win.fetch
133
+ const missingDiag = plugin.__test.createDiag(clock)
134
+ plugin.__test.startDiagSampling({ win: missing.win, clock, diag: missingDiag, doc: missing.win.document })
135
+ plugin.__test.startDiagReporting({ win: missing.win, clock, diag: missingDiag, options: { enabled: true, reportEvery: 1 } })
136
+ assert.doesNotThrow(() => clock.advance(plugin.__test.DIAG_SAMPLE_MS))
137
+
138
+ const throwing = fakeWindow(clock)
139
+ throwing.win.fetch = () => { throw new Error('fetch boom') }
140
+ const throwingDiag = plugin.__test.createDiag(clock)
141
+ plugin.__test.startDiagSampling({ win: throwing.win, clock, diag: throwingDiag, doc: throwing.win.document })
142
+ plugin.__test.startDiagReporting({ win: throwing.win, clock, diag: throwingDiag, options: { enabled: true, reportEvery: 1 } })
143
+ assert.doesNotThrow(() => clock.advance(plugin.__test.DIAG_SAMPLE_MS))
144
+ })
145
+
146
+ // Given fetch 返回拒绝或非 2xx, When 到达上报点, Then 不产生未处理异常
147
+ test('诊断_上报异步失败_静默', async () => {
148
+ const plugin = loadPlugin()
149
+ const clock = fakeClock()
150
+ const rejecting = fakeWindow(clock)
151
+ rejecting.win.fetch = () => Promise.reject(new Error('fetch rejected'))
152
+ const rejectingDiag = plugin.__test.createDiag(clock)
153
+ plugin.__test.startDiagSampling({ win: rejecting.win, clock, diag: rejectingDiag, doc: rejecting.win.document })
154
+ plugin.__test.startDiagReporting({ win: rejecting.win, clock, diag: rejectingDiag, options: { enabled: true, reportEvery: 1 } })
155
+ assert.doesNotThrow(() => clock.advance(plugin.__test.DIAG_SAMPLE_MS))
156
+ await Promise.resolve()
157
+
158
+ const notOk = fakeWindow(clock)
159
+ notOk.win.fetch = () => Promise.resolve({ ok: false })
160
+ const notOkDiag = plugin.__test.createDiag(clock)
161
+ plugin.__test.startDiagSampling({ win: notOk.win, clock, diag: notOkDiag, doc: notOk.win.document })
162
+ plugin.__test.startDiagReporting({ win: notOk.win, clock, diag: notOkDiag, options: { enabled: true, reportEvery: 1 } })
163
+ assert.doesNotThrow(() => clock.advance(plugin.__test.DIAG_SAMPLE_MS))
164
+ await Promise.resolve()
165
+ })
@@ -127,7 +127,14 @@ test('配置_客户端内联_与宿主同源', async () => {
127
127
  const plugin = loadPlugin()
128
128
  const host = await import('../src/config.mjs')
129
129
  assert.deepEqual(plugin.__test.DEFAULT_CONFIG, host.DEFAULT_CONFIG)
130
+ assert.deepEqual(plugin.__test.LIMITS, host.LIMITS)
130
131
  assert.deepEqual(plugin.__test.normalizeConfig({}), host.normalizeConfig({}))
132
+ const raw = { diag: { enabled: true, reportEvery: 0 } }
133
+ assert.deepEqual(plugin.__test.normalizeConfig(raw), host.normalizeConfig(raw))
134
+ assert.deepEqual(
135
+ plugin.__test.normalizeConfig({ diag: { reportEvery: 99999 } }),
136
+ host.normalizeConfig({ diag: { reportEvery: 99999 } }),
137
+ )
131
138
  })
132
139
 
133
140
  // Given 远程配置缺失, When 客户端归一化, Then 落在与宿主一致的默认值上
@@ -0,0 +1,76 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ const source = readFileSync(new URL('../src/client.js', import.meta.url), 'utf8')
6
+
7
+ function loadPlugin({ modules = {} } = {}) {
8
+ let registered
9
+ const window = { __ModuleLoader__: { load: (entry) => { registered = entry } } }
10
+ new Function('window', source)(window)
11
+ const requireImpl = (id) => {
12
+ if (Object.hasOwn(modules, id)) return modules[id]
13
+ throw new Error(`module not registered: ${id}`)
14
+ }
15
+ return registered.factory(requireImpl)
16
+ }
17
+
18
+ const reactStub = {
19
+ createElement: (type, props, ...children) => ({ type, props, children }),
20
+ useState: () => [undefined, () => {}],
21
+ useEffect: () => {},
22
+ }
23
+
24
+ // Given React 与 slots 就绪, When 注册设置 UI, Then 两个官方插槽都登记且 key 正确
25
+ test('设置UI_注册官方插槽', () => {
26
+ const plugin = loadPlugin({ modules: { react: reactStub } })
27
+ const registrations = []
28
+ const injected = []
29
+ const styleEffects = []
30
+ const scope = {
31
+ slots: {
32
+ inject: (name, callback) => { injected.push(name); callback() },
33
+ register: (options, component) => registrations.push({ options, component }),
34
+ },
35
+ effect: (factory) => { styleEffects.push(factory) },
36
+ }
37
+ const ctx = { inject: (services, callback) => { if (services.includes('slots')) callback(scope) } }
38
+ plugin.__test.registerSettingsUi(ctx)
39
+ assert.deepEqual(injected.sort(), ['plugins.bundle.config', 'settings.plugin.item'])
40
+ const byName = Object.fromEntries(registrations.map((entry) => [entry.options.name, entry.options]))
41
+ assert.equal(byName['settings.plugin.item'].key, 'bug-perf')
42
+ assert.equal(byName['plugins.bundle.config'].key, '@mzzsfy/dsh-bug-perf')
43
+ assert.equal(styleEffects.length, 1)
44
+ })
45
+
46
+ // Given 无 React 或 slots, When 注册, Then 静默跳过,不影响其它补丁
47
+ test('设置UI_缺依赖_静默跳过', () => {
48
+ const noReact = loadPlugin()
49
+ let called = false
50
+ noReact.__test.registerSettingsUi({ inject: () => { called = true } })
51
+ assert.equal(called, false)
52
+ const plugin = loadPlugin({ modules: { react: reactStub } })
53
+ assert.doesNotThrow(() => plugin.__test.registerSettingsUi({}))
54
+ })
55
+
56
+ // Given 设置节定义, When 检查字段, Then 各节与共享配置的键一致
57
+ test('设置UI_节字段与配置同源', () => {
58
+ const plugin = loadPlugin({ modules: { react: reactStub } })
59
+ const keys = plugin.__test.UI_SECTIONS.map((section) => section.key)
60
+ assert.deepEqual(keys, Object.keys(plugin.__test.DEFAULT_CONFIG))
61
+ for (const section of plugin.__test.UI_SECTIONS) {
62
+ for (const field of section.fields) {
63
+ assert.equal(typeof plugin.__test.DEFAULT_CONFIG[section.key][field.name], 'number')
64
+ }
65
+ }
66
+ })
67
+
68
+ // Given 客户端注入设置样式, When 检查注入契约, Then data-plugin 与样式创建点数一致且开关选择器锚定 checkbox
69
+ test('设置UI_样式注入契约', () => {
70
+ const creates = (source.match(/createElement\('style'\)/g) ?? []).length
71
+ const markers = (source.match(/style\.setAttribute\('data-plugin'/g) ?? []).length
72
+ assert.equal(creates, 2)
73
+ assert.equal(markers, creates)
74
+ assert.match(source, /input\[type="checkbox"\]:checked \+ \.bp-switch__track/)
75
+ assert.match(source, /input\[type="checkbox"\]:focus-visible \+ \.bp-switch__track/)
76
+ })
@@ -2,8 +2,8 @@ import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { DEFAULT_CONFIG, normalizeConfig } from '../src/config.mjs'
4
4
 
5
- // Given 无配置, When 归一化, Then 两个功能默认开启且阈值取默认
6
- test('配置_缺省_默认全开', () => {
5
+ // Given 无配置, When 归一化, Then 各补丁默认开启(诊断上报默认关闭)且阈值取默认
6
+ test('配置_缺省_功能默认值与阈值', () => {
7
7
  const config = normalizeConfig(undefined)
8
8
  assert.deepEqual(config, DEFAULT_CONFIG)
9
9
  assert.equal(config.restartCoalesce.enabled, true)
@@ -116,3 +116,30 @@ test('配置_任务流上限_开关与回退', () => {
116
116
  const inverted = normalizeConfig({ jobStreamLimit: { base: 200, max: 10 } })
117
117
  assert.equal(inverted.jobStreamLimit.base, 10)
118
118
  })
119
+
120
+ // Given 列表瘦身缺省或关闭, When 归一化, Then 默认开启、显式关闭保留、非法类型回退默认
121
+ test('配置_列表瘦身_开关与回退', () => {
122
+ assert.deepEqual(normalizeConfig(undefined).listSlim, { enabled: true })
123
+ assert.equal(normalizeConfig({ listSlim: { enabled: false } }).listSlim.enabled, false)
124
+ assert.equal(normalizeConfig({ listSlim: { enabled: 'off' } }).listSlim.enabled, true)
125
+ })
126
+
127
+ // Given 诊断上报缺省, When 归一化, Then 默认关闭且上报间隔取默认
128
+ test('配置_诊断上报_默认关闭', () => {
129
+ assert.deepEqual(normalizeConfig(undefined).diag, { enabled: false, reportEvery: 12 })
130
+ })
131
+
132
+ // Given 诊断上报显式开关与非法值, When 归一化, Then 开关保留、非法类型回退、间隔越界收敛、未知字段丢弃
133
+ test('配置_诊断上报_开关边界', () => {
134
+ const off = normalizeConfig({ diag: { enabled: false, reportEvery: 5 } })
135
+ assert.equal(off.diag.enabled, false)
136
+ assert.equal(off.diag.reportEvery, 5)
137
+ const on = normalizeConfig({ diag: { enabled: true } })
138
+ assert.equal(on.diag.enabled, true)
139
+ const bad = normalizeConfig({ diag: { enabled: 'yes', reportEvery: 'often' } })
140
+ assert.equal(bad.diag.enabled, false)
141
+ assert.equal(bad.diag.reportEvery, 12)
142
+ assert.equal(normalizeConfig({ diag: { reportEvery: 0 } }).diag.reportEvery, 1)
143
+ assert.equal(normalizeConfig({ diag: { reportEvery: 99999 } }).diag.reportEvery, 720)
144
+ assert.deepEqual(normalizeConfig({ diag: { reportEvery: 3, unknown: 1 } }).diag, { enabled: false, reportEvery: 3 })
145
+ })