metame-cli 1.4.34 → 1.5.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 +146 -32
- package/index.js +148 -9
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +254 -9
- package/scripts/daemon-agent-commands.js +64 -6
- package/scripts/daemon-agent-tools.js +26 -5
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-claude-engine.js +698 -239
- package/scripts/daemon-command-router.js +24 -8
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +275 -0
- package/scripts/daemon-exec-commands.js +10 -4
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-runtime-lifecycle.js +2 -1
- package/scripts/daemon-session-commands.js +52 -4
- package/scripts/daemon-session-store.js +2 -1
- package/scripts/daemon-task-scheduler.js +68 -38
- package/scripts/daemon-user-acl.js +26 -9
- package/scripts/daemon.js +81 -17
- package/scripts/distill.js +323 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +119 -0
- package/scripts/docs/pointer-map.md +88 -0
- package/scripts/feishu-adapter.js +6 -1
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +100 -5
- package/scripts/memory-nightly-reflect.js +196 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +2 -0
- package/scripts/providers.js +169 -21
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { createAdminCommandHandler } = require('./daemon-admin-commands');
|
|
6
|
-
const taskEnvelope = require('./daemon-task-envelope');
|
|
7
|
-
|
|
8
|
-
function createHandler(getAllTasksImpl, overrides = {}) {
|
|
9
|
-
return createAdminCommandHandler({
|
|
10
|
-
fs: require('fs'),
|
|
11
|
-
yaml: { load: () => ({}), dump: () => '' },
|
|
12
|
-
execSync: () => '',
|
|
13
|
-
BRAIN_FILE: '/tmp/brain.yaml',
|
|
14
|
-
CONFIG_FILE: '/tmp/config.yaml',
|
|
15
|
-
DISPATCH_LOG: '/tmp/dispatch.log',
|
|
16
|
-
providerMod: null,
|
|
17
|
-
loadConfig: () => ({}),
|
|
18
|
-
backupConfig: () => {},
|
|
19
|
-
writeConfigSafe: () => {},
|
|
20
|
-
restoreConfig: () => false,
|
|
21
|
-
getSession: () => null,
|
|
22
|
-
getAllTasks: getAllTasksImpl,
|
|
23
|
-
dispatchTask: () => ({ success: true }),
|
|
24
|
-
log: () => {},
|
|
25
|
-
skillEvolution: null,
|
|
26
|
-
taskBoard: null,
|
|
27
|
-
taskEnvelope: null,
|
|
28
|
-
...overrides,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('daemon-admin-commands /tasks', () => {
|
|
33
|
-
it('renders interval and fixed-time schedules for mobile task list', async () => {
|
|
34
|
-
const sent = [];
|
|
35
|
-
const { handleAdminCommand } = createHandler(() => ({
|
|
36
|
-
general: [
|
|
37
|
-
{ name: 'memory-extract', interval: '4h', enabled: true },
|
|
38
|
-
],
|
|
39
|
-
project: [
|
|
40
|
-
{
|
|
41
|
-
name: 'morning-brief',
|
|
42
|
-
at: '09:00',
|
|
43
|
-
days: 'weekdays',
|
|
44
|
-
enabled: true,
|
|
45
|
-
_project: { key: 'writer', icon: '✍️', name: 'Writer' },
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
const bot = {
|
|
51
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const res = await handleAdminCommand({
|
|
55
|
-
bot,
|
|
56
|
-
chatId: 'mobile-user-1',
|
|
57
|
-
text: '/tasks',
|
|
58
|
-
config: {},
|
|
59
|
-
state: {
|
|
60
|
-
tasks: {
|
|
61
|
-
'memory-extract': { status: 'success' },
|
|
62
|
-
'morning-brief': { status: 'never_run' },
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
assert.equal(res.handled, true);
|
|
68
|
-
assert.equal(sent.length, 1);
|
|
69
|
-
const body = sent[0];
|
|
70
|
-
assert.match(body, /memory-extract \(every 4h\) success/);
|
|
71
|
-
assert.match(body, /morning-brief \(at 09:00 weekdays\) never_run/);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('daemon-admin-commands /TeamTask', () => {
|
|
76
|
-
it('creates team task via /TeamTask create', async () => {
|
|
77
|
-
const sent = [];
|
|
78
|
-
const dispatchCalls = [];
|
|
79
|
-
const { handleAdminCommand } = createHandler(
|
|
80
|
-
() => ({ general: [], project: [] }),
|
|
81
|
-
{
|
|
82
|
-
taskEnvelope,
|
|
83
|
-
taskBoard: {
|
|
84
|
-
listScopeParticipants: () => ['planner'],
|
|
85
|
-
},
|
|
86
|
-
dispatchTask: (target, packet) => {
|
|
87
|
-
dispatchCalls.push({ target, packet });
|
|
88
|
-
return { success: true };
|
|
89
|
-
},
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const bot = {
|
|
94
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const res = await handleAdminCommand({
|
|
98
|
-
bot,
|
|
99
|
-
chatId: 'mobile-user-2',
|
|
100
|
-
text: '/TeamTask create coder 重构登录流程 --scope epic_auth',
|
|
101
|
-
config: {
|
|
102
|
-
projects: {
|
|
103
|
-
coder: { name: 'Coder' },
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
state: { tasks: {} },
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
assert.equal(res.handled, true);
|
|
110
|
-
assert.equal(dispatchCalls.length, 1);
|
|
111
|
-
assert.equal(dispatchCalls[0].target, 'coder');
|
|
112
|
-
assert.equal(dispatchCalls[0].packet.payload.task_envelope.scope_id, 'epic_auth');
|
|
113
|
-
assert.match(sent[0], /已创建 TeamTask 并派发/);
|
|
114
|
-
assert.match(sent[0], /查看: \/TeamTask t_/);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('shows usage when /TeamTask create is missing payload', async () => {
|
|
118
|
-
const sent = [];
|
|
119
|
-
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }), { taskEnvelope });
|
|
120
|
-
const bot = {
|
|
121
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const res = await handleAdminCommand({
|
|
125
|
-
bot,
|
|
126
|
-
chatId: 'mobile-user-2b',
|
|
127
|
-
text: '/TeamTask create',
|
|
128
|
-
config: {},
|
|
129
|
-
state: { tasks: {} },
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
assert.equal(res.handled, true);
|
|
133
|
-
assert.match(sent[0], /用法: \/TeamTask create <agent> <目标>/);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('lists team tasks via /TeamTask', async () => {
|
|
137
|
-
const sent = [];
|
|
138
|
-
const { handleAdminCommand } = createHandler(
|
|
139
|
-
() => ({ general: [], project: [] }),
|
|
140
|
-
{
|
|
141
|
-
taskBoard: {
|
|
142
|
-
listRecentTasks: () => [
|
|
143
|
-
{
|
|
144
|
-
task_id: 't_20260225_abc123',
|
|
145
|
-
scope_id: 'epic_auth',
|
|
146
|
-
status: 'running',
|
|
147
|
-
from_agent: 'user',
|
|
148
|
-
to_agent: 'coder',
|
|
149
|
-
goal: '重构登录流程',
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const bot = {
|
|
157
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const res = await handleAdminCommand({
|
|
161
|
-
bot,
|
|
162
|
-
chatId: 'mobile-user-3',
|
|
163
|
-
text: '/TeamTask',
|
|
164
|
-
config: {},
|
|
165
|
-
state: { tasks: {} },
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
assert.equal(res.handled, true);
|
|
169
|
-
assert.match(sent[0], /TeamTask \(最近10条\)/);
|
|
170
|
-
assert.match(sent[0], /查看详情: \/TeamTask <task_id>/);
|
|
171
|
-
assert.match(sent[0], /续跑: \/TeamTask resume <task_id>/);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('renders detail via /TeamTask <task_id>', async () => {
|
|
175
|
-
const sent = [];
|
|
176
|
-
const { handleAdminCommand } = createHandler(
|
|
177
|
-
() => ({ general: [], project: [] }),
|
|
178
|
-
{
|
|
179
|
-
taskBoard: {
|
|
180
|
-
getTask: () => ({
|
|
181
|
-
task_id: 't_20260225_xyz789',
|
|
182
|
-
scope_id: 'epic_auth',
|
|
183
|
-
status: 'running',
|
|
184
|
-
priority: 'normal',
|
|
185
|
-
from_agent: 'planner',
|
|
186
|
-
to_agent: 'coder',
|
|
187
|
-
task_kind: 'team',
|
|
188
|
-
goal: '重构登录流程',
|
|
189
|
-
definition_of_done: ['提交可运行代码'],
|
|
190
|
-
artifacts: ['src/login.js'],
|
|
191
|
-
}),
|
|
192
|
-
listTaskEvents: () => [],
|
|
193
|
-
listScopeTasks: () => [],
|
|
194
|
-
listScopeParticipants: () => ['planner', 'coder'],
|
|
195
|
-
},
|
|
196
|
-
}
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const bot = {
|
|
200
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const res = await handleAdminCommand({
|
|
204
|
-
bot,
|
|
205
|
-
chatId: 'mobile-user-3b',
|
|
206
|
-
text: '/TeamTask t_20260225_xyz789',
|
|
207
|
-
config: {},
|
|
208
|
-
state: { tasks: {} },
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
assert.equal(res.handled, true);
|
|
212
|
-
assert.match(sent[0], /🧩 TeamTask: t_20260225_xyz789/);
|
|
213
|
-
assert.match(sent[0], /Scope: epic_auth/);
|
|
214
|
-
assert.match(sent[0], /参与者: planner, coder/);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('resumes task via /TeamTask resume <task_id>', async () => {
|
|
218
|
-
const sent = [];
|
|
219
|
-
const dispatchCalls = [];
|
|
220
|
-
const { handleAdminCommand } = createHandler(
|
|
221
|
-
() => ({ general: [], project: [] }),
|
|
222
|
-
{
|
|
223
|
-
taskEnvelope,
|
|
224
|
-
taskBoard: {
|
|
225
|
-
getTask: () => ({
|
|
226
|
-
task_id: 't_20260225_resume1',
|
|
227
|
-
scope_id: 'epic_auth',
|
|
228
|
-
status: 'queued',
|
|
229
|
-
priority: 'normal',
|
|
230
|
-
from_agent: 'planner',
|
|
231
|
-
to_agent: 'coder',
|
|
232
|
-
task_kind: 'team',
|
|
233
|
-
goal: '重构登录流程',
|
|
234
|
-
definition_of_done: [],
|
|
235
|
-
inputs: {},
|
|
236
|
-
artifacts: [],
|
|
237
|
-
owned_paths: [],
|
|
238
|
-
created_at: '2026-02-25T00:00:00.000Z',
|
|
239
|
-
}),
|
|
240
|
-
listScopeParticipants: () => ['planner', 'coder'],
|
|
241
|
-
appendTaskEvent: () => {},
|
|
242
|
-
},
|
|
243
|
-
dispatchTask: (target, packet) => {
|
|
244
|
-
dispatchCalls.push({ target, packet });
|
|
245
|
-
return { success: true };
|
|
246
|
-
},
|
|
247
|
-
}
|
|
248
|
-
);
|
|
249
|
-
const bot = {
|
|
250
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const res = await handleAdminCommand({
|
|
254
|
-
bot,
|
|
255
|
-
chatId: 'mobile-user-3c',
|
|
256
|
-
text: '/TeamTask resume t_20260225_resume1',
|
|
257
|
-
config: {
|
|
258
|
-
projects: {
|
|
259
|
-
coder: { name: 'Coder' },
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
state: { tasks: {} },
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
assert.equal(res.handled, true);
|
|
266
|
-
assert.equal(dispatchCalls.length, 1);
|
|
267
|
-
assert.equal(dispatchCalls[0].target, 'coder');
|
|
268
|
-
assert.match(sent[0], /已续跑 TeamTask: t_20260225_resume1/);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('shows usage when /TeamTask resume is missing task id', async () => {
|
|
272
|
-
const sent = [];
|
|
273
|
-
const { handleAdminCommand } = createHandler(
|
|
274
|
-
() => ({ general: [], project: [] }),
|
|
275
|
-
{
|
|
276
|
-
taskBoard: {
|
|
277
|
-
getTask: () => null,
|
|
278
|
-
},
|
|
279
|
-
}
|
|
280
|
-
);
|
|
281
|
-
const bot = {
|
|
282
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const res = await handleAdminCommand({
|
|
286
|
-
bot,
|
|
287
|
-
chatId: 'mobile-user-3d',
|
|
288
|
-
text: '/TeamTask resume',
|
|
289
|
-
config: {},
|
|
290
|
-
state: { tasks: {} },
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
assert.equal(res.handled, true);
|
|
294
|
-
assert.match(sent[0], /用法: \/TeamTask resume <task_id>/);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('does not handle legacy /task command', async () => {
|
|
298
|
-
const sent = [];
|
|
299
|
-
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }));
|
|
300
|
-
const bot = {
|
|
301
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
302
|
-
};
|
|
303
|
-
const res = await handleAdminCommand({
|
|
304
|
-
bot,
|
|
305
|
-
chatId: 'mobile-user-4',
|
|
306
|
-
text: '/task',
|
|
307
|
-
config: {},
|
|
308
|
-
state: { tasks: {} },
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
assert.equal(res.handled, false);
|
|
312
|
-
assert.equal(sent.length, 0);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('does not accept legacy /dispatch task syntax', async () => {
|
|
316
|
-
const sent = [];
|
|
317
|
-
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }));
|
|
318
|
-
const bot = {
|
|
319
|
-
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
320
|
-
};
|
|
321
|
-
const res = await handleAdminCommand({
|
|
322
|
-
bot,
|
|
323
|
-
chatId: 'mobile-user-5',
|
|
324
|
-
text: '/dispatch task coder 重构登录流程',
|
|
325
|
-
config: {},
|
|
326
|
-
state: { tasks: {} },
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
assert.equal(res.handled, true);
|
|
330
|
-
assert.match(sent[0], /\/TeamTask create <agent> <目标>/);
|
|
331
|
-
assert.doesNotMatch(sent[0], /\/dispatch task/);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
normalizeTaskEnvelope,
|
|
8
|
-
validateTaskEnvelope,
|
|
9
|
-
newTaskId,
|
|
10
|
-
newHandoffId,
|
|
11
|
-
} = require('./daemon-task-envelope');
|
|
12
|
-
|
|
13
|
-
test('normalizeTaskEnvelope sets defaults for team task', () => {
|
|
14
|
-
const env = normalizeTaskEnvelope({
|
|
15
|
-
from_agent: 'assistant',
|
|
16
|
-
to_agent: 'coder',
|
|
17
|
-
goal: 'run tests',
|
|
18
|
-
});
|
|
19
|
-
assert.ok(env.task_id.startsWith('t_'));
|
|
20
|
-
assert.equal(env.scope_id, env.task_id);
|
|
21
|
-
assert.equal(env.task_kind, 'team');
|
|
22
|
-
assert.equal(env.status, 'queued');
|
|
23
|
-
assert.equal(env.priority, 'normal');
|
|
24
|
-
assert.equal(env.from_agent, 'assistant');
|
|
25
|
-
assert.equal(env.to_agent, 'coder');
|
|
26
|
-
assert.equal(env.goal, 'run tests');
|
|
27
|
-
assert.deepEqual(env.participants.sort(), ['assistant', 'coder'].sort());
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('validateTaskEnvelope rejects missing goal', () => {
|
|
31
|
-
const env = normalizeTaskEnvelope({
|
|
32
|
-
from_agent: 'assistant',
|
|
33
|
-
to_agent: 'coder',
|
|
34
|
-
goal: '',
|
|
35
|
-
});
|
|
36
|
-
const v = validateTaskEnvelope(env);
|
|
37
|
-
assert.equal(v.ok, false);
|
|
38
|
-
assert.equal(v.error, 'goal_required');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('id generators return expected prefixes', () => {
|
|
42
|
-
const taskId = newTaskId(new Date('2026-02-25T00:00:00.000Z'));
|
|
43
|
-
const handoffId = newHandoffId();
|
|
44
|
-
assert.ok(taskId.startsWith('t_20260225_'));
|
|
45
|
-
assert.ok(handoffId.startsWith('h_'));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('normalizeTaskEnvelope keeps explicit scope and merges participants', () => {
|
|
49
|
-
const env = normalizeTaskEnvelope({
|
|
50
|
-
task_id: 't_1',
|
|
51
|
-
scope_id: 'scope#A/1',
|
|
52
|
-
from_agent: 'assistant',
|
|
53
|
-
to_agent: 'reviewer',
|
|
54
|
-
participants: ['assistant', 'coder'],
|
|
55
|
-
goal: 'review changes',
|
|
56
|
-
});
|
|
57
|
-
assert.equal(env.scope_id, 'scope_A_1');
|
|
58
|
-
assert.deepEqual(env.participants.sort(), ['assistant', 'coder', 'reviewer'].sort());
|
|
59
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { _private } = require('./daemon-task-scheduler');
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
parseAtTime,
|
|
9
|
-
parseDays,
|
|
10
|
-
nextClockRunAfter,
|
|
11
|
-
buildTaskSchedule,
|
|
12
|
-
computeInitialNextRun,
|
|
13
|
-
nextRunAfter,
|
|
14
|
-
} = _private;
|
|
15
|
-
|
|
16
|
-
function nextDayOfWeek(base, day) {
|
|
17
|
-
const d = new Date(base);
|
|
18
|
-
while (d.getDay() !== day) d.setDate(d.getDate() + 1);
|
|
19
|
-
return d;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('daemon-task-scheduler private helpers', () => {
|
|
23
|
-
it('parses HH:MM time for clock tasks', () => {
|
|
24
|
-
assert.deepEqual(parseAtTime('09:30'), { hour: 9, minute: 30 });
|
|
25
|
-
assert.deepEqual(parseAtTime('23:59'), { hour: 23, minute: 59 });
|
|
26
|
-
assert.equal(parseAtTime('24:00'), null);
|
|
27
|
-
assert.equal(parseAtTime('9:7'), null);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('parses days keywords and weekday names', () => {
|
|
31
|
-
assert.deepEqual([...parseDays('weekdays').days], [1, 2, 3, 4, 5]);
|
|
32
|
-
assert.deepEqual([...parseDays('weekends').days], [0, 6]);
|
|
33
|
-
assert.deepEqual([...parseDays(['mon', 'wed', 'fri']).days], [1, 3, 5]);
|
|
34
|
-
assert.equal(parseDays('daily').days, null);
|
|
35
|
-
assert.equal(parseDays().days, null);
|
|
36
|
-
assert.equal(parseDays('funday').ok, false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('computes next run for daily fixed-time schedule', () => {
|
|
40
|
-
const schedule = { mode: 'clock', hour: 9, minute: 30, days: null };
|
|
41
|
-
const fromBefore = new Date(2026, 1, 25, 8, 0, 0, 0).getTime();
|
|
42
|
-
const fromAfter = new Date(2026, 1, 25, 10, 0, 0, 0).getTime();
|
|
43
|
-
|
|
44
|
-
const next1 = new Date(nextClockRunAfter(schedule, fromBefore));
|
|
45
|
-
const next2 = new Date(nextClockRunAfter(schedule, fromAfter));
|
|
46
|
-
|
|
47
|
-
assert.equal(next1.getHours(), 9);
|
|
48
|
-
assert.equal(next1.getMinutes(), 30);
|
|
49
|
-
assert.equal(next1.getDate(), 25);
|
|
50
|
-
|
|
51
|
-
assert.equal(next2.getHours(), 9);
|
|
52
|
-
assert.equal(next2.getMinutes(), 30);
|
|
53
|
-
assert.equal(next2.getDate(), 26);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('computes next run for weekday-only fixed-time schedule', () => {
|
|
57
|
-
const saturdayBase = nextDayOfWeek(new Date(2026, 1, 1, 8, 0, 0, 0), 6);
|
|
58
|
-
const schedule = { mode: 'clock', hour: 9, minute: 0, days: parseDays('weekdays').days };
|
|
59
|
-
const next = new Date(nextClockRunAfter(schedule, saturdayBase.getTime()));
|
|
60
|
-
const monday = nextDayOfWeek(new Date(saturdayBase), 1);
|
|
61
|
-
|
|
62
|
-
assert.equal(next.getDay(), 1);
|
|
63
|
-
assert.equal(next.getHours(), 9);
|
|
64
|
-
assert.equal(next.getMinutes(), 0);
|
|
65
|
-
assert.equal(next.getDate(), monday.getDate());
|
|
66
|
-
assert.equal(next.getMonth(), monday.getMonth());
|
|
67
|
-
assert.equal(next.getFullYear(), monday.getFullYear());
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('builds interval or clock schedule from task config', () => {
|
|
71
|
-
const intervalTask = { name: 'a', interval: '2h' };
|
|
72
|
-
const clockTask = { name: 'b', at: '07:15', days: 'weekdays' };
|
|
73
|
-
|
|
74
|
-
const interval = buildTaskSchedule(intervalTask, () => 7200);
|
|
75
|
-
const clock = buildTaskSchedule(clockTask, () => 3600);
|
|
76
|
-
const invalid = buildTaskSchedule({ name: 'bad', at: '25:99' }, () => 3600);
|
|
77
|
-
|
|
78
|
-
assert.equal(interval.ok, true);
|
|
79
|
-
assert.equal(interval.schedule.mode, 'interval');
|
|
80
|
-
assert.equal(interval.schedule.intervalSec, 7200);
|
|
81
|
-
|
|
82
|
-
assert.equal(clock.ok, true);
|
|
83
|
-
assert.equal(clock.schedule.mode, 'clock');
|
|
84
|
-
assert.equal(clock.schedule.hour, 7);
|
|
85
|
-
assert.equal(clock.schedule.minute, 15);
|
|
86
|
-
assert.deepEqual([...clock.schedule.days], [1, 2, 3, 4, 5]);
|
|
87
|
-
|
|
88
|
-
assert.equal(invalid.ok, false);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('does catch-up for missed fixed-time runs and computes next run after execution', () => {
|
|
92
|
-
const task = { name: 'daily-report', at: '09:00' };
|
|
93
|
-
const schedule = { mode: 'clock', hour: 9, minute: 0, days: null };
|
|
94
|
-
const now = new Date(2026, 1, 25, 10, 0, 0, 0).getTime();
|
|
95
|
-
const yesterday = new Date(2026, 1, 24, 9, 0, 0, 0).toISOString();
|
|
96
|
-
const state = { tasks: { 'daily-report': { last_run: yesterday } } };
|
|
97
|
-
|
|
98
|
-
const initial = computeInitialNextRun(task, schedule, state, now, 60, 1);
|
|
99
|
-
const next = new Date(nextRunAfter(schedule, now));
|
|
100
|
-
|
|
101
|
-
assert.equal(initial, now);
|
|
102
|
-
assert.equal(next.getDate(), 26);
|
|
103
|
-
assert.equal(next.getHours(), 9);
|
|
104
|
-
assert.equal(next.getMinutes(), 0);
|
|
105
|
-
});
|
|
106
|
-
});
|