@mzzsfy/dsh-llm-pi-gateway 0.3.0 → 0.3.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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/index.js +34 -7
- package/src/takeover.mjs +45 -0
- package/test/takeover-guard.test.mjs +129 -7
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ format 未声明时由 pi-ai 自动检测:provider 名为 `openrouter`(精确匹
|
|
|
36
36
|
- 接管的路由自动获得 sessionId 注入(请求体标记默认开启;亲和头 anthropic-messages / openai-completions 经 compat 开启,openai-responses 默认开启),其余行为与官方逐项对表(见下节)
|
|
37
37
|
- `llm-pi-gateway:` 节仍可用:独立声明路由,或与官方节同名时整体覆盖(增强)
|
|
38
38
|
- **卸载即还原**:patch 随 bundle 移除,官方插件恢复,同一份配置继续由官方服务
|
|
39
|
-
- 防御:若 patch 失效(宿主升级等)官方插件仍在,本包接管官方节失败时降级为只服务 `llm-pi-gateway:` 节并记日志(命名空间注册冲突与一般失败区分文案)
|
|
39
|
+
- 防御:若 patch 失效(宿主升级等)官方插件仍在,本包接管官方节失败时降级为只服务 `llm-pi-gateway:` 节并记日志(命名空间注册冲突与一般失败区分文案),不阻塞启动;官方包本体不可用时同样降级并告警;运行中安装/热重载时官方行退场被在途流拖长属于常态,快速等待窗耗尽只表示接管推迟——官方行退场后本包自动补完成接管,不永久降级
|
|
40
40
|
|
|
41
41
|
### 禁用与还原
|
|
42
42
|
|
|
@@ -53,7 +53,7 @@ format 未声明时由 pi-ai 自动检测:provider 名为 `openrouter`(精确匹
|
|
|
53
53
|
disabled: false
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
- 官方行复活让位守卫:上述两行写入后热重载时,本包在官方插件重新注册前自停让位,官方无缝接管,不出现注册冲突拖垮 patch
|
|
56
|
+
- 官方行复活让位守卫:上述两行写入后热重载时,本包在官方插件重新注册前自停让位,官方无缝接管,不出现注册冲突拖垮 patch 应用;反之删掉这两行恢复接管时,本包等官方插件退场落定后再接管——退场受在途流拖长而超出快速等待窗(约 1 秒)时,本包先只服务 `llm-pi-gateway:` 节(告警披露),官方行最终退场后**自动补完成接管**(复活守卫 + 官方 discovery/节 + 路由重算),服务与 sessionId 注入最终一致,无需重启
|
|
57
57
|
- 完全卸载本包(`dsh plugin remove`)无需任何 patch 改动,patch 随 bundle 移除,官方行自动恢复
|
|
58
58
|
- 两节 provider 同名重叠时的归属:官方在场(让位态)下官方插件先注册,本包自有节若声明了与官方节同名的 provider,整组路由注册被宿主排他检查拒绝,本包节路由(含不重叠的)整体不服务,仅日志披露——让位态请避免两节同名,或接受官方节优先
|
|
59
59
|
- 官方行 `disabled` 写为 `!!js` 表达式且求值出错时,本包按未禁用让位(官方节归官方,不制造注册冲突);若官方行实际未启用,会出现双空闲(provider 路由无人服务),需修正表达式或改静态布尔
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
takeoverDecision,
|
|
15
15
|
awaitOfficialExit,
|
|
16
16
|
installOfficialRevivalGuard,
|
|
17
|
+
armDeferredTakeover,
|
|
17
18
|
} from './takeover.mjs'
|
|
18
19
|
import { beginGatewayApply, endGatewayApplyActive, endGatewayApplyInactive } from './apply-state.mjs'
|
|
19
20
|
|
|
@@ -82,8 +83,11 @@ export function missingHostExports(dshLlm) {
|
|
|
82
83
|
* @param {() => Promise<object>} [importOfficial] 官方包加载器,测试注入桩;
|
|
83
84
|
* 默认动态 import(官方包缺失时仅降级本节接管,不拖垮本包加载——
|
|
84
85
|
* 静态 import 命名导出缺失即加载崩溃,违反干净禁用规约)
|
|
86
|
+
* @param {{exitPoll?: object, deferredExit?: object}} [pollOptions] 轮询参数,
|
|
87
|
+
* 仅测试注入:exitPoll 透传快速退场窗(awaitOfficialExit),deferredExit
|
|
88
|
+
* 透传延迟补接管轮询(armDeferredTakeover);生产双双缺省
|
|
85
89
|
*/
|
|
86
|
-
export async function apply(ctx, config, importOfficial = () => import('@deepseek-ai/dsh-llm-pi-ai')) {
|
|
90
|
+
export async function apply(ctx, config, importOfficial = () => import('@deepseek-ai/dsh-llm-pi-ai'), { exitPoll = {}, deferredExit = {} } = {}) {
|
|
87
91
|
// 生命周期旗标:guard 据此识别本行的功能性停摆(早退 = 假活,详见
|
|
88
92
|
// apply-state.mjs);中途崩溃旗标停留 undefined,guard 保守不代挂
|
|
89
93
|
beginGatewayApply()
|
|
@@ -120,13 +124,20 @@ export async function apply(ctx, config, importOfficial = () => import('@deepsee
|
|
|
120
124
|
const officialState = officialEntryState(ctx.loader)
|
|
121
125
|
const decision = takeoverDecision(officialState)
|
|
122
126
|
let takeover
|
|
127
|
+
// 退场超时标记:arm 延迟到全部装配落定后执行,装配中途失败不留僵尸轮询
|
|
128
|
+
let exitTimedOut = false
|
|
123
129
|
if (decision === 'yield') {
|
|
124
130
|
ctx.logger.warn('llm-pi-gateway: 官方 llm-pi-ai 行未被禁用(用户层启用),本包降级为只服务 llm-pi-gateway 节')
|
|
125
131
|
takeover = false
|
|
126
132
|
} else if (decision === 'await-exit') {
|
|
127
133
|
ctx.logger.warn('llm-pi-gateway: 官方 llm-pi-ai 插件退场中,等待其完全卸载后接管')
|
|
128
|
-
takeover = await awaitOfficialExit(officialState.entry)
|
|
129
|
-
if (!takeover)
|
|
134
|
+
takeover = await awaitOfficialExit(officialState.entry, exitPoll)
|
|
135
|
+
if (!takeover) {
|
|
136
|
+
// 退场受在途流拖长属常态(用户边用边装),快速窗耗尽只是接管推迟:
|
|
137
|
+
// 先服务本包节,官方退场后补完成接管,服务与标记注入最终一致
|
|
138
|
+
ctx.logger.warn('llm-pi-gateway: 官方 llm-pi-ai 退场超时(存在在途流),先服务本包节,官方退场后自动完成接管')
|
|
139
|
+
exitTimedOut = true
|
|
140
|
+
}
|
|
130
141
|
} else {
|
|
131
142
|
takeover = true
|
|
132
143
|
}
|
|
@@ -184,8 +195,9 @@ export async function apply(ctx, config, importOfficial = () => import('@deepsee
|
|
|
184
195
|
// 官方 ns 补注册仅在接管态:让位态官方插件在场,会自行注册该 ns,本包
|
|
185
196
|
// 抢注册必令官方 init 撞 DUPLICATE_DISCOVERY 而拖垮整批 patch 应用。
|
|
186
197
|
// 注册成功即置 servingOfficial:持有任一官方注册就需要复活守卫,官方
|
|
187
|
-
// 复活撞本包在场 discovery 与撞 settings
|
|
188
|
-
|
|
198
|
+
// 复活撞本包在场 discovery 与撞 settings 节的后果相同。
|
|
199
|
+
// 同步接管与延迟补接管共用同一装配,延迟路径在退场后的轮询序列上调用
|
|
200
|
+
const registerOfficialDiscovery = () => {
|
|
189
201
|
try {
|
|
190
202
|
ctx.llm.registerModelDiscovery(OFFICIAL_NS, discoverFor)
|
|
191
203
|
servingOfficial = true
|
|
@@ -194,6 +206,7 @@ export async function apply(ctx, config, importOfficial = () => import('@deepsee
|
|
|
194
206
|
ctx.logger.warn(error)
|
|
195
207
|
}
|
|
196
208
|
}
|
|
209
|
+
if (takeover) registerOfficialDiscovery()
|
|
197
210
|
const onSectionChange = () => {
|
|
198
211
|
try {
|
|
199
212
|
manager.ensureRegistration()
|
|
@@ -212,8 +225,10 @@ export async function apply(ctx, config, importOfficial = () => import('@deepsee
|
|
|
212
225
|
// 本包以官方 schema 注册。若注册冲突(patch 失效、官方仍在),降级为只
|
|
213
226
|
// 服务本包节。validate 拒绝组合后不可解析的官方节,防坏配置穿透 profiles
|
|
214
227
|
// 快照记忆。官方包缺失时跳过接管(动态获取已告警)。servingOfficial 已在
|
|
215
|
-
// 官方 discovery
|
|
216
|
-
|
|
228
|
+
// 官方 discovery 注册处置位,此处不再改写。
|
|
229
|
+
// 同步接管与延迟补接管共用同一装配
|
|
230
|
+
const installOfficialSection = () => {
|
|
231
|
+
if (OfficialConfig === undefined) return
|
|
217
232
|
try {
|
|
218
233
|
ctx.settings.installSection(ctx, OFFICIAL_NS, OfficialConfig, undefined, {
|
|
219
234
|
validate: (section) => resolveRoutes(section.providers, readGateway()?.providers, onUnserviceable),
|
|
@@ -227,6 +242,15 @@ export async function apply(ctx, config, importOfficial = () => import('@deepsee
|
|
|
227
242
|
ctx.logger.error(error)
|
|
228
243
|
}
|
|
229
244
|
}
|
|
245
|
+
// 延迟补接管装配:与同步接管同一套动作、同一顺序(守卫→discovery→节→
|
|
246
|
+
// 路由重算),在官方行退场后的轮询序列上执行
|
|
247
|
+
const completeOfficialTakeover = () => {
|
|
248
|
+
installOfficialRevivalGuard(ctx, () => servingOfficial)
|
|
249
|
+
registerOfficialDiscovery()
|
|
250
|
+
installOfficialSection()
|
|
251
|
+
onSectionChange()
|
|
252
|
+
}
|
|
253
|
+
if (takeover) installOfficialSection()
|
|
230
254
|
ctx.settings.installSection(ctx, NS, Config, config, {
|
|
231
255
|
validate: (section) => resolveRoutes(readOfficial()?.providers, section.providers, onUnserviceable),
|
|
232
256
|
setSource: (source) => {
|
|
@@ -238,4 +262,7 @@ export async function apply(ctx, config, importOfficial = () => import('@deepsee
|
|
|
238
262
|
profiles()
|
|
239
263
|
onSectionChange()
|
|
240
264
|
endGatewayApplyActive()
|
|
265
|
+
// 延迟补接管:全部装配落定后才武装;轮询在 apply 返回后的轮询序列上执行,
|
|
266
|
+
// completeOfficialTakeover 闭包至此全部就绪,装配中途失败不会留下僵尸轮询
|
|
267
|
+
if (exitTimedOut) armDeferredTakeover(ctx, officialState.entry, () => completeOfficialTakeover(), deferredExit)
|
|
241
268
|
}
|
package/src/takeover.mjs
CHANGED
|
@@ -14,6 +14,9 @@ export const OFFICIAL_ENTRY_ID = 'llm-pi-ai'
|
|
|
14
14
|
export const EXIT_POLL_INTERVAL_MS = 20
|
|
15
15
|
export const EXIT_POLL_ROUNDS = 50
|
|
16
16
|
|
|
17
|
+
// 延迟补接管轮询粒度:dispose 拖长(在途流)场景下,退场完成后的接管时延上界
|
|
18
|
+
export const DEFERRED_EXIT_POLL_INTERVAL_MS = 500
|
|
19
|
+
|
|
17
20
|
// 行 id 解析候选前缀:宿主把 profile 行树经 cordis:include 行挂载(嵌套
|
|
18
21
|
// 分隔符 EntryTree.sep = ":"),受控行的实际解析 id 带前缀;空串覆盖裸树
|
|
19
22
|
// 形态。resolve 对缺失 id 抛错,调用方逐候选试解
|
|
@@ -74,6 +77,12 @@ export function takeoverDecision(state) {
|
|
|
74
77
|
|
|
75
78
|
const defaultDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
76
79
|
|
|
80
|
+
// unref 延迟:延迟补接管为常驻轮询,不能凭 pending timer 阻止进程退出
|
|
81
|
+
const unrefDelay = (ms) => new Promise((resolve) => {
|
|
82
|
+
const timer = setTimeout(resolve, ms)
|
|
83
|
+
timer.unref?.()
|
|
84
|
+
})
|
|
85
|
+
|
|
77
86
|
/**
|
|
78
87
|
* 有界等待官方插件退场(fiber 消失)。dispose 为纯内存操作毫秒级完成,
|
|
79
88
|
* 轮询上界仅防御异常宿主状态。fiber 清空后官方注册的撤销还在卸载收尾
|
|
@@ -95,6 +104,42 @@ export async function awaitOfficialExit(entry, {
|
|
|
95
104
|
return true
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 延迟补接管:await-exit 快速窗耗尽只是决策推迟——官方行 dispose 受在途流拖长
|
|
109
|
+
* 时,退场晚于快速窗,若就此让位则永远无人补接管。本函数持续轮询官方行,
|
|
110
|
+
* 退场完成后执行 complete(复活守卫 + 官方 discovery/节 + 路由重算),把接管
|
|
111
|
+
* 从一次性决策补成最终一致。gateway 行先退场(被删/禁)则静默放弃,不在死
|
|
112
|
+
* context 上注册;退场完成后补一拍间隔,与快速路径的收尾落定语义一致。
|
|
113
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx 本包 apply context
|
|
114
|
+
* @param {object} entry 官方 loader entry
|
|
115
|
+
* @param {() => void} complete 接管完成动作,在退场后的当前轮询序列上同步执行
|
|
116
|
+
* @param {{intervalMs?: number, delay?: (ms: number) => Promise<void>}} [options]
|
|
117
|
+
* @returns {() => void} 取消轮询(测试用)
|
|
118
|
+
* 轮询持有 entry 引用,依赖宿主 dispose 原地清 fiber 的既有契约(与
|
|
119
|
+
* awaitOfficialExit 同源);行删重建形态超出该契约,由下次重载重新 apply 兜底
|
|
120
|
+
*/
|
|
121
|
+
export function armDeferredTakeover(ctx, entry, complete, {
|
|
122
|
+
intervalMs = DEFERRED_EXIT_POLL_INTERVAL_MS,
|
|
123
|
+
delay = unrefDelay,
|
|
124
|
+
} = {}) {
|
|
125
|
+
let cancelled = false
|
|
126
|
+
const poll = async () => {
|
|
127
|
+
while (!cancelled && entry.fiber?.uid != null && ctx.fiber?.uid != null) {
|
|
128
|
+
await delay(intervalMs)
|
|
129
|
+
}
|
|
130
|
+
if (cancelled || ctx.fiber?.uid == null) return
|
|
131
|
+
await delay(intervalMs)
|
|
132
|
+
if (cancelled || ctx.fiber?.uid == null) return
|
|
133
|
+
complete()
|
|
134
|
+
}
|
|
135
|
+
void poll().catch((error) => {
|
|
136
|
+
if (!cancelled) ctx.logger?.warn?.(`llm-pi-gateway: 延迟接管失败: ${error?.message ?? error}`)
|
|
137
|
+
})
|
|
138
|
+
return () => {
|
|
139
|
+
cancelled = true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
98
143
|
/**
|
|
99
144
|
* 安装官方复活守卫:官方行即将 init(无 fiber 的 patch-context 事件)且本包
|
|
100
145
|
* 正服务官方节时,await 自停完全卸载本包全部注册后放行,官方 apply 无冲突。
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
takeoverDecision,
|
|
12
12
|
awaitOfficialExit,
|
|
13
13
|
installOfficialRevivalGuard,
|
|
14
|
+
armDeferredTakeover,
|
|
14
15
|
} from '../src/takeover.mjs'
|
|
15
16
|
|
|
16
17
|
const ABSENT_LOADER = undefined
|
|
@@ -91,6 +92,78 @@ test('等待退场: 轮询耗尽仍在场返回 false', async () => {
|
|
|
91
92
|
assert.equal(polled, false)
|
|
92
93
|
})
|
|
93
94
|
|
|
95
|
+
// 延迟补接管:await-exit 快速窗超时只是"决策推迟",官方行退场完成后必须补完
|
|
96
|
+
// 接管(复活守卫 + 官方 discovery/节),把接管从一次性决策补成最终一致。
|
|
97
|
+
|
|
98
|
+
// 真实定时器微延迟:供轮询循环退场后的收尾等待;空转延迟(noop)的循环必须
|
|
99
|
+
// 靠 cancel/清 fiber 退出,任何断言先失败都会挂死测试进程,收尾一律 try/finally
|
|
100
|
+
const tickDelay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
101
|
+
|
|
102
|
+
test('延迟接管: 官方退场后执行 complete 恰好一次', async () => {
|
|
103
|
+
const entry = entryOf({ running: true })
|
|
104
|
+
const logs = { warn: [] }
|
|
105
|
+
const ctx = { logger: { warn: (m) => logs.warn.push(m) }, fiber: { uid: 1 } }
|
|
106
|
+
let completed = 0
|
|
107
|
+
let delayCalls = 0
|
|
108
|
+
const cancel = armDeferredTakeover(ctx, entry, () => { completed += 1 }, {
|
|
109
|
+
intervalMs: 1,
|
|
110
|
+
delay: async () => { delayCalls += 1 },
|
|
111
|
+
})
|
|
112
|
+
try {
|
|
113
|
+
assert.equal(completed, 0, '官方在场时不得提前接管')
|
|
114
|
+
entry.fiber = undefined
|
|
115
|
+
await tickDelay(10)
|
|
116
|
+
assert.equal(completed, 1, '退场完成后必须补接管')
|
|
117
|
+
assert.ok(delayCalls >= 2, '退场后必须补一拍收尾延迟再接管')
|
|
118
|
+
} finally {
|
|
119
|
+
cancel()
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('延迟接管: gateway 先退场则放弃,complete 不执行', async () => {
|
|
124
|
+
const entry = entryOf({ running: true })
|
|
125
|
+
const ctx = { logger: { warn: () => {} }, fiber: { uid: 1 } }
|
|
126
|
+
let completed = 0
|
|
127
|
+
const cancel = armDeferredTakeover(ctx, entry, () => { completed += 1 }, { intervalMs: 1, delay: async () => {} })
|
|
128
|
+
try {
|
|
129
|
+
ctx.fiber = undefined
|
|
130
|
+
entry.fiber = undefined
|
|
131
|
+
await tickDelay(10)
|
|
132
|
+
assert.equal(completed, 0, 'gateway 行已退场,补接管必须在死 context 前放弃')
|
|
133
|
+
} finally {
|
|
134
|
+
cancel()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('延迟接管: cancel 句柄生效,complete 不再执行', async () => {
|
|
139
|
+
const entry = entryOf({ running: true })
|
|
140
|
+
const ctx = { logger: { warn: () => {} }, fiber: { uid: 1 } }
|
|
141
|
+
let completed = 0
|
|
142
|
+
const cancel = armDeferredTakeover(ctx, entry, () => { completed += 1 }, { intervalMs: 1, delay: async () => {} })
|
|
143
|
+
try {
|
|
144
|
+
cancel()
|
|
145
|
+
entry.fiber = undefined
|
|
146
|
+
await tickDelay(10)
|
|
147
|
+
assert.equal(completed, 0)
|
|
148
|
+
} finally {
|
|
149
|
+
cancel()
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('延迟接管: complete 抛错只告警,不产生未处理拒绝', async () => {
|
|
154
|
+
const entry = entryOf({ running: true })
|
|
155
|
+
const logs = { warn: [] }
|
|
156
|
+
const ctx = { logger: { warn: (m) => logs.warn.push(m) }, fiber: { uid: 1 } }
|
|
157
|
+
const cancel = armDeferredTakeover(ctx, entry, () => { throw new Error('boom') }, { intervalMs: 1, delay: async () => {} })
|
|
158
|
+
try {
|
|
159
|
+
entry.fiber = undefined
|
|
160
|
+
await tickDelay(10)
|
|
161
|
+
assert.match(logs.warn.join('\n'), /延迟接管失败/)
|
|
162
|
+
} finally {
|
|
163
|
+
cancel()
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
94
167
|
test('守卫: 以 global 监听注册 loader/patch-context', () => {
|
|
95
168
|
const registered = []
|
|
96
169
|
const ctx = { on: (name, callback, options) => registered.push({ name, callback, options }) }
|
|
@@ -174,7 +247,8 @@ function integrationCtx({ officialEntry, officialDiscoveryPresent = false } = {}
|
|
|
174
247
|
get: () => undefined,
|
|
175
248
|
loader: officialEntry === undefined ? undefined : loaderOf(officialEntry),
|
|
176
249
|
on: (name, callback, options) => guards.push({ name, callback, options }),
|
|
177
|
-
fiber
|
|
250
|
+
// 真实 cordis 契约:活跃 fiber 带 uid,dispose 置 null(延迟补接管以此为退场判据)
|
|
251
|
+
fiber: { uid: 7, dispose: async () => { disposed.value = true } },
|
|
178
252
|
llm: {
|
|
179
253
|
registerAdapter: () => ({ replace: () => {} }),
|
|
180
254
|
registerConfigurableProviders: () => ({ replace: () => {} }),
|
|
@@ -229,12 +303,19 @@ test('接线: 官方行禁用但仍在退场 → 等待退场后接管', async (
|
|
|
229
303
|
assert.deepEqual(installed, [OFFICIAL_SETTINGS_NS, SETTINGS_NS], '退场完成后必须接管官方节')
|
|
230
304
|
})
|
|
231
305
|
|
|
232
|
-
test('接线: 官方行禁用但退场超时 →
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
306
|
+
test('接线: 官方行禁用但退场超时 → 降级为只服务本包节,武装延迟补接管', async () => {
|
|
307
|
+
const entry = entryOf({ disabled: true, running: true })
|
|
308
|
+
const { ctx, logs, installed, discovery } = integrationCtx({ officialEntry: entry })
|
|
309
|
+
try {
|
|
310
|
+
await apply(ctx, undefined, OFFICIAL_STUB, { exitPoll: { intervalMs: 1, rounds: 1 }, deferredExit: { intervalMs: 1, delay: async () => {} } })
|
|
311
|
+
assert.match(logs.warn.join('\n'), /退场超时/)
|
|
312
|
+
assert.deepEqual(installed, [SETTINGS_NS])
|
|
313
|
+
assert.deepEqual(discovery, [SETTINGS_NS])
|
|
314
|
+
} finally {
|
|
315
|
+
// 释放轮询循环,断言先失败也不得挂死测试进程
|
|
316
|
+
entry.fiber = undefined
|
|
317
|
+
await tickDelay(5)
|
|
318
|
+
}
|
|
238
319
|
})
|
|
239
320
|
|
|
240
321
|
test('接线: loader 缺失(官方行缺席)→ 接管态,行为与官方包缺失路径一致', async () => {
|
|
@@ -284,3 +365,44 @@ test('接线: 官方包缺失 + loader 缺失(双缺席)守卫在位,官方补
|
|
|
284
365
|
assert.equal(disposed.value, true, '官方 ns 补注册在本包手中,复活守卫必须生效')
|
|
285
366
|
assert.equal(released, true)
|
|
286
367
|
})
|
|
368
|
+
|
|
369
|
+
test('接线: 退场超时先降级服务本包节,官方退场后自动补接管(守卫+discovery+节+路由重算)', async () => {
|
|
370
|
+
const entry = entryOf({ disabled: true, running: true })
|
|
371
|
+
const { ctx, logs, installed, discovery, guards, hooksByNs } = integrationCtx({ officialEntry: entry })
|
|
372
|
+
const pollOptions = { exitPoll: { intervalMs: 1, rounds: 1 }, deferredExit: { intervalMs: 1, delay: async () => {} } }
|
|
373
|
+
try {
|
|
374
|
+
await apply(ctx, undefined, OFFICIAL_STUB, pollOptions)
|
|
375
|
+
assert.match(logs.warn.join('\n'), /退场超时/)
|
|
376
|
+
assert.deepEqual(installed, [SETTINGS_NS], '超时窗口只服务本包节')
|
|
377
|
+
assert.deepEqual(discovery, [SETTINGS_NS])
|
|
378
|
+
assert.equal(guards.length, 0, '补接管前未持有官方注册,不装复活守卫')
|
|
379
|
+
// 官方行退场完成
|
|
380
|
+
entry.fiber = undefined
|
|
381
|
+
await tickDelay(30)
|
|
382
|
+
assert.deepEqual(installed, [SETTINGS_NS, OFFICIAL_SETTINGS_NS], '退场后必须补装官方节')
|
|
383
|
+
assert.deepEqual(discovery, [SETTINGS_NS, OFFICIAL_SETTINGS_NS], '退场后必须补注册官方 discovery')
|
|
384
|
+
assert.equal(guards.length, 1, '补接管完成即持有官方注册,复活守卫必须在位')
|
|
385
|
+
assert.equal(typeof hooksByNs[OFFICIAL_SETTINGS_NS].onChange, 'function')
|
|
386
|
+
} finally {
|
|
387
|
+
entry.fiber = undefined
|
|
388
|
+
await tickDelay(5)
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('接线: 退场超时后 gateway 先退场,官方退场不补接管(死 context 放弃)', async () => {
|
|
393
|
+
const entry = entryOf({ disabled: true, running: true })
|
|
394
|
+
const { ctx, logs, installed, discovery } = integrationCtx({ officialEntry: entry })
|
|
395
|
+
const pollOptions = { exitPoll: { intervalMs: 1, rounds: 1 }, deferredExit: { intervalMs: 1, delay: async () => {} } }
|
|
396
|
+
try {
|
|
397
|
+
await apply(ctx, undefined, OFFICIAL_STUB, pollOptions)
|
|
398
|
+
assert.match(logs.warn.join('\n'), /退场超时/)
|
|
399
|
+
ctx.fiber = { uid: null, dispose: async () => {} }
|
|
400
|
+
entry.fiber = undefined
|
|
401
|
+
await tickDelay(30)
|
|
402
|
+
assert.deepEqual(installed, [SETTINGS_NS], 'gateway 已退场,不得在死 context 上补装官方节')
|
|
403
|
+
assert.deepEqual(discovery, [SETTINGS_NS])
|
|
404
|
+
} finally {
|
|
405
|
+
entry.fiber = undefined
|
|
406
|
+
await tickDelay(5)
|
|
407
|
+
}
|
|
408
|
+
})
|