@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.
@@ -13,16 +13,36 @@ function makeRes() {
13
13
  }
14
14
  }
15
15
 
16
- function makeCtx({ server, registerError } = {}) {
16
+ function makeCtx({ server, registerError, sessionController, settings, configEditor, fiberEntry } = {}) {
17
17
  const registrations = []
18
18
  const disposed = []
19
19
  const ctx = {
20
- get: (service) => (service === 'webServer' ? server : undefined),
20
+ fiber: { entry: fiberEntry },
21
+ get: (service) => {
22
+ if (service === 'webServer') return server
23
+ if (service === 'sessionController') return sessionController
24
+ if (service === 'settings') return settings
25
+ if (service === 'configEditor') return configEditor
26
+ return undefined
27
+ },
21
28
  effect: (factory) => {
22
29
  const dispose = factory()
23
30
  ctx.effects.push(dispose)
24
31
  return dispose
25
32
  },
33
+ inject: (services, callback) => {
34
+ const scope = {
35
+ get: ctx.get,
36
+ get sessionController() { return sessionController },
37
+ get settings() { return settings },
38
+ on: () => {},
39
+ effect: (factory) => {
40
+ ctx.effects.push(factory())
41
+ },
42
+ }
43
+ if (services.includes('sessionController') && sessionController !== undefined) callback(scope)
44
+ if (services.includes('settings') && settings !== undefined) callback(scope)
45
+ },
26
46
  effects: [],
27
47
  registrations,
28
48
  disposed,
@@ -55,7 +75,7 @@ test('宿主_注入声明_等待webServer', async () => {
55
75
  assert.deepEqual(index.inject, ['webServer'])
56
76
  })
57
77
 
58
- // Given 宿主提供 webServer, When 插件激活, Then 注册只读配置路由且路径固定
78
+ // Given 宿主提供 webServer, When 插件激活, Then 注册配置读写路由且路径固定
59
79
  test('宿主_激活_注册配置路由', () => {
60
80
  const ctx = makeCtx({ server: {} })
61
81
  apply(ctx, undefined)
@@ -86,6 +106,124 @@ test('宿主_部分覆盖_返回归一化结果', async () => {
86
106
  assert.equal(json.config.terminalWatchdog.stallMs, 12000)
87
107
  })
88
108
 
109
+ async function postConfig(route, body) {
110
+ const res = makeRes()
111
+ const payload = typeof body === 'string' ? body : JSON.stringify(body)
112
+ const req = {
113
+ url: '/api/dsh-bug-perf/config',
114
+ method: 'POST',
115
+ on(event, callback) {
116
+ if (event === 'data') callback(Buffer.from(payload))
117
+ else if (event === 'end') callback()
118
+ },
119
+ destroy() {},
120
+ }
121
+ await route.handler(req, res)
122
+ return res
123
+ }
124
+
125
+ // Given 旧宿主 settings 命名空间可用, When POST 配置, Then 归一化后持久化并可读回
126
+ test('宿主_配置保存_settings持久化', async () => {
127
+ const settings = {
128
+ stored: undefined,
129
+ registered: undefined,
130
+ register(namespace, schema, options) { this.registered = { namespace, schema, options } },
131
+ get() { return this.stored },
132
+ async update(_namespace, patch) { this.stored = { ...(this.stored ?? {}), ...patch } },
133
+ }
134
+ const ctx = makeCtx({ server: {}, settings })
135
+ apply(ctx, undefined)
136
+ assert.equal(settings.registered.namespace, 'bug-perf')
137
+ const res = await postConfig(ctx.registrations[0], { config: { listSlim: { enabled: false } } })
138
+ assert.equal(res.status, 200)
139
+ const saved = JSON.parse(res.body).config
140
+ assert.equal(saved.listSlim.enabled, false)
141
+ assert.equal(settings.stored.listSlim.enabled, false)
142
+ const read = JSON.parse((await request(ctx.registrations[0])).body)
143
+ assert.equal(read.config.listSlim.enabled, false)
144
+ })
145
+
146
+ // Given legacy settings 只存 diag, When 读取配置, Then diag 节也参与已存判断
147
+ test('宿主_配置读取_settings仅diag_生效', async () => {
148
+ const settings = {
149
+ stored: { diag: { enabled: true, reportEvery: 2 } },
150
+ register() {},
151
+ get() { return this.stored },
152
+ async update() {},
153
+ }
154
+ const ctx = makeCtx({ server: {}, settings })
155
+ apply(ctx, undefined)
156
+ const read = JSON.parse((await request(ctx.registrations[0])).body)
157
+ assert.deepEqual(read.config.diag, { enabled: true, reportEvery: 2 })
158
+ })
159
+
160
+ // Given 保存值越界, When POST, Then 上限收敛后再落盘
161
+ test('宿主_配置保存_数值收敛', async () => {
162
+ const settings = {
163
+ stored: undefined,
164
+ register() {},
165
+ get() { return this.stored },
166
+ async update(_namespace, patch) { this.stored = patch },
167
+ }
168
+ const ctx = makeCtx({ server: {}, settings })
169
+ apply(ctx, undefined)
170
+ const res = await postConfig(ctx.registrations[0], { config: { terminalWatchdog: { stallMs: 99999999 } } })
171
+ assert.equal(JSON.parse(res.body).config.terminalWatchdog.stallMs, 600000)
172
+ assert.equal(settings.stored.terminalWatchdog.stallMs, 600000)
173
+ })
174
+
175
+ // Given 0.1.7 行配置编辑器可用, When POST, Then 通过 editor.edit 写当前 fiber 条目
176
+ test('宿主_配置保存_行配置编辑器', async () => {
177
+ const edits = []
178
+ const configEditor = {
179
+ async edit(entry, update) {
180
+ const next = update({ restartCoalesce: { enabled: true, windowMs: 100 } })
181
+ edits.push({ entry, next })
182
+ },
183
+ }
184
+ const entry = { id: 'dsh-bug-perf' }
185
+ const ctx = makeCtx({ server: {}, configEditor, fiberEntry: entry })
186
+ apply(ctx, undefined)
187
+ const res = await postConfig(ctx.registrations[0], { config: { restartCoalesce: { windowMs: 250 } } })
188
+ assert.equal(res.status, 200)
189
+ assert.equal(edits.length, 1)
190
+ assert.equal(edits[0].entry, entry)
191
+ assert.equal(edits[0].next.restartCoalesce.windowMs, 250)
192
+ })
193
+
194
+ // Given 坏 JSON 或非法 config, When POST, Then 400 且不写
195
+ test('宿主_配置保存_拒绝坏载荷', async () => {
196
+ let updates = 0
197
+ const settings = {
198
+ register() {},
199
+ get() { return undefined },
200
+ async update() { updates += 1 },
201
+ }
202
+ const ctx = makeCtx({ server: {}, settings })
203
+ apply(ctx, undefined)
204
+ assert.equal((await postConfig(ctx.registrations[0], 'not-json')).status, 400)
205
+ assert.equal((await postConfig(ctx.registrations[0], { config: null })).status, 400)
206
+ assert.equal((await postConfig(ctx.registrations[0], { config: [] })).status, 400)
207
+ assert.equal(updates, 0)
208
+ })
209
+
210
+ // Given 宿主没有 settings 与 configEditor, When POST, Then 503 且不抛
211
+ test('宿主_配置保存_服务缺失返回503', async () => {
212
+ const ctx = makeCtx({ server: {} })
213
+ apply(ctx, undefined)
214
+ const res = await postConfig(ctx.registrations[0], { config: { listSlim: { enabled: false } } })
215
+ assert.equal(res.status, 503)
216
+ })
217
+
218
+ // Given 非 GET/POST 方法, When 请求配置路由, Then 405 且不读不写
219
+ test('宿主_配置路由_拒绝未知方法', async () => {
220
+ const ctx = makeCtx({ server: {} })
221
+ apply(ctx, undefined)
222
+ const res = makeRes()
223
+ await ctx.registrations[0].handler({ url: '/api/dsh-bug-perf/config', method: 'DELETE' }, res)
224
+ assert.equal(res.status, 405)
225
+ })
226
+
89
227
  // Given 0.1.7 宿主把整节配置包成 volatile ref, When 读取, Then 解箱后返回
90
228
  test('宿主_volatile配置_解箱', async () => {
91
229
  const ctx = makeCtx({ server: {} })
@@ -119,9 +257,70 @@ test('宿主_Config默认值_与共享默认一致', () => {
119
257
  assert.equal(watchdog.checkMs, DEFAULT_CONFIG.terminalWatchdog.checkMs)
120
258
  assert.equal(watchdog.stallMs, DEFAULT_CONFIG.terminalWatchdog.stallMs)
121
259
  assert.equal(watchdog.cooldownMs, DEFAULT_CONFIG.terminalWatchdog.cooldownMs)
260
+ assert.deepEqual(Config.dict.listSlim({}), DEFAULT_CONFIG.listSlim)
261
+ assert.deepEqual(Config.dict.diag({}), DEFAULT_CONFIG.diag)
262
+ assert.equal(Config.dict.diag({}).enabled, false)
263
+ })
264
+
265
+ // Given sessionController 就绪, When 插件激活并调用列表接口, Then 投影按白名单裁剪且第二次命中缓存
266
+ test('宿主_列表瘦身_按配置接管列表接口', async () => {
267
+ let built = 0
268
+ const controller = {
269
+ list: async () => {
270
+ built += 1
271
+ return { items: [{ sessionId: 'a', projections: { kind: 'cached', values: { title: 'A', contextHeaders: [1] } } }] }
272
+ },
273
+ }
274
+ const ctx = makeCtx({ server: {}, sessionController: controller })
275
+ apply(ctx, undefined)
276
+ const first = await controller.list({}, undefined)
277
+ const second = await controller.list({}, undefined)
278
+ assert.equal(built, 1)
279
+ assert.deepEqual(Object.keys(first.items[0].projections.values), ['title'])
280
+ assert.equal(second, first)
281
+ })
282
+
283
+ // Given 行配置关闭列表瘦身, When 调用列表接口, Then 原样透传且不缓存
284
+ test('宿主_列表瘦身_关闭时原样透传', async () => {
285
+ let built = 0
286
+ const controller = {
287
+ list: async () => {
288
+ built += 1
289
+ return { items: [{ sessionId: 'a', projections: { values: { contextHeaders: [1] } } }] }
290
+ },
291
+ }
292
+ const ctx = makeCtx({ server: {}, sessionController: controller })
293
+ apply(ctx, { listSlim: { enabled: false } })
294
+ await controller.list({}, undefined)
295
+ const value = await controller.list({}, undefined)
296
+ assert.equal(built, 2)
297
+ assert.deepEqual(value.items[0].projections.values, { contextHeaders: [1] })
298
+ })
299
+
300
+ // Given 0.1.7 live ref 热更新行配置, When 连续切换 listSlim 开关, Then 每次调用按当前值决定裁剪或透传
301
+ test('宿主_列表瘦身_热切换开关即时生效', async () => {
302
+ let current = { listSlim: { enabled: false } }
303
+ let built = 0
304
+ const controller = {
305
+ list: async () => {
306
+ built += 1
307
+ return { items: [{ sessionId: 'a', projections: { values: { title: 'A', contextHeaders: [1] } } }] }
308
+ },
309
+ }
310
+ const ctx = makeCtx({ server: {}, sessionController: controller })
311
+ apply(ctx, { get: () => current })
312
+ const off = await controller.list({}, undefined)
313
+ assert.deepEqual(off.items[0].projections.values.contextHeaders, [1], '关闭时透传完整投影')
314
+ current = { listSlim: { enabled: true } }
315
+ const on = await controller.list({}, undefined)
316
+ assert.deepEqual(Object.keys(on.items[0].projections.values), ['title'], '开启后裁剪')
317
+ current = { listSlim: { enabled: false } }
318
+ const offAgain = await controller.list({}, undefined)
319
+ assert.deepEqual(offAgain.items[0].projections.values.contextHeaders, [1], '再次关闭恢复完整投影')
320
+ assert.equal(built, 3)
122
321
  })
123
322
 
124
- // Given 浏览器上报采样, When 宿主收到合法载荷, Then 返回 ok 并落盘
323
+ // Given diag 开关默认关闭, When 浏览器仍上报合法载荷, Then 宿主不按开关拒绝, 返回 ok 并落盘
125
324
  test('宿主_采样上报_接受合法载荷', async () => {
126
325
  const ctx = makeCtx({ server: {} })
127
326
  apply(ctx, undefined)
@@ -0,0 +1,421 @@
1
+ // 列表瘦身的契约:只裁白名单、命中缓存不重建、增删事件失效、任何异常回落到原行为。
2
+ // 安全面:包装必须保留调用方 receiver(宿主在 list 内按 invocation 逐项鉴权),
3
+ // 缓存只服务 owner/本地调用,非 owner 主体不得命中共享缓存。
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { installListSlim, slimItem } from '../src/list-slim.mjs'
7
+
8
+ /** 一条带完整投影的列表项。 */
9
+ const item = (id, values) => ({ sessionId: id, updatedAt: 1, projections: { kind: 'cached', asOfSeq: 1, values } })
10
+
11
+ /** 造一个只实现本模块用到表面的上下文(缺服务时不回调,模拟 cordis inject 门控)。 */
12
+ function makeContext(list, { withController = true, controller: given, effectError } = {}) {
13
+ const handlers = new Map()
14
+ const disposers = []
15
+ const logs = { info: [], warn: [] }
16
+ const controller = withController ? (given ?? { ctx: {}, list }) : undefined
17
+ const ctx = {
18
+ get: (name) => (name === 'sessionController' ? controller : undefined),
19
+ on: (event, handler) => handlers.set(event, handler),
20
+ logger: {
21
+ info: (message) => logs.info.push(message),
22
+ warn: (message) => logs.warn.push(message),
23
+ },
24
+ inject: (deps, callback) => {
25
+ if (controller === undefined) return
26
+ callback({
27
+ sessionController: controller,
28
+ on: (event, handler) => handlers.set(event, handler),
29
+ effect: (factory) => {
30
+ if (effectError !== undefined) throw effectError
31
+ disposers.push(factory())
32
+ },
33
+ })
34
+ },
35
+ }
36
+ return { ctx, controller, handlers, disposers, logs }
37
+ }
38
+
39
+ const sample = () => [
40
+ item('a', { title: 'A', sessionListMetadata: { blank: false }, contextHeaders: [{ pad: 'x'.repeat(64) }] }),
41
+ item('b', { title: 'B', inbox: { 'next-turn': [], 'next-step': [] } }),
42
+ ]
43
+
44
+ /** 模拟宿主 service proxy 的 receiver:invocation 决定 remoteAccess 的判定。 */
45
+ const receiverOf = (principal) => ({ ctx: { invocation: principal === undefined ? undefined : { peer: { principal } } } })
46
+
47
+ test('裁剪只保留白名单里的投影', () => {
48
+ const slim = slimItem(sample()[0], ['title'])
49
+ assert.deepEqual(Object.keys(slim.projections.values), ['title'])
50
+ assert.equal(slim.projections.kind, 'cached')
51
+ assert.equal(slim.sessionId, 'a')
52
+ })
53
+
54
+ test('形状不符的条目原样返回', () => {
55
+ const bare = { sessionId: 'c' }
56
+ assert.equal(slimItem(bare, ['title']), bare)
57
+ assert.equal(slimItem({ projections: { values: null } }, ['title']).projections.values, null)
58
+ const array = { projections: { values: ['x'] } }
59
+ assert.equal(slimItem(array, ['title']), array)
60
+ })
61
+
62
+ test('开关关闭时包装保留,但每次调用透传原方法', async () => {
63
+ const original = async () => ({ items: sample() })
64
+ const { ctx, controller } = makeContext(original)
65
+ installListSlim(ctx, { isEnabled: () => false, warmupDelayMs: -1 })
66
+ assert.notEqual(controller.list, original)
67
+ const value = await controller.list({}, undefined)
68
+ assert.equal(value.items[0].projections.values.contextHeaders[0].pad.length, 64)
69
+ })
70
+
71
+ // Given 宿主没有 sessionController, When 装载, Then inject 不回调、不接管、不抛错
72
+ test('宿主没有 sessionController 时静默等待', () => {
73
+ const { ctx, logs } = makeContext(undefined, { withController: false })
74
+ assert.equal(installListSlim(ctx, { warmupDelayMs: -1 }), true)
75
+ assert.deepEqual(logs.warn, [])
76
+ })
77
+
78
+ // Given 服务在场但没有 list 方法, When 装载, Then 记录原因并放弃
79
+ test('controller 缺 list 方法时记录原因', () => {
80
+ const { ctx, logs } = makeContext(undefined, { controller: { ctx: {} } })
81
+ installListSlim(ctx, { warmupDelayMs: -1 })
82
+ assert.ok(logs.warn.some((message) => message.includes('list 不可用')))
83
+ })
84
+
85
+ test('第二次调用命中缓存,不重建', async () => {
86
+ let built = 0
87
+ const { ctx, controller } = makeContext(async () => {
88
+ built += 1
89
+ return { items: sample() }
90
+ })
91
+ installListSlim(ctx, { warmupDelayMs: -1 })
92
+ await controller.list({}, undefined)
93
+ const value = await controller.list({}, undefined)
94
+ assert.equal(built, 1)
95
+ assert.equal(value.items[0].projections.values.contextHeaders, undefined)
96
+ })
97
+
98
+ test('过期后重建', async () => {
99
+ let built = 0
100
+ const { ctx, controller } = makeContext(async () => {
101
+ built += 1
102
+ return { items: sample() }
103
+ })
104
+ installListSlim(ctx, { ttlMs: 0, warmupDelayMs: -1 })
105
+ await controller.list({}, undefined)
106
+ await controller.list({}, undefined)
107
+ assert.equal(built, 2)
108
+ })
109
+
110
+ test('会话增删让缓存失效', async () => {
111
+ let built = 0
112
+ const { ctx, controller, handlers } = makeContext(async () => {
113
+ built += 1
114
+ return { items: sample() }
115
+ })
116
+ installListSlim(ctx, { warmupDelayMs: -1 })
117
+ await controller.list({}, undefined)
118
+ handlers.get('api-session/added')()
119
+ await controller.list({}, undefined)
120
+ assert.equal(built, 2)
121
+ handlers.get('api-session/removed')()
122
+ await controller.list({}, undefined)
123
+ assert.equal(built, 3)
124
+ })
125
+
126
+ test('并发请求只触发一次重建', async () => {
127
+ let built = 0
128
+ const { ctx, controller } = makeContext(async () => {
129
+ built += 1
130
+ return { items: sample() }
131
+ })
132
+ installListSlim(ctx, { warmupDelayMs: -1 })
133
+ await Promise.all([controller.list({}, undefined), controller.list({}, undefined), controller.list({}, undefined)])
134
+ assert.equal(built, 1)
135
+ })
136
+
137
+ test('不同 signal 的并发调用共享一次构建,取消互不影响', async () => {
138
+ let built = 0
139
+ let release
140
+ const { ctx, controller } = makeContext(() => {
141
+ built += 1
142
+ return new Promise((resolve) => {
143
+ release = () => resolve({ items: sample() })
144
+ })
145
+ })
146
+ installListSlim(ctx, { warmupDelayMs: -1 })
147
+ const firstController = new AbortController()
148
+ const secondController = new AbortController()
149
+ const first = controller.list({}, firstController.signal)
150
+ const second = controller.list({}, secondController.signal)
151
+ firstController.abort()
152
+ release()
153
+ const [firstResult, secondResult] = await Promise.allSettled([first, second])
154
+ assert.equal(built, 1)
155
+ assert.equal(firstResult.status, 'rejected')
156
+ assert.equal(secondResult.status, 'fulfilled')
157
+ })
158
+
159
+ test('原方法抛错时错误照旧透传给调用方', async () => {
160
+ const { ctx, controller } = makeContext(async () => {
161
+ throw new Error('宿主故障')
162
+ })
163
+ installListSlim(ctx, { warmupDelayMs: -1 })
164
+ await assert.rejects(() => controller.list({}, undefined), /宿主故障/)
165
+ })
166
+
167
+ test('响应形状不符时原样透传,不缓存', async () => {
168
+ let built = 0
169
+ const { ctx, controller } = makeContext(async () => {
170
+ built += 1
171
+ return { sessions: sample() }
172
+ })
173
+ installListSlim(ctx, { warmupDelayMs: -1 })
174
+ const first = await controller.list({}, undefined)
175
+ const second = await controller.list({}, undefined)
176
+ assert.deepEqual(Object.keys(first), ['sessions'])
177
+ assert.equal(built, 2)
178
+ assert.notEqual(second, first)
179
+ })
180
+
181
+ test('把调用方 request 原样交给原方法', async () => {
182
+ const seen = []
183
+ const { ctx, controller } = makeContext(async (request) => {
184
+ seen.push(request)
185
+ return { items: sample() }
186
+ })
187
+ installListSlim(ctx, { warmupDelayMs: -1 })
188
+ const request = { cursor: 'abc' }
189
+ await controller.list(request, undefined)
190
+ assert.equal(seen[0], request)
191
+ })
192
+
193
+ test('预热在延迟后构建一次', async () => {
194
+ let built = 0
195
+ const { ctx } = makeContext(async () => {
196
+ built += 1
197
+ return { items: sample() }
198
+ })
199
+ installListSlim(ctx, { warmupDelayMs: 0 })
200
+ await new Promise((resolve) => setTimeout(resolve, 20))
201
+ assert.equal(built, 1)
202
+ })
203
+
204
+ test('卸载时恢复原方法', async () => {
205
+ const original = async () => ({ items: sample() })
206
+ const { ctx, controller, disposers } = makeContext(original)
207
+ installListSlim(ctx, { warmupDelayMs: -1 })
208
+ assert.notEqual(controller.list, original)
209
+ for (const dispose of disposers) dispose()
210
+ assert.equal(controller.list, original)
211
+ })
212
+
213
+ // Given 另一个插件在本插件之后包装了 list, When 本插件卸载, Then 不摘掉后装包装且链上透传
214
+ test('卸载不摘掉后装的包装', async () => {
215
+ const original = async () => ({ items: sample() })
216
+ const { ctx, controller, disposers } = makeContext(original)
217
+ installListSlim(ctx, { warmupDelayMs: -1 })
218
+ const wrapped = controller.list
219
+ const later = async function (request, signal) {
220
+ return wrapped.call(this, request, signal)
221
+ }
222
+ controller.list = later
223
+ for (const dispose of disposers) dispose()
224
+ assert.equal(controller.list, later)
225
+ const value = await controller.list({}, undefined)
226
+ assert.equal(value.items[0].projections.values.contextHeaders[0].pad.length, 64, '卸载后链上的旧包装应透传')
227
+ })
228
+
229
+ // Given 服务代理每次读取 list 都返回新引用(真实 cordis traceable), When 卸载, Then 按包装标记仍恢复原方法
230
+ test('服务代理返回新引用时卸载仍恢复原方法', async () => {
231
+ const original = async () => ({ items: sample() })
232
+ let current = original
233
+ const service = {
234
+ ctx: {},
235
+ get list() {
236
+ return new Proxy(current, {})
237
+ },
238
+ set list(value) {
239
+ current = value
240
+ },
241
+ }
242
+ const { ctx, disposers } = makeContext(undefined, { controller: service })
243
+ installListSlim(ctx, { warmupDelayMs: -1 })
244
+ const installed = current
245
+ assert.notEqual(service.list, installed, '代理每次返回新引用')
246
+ for (const dispose of disposers) dispose()
247
+ assert.notEqual(current, installed, '卸载后不再指向包装')
248
+ const value = await current({})
249
+ assert.equal(value.items[0].projections.values.contextHeaders[0].pad.length, 64, '恢复后透传完整投影')
250
+ })
251
+
252
+ // Given 宿主 effect 注册失败, When 装载, Then 包装与标记回滚,不留下无法卸载的包装
253
+ test('effect 注册失败时回滚包装', () => {
254
+ const original = async () => ({ items: sample() })
255
+ const { ctx, controller, logs } = makeContext(original, { effectError: new Error('inactive') })
256
+ installListSlim(ctx, { warmupDelayMs: -1 })
257
+ assert.equal(controller.list, original)
258
+ assert.equal(controller[Symbol.for('@mzzsfy/dsh-bug-perf/list-slim')], undefined)
259
+ assert.ok(logs.warn.some((message) => message.includes('装载失败')))
260
+ })
261
+
262
+ // Given 第一实例的安装标记不可见, When 第二实例再包装且第一实例卸载, Then 不摘掉第二实例的包装
263
+ test('不摘掉其它实例的包装', () => {
264
+ const original = async () => ({ items: sample() })
265
+ const first = makeContext(original)
266
+ installListSlim(first.ctx, { warmupDelayMs: -1 })
267
+ const firstWrapped = first.controller.list
268
+ delete first.controller[Symbol.for('@mzzsfy/dsh-bug-perf/list-slim')]
269
+ const second = makeContext(undefined, { controller: first.controller })
270
+ installListSlim(second.ctx, { warmupDelayMs: -1 })
271
+ const secondWrapped = first.controller.list
272
+ assert.notEqual(secondWrapped, firstWrapped)
273
+ for (const dispose of first.disposers) dispose()
274
+ assert.equal(first.controller.list, secondWrapped)
275
+ })
276
+
277
+ // Given 所有调用者都取消, When 最后一个等待者结束, Then 中止共享构建,后续调用可重试
278
+ test('所有调用者取消后中止共享构建', async () => {
279
+ let built = 0
280
+ const signals = []
281
+ const original = (request, signal) => {
282
+ built += 1
283
+ signals.push(signal)
284
+ if (built === 1) {
285
+ return new Promise((_, reject) => {
286
+ signal.addEventListener('abort', () => reject(new Error('aborted')), { once: true })
287
+ })
288
+ }
289
+ return Promise.resolve({ items: sample() })
290
+ }
291
+ const { ctx, controller } = makeContext(original)
292
+ installListSlim(ctx, { warmupDelayMs: -1 })
293
+ const first = new AbortController()
294
+ const second = new AbortController()
295
+ const waitFirst = controller.list({}, first.signal)
296
+ const waitSecond = controller.list({}, second.signal)
297
+ first.abort()
298
+ second.abort()
299
+ await Promise.allSettled([waitFirst, waitSecond])
300
+ assert.equal(signals[0].aborted, true, '所有等待者取消后应中止构建')
301
+ const third = await controller.list({}, undefined)
302
+ assert.equal(built, 2)
303
+ assert.deepEqual(third.items.map((entry) => entry.sessionId), ['a', 'b'])
304
+ })
305
+
306
+ // Given 列表构建在途, When 卸载, Then 中止内部构建
307
+ test('卸载中止在途构建', () => {
308
+ let internal
309
+ const original = (request, signal) => {
310
+ internal = signal
311
+ return new Promise(() => {})
312
+ }
313
+ const { ctx, controller, disposers } = makeContext(original)
314
+ installListSlim(ctx, { warmupDelayMs: -1 })
315
+ void controller.list({}, undefined)
316
+ for (const dispose of disposers) dispose()
317
+ assert.equal(internal.aborted, true)
318
+ })
319
+
320
+ // Given 同一服务重复装载, When 再次 install, Then 不叠加第二层包装
321
+ test('重复装载不叠加包装', () => {
322
+ const original = async () => ({ items: sample() })
323
+ const { ctx, controller } = makeContext(original)
324
+ installListSlim(ctx, { warmupDelayMs: -1 })
325
+ const first = controller.list
326
+ installListSlim(ctx, { warmupDelayMs: -1 })
327
+ assert.equal(controller.list, first)
328
+ })
329
+
330
+ // Given 宿主 list 按 this.ctx.invocation 过滤, When viewer 主体调用, Then 包装保留 receiver 且只拿到可见项
331
+ test('包装保留调用方 receiver,非 owner 由宿主逐项过滤', async () => {
332
+ const original = async function list() {
333
+ const principal = this?.ctx?.invocation?.peer?.principal
334
+ const items = principal?.kind === 'viewer' ? [item('shared', { title: 'S' })] : [item('secret', { title: 'X' }), item('shared', { title: 'S' })]
335
+ return { items }
336
+ }
337
+ const { ctx } = makeContext(original)
338
+ installListSlim(ctx, { warmupDelayMs: -1 })
339
+ const value = await ctx.get('sessionController').list.call(receiverOf({ kind: 'viewer' }), {}, undefined)
340
+ assert.deepEqual(value.items.map((entry) => entry.sessionId), ['shared'])
341
+ })
342
+
343
+ // Given owner 已把全量列表写入缓存, When viewer 调用, Then 不命中 owner 缓存、仍由宿主过滤
344
+ test('非 owner 不命中 owner 缓存', async () => {
345
+ let built = 0
346
+ const original = async function list() {
347
+ built += 1
348
+ const principal = this?.ctx?.invocation?.peer?.principal
349
+ const items = principal?.kind === 'viewer' ? [item('shared', { title: 'S' })] : [item('secret', { title: 'X' }), item('shared', { title: 'S' })]
350
+ return { items }
351
+ }
352
+ const { ctx, controller } = makeContext(original)
353
+ installListSlim(ctx, { warmupDelayMs: -1 })
354
+ const owner = await controller.list({}, undefined)
355
+ assert.deepEqual(owner.items.map((entry) => entry.sessionId), ['secret', 'shared'])
356
+ const viewer = await controller.list.call(receiverOf({ kind: 'viewer' }), {}, undefined)
357
+ assert.deepEqual(viewer.items.map((entry) => entry.sessionId), ['shared'])
358
+ assert.equal(built, 2, 'viewer 调用必须重新走原方法')
359
+ })
360
+
361
+ // Given 预热已构建 owner 缓存, When viewer 调用, Then 仍不命中缓存
362
+ test('预热缓存不对非 owner 生效', async () => {
363
+ let built = 0
364
+ const original = async function list() {
365
+ built += 1
366
+ const principal = this?.ctx?.invocation?.peer?.principal
367
+ const items = principal?.kind === 'viewer' ? [item('shared', { title: 'S' })] : [item('secret', { title: 'X' }), item('shared', { title: 'S' })]
368
+ return { items }
369
+ }
370
+ const { ctx, controller } = makeContext(original)
371
+ installListSlim(ctx, { warmupDelayMs: 0 })
372
+ await new Promise((resolve) => setTimeout(resolve, 20))
373
+ assert.equal(built, 1)
374
+ const viewer = await controller.list.call(receiverOf({ kind: 'viewer' }), {}, undefined)
375
+ assert.deepEqual(viewer.items.map((entry) => entry.sessionId), ['shared'])
376
+ assert.equal(built, 2)
377
+ })
378
+
379
+ // Given 列表构建在途, When 会话增删事件触发且同 signal 再次调用, Then 不复用失效前的构建
380
+ test('在途构建遇到失效后新调用重建', async () => {
381
+ let built = 0
382
+ const releases = []
383
+ const original = () => {
384
+ built += 1
385
+ return new Promise((resolve) => {
386
+ releases.push(() => resolve({ items: sample() }))
387
+ })
388
+ }
389
+ const { ctx, controller, handlers } = makeContext(original)
390
+ installListSlim(ctx, { warmupDelayMs: -1 })
391
+ const signal = new AbortController().signal
392
+ const pending = controller.list({}, signal)
393
+ handlers.get('api-session/added')()
394
+ const second = controller.list({}, signal)
395
+ assert.equal(built, 2, '失效后的新调用必须重建')
396
+ releases[0]()
397
+ releases[1]()
398
+ await Promise.all([pending, second])
399
+ })
400
+
401
+ // Given 在途构建期间失效, When 旧构建完成, Then 旧结果不写回缓存
402
+ test('在途构建遇到失效后不写回旧快照', async () => {
403
+ let built = 0
404
+ const releases = []
405
+ const original = () => {
406
+ built += 1
407
+ return new Promise((resolve) => {
408
+ releases.push(() => resolve({ items: sample() }))
409
+ })
410
+ }
411
+ const { ctx, controller, handlers } = makeContext(original)
412
+ installListSlim(ctx, { warmupDelayMs: -1 })
413
+ const pending = controller.list({}, undefined)
414
+ handlers.get('api-session/added')()
415
+ releases[0]()
416
+ await pending
417
+ const second = controller.list({}, undefined)
418
+ assert.equal(built, 2, '失效期间的结果不得作为缓存命中')
419
+ releases[1]()
420
+ await second
421
+ })