@mzzsfy/dsh-cron-board 0.5.0 → 0.6.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 +1 -1
- package/package.json +1 -1
- package/src/api.mjs +2 -2
- package/src/client.js +10 -0
- package/src/index.js +55 -8
- package/test/index.test.mjs +213 -2
- package/test/routes.test.mjs +3 -2
- package/test/switch-guard.test.mjs +5 -0
package/README.md
CHANGED
|
@@ -55,4 +55,4 @@ node --test "test/*.test.mjs" # 测试
|
|
|
55
55
|
|
|
56
56
|
## dsh 版本兼容
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
三版本(0.1.2-rc.1 / 0.1.5-rc.3 / 0.1.7-rc.1)兼容通过:cron 看板渲染与编辑器、侧栏入口、激活 live。0.1.6 新侧栏标签页系统下联动开关双态均验证通过;0.1.7-rc.1 修复 ui-settings 写失败打崩宿主(await 化 + 路由级 catch 降级),内置插件页改版后设置开关卡锚缺失(宿主演进),看板面实测正常。
|
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -297,8 +297,8 @@ export function createApi({ store, logger, executor, scheduler, periodic, sessio
|
|
|
297
297
|
if (typeof body.sidebarTab === 'boolean') patch.sidebarTab = body.sidebarTab
|
|
298
298
|
if (!Object.keys(patch).length) return sendJson(res, 400, { error: '无有效字段' })
|
|
299
299
|
if (typeof updateUiSettings !== 'function') return sendJson(res, 200, { ok: false, error: '设置服务不可用' })
|
|
300
|
-
updateUiSettings(patch)
|
|
301
|
-
sendJson(res, 200, { ok:
|
|
300
|
+
const persisted = await updateUiSettings(patch)
|
|
301
|
+
sendJson(res, 200, { ok: persisted, ui: { sidebarTab: readSidebarTab ? readSidebarTab() === true : false } })
|
|
302
302
|
},
|
|
303
303
|
},
|
|
304
304
|
{
|
package/src/client.js
CHANGED
|
@@ -1069,6 +1069,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
1069
1069
|
const TAB_ID = 'cron-board:board'
|
|
1070
1070
|
const PANEL_ID = 'cron-board'
|
|
1071
1071
|
const PANEL_LABEL = '定时任务'
|
|
1072
|
+
const PKG_ID = '@mzzsfy/dsh-cron-board'
|
|
1072
1073
|
|
|
1073
1074
|
// 设置>插件页卡片:官方 PluginCard 形制(名称/描述头部 + chevron 折叠 + 行内开关);better-sidebar 在场可切换,否则仅展示禁用态
|
|
1074
1075
|
const CARD_TITLE = '定时任务'
|
|
@@ -1219,6 +1220,15 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
1219
1220
|
() => h(CronBoardPluginCard, { ctx }),
|
|
1220
1221
|
)
|
|
1221
1222
|
}), 'cron-board settings card')
|
|
1223
|
+
// 插件管理页(0.1.7-rc.1+)包详情配置卡:key = 包名,管理页 ledger.bundles 判定 section
|
|
1224
|
+
// 在场;卡片自包含(只调本插件 API,不触宿主 settings)。两代槽位并存注册:inject 是
|
|
1225
|
+
// 声明生命周期效应,错误世代宿主永不声明该槽,回调挂起不执行,零成本
|
|
1226
|
+
ctx.effect(() => ctx.slots.inject('plugins.bundle.config', function* () {
|
|
1227
|
+
yield ctx.slots.register(
|
|
1228
|
+
{ name: 'plugins.bundle.config', key: PKG_ID },
|
|
1229
|
+
() => h(CronBoardPluginCard, { ctx }),
|
|
1230
|
+
)
|
|
1231
|
+
}), 'cron-board plugin manager card')
|
|
1222
1232
|
ctx.inject(['betterSidebar'], (bsCtx) => {
|
|
1223
1233
|
currentSidebar = bsCtx.betterSidebar
|
|
1224
1234
|
applyPref()
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,21 @@ const SETTINGS_SCHEMA = schemastery.object({
|
|
|
43
43
|
sidebarTab: schemastery.boolean().default(false).description('看板移入 better-sidebar 侧边栏(需已安装;关闭时始终使用主界面)'),
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
+
// 0.1.7 宿主以静态 Config 导出生成设置节表单(maintain 同构);legacy 宿主经
|
|
47
|
+
// settings.register 注册同名命名空间。根级 volatile 包装:0.1.7 下整节为 live
|
|
48
|
+
// 表单,节写经 loader 原地热更;旧宿主 schemastery 无 volatile 方法,特性检测原样返回
|
|
49
|
+
export const Config = volatileWrap(SETTINGS_SCHEMA)
|
|
50
|
+
|
|
51
|
+
function volatileWrap(schema) {
|
|
52
|
+
return typeof schema.volatile === 'function' ? schema.volatile() : schema
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// volatile ref 动态解包(get 协议):0.1.7 下 apply 入参 config 为整节单 ref;
|
|
56
|
+
// legacy 宿主 config 为普通对象原样透传
|
|
57
|
+
function unwrapVolatile(value) {
|
|
58
|
+
return typeof value?.get === 'function' ? value.get() : value
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
export function resolveDataDir(env = process.env) {
|
|
47
62
|
const override = env[DATA_DIR_ENV]
|
|
48
63
|
if (override && override.trim() !== '') return override.trim()
|
|
@@ -139,17 +154,42 @@ export function apply(ctx, config) {
|
|
|
139
154
|
const value = readSettingValue(name)
|
|
140
155
|
return typeof value === 'boolean' ? value : fallback
|
|
141
156
|
}
|
|
142
|
-
//
|
|
143
|
-
|
|
157
|
+
// 配置面双形态(maintain 同构,判定一律延迟到使用点,settings 挂载时序无保证):
|
|
158
|
+
// legacy(≤0.1.6)settings 服务 register/get/update 命名空间语义;
|
|
159
|
+
// 0.1.7+ 静态 Config 导出生成节表单,读经 volatile ref 解包,写经 configEditor
|
|
160
|
+
const legacySettingsFace = () => {
|
|
144
161
|
const settings = ctx.get('settings')
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
return typeof settings?.register === 'function' && typeof settings?.update === 'function'
|
|
163
|
+
}
|
|
164
|
+
// 0.1.7 写路径:configEditor.edit 定位本条目(fiber.entry,与命名空间解耦),
|
|
165
|
+
// change 回调合并落 profile patch;仅 volatile 字段变化时 loader 原地热更不重启
|
|
166
|
+
async function persistPatch(patch) {
|
|
167
|
+
if (legacySettingsFace()) {
|
|
168
|
+
await ctx.get('settings').update(SETTINGS_NS, patch)
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
const editor = ctx.get('configEditor')
|
|
172
|
+
const entry = ctx.fiber?.entry
|
|
173
|
+
if (!editor || typeof editor.edit !== 'function' || !entry) return false
|
|
174
|
+
await editor.edit(entry, (current) => ({ ...current, ...patch }))
|
|
147
175
|
return true
|
|
148
176
|
}
|
|
177
|
+
// 客户端设置面板写入通道。必须 await:宿主写失败在异步段抛出(0.1.5 前
|
|
178
|
+
// settings 对未注册命名空间 "No configurable plugin entry" 实测打崩宿主),
|
|
179
|
+
// await 后异常传播到 api.handle 的路由级 catch,降级为 400 响应,宿主存活
|
|
180
|
+
async function updateUiSettings(patch) {
|
|
181
|
+
return persistPatch(patch)
|
|
182
|
+
}
|
|
149
183
|
function readSettingValue(name) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
try {
|
|
185
|
+
const settings = ctx.get('settings')
|
|
186
|
+
// 双形读:legacy settings 命名空间;0.1.7+ apply 入参 config 整节解包
|
|
187
|
+
const scope = settings && settings.get ? settings.get(SETTINGS_NS) : unwrapVolatile(config)
|
|
188
|
+
return scope ? scope[name] : undefined
|
|
189
|
+
} catch {
|
|
190
|
+
// 与写入同防:宿主 settings 对未注册命名空间抛错时读面降级为缺省
|
|
191
|
+
return undefined
|
|
192
|
+
}
|
|
153
193
|
}
|
|
154
194
|
function readTickMs() {
|
|
155
195
|
return Math.max(MIN_TICK_MS, readNumber('tickSeconds', DEFAULT_TICK_MS / 1000) * 1000)
|
|
@@ -183,7 +223,14 @@ export function apply(ctx, config) {
|
|
|
183
223
|
})
|
|
184
224
|
|
|
185
225
|
ctx.inject(['settings'], (sctx) => {
|
|
186
|
-
|
|
226
|
+
// 方法面守卫:settings 服务缺 register 面(0.1.7 已移除)即走新形态分支
|
|
227
|
+
if (typeof sctx.settings.register !== 'function') {
|
|
228
|
+
// 0.1.7:原生自动设置页与本包插件卡重复,特性检测关闭(maintain 同构)
|
|
229
|
+
if (typeof sctx.settings?.configure === 'function') {
|
|
230
|
+
sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber))
|
|
231
|
+
}
|
|
232
|
+
return
|
|
233
|
+
}
|
|
187
234
|
const scope = sctx.settings.register(SETTINGS_NS, SETTINGS_SCHEMA, { base: config })
|
|
188
235
|
// tick 周期等设置变更即时对账(重建 interval)
|
|
189
236
|
if (scope && typeof scope.watch === 'function') {
|
package/test/index.test.mjs
CHANGED
|
@@ -185,6 +185,36 @@ test('index:settings 注册 cron-board 命名空间且 timer 承载默认周期
|
|
|
185
185
|
assert.deepEqual(intervals.map((entry) => entry.ms), [30 * 1000])
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
+
test('index:settings.update 异步拒绝(镜像 rc.1 无命名空间条目)被路由级 catch 接住降级 400,不打崩进程', async () => {
|
|
189
|
+
// Given settings.update 返回 rejected promise 的宿主形态(0.1.7-rc.1 L3 实测:
|
|
190
|
+
// "No configurable plugin entry 'cron-board'" 在异步段抛出,不 await 时逃逸成
|
|
191
|
+
// uncaughtException 打崩宿主;await 后由 api.handle 路由级 catch 接住)
|
|
192
|
+
const { ctx, settingsService } = makeFullCtx()
|
|
193
|
+
settingsService.update = () => Promise.reject(new Error("No configurable plugin entry 'cron-board'"))
|
|
194
|
+
delete settingsService.get
|
|
195
|
+
const captured = []
|
|
196
|
+
const ctx2 = {
|
|
197
|
+
...ctx,
|
|
198
|
+
get(name) {
|
|
199
|
+
if (name === 'settings') return settingsService
|
|
200
|
+
return ctx.get(name)
|
|
201
|
+
},
|
|
202
|
+
webServer: { register(route) { captured.push(route); return () => {} } },
|
|
203
|
+
}
|
|
204
|
+
apply(ctx2)
|
|
205
|
+
const route = captured[0]
|
|
206
|
+
assert.ok(route)
|
|
207
|
+
const res = { status: null, payload: null }
|
|
208
|
+
res.writeHead = (status) => { res.status = status }
|
|
209
|
+
res.end = (text) => { res.payload = JSON.parse(text) }
|
|
210
|
+
const req = { method: 'POST', url: 'http://localhost/api/cron-board/ui-settings', headers: { 'content-type': 'application/json' } }
|
|
211
|
+
req.on = (event, fn) => { if (event === 'data') setImmediate(() => fn(Buffer.from(JSON.stringify({ sidebarTab: true })))); if (event === 'end') setImmediate(fn) }
|
|
212
|
+
await route.handler(req, res)
|
|
213
|
+
// Then 路由级 catch 接住,降级 400 系统级错误响应,进程存活(测试正常走完即证)
|
|
214
|
+
assert.equal(res.status, 400)
|
|
215
|
+
assert.ok(res.payload.error)
|
|
216
|
+
})
|
|
217
|
+
|
|
188
218
|
test('index:tickSeconds 设置变更经 watch 重建 interval', async () => {
|
|
189
219
|
// Given 全服务桩,settings.register 的 watch 回调被捕获
|
|
190
220
|
let watchFn = null
|
|
@@ -264,7 +294,188 @@ function waitForRecord(poll, timeoutMs = 10 * 1000) {
|
|
|
264
294
|
}
|
|
265
295
|
setTimeout(tick, 50)
|
|
266
296
|
}).catch(reject)
|
|
297
|
+
}
|
|
298
|
+
tick()
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// --- 0.1.7 settings 面双形适配(maintain a913c2c 同构) ---
|
|
303
|
+
|
|
304
|
+
// 0.1.7 形态装配桩:settings 无 register/get(宿主已移除),config 为整节单 ref 桩,
|
|
305
|
+
// configEditor 桩经 change 合并后原样写回(模拟 loader 仅 volatile 变化的原地热更)
|
|
306
|
+
function makeRc17Ctx({ configStore = {}, configEditorAvailable = true, configureAvailable = true } = {}) {
|
|
307
|
+
const routes = new Map()
|
|
308
|
+
const calls = { editCalls: [], configureCalls: [], registerCalls: [] }
|
|
309
|
+
const config = { get: () => ({ ...configStore }) }
|
|
310
|
+
const settingsService = {}
|
|
311
|
+
if (configureAvailable) {
|
|
312
|
+
settingsService.configure = (presentation, owner) => {
|
|
313
|
+
calls.configureCalls.push({ presentation, owner })
|
|
314
|
+
return () => {}
|
|
267
315
|
}
|
|
268
|
-
|
|
269
|
-
|
|
316
|
+
}
|
|
317
|
+
const configEditor = {
|
|
318
|
+
async edit(entry, change) {
|
|
319
|
+
calls.editCalls.push(entry)
|
|
320
|
+
const next = change({ ...configStore }, {})
|
|
321
|
+
for (const [key, value] of Object.entries(next)) configStore[key] = value
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
const effects = []
|
|
325
|
+
const ctx = {
|
|
326
|
+
calls,
|
|
327
|
+
fiber: { entry: { options: { id: 'cron-board', name: '@mzzsfy/dsh-cron-board' } } },
|
|
328
|
+
effect(fn, label) {
|
|
329
|
+
// 会话等待轮询为真实定时器形态:桩跳过(与 makeFullCtx 同款),其余同步执行
|
|
330
|
+
if (label === 'cron-board session wait') return
|
|
331
|
+
effects.push(fn)
|
|
332
|
+
fn()
|
|
333
|
+
},
|
|
334
|
+
get(name) {
|
|
335
|
+
if (name === 'settings') return settingsService
|
|
336
|
+
if (name === 'configEditor') return configEditorAvailable ? configEditor : undefined
|
|
337
|
+
if (name === 'agents') return { get: () => undefined }
|
|
338
|
+
if (name === 'sessionQuery') return { listSessions: async () => [] }
|
|
339
|
+
if (name === 'workspaceRegistry') return { archivedSessionIds: [] }
|
|
340
|
+
if (name === 'agentPresets') return { defaultId: '', list: async () => [] }
|
|
341
|
+
return undefined
|
|
342
|
+
},
|
|
343
|
+
inject(deps, fn) {
|
|
344
|
+
if (deps[0] === 'timer') {
|
|
345
|
+
fn({ interval(intervalFn) { return () => {} } })
|
|
346
|
+
}
|
|
347
|
+
if (deps[0] === 'settings') {
|
|
348
|
+
fn({
|
|
349
|
+
settings: settingsService,
|
|
350
|
+
effect(stubEffect) {
|
|
351
|
+
const disposer = stubEffect()
|
|
352
|
+
if (typeof disposer === 'function') effects.push(disposer)
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
webServer: {
|
|
358
|
+
register(route) {
|
|
359
|
+
routes.set(route.path, route.handler)
|
|
360
|
+
return () => {}
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
return { ctx, routes, config, configStore, calls, effects }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ui-settings 请求走 prefix 路由(api.handle 按 method+segments 分发);
|
|
368
|
+
// 需真实数据目录(getRuntime 装配 store),env 注入临时目录
|
|
369
|
+
async function callApi(routes, path, body) {
|
|
370
|
+
const handler = routes.get('/api/cron-board')
|
|
371
|
+
assert.ok(handler, 'prefix 路由未注册')
|
|
372
|
+
const res = { status: null, payload: null }
|
|
373
|
+
res.writeHead = (status) => { res.status = status }
|
|
374
|
+
res.end = (text) => { res.payload = text ? JSON.parse(text) : null }
|
|
375
|
+
const req = new EventEmitter()
|
|
376
|
+
req.method = body === undefined ? 'GET' : 'POST'
|
|
377
|
+
req.url = 'http://localhost' + path
|
|
378
|
+
req.headers = { 'content-type': 'application/json' }
|
|
379
|
+
// 装配层初始化后才挂读体监听:桩事件须延后发射(与既有 post 桩同款)
|
|
380
|
+
setTimeout(() => {
|
|
381
|
+
if (body !== undefined) req.emit('data', Buffer.from(JSON.stringify(body)))
|
|
382
|
+
req.emit('end')
|
|
383
|
+
}, 80)
|
|
384
|
+
await handler(req, res)
|
|
385
|
+
return res
|
|
270
386
|
}
|
|
387
|
+
|
|
388
|
+
test('Config 导出契约:根级 volatile 包装,validate 产整节单 ref,默认值对拍', () => {
|
|
389
|
+
// Given 静态 Config 导出(0.1.7 节表单事实源)
|
|
390
|
+
assert.ok(mod.Config, 'Config 导出必须在场')
|
|
391
|
+
// When 空配置校验
|
|
392
|
+
const resolved = mod.Config['~standard'].validate({})
|
|
393
|
+
// Then 产整节单 ref(get 协议),字段为 schema 默认值
|
|
394
|
+
assert.equal(resolved.issues, undefined)
|
|
395
|
+
const section = resolved.value.get()
|
|
396
|
+
assert.equal(section.tickSeconds, 30)
|
|
397
|
+
assert.equal(section.maxConcurrent, 2)
|
|
398
|
+
assert.equal(section.logKeepPerJob, 200)
|
|
399
|
+
assert.equal(section.maskEnvInPrompt, false)
|
|
400
|
+
assert.equal(section.sidebarTab, false)
|
|
401
|
+
// When 显式值校验
|
|
402
|
+
// Then 值透传
|
|
403
|
+
const provided = mod.Config['~standard'].validate({ sidebarTab: true }).value.get()
|
|
404
|
+
assert.equal(provided.sidebarTab, true)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('0.1.7 形态:ui-settings GET 读 config 节值(ref 解包),非默认值', async (t) => {
|
|
408
|
+
// Given settings 无 register/get 的宿主 + config ref 携带用户值
|
|
409
|
+
const dir = await mkdtemp(join(tmpdir(), 'cron-board-rc17-'))
|
|
410
|
+
t.after(async () => {
|
|
411
|
+
delete process.env.DSH_CRON_BOARD_DATA_DIR
|
|
412
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
413
|
+
})
|
|
414
|
+
process.env.DSH_CRON_BOARD_DATA_DIR = dir
|
|
415
|
+
const { ctx, routes, config } = makeRc17Ctx({ configStore: { sidebarTab: true, tickSeconds: 60 } })
|
|
416
|
+
apply(ctx, config)
|
|
417
|
+
// When GET ui-settings
|
|
418
|
+
const res = await callApi(routes, '/api/cron-board/status')
|
|
419
|
+
// Then 返回 config 节值
|
|
420
|
+
assert.equal(res.status, 200)
|
|
421
|
+
assert.equal(res.payload.ui.sidebarTab, true)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test('0.1.7 形态:ui-settings POST 经 configEditor.edit 落 fiber.entry 且持久合并', async (t) => {
|
|
425
|
+
// Given configEditor 桩记录 edit 调用
|
|
426
|
+
const dir = await mkdtemp(join(tmpdir(), 'cron-board-rc17-'))
|
|
427
|
+
t.after(async () => {
|
|
428
|
+
delete process.env.DSH_CRON_BOARD_DATA_DIR
|
|
429
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
430
|
+
})
|
|
431
|
+
process.env.DSH_CRON_BOARD_DATA_DIR = dir
|
|
432
|
+
const { ctx, routes, config, configStore, calls } = makeRc17Ctx({ configStore: { tickSeconds: 60 } })
|
|
433
|
+
apply(ctx, config)
|
|
434
|
+
// When POST 写 sidebarTab
|
|
435
|
+
const res = await callApi(routes, '/api/cron-board/ui-settings', { sidebarTab: true })
|
|
436
|
+
// Then edit 定位本条目,合并写回,响应 200 携新值
|
|
437
|
+
assert.equal(res.status, 200)
|
|
438
|
+
assert.equal(calls.editCalls.length, 1)
|
|
439
|
+
assert.equal(calls.editCalls[0].options.id, 'cron-board')
|
|
440
|
+
assert.equal(configStore.sidebarTab, true)
|
|
441
|
+
assert.equal(configStore.tickSeconds, 60)
|
|
442
|
+
assert.equal(res.payload.ui.sidebarTab, true)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('0.1.7 形态:configEditor 缺失即 200 ok:false 拒写,不崩溃', async (t) => {
|
|
446
|
+
// Given configEditor 服务缺席
|
|
447
|
+
const dir = await mkdtemp(join(tmpdir(), 'cron-board-rc17-'))
|
|
448
|
+
t.after(async () => {
|
|
449
|
+
delete process.env.DSH_CRON_BOARD_DATA_DIR
|
|
450
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
451
|
+
})
|
|
452
|
+
process.env.DSH_CRON_BOARD_DATA_DIR = dir
|
|
453
|
+
const { ctx, routes, config } = makeRc17Ctx({ configEditorAvailable: false })
|
|
454
|
+
apply(ctx, config)
|
|
455
|
+
// When POST 写设置
|
|
456
|
+
const res = await callApi(routes, '/api/cron-board/ui-settings', { sidebarTab: true })
|
|
457
|
+
// Then ok:false 拒写(api 层 persisted=false 语义),进程存活(测试走完即证)
|
|
458
|
+
assert.equal(res.status, 200)
|
|
459
|
+
assert.equal(res.payload.ok, false)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
test('0.1.7 形态:settings.configure 在场即关闭原生自动页,缺席静默跳过', () => {
|
|
463
|
+
// Given configure 在场
|
|
464
|
+
const { ctx, calls, config } = makeRc17Ctx()
|
|
465
|
+
apply(ctx, config)
|
|
466
|
+
// Then 关闭自动页,owner 为本插件 fiber
|
|
467
|
+
assert.deepEqual(calls.configureCalls, [{ presentation: { auto: false }, owner: ctx.fiber }])
|
|
468
|
+
// Given configure 缺席
|
|
469
|
+
const bare = makeRc17Ctx({ configureAvailable: false })
|
|
470
|
+
// When apply
|
|
471
|
+
// Then 不抛错(测试走完即证)
|
|
472
|
+
apply(bare.ctx, bare.config)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('0.1.7 形态:settings 注入回调不因 register 缺失抛错', () => {
|
|
476
|
+
// Given settings 桩全空方法面(比真实宿主更严)
|
|
477
|
+
const { ctx, config } = makeRc17Ctx({ configureAvailable: false })
|
|
478
|
+
// When apply
|
|
479
|
+
// Then 注入回调静默返回,无异常
|
|
480
|
+
apply(ctx, config)
|
|
481
|
+
})
|
package/test/routes.test.mjs
CHANGED
|
@@ -324,10 +324,11 @@ test('status 路由:ui.sidebarTab 透出用户侧边栏移入偏好', async (t)
|
|
|
324
324
|
})
|
|
325
325
|
|
|
326
326
|
test('ui-settings 路由:布尔补丁经 updateUiSettings 写入并回读', async (t) => {
|
|
327
|
-
// Given 写入桩记录补丁,读取桩翻转返回(模拟 settings.update 后生效)
|
|
327
|
+
// Given 写入桩记录补丁,读取桩翻转返回(模拟 settings.update 后生效);
|
|
328
|
+
// 写入桩 async 返回 true 镜像持久化成功契约(宿主 update 为异步面)
|
|
328
329
|
let stored = false
|
|
329
330
|
const { api } = await makeApi(t, {
|
|
330
|
-
updateUiSettings: (patch) => { if (typeof patch.sidebarTab === 'boolean') stored = patch.sidebarTab },
|
|
331
|
+
updateUiSettings: async (patch) => { if (typeof patch.sidebarTab === 'boolean') { stored = patch.sidebarTab; return true } return false },
|
|
331
332
|
readSidebarTab: () => stored,
|
|
332
333
|
})
|
|
333
334
|
// When POST 开关开
|
|
@@ -109,6 +109,11 @@ test('client.js 主页面双形态挂载契约(默认宿主全局面板;设置
|
|
|
109
109
|
assert.match(source, /function CronBoardPluginCard/)
|
|
110
110
|
assert.match(source, /'ui-settings'/)
|
|
111
111
|
assert.match(source, /sidebarReady/)
|
|
112
|
+
// 插件管理页包详情配置卡(0.1.7-rc.1+ plugins.bundle.config,key=包名);两代槽位并存注册,
|
|
113
|
+
// 各自宿主只消费其一(旧宿主无 plugins.bundle.config 消费方,rc.1 无 settings.plugin.item 消费方)
|
|
114
|
+
assert.match(source, /const PKG_ID = '@mzzsfy\/dsh-cron-board'/)
|
|
115
|
+
assert.match(source, /ctx\.slots\.inject\('plugins\.bundle\.config'/)
|
|
116
|
+
assert.match(source, /name: 'plugins\.bundle\.config', key: PKG_ID/)
|
|
112
117
|
// 挂载仲裁:偏好(status.ui.sidebarTab)驱动全局面板/扩展槽互斥
|
|
113
118
|
assert.match(source, /applyPref\(\)/)
|
|
114
119
|
assert.match(source, /ui\.sidebarTab/)
|