amalgm 0.1.35 → 0.1.36
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +29 -11
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +6 -3
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +45 -0
- package/runtime/scripts/amalgm-mcp/tests/local-live-snapshot.test.js +44 -0
- package/runtime/scripts/amalgm-mcp/tests/scheduler.test.js +64 -0
package/package.json
CHANGED
|
@@ -52,20 +52,38 @@ function readResource(resource, cache) {
|
|
|
52
52
|
|
|
53
53
|
function buildSnapshot(resourcesInput) {
|
|
54
54
|
const resources = normalizeResources(resourcesInput);
|
|
55
|
-
|
|
56
|
-
const cache = {};
|
|
57
|
-
const data = {};
|
|
55
|
+
let lastUnstable = null;
|
|
58
56
|
|
|
59
|
-
for (
|
|
60
|
-
const
|
|
61
|
-
|
|
57
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
58
|
+
const beforeSeq = currentSeq();
|
|
59
|
+
const cache = {};
|
|
60
|
+
const data = {};
|
|
61
|
+
|
|
62
|
+
for (const resource of resources) {
|
|
63
|
+
const value = readResource(resource, cache);
|
|
64
|
+
if (value !== undefined) data[resource] = value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const afterSeq = currentSeq();
|
|
68
|
+
if (beforeSeq === afterSeq) {
|
|
69
|
+
return {
|
|
70
|
+
seq: afterSeq,
|
|
71
|
+
stable: true,
|
|
72
|
+
resources: data,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lastUnstable = {
|
|
77
|
+
seq: beforeSeq,
|
|
78
|
+
stable: false,
|
|
79
|
+
resources: data,
|
|
80
|
+
};
|
|
62
81
|
}
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
resources: data,
|
|
83
|
+
return lastUnstable || {
|
|
84
|
+
seq: currentSeq(),
|
|
85
|
+
stable: true,
|
|
86
|
+
resources: {},
|
|
69
87
|
};
|
|
70
88
|
}
|
|
71
89
|
|
|
@@ -57,7 +57,9 @@ function isTaskDue(task, now) {
|
|
|
57
57
|
currentDate: now,
|
|
58
58
|
tz: schedule.tz || 'UTC',
|
|
59
59
|
});
|
|
60
|
-
const prev = interval.
|
|
60
|
+
const prev = interval.includesDate(now)
|
|
61
|
+
? new Date(Math.floor(now.getTime() / 1000) * 1000)
|
|
62
|
+
: interval.prev().toDate();
|
|
61
63
|
const createdAt = validDate(task.createdAt);
|
|
62
64
|
const lastRunAt = validDate(task.lastRunAt);
|
|
63
65
|
|
|
@@ -68,8 +70,9 @@ function isTaskDue(task, now) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
if (schedule.kind === 'interval') {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
const baseline = validDate(task.lastRunAt) || validDate(task.createdAt);
|
|
74
|
+
if (!baseline) return true;
|
|
75
|
+
return now - baseline >= schedule.ms;
|
|
73
76
|
}
|
|
74
77
|
return false;
|
|
75
78
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
computeSignature,
|
|
8
|
+
extractSignature,
|
|
9
|
+
matchBySignature,
|
|
10
|
+
} = require('../events/matcher');
|
|
11
|
+
|
|
12
|
+
function trigger(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id: 'trigger-1',
|
|
15
|
+
enabled: true,
|
|
16
|
+
secret: 'super-secret',
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('event matcher accepts generic HMAC webhook signatures', () => {
|
|
22
|
+
const rawBody = JSON.stringify({ ok: true });
|
|
23
|
+
const signature = extractSignature({
|
|
24
|
+
'x-webhook-signature': computeSignature('super-secret', rawBody),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(matchBySignature([trigger()], signature, rawBody)?.id, 'trigger-1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('event matcher accepts bearer token webhook secrets', () => {
|
|
31
|
+
const signature = extractSignature({
|
|
32
|
+
authorization: 'Bearer super-secret',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(matchBySignature([trigger()], signature, '{}')?.id, 'trigger-1');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('event matcher rejects disabled triggers and wrong secrets', () => {
|
|
39
|
+
const rawBody = JSON.stringify({ ok: true });
|
|
40
|
+
const signature = extractSignature({
|
|
41
|
+
'x-webhook-signature': computeSignature('wrong-secret', rawBody),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.equal(matchBySignature([trigger(), trigger({ id: 'disabled', enabled: false })], signature, rawBody), null);
|
|
45
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
let seq = 0;
|
|
7
|
+
const eventsPath = require.resolve('../state/events');
|
|
8
|
+
require.cache[eventsPath] = {
|
|
9
|
+
id: eventsPath,
|
|
10
|
+
filename: eventsPath,
|
|
11
|
+
loaded: true,
|
|
12
|
+
exports: {
|
|
13
|
+
currentSeq: () => seq,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const taskStore = require('../tasks/store');
|
|
18
|
+
const { buildSnapshot } = require('../state/snapshot');
|
|
19
|
+
|
|
20
|
+
test.after(() => {
|
|
21
|
+
taskStore.loadTasks = originalLoadTasks;
|
|
22
|
+
delete require.cache[eventsPath];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const originalLoadTasks = taskStore.loadTasks;
|
|
26
|
+
|
|
27
|
+
test('snapshot retries when a state event lands during resource reads', () => {
|
|
28
|
+
let calls = 0;
|
|
29
|
+
taskStore.loadTasks = () => {
|
|
30
|
+
calls += 1;
|
|
31
|
+
if (calls === 1) {
|
|
32
|
+
seq = 1;
|
|
33
|
+
return { version: 1, tasks: [{ id: 'stale-task', name: 'Stale task' }] };
|
|
34
|
+
}
|
|
35
|
+
return { version: 1, tasks: [{ id: 'new-task', name: 'New task' }] };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const snapshot = buildSnapshot('tasks');
|
|
39
|
+
|
|
40
|
+
assert.equal(calls, 2);
|
|
41
|
+
assert.equal(snapshot.stable, true);
|
|
42
|
+
assert.equal(snapshot.seq, 1);
|
|
43
|
+
assert.deepEqual(snapshot.resources.tasks, [{ id: 'new-task', name: 'New task' }]);
|
|
44
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { isTaskDue } = require('../tasks/scheduler');
|
|
7
|
+
|
|
8
|
+
function baseTask(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id: 'task-1',
|
|
11
|
+
enabled: true,
|
|
12
|
+
createdAt: '2026-05-18T10:00:00.000Z',
|
|
13
|
+
lastRunAt: null,
|
|
14
|
+
endsAt: null,
|
|
15
|
+
schedule: { kind: 'once', at: '2026-05-18T10:01:00.000Z' },
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('interval task waits for the first interval after creation', () => {
|
|
21
|
+
const task = baseTask({
|
|
22
|
+
schedule: { kind: 'interval', ms: 60_000 },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:00:30.000Z')), false);
|
|
26
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:01:00.000Z')), true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('interval task uses lastRunAt after the first run', () => {
|
|
30
|
+
const task = baseTask({
|
|
31
|
+
lastRunAt: '2026-05-18T10:03:00.000Z',
|
|
32
|
+
schedule: { kind: 'interval', ms: 60_000 },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:03:59.000Z')), false);
|
|
36
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:04:00.000Z')), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('cron task is due at the exact scheduled boundary in its timezone', () => {
|
|
40
|
+
const task = baseTask({
|
|
41
|
+
createdAt: '2026-05-18T15:55:00.000Z',
|
|
42
|
+
schedule: {
|
|
43
|
+
kind: 'cron',
|
|
44
|
+
expr: '0 9 * * *',
|
|
45
|
+
tz: 'America/Los_Angeles',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T16:00:00.000Z')), true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('cron task does not rerun for the same scheduled instant', () => {
|
|
53
|
+
const task = baseTask({
|
|
54
|
+
createdAt: '2026-05-18T15:55:00.000Z',
|
|
55
|
+
lastRunAt: '2026-05-18T16:00:00.000Z',
|
|
56
|
+
schedule: {
|
|
57
|
+
kind: 'cron',
|
|
58
|
+
expr: '0 9 * * *',
|
|
59
|
+
tz: 'America/Los_Angeles',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T16:00:30.000Z')), false);
|
|
64
|
+
});
|