@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.
Files changed (127) hide show
  1. package/README.md +18 -0
  2. package/dist/renderer/assets/AutomationList-BgJgAdVO.js +16 -0
  3. package/dist/renderer/assets/{Badge-BLI4LHse.js → Badge-Bq2hPVcR.js} +1 -1
  4. package/dist/renderer/assets/{Card-DJeLDdL3.js → Card-BPJNSZce.js} +1 -1
  5. package/dist/renderer/assets/{Config-D1awc81A.js → Config-aSFqlI88.js} +2 -2
  6. package/dist/renderer/assets/Dashboard-D3ezFmb7.js +36 -0
  7. package/dist/renderer/assets/{EmptyState-CkI1RwPX.js → EmptyState-DK496gBq.js} +1 -1
  8. package/dist/renderer/assets/{HighlightedMarkdown-BUhDlhgW.js → HighlightedMarkdown-Ckre5jNv.js} +1 -1
  9. package/dist/renderer/assets/{Input-DHYrn-Ne.js → Input-C5C1d0ba.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeDetail-Bij-Xo90.js → KnowledgeDetail-N8Ey1ikc.js} +2 -2
  11. package/dist/renderer/assets/{KnowledgeHome-DmPZ2IlI.js → KnowledgeHome-9aF8nJEf.js} +2 -2
  12. package/dist/renderer/assets/{Logs-ISY99heA.js → Logs-sgAdwEeS.js} +2 -2
  13. package/dist/renderer/assets/{Modal-DAQfz0ot.js → Modal-BB10hTRc.js} +3 -3
  14. package/dist/renderer/assets/{Page-Dw43UYDT.js → Page-DfC319Ck.js} +1 -1
  15. package/dist/renderer/assets/{Select-DzmN5OGQ.js → Select-RHJBmNER.js} +1 -1
  16. package/dist/renderer/assets/{ThreadChat-B5CWqqMK.js → ThreadChat-BU1zCdr4.js} +3 -3
  17. package/dist/renderer/assets/{Workspace-C5g9LBj9.js → Workspace-BoZCz7S8.js} +1 -1
  18. package/dist/renderer/assets/{arrow-left-BLXh56Ip.js → arrow-left-D2V1eZSD.js} +1 -1
  19. package/dist/renderer/assets/{book-open-DhPGbc-7.js → book-open-DUcBn7Bb.js} +1 -1
  20. package/dist/renderer/assets/{channels-DfQrmuYO.js → channels-DMhDSlf2.js} +2 -2
  21. package/dist/renderer/assets/{chevron-down-CEFtpPDc.js → chevron-down-Ca2FQDB7.js} +1 -1
  22. package/dist/renderer/assets/en-Bnr4nMzg.js +1 -0
  23. package/dist/renderer/assets/es-B1GIFh3i.js +1 -0
  24. package/dist/renderer/assets/{index-BIKL3fTP.css → index-B6WcpkJF.css} +1 -1
  25. package/dist/renderer/assets/{index-CYhDUAR_.js → index-BlnvlekJ.js} +1 -1
  26. package/dist/renderer/assets/index-DRJ5wgw_.js +162 -0
  27. package/dist/renderer/assets/ja-CDWPWqEW.js +1 -0
  28. package/dist/renderer/assets/{knowledge-uHRQ80sR.js → knowledge-D1-k56eY.js} +1 -1
  29. package/dist/renderer/assets/{pencil-BD2Y31YC.js → pencil-BhWdv_86.js} +1 -1
  30. package/dist/renderer/assets/{plus-DDcldp_c.js → plus-BacLMlK3.js} +1 -1
  31. package/dist/renderer/assets/{save-CMW_cZI9.js → save-Bpoxbzau.js} +1 -1
  32. package/dist/renderer/assets/{search-BM-0CvJr.js → search-BVJcpdFX.js} +1 -1
  33. package/dist/renderer/assets/{shield-check-DaMB0FKG.js → shield-check-C2cw_Ip6.js} +1 -1
  34. package/dist/renderer/assets/{threads-DZgt9tWj.js → threads-DOsBQBg_.js} +1 -1
  35. package/dist/renderer/assets/{trash-2-DTXVbC5y.js → trash-2-Bs3rEm3g.js} +1 -1
  36. package/dist/renderer/assets/zh-MSddxMDX.js +1 -0
  37. package/dist/renderer/assets/zh-TW-C_LNWX0o.js +1 -0
  38. package/dist/renderer/index.html +2 -2
  39. package/dist-electron/electron/managed-skills/agent-browser/SKILL.md +41 -0
  40. package/dist-electron/electron/managed-skills/agent-browser/references/authentication.md +83 -0
  41. package/dist-electron/electron/managed-skills/agent-browser/references/commands.md +50 -0
  42. package/dist-electron/electron/managed-skills/agent-browser/references/profiling.md +20 -0
  43. package/dist-electron/electron/managed-skills/agent-browser/references/proxy-support.md +18 -0
  44. package/dist-electron/electron/managed-skills/agent-browser/references/session-management.md +17 -0
  45. package/dist-electron/electron/managed-skills/agent-browser/references/snapshot-refs.md +17 -0
  46. package/dist-electron/electron/managed-skills/agent-browser/references/video-recording.md +21 -0
  47. package/dist-electron/electron/managed-skills/agent-browser/templates/authenticated-session.sh +11 -0
  48. package/dist-electron/electron/managed-skills/agent-browser/templates/capture-workflow.sh +12 -0
  49. package/dist-electron/electron/managed-skills/agent-browser/templates/form-automation.sh +10 -0
  50. package/dist-electron/electron/managed-skills/condition-trigger/SKILL.md +21 -0
  51. package/dist-electron/electron/managed-skills/condition-trigger/scripts/register-condition-trigger.sh +106 -0
  52. package/dist-electron/electron/managed-skills/knowledge-base/SKILL.md +13 -0
  53. package/dist-electron/electron/managed-skills/knowledge-base/scripts/search-knowledge.sh +80 -0
  54. package/dist-electron/packages/contracts/src/automations.js +211 -0
  55. package/dist-electron/packages/contracts/src/index.js +1 -0
  56. package/dist-electron/packages/core-sdk/src/automations.js +87 -0
  57. package/dist-electron/packages/core-sdk/src/client.js +101 -0
  58. package/dist-electron/packages/core-sdk/src/index.js +2 -1
  59. package/dist-electron/services/local-ai-core/src/acp/store/automation-script-store.js +415 -0
  60. package/dist-electron/services/local-ai-core/src/acp/store/automation-store.js +829 -0
  61. package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +158 -0
  62. package/dist-electron/services/local-ai-core/src/acp/store/schema.js +81 -0
  63. package/dist-electron/services/local-ai-core/src/automation/automation-action-executor.js +144 -0
  64. package/dist-electron/services/local-ai-core/src/automation/automation-condition-engine.js +79 -0
  65. package/dist-electron/services/local-ai-core/src/automation/automation-conversation-executor.js +39 -73
  66. package/dist-electron/services/local-ai-core/src/automation/automation-monitor-service.js +695 -260
  67. package/dist-electron/services/local-ai-core/src/automation/automation-script-service.js +507 -0
  68. package/dist-electron/services/local-ai-core/src/automation/automation-service.js +1019 -0
  69. package/dist-electron/services/local-ai-core/src/automation/automation-trigger-engine.js +141 -0
  70. package/dist-electron/services/local-ai-core/src/automation/condition-evaluator.js +60 -17
  71. package/dist-electron/services/local-ai-core/src/automation/legacy-automation-mappers.js +286 -0
  72. package/dist-electron/services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js +792 -0
  73. package/dist-electron/services/local-ai-core/src/automation/scripts/sandbox-runner.js +2 -0
  74. package/dist-electron/services/local-ai-core/src/automation/scripts/script-package.js +475 -0
  75. package/dist-electron/services/local-ai-core/src/automation/scripts/script-protocol-runner.js +266 -0
  76. package/dist-electron/services/local-ai-core/src/automation/scripts/secret-resolver.js +37 -0
  77. package/dist-electron/services/local-ai-core/src/channel/weixin/inbound-poller.js +1 -2
  78. package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +9 -8
  79. package/dist-electron/services/local-ai-core/src/cli/lac.js +229 -0
  80. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +21 -0
  81. package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +76 -0
  82. package/dist-electron/services/local-ai-core/src/runtime/handlers/automations-handler.js +322 -0
  83. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +20 -0
  84. package/dist-electron/services/local-ai-core/src/runtime/managed-skill-catalog.js +37 -0
  85. package/dist-electron/services/local-ai-core/src/runtime/server-helpers.js +14 -4
  86. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +75 -0
  87. package/dist-electron/services/local-ai-core/src/runtime/server.js +21 -0
  88. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +1 -0
  89. package/dist-electron/services/local-ai-core/src/scheduler/cron.js +151 -39
  90. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +2 -4
  91. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +64 -12
  92. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +71 -4
  93. package/dist-electron/services/local-ai-core/src/scheduler/thread-resolution.js +10 -0
  94. package/dist-electron/services/local-ai-core/src/thread/agent-message-policy.js +16 -1
  95. package/dist-electron/src/pages/Automation/automation-page-model.js +76 -0
  96. package/dist-electron/tests/contracts/agent-message-policy.test.js +32 -0
  97. package/dist-electron/tests/contracts/architecture-docs.test.js +30 -0
  98. package/dist-electron/tests/contracts/automation-contracts.test.js +154 -0
  99. package/dist-electron/tests/contracts/automation-renderer-model.test.js +47 -0
  100. package/dist-electron/tests/electron/automation-action-executor.test.js +75 -0
  101. package/dist-electron/tests/electron/automation-compatibility.test.js +463 -0
  102. package/dist-electron/tests/electron/automation-deployment.test.js +66 -0
  103. package/dist-electron/tests/electron/automation-engine.test.js +264 -0
  104. package/dist-electron/tests/electron/automation-monitor.test.js +1333 -0
  105. package/dist-electron/tests/electron/automation-sandbox-runner.test.js +333 -0
  106. package/dist-electron/tests/electron/automation-script-approval.test.js +553 -0
  107. package/dist-electron/tests/electron/automation-script-package.test.js +499 -0
  108. package/dist-electron/tests/electron/automation-script-runner.test.js +120 -0
  109. package/dist-electron/tests/electron/automation-service.test.js +1078 -0
  110. package/dist-electron/tests/electron/automation-store.test.js +593 -0
  111. package/dist-electron/tests/electron/condition-trigger-skill.test.js +93 -0
  112. package/dist-electron/tests/electron/core-client.test.js +99 -0
  113. package/dist-electron/tests/electron/lac-cli.test.js +110 -0
  114. package/dist-electron/tests/electron/workspace-task-store.test.js +25 -0
  115. package/dist-electron/tests/integration/automation-routes.test.js +186 -0
  116. package/dist-electron/tests/integration/automation-sandbox-runtime.test.js +92 -0
  117. package/package.json +4 -3
  118. package/dist/renderer/assets/CronList-CupZoNwW.js +0 -1
  119. package/dist/renderer/assets/Dashboard-nLZnOtcs.js +0 -41
  120. package/dist/renderer/assets/MonitorList-BlX82rXF.js +0 -1
  121. package/dist/renderer/assets/en-CmAr_0e6.js +0 -1
  122. package/dist/renderer/assets/es-Cs9x52ca.js +0 -1
  123. package/dist/renderer/assets/index-Dd00oqgY.js +0 -167
  124. package/dist/renderer/assets/ja-DtvtBRTW.js +0 -1
  125. package/dist/renderer/assets/play-DBJ8wsSy.js +0 -6
  126. package/dist/renderer/assets/zh-CcJ9vBZj.js +0 -1
  127. package/dist/renderer/assets/zh-TW-elI72AC1.js +0 -1
@@ -0,0 +1,593 @@
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 automation_store_js_1 = require("../../services/local-ai-core/src/acp/store/automation-store.js");
13
+ function createInput(overrides = {}) {
14
+ return {
15
+ workspaceId: 'workspace-1',
16
+ title: 'Check service health',
17
+ enabled: true,
18
+ activation: { kind: 'cron', expression: '*/5 * * * *', timezone: 'UTC' },
19
+ condition: { kind: 'expression', expression: 'status == "ok"' },
20
+ action: { kind: 'agent-prompt', promptTemplate: 'Investigate {{status}}', executionMode: 'side-thread' },
21
+ delivery: { platform: 'local', route: { type: 'local.thread', channelId: 'workspace-1' } },
22
+ policies: { concurrency: 'skip-if-running', cooldownMs: 1_000 },
23
+ ...overrides,
24
+ };
25
+ }
26
+ function fixture() {
27
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-store-'));
28
+ const facade = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
29
+ const db = facade.db;
30
+ const store = new automation_store_js_1.LocalAutomationStore(db);
31
+ return {
32
+ facade,
33
+ store,
34
+ db,
35
+ close() {
36
+ facade.close();
37
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
38
+ },
39
+ };
40
+ }
41
+ (0, node_test_1.default)('creates, reads, updates, and persists trusted automation state', () => {
42
+ const context = fixture();
43
+ try {
44
+ const created = context.store.create(createInput());
45
+ strict_1.default.equal(context.store.get(created.id)?.title, 'Check service health');
46
+ strict_1.default.equal(context.store.list('workspace-1').length, 1);
47
+ strict_1.default.equal(context.store.list('other-workspace').length, 0);
48
+ const updated = context.store.update(created.id, { title: 'Check API health', enabled: false });
49
+ strict_1.default.equal(updated.title, 'Check API health');
50
+ strict_1.default.equal(updated.enabled, false);
51
+ const state = context.store.updateState(created.id, {
52
+ health: 'blocked',
53
+ blockedReason: 'Approved script was revoked',
54
+ lastSuccessfulMatch: false,
55
+ lastEvaluationAt: '2026-07-05T01:00:00.000Z',
56
+ consecutiveEvaluationFailures: 2,
57
+ nextCheckAt: '2026-07-05T01:05:00.000Z',
58
+ });
59
+ strict_1.default.equal(state.health, 'blocked');
60
+ strict_1.default.equal(state.blockedReason, 'Approved script was revoked');
61
+ strict_1.default.equal(state.lastSuccessfulMatch, false);
62
+ strict_1.default.equal(state.consecutiveEvaluationFailures, 2);
63
+ const row = context.db.prepare('SELECT next_check_at FROM automations WHERE id = ?').get(created.id);
64
+ strict_1.default.equal(row.next_check_at, '2026-07-05T01:05:00.000Z');
65
+ strict_1.default.equal(context.store.getNextCheckAt(created.id), '2026-07-05T01:05:00.000Z');
66
+ strict_1.default.equal(context.facade.getAutomationNextCheckAt(created.id), '2026-07-05T01:05:00.000Z');
67
+ }
68
+ finally {
69
+ context.close();
70
+ }
71
+ });
72
+ (0, node_test_1.default)('reconciles queued and running action runs as interrupted after restart', () => {
73
+ const context = fixture();
74
+ try {
75
+ const createdRuns = ['queued', 'running'].map((status, index) => {
76
+ const automation = context.store.create(createInput({ title: `Automation ${index}` }));
77
+ const evaluation = context.store.createEvaluation(automation.id, {
78
+ activationKind: 'once',
79
+ startedAt: `2026-07-05T07:0${index}:00.000Z`,
80
+ });
81
+ context.store.finishEvaluation(evaluation.id, {
82
+ conditionOutcome: 'matched',
83
+ triggerDecision: 'triggered',
84
+ finishedAt: `2026-07-05T07:0${index}:01.000Z`,
85
+ });
86
+ return context.store.createRun(automation.id, evaluation.id, { status });
87
+ });
88
+ const recovered = context.store.reconcileInterruptedRuns('Automation action interrupted by Local AI Core restart.', '2026-07-05T08:00:00.000Z');
89
+ strict_1.default.deepEqual(new Set(recovered.map((run) => run.id)), new Set(createdRuns.map((run) => run.id)));
90
+ for (const run of recovered) {
91
+ strict_1.default.equal(run.status, 'failed');
92
+ strict_1.default.equal(run.deliveryStatus, 'failed');
93
+ strict_1.default.equal(run.finishedAt, '2026-07-05T08:00:00.000Z');
94
+ strict_1.default.match(run.error || '', /interrupted.*restart/i);
95
+ }
96
+ strict_1.default.equal(context.facade.reconcileInterruptedAutomationRuns('again', '2026-07-05T08:01:00.000Z').length, 0);
97
+ }
98
+ finally {
99
+ context.close();
100
+ }
101
+ });
102
+ (0, node_test_1.default)('enforces blocked health reasons and clears stale reasons when health recovers', () => {
103
+ const context = fixture();
104
+ try {
105
+ const automation = context.store.create(createInput());
106
+ strict_1.default.throws(() => context.store.updateState(automation.id, { health: 'blocked' }), /blocked reason/i);
107
+ strict_1.default.throws(() => context.store.updateState(automation.id, { health: 'blocked', blockedReason: ' ' }), /blocked reason/i);
108
+ const blocked = context.store.updateState(automation.id, {
109
+ health: 'blocked',
110
+ blockedReason: 'Sandbox unavailable',
111
+ });
112
+ strict_1.default.equal(blocked.blockedReason, 'Sandbox unavailable');
113
+ const healthy = context.store.updateState(automation.id, { health: 'healthy' });
114
+ strict_1.default.equal(healthy.health, 'healthy');
115
+ strict_1.default.equal(healthy.blockedReason, undefined);
116
+ strict_1.default.throws(() => context.store.updateState(automation.id, { health: 'blocked' }), /blocked reason/i);
117
+ }
118
+ finally {
119
+ context.close();
120
+ }
121
+ });
122
+ (0, node_test_1.default)('canonicalizes persisted timestamps and orders mixed offsets chronologically', () => {
123
+ const context = fixture();
124
+ try {
125
+ const automation = context.store.create(createInput({
126
+ activation: { kind: 'once', runAt: '2026-07-05T12:34:56.1+08:00' },
127
+ }));
128
+ strict_1.default.deepEqual(automation.activation, { kind: 'once', runAt: '2026-07-05T04:34:56.100Z' });
129
+ const state = context.store.updateState(automation.id, {
130
+ lastEvaluationAt: '2026-07-05T12:34:56.12+08:00',
131
+ lastTriggeredAt: '2026-07-05T12:34:56.123+08:00',
132
+ nextCheckAt: '2026-07-05T12:35:00+08:00',
133
+ });
134
+ strict_1.default.equal(state.lastEvaluationAt, '2026-07-05T04:34:56.120Z');
135
+ strict_1.default.equal(state.lastTriggeredAt, '2026-07-05T04:34:56.123Z');
136
+ const definitionRow = context.db.prepare('SELECT next_check_at FROM automations WHERE id = ?').get(automation.id);
137
+ strict_1.default.equal(definitionRow.next_check_at, '2026-07-05T04:35:00.000Z');
138
+ const lexicallyLater = context.store.createEvaluation(automation.id, {
139
+ activationKind: 'once',
140
+ startedAt: '2026-07-05T00:30:00.12+14:00',
141
+ });
142
+ context.store.finishEvaluation(lexicallyLater.id, {
143
+ conditionOutcome: 'skipped',
144
+ triggerDecision: 'not_evaluated',
145
+ finishedAt: '2026-07-05T00:30:01.1+14:00',
146
+ nextState: { marker: 'chronologically-older' },
147
+ });
148
+ const chronologicallyLater = context.store.createEvaluation(automation.id, {
149
+ activationKind: 'once',
150
+ startedAt: '2026-07-04T23:00:00.123-02:00',
151
+ });
152
+ context.store.finishEvaluation(chronologicallyLater.id, {
153
+ conditionOutcome: 'skipped',
154
+ triggerDecision: 'not_evaluated',
155
+ finishedAt: '2026-07-04T23:00:01.12-02:00',
156
+ nextState: { marker: 'chronologically-later' },
157
+ });
158
+ strict_1.default.equal(context.store.listEvaluations(automation.id)[0]?.id, chronologicallyLater.id);
159
+ const oldByUtc = context.store.createEvaluation(automation.id, {
160
+ activationKind: 'once',
161
+ startedAt: '2026-06-06T00:00:00.123+14:00',
162
+ });
163
+ context.store.pruneEvaluations(new Date('2026-07-05T12:00:00.000Z'));
164
+ strict_1.default.ok(!context.store.listEvaluations(automation.id).some((entry) => entry.id === oldByUtc.id));
165
+ context.store.pruneEvaluations(new Date('2027-07-05T00:00:00.000Z'));
166
+ const retained = context.store.listEvaluations(automation.id);
167
+ strict_1.default.equal(retained.length, 1);
168
+ strict_1.default.equal(retained[0]?.id, chronologicallyLater.id);
169
+ if (retained[0]?.status === 'finished') {
170
+ strict_1.default.deepEqual(retained[0].nextState, { marker: 'chronologically-later' });
171
+ }
172
+ }
173
+ finally {
174
+ context.close();
175
+ }
176
+ });
177
+ (0, node_test_1.default)('persists unified automations through the LocalCoreAcpStore facade', () => {
178
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-facade-'));
179
+ try {
180
+ const first = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
181
+ const created = first.createAutomation(createInput());
182
+ first.updateAutomation(created.id, { title: 'Persisted automation' });
183
+ first.updateAutomationState(created.id, { lastSuccessfulMatch: true, health: 'healthy' });
184
+ first.close();
185
+ const reopened = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
186
+ strict_1.default.equal(reopened.getAutomation(created.id)?.title, 'Persisted automation');
187
+ strict_1.default.equal(reopened.getAutomation(created.id)?.lastSuccessfulMatch, true);
188
+ reopened.close();
189
+ }
190
+ finally {
191
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
192
+ }
193
+ });
194
+ (0, node_test_1.default)('keeps evaluations and action runs separate while enforcing the evaluation lifecycle', () => {
195
+ const context = fixture();
196
+ try {
197
+ const automation = context.store.create(createInput());
198
+ const evaluation = context.store.createEvaluation(automation.id, {
199
+ activationKind: 'cron',
200
+ startedAt: '2026-07-05T02:00:00.000Z',
201
+ });
202
+ strict_1.default.deepEqual(evaluation, {
203
+ id: evaluation.id,
204
+ automationId: automation.id,
205
+ status: 'running',
206
+ activationKind: 'cron',
207
+ startedAt: '2026-07-05T02:00:00.000Z',
208
+ });
209
+ strict_1.default.throws(() => context.store.finishEvaluation(evaluation.id, {
210
+ conditionOutcome: 'error',
211
+ triggerDecision: 'not_rising',
212
+ finishedAt: '2026-07-05T02:00:01.000Z',
213
+ }), /trigger decision/i);
214
+ const finished = context.store.finishEvaluation(evaluation.id, {
215
+ conditionOutcome: 'matched',
216
+ triggerDecision: 'triggered',
217
+ triggeredAt: '2026-07-05T02:00:01.000Z',
218
+ finishedAt: '2026-07-05T02:00:01.000Z',
219
+ durationMs: 1_000,
220
+ payload: { status: 'ok' },
221
+ });
222
+ strict_1.default.equal(finished.status, 'finished');
223
+ strict_1.default.throws(() => context.store.finishEvaluation(evaluation.id, {
224
+ conditionOutcome: 'not_matched',
225
+ triggerDecision: 'not_rising',
226
+ finishedAt: '2026-07-05T02:00:02.000Z',
227
+ }), /already finished/i);
228
+ const run = context.store.createRun(automation.id, evaluation.id, { status: 'running' });
229
+ strict_1.default.equal(run.executionMode, 'side-thread');
230
+ strict_1.default.equal(context.store.listEvaluations(automation.id).length, 1);
231
+ strict_1.default.equal(context.store.listRuns(automation.id).length, 1);
232
+ strict_1.default.equal(context.store.updateRun(run.id, {
233
+ status: 'succeeded',
234
+ acpRunId: 'acp-run-1',
235
+ finishedAt: '2026-07-05T02:01:00.000Z',
236
+ }).status, 'succeeded');
237
+ }
238
+ finally {
239
+ context.close();
240
+ }
241
+ });
242
+ (0, node_test_1.default)('creates at most one action run only for a triggered matched evaluation', () => {
243
+ const context = fixture();
244
+ try {
245
+ const automation = context.store.create(createInput());
246
+ const running = context.store.createEvaluation(automation.id, {
247
+ activationKind: 'cron',
248
+ startedAt: '2026-07-05T02:10:00.000Z',
249
+ });
250
+ strict_1.default.throws(() => context.store.createRun(automation.id, running.id), /finished.*matched.*triggered/i);
251
+ const rejectedResults = [
252
+ { conditionOutcome: 'not_matched', triggerDecision: 'not_rising' },
253
+ { conditionOutcome: 'error', triggerDecision: 'not_evaluated' },
254
+ { conditionOutcome: 'skipped', triggerDecision: 'not_evaluated' },
255
+ ];
256
+ for (const [index, result] of rejectedResults.entries()) {
257
+ const evaluation = context.store.createEvaluation(automation.id, {
258
+ activationKind: 'cron',
259
+ startedAt: `2026-07-05T02:1${index + 1}:00.000Z`,
260
+ });
261
+ context.store.finishEvaluation(evaluation.id, {
262
+ ...result,
263
+ finishedAt: `2026-07-05T02:1${index + 1}:01.000Z`,
264
+ });
265
+ strict_1.default.throws(() => context.store.createRun(automation.id, evaluation.id), /finished.*matched.*triggered/i);
266
+ }
267
+ const triggered = context.store.createEvaluation(automation.id, {
268
+ activationKind: 'cron',
269
+ startedAt: '2026-07-05T02:20:00.000Z',
270
+ });
271
+ context.store.finishEvaluation(triggered.id, {
272
+ conditionOutcome: 'matched',
273
+ triggerDecision: 'triggered',
274
+ finishedAt: '2026-07-05T02:20:01.000Z',
275
+ });
276
+ context.store.createRun(automation.id, triggered.id);
277
+ strict_1.default.throws(() => context.store.createRun(automation.id, triggered.id), /already has an action run|unique/i);
278
+ }
279
+ finally {
280
+ context.close();
281
+ }
282
+ });
283
+ (0, node_test_1.default)('ignores extra runtime keys that attempt to overwrite protected persisted fields', () => {
284
+ const context = fixture();
285
+ try {
286
+ const automation = context.store.create({
287
+ ...createInput(),
288
+ id: 'attacker-automation',
289
+ health: 'blocked',
290
+ originKind: 'automation-monitor',
291
+ legacyMetadata: { scheduledDescription: 'attacker' },
292
+ createdAt: '2020-01-01T00:00:00.000Z',
293
+ });
294
+ strict_1.default.notEqual(automation.id, 'attacker-automation');
295
+ strict_1.default.equal(automation.health, 'healthy');
296
+ strict_1.default.equal(automation.originKind, 'native');
297
+ strict_1.default.equal(automation.legacyMetadata, undefined);
298
+ const updated = context.store.update(automation.id, {
299
+ title: 'Safe update',
300
+ id: 'attacker-update',
301
+ workspaceId: 'attacker-workspace',
302
+ health: 'blocked',
303
+ createdAt: '2020-01-01T00:00:00.000Z',
304
+ legacyMetadata: { scheduledDescription: 'attacker-update' },
305
+ });
306
+ strict_1.default.equal(updated.id, automation.id);
307
+ strict_1.default.equal(updated.workspaceId, 'workspace-1');
308
+ strict_1.default.equal(updated.health, 'healthy');
309
+ strict_1.default.equal(updated.createdAt, automation.createdAt);
310
+ strict_1.default.equal(updated.legacyMetadata, undefined);
311
+ const evaluation = context.store.createEvaluation(automation.id, {
312
+ activationKind: 'cron',
313
+ startedAt: '2026-07-05T03:00:00.000Z',
314
+ id: 'attacker-evaluation',
315
+ automationId: 'attacker-automation',
316
+ status: 'finished',
317
+ });
318
+ strict_1.default.notEqual(evaluation.id, 'attacker-evaluation');
319
+ strict_1.default.equal(evaluation.automationId, automation.id);
320
+ strict_1.default.equal(evaluation.status, 'running');
321
+ const finished = context.store.finishEvaluation(evaluation.id, {
322
+ conditionOutcome: 'matched',
323
+ triggerDecision: 'triggered',
324
+ finishedAt: '2026-07-05T03:00:01.000Z',
325
+ id: 'attacker-finished-evaluation',
326
+ automationId: 'attacker-automation',
327
+ activationKind: 'once',
328
+ startedAt: '2020-01-01T00:00:00.000Z',
329
+ status: 'running',
330
+ });
331
+ strict_1.default.equal(finished.id, evaluation.id);
332
+ strict_1.default.equal(finished.automationId, automation.id);
333
+ strict_1.default.equal(finished.activationKind, 'cron');
334
+ strict_1.default.equal(finished.startedAt, '2026-07-05T03:00:00.000Z');
335
+ strict_1.default.equal(finished.status, 'finished');
336
+ const run = context.store.createRun(automation.id, evaluation.id, {
337
+ status: 'running',
338
+ id: 'attacker-run',
339
+ automationId: 'attacker-automation',
340
+ evaluationId: 'attacker-evaluation',
341
+ executionMode: 'same-thread',
342
+ createdAt: '2020-01-01T00:00:00.000Z',
343
+ });
344
+ strict_1.default.notEqual(run.id, 'attacker-run');
345
+ strict_1.default.equal(run.automationId, automation.id);
346
+ strict_1.default.equal(run.evaluationId, evaluation.id);
347
+ strict_1.default.equal(run.executionMode, 'side-thread');
348
+ const updatedRun = context.store.updateRun(run.id, {
349
+ status: 'succeeded',
350
+ id: 'attacker-updated-run',
351
+ automationId: 'attacker-automation',
352
+ evaluationId: 'attacker-evaluation',
353
+ executionMode: 'same-thread',
354
+ createdAt: '2020-01-01T00:00:00.000Z',
355
+ });
356
+ strict_1.default.equal(updatedRun.id, run.id);
357
+ strict_1.default.equal(updatedRun.automationId, automation.id);
358
+ strict_1.default.equal(updatedRun.evaluationId, evaluation.id);
359
+ strict_1.default.equal(updatedRun.executionMode, 'side-thread');
360
+ strict_1.default.equal(updatedRun.createdAt, run.createdAt);
361
+ const evaluationRow = context.db.prepare('SELECT automation_id, evaluation_json FROM automation_evaluations WHERE id = ?')
362
+ .get(evaluation.id);
363
+ const evaluationJson = JSON.parse(evaluationRow.evaluation_json);
364
+ strict_1.default.deepEqual({ indexed: evaluationRow.automation_id, jsonId: evaluationJson.id, jsonAutomationId: evaluationJson.automationId }, { indexed: automation.id, jsonId: evaluation.id, jsonAutomationId: automation.id });
365
+ const runRow = context.db.prepare('SELECT automation_id, evaluation_id, run_json FROM automation_runs WHERE id = ?')
366
+ .get(run.id);
367
+ const runJson = JSON.parse(runRow.run_json);
368
+ strict_1.default.deepEqual({
369
+ indexedAutomation: runRow.automation_id,
370
+ indexedEvaluation: runRow.evaluation_id,
371
+ jsonId: runJson.id,
372
+ jsonAutomationId: runJson.automationId,
373
+ jsonEvaluationId: runJson.evaluationId,
374
+ }, {
375
+ indexedAutomation: automation.id,
376
+ indexedEvaluation: evaluation.id,
377
+ jsonId: run.id,
378
+ jsonAutomationId: automation.id,
379
+ jsonEvaluationId: evaluation.id,
380
+ });
381
+ }
382
+ finally {
383
+ context.close();
384
+ }
385
+ });
386
+ (0, node_test_1.default)('prunes old or excess evaluations per automation without deleting runs', () => {
387
+ const context = fixture();
388
+ try {
389
+ const automation = context.store.create(createInput());
390
+ let protectedEvaluationId = '';
391
+ let protectedRunId = '';
392
+ for (let index = 0; index < 1_002; index += 1) {
393
+ const startedAt = new Date(Date.UTC(2026, 6, 4, 0, 0, index)).toISOString();
394
+ const evaluation = context.store.createEvaluation(automation.id, { activationKind: 'cron', startedAt });
395
+ context.store.finishEvaluation(evaluation.id, index === 0
396
+ ? { conditionOutcome: 'matched', triggerDecision: 'triggered', finishedAt: startedAt }
397
+ : { conditionOutcome: 'not_matched', triggerDecision: 'not_rising', finishedAt: startedAt });
398
+ if (index === 0) {
399
+ protectedEvaluationId = evaluation.id;
400
+ protectedRunId = context.store.createRun(automation.id, evaluation.id).id;
401
+ }
402
+ }
403
+ const old = context.store.createEvaluation(automation.id, {
404
+ activationKind: 'cron',
405
+ startedAt: '2026-05-01T00:00:00.000Z',
406
+ });
407
+ const otherAutomation = context.store.create(createInput({ workspaceId: 'workspace-2', title: 'Other automation' }));
408
+ for (const startedAt of ['2026-07-04T00:00:00.000Z', '2026-07-04T00:00:01.000Z']) {
409
+ context.store.createEvaluation(otherAutomation.id, { activationKind: 'cron', startedAt });
410
+ }
411
+ const removed = context.store.pruneEvaluations(new Date('2026-07-05T00:00:00.000Z'));
412
+ strict_1.default.equal(removed, 2);
413
+ strict_1.default.equal(context.store.listEvaluations(automation.id).length, 1_001);
414
+ strict_1.default.ok(context.store.listEvaluations(automation.id).some((entry) => entry.id === protectedEvaluationId));
415
+ strict_1.default.ok(context.store.listRuns(automation.id).some((entry) => entry.id === protectedRunId));
416
+ strict_1.default.ok(!context.store.listEvaluations(automation.id).some((entry) => entry.id === old.id));
417
+ strict_1.default.equal(context.store.listEvaluations(otherAutomation.id).length, 2);
418
+ }
419
+ finally {
420
+ context.close();
421
+ }
422
+ });
423
+ (0, node_test_1.default)('fails legacy import atomically with row context when any legacy row is malformed', () => {
424
+ const context = fixture();
425
+ try {
426
+ const validScheduled = context.facade.createScheduledJob({
427
+ workspaceId: 'workspace-1',
428
+ platform: 'local',
429
+ route: { type: 'local.thread', channelId: 'workspace-1' },
430
+ triggerType: 'cron',
431
+ cronExpr: '0 * * * *',
432
+ promptTemplate: 'Valid legacy job',
433
+ });
434
+ const malformedMonitor = context.facade.createAutomationMonitor({
435
+ workspaceId: 'workspace-1',
436
+ title: 'Malformed legacy monitor',
437
+ sourceType: 'stock.quote',
438
+ condition: { metric: 'price', operator: '>=', value: 200 },
439
+ promptTemplate: 'Do not import partially',
440
+ platform: 'local',
441
+ route: { type: 'local.thread', channelId: 'workspace-1' },
442
+ });
443
+ context.db.prepare('UPDATE automation_monitors SET condition_json = ? WHERE id = ?')
444
+ .run('{malformed', malformedMonitor.id);
445
+ // Migration is deliberately fail-fast and atomic: one malformed row rolls back every valid row in the batch.
446
+ strict_1.default.throws(() => context.store.importLegacyRecords(), new RegExp(`automation monitor ${malformedMonitor.id} condition`, 'i'));
447
+ strict_1.default.equal(context.store.get(validScheduled.id), undefined);
448
+ strict_1.default.equal(context.store.get(malformedMonitor.id), undefined);
449
+ strict_1.default.equal(context.store.list().length, 0);
450
+ }
451
+ finally {
452
+ context.close();
453
+ }
454
+ });
455
+ (0, node_test_1.default)('detects corruption in every duplicated evaluation and run column', () => {
456
+ const context = fixture();
457
+ try {
458
+ const automation = context.store.create(createInput());
459
+ const evaluation = context.store.createEvaluation(automation.id, {
460
+ activationKind: 'cron',
461
+ scriptVersionId: 'version-1',
462
+ startedAt: '2026-07-05T05:00:00.000Z',
463
+ });
464
+ context.store.finishEvaluation(evaluation.id, {
465
+ conditionOutcome: 'matched',
466
+ triggerDecision: 'triggered',
467
+ finishedAt: '2026-07-05T05:00:01.000Z',
468
+ });
469
+ const run = context.store.createRun(automation.id, evaluation.id, { status: 'running' });
470
+ const evaluationCorruptions = [
471
+ ['status', 'running'],
472
+ ['activation_kind', 'once'],
473
+ ['script_version_id', null],
474
+ ['started_at', '2026-07-05T05:00:02.000Z'],
475
+ ['finished_at', null],
476
+ ];
477
+ const originalEvaluationRow = context.db.prepare(`
478
+ SELECT status, activation_kind, script_version_id, started_at, finished_at
479
+ FROM automation_evaluations WHERE id = ?
480
+ `).get(evaluation.id);
481
+ for (const [column, corruptValue] of evaluationCorruptions) {
482
+ context.db.prepare(`UPDATE automation_evaluations SET ${column} = ? WHERE id = ?`).run(corruptValue, evaluation.id);
483
+ strict_1.default.throws(() => context.store.listEvaluations(automation.id), new RegExp(`automation evaluation ${evaluation.id} contains invalid persisted data`, 'i'));
484
+ context.db.prepare(`UPDATE automation_evaluations SET ${column} = ? WHERE id = ?`)
485
+ .run(originalEvaluationRow[column] ?? null, evaluation.id);
486
+ }
487
+ const originalRunRow = context.db.prepare('SELECT status, created_at FROM automation_runs WHERE id = ?')
488
+ .get(run.id);
489
+ for (const [column, corruptValue] of [
490
+ ['status', 'succeeded'],
491
+ ['created_at', '2026-07-05T05:00:02.000Z'],
492
+ ]) {
493
+ context.db.prepare(`UPDATE automation_runs SET ${column} = ? WHERE id = ?`).run(corruptValue, run.id);
494
+ strict_1.default.throws(() => context.store.listRuns(automation.id), new RegExp(`automation run ${run.id} contains invalid persisted data`, 'i'));
495
+ context.db.prepare(`UPDATE automation_runs SET ${column} = ? WHERE id = ?`)
496
+ .run(originalRunRow[column], run.id);
497
+ }
498
+ }
499
+ finally {
500
+ context.close();
501
+ }
502
+ });
503
+ (0, node_test_1.default)('enforces automation foreign keys and cascades child records on deletion', () => {
504
+ const context = fixture();
505
+ try {
506
+ const automation = context.store.create(createInput());
507
+ const evaluation = context.store.createEvaluation(automation.id, {
508
+ activationKind: 'cron',
509
+ startedAt: '2026-07-05T06:00:00.000Z',
510
+ });
511
+ context.store.finishEvaluation(evaluation.id, {
512
+ conditionOutcome: 'matched',
513
+ triggerDecision: 'triggered',
514
+ finishedAt: '2026-07-05T06:00:01.000Z',
515
+ });
516
+ context.store.createRun(automation.id, evaluation.id);
517
+ context.store.delete(automation.id);
518
+ strict_1.default.equal(context.db.prepare('SELECT COUNT(*) AS count FROM automation_evaluations').get().count, 0);
519
+ strict_1.default.equal(context.db.prepare('SELECT COUNT(*) AS count FROM automation_runs').get().count, 0);
520
+ strict_1.default.deepEqual(context.db.prepare('PRAGMA foreign_key_check').all(), []);
521
+ }
522
+ finally {
523
+ context.close();
524
+ }
525
+ });
526
+ (0, node_test_1.default)('imports legacy scheduled jobs and monitors once while preserving IDs and origins', () => {
527
+ const context = fixture();
528
+ try {
529
+ const scheduled = context.facade.createScheduledJob({
530
+ workspaceId: 'workspace-1',
531
+ platform: 'local',
532
+ route: { type: 'local.thread', channelId: 'workspace-1' },
533
+ executionMode: 'same-thread',
534
+ triggerType: 'cron',
535
+ cronExpr: '0 * * * *',
536
+ promptTemplate: 'Run hourly summary',
537
+ description: 'Hourly summary',
538
+ });
539
+ const monitor = context.facade.createAutomationMonitor({
540
+ workspaceId: 'workspace-1',
541
+ title: 'Price alert',
542
+ sourceType: 'stock.quote',
543
+ sourceConfig: { symbol: 'AAPL' },
544
+ condition: { metric: 'price', operator: '>=', value: 200 },
545
+ promptTemplate: 'Explain the price move',
546
+ platform: 'local',
547
+ route: { type: 'local.thread', channelId: 'workspace-1' },
548
+ executionMode: 'side-thread',
549
+ });
550
+ const legacyState = {
551
+ lastEventId: 'event-42',
552
+ lastEventAt: '2026-07-05T04:00:00.000Z',
553
+ latestPrice: 201,
554
+ previousPrice: 198,
555
+ payload: { symbol: 'AAPL', exchange: 'NASDAQ' },
556
+ };
557
+ context.facade.updateAutomationMonitorState(monitor.id, { lastState: legacyState });
558
+ strict_1.default.deepEqual(context.store.importLegacyRecords(), { scheduled: 1, monitors: 1 });
559
+ strict_1.default.deepEqual(context.store.importLegacyRecords(), { scheduled: 0, monitors: 0 });
560
+ strict_1.default.equal(context.store.get(scheduled.id)?.originKind, 'scheduled-job');
561
+ strict_1.default.deepEqual(context.store.get(scheduled.id)?.legacyMetadata, { scheduledDescription: 'Hourly summary' });
562
+ strict_1.default.equal(context.store.get(scheduled.id)?.activation.kind, 'cron');
563
+ strict_1.default.equal(context.store.get(monitor.id)?.originKind, 'automation-monitor');
564
+ strict_1.default.equal(context.store.get(monitor.id)?.activation.kind, 'provider-event');
565
+ strict_1.default.equal(context.store.get(monitor.id)?.condition.kind, 'expression');
566
+ const importedEvaluations = context.store.listEvaluations(monitor.id);
567
+ strict_1.default.equal(importedEvaluations.length, 1);
568
+ strict_1.default.equal(importedEvaluations[0]?.status, 'finished');
569
+ strict_1.default.equal(importedEvaluations[0]?.conditionOutcome, 'skipped');
570
+ strict_1.default.equal(importedEvaluations[0]?.triggerDecision, 'not_evaluated');
571
+ strict_1.default.deepEqual(importedEvaluations[0]?.nextState, legacyState);
572
+ context.store.pruneEvaluations(new Date('2027-07-05T00:00:00.000Z'));
573
+ const retainedStateEvaluation = context.store.listEvaluations(monitor.id)[0];
574
+ strict_1.default.equal(retainedStateEvaluation?.status, 'finished');
575
+ if (retainedStateEvaluation?.status === 'finished') {
576
+ strict_1.default.deepEqual(retainedStateEvaluation.nextState, legacyState);
577
+ }
578
+ }
579
+ finally {
580
+ context.close();
581
+ }
582
+ });
583
+ (0, node_test_1.default)('rejects malformed persisted JSON with row context', () => {
584
+ const context = fixture();
585
+ try {
586
+ const automation = context.store.create(createInput());
587
+ context.db.prepare('UPDATE automations SET condition_json = ? WHERE id = ?').run('{oops', automation.id);
588
+ strict_1.default.throws(() => context.store.get(automation.id), new RegExp(`automation ${automation.id} condition`, 'i'));
589
+ }
590
+ finally {
591
+ context.close();
592
+ }
593
+ });