@mzzsfy/dsh-maintain 0.7.1 → 0.9.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.
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/client.js +175 -30
- package/src/core.mjs +140 -20
- package/src/index.js +40 -9
- package/test/client-id.test.mjs +17 -0
- package/test/core.test.mjs +51 -0
- package/test/parity.test.mjs +128 -0
- package/test/release-notes.test.mjs +165 -0
- package/test/restart-ready.test.mjs +2 -1
- package/test/route.test.mjs +271 -22
package/test/route.test.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { test } from 'node:test'
|
|
|
6
6
|
import assert from 'node:assert/strict'
|
|
7
7
|
import { EventEmitter } from 'node:events'
|
|
8
8
|
|
|
9
|
-
import { apply, RESTART_DELAY_MS, AUTO_RESTART_DELAY_MS, UPGRADE_LOCK_PATH, collectActiveWork } from '../src/index.js'
|
|
9
|
+
import { apply, RESTART_DELAY_MS, AUTO_RESTART_DELAY_MS, UPGRADE_LOCK_PATH, RELEASE_NOTES_CACHE_MAX, collectActiveWork } from '../src/index.js'
|
|
10
10
|
import { rmSync, readFileSync } from 'node:fs'
|
|
11
11
|
|
|
12
12
|
// 锁文件是固定共享路径:测试进程中断可能残留幽灵锁毒化后续运行,用例前预热清理
|
|
@@ -137,10 +137,10 @@ const realSleep = (ms) => new Promise((resolve) => {
|
|
|
137
137
|
setImmediate(spin)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
test('挂载:
|
|
140
|
+
test('挂载:9 条路由注册,启动检查后快照就绪', async () => {
|
|
141
141
|
const { ctx, routes } = makeCtx()
|
|
142
142
|
apply(ctx)
|
|
143
|
-
assert.equal(routes.size,
|
|
143
|
+
assert.equal(routes.size, 9)
|
|
144
144
|
assert.deepEqual(
|
|
145
145
|
[...routes.keys()].sort(),
|
|
146
146
|
[
|
|
@@ -148,6 +148,7 @@ test('挂载:8 条路由注册,启动检查后快照就绪', async () => {
|
|
|
148
148
|
'/api/maintain/poll-interval',
|
|
149
149
|
'/api/maintain/refresh',
|
|
150
150
|
'/api/maintain/registry-base',
|
|
151
|
+
'/api/maintain/release-notes',
|
|
151
152
|
'/api/maintain/restart',
|
|
152
153
|
'/api/maintain/status',
|
|
153
154
|
'/api/maintain/upgrade',
|
|
@@ -156,16 +157,7 @@ test('挂载:8 条路由注册,启动检查后快照就绪', async () => {
|
|
|
156
157
|
)
|
|
157
158
|
// 启动检查是异步链(mock fetch 微任务 + resolveHostVersion 真实文件读),
|
|
158
159
|
// 轮询等快照落定后再断言
|
|
159
|
-
|
|
160
|
-
for (let waited = 0; waited < 5000; waited += 25) {
|
|
161
|
-
const poll = await call(routes, '/api/maintain/status', makeReq({ method: 'GET' }))
|
|
162
|
-
if (poll.payload.checkedAt !== null) {
|
|
163
|
-
snapshotReady = true
|
|
164
|
-
break
|
|
165
|
-
}
|
|
166
|
-
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
167
|
-
}
|
|
168
|
-
assert.ok(snapshotReady, '启动检查 5 秒内未完成')
|
|
160
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
169
161
|
const res = await call(routes, '/api/maintain/status', makeReq({ method: 'GET' }))
|
|
170
162
|
assert.equal(res.status, 200)
|
|
171
163
|
assert.equal(res.payload.packageName, '@deepseek-ai/dsh')
|
|
@@ -209,8 +201,10 @@ test('interval dispose 回归:fiber 停用后轮询 tick 失效(防双 interval
|
|
|
209
201
|
test('方法守卫:全部路由错误方法一律 405', async () => {
|
|
210
202
|
const { ctx, routes } = makeCtx()
|
|
211
203
|
apply(ctx)
|
|
204
|
+
// GET 端点(status/release-notes)以 POST 拒绝,其余以 GET 拒绝
|
|
205
|
+
const readPaths = ['/api/maintain/status', '/api/maintain/release-notes']
|
|
212
206
|
for (const path of routes.keys()) {
|
|
213
|
-
const expected = path
|
|
207
|
+
const expected = readPaths.indexOf(path) >= 0 ? 'POST' : 'GET'
|
|
214
208
|
const res = await call(routes, path, makeReq({ method: expected }))
|
|
215
209
|
assert.equal(res.status, 405, path + ' 应拒绝 ' + expected)
|
|
216
210
|
}
|
|
@@ -295,28 +289,38 @@ test('registry-base:非法 scheme 400;合法值持久化', async () => {
|
|
|
295
289
|
assert.equal(store.registryBase, 'https://mirror.example')
|
|
296
290
|
})
|
|
297
291
|
|
|
298
|
-
test('upgrade
|
|
292
|
+
test('upgrade:运行版本已是通道最新放行(重装/回退场景),不再 409 拒绝', async () => {
|
|
299
293
|
const store = { upgradeCommandTemplate: 'node -e "process.exit(0)"' }
|
|
300
294
|
// 版本探测注入受控值:CI 无 dsh 本体,真实盘读回 null 会把 verdict 打成 unknown,
|
|
301
|
-
//
|
|
295
|
+
// 重装放行断言(本用例核心)在 CI 恒不触发
|
|
302
296
|
const { ctx, routes } = makeCtx({
|
|
303
297
|
settingsStore: store,
|
|
304
298
|
services: { hostVersionProbe: () => Promise.resolve('5.4.3') },
|
|
305
299
|
})
|
|
306
300
|
apply(ctx)
|
|
307
|
-
//
|
|
301
|
+
// 注入运行版本远高于假目标版本:verdict 应转 up-to-date,安装入口照常放行
|
|
308
302
|
const originalFetch = globalThis.fetch
|
|
309
303
|
globalThis.fetch = async () => tagsBody({ latest: '0.0.1', next: '0.0.2' })
|
|
304
|
+
const warns = []
|
|
305
|
+
const originalWarn = console.warn
|
|
306
|
+
console.warn = (text) => warns.push(String(text))
|
|
310
307
|
try {
|
|
311
308
|
const refreshed = await post(routes, '/api/maintain/refresh')
|
|
312
309
|
assert.equal(refreshed.status, 200)
|
|
313
310
|
assert.equal(refreshed.payload.verdict, 'up-to-date', '前置:注入运行版本应高于 0.0.1 假目标')
|
|
314
|
-
const
|
|
315
|
-
assert.equal(
|
|
316
|
-
assert.
|
|
317
|
-
|
|
311
|
+
const accepted = await post(routes, '/api/maintain/upgrade', { autoRestart: true })
|
|
312
|
+
assert.equal(accepted.status, 200, '已是通道最新必须放行:重装修复与回退是显式用户意图')
|
|
313
|
+
assert.equal(accepted.payload.upgrade.running, true)
|
|
314
|
+
for (let i = 0; i < 50; i += 1) {
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
316
|
+
const status = await get(routes, '/api/maintain/status').then((r) => r.payload).catch(() => null)
|
|
317
|
+
if (status && status.upgrade && status.upgrade.running === false && status.upgrade.last !== null) break
|
|
318
|
+
}
|
|
319
|
+
assert.equal(store.upgradeCommandTemplate, 'node -e "process.exit(0)"', '放行路径命令模板不得被改动')
|
|
320
|
+
assert.ok(warns.some((text) => /audit endpoint=upgrade outcome=triggered/.test(text)), '放行安装须留审计行')
|
|
318
321
|
} finally {
|
|
319
322
|
globalThis.fetch = originalFetch
|
|
323
|
+
console.warn = originalWarn
|
|
320
324
|
}
|
|
321
325
|
})
|
|
322
326
|
|
|
@@ -586,6 +590,39 @@ test('upgrade:未勾选自动重启,升级成功落定继续运行并标 stale',
|
|
|
586
590
|
}
|
|
587
591
|
})
|
|
588
592
|
|
|
593
|
+
test('upgrade:模板钉定版本时按钉定意图判 fresh(通道目标不参与对拍)', async (t) => {
|
|
594
|
+
// 假命令携带钉定 token(参数位),语义等同 `npm i -g pkg@1.0.0` 但不真装
|
|
595
|
+
const store = { upgradeCommandTemplate: 'node -e "process.exit(0)" @deepseek-ai/dsh@1.0.0' }
|
|
596
|
+
const exits = []
|
|
597
|
+
const { ctx, routes } = makeCtx({
|
|
598
|
+
settingsStore: store,
|
|
599
|
+
appExit: (code) => exits.push(code),
|
|
600
|
+
// 探针恒读 1.0.0:钉定 1.0.0 时"磁盘未变"即达成,不得按通道目标误报 stale
|
|
601
|
+
services: { hostVersionProbe: () => Promise.resolve('1.0.0') },
|
|
602
|
+
})
|
|
603
|
+
t.mock.timers.enable({ apis: ['setTimeout'] })
|
|
604
|
+
apply(ctx)
|
|
605
|
+
await drainMicrotasks()
|
|
606
|
+
const trigger = await post(routes, '/api/maintain/upgrade', { autoRestart: false })
|
|
607
|
+
assert.equal(trigger.status, 200)
|
|
608
|
+
let settled = null
|
|
609
|
+
try {
|
|
610
|
+
for (let i = 0; i < 50 && settled === null; i += 1) {
|
|
611
|
+
await realSleep(30)
|
|
612
|
+
const status = await get(routes, '/api/maintain/status').then((r) => r.payload).catch(() => null)
|
|
613
|
+
if (status && status.upgrade && status.upgrade.running === false && status.upgrade.last !== null) settled = status
|
|
614
|
+
}
|
|
615
|
+
assert.ok(settled, '升级应在假命令退出后落定')
|
|
616
|
+
assert.equal(settled.upgrade.last.ok, true)
|
|
617
|
+
assert.equal(settled.upgrade.last.stale, false, '钉定版本装到位即达成')
|
|
618
|
+
assert.equal(settled.upgrade.last.reason, null)
|
|
619
|
+
t.mock.timers.tick(AUTO_RESTART_DELAY_MS + 1)
|
|
620
|
+
assert.deepEqual(exits, [])
|
|
621
|
+
} finally {
|
|
622
|
+
rmSync(UPGRADE_LOCK_PATH, { force: true })
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
|
|
589
626
|
test('upgrade:勾选自动重启+落定,延迟窗口后调度宿主退出', async (t) => {
|
|
590
627
|
const store = { upgradeCommandTemplate: 'node -e "process.exit(0)"' }
|
|
591
628
|
const exits = []
|
|
@@ -891,4 +928,216 @@ test('audit:触发/落定/拒绝各留一行结构化日志', async () => {
|
|
|
891
928
|
}
|
|
892
929
|
assert.equal(plainWarns.some((text) => /audit endpoint=upgrade outcome=triggered/.test(text)), true, '升级触发须留审计行')
|
|
893
930
|
assert.equal(plainWarns.some((text) => /audit endpoint=upgrade outcome=(ok|failed) durationMs=[0-9]+ code=/.test(text)), true, '升级落定须留审计行')
|
|
894
|
-
})
|
|
931
|
+
})
|
|
932
|
+
|
|
933
|
+
// ---- release notes 路由 ----
|
|
934
|
+
|
|
935
|
+
const RELEASE_PATH = '/api/maintain/release-notes'
|
|
936
|
+
// 发布页 fixture:与真实页面同构(relative-time + markdown-body 正文容器),正文映射由 core 提取器完成
|
|
937
|
+
const RELEASE_HTML = '<!doctype html><html><body>'
|
|
938
|
+
+ '<relative-time datetime="2026-09-10T03:09:00Z"></relative-time>'
|
|
939
|
+
+ '<div data-test-selector="body-content" class="markdown-body">'
|
|
940
|
+
+ '<h3>更新内容</h3>'
|
|
941
|
+
+ '<ul><li>新功能</li><li>问题修复</li></ul>'
|
|
942
|
+
+ '<p>详情见发布页。</p>'
|
|
943
|
+
+ '</div></body></html>'
|
|
944
|
+
const RELEASE_URL = 'https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v9.9.9'
|
|
945
|
+
const RELEASE_BODY = ['### 更新内容', '- 新功能', '- 问题修复', '', '详情见发布页。'].join('\n')
|
|
946
|
+
|
|
947
|
+
function releaseHtmlBody() {
|
|
948
|
+
const chunks = [new TextEncoder().encode(RELEASE_HTML)]
|
|
949
|
+
return {
|
|
950
|
+
getReader: () => ({
|
|
951
|
+
read: async () => (chunks.length ? { done: false, value: chunks.shift() } : { done: true, value: undefined }),
|
|
952
|
+
cancel: async () => {},
|
|
953
|
+
}),
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// dist-tags 与 GitHub 发布页双上游分流:registry 路径回 tags 形态,GitHub 路径回页面形态
|
|
958
|
+
function installDualUpstream({ requestedUrls, releaseThrows }) {
|
|
959
|
+
const originalFetch = globalThis.fetch
|
|
960
|
+
globalThis.fetch = async (url, options) => {
|
|
961
|
+
const text = String(url)
|
|
962
|
+
if (text.includes('/releases/tag/')) {
|
|
963
|
+
requestedUrls.push(text)
|
|
964
|
+
if (releaseThrows) throw new Error(releaseThrows)
|
|
965
|
+
return { ok: true, status: 200, body: releaseHtmlBody() }
|
|
966
|
+
}
|
|
967
|
+
void options
|
|
968
|
+
return tagsBody(MOCK_TAGS)
|
|
969
|
+
}
|
|
970
|
+
return () => { globalThis.fetch = originalFetch }
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function waitSnapshotReady(routes) {
|
|
974
|
+
for (let waited = 0; waited < 5000; waited += 25) {
|
|
975
|
+
const poll = await get(routes, '/api/maintain/status').then((r) => r.payload).catch(() => null)
|
|
976
|
+
if (poll && poll.checkedAt !== null) return poll
|
|
977
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
978
|
+
}
|
|
979
|
+
return null
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
test('release-notes:默认取通道最新版,200 返回发布说明', async () => {
|
|
983
|
+
const requestedUrls = []
|
|
984
|
+
const restore = installDualUpstream({ requestedUrls })
|
|
985
|
+
try {
|
|
986
|
+
const { ctx, routes } = makeCtx({ services: { hostVersionProbe: () => Promise.resolve('5.4.3') } })
|
|
987
|
+
apply(ctx)
|
|
988
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
989
|
+
const res = await get(routes, RELEASE_PATH)
|
|
990
|
+
assert.equal(res.status, 200)
|
|
991
|
+
assert.deepEqual(res.payload, {
|
|
992
|
+
version: '9.9.9',
|
|
993
|
+
url: RELEASE_URL,
|
|
994
|
+
publishedAt: '2026-09-10T03:09:00Z',
|
|
995
|
+
body: RELEASE_BODY,
|
|
996
|
+
})
|
|
997
|
+
assert.equal(requestedUrls.length, 1)
|
|
998
|
+
assert.ok(requestedUrls[0].endsWith('/releases/tag/dsh-v9.9.9'), '默认版本必须取通道 latest 的目标')
|
|
999
|
+
} finally {
|
|
1000
|
+
restore()
|
|
1001
|
+
}
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
test('release-notes:缓存生效,切通道后按新通道版本拉取', async () => {
|
|
1005
|
+
const requestedUrls = []
|
|
1006
|
+
const restore = installDualUpstream({ requestedUrls })
|
|
1007
|
+
try {
|
|
1008
|
+
const { ctx, routes } = makeCtx()
|
|
1009
|
+
apply(ctx)
|
|
1010
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
1011
|
+
const first = await get(routes, RELEASE_PATH)
|
|
1012
|
+
assert.equal(first.status, 200)
|
|
1013
|
+
assert.equal(first.payload.version, '9.9.9')
|
|
1014
|
+
assert.ok(requestedUrls[0].endsWith('/releases/tag/dsh-v9.9.9'))
|
|
1015
|
+
const again = await get(routes, RELEASE_PATH)
|
|
1016
|
+
assert.equal(again.status, 200)
|
|
1017
|
+
assert.equal(requestedUrls.length, 1, '同版本两次调用必须命中缓存,上游仅一次')
|
|
1018
|
+
const switched = await post(routes, '/api/maintain/channel', { channel: 'next' })
|
|
1019
|
+
assert.equal(switched.status, 200)
|
|
1020
|
+
assert.equal(switched.payload.channelLatest, '10.0.0')
|
|
1021
|
+
const after = await get(routes, RELEASE_PATH)
|
|
1022
|
+
assert.equal(after.status, 200)
|
|
1023
|
+
assert.equal(after.payload.version, '10.0.0')
|
|
1024
|
+
assert.equal(requestedUrls.length, 2, '通道版本变化必须重新拉取')
|
|
1025
|
+
assert.ok(requestedUrls[1].endsWith('/releases/tag/dsh-v10.0.0'))
|
|
1026
|
+
} finally {
|
|
1027
|
+
restore()
|
|
1028
|
+
}
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
test('release-notes:tags 未就绪且无 version 400 提示先检查更新', async () => {
|
|
1032
|
+
const originalFetch = globalThis.fetch
|
|
1033
|
+
globalThis.fetch = async () => { throw new Error('ECONNREFUSED') }
|
|
1034
|
+
try {
|
|
1035
|
+
const { ctx, routes } = makeCtx()
|
|
1036
|
+
apply(ctx)
|
|
1037
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
1038
|
+
const res = await get(routes, RELEASE_PATH)
|
|
1039
|
+
assert.equal(res.status, 400)
|
|
1040
|
+
assert.match(res.payload.error, /检查更新/)
|
|
1041
|
+
} finally {
|
|
1042
|
+
globalThis.fetch = originalFetch
|
|
1043
|
+
}
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
test('release-notes:通道标签非 semver,构建上游标签前归一 400', async () => {
|
|
1047
|
+
const requestedUrls = []
|
|
1048
|
+
const originalFetch = globalThis.fetch
|
|
1049
|
+
globalThis.fetch = async (url, options) => {
|
|
1050
|
+
const text = String(url)
|
|
1051
|
+
if (text.includes('/releases/tag/')) {
|
|
1052
|
+
requestedUrls.push(text)
|
|
1053
|
+
return { ok: true, status: 200, body: releaseHtmlBody() }
|
|
1054
|
+
}
|
|
1055
|
+
return tagsBody({ latest: 'banana' })
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const { ctx, routes } = makeCtx()
|
|
1059
|
+
apply(ctx)
|
|
1060
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
1061
|
+
const res = await get(routes, RELEASE_PATH)
|
|
1062
|
+
assert.equal(res.status, 400)
|
|
1063
|
+
assert.match(res.payload.error, /semver/, '病理标签必须在触达上游前被 semver 白名单拦截')
|
|
1064
|
+
assert.equal(requestedUrls.length, 0, '拒绝路径不得触达上游')
|
|
1065
|
+
} finally {
|
|
1066
|
+
globalThis.fetch = originalFetch
|
|
1067
|
+
}
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
test('release-notes:重启调度窗口内 409,不发起上游请求', async (t) => {
|
|
1071
|
+
t.mock.timers.enable({ apis: ['setTimeout'] })
|
|
1072
|
+
const requestedUrls = []
|
|
1073
|
+
const restore = installDualUpstream({ requestedUrls })
|
|
1074
|
+
try {
|
|
1075
|
+
const { ctx, routes } = makeCtx({ appExit: () => {} })
|
|
1076
|
+
apply(ctx)
|
|
1077
|
+
const restart = await post(routes, '/api/maintain/restart')
|
|
1078
|
+
assert.equal(restart.status, 200)
|
|
1079
|
+
const res = await get(routes, RELEASE_PATH)
|
|
1080
|
+
assert.equal(res.status, 409, '关机窗口内查询更新内容必须拒绝')
|
|
1081
|
+
assert.match(res.payload.error, /重启已调度/)
|
|
1082
|
+
assert.equal(requestedUrls.length, 0, '关机窗口内不得发起上游请求')
|
|
1083
|
+
t.mock.timers.tick(RESTART_DELAY_MS + 1)
|
|
1084
|
+
} finally {
|
|
1085
|
+
restore()
|
|
1086
|
+
}
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
test('release-notes:缓存超限 FIFO 淘汰,最早键被逐出', async () => {
|
|
1090
|
+
// N+1 个通道各配独立版本:逐个切换并查看,第 N+1 键入缓存时第 1 键必被淘汰;
|
|
1091
|
+
// 回访第 1 通道触发重新拉取,回访末位通道命中缓存不再拉取
|
|
1092
|
+
const channels = {}
|
|
1093
|
+
for (let i = 1; i <= RELEASE_NOTES_CACHE_MAX + 1; i += 1) channels['c' + i] = '1.0.' + i
|
|
1094
|
+
const requestedUrls = []
|
|
1095
|
+
const originalFetch = globalThis.fetch
|
|
1096
|
+
globalThis.fetch = async (url) => {
|
|
1097
|
+
if (String(url).includes('/releases/tag/')) {
|
|
1098
|
+
requestedUrls.push(String(url))
|
|
1099
|
+
return { ok: true, status: 200, body: releaseHtmlBody() }
|
|
1100
|
+
}
|
|
1101
|
+
return tagsBody(channels)
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
const { ctx, routes } = makeCtx()
|
|
1105
|
+
apply(ctx)
|
|
1106
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
1107
|
+
for (const [name, version] of Object.entries(channels)) {
|
|
1108
|
+
const switched = await post(routes, '/api/maintain/channel', { channel: name })
|
|
1109
|
+
assert.equal(switched.status, 200)
|
|
1110
|
+
const res = await get(routes, RELEASE_PATH)
|
|
1111
|
+
assert.equal(res.status, 200)
|
|
1112
|
+
assert.equal(res.payload.version, version)
|
|
1113
|
+
}
|
|
1114
|
+
assert.equal(requestedUrls.length, RELEASE_NOTES_CACHE_MAX + 1, '每通道版本独立,各拉取一次')
|
|
1115
|
+
const first = Object.keys(channels)[0]
|
|
1116
|
+
const last = Object.keys(channels)[RELEASE_NOTES_CACHE_MAX]
|
|
1117
|
+
const backToFirst = await post(routes, '/api/maintain/channel', { channel: first })
|
|
1118
|
+
assert.equal(backToFirst.status, 200)
|
|
1119
|
+
await get(routes, RELEASE_PATH)
|
|
1120
|
+
assert.equal(requestedUrls.length, RELEASE_NOTES_CACHE_MAX + 2, '最早键已被淘汰,回访必须重新拉取')
|
|
1121
|
+
const backToLast = await post(routes, '/api/maintain/channel', { channel: last })
|
|
1122
|
+
assert.equal(backToLast.status, 200)
|
|
1123
|
+
await get(routes, RELEASE_PATH)
|
|
1124
|
+
assert.equal(requestedUrls.length, RELEASE_NOTES_CACHE_MAX + 2, '未淘汰键回访必须命中缓存')
|
|
1125
|
+
} finally {
|
|
1126
|
+
globalThis.fetch = originalFetch
|
|
1127
|
+
}
|
|
1128
|
+
})
|
|
1129
|
+
|
|
1130
|
+
test('release-notes:上游失败 400 带错误信息', async () => {
|
|
1131
|
+
const requestedUrls = []
|
|
1132
|
+
const restore = installDualUpstream({ requestedUrls, releaseThrows: 'ECONNRESET' })
|
|
1133
|
+
try {
|
|
1134
|
+
const { ctx, routes } = makeCtx()
|
|
1135
|
+
apply(ctx)
|
|
1136
|
+
assert.ok(await waitSnapshotReady(routes), '启动检查 5 秒内未完成')
|
|
1137
|
+
const res = await get(routes, RELEASE_PATH)
|
|
1138
|
+
assert.equal(res.status, 400)
|
|
1139
|
+
assert.match(res.payload.error, /ECONNRESET/)
|
|
1140
|
+
} finally {
|
|
1141
|
+
restore()
|
|
1142
|
+
}
|
|
1143
|
+
})
|