@kafca/agentdock 0.1.62 → 0.1.64
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 +4 -0
- package/dist/renderer/assets/{AutomationList-BgJgAdVO.js → AutomationList-uiR0seMA.js} +1 -1
- package/dist/renderer/assets/{Badge-Bq2hPVcR.js → Badge-BZwdECxe.js} +1 -1
- package/dist/renderer/assets/{Card-BPJNSZce.js → Card-JMY-xmLX.js} +1 -1
- package/dist/renderer/assets/{Config-aSFqlI88.js → Config-BMcWRfWu.js} +1 -1
- package/dist/renderer/assets/{Dashboard-D3ezFmb7.js → Dashboard-CSk6wUMx.js} +2 -2
- package/dist/renderer/assets/{EmptyState-DK496gBq.js → EmptyState-3J94zJRa.js} +1 -1
- package/dist/renderer/assets/{HighlightedMarkdown-Ckre5jNv.js → HighlightedMarkdown-DKL7rO-G.js} +1 -1
- package/dist/renderer/assets/{Input-C5C1d0ba.js → Input-BGK524F9.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-N8Ey1ikc.js → KnowledgeDetail-XZ9sxTBY.js} +1 -1
- package/dist/renderer/assets/{KnowledgeHome-9aF8nJEf.js → KnowledgeHome-CJlGiu7a.js} +1 -1
- package/dist/renderer/assets/{Logs-sgAdwEeS.js → Logs-B5Y0yspB.js} +1 -1
- package/dist/renderer/assets/{Modal-BB10hTRc.js → Modal-DlIfXQ8_.js} +1 -1
- package/dist/renderer/assets/{Page-DfC319Ck.js → Page-B1i9DSW-.js} +1 -1
- package/dist/renderer/assets/{Select-RHJBmNER.js → Select-DDpZ4sBt.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-BU1zCdr4.js → ThreadChat-D63t_k2d.js} +3 -3
- package/dist/renderer/assets/{Workspace-BoZCz7S8.js → Workspace-BPkGxjuF.js} +1 -1
- package/dist/renderer/assets/{arrow-left-D2V1eZSD.js → arrow-left-C6VSgxEk.js} +1 -1
- package/dist/renderer/assets/{book-open-DUcBn7Bb.js → book-open-DWJZSr5x.js} +1 -1
- package/dist/renderer/assets/{channels-DMhDSlf2.js → channels-Dkik3Wxt.js} +1 -1
- package/dist/renderer/assets/{chevron-down-Ca2FQDB7.js → chevron-down-BHtOK5fW.js} +1 -1
- package/dist/renderer/assets/{index-DRJ5wgw_.js → index-DV9qeL9P.js} +2 -2
- package/dist/renderer/assets/{index-BlnvlekJ.js → index-TTj4JT0d.js} +1 -1
- package/dist/renderer/assets/{knowledge-D1-k56eY.js → knowledge-C9mJpMmJ.js} +1 -1
- package/dist/renderer/assets/{pencil-BhWdv_86.js → pencil-B7bBxE-w.js} +1 -1
- package/dist/renderer/assets/{plus-BacLMlK3.js → plus-DPOTxoL2.js} +1 -1
- package/dist/renderer/assets/{save-Bpoxbzau.js → save-Drzhy5En.js} +1 -1
- package/dist/renderer/assets/{search-BVJcpdFX.js → search-m-2mVuF6.js} +1 -1
- package/dist/renderer/assets/{shield-check-C2cw_Ip6.js → shield-check-C8NOMoui.js} +1 -1
- package/dist/renderer/assets/{threads-DOsBQBg_.js → threads-BG4F9grx.js} +1 -1
- package/dist/renderer/assets/{trash-2-Bs3rEm3g.js → trash-2-Bh0nSQUA.js} +1 -1
- package/dist/renderer/index.html +1 -1
- package/dist-electron/services/local-ai-core/src/acp/store/automation-store.js +69 -3
- package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +14 -2
- package/dist-electron/services/local-ai-core/src/automation/automation-monitor-service.js +5 -3
- package/dist-electron/services/local-ai-core/src/automation/automation-service.js +13 -4
- package/dist-electron/services/local-ai-core/src/automation/automation-trigger-engine.js +19 -18
- package/dist-electron/services/local-ai-core/src/automation/legacy-automation-mappers.js +25 -1
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +15 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +13 -3
- package/dist-electron/services/local-ai-core/src/scheduler/cron.js +197 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +4 -3
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +3 -3
- package/dist-electron/tests/electron/automation-compatibility.test.js +6 -1
- package/dist-electron/tests/electron/automation-engine.test.js +67 -3
- package/dist-electron/tests/electron/automation-sandbox-runner.test.js +20 -9
- package/dist-electron/tests/electron/automation-store.test.js +189 -0
- package/package.json +1 -1
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.compileCronExpression = compileCronExpression;
|
|
4
|
+
exports.extractFieldsInTimezone = extractFieldsInTimezone;
|
|
5
|
+
exports.cronMatchesFieldsDay = cronMatchesFieldsDay;
|
|
4
6
|
exports.cronMatchesFields = cronMatchesFields;
|
|
5
7
|
exports.cronMatchesDate = cronMatchesDate;
|
|
6
8
|
exports.findNextCronMatchUtc = findNextCronMatchUtc;
|
|
7
9
|
exports.findPreviousCronMatchUtc = findPreviousCronMatchUtc;
|
|
10
|
+
exports.findNextCronMatchInTimezone = findNextCronMatchInTimezone;
|
|
11
|
+
exports.findPreviousCronMatchInTimezone = findPreviousCronMatchInTimezone;
|
|
8
12
|
exports.floorToMinute = floorToMinute;
|
|
13
|
+
exports.isValidTimezone = isValidTimezone;
|
|
14
|
+
exports.assertSupportedTimezone = assertSupportedTimezone;
|
|
9
15
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
10
16
|
const GREGORIAN_CYCLE_DAYS = 146_097;
|
|
11
17
|
const MIN_DATE_MS = -8_640_000_000_000_000;
|
|
@@ -25,6 +31,62 @@ function compileCronExpression(expression) {
|
|
|
25
31
|
daysOfWeek: compileField(normalized, 'day-of-week', fields[4], 0, 6),
|
|
26
32
|
});
|
|
27
33
|
}
|
|
34
|
+
function extractFieldsInTimezone(date, timezone) {
|
|
35
|
+
return stripYear(extractWallClock(date, timezone));
|
|
36
|
+
}
|
|
37
|
+
function extractWallClock(date, timezone) {
|
|
38
|
+
const parts = getFormatter(timezone).formatToParts(date);
|
|
39
|
+
const map = {};
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
if (part.type !== 'literal')
|
|
42
|
+
map[part.type] = part.value;
|
|
43
|
+
}
|
|
44
|
+
const weekday = String(map.weekday ?? '').toLowerCase();
|
|
45
|
+
const dayOfWeek = WEEKDAY_SHORT_TO_NUMBER[weekday];
|
|
46
|
+
return {
|
|
47
|
+
year: Number(map.year ?? '1970'),
|
|
48
|
+
minute: Number(map.minute ?? '0'),
|
|
49
|
+
hour: Number(map.hour ?? '0'),
|
|
50
|
+
dayOfMonth: Number(map.day ?? '1'),
|
|
51
|
+
month: Number(map.month ?? '1'),
|
|
52
|
+
dayOfWeek: dayOfWeek ?? 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function stripYear(fields) {
|
|
56
|
+
return {
|
|
57
|
+
minute: fields.minute,
|
|
58
|
+
hour: fields.hour,
|
|
59
|
+
dayOfMonth: fields.dayOfMonth,
|
|
60
|
+
month: fields.month,
|
|
61
|
+
dayOfWeek: fields.dayOfWeek,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Convert a wall-clock time (Y/M/D h:m) in `timezone` to the matching UTC instant.
|
|
65
|
+
// Returns null when that wall clock never exists on that day (DST spring-forward gap),
|
|
66
|
+
// so callers can treat the slot as non-matching.
|
|
67
|
+
function wallClockToUtc(year, month, dayOfMonth, hour, minute, timezone) {
|
|
68
|
+
const targetWallMs = Date.UTC(year, month - 1, dayOfMonth, hour, minute, 0, 0);
|
|
69
|
+
let utc = targetWallMs;
|
|
70
|
+
for (let i = 0; i < 5; i += 1) {
|
|
71
|
+
const f = extractWallClock(new Date(utc), timezone);
|
|
72
|
+
const wallAsUtc = Date.UTC(f.year, f.month - 1, f.dayOfMonth, f.hour, f.minute, 0, 0);
|
|
73
|
+
if (wallAsUtc === targetWallMs) {
|
|
74
|
+
// Round-trip verified — this instant really is the requested wall clock in `timezone`.
|
|
75
|
+
return utc;
|
|
76
|
+
}
|
|
77
|
+
// Fixed-point step: shift utc by the gap between where its wall clock lands and the target.
|
|
78
|
+
utc = utc - wallAsUtc + targetWallMs;
|
|
79
|
+
}
|
|
80
|
+
// Did not converge to a self-consistent wall clock: the requested time is in a DST gap.
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
// Day-level match: day-of-month AND month AND day-of-week (cron DOM+DOW AND semantics).
|
|
84
|
+
// Hour and minute are matched separately once a wall-clock day qualifies.
|
|
85
|
+
function cronMatchesFieldsDay(compiled, fields) {
|
|
86
|
+
return compiled.daysOfMonth.includes(fields.dayOfMonth)
|
|
87
|
+
&& compiled.months.includes(fields.month)
|
|
88
|
+
&& compiled.daysOfWeek.includes(fields.dayOfWeek);
|
|
89
|
+
}
|
|
28
90
|
function cronMatchesFields(compiled, fields) {
|
|
29
91
|
// Preserve the legacy scheduler's DOM+DOW AND behavior.
|
|
30
92
|
return compiled.minutes.includes(fields.minute)
|
|
@@ -107,6 +169,84 @@ function findPreviousCronMatchUtc(compiled, atOrBefore, afterExclusive) {
|
|
|
107
169
|
}
|
|
108
170
|
return { date: null, inspectedDays: GREGORIAN_CYCLE_DAYS + 1 };
|
|
109
171
|
}
|
|
172
|
+
// Timezone-aware variants: iterate in the target timezone's wall clock rather than UTC.
|
|
173
|
+
// These replace the UTC-only search used by the activation engine when timezone !== 'UTC'.
|
|
174
|
+
function findNextCronMatchInTimezone(compiled, after, timezone) {
|
|
175
|
+
assertSupportedTimezone(timezone);
|
|
176
|
+
const afterMs = validDateMs(after, 'cron search date');
|
|
177
|
+
const startFields = extractWallClock(after, timezone);
|
|
178
|
+
// Begin at the start of the current wall-clock day so a later match on the same day is found.
|
|
179
|
+
const startDay = wallClockToUtc(startFields.year, startFields.month, startFields.dayOfMonth, 0, 0, timezone);
|
|
180
|
+
let dayMs = startDay ?? startOfUtcDay(Date.UTC(startFields.year, startFields.month - 1, startFields.dayOfMonth, 0, 0, 0, 0));
|
|
181
|
+
for (let inspectedDays = 1; inspectedDays <= GREGORIAN_CYCLE_DAYS + 1; inspectedDays += 1) {
|
|
182
|
+
if (dayMs > MAX_DATE_MS)
|
|
183
|
+
return { date: null, inspectedDays };
|
|
184
|
+
const dayFields = extractWallClock(new Date(dayMs), timezone);
|
|
185
|
+
if (cronMatchesFieldsDay(compiled, stripYear(dayFields))) {
|
|
186
|
+
for (const hour of compiled.hours) {
|
|
187
|
+
for (const minute of compiled.minutes) {
|
|
188
|
+
const candidateMs = wallClockToUtc(dayFields.year, dayFields.month, dayFields.dayOfMonth, hour, minute, timezone);
|
|
189
|
+
if (candidateMs !== null && candidateMs > afterMs && candidateMs <= MAX_DATE_MS) {
|
|
190
|
+
return { date: new Date(candidateMs), inspectedDays };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const nextDay = wallClockToUtc(dayFields.year, dayFields.month, dayFields.dayOfMonth + 1, 0, 0, timezone);
|
|
196
|
+
// A null here only means the next wall-clock midnight is in a DST gap (a handful of
|
|
197
|
+
// zones transition at midnight). Fall back to 24 h later — the loop extracts the wall-clock
|
|
198
|
+
// day from dayMs itself, so a one-day-probe offset cannot drop or duplicate a match.
|
|
199
|
+
if (nextDay === null) {
|
|
200
|
+
dayMs += DAY_MS;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
dayMs = nextDay;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { date: null, inspectedDays: GREGORIAN_CYCLE_DAYS + 1 };
|
|
207
|
+
}
|
|
208
|
+
function findPreviousCronMatchInTimezone(compiled, atOrBefore, timezone, afterExclusive) {
|
|
209
|
+
assertSupportedTimezone(timezone);
|
|
210
|
+
const atOrBeforeMs = validDateMs(atOrBefore, 'cron search date');
|
|
211
|
+
const afterExclusiveMs = afterExclusive ? validDateMs(afterExclusive, 'cron search lower bound') : undefined;
|
|
212
|
+
if (afterExclusiveMs !== undefined && afterExclusiveMs >= atOrBeforeMs) {
|
|
213
|
+
return { date: null, inspectedDays: 0 };
|
|
214
|
+
}
|
|
215
|
+
const upperFields = extractWallClock(atOrBefore, timezone);
|
|
216
|
+
const upperDay = wallClockToUtc(upperFields.year, upperFields.month, upperFields.dayOfMonth, 0, 0, timezone);
|
|
217
|
+
let dayMs = upperDay ?? startOfUtcDay(Date.UTC(upperFields.year, upperFields.month - 1, upperFields.dayOfMonth, 0, 0, 0, 0));
|
|
218
|
+
for (let inspectedDays = 1; inspectedDays <= GREGORIAN_CYCLE_DAYS + 1; inspectedDays += 1) {
|
|
219
|
+
if (dayMs < MIN_DATE_MS)
|
|
220
|
+
return { date: null, inspectedDays };
|
|
221
|
+
if (afterExclusiveMs !== undefined && dayMs <= afterExclusiveMs) {
|
|
222
|
+
return { date: null, inspectedDays: inspectedDays - 1 };
|
|
223
|
+
}
|
|
224
|
+
const dayFields = extractWallClock(new Date(dayMs), timezone);
|
|
225
|
+
if (cronMatchesFieldsDay(compiled, stripYear(dayFields))) {
|
|
226
|
+
for (let hourIndex = compiled.hours.length - 1; hourIndex >= 0; hourIndex -= 1) {
|
|
227
|
+
for (let minuteIndex = compiled.minutes.length - 1; minuteIndex >= 0; minuteIndex -= 1) {
|
|
228
|
+
const candidateMs = wallClockToUtc(dayFields.year, dayFields.month, dayFields.dayOfMonth, compiled.hours[hourIndex], compiled.minutes[minuteIndex], timezone);
|
|
229
|
+
if (candidateMs !== null
|
|
230
|
+
&& candidateMs <= atOrBeforeMs
|
|
231
|
+
&& candidateMs >= MIN_DATE_MS
|
|
232
|
+
&& (afterExclusiveMs === undefined || candidateMs > afterExclusiveMs)) {
|
|
233
|
+
return { date: new Date(candidateMs), inspectedDays };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const prevDay = wallClockToUtc(dayFields.year, dayFields.month, dayFields.dayOfMonth - 1, 0, 0, timezone);
|
|
239
|
+
// See findNextCronMatchInTimezone: a null only marks a DST-at-midnight quirk, not the end
|
|
240
|
+
// of schedulable time. Step back 24 h and let the loop read the wall-clock day from dayMs.
|
|
241
|
+
if (prevDay === null) {
|
|
242
|
+
dayMs -= DAY_MS;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
dayMs = prevDay;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { date: null, inspectedDays: GREGORIAN_CYCLE_DAYS + 1 };
|
|
249
|
+
}
|
|
110
250
|
function floorToMinute(date) {
|
|
111
251
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), 0, 0);
|
|
112
252
|
}
|
|
@@ -163,3 +303,60 @@ function validDateMs(date, label) {
|
|
|
163
303
|
function invalidCron(expression, reason) {
|
|
164
304
|
return new Error(`Invalid cron expression "${expression}": ${reason}.`);
|
|
165
305
|
}
|
|
306
|
+
const WEEKDAY_SHORT_TO_NUMBER = {
|
|
307
|
+
sun: 0,
|
|
308
|
+
mon: 1,
|
|
309
|
+
tue: 2,
|
|
310
|
+
wed: 3,
|
|
311
|
+
thu: 4,
|
|
312
|
+
fri: 5,
|
|
313
|
+
sat: 6,
|
|
314
|
+
};
|
|
315
|
+
// formatToParts is dramatically faster when the formatter is reused; cache one per zone.
|
|
316
|
+
const formatterCache = new Map();
|
|
317
|
+
function getFormatter(timezone) {
|
|
318
|
+
const cached = formatterCache.get(timezone);
|
|
319
|
+
if (cached)
|
|
320
|
+
return cached;
|
|
321
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
322
|
+
timeZone: timezone,
|
|
323
|
+
hourCycle: 'h23',
|
|
324
|
+
minute: '2-digit',
|
|
325
|
+
hour: '2-digit',
|
|
326
|
+
day: '2-digit',
|
|
327
|
+
month: '2-digit',
|
|
328
|
+
weekday: 'short',
|
|
329
|
+
year: 'numeric',
|
|
330
|
+
});
|
|
331
|
+
formatterCache.set(timezone, formatter);
|
|
332
|
+
return formatter;
|
|
333
|
+
}
|
|
334
|
+
// Intl.supportedValuesOf('timeZone') builds a full IANA list on first call; cache it per runtime.
|
|
335
|
+
let supportedTimezones = null;
|
|
336
|
+
function getSupportedTimezones() {
|
|
337
|
+
if (supportedTimezones)
|
|
338
|
+
return supportedTimezones;
|
|
339
|
+
supportedTimezones = Intl.supportedValuesOf ? new Set(Intl.supportedValuesOf('timeZone')) : null;
|
|
340
|
+
return supportedTimezones;
|
|
341
|
+
}
|
|
342
|
+
function isValidTimezone(timezone) {
|
|
343
|
+
// "UTC"/"Z" are valid cron timezones but may be missing from Intl's list (which uses "Etc/UTC").
|
|
344
|
+
if (timezone === 'UTC' || timezone === 'Z' || timezone === 'Etc/UTC')
|
|
345
|
+
return true;
|
|
346
|
+
const supported = getSupportedTimezones();
|
|
347
|
+
if (supported)
|
|
348
|
+
return supported.has(timezone);
|
|
349
|
+
// Fallback for old runtimes: let the formatter constructor throw on bad zones.
|
|
350
|
+
try {
|
|
351
|
+
Intl.DateTimeFormat('en-US', { timeZone: timezone });
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function assertSupportedTimezone(timezone) {
|
|
359
|
+
if (!isValidTimezone(timezone)) {
|
|
360
|
+
throw new Error(`Automation scheduling received an unsupported timezone: ${timezone}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js
CHANGED
|
@@ -10,9 +10,10 @@ class ScheduledJobApplicationService {
|
|
|
10
10
|
this.options = options;
|
|
11
11
|
}
|
|
12
12
|
listJobs(workspaceId) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.
|
|
13
|
+
const latestRunById = this.options.automations.listLatestRunByOrigin('scheduled-job', workspaceId);
|
|
14
|
+
return this.options.automations
|
|
15
|
+
.list(workspaceId, 'scheduled-job')
|
|
16
|
+
.map((automation) => (0, legacy_automation_mappers_js_1.automationToScheduledJob)(automation, latestRunById.get(automation.id)));
|
|
16
17
|
}
|
|
17
18
|
getJob(jobId) {
|
|
18
19
|
const resolved = this.resolveJobId(jobId);
|
|
@@ -41,9 +41,9 @@ class SchedulerService extends node_events_1.EventEmitter {
|
|
|
41
41
|
listJobs(workspaceId) {
|
|
42
42
|
const automations = this.options.automations;
|
|
43
43
|
if (automations) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.map((automation) => (0, legacy_automation_mappers_js_1.automationToScheduledJob)(automation,
|
|
44
|
+
const latestRunById = automations.listLatestRunByOrigin('scheduled-job', workspaceId);
|
|
45
|
+
return automations.list(workspaceId, 'scheduled-job')
|
|
46
|
+
.map((automation) => (0, legacy_automation_mappers_js_1.automationToScheduledJob)(automation, latestRunById.get(automation.id)));
|
|
47
47
|
}
|
|
48
48
|
return this.options.store.listScheduledJobs(workspaceId);
|
|
49
49
|
}
|
|
@@ -53,7 +53,12 @@ function fixture(executeAction, conditionEvaluator) {
|
|
|
53
53
|
description: 'Daily report',
|
|
54
54
|
enabled: true,
|
|
55
55
|
});
|
|
56
|
-
|
|
56
|
+
// Legacy cron jobs default to the host's local IANA timezone (matches the old
|
|
57
|
+
// SchedulerService, which matched cron in local wall clock). Any valid IANA zone is correct.
|
|
58
|
+
strict_1.default.equal(input.activation.kind, 'cron');
|
|
59
|
+
strict_1.default.equal(input.activation.expression, '0 9 * * *');
|
|
60
|
+
strict_1.default.equal(typeof input.activation.timezone, 'string');
|
|
61
|
+
strict_1.default.ok(input.activation.timezone.length > 0, 'timezone should be a non-empty IANA zone');
|
|
57
62
|
strict_1.default.deepEqual(input.condition, { kind: 'always' });
|
|
58
63
|
strict_1.default.equal(input.originKind, 'scheduled-job');
|
|
59
64
|
strict_1.default.equal(input.action.executionMode, 'same-thread');
|
|
@@ -62,8 +62,13 @@ const at = (value) => new Date(value);
|
|
|
62
62
|
strict_1.default.equal((0, automation_trigger_engine_js_1.isActivationDue)(activation, now, '2026-07-05T09:00:00.000Z'), false);
|
|
63
63
|
strict_1.default.equal((0, automation_trigger_engine_js_1.missedActivationAt)(activation, undefined, now), null);
|
|
64
64
|
});
|
|
65
|
-
(0, node_test_1.default)('cron
|
|
66
|
-
|
|
65
|
+
(0, node_test_1.default)('cron now supports IANA timezones beyond UTC', () => {
|
|
66
|
+
// Shanghai is UTC+8 with no DST: next match of "0 1 * * *" after 2026-07-15T00:00Z
|
|
67
|
+
// (08:00 CST) should be 2026-07-16 01:00 CST = 2026-07-15T17:00Z.
|
|
68
|
+
strict_1.default.equal((0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: '0 1 * * *', timezone: 'Asia/Shanghai' }, at('2026-07-15T00:00:00.000Z'))?.toISOString(), '2026-07-15T17:00:00.000Z');
|
|
69
|
+
});
|
|
70
|
+
(0, node_test_1.default)('cron rejects unknown timezones', () => {
|
|
71
|
+
strict_1.default.throws(() => (0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: '0 9 * * *', timezone: 'Not/AZone' }, at('2026-07-05T00:00:00.000Z')), /unsupported timezone/i);
|
|
67
72
|
});
|
|
68
73
|
(0, node_test_1.default)('cron compilation is strict and all activation entry points reject invalid expressions', () => {
|
|
69
74
|
const invalidExpressions = [
|
|
@@ -129,7 +134,66 @@ const at = (value) => new Date(value);
|
|
|
129
134
|
});
|
|
130
135
|
(0, node_test_1.default)('cron search does not return dates outside the JavaScript Date range', () => {
|
|
131
136
|
const activation = { kind: 'cron', expression: '* * * * *', timezone: 'UTC' };
|
|
132
|
-
strict_1.default.throws(() => (0, automation_trigger_engine_js_1.nextActivationAt)(activation, new Date(8_640_000_000_000_000)), /no
|
|
137
|
+
strict_1.default.throws(() => (0, automation_trigger_engine_js_1.nextActivationAt)(activation, new Date(8_640_000_000_000_000)), /no cron activation exists/i);
|
|
138
|
+
});
|
|
139
|
+
(0, node_test_1.default)('timezone-aware cron next match honors the target zone', () => {
|
|
140
|
+
// 0 1 * * * in Asia/Shanghai (UTC+8) after 2026-07-15T00:00Z (08:00 CST) → next 01:00 CST.
|
|
141
|
+
strict_1.default.equal((0, cron_js_1.findNextCronMatchInTimezone)((0, cron_js_1.compileCronExpression)('0 1 * * *'), at('2026-07-15T00:00:00.000Z'), 'Asia/Shanghai').date?.toISOString(), '2026-07-15T17:00:00.000Z');
|
|
142
|
+
});
|
|
143
|
+
(0, node_test_1.default)('timezone-aware cron next match lands later when the target-zone hour has already passed', () => {
|
|
144
|
+
// After 2026-07-15T18:00Z, Shanghai wall clock is 2026-07-16 02:00. Next 01:00 CST is a day later.
|
|
145
|
+
strict_1.default.equal((0, cron_js_1.findNextCronMatchInTimezone)((0, cron_js_1.compileCronExpression)('0 1 * * *'), at('2026-07-15T18:00:00.000Z'), 'Asia/Shanghai').date?.toISOString(), '2026-07-16T17:00:00.000Z');
|
|
146
|
+
});
|
|
147
|
+
(0, node_test_1.default)('timezone-aware previous match finds the most recent wall-clock hit', () => {
|
|
148
|
+
// At 2026-07-15T05:00Z (13:00 CST), previous 01:00 CST is 2026-07-14T17:00Z.
|
|
149
|
+
strict_1.default.equal((0, cron_js_1.findPreviousCronMatchInTimezone)((0, cron_js_1.compileCronExpression)('0 1 * * *'), at('2026-07-15T05:00:00.000Z'), 'Asia/Shanghai').date?.toISOString(), '2026-07-14T17:00:00.000Z');
|
|
150
|
+
});
|
|
151
|
+
(0, node_test_1.default)('extractFieldsInTimezone reads wall clock in the given zone', () => {
|
|
152
|
+
// 2026-07-15T09:00Z = 2026-07-15 17:00 CST = a Wednesday (dayOfWeek 3).
|
|
153
|
+
const fields = (0, cron_js_1.extractFieldsInTimezone)(at('2026-07-15T09:00:00.000Z'), 'Asia/Shanghai');
|
|
154
|
+
strict_1.default.equal(fields.hour, 17);
|
|
155
|
+
strict_1.default.equal(fields.minute, 0);
|
|
156
|
+
strict_1.default.equal(fields.dayOfMonth, 15);
|
|
157
|
+
strict_1.default.equal(fields.month, 7);
|
|
158
|
+
strict_1.default.equal(fields.dayOfWeek, 3);
|
|
159
|
+
});
|
|
160
|
+
(0, node_test_1.default)('extractFieldsInTimezone accounts for DST (America/New_York)', () => {
|
|
161
|
+
// 2026-07-15 is in EDT (UTC-4): 09:00Z = 05:00 local.
|
|
162
|
+
const fields = (0, cron_js_1.extractFieldsInTimezone)(at('2026-07-15T09:00:00.000Z'), 'America/New_York');
|
|
163
|
+
strict_1.default.equal(fields.hour, 5);
|
|
164
|
+
strict_1.default.equal(fields.minute, 0);
|
|
165
|
+
strict_1.default.equal(fields.dayOfMonth, 15);
|
|
166
|
+
// 2026-01-15 is in EST (UTC-5): 09:00Z = 04:00 local — DST shift confirmed.
|
|
167
|
+
const winter = (0, cron_js_1.extractFieldsInTimezone)(at('2026-01-15T09:00:00.000Z'), 'America/New_York');
|
|
168
|
+
strict_1.default.equal(winter.hour, 4);
|
|
169
|
+
});
|
|
170
|
+
(0, node_test_1.default)('cron next activation skips a DST gap and returns the next valid wall clock', () => {
|
|
171
|
+
// US spring-forward 2026: clocks jump from 02:00 to 03:00 local on 2026-03-08.
|
|
172
|
+
// A cron of "0 2 * * *" has no valid 02:00 local that day; the next match is 02:00
|
|
173
|
+
// the following day (2026-03-09), which does exist.
|
|
174
|
+
strict_1.default.equal((0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: '0 2 * * *', timezone: 'America/New_York' }, at('2026-03-07T07:00:00.000Z'))?.toISOString(), '2026-03-09T06:00:00.000Z');
|
|
175
|
+
});
|
|
176
|
+
(0, node_test_1.default)('cron resolves ambiguous fall-back wall clocks to a single instant', () => {
|
|
177
|
+
// US fall-back 2026: 2026-11-01 01:00 local happens twice (EDT 05:00Z then EST 06:00Z).
|
|
178
|
+
// The engine must resolve "0 1 * * *" to exactly one instant, not two or zero.
|
|
179
|
+
const first = (0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: '0 1 * * *', timezone: 'America/New_York' }, at('2026-10-31T05:00:00.000Z'))?.toISOString();
|
|
180
|
+
strict_1.default.ok(first === '2026-11-01T05:00:00.000Z' || first === '2026-11-01T06:00:00.000Z', `fall-back resolved to ${first}`);
|
|
181
|
+
// And the following day still triggers exactly once, at EST 01:00 = 06:00Z.
|
|
182
|
+
strict_1.default.equal((0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: '0 1 * * *', timezone: 'America/New_York' }, at('2026-11-01T06:00:00.000Z'))?.toISOString(), '2026-11-02T06:00:00.000Z');
|
|
183
|
+
});
|
|
184
|
+
(0, node_test_1.default)('cron accepts Z and Etc/UTC as aliases of UTC', () => {
|
|
185
|
+
// All three aliases must produce identical, UTC-aligned matches.
|
|
186
|
+
const after = at('2026-07-15T00:00:00.000Z');
|
|
187
|
+
const expr = '0 * * * *';
|
|
188
|
+
const viaUtc = (0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: expr, timezone: 'UTC' }, after)?.toISOString();
|
|
189
|
+
strict_1.default.equal(viaUtc, '2026-07-15T01:00:00.000Z');
|
|
190
|
+
strict_1.default.equal((0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: expr, timezone: 'Z' }, after)?.toISOString(), viaUtc, 'Z alias should match UTC');
|
|
191
|
+
strict_1.default.equal((0, automation_trigger_engine_js_1.nextActivationAt)({ kind: 'cron', expression: expr, timezone: 'Etc/UTC' }, after)?.toISOString(), viaUtc, 'Etc/UTC alias should match UTC');
|
|
192
|
+
});
|
|
193
|
+
(0, node_test_1.default)('timezone-aware search handles a half-hour offset zone', () => {
|
|
194
|
+
// Pacific/Chatham is UTC+12:45 in winter (July). 2026-07-16 00:00 local = 2026-07-15T11:15Z,
|
|
195
|
+
// so a "0 0 * * *" cron resolved right after 2026-07-15T23:00Z lands on the next calendar day.
|
|
196
|
+
strict_1.default.equal((0, cron_js_1.findNextCronMatchInTimezone)((0, cron_js_1.compileCronExpression)('0 0 * * *'), at('2026-07-15T23:00:00.000Z'), 'Pacific/Chatham').date?.toISOString(), '2026-07-16T11:15:00.000Z');
|
|
133
197
|
});
|
|
134
198
|
(0, node_test_1.default)('restart recovery returns only the most recent missed activation', () => {
|
|
135
199
|
const now = at('2026-07-05T10:05:30.000Z');
|
|
@@ -51,6 +51,17 @@ function input(root, overrides = {}) {
|
|
|
51
51
|
...overrides,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
function createFakeRunner(root, manager = new FakeManager()) {
|
|
55
|
+
return new anthropic_sandbox_runner_js_1.AnthropicSandboxRunner({
|
|
56
|
+
platform: process.platform,
|
|
57
|
+
manager,
|
|
58
|
+
tempRoot: root,
|
|
59
|
+
// These execution-path tests use FakeManager and must not depend on
|
|
60
|
+
// packages installed on the host runner. Capability-specific behavior is
|
|
61
|
+
// covered by the explicit macOS/Linux probe tests above.
|
|
62
|
+
commandExists: () => true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
54
65
|
(0, node_test_1.default)('builds read-only package and temp-only write policy', () => {
|
|
55
66
|
const root = '/workspace/automation';
|
|
56
67
|
const config = (0, anthropic_sandbox_runner_js_1.buildSandboxRuntimeConfig)(input(root, {
|
|
@@ -215,7 +226,7 @@ function input(root, overrides = {}) {
|
|
|
215
226
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-runner-'));
|
|
216
227
|
try {
|
|
217
228
|
const manager = new FakeManager();
|
|
218
|
-
const runner =
|
|
229
|
+
const runner = createFakeRunner(root, manager);
|
|
219
230
|
const result = await runner.run({ ...input(root), env: { PATH: (0, node_path_1.join)(root, 'fake-bin') }, allowedEnv: ['PATH'] });
|
|
220
231
|
strict_1.default.equal(result.exitCode, 0);
|
|
221
232
|
strict_1.default.equal(result.stdout, 'sandbox-ok');
|
|
@@ -229,7 +240,7 @@ function input(root, overrides = {}) {
|
|
|
229
240
|
(0, node_test_1.default)('enforces streamed output ceilings before buffering unbounded output', async () => {
|
|
230
241
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-runner-'));
|
|
231
242
|
try {
|
|
232
|
-
const runner =
|
|
243
|
+
const runner = createFakeRunner(root);
|
|
233
244
|
const result = await runner.run({ ...input(root, { command: 'yes output', stdoutBytes: 64, timeoutMs: 5_000 }) });
|
|
234
245
|
strict_1.default.equal(result.outputLimitExceeded, 'stdout');
|
|
235
246
|
strict_1.default.ok(Buffer.byteLength(result.stdout, 'utf8') <= 64);
|
|
@@ -241,7 +252,7 @@ function input(root, overrides = {}) {
|
|
|
241
252
|
(0, node_test_1.default)('stream truncation respects UTF-8 byte limits without replacement characters', async () => {
|
|
242
253
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-runner-'));
|
|
243
254
|
try {
|
|
244
|
-
const runner =
|
|
255
|
+
const runner = createFakeRunner(root);
|
|
245
256
|
const result = await runner.run({ ...input(root, { command: "printf '€€'", stdoutBytes: 4, timeoutMs: 5_000 }) });
|
|
246
257
|
strict_1.default.equal(result.outputLimitExceeded, 'stdout');
|
|
247
258
|
strict_1.default.ok(Buffer.byteLength(result.stdout, 'utf8') <= 4);
|
|
@@ -254,7 +265,7 @@ function input(root, overrides = {}) {
|
|
|
254
265
|
(0, node_test_1.default)('stream truncation preserves a complete replacement character before overflow', async () => {
|
|
255
266
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-runner-'));
|
|
256
267
|
try {
|
|
257
|
-
const runner =
|
|
268
|
+
const runner = createFakeRunner(root);
|
|
258
269
|
const result = await runner.run({ ...input(root, { command: "printf '�€'", stdoutBytes: 5, timeoutMs: 5_000 }) });
|
|
259
270
|
strict_1.default.equal(result.stdout, '�');
|
|
260
271
|
strict_1.default.equal(Buffer.byteLength(result.stdout, 'utf8'), 3);
|
|
@@ -266,7 +277,7 @@ function input(root, overrides = {}) {
|
|
|
266
277
|
(0, node_test_1.default)('timeout terminates the sandbox command process group', { skip: process.platform === 'win32' }, async () => {
|
|
267
278
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-runner-'));
|
|
268
279
|
try {
|
|
269
|
-
const runner =
|
|
280
|
+
const runner = createFakeRunner(root);
|
|
270
281
|
const started = Date.now();
|
|
271
282
|
const result = await runner.run({ ...input(root, { command: 'sleep 3 & wait', timeoutMs: 50 }) });
|
|
272
283
|
strict_1.default.ok(Date.now() - started < 2_000);
|
|
@@ -280,8 +291,8 @@ function input(root, overrides = {}) {
|
|
|
280
291
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-lock-'));
|
|
281
292
|
try {
|
|
282
293
|
const manager = new FakeManager();
|
|
283
|
-
const first =
|
|
284
|
-
const second =
|
|
294
|
+
const first = createFakeRunner(root, manager);
|
|
295
|
+
const second = createFakeRunner(root, manager);
|
|
285
296
|
const [left, right] = await Promise.all([
|
|
286
297
|
first.run(input(root)),
|
|
287
298
|
second.run(input(root)),
|
|
@@ -298,7 +309,7 @@ function input(root, overrides = {}) {
|
|
|
298
309
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-env-'));
|
|
299
310
|
try {
|
|
300
311
|
const manager = new FakeManager();
|
|
301
|
-
const runner =
|
|
312
|
+
const runner = createFakeRunner(root, manager);
|
|
302
313
|
const result = await runner.run({
|
|
303
314
|
...input(root),
|
|
304
315
|
command: 'printf "%s-%s" "$TOKEN" "$BASH_ENV"',
|
|
@@ -316,7 +327,7 @@ function input(root, overrides = {}) {
|
|
|
316
327
|
const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-empty-env-'));
|
|
317
328
|
try {
|
|
318
329
|
const manager = new FakeManager();
|
|
319
|
-
const runner =
|
|
330
|
+
const runner = createFakeRunner(root, manager);
|
|
320
331
|
const result = await runner.run({
|
|
321
332
|
...input(root),
|
|
322
333
|
command: 'printf "%s" "$TOKEN"',
|
|
@@ -69,6 +69,195 @@ function fixture() {
|
|
|
69
69
|
context.close();
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
+
(0, node_test_1.default)('listDueAutomationIds returns only automations whose next_check_at is at or before now', () => {
|
|
73
|
+
const context = fixture();
|
|
74
|
+
try {
|
|
75
|
+
const due = context.store.create(createInput({ title: 'Due' }));
|
|
76
|
+
const future = context.store.create(createInput({ title: 'Future' }));
|
|
77
|
+
const unset = context.store.create(createInput({ title: 'Unset' }));
|
|
78
|
+
const disabled = context.store.create(createInput({ title: 'Disabled' }));
|
|
79
|
+
const blocked = context.store.create(createInput({ title: 'Blocked' }));
|
|
80
|
+
context.store.updateState(due.id, { nextCheckAt: '2026-07-05T01:05:00.000Z' });
|
|
81
|
+
context.store.updateState(future.id, { nextCheckAt: '2026-07-05T02:00:00.000Z' });
|
|
82
|
+
context.store.updateState(disabled.id, { nextCheckAt: '2026-07-05T01:05:00.000Z' });
|
|
83
|
+
context.store.update(disabled.id, { enabled: false });
|
|
84
|
+
context.store.updateState(blocked.id, {
|
|
85
|
+
nextCheckAt: '2026-07-05T01:05:00.000Z',
|
|
86
|
+
health: 'blocked',
|
|
87
|
+
blockedReason: 'Provider unavailable',
|
|
88
|
+
});
|
|
89
|
+
const atDue = context.store.listDueAutomationIds(new Date('2026-07-05T01:05:00.000Z'));
|
|
90
|
+
strict_1.default.equal(atDue.size, 1);
|
|
91
|
+
strict_1.default.equal(atDue.has(due.id), true);
|
|
92
|
+
strict_1.default.equal(atDue.has(future.id), false);
|
|
93
|
+
strict_1.default.equal(atDue.has(unset.id), false);
|
|
94
|
+
strict_1.default.equal(atDue.has(disabled.id), false);
|
|
95
|
+
strict_1.default.equal(atDue.has(blocked.id), false);
|
|
96
|
+
const atLater = context.store.listDueAutomationIds(new Date('2026-07-05T02:00:00.000Z'));
|
|
97
|
+
strict_1.default.equal(atLater.size, 2);
|
|
98
|
+
strict_1.default.equal(atLater.has(due.id), true);
|
|
99
|
+
strict_1.default.equal(atLater.has(future.id), true);
|
|
100
|
+
const facadeDue = context.facade.listDueAutomationIds(new Date('2026-07-05T01:05:00.000Z'));
|
|
101
|
+
strict_1.default.equal(facadeDue.size, 1);
|
|
102
|
+
strict_1.default.equal(facadeDue.has(due.id), true);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
context.close();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
(0, node_test_1.default)('listLatestFinishedEvaluationByOrigin returns the most recent finished evaluation per automation', () => {
|
|
109
|
+
const context = fixture();
|
|
110
|
+
try {
|
|
111
|
+
const monitor = context.store.createTrusted({
|
|
112
|
+
...createInput({ title: 'Monitor' }),
|
|
113
|
+
originKind: 'automation-monitor',
|
|
114
|
+
});
|
|
115
|
+
const otherWorkspace = context.store.createTrusted({
|
|
116
|
+
...createInput({ workspaceId: 'workspace-2', title: 'Other' }),
|
|
117
|
+
originKind: 'automation-monitor',
|
|
118
|
+
});
|
|
119
|
+
const earlier = context.store.createEvaluation(monitor.id, {
|
|
120
|
+
activationKind: 'provider-event',
|
|
121
|
+
startedAt: '2026-07-05T01:00:00.000Z',
|
|
122
|
+
});
|
|
123
|
+
context.store.finishEvaluation(earlier.id, {
|
|
124
|
+
conditionOutcome: 'matched',
|
|
125
|
+
triggerDecision: 'triggered',
|
|
126
|
+
finishedAt: '2026-07-05T01:00:05.000Z',
|
|
127
|
+
});
|
|
128
|
+
const later = context.store.createEvaluation(monitor.id, {
|
|
129
|
+
activationKind: 'provider-event',
|
|
130
|
+
startedAt: '2026-07-05T02:00:00.000Z',
|
|
131
|
+
});
|
|
132
|
+
context.store.finishEvaluation(later.id, {
|
|
133
|
+
conditionOutcome: 'matched',
|
|
134
|
+
triggerDecision: 'triggered',
|
|
135
|
+
finishedAt: '2026-07-05T02:00:05.000Z',
|
|
136
|
+
});
|
|
137
|
+
const running = context.store.createEvaluation(monitor.id, {
|
|
138
|
+
activationKind: 'provider-event',
|
|
139
|
+
startedAt: '2026-07-05T03:00:00.000Z',
|
|
140
|
+
});
|
|
141
|
+
const other = context.store.createEvaluation(otherWorkspace.id, {
|
|
142
|
+
activationKind: 'provider-event',
|
|
143
|
+
startedAt: '2026-07-05T01:00:00.000Z',
|
|
144
|
+
});
|
|
145
|
+
context.store.finishEvaluation(other.id, {
|
|
146
|
+
conditionOutcome: 'matched',
|
|
147
|
+
triggerDecision: 'triggered',
|
|
148
|
+
finishedAt: '2026-07-05T01:00:05.000Z',
|
|
149
|
+
});
|
|
150
|
+
void running;
|
|
151
|
+
const all = context.store.listLatestFinishedEvaluationByOrigin('automation-monitor');
|
|
152
|
+
strict_1.default.equal(all.size, 2);
|
|
153
|
+
strict_1.default.equal(all.get(monitor.id)?.id, later.id);
|
|
154
|
+
strict_1.default.equal(all.get(otherWorkspace.id)?.id, other.id);
|
|
155
|
+
const scoped = context.store.listLatestFinishedEvaluationByOrigin('automation-monitor', 'workspace-1');
|
|
156
|
+
strict_1.default.equal(scoped.size, 1);
|
|
157
|
+
strict_1.default.equal(scoped.get(monitor.id)?.id, later.id);
|
|
158
|
+
const facadeScoped = context.facade.listLatestFinishedAutomationEvaluationByOrigin('automation-monitor', 'workspace-1');
|
|
159
|
+
strict_1.default.equal(facadeScoped.get(monitor.id)?.id, later.id);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
context.close();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
(0, node_test_1.default)('listLatestRunByOrigin returns the most recent run per automation', () => {
|
|
166
|
+
const context = fixture();
|
|
167
|
+
try {
|
|
168
|
+
const job = context.store.createTrusted({
|
|
169
|
+
...createInput({ title: 'Job' }),
|
|
170
|
+
originKind: 'scheduled-job',
|
|
171
|
+
});
|
|
172
|
+
const evaluations = ['2026-07-05T01:00:00.000Z', '2026-07-05T02:00:00.000Z'].map((finishedAt, index) => {
|
|
173
|
+
const evaluation = context.store.createEvaluation(job.id, {
|
|
174
|
+
activationKind: 'cron',
|
|
175
|
+
startedAt: `2026-07-05T0${index + 1}:00:00.000Z`,
|
|
176
|
+
});
|
|
177
|
+
context.store.finishEvaluation(evaluation.id, {
|
|
178
|
+
conditionOutcome: 'matched',
|
|
179
|
+
triggerDecision: 'triggered',
|
|
180
|
+
finishedAt,
|
|
181
|
+
});
|
|
182
|
+
return evaluation;
|
|
183
|
+
});
|
|
184
|
+
const earlierRun = context.store.createRun(job.id, evaluations[0].id, { status: 'succeeded' });
|
|
185
|
+
const laterRun = context.store.createRun(job.id, evaluations[1].id, { status: 'failed', error: 'boom' });
|
|
186
|
+
void earlierRun;
|
|
187
|
+
const all = context.store.listLatestRunByOrigin('scheduled-job');
|
|
188
|
+
strict_1.default.equal(all.size, 1);
|
|
189
|
+
strict_1.default.equal(all.get(job.id)?.id, laterRun.id);
|
|
190
|
+
strict_1.default.equal(all.get(job.id)?.status, 'failed');
|
|
191
|
+
const facadeAll = context.facade.listLatestAutomationRunByOrigin('scheduled-job');
|
|
192
|
+
strict_1.default.equal(facadeAll.get(job.id)?.id, laterRun.id);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
context.close();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
(0, node_test_1.default)('listLatestEvaluationWithStateByOrigin returns the most recent finished-with-state evaluation per automation', () => {
|
|
199
|
+
const context = fixture();
|
|
200
|
+
try {
|
|
201
|
+
const monitor = context.store.createTrusted({
|
|
202
|
+
...createInput({ title: 'Monitor' }),
|
|
203
|
+
originKind: 'automation-monitor',
|
|
204
|
+
});
|
|
205
|
+
const withoutState = context.store.createEvaluation(monitor.id, {
|
|
206
|
+
activationKind: 'provider-event',
|
|
207
|
+
startedAt: '2026-07-05T01:00:00.000Z',
|
|
208
|
+
});
|
|
209
|
+
context.store.finishEvaluation(withoutState.id, {
|
|
210
|
+
conditionOutcome: 'matched',
|
|
211
|
+
triggerDecision: 'triggered',
|
|
212
|
+
finishedAt: '2026-07-05T01:00:05.000Z',
|
|
213
|
+
});
|
|
214
|
+
const withState = context.store.createEvaluation(monitor.id, {
|
|
215
|
+
activationKind: 'provider-event',
|
|
216
|
+
startedAt: '2026-07-05T02:00:00.000Z',
|
|
217
|
+
});
|
|
218
|
+
context.store.finishEvaluation(withState.id, {
|
|
219
|
+
conditionOutcome: 'matched',
|
|
220
|
+
triggerDecision: 'triggered',
|
|
221
|
+
finishedAt: '2026-07-05T02:00:05.000Z',
|
|
222
|
+
nextState: { counter: 1 },
|
|
223
|
+
});
|
|
224
|
+
const all = context.store.listLatestEvaluationWithStateByOrigin('automation-monitor');
|
|
225
|
+
strict_1.default.equal(all.size, 1);
|
|
226
|
+
strict_1.default.equal(all.get(monitor.id)?.id, withState.id);
|
|
227
|
+
const finished = all.get(monitor.id);
|
|
228
|
+
strict_1.default.equal(finished?.status, 'finished');
|
|
229
|
+
if (finished?.status === 'finished') {
|
|
230
|
+
strict_1.default.deepEqual(finished.nextState, { counter: 1 });
|
|
231
|
+
}
|
|
232
|
+
const facadeAll = context.facade.listLatestAutomationEvaluationWithStateByOrigin('automation-monitor');
|
|
233
|
+
strict_1.default.equal(facadeAll.get(monitor.id)?.id, withState.id);
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
context.close();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
(0, node_test_1.default)('list filters by origin kind when provided', () => {
|
|
240
|
+
const context = fixture();
|
|
241
|
+
try {
|
|
242
|
+
const monitor = context.store.createTrusted({
|
|
243
|
+
...createInput({ title: 'Monitor' }),
|
|
244
|
+
originKind: 'automation-monitor',
|
|
245
|
+
});
|
|
246
|
+
const job = context.store.createTrusted({
|
|
247
|
+
...createInput({ title: 'Job' }),
|
|
248
|
+
originKind: 'scheduled-job',
|
|
249
|
+
});
|
|
250
|
+
void monitor;
|
|
251
|
+
void job;
|
|
252
|
+
strict_1.default.equal(context.store.list().length, 2);
|
|
253
|
+
strict_1.default.equal(context.store.list(undefined, 'automation-monitor').length, 1);
|
|
254
|
+
strict_1.default.equal(context.store.list(undefined, 'scheduled-job').length, 1);
|
|
255
|
+
strict_1.default.equal(context.facade.listAutomations(undefined, 'scheduled-job').length, 1);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
context.close();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
72
261
|
(0, node_test_1.default)('reconciles queued and running action runs as interrupted after restart', () => {
|
|
73
262
|
const context = fixture();
|
|
74
263
|
try {
|