@mzzsfy/dsh-cron-board 0.1.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 +55 -0
- package/cordis.patch.yml +5 -0
- package/package.json +44 -0
- package/src/api.mjs +434 -0
- package/src/client.js +949 -0
- package/src/config.mjs +10 -0
- package/src/cron.mjs +50 -0
- package/src/env-expand.mjs +51 -0
- package/src/env-text.mjs +17 -0
- package/src/executor.mjs +139 -0
- package/src/index.js +167 -0
- package/src/logger.mjs +82 -0
- package/src/scheduler.mjs +99 -0
- package/src/session-driver.mjs +82 -0
- package/src/session-window.mjs +37 -0
- package/src/shell-runner.mjs +71 -0
- package/src/status-meta.mjs +16 -0
- package/src/store.mjs +195 -0
- package/test/env-expand.test.mjs +101 -0
- package/test/executor.test.mjs +107 -0
- package/test/index.test.mjs +214 -0
- package/test/logger.test.mjs +98 -0
- package/test/parity.test.mjs +72 -0
- package/test/routes.test.mjs +361 -0
- package/test/scheduler.test.mjs +383 -0
- package/test/session-driver.test.mjs +161 -0
- package/test/shell-runner.test.mjs +87 -0
- package/test/store.test.mjs +92 -0
- package/test/switch-guard.test.mjs +63 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// 环境变量路由测试:CRUD / 打码 / 导入导出(设计 §4.4、§7 BDD)。
|
|
2
|
+
// ctx 桩形态对齐 usage-panel route.test.mjs;数据走临时目录。
|
|
3
|
+
|
|
4
|
+
import { test } from 'node:test'
|
|
5
|
+
import assert from 'node:assert/strict'
|
|
6
|
+
import { EventEmitter } from 'node:events'
|
|
7
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { createStore } from '../src/store.mjs'
|
|
12
|
+
import { createLogger } from '../src/logger.mjs'
|
|
13
|
+
import { createExecutor } from '../src/executor.mjs'
|
|
14
|
+
import { createApi } from '../src/api.mjs'
|
|
15
|
+
|
|
16
|
+
async function makeApi(t, overrides = {}) {
|
|
17
|
+
const dir = await mkdtemp(join(tmpdir(), 'cron-board-api-'))
|
|
18
|
+
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
19
|
+
const store = await createStore({ dir: join(dir, 'data') })
|
|
20
|
+
const logger = createLogger({ rootDir: join(dir, 'logs') })
|
|
21
|
+
const executor = createExecutor({ store, logger, readMaxConcurrent: () => 2 })
|
|
22
|
+
const api = createApi({ store, logger, executor, ...overrides })
|
|
23
|
+
return { store, api }
|
|
24
|
+
}
|
|
25
|
+
function makeReq(method, body) {
|
|
26
|
+
const req = new EventEmitter()
|
|
27
|
+
req.method = method
|
|
28
|
+
req.headers = { host: 'localhost:3000' }
|
|
29
|
+
if (body !== undefined) {
|
|
30
|
+
process.nextTick(() => {
|
|
31
|
+
req.emit('data', Buffer.from(JSON.stringify(body)))
|
|
32
|
+
req.emit('end')
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
return req
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function call(api, method, pathAndQuery, body) {
|
|
39
|
+
const res = { status: null, payload: null }
|
|
40
|
+
res.writeHead = (status) => { res.status = status }
|
|
41
|
+
res.end = (text) => {
|
|
42
|
+
res.raw = text
|
|
43
|
+
try { res.payload = JSON.parse(text) } catch { res.payload = null }
|
|
44
|
+
}
|
|
45
|
+
const req = makeReq(method, body)
|
|
46
|
+
req.url = 'http://localhost' + pathAndQuery
|
|
47
|
+
await api.handle(req, res)
|
|
48
|
+
return res
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('envs 路由:新建后列表打码,长度不足四字符全打码', async (t) => {
|
|
52
|
+
// Given 新建变量 value=abcdef123456
|
|
53
|
+
const { store, api } = await makeApi(t)
|
|
54
|
+
await store.envs.create({ name: 'TOKEN', value: 'abcdef123456', remarks: 'r', enabled: true })
|
|
55
|
+
await store.envs.create({ name: 'SHORT', value: 'abc', remarks: '', enabled: true })
|
|
56
|
+
// When 列表
|
|
57
|
+
const res = await call(api, 'GET', '/api/cron-board/envs')
|
|
58
|
+
// Then 打码:前2后2中间***;短值全 ***
|
|
59
|
+
assert.equal(res.status, 200)
|
|
60
|
+
const token = res.payload.items.find((row) => row.name === 'TOKEN')
|
|
61
|
+
assert.equal(token.value, 'ab***56')
|
|
62
|
+
const short = res.payload.items.find((row) => row.name === 'SHORT')
|
|
63
|
+
assert.equal(short.value, '***')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('envs 路由:PATCH 更新与 DELETE 删除', async (t) => {
|
|
67
|
+
const { store, api } = await makeApi(t)
|
|
68
|
+
const row = await store.envs.create({ name: 'K', value: 'v', remarks: '', enabled: true })
|
|
69
|
+
// When 更新值与启停,再删除
|
|
70
|
+
const patched = await call(api, 'PATCH', '/api/cron-board/envs/' + row.id, { value: 'v2', enabled: false })
|
|
71
|
+
// Then 更新生效
|
|
72
|
+
assert.equal(patched.status, 200)
|
|
73
|
+
assert.equal(store.envs.get(row.id).value, 'v2')
|
|
74
|
+
assert.equal(store.envs.get(row.id).enabled, false)
|
|
75
|
+
const deleted = await call(api, 'DELETE', '/api/cron-board/envs/' + row.id)
|
|
76
|
+
assert.equal(deleted.status, 200)
|
|
77
|
+
assert.equal(store.envs.get(row.id), undefined)
|
|
78
|
+
// 未知 id 删除报业务错误
|
|
79
|
+
const missing = await call(api, 'DELETE', '/api/cron-board/envs/no-such-id')
|
|
80
|
+
assert.equal(missing.status, 400)
|
|
81
|
+
assert.match(missing.payload.error, /不存在/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('envs 路由:导入预览不落盘,确认导入追加(client 契约形态)', async (t) => {
|
|
85
|
+
// Given 已有一条 A=old
|
|
86
|
+
const { store, api } = await makeApi(t)
|
|
87
|
+
await store.envs.create({ name: 'A', value: 'old', remarks: '', enabled: true })
|
|
88
|
+
const text = 'A=1 #备注一\nB=2\n坏行\n'
|
|
89
|
+
// When 预览(body.apply 缺省,契约:parsed 为解析行数组)
|
|
90
|
+
const preview = await call(api, 'POST', '/api/cron-board/envs/import', { text })
|
|
91
|
+
// Then 解析行数组与非法计数正确且未落盘
|
|
92
|
+
assert.equal(preview.status, 200)
|
|
93
|
+
assert.equal(preview.payload.parsed.length, 2)
|
|
94
|
+
assert.deepEqual(preview.payload.parsed.map((row) => row.name), ['A', 'B'])
|
|
95
|
+
assert.equal(preview.payload.invalid, 1)
|
|
96
|
+
assert.equal(store.envs.list().length, 1)
|
|
97
|
+
// When 确认导入(body.apply=true 且 overwrite 缺省 = 追加)
|
|
98
|
+
const committed = await call(api, 'POST', '/api/cron-board/envs/import', { text, apply: true })
|
|
99
|
+
// Then 追加两条,A 有新旧两条(同名多值)
|
|
100
|
+
assert.equal(committed.status, 200)
|
|
101
|
+
const rows = store.envs.list()
|
|
102
|
+
assert.equal(rows.length, 3)
|
|
103
|
+
assert.equal(rows.filter((row) => row.name === 'A').length, 2)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('envs 路由:覆盖导入清空原集合再落盘(client 契约 overwrite 字段)', async (t) => {
|
|
107
|
+
const { store, api } = await makeApi(t)
|
|
108
|
+
await store.envs.create({ name: 'OLD', value: 'x', remarks: '', enabled: true })
|
|
109
|
+
// When 覆盖导入
|
|
110
|
+
const committed = await call(api, 'POST', '/api/cron-board/envs/import', { text: 'N=1\n', apply: true, overwrite: true })
|
|
111
|
+
// Then 原集合清空,仅剩新行
|
|
112
|
+
assert.equal(committed.status, 200)
|
|
113
|
+
assert.deepEqual(store.envs.list().map((row) => row.name), ['N'])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('envs 路由:导出默认仅启用行,备注折叠为行尾注释', async (t) => {
|
|
117
|
+
const { store, api } = await makeApi(t)
|
|
118
|
+
await store.envs.create({ name: 'ON', value: '1', remarks: 'note', enabled: true })
|
|
119
|
+
await store.envs.create({ name: 'OFF', value: '2', remarks: '', enabled: false })
|
|
120
|
+
// When 导出(默认不含禁用)
|
|
121
|
+
const res = await call(api, 'GET', '/api/cron-board/envs/export')
|
|
122
|
+
// Then 行文本形态
|
|
123
|
+
assert.equal(res.payload.text, 'ON=1 #note\n')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('envs 路由:multi 新建折叠 values 数组为换行存储(client 契约)', async (t) => {
|
|
127
|
+
// Given 客户端 multi 提交形态:name+values 数组
|
|
128
|
+
const { store, api } = await makeApi(t)
|
|
129
|
+
// When 新建
|
|
130
|
+
const created = await call(api, 'POST', '/api/cron-board/envs', { name: 'TOKEN', values: ['a', ' b ', '', 'c'], multi: true })
|
|
131
|
+
// Then 单行存储 + multi 标志,值按行折叠,空白行剔除
|
|
132
|
+
assert.equal(created.status, 200)
|
|
133
|
+
const row = store.envs.get(created.payload.id)
|
|
134
|
+
assert.equal(row.multi, true)
|
|
135
|
+
assert.equal(row.value, 'a\nb\nc')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('envs 路由:multi PATCH 折叠 values,非 multi 保留 value 语义', async (t) => {
|
|
139
|
+
const { store, api } = await makeApi(t)
|
|
140
|
+
const multiRow = await store.envs.create({ name: 'M', value: 'x', multi: true, enabled: true })
|
|
141
|
+
const plainRow = await store.envs.create({ name: 'P', value: 'y', enabled: true })
|
|
142
|
+
// When 多值行 PATCH values 数组;普通行 PATCH value 字符串
|
|
143
|
+
const patchedMulti = await call(api, 'PATCH', '/api/cron-board/envs/' + multiRow.id, { values: ['u', 'v'], multi: true })
|
|
144
|
+
const patchedPlain = await call(api, 'PATCH', '/api/cron-board/envs/' + plainRow.id, { value: 'z' })
|
|
145
|
+
// Then 各按各自形态落库
|
|
146
|
+
assert.equal(patchedMulti.status, 200)
|
|
147
|
+
assert.equal(patchedPlain.status, 200)
|
|
148
|
+
assert.equal(store.envs.get(multiRow.id).value, 'u\nv')
|
|
149
|
+
assert.equal(store.envs.get(multiRow.id).multi, true)
|
|
150
|
+
assert.equal(store.envs.get(plainRow.id).value, 'z')
|
|
151
|
+
assert.equal(store.envs.get(plainRow.id).multi, undefined)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('jobs 路由:缺必填字段报中文业务错误', async (t) => {
|
|
155
|
+
const { api } = await makeApi(t)
|
|
156
|
+
// When 缺 name 新建任务
|
|
157
|
+
const res = await call(api, 'POST', '/api/cron-board/jobs', { kind: 'shell', command: 'x', schedule: '* * * * *' })
|
|
158
|
+
// Then 400 且中文文案
|
|
159
|
+
assert.equal(res.status, 400)
|
|
160
|
+
assert.match(res.payload.error, /名称/)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('jobs 路由:concurrency 合法正整数透传落库(client 契约)', async (t) => {
|
|
164
|
+
const { store, api } = await makeApi(t)
|
|
165
|
+
// When 带并发上限新建
|
|
166
|
+
const created = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
167
|
+
name: '限流', kind: 'shell', command: 'x', schedule: '* * * * *', concurrency: 3,
|
|
168
|
+
})
|
|
169
|
+
// Then 落库透传,任务级闸门可读
|
|
170
|
+
assert.equal(created.status, 200)
|
|
171
|
+
assert.equal(created.payload.concurrency, 3)
|
|
172
|
+
// When 非法值(0/负数/非整数)不落库
|
|
173
|
+
const bad = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
174
|
+
name: '不限', kind: 'shell', command: 'x', schedule: '* * * * *', concurrency: 0,
|
|
175
|
+
})
|
|
176
|
+
assert.equal(bad.status, 200)
|
|
177
|
+
assert.equal(bad.payload.concurrency, undefined)
|
|
178
|
+
assert.equal(store.jobs.list().filter((job) => job.name === '限流')[0].concurrency, 3)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('jobs 路由:非法 cron 表达式被拒', async (t) => {
|
|
182
|
+
const { api } = await makeApi(t)
|
|
183
|
+
// When schedule 非法
|
|
184
|
+
const res = await call(api, 'POST', '/api/cron-board/jobs', { name: 'j', kind: 'shell', command: 'x', schedule: 'not-a-cron' })
|
|
185
|
+
// Then 400 且文案含表达式
|
|
186
|
+
assert.equal(res.status, 400)
|
|
187
|
+
assert.match(res.payload.error, /cron/)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('jobs 路由:手动运行端到端——RunRecord 产生、日志可读、状态终态', async (t) => {
|
|
191
|
+
// Given shell 任务(node -e 输出标记)
|
|
192
|
+
const { store, api } = await makeApi(t)
|
|
193
|
+
const job = await store.jobs.create({
|
|
194
|
+
name: 'e2e', kind: 'shell',
|
|
195
|
+
command: `${process.execPath} -e "console.log('e2e-marker')"`,
|
|
196
|
+
schedule: '* * * * *', enabled: true, timeoutMs: 10 * 1000,
|
|
197
|
+
})
|
|
198
|
+
// When 手动运行
|
|
199
|
+
const runRes = await call(api, 'POST', '/api/cron-board/jobs/' + job.id + '/run')
|
|
200
|
+
assert.equal(runRes.status, 200)
|
|
201
|
+
const { runIds } = runRes.payload
|
|
202
|
+
assert.equal(runIds.length, 1)
|
|
203
|
+
// Then 等待终态,记录与日志齐备
|
|
204
|
+
const record = await waitFor(() => {
|
|
205
|
+
const row = store.runs.get(runIds[0])
|
|
206
|
+
return row && row.status !== 'queued' && row.status !== 'running' ? row : null
|
|
207
|
+
})
|
|
208
|
+
assert.equal(record.status, 'success')
|
|
209
|
+
assert.equal(record.trigger, 'manual')
|
|
210
|
+
const logRes = await call(api, 'GET', '/api/cron-board/runs/' + runIds[0] + '/log')
|
|
211
|
+
assert.match(logRes.payload.text, /e2e-marker/)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('jobs 路由:同名多值展开为多条 RunRecord 与多次运行', async (t) => {
|
|
215
|
+
// Given 启用变量 A=1/A=2,任务回显变量值
|
|
216
|
+
const { store, api } = await makeApi(t)
|
|
217
|
+
await store.envs.create({ name: 'CB_E2E', value: 'one', enabled: true })
|
|
218
|
+
await store.envs.create({ name: 'CB_E2E', value: 'two', enabled: true })
|
|
219
|
+
const job = await store.jobs.create({
|
|
220
|
+
name: 'multi', kind: 'shell',
|
|
221
|
+
command: `${process.execPath} -e "console.log('V=' + process.env.CB_E2E)"`,
|
|
222
|
+
schedule: '* * * * *', enabled: true, timeoutMs: 10 * 1000,
|
|
223
|
+
})
|
|
224
|
+
// When 手动运行
|
|
225
|
+
const runRes = await call(api, 'POST', '/api/cron-board/jobs/' + job.id + '/run')
|
|
226
|
+
const { runIds } = runRes.payload
|
|
227
|
+
// Then 两条记录、两条日志各注入一个值
|
|
228
|
+
assert.equal(runIds.length, 2)
|
|
229
|
+
const records = await waitFor(() => {
|
|
230
|
+
const rows = runIds.map((id) => store.runs.get(id))
|
|
231
|
+
return rows.every((row) => row && row.status !== 'queued' && row.status !== 'running') ? rows : null
|
|
232
|
+
})
|
|
233
|
+
assert.equal(records.length, 2)
|
|
234
|
+
const logs = new Set()
|
|
235
|
+
for (const id of runIds) {
|
|
236
|
+
const logRes = await call(api, 'GET', '/api/cron-board/runs/' + id + '/log')
|
|
237
|
+
logs.add(logRes.payload.text.match(/V=(\w+)/)[1])
|
|
238
|
+
}
|
|
239
|
+
assert.deepEqual([...logs].sort(), ['one', 'two'])
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('status 路由:报告调度器与 timer 可用性', async (t) => {
|
|
243
|
+
// Given scheduler 桩与 timer 运行标记
|
|
244
|
+
const { api } = await makeApi(t, {
|
|
245
|
+
scheduler: { status: () => ({ active: 1, queued: 2 }), nextRunAt: () => 1750000000000 },
|
|
246
|
+
periodic: { running: true, reason: null },
|
|
247
|
+
})
|
|
248
|
+
// When GET /status
|
|
249
|
+
const res = await call(api, 'GET', '/api/cron-board/status')
|
|
250
|
+
// Then 状态齐备
|
|
251
|
+
assert.equal(res.status, 200)
|
|
252
|
+
assert.equal(res.payload.timerRunning, true)
|
|
253
|
+
assert.deepEqual(res.payload.scheduler, { active: 1, queued: 2 })
|
|
254
|
+
assert.equal(res.payload.nextAt, 1750000000000)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('status 路由:timer 缺失降级时 timerRunning=false 且带原因', async (t) => {
|
|
258
|
+
const { api } = await makeApi(t, { periodic: { running: false, reason: '宿主定时服务不可用' } })
|
|
259
|
+
const res = await call(api, 'GET', '/api/cron-board/status')
|
|
260
|
+
assert.equal(res.payload.timerRunning, false)
|
|
261
|
+
assert.equal(res.payload.timerReason, '宿主定时服务不可用')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('jobs 路由:会话任务字段校验与默认值', async (t) => {
|
|
265
|
+
const { store, api } = await makeApi(t)
|
|
266
|
+
// When 建 pinned 会话任务
|
|
267
|
+
const created = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
268
|
+
name: '日报', kind: 'session', prompt: '写日报',
|
|
269
|
+
schedule: '0 9 * * *', enabled: true, timeoutMs: 60 * 1000,
|
|
270
|
+
session: { mode: 'pinned', pinnedSessionId: ' s-9 ', windowStart: '09:00', windowEnd: '23:00', onMiss: 'defer' },
|
|
271
|
+
})
|
|
272
|
+
// Then 会话子对象落库,id 已裁剪,默认值正确
|
|
273
|
+
assert.equal(created.status, 200)
|
|
274
|
+
assert.deepEqual(created.payload.session, {
|
|
275
|
+
mode: 'pinned', pinnedSessionId: 's-9', windowStart: '09:00', windowEnd: '23:00', onMiss: 'defer',
|
|
276
|
+
})
|
|
277
|
+
// When shell 任务不带 session 字段
|
|
278
|
+
const shellJob = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
279
|
+
name: 's1', kind: 'shell', command: 'echo x', schedule: '* * * * *', enabled: true, timeoutMs: 1000,
|
|
280
|
+
})
|
|
281
|
+
// Then session 字段缺省
|
|
282
|
+
assert.equal(shellJob.payload.session, undefined)
|
|
283
|
+
// When 窗口只给一端
|
|
284
|
+
const half = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
285
|
+
name: 's2', kind: 'session', prompt: 'x', schedule: '* * * * *', enabled: true, timeoutMs: 1000,
|
|
286
|
+
session: { windowStart: '09:00' },
|
|
287
|
+
})
|
|
288
|
+
// Then 400(窗口须成对)
|
|
289
|
+
assert.equal(half.status, 400)
|
|
290
|
+
assert.match(half.payload.error, /窗口/)
|
|
291
|
+
// When 非法窗口格式
|
|
292
|
+
const badWindow = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
293
|
+
name: 's3', kind: 'session', prompt: 'x', schedule: '* * * * *', enabled: true, timeoutMs: 1000,
|
|
294
|
+
session: { windowStart: '9:0', windowEnd: '23:00' },
|
|
295
|
+
})
|
|
296
|
+
// Then 400
|
|
297
|
+
assert.equal(badWindow.status, 400)
|
|
298
|
+
// When 非法 onMiss
|
|
299
|
+
const badMiss = await call(api, 'POST', '/api/cron-board/jobs', {
|
|
300
|
+
name: 's4', kind: 'session', prompt: 'x', schedule: '* * * * *', enabled: true, timeoutMs: 1000,
|
|
301
|
+
session: { onMiss: 'retry' },
|
|
302
|
+
})
|
|
303
|
+
// Then 400
|
|
304
|
+
assert.equal(badMiss.status, 400)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('cron preview 路由:人话摘要与下三次触发', async (t) => {
|
|
308
|
+
const { api } = await makeApi(t)
|
|
309
|
+
// When 预览「每天 08:30」
|
|
310
|
+
const ok = await call(api, 'POST', '/api/cron-board/cron/preview', { schedule: '30 8 * * *' })
|
|
311
|
+
// Then 摘要含每天与时刻,三次触发时间戳齐备
|
|
312
|
+
assert.equal(ok.status, 200)
|
|
313
|
+
assert.equal(ok.payload.summary, '每天 08:30')
|
|
314
|
+
assert.equal(ok.payload.nextAt.length, 3)
|
|
315
|
+
assert.ok(ok.payload.nextAt.every((value) => typeof value === 'number' && value > Date.now()))
|
|
316
|
+
// When 非法表达式
|
|
317
|
+
const bad = await call(api, 'POST', '/api/cron-board/cron/preview', { schedule: 'not-cron' })
|
|
318
|
+
// Then 400 中文报错
|
|
319
|
+
assert.equal(bad.status, 400)
|
|
320
|
+
assert.match(bad.payload.error, /cron/i)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('runs 路由:无参数查全量,带 jobId 查单任务', async (t) => {
|
|
324
|
+
// Given 两个任务各有一条运行记录
|
|
325
|
+
const { store, api } = await makeApi(t)
|
|
326
|
+
const j1 = await store.jobs.create({ name: 'j1', kind: 'shell', command: 'x', schedule: '* * * * *', enabled: true, timeoutMs: 1000 })
|
|
327
|
+
const j2 = await store.jobs.create({ name: 'j2', kind: 'shell', command: 'x', schedule: '* * * * *', enabled: true, timeoutMs: 1000 })
|
|
328
|
+
await store.runs.create({ jobId: j1.id, trigger: 'cron', status: 'success' })
|
|
329
|
+
await store.runs.create({ jobId: j2.id, trigger: 'manual', status: 'success' })
|
|
330
|
+
// When 无参数查全量
|
|
331
|
+
const all = await call(api, 'GET', '/api/cron-board/runs')
|
|
332
|
+
// Then 两条齐全(新记录在前)
|
|
333
|
+
assert.equal(all.status, 200)
|
|
334
|
+
assert.equal(all.payload.items.length, 2)
|
|
335
|
+
assert.equal(all.payload.items[0].trigger, 'manual')
|
|
336
|
+
// When 带 jobId 过滤
|
|
337
|
+
const mine = await call(api, 'GET', '/api/cron-board/runs?jobId=' + j1.id)
|
|
338
|
+
// Then 仅该任务记录
|
|
339
|
+
assert.equal(mine.payload.items.length, 1)
|
|
340
|
+
assert.equal(mine.payload.items[0].jobId, j1.id)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// 轮询等待:超时抛错(测试内时间敏感操作统一出口)
|
|
344
|
+
function waitFor(pick, timeoutMs = 10 * 1000) {
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
const started = Date.now()
|
|
347
|
+
const tick = () => {
|
|
348
|
+
const value = pick()
|
|
349
|
+
if (value) {
|
|
350
|
+
resolve(value)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
if (Date.now() - started > timeoutMs) {
|
|
354
|
+
reject(new Error('waitFor 超时'))
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
setTimeout(tick, 50)
|
|
358
|
+
}
|
|
359
|
+
tick()
|
|
360
|
+
})
|
|
361
|
+
}
|