@mzzsfy/dsh-turn-notify 0.10.0 → 0.11.1

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.
@@ -1,4 +1,4 @@
1
- // 宿主桌面通知 BDD:core 纯函数(回环判定 / 派发决策 / 平台命令 / spawn 执行 / 配置键)
1
+ // 宿主桌面通知 BDD:core 纯函数(派发决策 / 平台命令 / spawn 执行 / 配置键)
2
2
  // 与 host 接线集成(stub ctx 装载真实 src/index.js,stub spawn 观测派发)。
3
3
  import test from 'node:test'
4
4
  import assert from 'node:assert/strict'
@@ -10,53 +10,32 @@ import {
10
10
  HOST_NOTIFY_TIMEOUT_MS,
11
11
  hostNotifyCommand,
12
12
  hostNotifyWanted,
13
- isLocalBrowserPoll,
14
- isLocalBrowserPresent,
15
- isLoopbackAddress,
13
+ isBrowserPoll,
14
+ isBrowserPresent,
16
15
  publicConfig,
17
16
  resolvedConfig,
18
17
  sendHostNotify,
19
18
  validateConfigPatch,
20
19
  } from '../src/core.mjs'
21
20
 
22
- // ---- isLoopbackAddress:同机判定,回环地址即本机浏览器 ----
21
+ // ---- isBrowserPoll:在场记账形态收紧,防伪造在场压制宿主通知 ----
23
22
 
24
- test('Given 回环形态地址 When 判定 Then 为真', () => {
25
- assert.equal(isLoopbackAddress('::1'), true)
26
- assert.equal(isLoopbackAddress('127.0.0.1'), true)
27
- assert.equal(isLoopbackAddress('127.8.8.8'), true)
28
- assert.equal(isLoopbackAddress('::ffff:127.0.0.1'), true)
23
+ test('Given 合格轮询形态(任意来源加 cursor 加 fetch 形态或无 Sec-Fetch 头)When 判定 Then 为真', () => {
24
+ assert.equal(isBrowserPoll({ hasCursor: true, secFetchMode: 'cors' }), true)
25
+ assert.equal(isBrowserPoll({ hasCursor: true, secFetchMode: undefined }), true)
29
26
  })
30
27
 
31
- test('Given 非回环或非法地址 When 判定 Then 为假', () => {
32
- assert.equal(isLoopbackAddress('192.168.1.5'), false)
33
- assert.equal(isLoopbackAddress('::ffff:192.168.1.5'), false)
34
- assert.equal(isLoopbackAddress('::2'), false)
35
- assert.equal(isLoopbackAddress(''), false)
36
- assert.equal(isLoopbackAddress(undefined), false)
37
- assert.equal(isLoopbackAddress(null), false)
38
- })
39
-
40
- // ---- isLocalBrowserPoll:在场记账形态收紧,防伪造在场压制宿主通知 ----
41
-
42
- test('Given 回环加 cursor 加 fetch 形态或无 Sec-Fetch 头 When 判定 Then 为真', () => {
43
- assert.equal(isLocalBrowserPoll({ remoteAddress: '127.0.0.1', hasCursor: true, secFetchMode: 'cors' }), true)
44
- assert.equal(isLocalBrowserPoll({ remoteAddress: '::1', hasCursor: true, secFetchMode: undefined }), true)
45
- })
46
-
47
- test('Given 非回环或无 cursor 或 no-cors 形态 When 判定 Then 为假', () => {
48
- assert.equal(isLocalBrowserPoll({ remoteAddress: '192.168.1.5', hasCursor: true, secFetchMode: 'cors' }), false)
49
- assert.equal(isLocalBrowserPoll({ remoteAddress: '127.0.0.1', hasCursor: false, secFetchMode: 'cors' }), false)
50
- assert.equal(isLocalBrowserPoll({ remoteAddress: '127.0.0.1', hasCursor: true, secFetchMode: 'no-cors' }), false)
51
- assert.equal(isLocalBrowserPoll({ remoteAddress: undefined, hasCursor: true, secFetchMode: 'cors' }), false)
28
+ test('Given 无 cursor 或 no-cors 形态 When 判定 Then 为假', () => {
29
+ assert.equal(isBrowserPoll({ hasCursor: false, secFetchMode: 'cors' }), false)
30
+ assert.equal(isBrowserPoll({ hasCursor: true, secFetchMode: 'no-cors' }), false)
52
31
  })
53
32
 
54
33
  test('Given Origin 缺席或与 Host 一致 When 判定 Then 为真;跨源或畸形 Origin Then 为假', () => {
55
- const base = { remoteAddress: '127.0.0.1', hasCursor: true, secFetchMode: 'cors' }
56
- assert.equal(isLocalBrowserPoll({ ...base, origin: undefined, host: '127.0.0.1:3080' }), true, '同源 GET fetch 不带 Origin')
57
- assert.equal(isLocalBrowserPoll({ ...base, origin: 'http://127.0.0.1:3080', host: '127.0.0.1:3080' }), true)
58
- assert.equal(isLocalBrowserPoll({ ...base, origin: 'https://evil.example', host: '127.0.0.1:3080' }), false, '跨源 fetch 必带且与 Host 不符')
59
- assert.equal(isLocalBrowserPoll({ ...base, origin: 'not a url', host: '127.0.0.1:3080' }), false, '畸形 Origin 不放行')
34
+ const base = { hasCursor: true, secFetchMode: 'cors' }
35
+ assert.equal(isBrowserPoll({ ...base, origin: undefined, host: '127.0.0.1:3080' }), true, '同源 GET fetch 不带 Origin')
36
+ assert.equal(isBrowserPoll({ ...base, origin: 'http://127.0.0.1:3080', host: '127.0.0.1:3080' }), true)
37
+ assert.equal(isBrowserPoll({ ...base, origin: 'https://evil.example', host: '127.0.0.1:3080' }), false, '跨源 fetch 必带且与 Host 不符')
38
+ assert.equal(isBrowserPoll({ ...base, origin: 'not a url', host: '127.0.0.1:3080' }), false, '畸形 Origin 不放行')
60
39
  })
61
40
 
62
41
  // ---- hostNotifyWanted:浏览器优先,在场让位、离场补位;重复只发生在边界情况 ----
@@ -64,36 +43,36 @@ test('Given Origin 缺席或与 Host 一致 When 判定 Then 为真;跨源或畸
64
43
  const NOW = 1000 * 1000
65
44
  const WINDOW_MS = 2 * 1000
66
45
 
67
- test('Given 任一开关开且本机浏览器不在场 When 决策 Then 弹', () => {
68
- assert.equal(hostNotifyWanted({ hostNotify: true, localBrowserPresent: false }), true)
69
- assert.equal(hostNotifyWanted({ hostNotifyFallback: true, localBrowserPresent: false }), true)
70
- assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true, localBrowserPresent: false }), true)
46
+ test('Given 任一开关开且浏览器不在场 When 决策 Then 弹', () => {
47
+ assert.equal(hostNotifyWanted({ hostNotify: true, browserPresent: false }), true)
48
+ assert.equal(hostNotifyWanted({ hostNotifyFallback: true, browserPresent: false }), true)
49
+ assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true, browserPresent: false }), true)
71
50
  })
72
51
 
73
- test('Given 任一开关开且本机浏览器在场 When 决策 Then 让位不弹(浏览器优先,正常路径零重复)', () => {
74
- assert.equal(hostNotifyWanted({ hostNotify: true, localBrowserPresent: true }), false)
75
- assert.equal(hostNotifyWanted({ hostNotifyFallback: true, localBrowserPresent: true }), false)
76
- assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true, localBrowserPresent: true }), false)
52
+ test('Given 任一开关开且浏览器在场 When 决策 Then 让位不弹(浏览器优先,正常路径零重复)', () => {
53
+ assert.equal(hostNotifyWanted({ hostNotify: true, browserPresent: true }), false)
54
+ assert.equal(hostNotifyWanted({ hostNotifyFallback: true, browserPresent: true }), false)
55
+ assert.equal(hostNotifyWanted({ hostNotify: true, hostNotifyFallback: true, browserPresent: true }), false)
77
56
  })
78
57
 
79
58
  test('Given 两开关均关 When 决策 Then 恒不弹', () => {
80
- assert.equal(hostNotifyWanted({ localBrowserPresent: false }), false)
81
- assert.equal(hostNotifyWanted({ hostNotify: false, hostNotifyFallback: false, localBrowserPresent: false }), false)
59
+ assert.equal(hostNotifyWanted({ browserPresent: false }), false)
60
+ assert.equal(hostNotifyWanted({ hostNotify: false, hostNotifyFallback: false, browserPresent: false }), false)
82
61
  })
83
62
 
84
- // ---- isLocalBrowserPresent:在场 = 有在途长轮询,或宽限窗口内有活动 ----
63
+ // ---- isBrowserPresent:在场 = 有在途长轮询,或宽限窗口内有活动 ----
85
64
 
86
65
  test('Given 在途长轮询存在 When 判定在场 Then 为真(时间戳过期不影响)', () => {
87
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 1, lastSeenAt: NOW - WINDOW_MS - 1, now: NOW, windowMs: WINDOW_MS }), true)
88
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 3, lastSeenAt: 0, now: NOW, windowMs: WINDOW_MS }), true)
66
+ assert.equal(isBrowserPresent({ inFlightPolls: 1, lastSeenAt: NOW - WINDOW_MS - 1, now: NOW, windowMs: WINDOW_MS }), true)
67
+ assert.equal(isBrowserPresent({ inFlightPolls: 3, lastSeenAt: 0, now: NOW, windowMs: WINDOW_MS }), true)
89
68
  })
90
69
 
91
70
  test('Given 无在途 When 判定在场 Then 最近活动在窗口内为真,超窗或从未为假', () => {
92
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - 1, now: NOW, windowMs: WINDOW_MS }), true)
93
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - WINDOW_MS, now: NOW, windowMs: WINDOW_MS }), true)
94
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - WINDOW_MS - 1, now: NOW, windowMs: WINDOW_MS }), false)
95
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 0, lastSeenAt: 0, now: NOW, windowMs: WINDOW_MS }), false)
96
- assert.equal(isLocalBrowserPresent({ inFlightPolls: 0, lastSeenAt: undefined, now: NOW, windowMs: WINDOW_MS }), false)
71
+ assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - 1, now: NOW, windowMs: WINDOW_MS }), true)
72
+ assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - WINDOW_MS, now: NOW, windowMs: WINDOW_MS }), true)
73
+ assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: NOW - WINDOW_MS - 1, now: NOW, windowMs: WINDOW_MS }), false)
74
+ assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: 0, now: NOW, windowMs: WINDOW_MS }), false)
75
+ assert.equal(isBrowserPresent({ inFlightPolls: 0, lastSeenAt: undefined, now: NOW, windowMs: WINDOW_MS }), false)
97
76
  })
98
77
 
99
78
  // ---- hostNotifyCommand:平台命令构造与文本转义 ----
@@ -209,7 +188,7 @@ test('Given 缺省配置 When 解析 Then 两键回 false;CHANNELS 含 host', ()
209
188
  assert.ok(CHANNELS.indexOf('host') >= 0, 'host 应为可路由通道')
210
189
  })
211
190
 
212
- // ---- host 接线集成:stub spawn 观测派发,presence 经投影路由的来源地址注入 ----
191
+ // ---- host 接线集成:stub spawn 观测派发,presence 经投影路由的请求形态注入 ----
213
192
 
214
193
  // res 桩带事件能力:投影处理器挂 close 事件感知断连,桩须支持 once/emit;
215
194
  // end 不自动触发 close,断连时机由测试显式 emit 模拟
@@ -272,30 +251,31 @@ async function postConfig(routes, patch) {
272
251
  return res
273
252
  }
274
253
 
275
- async function pollProjection(routes, remoteAddress) {
254
+ async function pollProjection(routes) {
276
255
  const req = makeReq('GET')
277
256
  req.url = '/api/turn-notify/projection'
278
- if (remoteAddress !== undefined) req.socket = { remoteAddress }
279
257
  const res = makeRes()
280
258
  await routes.get('/api/turn-notify/projection')(req, res)
281
259
  assert.equal(res.status, 200)
282
260
  return res
283
261
  }
284
262
 
285
- // 本机浏览器长轮询记账形态:回环 + 带续传 cursor + fetch 形态头;
286
- // cursor 取超前值立即返回,不挂起
263
+ // 浏览器长轮询记账形态:任意来源 + 带续传 cursor + fetch 形态头;
264
+ // cursor 取超前值立即返回,不挂起。跨机地址为主场景(真实部署拓扑:
265
+ // 浏览器在局域网另一台机器访问 dsh),回环为同机特例
287
266
  const FUTURE_CURSOR = 2 ** 40
288
- async function browserPoll(routes, remoteAddress) {
267
+ const REMOTE_BROWSER = '10.123.90.14'
268
+ async function browserPoll(routes) {
289
269
  const req = makeReq('GET', undefined, { 'sec-fetch-mode': 'cors' })
290
270
  req.url = '/api/turn-notify/projection?cursor=' + FUTURE_CURSOR
291
- req.socket = { remoteAddress }
271
+ req.socket = { remoteAddress: REMOTE_BROWSER }
292
272
  const res = makeRes()
293
273
  await routes.get('/api/turn-notify/projection')(req, res)
294
274
  assert.equal(res.status, 200)
295
275
  return res
296
276
  }
297
277
 
298
- // 伪形态轮询:回环来源但缺 cursor / no-cors / 跨源 Origin,记账须拒绝
278
+ // 伪形态轮询:缺 cursor / no-cors / 跨源 Origin,记账须拒绝(与来源地址无关)
299
279
  async function rawShapePoll(routes, remoteAddress, secFetchMode, withCursor, origin) {
300
280
  const headers = secFetchMode === undefined ? {} : { 'sec-fetch-mode': secFetchMode }
301
281
  if (origin !== undefined) headers.origin = origin
@@ -330,9 +310,11 @@ async function emitCompletedTurn(handlers) {
330
310
  }
331
311
 
332
312
  // 挂起中的长轮询:cursor 等于当前版本(配置 POST 后为 1),服务端挂起等待,
333
- // 模拟真实客户端的在途请求;返回 pending 承诺与 res 桩(供断连模拟与唤醒等待)
334
- function hangingPoll(routes, remoteAddress) {
335
- const req = makeReq('GET', undefined, { 'sec-fetch-mode': 'cors' })
313
+ // 模拟真实客户端的在途请求;返回 pending 承诺与 res 桩(供断连模拟与唤醒等待);
314
+ // secFetchMode 缺省 cors,传 undefined 模拟不发 Sec-Fetch 头的旧浏览器
315
+ function hangingPoll(routes, remoteAddress, secFetchMode) {
316
+ const headers = secFetchMode === undefined ? {} : { 'sec-fetch-mode': secFetchMode }
317
+ const req = makeReq('GET', undefined, headers)
336
318
  req.url = '/api/turn-notify/projection?cursor=1'
337
319
  req.socket = { remoteAddress }
338
320
  const res = makeRes()
@@ -340,39 +322,53 @@ function hangingPoll(routes, remoteAddress) {
340
322
  return { pending, res }
341
323
  }
342
324
 
343
- test('Given 总开关开且本机长轮询在途 When 回合完成 Then 宿主让位;离场超窗后接管', async () => {
325
+ test('Given 旧浏览器形态(无 Sec-Fetch 头)跨机长轮询在途 When 回合完成 Then 宿主让位', async () => {
326
+ await withStubSpawn(async (spawns) => {
327
+ const { ctx, routes, handlers } = makeCtx()
328
+ apply(ctx)
329
+ await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
330
+ const { pending } = hangingPoll(routes, REMOTE_BROWSER, undefined)
331
+ await flushMicrotasks()
332
+ await emitCompletedTurn(handlers)
333
+ assert.equal(spawns.length, 0, '无 Sec-Fetch 头的同源续传轮询同样入账,宿主让位')
334
+ await pending
335
+ })
336
+ })
337
+
338
+ test('Given 总开关开且跨机长轮询在途 When 回合完成 Then 宿主让位;离场超窗后接管', async () => {
344
339
  await withStubSpawn(async (spawns) => {
345
340
  const { ctx, routes, handlers } = makeCtx()
346
341
  apply(ctx)
347
342
  const saved = await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
348
343
  assert.equal(saved.body.hostNotify, true)
349
- const { pending, res } = hangingPoll(routes, '127.0.0.1')
344
+ const { pending, res } = hangingPoll(routes, REMOTE_BROWSER)
350
345
  await flushMicrotasks()
351
- // 在途请求即在场信号:浏览器优先,宿主让位,正常路径零重复
346
+ // 在途请求即在场信号:浏览器优先,宿主让位,正常路径零重复;
347
+ // 浏览器在局域网另一台机器访问时同样让位
352
348
  await emitCompletedTurn(handlers)
353
- assert.equal(spawns.length, 0, '本机长轮询在途,宿主让位')
349
+ assert.equal(spawns.length, 0, '跨机浏览器长轮询在途,宿主让位')
354
350
  // 在途请求被回合事件唤醒完成,宽限窗口内仍算在场
355
351
  await pending
356
352
  await emitCompletedTurn(handlers)
357
353
  assert.equal(spawns.length, 0, '长轮询刚完成,宽限窗口内仍由浏览器呈现')
358
- // 推进时钟越过宽限窗口:本机视为离场,宿主接管(宁可重复不可漏)
354
+ // 推进时钟越过宽限窗口:浏览器视为离场,宿主接管(宁可重复不可漏)
359
355
  const realNow = Date.now
360
356
  Date.now = () => realNow() + CLIENT_PRESENCE_WINDOW_MS + 1
361
357
  try {
362
358
  await emitCompletedTurn(handlers)
363
359
  } finally { Date.now = realNow }
364
360
  assert.equal(spawns.length, 1, '离场超窗,宿主接管')
365
- const projection = await pollProjection(routes, undefined)
361
+ const projection = await pollProjection(routes)
366
362
  assert.equal(projection.body.units.length, 3, '浏览器呈现通道不受让位影响')
367
363
  })
368
364
  })
369
365
 
370
- test('Given 总开关开且本机长轮询断连 When 回合完成 Then 认可窗口内让位,超窗接管', async () => {
366
+ test('Given 总开关开且长轮询断连 When 回合完成 Then 认可窗口内让位,超窗接管', async () => {
371
367
  await withStubSpawn(async (spawns) => {
372
368
  const { ctx, routes, handlers } = makeCtx()
373
369
  apply(ctx)
374
370
  await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
375
- const { pending, res } = hangingPoll(routes, '127.0.0.1')
371
+ const { pending, res } = hangingPoll(routes, REMOTE_BROWSER)
376
372
  await flushMicrotasks()
377
373
  // 浏览器关闭中止请求:close 事件即时出账,最近活动定格于断连时刻
378
374
  res.emit('close')
@@ -401,7 +397,7 @@ test('Given 记账轮询正常完成后再触发 close When 时钟推进越过
401
397
  let fake = 1000 * 1000
402
398
  Date.now = () => fake
403
399
  try {
404
- const first = await browserPoll(routes, '127.0.0.1')
400
+ const first = await browserPoll(routes)
405
401
  // 生产正常完成路径:finally 已出账,响应完成后的 close 再触发须被幂等消化;
406
402
  // 若双出账致计数走负,时钟推进后本应让位的在途轮询将误弹
407
403
  first.emit('close')
@@ -427,8 +423,8 @@ test('Given 双 tab 并发挂起其一断连 When 时钟推进越过认可窗 Th
427
423
  let fake = 1000 * 1000
428
424
  Date.now = () => fake
429
425
  try {
430
- const tabA = hangingPoll(routes, '127.0.0.1')
431
- const tabB = hangingPoll(routes, '::1')
426
+ const tabA = hangingPoll(routes, REMOTE_BROWSER)
427
+ const tabB = hangingPoll(routes, '127.0.0.1')
432
428
  await flushMicrotasks()
433
429
  // tabA 断连即时出账,tabB 独力承载在途信号
434
430
  tabA.res.emit('close')
@@ -445,15 +441,23 @@ test('Given 双 tab 并发挂起其一断连 When 时钟推进越过认可窗 Th
445
441
  })
446
442
  })
447
443
 
448
- test('Given 总开关开且仅远程浏览器在线 When 回合完成 Then 宿主弹', async () => {
444
+ test('Given 回退开关开 When 跨机长轮询在途 Then 不弹;无浏览器在场 Then 弹', async () => {
449
445
  await withStubSpawn(async (spawns) => {
450
- const { ctx, routes, handlers } = makeCtx()
451
- apply(ctx)
452
- await postConfig(routes, { hostNotify: true, minTurnDurationMs: 0 })
453
- await browserPoll(routes, '192.168.1.5')
454
- await emitCompletedTurn(handlers)
455
- assert.equal(spawns.length, 1, '远程浏览器不与宿主同机,照常弹')
456
- assert.ok(spawns[0].args.flat().join(' ').includes('任务完成'), '通知文本应随 spawn 传出')
446
+ const local = makeCtx()
447
+ apply(local.ctx)
448
+ await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
449
+ const localPoll = hangingPoll(local.routes, REMOTE_BROWSER)
450
+ await flushMicrotasks()
451
+ await emitCompletedTurn(local.handlers)
452
+ assert.equal(spawns.length, 0, '跨机浏览器在途,回退不触发')
453
+ await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
454
+ await localPoll.pending
455
+ // 独立实例:从未有浏览器轮询
456
+ const remote = makeCtx()
457
+ apply(remote.ctx)
458
+ await postConfig(remote.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
459
+ await emitCompletedTurn(remote.handlers)
460
+ assert.equal(spawns.length, 1, '无浏览器在场,回退触发')
457
461
  })
458
462
  })
459
463
 
@@ -477,40 +481,19 @@ test('Given kindRoutes 名单不含 host When 总开关开 Then 不弹', async (
477
481
  })
478
482
  })
479
483
 
480
- test('Given 回退开关开 When 本机长轮询在途 Then 不弹;仅远程在线 Then 弹', async () => {
481
- await withStubSpawn(async (spawns) => {
482
- const local = makeCtx()
483
- apply(local.ctx)
484
- await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
485
- const localPoll = hangingPoll(local.routes, '::1')
486
- await flushMicrotasks()
487
- await emitCompletedTurn(local.handlers)
488
- assert.equal(spawns.length, 0, '本机浏览器在途,回退不触发')
489
- await postConfig(local.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
490
- await localPoll.pending
491
- // 独立实例:本机从未有浏览器,仅远程浏览器轮询过
492
- const remote = makeCtx()
493
- apply(remote.ctx)
494
- await postConfig(remote.routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
495
- await browserPoll(remote.routes, '10.0.0.8')
496
- await emitCompletedTurn(remote.handlers)
497
- assert.equal(spawns.length, 1, '本机无浏览器,回退触发')
498
- })
499
- })
500
-
501
484
  test('Given 裸 GET 形态(无 cursor 或 no-cors 或跨源)When 回合完成 Then 回退不触发(伪造在场不入账)', async () => {
502
485
  await withStubSpawn(async (spawns) => {
503
486
  const { ctx, routes, handlers } = makeCtx()
504
487
  apply(ctx)
505
488
  await postConfig(routes, { hostNotifyFallback: true, minTurnDurationMs: 0 })
506
- // 回环来源但缺 cursor:手工探测形态
507
- await rawShapePoll(routes, '127.0.0.1', 'cors', false)
508
- // 回环来源带 cursor 但 no-cors:同机网页 img 探活形态
509
- await rawShapePoll(routes, '127.0.0.1', 'no-cors', true)
510
- // 回环来源带 cursor 且 cors 但跨源 Origin:同机恶意网页标准 fetch 形态
511
- await rawShapePoll(routes, '127.0.0.1', 'cors', true, 'https://evil.example')
489
+ // 浏览器来源但缺 cursor:手工探测形态
490
+ await rawShapePoll(routes, REMOTE_BROWSER, 'cors', false)
491
+ // 浏览器来源带 cursor 但 no-cors:dsh 同源网页的 img 探活形态
492
+ await rawShapePoll(routes, REMOTE_BROWSER, 'no-cors', true)
493
+ // 浏览器来源带 cursor 且 cors 但跨源 Origin:任意机器上恶意网页的标准 fetch 形态
494
+ await rawShapePoll(routes, REMOTE_BROWSER, 'cors', true, 'https://evil.example')
512
495
  await emitCompletedTurn(handlers)
513
- assert.equal(spawns.length, 1, '伪造在场不入账,本机无真实浏览器时回退照常触发')
496
+ assert.equal(spawns.length, 1, '伪造在场不入账,无真实浏览器时回退照常触发')
514
497
  })
515
498
  })
516
499
 
@@ -529,7 +512,7 @@ test('Given spawn 失败 When 回合完成 Then 无未处理拒绝且投影照
529
512
  await flushMicrotasks()
530
513
  await flushMicrotasks()
531
514
  assert.equal(rejections.length, 0)
532
- const projection = await pollProjection(routes, undefined)
515
+ const projection = await pollProjection(routes)
533
516
  assert.equal(projection.body.units.length, 1)
534
517
  } finally {
535
518
  process.off('unhandledRejection', onUnhandled)