metame-cli 1.4.34 → 1.5.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.
Files changed (48) hide show
  1. package/README.md +136 -94
  2. package/index.js +312 -57
  3. package/package.json +8 -4
  4. package/scripts/agent-layer.js +320 -0
  5. package/scripts/daemon-admin-commands.js +328 -28
  6. package/scripts/daemon-agent-commands.js +145 -6
  7. package/scripts/daemon-agent-tools.js +163 -7
  8. package/scripts/daemon-bridges.js +110 -20
  9. package/scripts/daemon-checkpoints.js +36 -7
  10. package/scripts/daemon-claude-engine.js +849 -358
  11. package/scripts/daemon-command-router.js +31 -10
  12. package/scripts/daemon-default.yaml +28 -4
  13. package/scripts/daemon-engine-runtime.js +328 -0
  14. package/scripts/daemon-exec-commands.js +15 -7
  15. package/scripts/daemon-notify.js +37 -1
  16. package/scripts/daemon-ops-commands.js +8 -6
  17. package/scripts/daemon-runtime-lifecycle.js +129 -5
  18. package/scripts/daemon-session-commands.js +60 -25
  19. package/scripts/daemon-session-store.js +121 -13
  20. package/scripts/daemon-task-scheduler.js +129 -49
  21. package/scripts/daemon-user-acl.js +35 -9
  22. package/scripts/daemon.js +268 -33
  23. package/scripts/distill.js +327 -18
  24. package/scripts/docs/agent-guide.md +12 -0
  25. package/scripts/docs/maintenance-manual.md +155 -0
  26. package/scripts/docs/pointer-map.md +110 -0
  27. package/scripts/feishu-adapter.js +42 -13
  28. package/scripts/hooks/stop-session-capture.js +243 -0
  29. package/scripts/memory-extract.js +105 -6
  30. package/scripts/memory-nightly-reflect.js +199 -11
  31. package/scripts/memory.js +134 -3
  32. package/scripts/mentor-engine.js +405 -0
  33. package/scripts/platform.js +24 -0
  34. package/scripts/providers.js +182 -22
  35. package/scripts/schema.js +12 -0
  36. package/scripts/session-analytics.js +245 -12
  37. package/scripts/skill-changelog.js +245 -0
  38. package/scripts/skill-evolution.js +288 -5
  39. package/scripts/telegram-adapter.js +12 -8
  40. package/scripts/usage-classifier.js +1 -1
  41. package/scripts/daemon-admin-commands.test.js +0 -333
  42. package/scripts/daemon-task-envelope.test.js +0 -59
  43. package/scripts/daemon-task-scheduler.test.js +0 -106
  44. package/scripts/reliability-core.test.js +0 -280
  45. package/scripts/skill-evolution.test.js +0 -113
  46. package/scripts/task-board.test.js +0 -83
  47. package/scripts/test_daemon.js +0 -1407
  48. package/scripts/utils.test.js +0 -192
@@ -12,6 +12,8 @@ const path = require('path');
12
12
 
13
13
  const API_BASE = 'https://api.telegram.org';
14
14
 
15
+ const { StringDecoder } = require('string_decoder');
16
+
15
17
  /**
16
18
  * Make an HTTPS request to Telegram Bot API
17
19
  */
@@ -34,8 +36,10 @@ function apiRequest(token, method, params = {}, timeout = 10000, signal = null)
34
36
 
35
37
  const req = https.request(options, (res) => {
36
38
  let data = '';
37
- res.on('data', (chunk) => { data += chunk; });
39
+ const decoder = new StringDecoder('utf8');
40
+ res.on('data', (chunk) => { data += decoder.write(chunk); });
38
41
  res.on('end', () => {
42
+ data += decoder.end();
39
43
  try {
40
44
  const parsed = JSON.parse(data);
41
45
  if (parsed.ok) {
@@ -252,7 +256,7 @@ function createBot(token) {
252
256
  resolve(destPath);
253
257
  });
254
258
  fileStream.on('error', (err) => {
255
- fs.unlink(destPath, () => {});
259
+ fs.unlink(destPath, () => { });
256
260
  reject(err);
257
261
  });
258
262
  }).on('error', reject);
@@ -324,7 +328,7 @@ function createBot(token) {
324
328
  });
325
329
  },
326
330
 
327
- };
331
+ };
328
332
  }
329
333
 
330
334
  /**
@@ -369,11 +373,11 @@ function toTelegramMarkdownV2(md) {
369
373
  while ((m = pattern.exec(md)) !== null) {
370
374
  if (m.index > last) out += escapePlain(md.slice(last, m.index));
371
375
 
372
- if (m[1] !== undefined) out += '```' + m[1].replace(/[`\\]/g, '\\$&') + '```';
373
- else if (m[2] !== undefined) out += '`' + m[2].replace(/[`\\]/g, '\\$&') + '`';
374
- else if (m[3] !== undefined) out += '*' + toTelegramMarkdownV2(m[3]) + '*';
375
- else if (m[4] !== undefined) out += '_' + toTelegramMarkdownV2(m[4]) + '_';
376
- else if (m[5] !== undefined) out += '_' + toTelegramMarkdownV2(m[5]) + '_';
376
+ if (m[1] !== undefined) out += '```' + m[1].replace(/[`\\]/g, '\\$&') + '```';
377
+ else if (m[2] !== undefined) out += '`' + m[2].replace(/[`\\]/g, '\\$&') + '`';
378
+ else if (m[3] !== undefined) out += '*' + toTelegramMarkdownV2(m[3]) + '*';
379
+ else if (m[4] !== undefined) out += '_' + toTelegramMarkdownV2(m[4]) + '_';
380
+ else if (m[5] !== undefined) out += '_' + toTelegramMarkdownV2(m[5]) + '_';
377
381
  else if (m[6] !== undefined) out += '[' + toTelegramMarkdownV2(m[6]) + '](' + m[7].replace(/[()\\]/g, '\\$&') + ')';
378
382
  else if (m[9] !== undefined) out += '*' + escapePlain(m[9]) + '*';
379
383
  else if (m[10] !== undefined) {
@@ -94,7 +94,7 @@ function classifyTaskUsage(task, context = {}, opts = {}) {
94
94
 
95
95
  if (!joined) return fallbackCategory;
96
96
  if (/\bteam[-_\s]?task\b|团队|协作|handoff|dispatch/.test(joined)) return 'team_task';
97
- if (/\bskill[-_\s]?(?:evo|evolution|manager|scout)\b|技能演化/.test(joined)) return 'skill_evolution';
97
+ if (/\bskill[-_\s]?(?:evo|evolution|manager)\b|技能演化/.test(joined)) return 'skill_evolution';
98
98
  if (/\bmemory(?:-extract)?\b|记忆|facts?|recall|retriev|rag/.test(joined)) return 'memory';
99
99
  if (/\bdistill\b|\bcognition\b|认知|反思|洞察/.test(joined)) return 'cognition';
100
100
  if (/\bheartbeat\b|提醒|定时|cron|every\s+\d/.test(joined)) return 'heartbeat';
@@ -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
- });