@kafca/agentdock 0.1.61 → 0.1.62
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 +18 -0
- package/dist/renderer/assets/AutomationList-BgJgAdVO.js +16 -0
- package/dist/renderer/assets/{Badge-BLI4LHse.js → Badge-Bq2hPVcR.js} +1 -1
- package/dist/renderer/assets/{Card-DJeLDdL3.js → Card-BPJNSZce.js} +1 -1
- package/dist/renderer/assets/{Config-D1awc81A.js → Config-aSFqlI88.js} +2 -2
- package/dist/renderer/assets/Dashboard-D3ezFmb7.js +36 -0
- package/dist/renderer/assets/{EmptyState-CkI1RwPX.js → EmptyState-DK496gBq.js} +1 -1
- package/dist/renderer/assets/{HighlightedMarkdown-BUhDlhgW.js → HighlightedMarkdown-Ckre5jNv.js} +1 -1
- package/dist/renderer/assets/{Input-DHYrn-Ne.js → Input-C5C1d0ba.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-Bij-Xo90.js → KnowledgeDetail-N8Ey1ikc.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-DmPZ2IlI.js → KnowledgeHome-9aF8nJEf.js} +2 -2
- package/dist/renderer/assets/{Logs-ISY99heA.js → Logs-sgAdwEeS.js} +2 -2
- package/dist/renderer/assets/{Modal-DAQfz0ot.js → Modal-BB10hTRc.js} +3 -3
- package/dist/renderer/assets/{Page-Dw43UYDT.js → Page-DfC319Ck.js} +1 -1
- package/dist/renderer/assets/{Select-DzmN5OGQ.js → Select-RHJBmNER.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-B5CWqqMK.js → ThreadChat-BU1zCdr4.js} +3 -3
- package/dist/renderer/assets/{Workspace-C5g9LBj9.js → Workspace-BoZCz7S8.js} +1 -1
- package/dist/renderer/assets/{arrow-left-BLXh56Ip.js → arrow-left-D2V1eZSD.js} +1 -1
- package/dist/renderer/assets/{book-open-DhPGbc-7.js → book-open-DUcBn7Bb.js} +1 -1
- package/dist/renderer/assets/{channels-DfQrmuYO.js → channels-DMhDSlf2.js} +2 -2
- package/dist/renderer/assets/{chevron-down-CEFtpPDc.js → chevron-down-Ca2FQDB7.js} +1 -1
- package/dist/renderer/assets/en-Bnr4nMzg.js +1 -0
- package/dist/renderer/assets/es-B1GIFh3i.js +1 -0
- package/dist/renderer/assets/{index-BIKL3fTP.css → index-B6WcpkJF.css} +1 -1
- package/dist/renderer/assets/{index-CYhDUAR_.js → index-BlnvlekJ.js} +1 -1
- package/dist/renderer/assets/index-DRJ5wgw_.js +162 -0
- package/dist/renderer/assets/ja-CDWPWqEW.js +1 -0
- package/dist/renderer/assets/{knowledge-uHRQ80sR.js → knowledge-D1-k56eY.js} +1 -1
- package/dist/renderer/assets/{pencil-BD2Y31YC.js → pencil-BhWdv_86.js} +1 -1
- package/dist/renderer/assets/{plus-DDcldp_c.js → plus-BacLMlK3.js} +1 -1
- package/dist/renderer/assets/{save-CMW_cZI9.js → save-Bpoxbzau.js} +1 -1
- package/dist/renderer/assets/{search-BM-0CvJr.js → search-BVJcpdFX.js} +1 -1
- package/dist/renderer/assets/{shield-check-DaMB0FKG.js → shield-check-C2cw_Ip6.js} +1 -1
- package/dist/renderer/assets/{threads-DZgt9tWj.js → threads-DOsBQBg_.js} +1 -1
- package/dist/renderer/assets/{trash-2-DTXVbC5y.js → trash-2-Bs3rEm3g.js} +1 -1
- package/dist/renderer/assets/zh-MSddxMDX.js +1 -0
- package/dist/renderer/assets/zh-TW-C_LNWX0o.js +1 -0
- package/dist/renderer/index.html +2 -2
- package/dist-electron/electron/managed-skills/agent-browser/SKILL.md +41 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/authentication.md +83 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/commands.md +50 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/profiling.md +20 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/proxy-support.md +18 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/session-management.md +17 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/snapshot-refs.md +17 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/video-recording.md +21 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/authenticated-session.sh +11 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/capture-workflow.sh +12 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/form-automation.sh +10 -0
- package/dist-electron/electron/managed-skills/condition-trigger/SKILL.md +21 -0
- package/dist-electron/electron/managed-skills/condition-trigger/scripts/register-condition-trigger.sh +106 -0
- package/dist-electron/electron/managed-skills/knowledge-base/SKILL.md +13 -0
- package/dist-electron/electron/managed-skills/knowledge-base/scripts/search-knowledge.sh +80 -0
- package/dist-electron/packages/contracts/src/automations.js +211 -0
- package/dist-electron/packages/contracts/src/index.js +1 -0
- package/dist-electron/packages/core-sdk/src/automations.js +87 -0
- package/dist-electron/packages/core-sdk/src/client.js +101 -0
- package/dist-electron/packages/core-sdk/src/index.js +2 -1
- package/dist-electron/services/local-ai-core/src/acp/store/automation-script-store.js +415 -0
- package/dist-electron/services/local-ai-core/src/acp/store/automation-store.js +829 -0
- package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +158 -0
- package/dist-electron/services/local-ai-core/src/acp/store/schema.js +81 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-action-executor.js +144 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-condition-engine.js +79 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-conversation-executor.js +39 -73
- package/dist-electron/services/local-ai-core/src/automation/automation-monitor-service.js +695 -260
- package/dist-electron/services/local-ai-core/src/automation/automation-script-service.js +507 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-service.js +1019 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-trigger-engine.js +141 -0
- package/dist-electron/services/local-ai-core/src/automation/condition-evaluator.js +60 -17
- package/dist-electron/services/local-ai-core/src/automation/legacy-automation-mappers.js +286 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js +792 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/sandbox-runner.js +2 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/script-package.js +475 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/script-protocol-runner.js +266 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/secret-resolver.js +37 -0
- package/dist-electron/services/local-ai-core/src/channel/weixin/inbound-poller.js +1 -2
- package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +9 -8
- package/dist-electron/services/local-ai-core/src/cli/lac.js +229 -0
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +21 -0
- package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +76 -0
- package/dist-electron/services/local-ai-core/src/runtime/handlers/automations-handler.js +322 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +20 -0
- package/dist-electron/services/local-ai-core/src/runtime/managed-skill-catalog.js +37 -0
- package/dist-electron/services/local-ai-core/src/runtime/server-helpers.js +14 -4
- package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +75 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +21 -0
- package/dist-electron/services/local-ai-core/src/runtime/standalone.js +1 -0
- package/dist-electron/services/local-ai-core/src/scheduler/cron.js +151 -39
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +2 -4
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +64 -12
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +71 -4
- package/dist-electron/services/local-ai-core/src/scheduler/thread-resolution.js +10 -0
- package/dist-electron/services/local-ai-core/src/thread/agent-message-policy.js +16 -1
- package/dist-electron/src/pages/Automation/automation-page-model.js +76 -0
- package/dist-electron/tests/contracts/agent-message-policy.test.js +32 -0
- package/dist-electron/tests/contracts/architecture-docs.test.js +30 -0
- package/dist-electron/tests/contracts/automation-contracts.test.js +154 -0
- package/dist-electron/tests/contracts/automation-renderer-model.test.js +47 -0
- package/dist-electron/tests/electron/automation-action-executor.test.js +75 -0
- package/dist-electron/tests/electron/automation-compatibility.test.js +463 -0
- package/dist-electron/tests/electron/automation-deployment.test.js +66 -0
- package/dist-electron/tests/electron/automation-engine.test.js +264 -0
- package/dist-electron/tests/electron/automation-monitor.test.js +1333 -0
- package/dist-electron/tests/electron/automation-sandbox-runner.test.js +333 -0
- package/dist-electron/tests/electron/automation-script-approval.test.js +553 -0
- package/dist-electron/tests/electron/automation-script-package.test.js +499 -0
- package/dist-electron/tests/electron/automation-script-runner.test.js +120 -0
- package/dist-electron/tests/electron/automation-service.test.js +1078 -0
- package/dist-electron/tests/electron/automation-store.test.js +593 -0
- package/dist-electron/tests/electron/condition-trigger-skill.test.js +93 -0
- package/dist-electron/tests/electron/core-client.test.js +99 -0
- package/dist-electron/tests/electron/lac-cli.test.js +110 -0
- package/dist-electron/tests/electron/workspace-task-store.test.js +25 -0
- package/dist-electron/tests/integration/automation-routes.test.js +186 -0
- package/dist-electron/tests/integration/automation-sandbox-runtime.test.js +92 -0
- package/package.json +4 -3
- package/dist/renderer/assets/CronList-CupZoNwW.js +0 -1
- package/dist/renderer/assets/Dashboard-nLZnOtcs.js +0 -41
- package/dist/renderer/assets/MonitorList-BlX82rXF.js +0 -1
- package/dist/renderer/assets/en-CmAr_0e6.js +0 -1
- package/dist/renderer/assets/es-Cs9x52ca.js +0 -1
- package/dist/renderer/assets/index-Dd00oqgY.js +0 -167
- package/dist/renderer/assets/ja-DtvtBRTW.js +0 -1
- package/dist/renderer/assets/play-DBJ8wsSy.js +0 -6
- package/dist/renderer/assets/zh-CcJ9vBZj.js +0 -1
- package/dist/renderer/assets/zh-TW-elI72AC1.js +0 -1
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
11
|
+
const local_core_acp_store_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-store.js");
|
|
12
|
+
const event_bus_js_1 = require("../../services/local-ai-core/src/kernel/event-bus.js");
|
|
13
|
+
const bootstrap_js_1 = require("../../services/local-ai-core/src/kernel/bootstrap.js");
|
|
14
|
+
const automation_service_js_1 = require("../../services/local-ai-core/src/automation/automation-service.js");
|
|
15
|
+
const script_protocol_runner_js_1 = require("../../services/local-ai-core/src/automation/scripts/script-protocol-runner.js");
|
|
16
|
+
const NOW = new Date('2026-07-05T08:00:00.000Z');
|
|
17
|
+
function input(overrides = {}) {
|
|
18
|
+
return {
|
|
19
|
+
workspaceId: 'workspace-1',
|
|
20
|
+
title: 'Native automation',
|
|
21
|
+
enabled: true,
|
|
22
|
+
activation: { kind: 'once', runAt: '2026-07-05T07:59:00.000Z' },
|
|
23
|
+
condition: { kind: 'always' },
|
|
24
|
+
action: { kind: 'agent-prompt', promptTemplate: 'Act for {{title}}', executionMode: 'side-thread' },
|
|
25
|
+
delivery: {
|
|
26
|
+
platform: 'local',
|
|
27
|
+
route: { type: 'local.thread', channelId: 'channel-1', metadata: { keep: true } },
|
|
28
|
+
},
|
|
29
|
+
policies: { concurrency: 'skip-if-running', cooldownMs: 0 },
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function fixture(overrides = {}) {
|
|
34
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-service-'));
|
|
35
|
+
const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
36
|
+
const eventBus = new event_bus_js_1.LocalCoreEventBus();
|
|
37
|
+
const actions = [];
|
|
38
|
+
const alerts = [];
|
|
39
|
+
let timerHandler;
|
|
40
|
+
let timerCleared = false;
|
|
41
|
+
const service = new automation_service_js_1.AutomationService({
|
|
42
|
+
store,
|
|
43
|
+
eventBus,
|
|
44
|
+
clock: () => new Date(NOW),
|
|
45
|
+
actionExecutor: {
|
|
46
|
+
async execute(value) {
|
|
47
|
+
actions.push(value);
|
|
48
|
+
return { threadId: 'thread-1', acpRunId: 'acp-run-1', deliveryStatus: 'succeeded' };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
setInterval: (handler, delayMs) => {
|
|
52
|
+
strict_1.default.equal(delayMs, 30_000);
|
|
53
|
+
timerHandler = handler;
|
|
54
|
+
return 1;
|
|
55
|
+
},
|
|
56
|
+
clearInterval: () => { timerCleared = true; },
|
|
57
|
+
alert: ({ count, error }) => { alerts.push({ count, error }); },
|
|
58
|
+
...overrides,
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
store,
|
|
62
|
+
service,
|
|
63
|
+
eventBus,
|
|
64
|
+
actions,
|
|
65
|
+
alerts,
|
|
66
|
+
get timerHandler() { return timerHandler; },
|
|
67
|
+
get timerCleared() { return timerCleared; },
|
|
68
|
+
close() {
|
|
69
|
+
store.close();
|
|
70
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
(0, node_test_1.default)('one due native check persists evaluation/run lifecycles and preserves delivery route', async () => {
|
|
75
|
+
const context = fixture();
|
|
76
|
+
const events = [];
|
|
77
|
+
for (const type of ['automation.definition.updated', 'automation.evaluation.updated', 'automation.run.updated']) {
|
|
78
|
+
context.eventBus.on(type, () => events.push(type));
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const automation = context.store.createAutomation(input());
|
|
82
|
+
const originalRoute = structuredClone(automation.delivery.route);
|
|
83
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: automation.activation.kind === 'once' ? automation.activation.runAt : null });
|
|
84
|
+
await context.service.tick();
|
|
85
|
+
const evaluations = context.service.listEvaluations(automation.id);
|
|
86
|
+
const runs = context.service.listRuns(automation.id);
|
|
87
|
+
strict_1.default.equal(evaluations.length, 1);
|
|
88
|
+
strict_1.default.equal(evaluations[0]?.status, 'finished');
|
|
89
|
+
strict_1.default.equal(evaluations[0]?.conditionOutcome, 'matched');
|
|
90
|
+
strict_1.default.equal(evaluations[0]?.triggerDecision, 'triggered');
|
|
91
|
+
strict_1.default.equal(runs.length, 1);
|
|
92
|
+
strict_1.default.equal(runs[0]?.status, 'succeeded');
|
|
93
|
+
strict_1.default.deepEqual(context.service.get(automation.id)?.delivery.route, originalRoute);
|
|
94
|
+
strict_1.default.equal(context.actions.length, 1);
|
|
95
|
+
strict_1.default.ok(events.filter((event) => event === 'automation.evaluation.updated').length >= 2);
|
|
96
|
+
strict_1.default.ok(events.filter((event) => event === 'automation.run.updated').length >= 3);
|
|
97
|
+
strict_1.default.ok(events.includes('automation.definition.updated'));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
context.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
(0, node_test_1.default)('condition false updates match state without creating a run', async () => {
|
|
104
|
+
const context = fixture({
|
|
105
|
+
conditionEvaluator: () => ({ kind: 'evaluated', matched: false }),
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
const automation = context.service.create(input());
|
|
109
|
+
await context.service.checkNow(automation.id);
|
|
110
|
+
strict_1.default.equal(context.service.get(automation.id)?.lastSuccessfulMatch, false);
|
|
111
|
+
strict_1.default.equal(context.service.listRuns(automation.id).length, 0);
|
|
112
|
+
strict_1.default.equal(context.actions.length, 0);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
context.close();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
(0, node_test_1.default)('condition errors preserve successful state, alert exponentially, and success resets failures', async () => {
|
|
119
|
+
let fail = true;
|
|
120
|
+
const context = fixture({
|
|
121
|
+
conditionEvaluator: () => {
|
|
122
|
+
if (fail)
|
|
123
|
+
throw new Error('provider unavailable');
|
|
124
|
+
return { kind: 'evaluated', matched: false };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
const automation = context.service.create(input());
|
|
129
|
+
context.store.updateAutomationState(automation.id, { lastSuccessfulMatch: true });
|
|
130
|
+
for (let count = 1; count <= 32; count += 1)
|
|
131
|
+
await context.service.checkNow(automation.id);
|
|
132
|
+
strict_1.default.deepEqual(context.alerts.map((entry) => entry.count), [1, 3, 7, 15, 31]);
|
|
133
|
+
strict_1.default.equal(context.service.get(automation.id)?.lastSuccessfulMatch, true);
|
|
134
|
+
strict_1.default.equal(context.service.get(automation.id)?.consecutiveEvaluationFailures, 32);
|
|
135
|
+
const latest = context.service.listEvaluations(automation.id).at(-1);
|
|
136
|
+
strict_1.default.equal(latest?.conditionOutcome, 'error');
|
|
137
|
+
strict_1.default.equal(latest?.triggerDecision, 'not_evaluated');
|
|
138
|
+
strict_1.default.match(latest?.resultSummary || '', /provider unavailable/);
|
|
139
|
+
fail = false;
|
|
140
|
+
await context.service.checkNow(automation.id);
|
|
141
|
+
strict_1.default.equal(context.service.get(automation.id)?.consecutiveEvaluationFailures, 0);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
context.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
(0, node_test_1.default)('overlap, cooldown, and running action decisions do not create runs', async () => {
|
|
148
|
+
let release;
|
|
149
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
150
|
+
const context = fixture({
|
|
151
|
+
actionExecutor: { async execute() { await blocked; return { threadId: 't', acpRunId: 'r' }; } },
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
const automation = context.service.create(input());
|
|
155
|
+
const first = context.service.checkNow(automation.id);
|
|
156
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
157
|
+
await context.service.checkNow(automation.id);
|
|
158
|
+
const overlap = context.service.listEvaluations(automation.id)
|
|
159
|
+
.find((entry) => entry.status === 'finished' && entry.triggerDecision === 'skipped_concurrent');
|
|
160
|
+
strict_1.default.equal(overlap?.conditionOutcome, 'skipped');
|
|
161
|
+
strict_1.default.equal(overlap?.triggerDecision, 'skipped_concurrent');
|
|
162
|
+
release();
|
|
163
|
+
await first;
|
|
164
|
+
const cooldown = context.service.create(input({
|
|
165
|
+
title: 'cooldown',
|
|
166
|
+
policies: { concurrency: 'skip-if-running', cooldownMs: 60_000 },
|
|
167
|
+
}));
|
|
168
|
+
context.store.updateAutomationState(cooldown.id, { lastTriggeredAt: NOW.toISOString(), lastSuccessfulMatch: false });
|
|
169
|
+
await context.service.checkNow(cooldown.id);
|
|
170
|
+
strict_1.default.equal(context.service.listEvaluations(cooldown.id)[0]?.triggerDecision, 'skipped_cooldown');
|
|
171
|
+
strict_1.default.equal(context.service.listRuns(cooldown.id).length, 0);
|
|
172
|
+
const actionRunning = context.service.create(input({ title: 'running' }));
|
|
173
|
+
const prior = context.store.createAutomationEvaluation(actionRunning.id, { activationKind: 'once', startedAt: NOW.toISOString() });
|
|
174
|
+
context.store.finishAutomationEvaluation(prior.id, {
|
|
175
|
+
conditionOutcome: 'matched', triggerDecision: 'triggered', finishedAt: NOW.toISOString(),
|
|
176
|
+
});
|
|
177
|
+
const existingRun = context.store.createAutomationRun(actionRunning.id, prior.id, { status: 'running' });
|
|
178
|
+
strict_1.default.equal(existingRun.status, 'running');
|
|
179
|
+
await context.service.checkNow(actionRunning.id);
|
|
180
|
+
const actionOverlap = context.service.listEvaluations(actionRunning.id).find((entry) => entry.id !== prior.id);
|
|
181
|
+
strict_1.default.equal(actionOverlap?.triggerDecision, 'skipped_action_running');
|
|
182
|
+
strict_1.default.equal(context.service.listRuns(actionRunning.id).length, 1);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
context.close();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
(0, node_test_1.default)('start catches up native and scheduled-job origins, skips provider events, and stop drains in-flight work', async () => {
|
|
189
|
+
let release;
|
|
190
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
191
|
+
const context = fixture({
|
|
192
|
+
actionExecutor: { async execute() { await blocked; return { threadId: 't', acpRunId: 'r' }; } },
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
const native = context.store.createAutomation(input({ activation: { kind: 'interval', intervalMs: 60_000 } }));
|
|
196
|
+
const provider = context.store.createAutomation(input({ title: 'provider', activation: { kind: 'provider-event', sourceType: 'x', sourceConfig: {} } }));
|
|
197
|
+
const legacy = context.store.createAutomation(input({ title: 'legacy' }));
|
|
198
|
+
const db = context.store.db;
|
|
199
|
+
db.prepare('UPDATE automations SET origin_kind = ? WHERE id = ?').run('scheduled-job', legacy.id);
|
|
200
|
+
const start = context.service.start();
|
|
201
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
202
|
+
strict_1.default.equal(context.service.listEvaluations(native.id).length, 1);
|
|
203
|
+
strict_1.default.equal(context.service.listEvaluations(provider.id).length, 0);
|
|
204
|
+
strict_1.default.equal(context.service.listEvaluations(legacy.id).length, 1);
|
|
205
|
+
release();
|
|
206
|
+
await start;
|
|
207
|
+
await context.service.tick();
|
|
208
|
+
strict_1.default.equal(context.service.listEvaluations(native.id).length, 1);
|
|
209
|
+
const running = context.service.checkNow(native.id);
|
|
210
|
+
const stop = context.service.stop();
|
|
211
|
+
await running;
|
|
212
|
+
await stop;
|
|
213
|
+
strict_1.default.equal(context.timerCleared, true);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
context.close();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
(0, node_test_1.default)('approved script delegation records unavailable error without executing an action', async () => {
|
|
220
|
+
const context = fixture();
|
|
221
|
+
try {
|
|
222
|
+
const automation = context.service.create(input({
|
|
223
|
+
condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' },
|
|
224
|
+
}));
|
|
225
|
+
await context.service.checkNow(automation.id);
|
|
226
|
+
const evaluation = context.service.listEvaluations(automation.id)[0];
|
|
227
|
+
strict_1.default.equal(evaluation?.conditionOutcome, 'error');
|
|
228
|
+
strict_1.default.equal(evaluation?.triggerDecision, 'not_evaluated');
|
|
229
|
+
strict_1.default.match(evaluation?.resultSummary || '', /unavailable/i);
|
|
230
|
+
strict_1.default.equal(context.actions.length, 0);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
context.close();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
(0, node_test_1.default)('approved scripts persist nextState only after success and pass it to the next evaluation', async () => {
|
|
237
|
+
const requests = [];
|
|
238
|
+
const context = fixture({
|
|
239
|
+
scriptProtocolRunner: {
|
|
240
|
+
async run(request) {
|
|
241
|
+
requests.push({ previousState: request.previousState });
|
|
242
|
+
return {
|
|
243
|
+
matched: false, stdout: '{"protocolVersion":1,"matched":false}', stderr: '', exitCode: 0,
|
|
244
|
+
outputTruncated: false, nextState: { cursor: requests.length },
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
const automation = context.service.create(input({
|
|
251
|
+
condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' },
|
|
252
|
+
}));
|
|
253
|
+
await context.service.checkNow(automation.id);
|
|
254
|
+
await context.service.checkNow(automation.id);
|
|
255
|
+
strict_1.default.deepEqual(requests.map((request) => request.previousState), [{}, { cursor: 1 }]);
|
|
256
|
+
strict_1.default.ok(context.service.listEvaluations(automation.id)
|
|
257
|
+
.some((evaluation) => evaluation.status === 'finished' && evaluation.nextState?.cursor === 2));
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
context.close();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
(0, node_test_1.default)('approval or sandbox fact failures block the automation and preserve prior state', async () => {
|
|
264
|
+
const context = fixture({
|
|
265
|
+
scriptProtocolRunner: {
|
|
266
|
+
async run() { throw new script_protocol_runner_js_1.ScriptProtocolError('approval_mismatch', 'Approved facts changed.', true); },
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
const automation = context.service.create(input({
|
|
271
|
+
condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' },
|
|
272
|
+
}));
|
|
273
|
+
context.store.updateAutomationState(automation.id, { lastSuccessfulMatch: true });
|
|
274
|
+
const evaluation = await context.service.checkNow(automation.id);
|
|
275
|
+
strict_1.default.equal(evaluation.conditionOutcome, 'error');
|
|
276
|
+
strict_1.default.equal(evaluation.status === 'finished' ? evaluation.nextState : undefined, undefined);
|
|
277
|
+
strict_1.default.equal(context.service.get(automation.id)?.health, 'blocked');
|
|
278
|
+
strict_1.default.equal(context.service.get(automation.id)?.lastSuccessfulMatch, true);
|
|
279
|
+
strict_1.default.match(context.service.get(automation.id)?.blockedReason || '', /Approved facts changed/);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
context.close();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
(0, node_test_1.default)('a failed script evaluation does not erase the last successful script state', async () => {
|
|
286
|
+
const previousStates = [];
|
|
287
|
+
let call = 0;
|
|
288
|
+
const context = fixture({
|
|
289
|
+
scriptProtocolRunner: {
|
|
290
|
+
async run(request) {
|
|
291
|
+
call += 1;
|
|
292
|
+
previousStates.push(request.previousState);
|
|
293
|
+
if (call === 2)
|
|
294
|
+
throw new script_protocol_runner_js_1.ScriptProtocolError('script_exit', 'temporary failure');
|
|
295
|
+
return { matched: false, stdout: '', stderr: '', exitCode: 0, outputTruncated: false, nextState: { cursor: call } };
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const automation = context.service.create(input({
|
|
301
|
+
condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' },
|
|
302
|
+
}));
|
|
303
|
+
await context.service.checkNow(automation.id);
|
|
304
|
+
await context.service.checkNow(automation.id);
|
|
305
|
+
await context.service.checkNow(automation.id);
|
|
306
|
+
strict_1.default.deepEqual(previousStates, [{}, { cursor: 1 }, { cursor: 1 }]);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
context.close();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
(0, node_test_1.default)('script network audit persists only bounded destination metadata', async () => {
|
|
313
|
+
const context = fixture({
|
|
314
|
+
scriptProtocolRunner: { async run() {
|
|
315
|
+
return { matched: false, stdout: '', stderr: '', exitCode: 0, outputTruncated: false,
|
|
316
|
+
networkAudit: [{ host: 'api.example.test', port: 443, allowed: true, timestamp: NOW.toISOString() }],
|
|
317
|
+
};
|
|
318
|
+
} },
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
const automation = context.service.create(input({ condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' } }));
|
|
322
|
+
const evaluation = await context.service.checkNow(automation.id);
|
|
323
|
+
strict_1.default.deepEqual(evaluation.status === 'finished' ? evaluation.networkAudit : undefined, [{ host: 'api.example.test', port: 443, allowed: true, timestamp: NOW.toISOString() }]);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
context.close();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
(0, node_test_1.default)('failed script evaluations retain their completed sandbox network audit', async () => {
|
|
330
|
+
const audit = [{ host: 'api.example.test', port: 443, allowed: true, timestamp: NOW.toISOString() }];
|
|
331
|
+
const context = fixture({ scriptProtocolRunner: { async run() {
|
|
332
|
+
throw new script_protocol_runner_js_1.ScriptProtocolError('script_exit', 'exit 1', false, audit);
|
|
333
|
+
} } });
|
|
334
|
+
try {
|
|
335
|
+
const automation = context.service.create(input({ condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' } }));
|
|
336
|
+
const evaluation = await context.service.checkNow(automation.id);
|
|
337
|
+
strict_1.default.equal(evaluation.conditionOutcome, 'error');
|
|
338
|
+
strict_1.default.deepEqual(evaluation.status === 'finished' ? evaluation.networkAudit : undefined, audit);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
context.close();
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
(0, node_test_1.default)('action executor failures persist a failed run and keep evaluation state consistent', async () => {
|
|
345
|
+
const context = fixture({
|
|
346
|
+
actionExecutor: { async execute() { throw new Error('ACP send failed'); } },
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
const automation = context.service.create(input());
|
|
350
|
+
const evaluation = await context.service.checkNow(automation.id);
|
|
351
|
+
const run = context.service.listRuns(automation.id)[0];
|
|
352
|
+
strict_1.default.equal(evaluation.conditionOutcome, 'matched');
|
|
353
|
+
strict_1.default.equal(evaluation.triggerDecision, 'triggered');
|
|
354
|
+
strict_1.default.equal(run?.status, 'failed');
|
|
355
|
+
strict_1.default.equal(run?.deliveryStatus, 'failed');
|
|
356
|
+
strict_1.default.match(run?.error || '', /ACP send failed/);
|
|
357
|
+
strict_1.default.equal(context.service.get(automation.id)?.lastSuccessfulMatch, true);
|
|
358
|
+
strict_1.default.equal(context.service.get(automation.id)?.consecutiveEvaluationFailures, 0);
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
context.close();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
(0, node_test_1.default)('overlapping global ticks share one due pass', async () => {
|
|
365
|
+
let release;
|
|
366
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
367
|
+
const context = fixture({
|
|
368
|
+
actionExecutor: { async execute() { await blocked; return { threadId: 't', acpRunId: 'r' }; } },
|
|
369
|
+
});
|
|
370
|
+
try {
|
|
371
|
+
const automation = context.store.createAutomation(input());
|
|
372
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: '2026-07-05T07:59:00.000Z' });
|
|
373
|
+
const first = context.service.tick();
|
|
374
|
+
const second = context.service.tick();
|
|
375
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
376
|
+
strict_1.default.equal(context.service.listEvaluations(automation.id).length, 1);
|
|
377
|
+
release();
|
|
378
|
+
await Promise.all([first, second]);
|
|
379
|
+
strict_1.default.equal(context.service.listRuns(automation.id).length, 1);
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
context.close();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
(0, node_test_1.default)('stop clears the timer and waits for an in-flight check', async () => {
|
|
386
|
+
let release;
|
|
387
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
388
|
+
let stopped = false;
|
|
389
|
+
const context = fixture({
|
|
390
|
+
actionExecutor: { async execute() { await blocked; return { threadId: 't', acpRunId: 'r' }; } },
|
|
391
|
+
});
|
|
392
|
+
try {
|
|
393
|
+
const automation = context.service.create(input({ activation: { kind: 'interval', intervalMs: 60_000 } }));
|
|
394
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: '2026-07-05T07:59:00.000Z' });
|
|
395
|
+
const starting = context.service.start();
|
|
396
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
397
|
+
const stopping = context.service.stop().then(() => { stopped = true; });
|
|
398
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
399
|
+
strict_1.default.equal(stopped, false);
|
|
400
|
+
release();
|
|
401
|
+
await Promise.all([starting, stopping]);
|
|
402
|
+
strict_1.default.equal(stopped, true);
|
|
403
|
+
strict_1.default.equal(context.timerCleared, true);
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
context.close();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
(0, node_test_1.default)('legacy import failure degrades only the unified automation loop', async () => {
|
|
410
|
+
const context = fixture();
|
|
411
|
+
try {
|
|
412
|
+
context.store.importLegacyAutomations = () => { throw new Error('malformed legacy row'); };
|
|
413
|
+
await context.service.start();
|
|
414
|
+
strict_1.default.deepEqual(context.service.getRuntimeStatus(), {
|
|
415
|
+
status: 'degraded',
|
|
416
|
+
reason: 'Legacy automation import failed: malformed legacy row',
|
|
417
|
+
});
|
|
418
|
+
strict_1.default.equal(context.timerHandler, undefined);
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
await context.service.stop();
|
|
422
|
+
context.close();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
(0, node_test_1.default)('runtime startup keeps scheduler and monitor services available when unified migration is blocked', async () => {
|
|
426
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-runtime-'));
|
|
427
|
+
const runtime = (0, bootstrap_js_1.bootstrapLocalCoreRuntime)({ userDataPath });
|
|
428
|
+
let schedulerStarted = false;
|
|
429
|
+
let monitorsStarted = false;
|
|
430
|
+
runtime.scheduler.start = async () => { schedulerStarted = true; };
|
|
431
|
+
runtime.automationMonitors.start = async () => { monitorsStarted = true; };
|
|
432
|
+
runtime.store.importLegacyAutomations = () => { throw new Error('legacy migration blocked'); };
|
|
433
|
+
try {
|
|
434
|
+
await runtime.start();
|
|
435
|
+
strict_1.default.equal(schedulerStarted, true);
|
|
436
|
+
strict_1.default.equal(monitorsStarted, true);
|
|
437
|
+
strict_1.default.deepEqual(runtime.automations.getRuntimeStatus(), {
|
|
438
|
+
status: 'degraded',
|
|
439
|
+
reason: 'Legacy automation import failed: legacy migration blocked',
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
await runtime.stop();
|
|
444
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
(0, node_test_1.default)('restart preserves terminal NULL for a consumed once automation that did not match', async () => {
|
|
448
|
+
const context = fixture({ conditionEvaluator: () => ({ kind: 'evaluated', matched: false }) });
|
|
449
|
+
try {
|
|
450
|
+
const automation = context.service.create(input());
|
|
451
|
+
await context.service.start();
|
|
452
|
+
await context.service.stop();
|
|
453
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
454
|
+
strict_1.default.equal(context.service.listEvaluations(automation.id).length, 1);
|
|
455
|
+
strict_1.default.equal(context.service.listRuns(automation.id).length, 0);
|
|
456
|
+
const restarted = new automation_service_js_1.AutomationService({
|
|
457
|
+
store: context.store,
|
|
458
|
+
eventBus: context.eventBus,
|
|
459
|
+
clock: () => new Date(NOW),
|
|
460
|
+
conditionEvaluator: () => ({ kind: 'evaluated', matched: true }),
|
|
461
|
+
actionExecutor: {
|
|
462
|
+
async execute() { return { threadId: 'unexpected', acpRunId: 'unexpected' }; },
|
|
463
|
+
},
|
|
464
|
+
setInterval: () => 2,
|
|
465
|
+
clearInterval: () => undefined,
|
|
466
|
+
});
|
|
467
|
+
await restarted.start();
|
|
468
|
+
await restarted.stop();
|
|
469
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
470
|
+
strict_1.default.equal(restarted.listEvaluations(automation.id).length, 1);
|
|
471
|
+
strict_1.default.equal(restarted.listRuns(automation.id).length, 0);
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
context.close();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
(0, node_test_1.default)('overdue never-evaluated once catches up once and remains consumed after restart', async () => {
|
|
478
|
+
const context = fixture();
|
|
479
|
+
try {
|
|
480
|
+
const automation = context.store.createAutomation(input());
|
|
481
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
482
|
+
await context.service.start();
|
|
483
|
+
await context.service.stop();
|
|
484
|
+
strict_1.default.equal(context.service.listEvaluations(automation.id).length, 1);
|
|
485
|
+
strict_1.default.equal(context.service.listRuns(automation.id).length, 1);
|
|
486
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
487
|
+
const restarted = new automation_service_js_1.AutomationService({
|
|
488
|
+
store: context.store,
|
|
489
|
+
eventBus: context.eventBus,
|
|
490
|
+
clock: () => new Date(NOW),
|
|
491
|
+
actionExecutor: {
|
|
492
|
+
async execute() { return { threadId: 'unexpected', acpRunId: 'unexpected' }; },
|
|
493
|
+
},
|
|
494
|
+
setInterval: () => 3,
|
|
495
|
+
clearInterval: () => undefined,
|
|
496
|
+
});
|
|
497
|
+
await restarted.start();
|
|
498
|
+
await restarted.stop();
|
|
499
|
+
strict_1.default.equal(restarted.listEvaluations(automation.id).length, 1);
|
|
500
|
+
strict_1.default.equal(restarted.listRuns(automation.id).length, 1);
|
|
501
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
context.close();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
(0, node_test_1.default)('legacy consumed once import stays terminal while never-run overdue once catches up once', async () => {
|
|
508
|
+
const context = fixture();
|
|
509
|
+
try {
|
|
510
|
+
const consumed = context.store.createScheduledJob({
|
|
511
|
+
workspaceId: 'workspace-1', platform: 'local', route: { type: 'local.thread', channelId: 'workspace-1' },
|
|
512
|
+
executionMode: 'same-thread', triggerType: 'once', runAt: '2026-07-05T07:00:00.000Z',
|
|
513
|
+
promptTemplate: 'already ran', description: '', enabled: true,
|
|
514
|
+
});
|
|
515
|
+
context.store.updateScheduledJobStatus(consumed.id, {
|
|
516
|
+
lastRunAt: '2026-07-05T07:00:00.000Z', lastStatus: 'succeeded',
|
|
517
|
+
});
|
|
518
|
+
const overdue = context.store.createScheduledJob({
|
|
519
|
+
workspaceId: 'workspace-1', platform: 'local', route: { type: 'local.thread', channelId: 'workspace-1' },
|
|
520
|
+
executionMode: 'same-thread', triggerType: 'once', runAt: '2026-07-05T07:30:00.000Z',
|
|
521
|
+
promptTemplate: 'run once', description: '', enabled: true,
|
|
522
|
+
});
|
|
523
|
+
await context.service.start();
|
|
524
|
+
strict_1.default.equal(context.service.listEvaluations(consumed.id).length, 0);
|
|
525
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(consumed.id), null);
|
|
526
|
+
strict_1.default.equal(context.service.get(consumed.id)?.enabled, false);
|
|
527
|
+
strict_1.default.equal(context.service.listEvaluations(overdue.id).length, 1);
|
|
528
|
+
strict_1.default.equal(context.service.listRuns(overdue.id).length, 1);
|
|
529
|
+
strict_1.default.equal(context.service.get(overdue.id)?.enabled, false);
|
|
530
|
+
strict_1.default.equal(context.actions.length, 1);
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
await context.service.stop();
|
|
534
|
+
context.close();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
(0, node_test_1.default)('due loop runs actions concurrently with an enforced worker bound', async () => {
|
|
538
|
+
let releaseFirst;
|
|
539
|
+
const firstBlocked = new Promise((resolve) => { releaseFirst = resolve; });
|
|
540
|
+
let firstStarted;
|
|
541
|
+
const firstDidStart = new Promise((resolve) => { firstStarted = resolve; });
|
|
542
|
+
const completed = [];
|
|
543
|
+
let active = 0;
|
|
544
|
+
let maxActive = 0;
|
|
545
|
+
let firstInvocation = true;
|
|
546
|
+
let blockedTitle = '';
|
|
547
|
+
const context = fixture({
|
|
548
|
+
maxConcurrency: 2,
|
|
549
|
+
actionExecutor: {
|
|
550
|
+
async execute({ automation }) {
|
|
551
|
+
active += 1;
|
|
552
|
+
maxActive = Math.max(maxActive, active);
|
|
553
|
+
if (firstInvocation) {
|
|
554
|
+
firstInvocation = false;
|
|
555
|
+
blockedTitle = automation.title;
|
|
556
|
+
firstStarted();
|
|
557
|
+
await firstBlocked;
|
|
558
|
+
}
|
|
559
|
+
completed.push(automation.title);
|
|
560
|
+
active -= 1;
|
|
561
|
+
return { threadId: automation.id, acpRunId: automation.id };
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
try {
|
|
566
|
+
for (const title of ['first', 'second', 'third']) {
|
|
567
|
+
const automation = context.store.createAutomation(input({ title }));
|
|
568
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: '2026-07-05T07:59:00.000Z' });
|
|
569
|
+
}
|
|
570
|
+
const ticking = context.service.tick();
|
|
571
|
+
await firstDidStart;
|
|
572
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
573
|
+
strict_1.default.ok(completed.length >= 1);
|
|
574
|
+
strict_1.default.ok(!completed.includes(blockedTitle));
|
|
575
|
+
strict_1.default.equal(maxActive, 2);
|
|
576
|
+
releaseFirst();
|
|
577
|
+
await ticking;
|
|
578
|
+
strict_1.default.equal(completed.length, 3);
|
|
579
|
+
strict_1.default.equal(maxActive, 2);
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
context.close();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
(0, node_test_1.default)('stop prevents workers from admitting more queued automations', async () => {
|
|
586
|
+
let release;
|
|
587
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
588
|
+
let started;
|
|
589
|
+
const didStart = new Promise((resolve) => { started = resolve; });
|
|
590
|
+
const context = fixture({
|
|
591
|
+
maxConcurrency: 1,
|
|
592
|
+
actionExecutor: {
|
|
593
|
+
async execute({ automation }) {
|
|
594
|
+
started();
|
|
595
|
+
await blocked;
|
|
596
|
+
return { threadId: automation.id, acpRunId: automation.id };
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
try {
|
|
601
|
+
const ids = ['first', 'second', 'third'].map((title) => {
|
|
602
|
+
const automation = context.store.createAutomation(input({ title }));
|
|
603
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: '2026-07-05T07:59:00.000Z' });
|
|
604
|
+
return automation.id;
|
|
605
|
+
});
|
|
606
|
+
const ticking = context.service.tick();
|
|
607
|
+
await didStart;
|
|
608
|
+
const stopping = context.service.stop();
|
|
609
|
+
release();
|
|
610
|
+
await Promise.all([ticking, stopping]);
|
|
611
|
+
strict_1.default.equal(ids.reduce((sum, id) => sum + context.service.listEvaluations(id).length, 0), 1);
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
context.close();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
(0, node_test_1.default)('timer tick failures are handled, sanitized, and degrade the service', async () => {
|
|
618
|
+
const logs = [];
|
|
619
|
+
const errors = [];
|
|
620
|
+
const context = fixture({ log: (message) => logs.push(message) });
|
|
621
|
+
context.eventBus.on('localcore.error', (event) => errors.push(String(event.error || '')));
|
|
622
|
+
try {
|
|
623
|
+
await context.service.start();
|
|
624
|
+
context.store.pruneAutomationEvaluations = () => {
|
|
625
|
+
throw new Error(`TOKEN=top-secret\u001b[31m ${'x'.repeat(3_000)}`);
|
|
626
|
+
};
|
|
627
|
+
context.timerHandler();
|
|
628
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
629
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
630
|
+
strict_1.default.equal(context.service.getRuntimeStatus().status, 'degraded');
|
|
631
|
+
const combined = [...logs, ...errors, JSON.stringify(context.service.getRuntimeStatus())].join('\n');
|
|
632
|
+
strict_1.default.doesNotMatch(combined, /top-secret|\u001b/);
|
|
633
|
+
strict_1.default.match(combined, /REDACTED_SECRET/);
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
await context.service.stop();
|
|
637
|
+
context.close();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
(0, node_test_1.default)('unexpected startup initialization failure leaves the service retryable', async () => {
|
|
641
|
+
const context = fixture();
|
|
642
|
+
const originalList = context.store.listAutomations.bind(context.store);
|
|
643
|
+
let fail = true;
|
|
644
|
+
context.store.listAutomations = (workspaceId) => {
|
|
645
|
+
if (fail)
|
|
646
|
+
throw new Error('temporary initialization failure');
|
|
647
|
+
return originalList(workspaceId);
|
|
648
|
+
};
|
|
649
|
+
try {
|
|
650
|
+
await strict_1.default.rejects(() => context.service.start(), /temporary initialization failure/);
|
|
651
|
+
strict_1.default.deepEqual(context.service.getRuntimeStatus(), { status: 'stopped' });
|
|
652
|
+
fail = false;
|
|
653
|
+
await context.service.start();
|
|
654
|
+
strict_1.default.deepEqual(context.service.getRuntimeStatus(), { status: 'running' });
|
|
655
|
+
}
|
|
656
|
+
finally {
|
|
657
|
+
await context.service.stop();
|
|
658
|
+
context.close();
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
(0, node_test_1.default)('consumed once remains terminal across disable, re-enable, and unrelated updates', async () => {
|
|
662
|
+
let throwCondition = false;
|
|
663
|
+
const context = fixture({
|
|
664
|
+
conditionEvaluator: () => {
|
|
665
|
+
if (throwCondition)
|
|
666
|
+
throw new Error('condition failed');
|
|
667
|
+
return { kind: 'evaluated', matched: false };
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
try {
|
|
671
|
+
const notMatched = context.service.create(input({ title: 'not matched' }));
|
|
672
|
+
await context.service.checkNow(notMatched.id);
|
|
673
|
+
throwCondition = true;
|
|
674
|
+
const errored = context.service.create(input({ title: 'errored' }));
|
|
675
|
+
await context.service.checkNow(errored.id);
|
|
676
|
+
for (const automation of [notMatched, errored]) {
|
|
677
|
+
context.service.update(automation.id, { enabled: false });
|
|
678
|
+
context.service.update(automation.id, { title: `${automation.title} renamed` });
|
|
679
|
+
context.service.update(automation.id, { enabled: true });
|
|
680
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
681
|
+
}
|
|
682
|
+
const before = context.service.listEvaluations(notMatched.id).length + context.service.listEvaluations(errored.id).length;
|
|
683
|
+
await context.service.start();
|
|
684
|
+
await context.service.stop();
|
|
685
|
+
const after = context.service.listEvaluations(notMatched.id).length + context.service.listEvaluations(errored.id).length;
|
|
686
|
+
strict_1.default.equal(after, before);
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
context.close();
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
(0, node_test_1.default)('replacing a consumed once activation schedules the new runAt', async () => {
|
|
693
|
+
const context = fixture({ conditionEvaluator: () => ({ kind: 'evaluated', matched: false }) });
|
|
694
|
+
try {
|
|
695
|
+
const automation = context.service.create(input());
|
|
696
|
+
await context.service.checkNow(automation.id);
|
|
697
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
698
|
+
const runAt = '2026-07-05T08:30:00.000Z';
|
|
699
|
+
context.service.update(automation.id, { activation: { kind: 'once', runAt } });
|
|
700
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), runAt);
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
context.close();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
(0, node_test_1.default)('caught condition and action errors are redacted, control-free, and bounded before persistence', async () => {
|
|
707
|
+
const secret = `TOKEN=token-value api-key=api-value password=hunter2\u001b[31m ${'z'.repeat(4_000)}`;
|
|
708
|
+
const alerts = [];
|
|
709
|
+
const logs = [];
|
|
710
|
+
const context = fixture({
|
|
711
|
+
conditionEvaluator: () => { throw new Error(secret); },
|
|
712
|
+
alert: ({ error }) => alerts.push(error),
|
|
713
|
+
log: (message) => logs.push(message),
|
|
714
|
+
});
|
|
715
|
+
try {
|
|
716
|
+
const condition = context.service.create(input({ title: 'condition error' }));
|
|
717
|
+
await context.service.checkNow(condition.id);
|
|
718
|
+
const conditionEvaluation = context.service.listEvaluations(condition.id)[0];
|
|
719
|
+
const summary = conditionEvaluation?.status === 'finished' ? conditionEvaluation.resultSummary || '' : '';
|
|
720
|
+
for (const value of [summary, alerts[0] || '', logs.join('\n')]) {
|
|
721
|
+
strict_1.default.doesNotMatch(value, /token-value|api-value|hunter2|\u001b/);
|
|
722
|
+
strict_1.default.match(value, /REDACTED_SECRET/);
|
|
723
|
+
}
|
|
724
|
+
strict_1.default.ok(summary.length <= 2_000);
|
|
725
|
+
const actionContext = fixture({ actionExecutor: { async execute() { throw new Error(secret); } } });
|
|
726
|
+
try {
|
|
727
|
+
const action = actionContext.service.create(input({ title: 'action error' }));
|
|
728
|
+
await actionContext.service.checkNow(action.id);
|
|
729
|
+
const persisted = actionContext.service.listRuns(action.id)[0]?.error || '';
|
|
730
|
+
strict_1.default.doesNotMatch(persisted, /token-value|api-value|hunter2|\u001b/);
|
|
731
|
+
strict_1.default.match(persisted, /REDACTED_SECRET/);
|
|
732
|
+
strict_1.default.ok(persisted.length <= 2_000);
|
|
733
|
+
}
|
|
734
|
+
finally {
|
|
735
|
+
actionContext.close();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
context.close();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
(0, node_test_1.default)('startup fails stale queued and running runs before admitting due checks', async () => {
|
|
743
|
+
const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-stale-runs-'));
|
|
744
|
+
const initialStore = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
745
|
+
const automations = ['queued', 'running'].map((status, index) => {
|
|
746
|
+
const automation = initialStore.createAutomation(input({
|
|
747
|
+
title: `stale ${status}`,
|
|
748
|
+
activation: { kind: 'once', runAt: '2026-07-05T09:00:00.000Z' },
|
|
749
|
+
}));
|
|
750
|
+
const evaluation = initialStore.createAutomationEvaluation(automation.id, {
|
|
751
|
+
activationKind: 'once', startedAt: `2026-07-05T07:0${index}:00.000Z`,
|
|
752
|
+
});
|
|
753
|
+
initialStore.finishAutomationEvaluation(evaluation.id, {
|
|
754
|
+
conditionOutcome: 'matched', triggerDecision: 'triggered', finishedAt: `2026-07-05T07:0${index}:01.000Z`,
|
|
755
|
+
});
|
|
756
|
+
const run = initialStore.createAutomationRun(automation.id, evaluation.id, { status });
|
|
757
|
+
return { automation, run };
|
|
758
|
+
});
|
|
759
|
+
initialStore.close();
|
|
760
|
+
const restartedStore = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
|
|
761
|
+
const eventBus = new event_bus_js_1.LocalCoreEventBus();
|
|
762
|
+
const emitted = [];
|
|
763
|
+
eventBus.on('automation.run.updated', (run) => emitted.push(run.id));
|
|
764
|
+
const service = new automation_service_js_1.AutomationService({
|
|
765
|
+
store: restartedStore,
|
|
766
|
+
eventBus,
|
|
767
|
+
clock: () => new Date(NOW),
|
|
768
|
+
actionExecutor: {
|
|
769
|
+
async execute({ automation }) { return { threadId: automation.id, acpRunId: automation.id }; },
|
|
770
|
+
},
|
|
771
|
+
setInterval: () => 1,
|
|
772
|
+
clearInterval: () => undefined,
|
|
773
|
+
});
|
|
774
|
+
try {
|
|
775
|
+
await service.start();
|
|
776
|
+
for (const { automation, run } of automations) {
|
|
777
|
+
const recovered = service.listRuns(automation.id).find((entry) => entry.id === run.id);
|
|
778
|
+
strict_1.default.equal(recovered?.status, 'failed');
|
|
779
|
+
strict_1.default.match(recovered?.error || '', /interrupted.*restart/i);
|
|
780
|
+
strict_1.default.ok(emitted.includes(run.id));
|
|
781
|
+
await service.checkNow(automation.id);
|
|
782
|
+
strict_1.default.equal(service.listEvaluations(automation.id).at(0)?.triggerDecision, 'triggered');
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
finally {
|
|
786
|
+
await service.stop();
|
|
787
|
+
restartedStore.close();
|
|
788
|
+
(0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
(0, node_test_1.default)('stop settles a rejecting timer tick, drains admitted work, and remains stopped', async () => {
|
|
792
|
+
let release;
|
|
793
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
794
|
+
let admitted;
|
|
795
|
+
const didAdmit = new Promise((resolve) => { admitted = resolve; });
|
|
796
|
+
let actionFinished = false;
|
|
797
|
+
const logs = [];
|
|
798
|
+
const context = fixture({
|
|
799
|
+
maxConcurrency: 2,
|
|
800
|
+
log: (message) => logs.push(message),
|
|
801
|
+
actionExecutor: {
|
|
802
|
+
async execute({ automation }) {
|
|
803
|
+
admitted();
|
|
804
|
+
await blocked;
|
|
805
|
+
actionFinished = true;
|
|
806
|
+
return { threadId: automation.id, acpRunId: automation.id };
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
try {
|
|
811
|
+
await context.service.start();
|
|
812
|
+
const timer = context.timerHandler;
|
|
813
|
+
const admittedAutomation = context.store.createAutomation(input({ title: 'admitted' }));
|
|
814
|
+
const rejectingAutomation = context.store.createAutomation(input({ title: 'rejecting' }));
|
|
815
|
+
for (const automation of [admittedAutomation, rejectingAutomation]) {
|
|
816
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: '2026-07-05T07:59:00.000Z' });
|
|
817
|
+
}
|
|
818
|
+
const originalCreate = context.store.createAutomationEvaluation.bind(context.store);
|
|
819
|
+
context.store.createAutomationEvaluation = (automationId, value) => {
|
|
820
|
+
if (automationId === rejectingAutomation.id)
|
|
821
|
+
throw new Error('TOKEN=timer-secret timer exploded');
|
|
822
|
+
return originalCreate(automationId, value);
|
|
823
|
+
};
|
|
824
|
+
timer();
|
|
825
|
+
await didAdmit;
|
|
826
|
+
let stopResolved = false;
|
|
827
|
+
const stopping = context.service.stop().then(() => { stopResolved = true; });
|
|
828
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
829
|
+
strict_1.default.equal(stopResolved, false);
|
|
830
|
+
strict_1.default.deepEqual(context.service.getRuntimeStatus(), { status: 'stopped' });
|
|
831
|
+
release();
|
|
832
|
+
await stopping;
|
|
833
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
834
|
+
strict_1.default.equal(actionFinished, true);
|
|
835
|
+
strict_1.default.deepEqual(context.service.getRuntimeStatus(), { status: 'stopped' });
|
|
836
|
+
strict_1.default.match(logs.join('\n'), /REDACTED_SECRET/);
|
|
837
|
+
strict_1.default.doesNotMatch(logs.join('\n'), /timer-secret/);
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
release();
|
|
841
|
+
await context.service.stop();
|
|
842
|
+
context.close();
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
(0, node_test_1.default)('timer callback from an old generation cannot run or degrade a restarted service', async () => {
|
|
846
|
+
const context = fixture();
|
|
847
|
+
try {
|
|
848
|
+
await context.service.start();
|
|
849
|
+
const staleTimer = context.timerHandler;
|
|
850
|
+
await context.service.stop();
|
|
851
|
+
await context.service.start();
|
|
852
|
+
const automation = context.service.create(input({ title: 'must not run from stale timer' }));
|
|
853
|
+
context.store.pruneAutomationEvaluations = () => { throw new Error('stale timer failure'); };
|
|
854
|
+
staleTimer();
|
|
855
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
856
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
857
|
+
strict_1.default.deepEqual(context.service.getRuntimeStatus(), { status: 'running' });
|
|
858
|
+
strict_1.default.equal(context.service.listEvaluations(automation.id).length, 0);
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
context.store.pruneAutomationEvaluations = () => 0;
|
|
862
|
+
await context.service.stop();
|
|
863
|
+
context.close();
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
(0, node_test_1.default)('concurrent start calls share one initialization and one working timer', async () => {
|
|
867
|
+
const context = fixture();
|
|
868
|
+
const originalImport = context.store.importLegacyAutomations.bind(context.store);
|
|
869
|
+
const originalPrune = context.store.pruneAutomationEvaluations.bind(context.store);
|
|
870
|
+
let imports = 0;
|
|
871
|
+
let timers = 0;
|
|
872
|
+
let prunes = 0;
|
|
873
|
+
let timerHandler;
|
|
874
|
+
context.store.importLegacyAutomations = () => {
|
|
875
|
+
imports += 1;
|
|
876
|
+
return originalImport();
|
|
877
|
+
};
|
|
878
|
+
context.store.pruneAutomationEvaluations = (now) => {
|
|
879
|
+
prunes += 1;
|
|
880
|
+
return originalPrune(now);
|
|
881
|
+
};
|
|
882
|
+
const service = new automation_service_js_1.AutomationService({
|
|
883
|
+
store: context.store,
|
|
884
|
+
eventBus: context.eventBus,
|
|
885
|
+
clock: () => new Date(NOW),
|
|
886
|
+
actionExecutor: { async execute() { return { threadId: 't', acpRunId: 'r' }; } },
|
|
887
|
+
setInterval: (handler) => {
|
|
888
|
+
timers += 1;
|
|
889
|
+
timerHandler = handler;
|
|
890
|
+
return timers;
|
|
891
|
+
},
|
|
892
|
+
clearInterval: () => undefined,
|
|
893
|
+
});
|
|
894
|
+
try {
|
|
895
|
+
await Promise.all([service.start(), service.start()]);
|
|
896
|
+
strict_1.default.equal(imports, 1);
|
|
897
|
+
strict_1.default.equal(timers, 1);
|
|
898
|
+
strict_1.default.equal(prunes, 1);
|
|
899
|
+
timerHandler();
|
|
900
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
901
|
+
strict_1.default.equal(prunes, 2);
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
await service.stop();
|
|
905
|
+
context.close();
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
(0, node_test_1.default)('start during blocked startup catch-up joins the original startup promise', async () => {
|
|
909
|
+
const context = fixture();
|
|
910
|
+
const automation = context.store.createAutomation(input({ title: 'startup catch-up' }));
|
|
911
|
+
context.store.updateAutomationState(automation.id, { nextCheckAt: '2026-07-05T07:59:00.000Z' });
|
|
912
|
+
const originalImport = context.store.importLegacyAutomations.bind(context.store);
|
|
913
|
+
let imports = 0;
|
|
914
|
+
let timers = 0;
|
|
915
|
+
let release;
|
|
916
|
+
const blocked = new Promise((resolve) => { release = resolve; });
|
|
917
|
+
let admitted;
|
|
918
|
+
const didAdmit = new Promise((resolve) => { admitted = resolve; });
|
|
919
|
+
context.store.importLegacyAutomations = () => {
|
|
920
|
+
imports += 1;
|
|
921
|
+
return originalImport();
|
|
922
|
+
};
|
|
923
|
+
const service = new automation_service_js_1.AutomationService({
|
|
924
|
+
store: context.store,
|
|
925
|
+
eventBus: context.eventBus,
|
|
926
|
+
clock: () => new Date(NOW),
|
|
927
|
+
actionExecutor: {
|
|
928
|
+
async execute() {
|
|
929
|
+
admitted();
|
|
930
|
+
await blocked;
|
|
931
|
+
return { threadId: 'thread-1', acpRunId: 'run-1' };
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
setInterval: () => {
|
|
935
|
+
timers += 1;
|
|
936
|
+
return timers;
|
|
937
|
+
},
|
|
938
|
+
clearInterval: () => undefined,
|
|
939
|
+
});
|
|
940
|
+
try {
|
|
941
|
+
const first = service.start();
|
|
942
|
+
await didAdmit;
|
|
943
|
+
let secondResolved = false;
|
|
944
|
+
const second = service.start().then(() => { secondResolved = true; });
|
|
945
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
946
|
+
strict_1.default.equal(secondResolved, false);
|
|
947
|
+
strict_1.default.equal(imports, 1);
|
|
948
|
+
strict_1.default.equal(timers, 1);
|
|
949
|
+
release();
|
|
950
|
+
await Promise.all([first, second]);
|
|
951
|
+
strict_1.default.equal(secondResolved, true);
|
|
952
|
+
strict_1.default.equal(imports, 1);
|
|
953
|
+
strict_1.default.equal(timers, 1);
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
release();
|
|
957
|
+
await service.stop();
|
|
958
|
+
context.close();
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
(0, node_test_1.default)('enabling never-run disabled definitions initializes missing schedules while running', async () => {
|
|
962
|
+
const context = fixture();
|
|
963
|
+
try {
|
|
964
|
+
await context.service.start();
|
|
965
|
+
const automations = [
|
|
966
|
+
context.service.create(input({
|
|
967
|
+
title: 'disabled cron',
|
|
968
|
+
enabled: false,
|
|
969
|
+
activation: { kind: 'cron', expression: '*/5 * * * *', timezone: 'UTC' },
|
|
970
|
+
})),
|
|
971
|
+
context.service.create(input({
|
|
972
|
+
title: 'disabled interval',
|
|
973
|
+
enabled: false,
|
|
974
|
+
activation: { kind: 'interval', intervalMs: 60_000 },
|
|
975
|
+
})),
|
|
976
|
+
context.service.create(input({ title: 'disabled once', enabled: false })),
|
|
977
|
+
];
|
|
978
|
+
for (const automation of automations) {
|
|
979
|
+
strict_1.default.equal(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
980
|
+
context.service.update(automation.id, { enabled: true });
|
|
981
|
+
strict_1.default.notEqual(context.store.getAutomationNextCheckAt(automation.id), null);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
await context.service.stop();
|
|
986
|
+
context.close();
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
(0, node_test_1.default)('non-throwing action delivery errors are sanitized before run persistence', async () => {
|
|
990
|
+
const secret = `password=delivery-secret\u001b[31m ${'d'.repeat(4_000)}`;
|
|
991
|
+
const context = fixture({
|
|
992
|
+
actionExecutor: {
|
|
993
|
+
async execute() {
|
|
994
|
+
return {
|
|
995
|
+
threadId: 'thread-1',
|
|
996
|
+
acpRunId: 'run-1',
|
|
997
|
+
deliveryStatus: 'failed',
|
|
998
|
+
deliveryError: secret,
|
|
999
|
+
};
|
|
1000
|
+
},
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
try {
|
|
1004
|
+
const automation = context.service.create(input({ title: 'delivery error' }));
|
|
1005
|
+
await context.service.checkNow(automation.id);
|
|
1006
|
+
const run = context.service.listRuns(automation.id)[0];
|
|
1007
|
+
strict_1.default.equal(run?.status, 'succeeded');
|
|
1008
|
+
strict_1.default.equal(run?.deliveryStatus, 'failed');
|
|
1009
|
+
strict_1.default.doesNotMatch(run?.error || '', /delivery-secret|\u001b/);
|
|
1010
|
+
strict_1.default.match(run?.error || '', /REDACTED_SECRET/);
|
|
1011
|
+
strict_1.default.ok((run?.error || '').length <= 2_000);
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
context.close();
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
(0, node_test_1.default)('definition CRUD remains successful when an event projection listener throws', () => {
|
|
1018
|
+
const context = fixture();
|
|
1019
|
+
context.eventBus.on('automation.definition.updated', () => { throw new Error('projection listener failed'); });
|
|
1020
|
+
try {
|
|
1021
|
+
const created = context.service.create(input({ title: 'committed despite listener' }));
|
|
1022
|
+
strict_1.default.equal(context.service.get(created.id)?.title, 'committed despite listener');
|
|
1023
|
+
const updated = context.service.update(created.id, { title: 'updated despite listener' });
|
|
1024
|
+
strict_1.default.equal(updated.title, 'updated despite listener');
|
|
1025
|
+
}
|
|
1026
|
+
finally {
|
|
1027
|
+
context.close();
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
(0, node_test_1.default)('definition and initial schedule writes roll back atomically when schedule calculation fails', () => {
|
|
1031
|
+
const context = fixture({ clock: () => new Date(Number.NaN) });
|
|
1032
|
+
try {
|
|
1033
|
+
strict_1.default.throws(() => context.service.create(input()), /invalid date/);
|
|
1034
|
+
strict_1.default.equal(context.service.list().length, 0);
|
|
1035
|
+
}
|
|
1036
|
+
finally {
|
|
1037
|
+
context.close();
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
(0, node_test_1.default)('activation update rolls back atomically when replacement schedule calculation fails', () => {
|
|
1041
|
+
let invalid = false;
|
|
1042
|
+
const context = fixture({ clock: () => invalid ? new Date(Number.NaN) : new Date(NOW) });
|
|
1043
|
+
try {
|
|
1044
|
+
const created = context.service.create(input());
|
|
1045
|
+
invalid = true;
|
|
1046
|
+
strict_1.default.throws(() => context.service.update(created.id, {
|
|
1047
|
+
activation: { kind: 'interval', intervalMs: 60_000 },
|
|
1048
|
+
}), /invalid date/);
|
|
1049
|
+
strict_1.default.deepEqual(context.service.get(created.id)?.activation, created.activation);
|
|
1050
|
+
}
|
|
1051
|
+
finally {
|
|
1052
|
+
context.close();
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
(0, node_test_1.default)('fail-closed definition and provider health changes roll back atomically on database rejection', () => {
|
|
1056
|
+
const context = fixture();
|
|
1057
|
+
try {
|
|
1058
|
+
const automation = context.store.createTrustedAutomation({
|
|
1059
|
+
...input({ activation: { kind: 'provider-event', sourceType: 'stock.quote', sourceConfig: {} } }),
|
|
1060
|
+
originKind: 'automation-monitor',
|
|
1061
|
+
});
|
|
1062
|
+
const db = context.store.db;
|
|
1063
|
+
db.exec(`
|
|
1064
|
+
CREATE TRIGGER reject_provider_block
|
|
1065
|
+
BEFORE UPDATE ON automations
|
|
1066
|
+
WHEN NEW.health = 'blocked'
|
|
1067
|
+
BEGIN
|
|
1068
|
+
SELECT RAISE(ABORT, 'provider block rejected');
|
|
1069
|
+
END
|
|
1070
|
+
`);
|
|
1071
|
+
strict_1.default.throws(() => context.service.failClosedLegacyAutomation(automation.id, 'provider failed'), /provider block rejected/);
|
|
1072
|
+
strict_1.default.equal(context.service.get(automation.id)?.enabled, true);
|
|
1073
|
+
strict_1.default.equal(context.service.get(automation.id)?.health, 'healthy');
|
|
1074
|
+
}
|
|
1075
|
+
finally {
|
|
1076
|
+
context.close();
|
|
1077
|
+
}
|
|
1078
|
+
});
|