spine-framework 0.4.3 → 0.5.0-rc.4

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 (320) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/agent-runner.ts +1000 -81
  15. package/.framework/functions/_shared/cil.ts +2 -1
  16. package/.framework/functions/_shared/db.ts +3 -6
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/kb.ts +61 -7
  19. package/.framework/functions/_shared/logging.ts +562 -224
  20. package/.framework/functions/_shared/message-sequence.ts +53 -0
  21. package/.framework/functions/_shared/middleware.ts +47 -44
  22. package/.framework/functions/_shared/pipeline-runner.ts +150 -37
  23. package/.framework/functions/_shared/principal.ts +9 -6
  24. package/.framework/functions/_shared/trigger-engine.ts +149 -40
  25. package/.framework/functions/admin-data.ts +48 -31
  26. package/.framework/functions/ai-agents.ts +103 -33
  27. package/.framework/functions/api-keys.ts +301 -122
  28. package/.framework/functions/apps.ts +43 -26
  29. package/.framework/functions/cil-ingest.ts +48 -15
  30. package/.framework/functions/debug-auth.ts +14 -8
  31. package/.framework/functions/deliveries.ts +39 -12
  32. package/.framework/functions/embeddings.ts +145 -76
  33. package/.framework/functions/integration-outbound.ts +67 -27
  34. package/.framework/functions/integrations.ts +254 -127
  35. package/.framework/functions/logs.ts +273 -133
  36. package/.framework/functions/observability.ts +277 -147
  37. package/.framework/functions/pipeline-executions.ts +1 -1
  38. package/.framework/functions/pipeline-runner-background.ts +120 -0
  39. package/.framework/functions/pipelines.ts +14 -9
  40. package/.framework/functions/prompt-configs.ts +7 -4
  41. package/.framework/functions/roles.ts +8 -5
  42. package/.framework/functions/security.ts +30 -17
  43. package/.framework/functions/system-cron.ts +250 -128
  44. package/.framework/functions/tests.ts +7 -6
  45. package/.framework/functions/timers.ts +310 -107
  46. package/.framework/functions/trigger-scheduler.ts +65 -5
  47. package/.framework/functions/triggers.ts +28 -13
  48. package/.framework/functions/types.ts +13 -10
  49. package/.framework/migrations/064_episode_leases.sql +196 -0
  50. package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
  51. package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
  52. package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
  53. package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
  54. package/.framework/migrations/069_log_stats_window.sql +54 -0
  55. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  56. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  57. package/.framework/migrations/072_slug_autofill.sql +77 -0
  58. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  59. package/.framework/schemas/spine.config.schema.json +62 -0
  60. package/.framework/src/App.tsx +3 -1
  61. package/.framework/src/apps/admin/index.tsx +1141 -775
  62. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  63. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  64. package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
  65. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  66. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  67. package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
  68. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  69. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  70. package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
  71. package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
  72. package/.framework/src/components/layout/Layout.tsx +4 -2
  73. package/.framework/src/components/layout/Sidebar.tsx +304 -179
  74. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  75. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  76. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  77. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  78. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  79. package/.framework/src/components/ui/sheet.tsx +24 -26
  80. package/.framework/src/components/ui/textarea.tsx +74 -48
  81. package/.framework/src/hooks/useBranding.ts +47 -0
  82. package/.framework/src/hooks/useEntityList.ts +12 -9
  83. package/.framework/src/hooks/useListSchema.ts +108 -58
  84. package/.framework/src/instructions/README.md +57 -0
  85. package/.framework/src/instructions/ai-agents.md +69 -0
  86. package/.framework/src/instructions/alerts.md +80 -0
  87. package/.framework/src/instructions/api-keys.md +87 -0
  88. package/.framework/src/instructions/apps.md +77 -0
  89. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  90. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  91. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  92. package/.framework/src/instructions/concepts/automation.md +186 -0
  93. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  94. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  95. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  96. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  97. package/.framework/src/instructions/concepts/observability.md +116 -0
  98. package/.framework/src/instructions/concepts/permissions.md +150 -0
  99. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  100. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  101. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  102. package/.framework/src/instructions/deliveries.md +59 -0
  103. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  104. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  105. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  106. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  107. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  108. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  109. package/.framework/src/instructions/integrations.md +96 -0
  110. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  111. package/.framework/src/instructions/invites.md +60 -0
  112. package/.framework/src/instructions/kb.md +80 -0
  113. package/.framework/src/instructions/logs.md +77 -0
  114. package/.framework/src/instructions/observability.md +61 -0
  115. package/.framework/src/instructions/operations/deployment.md +115 -0
  116. package/.framework/src/instructions/operations/migrations.md +125 -0
  117. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  118. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  119. package/.framework/src/instructions/operations/upgrade.md +116 -0
  120. package/.framework/src/instructions/pipeline-executions.md +62 -0
  121. package/.framework/src/instructions/pipelines.md +78 -0
  122. package/.framework/src/instructions/prompts.md +95 -0
  123. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  124. package/.framework/src/instructions/reference/api/auth.md +113 -0
  125. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  126. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  127. package/.framework/src/instructions/reference/cli.md +124 -0
  128. package/.framework/src/instructions/reference/database.md +132 -0
  129. package/.framework/src/instructions/reference/design-schema.md +280 -0
  130. package/.framework/src/instructions/reference/env-vars.md +133 -0
  131. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  132. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  133. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  134. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  135. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  136. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  137. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  138. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  139. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  140. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  141. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  142. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  143. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  144. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  145. package/.framework/src/instructions/reference/functions/index.md +103 -0
  146. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  147. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  148. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  149. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  150. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  151. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  152. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  153. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  154. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  155. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  156. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  157. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  158. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  159. package/.framework/src/instructions/reference/functions/security.md +140 -0
  160. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  161. package/.framework/src/instructions/reference/functions/system.md +106 -0
  162. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  163. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  164. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  165. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  166. package/.framework/src/instructions/reference/functions/types.md +146 -0
  167. package/.framework/src/instructions/reference/seeds.md +126 -0
  168. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  169. package/.framework/src/instructions/reference/spine-config.md +114 -0
  170. package/.framework/src/instructions/roles.md +80 -0
  171. package/.framework/src/instructions/runtime-detail.md +70 -0
  172. package/.framework/src/instructions/runtime-list.md +77 -0
  173. package/.framework/src/instructions/security.md +67 -0
  174. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  175. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  176. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  177. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  178. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  179. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  180. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  181. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  182. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  183. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  184. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  185. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  186. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  187. package/.framework/src/instructions/testing.md +59 -0
  188. package/.framework/src/instructions/timers.md +82 -0
  189. package/.framework/src/instructions/triggers.md +69 -0
  190. package/.framework/src/instructions/types.md +93 -0
  191. package/.framework/src/lib/branding.ts +128 -0
  192. package/.framework/src/pages/NoAppPage.tsx +3 -1
  193. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  194. package/.framework/src/pages/SetupPage.tsx +5 -2
  195. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  196. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  197. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  198. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  199. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  200. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  201. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  202. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  203. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  204. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  205. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  206. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  207. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  208. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  209. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  210. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  211. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  212. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  213. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  214. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  215. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  216. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  217. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  218. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  219. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  220. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  221. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  222. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  223. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  224. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  225. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  226. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  227. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  228. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  229. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  230. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  231. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  232. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  233. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  234. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  235. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  236. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  237. package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
  238. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
  239. package/.framework/src/pages/auth/LoginPage.tsx +4 -2
  240. package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
  241. package/.framework/templates/agent/AGENTS.md +150 -0
  242. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  243. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  244. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  245. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  246. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  247. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  248. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  249. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  250. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  251. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  252. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  253. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  254. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  255. package/.framework/templates/project.json +5 -1
  256. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  257. package/.framework/tests/unit/agent-files.test.ts +355 -0
  258. package/.framework/tests/unit/agent-runner.test.ts +496 -47
  259. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  260. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  261. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  262. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  263. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  264. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  265. package/.framework/tests/unit/episode-lease.test.ts +695 -0
  266. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  267. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  268. package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
  269. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  270. package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
  271. package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
  272. package/.framework/tests/unit/principal-cache.test.ts +15 -0
  273. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  274. package/.framework/tests/unit/system-cron.test.ts +240 -0
  275. package/.framework/tests/unit/timers.test.ts +159 -0
  276. package/.framework/tests/unit/trigger-engine.test.ts +278 -91
  277. package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
  278. package/README.md +125 -166
  279. package/config/tsconfig.cli.json +2 -2
  280. package/netlify.toml +5 -0
  281. package/package.json +15 -4
  282. package/templates/project/package-lock.json +2 -2
  283. package/.framework/docs/admin-routes.md +0 -76
  284. package/.framework/docs/api-docs-progress.md +0 -38
  285. package/.framework/docs/api-governance.md +0 -146
  286. package/.framework/docs/api-testing-results.md +0 -212
  287. package/.framework/docs/assembly-launch-guide.md +0 -138
  288. package/.framework/docs/audit-results.md +0 -590
  289. package/.framework/docs/db-api-inventory.md +0 -95
  290. package/.framework/docs/gap-remediation-backlog.md +0 -103
  291. package/.framework/docs/ui-adoption-verification.md +0 -111
  292. package/.framework/docs/ui-api-coverage.md +0 -84
  293. package/.framework/docs/v2-compatibility-audit.md +0 -228
  294. package/.framework/mcp/index.ts +0 -133
  295. package/.framework/mcp/load-env.ts +0 -60
  296. package/.framework/mcp/shared/app-manifest.ts +0 -275
  297. package/.framework/mcp/shared/db.ts +0 -477
  298. package/.framework/mcp/shared/kb.ts +0 -118
  299. package/.framework/mcp/tools/architect.ts +0 -431
  300. package/.framework/mcp/tools/cil/context.ts +0 -345
  301. package/.framework/mcp/tools/cil/graph.ts +0 -242
  302. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  303. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  304. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  305. package/.framework/mcp/tools/cil/project.ts +0 -791
  306. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  307. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  308. package/.framework/mcp/tools/cil/search.ts +0 -97
  309. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  310. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  311. package/.framework/mcp/tools/kb/delete.ts +0 -139
  312. package/.framework/mcp/tools/kb/index.ts +0 -52
  313. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  314. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  315. package/.framework/mcp/tools/kb/publish.ts +0 -100
  316. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  317. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  318. package/.framework/mcp/tools/kb/search.ts +0 -101
  319. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  320. package/.framework/mcp/tools/observability.ts +0 -510
@@ -30,7 +30,7 @@ vi.mock('../../functions/ai-interactions.ts', () => ({
30
30
 
31
31
  import { adminDb } from '../../functions/_shared/db.ts'
32
32
  import { emitAudit } from '../../functions/_shared/audit.ts'
33
- import { runPipeline } from '../../functions/_shared/pipeline-runner.ts'
33
+ import { runPipeline, sweepStaleExecutions } from '../../functions/_shared/pipeline-runner.ts'
34
34
  // CIL[DOC-VECTOR type="context" content="purpose='Test setup and imports for pipeline runner tests'; No verified external callers; imports: .framework/functions/_shared/middleware.ts,.framework/functions/_shared/db.ts,.framework/functions/_shared/audit.ts"]
35
35
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
36
36
  // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
@@ -276,6 +276,105 @@ describe('runPipeline — lifecycle', () => {
276
276
  expect.objectContaining({ pipeline_id: 'pipe-1' })
277
277
  )
278
278
  })
279
+
280
+ it('claims a pre-created queued execution row when opts.executionId is supplied', async () => {
281
+ mockPipeline([])
282
+ const PRE_CREATED_ID = 'exec-pre-created-1'
283
+ const updateSpy = vi.fn().mockReturnThis()
284
+ const eqSpy = vi.fn().mockReturnThis()
285
+ const claimSelectSpy = vi.fn().mockResolvedValue({ data: [{ id: PRE_CREATED_ID }], error: null })
286
+ const claimChain = { update: updateSpy, eq: eqSpy, select: claimSelectSpy, insert: vi.fn().mockReturnThis() }
287
+ vi.mocked(adminDb.from).mockImplementation((table: string) => {
288
+ if (table === 'pipelines') {
289
+ return {
290
+ select: vi.fn().mockReturnThis(),
291
+ eq: vi.fn().mockReturnThis(),
292
+ single: vi.fn().mockResolvedValue({ data: { id: 'pipe-1', stages: [], is_active: true }, error: null })
293
+ } as any
294
+ }
295
+ if (table === 'pipeline_executions') return claimChain as any
296
+ return {
297
+ select: vi.fn().mockReturnThis(),
298
+ insert: vi.fn().mockReturnThis(),
299
+ update: vi.fn().mockReturnThis(),
300
+ eq: vi.fn().mockReturnThis(),
301
+ single: vi.fn().mockResolvedValue({ data: null, error: null })
302
+ } as any
303
+ })
304
+
305
+ const result = await runPipeline('pipe-1', { x: 1 }, makeCtx(), { executionId: PRE_CREATED_ID })
306
+ expect(result.executionId).toBe(PRE_CREATED_ID)
307
+ expect(result.status).toBe('completed')
308
+ // The claim path must call update, not insert.
309
+ expect(updateSpy).toHaveBeenCalledWith(
310
+ expect.objectContaining({ status: 'running' })
311
+ )
312
+ expect(eqSpy).toHaveBeenCalledWith('id', PRE_CREATED_ID)
313
+ expect(eqSpy).toHaveBeenCalledWith('status', 'queued')
314
+ // and it must NOT have inserted a fresh row
315
+ expect(claimChain.insert).not.toHaveBeenCalled()
316
+ })
317
+
318
+ it('inserts with the same id when the queued row is absent (fallback path)', async () => {
319
+ const PRE_CREATED_ID = 'exec-missing-1'
320
+ const insertSpy = vi.fn().mockReturnThis()
321
+ const singleSpy = vi.fn().mockResolvedValue({ data: { id: PRE_CREATED_ID }, error: null })
322
+ vi.mocked(adminDb.from).mockImplementation((table: string) => {
323
+ if (table === 'pipelines') {
324
+ return {
325
+ select: vi.fn().mockReturnThis(),
326
+ eq: vi.fn().mockReturnThis(),
327
+ single: vi.fn().mockResolvedValue({ data: { id: 'pipe-1', stages: [], is_active: true }, error: null })
328
+ } as any
329
+ }
330
+ if (table === 'pipeline_executions') {
331
+ const chain: any = {
332
+ update: vi.fn().mockReturnThis(),
333
+ eq: vi.fn().mockReturnThis(),
334
+ select: vi.fn().mockResolvedValue({ data: [], error: null }), // claim finds no rows
335
+ insert: insertSpy,
336
+ single: singleSpy
337
+ }
338
+ // insert().select().single() — select must chain to an object with single
339
+ insertSpy.mockImplementation(() => ({
340
+ select: vi.fn().mockImplementation(() => ({ single: singleSpy }))
341
+ }))
342
+ return chain
343
+ }
344
+ return {
345
+ select: vi.fn().mockReturnThis(),
346
+ update: vi.fn().mockReturnThis(),
347
+ eq: vi.fn().mockReturnThis(),
348
+ single: vi.fn().mockResolvedValue({ data: null, error: null })
349
+ } as any
350
+ })
351
+
352
+ const result = await runPipeline('pipe-1', {}, makeCtx(), { executionId: PRE_CREATED_ID })
353
+ expect(result.executionId).toBe(PRE_CREATED_ID)
354
+ expect(insertSpy).toHaveBeenCalledWith(
355
+ expect.objectContaining({ id: PRE_CREATED_ID, status: 'running' })
356
+ )
357
+ })
358
+
359
+ it('throws when claiming a queued execution row fails', async () => {
360
+ vi.mocked(adminDb.from).mockImplementation((table: string) => {
361
+ if (table === 'pipelines') {
362
+ return {
363
+ select: vi.fn().mockReturnThis(),
364
+ eq: vi.fn().mockReturnThis(),
365
+ single: vi.fn().mockResolvedValue({ data: { id: 'pipe-1', stages: [], is_active: true }, error: null })
366
+ } as any
367
+ }
368
+ return {
369
+ update: vi.fn().mockReturnThis(),
370
+ eq: vi.fn().mockReturnThis(),
371
+ select: vi.fn().mockResolvedValue({ data: null, error: { message: 'row not found' } })
372
+ } as any
373
+ })
374
+ await expect(
375
+ runPipeline('pipe-1', {}, makeCtx(), { executionId: 'missing-exec' })
376
+ ).rejects.toThrow('Failed to claim queued execution missing-exec')
377
+ })
279
378
  })
280
379
  // CIL[DOC-VECTOR type="context" content="purpose='Test runPipeline lifecycle and error handling'; No verified external callers; imports: .framework/functions/_shared/middleware.ts,.framework/functions/_shared/db.ts,.framework/functions/_shared/audit.ts"]
281
380
  // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
@@ -920,3 +1019,56 @@ describe('send_email — recipient resolution + outbound send', () => {
920
1019
  expect(result.stages[0].output.skipped).toMatch(/not configured/)
921
1020
  })
922
1021
  })
1022
+
1023
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/pipeline-runner.test.ts:describe_sweepStaleExecutions:block" type="block" identity="describe_sweepStaleExecutions" parent="none"]
1024
+ // CIL[DOC-MACRO purpose="Test stale execution sweeper"]
1025
+ // CIL[DOC-RESPONSIBILITY responsibility="Verify sweepStaleExecutions marks stale rows failed and leaves fresh rows alone"]
1026
+ // CIL[DOC-CONTEXT module="tests/unit/pipeline-runner" layer="test-unit" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/pipeline-runner.ts"]
1027
+ // CIL[DOC-CONTRACT inputs="none" outputs="test assertions" errors="none" side_effects="Tests sweepStaleExecutions"]
1028
+ // CIL[DOC-MICRO logic_summary="Mocks adminDb.from('pipeline_executions').update chain and verifies the correct statuses and cutoff are targeted" notable_decisions="[]"]
1029
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
1030
+ // CIL[DOC-STABILITY stability="stable"]
1031
+ describe('sweepStaleExecutions', () => {
1032
+ beforeEach(() => vi.clearAllMocks())
1033
+
1034
+ it('marks running/pending/queued rows older than the stale window as failed', async () => {
1035
+ const updateSpy = vi.fn().mockReturnThis()
1036
+ const inSpy = vi.fn().mockReturnThis()
1037
+ const ltSpy = vi.fn().mockResolvedValue({ error: null })
1038
+ vi.mocked(adminDb.from).mockReturnValue({
1039
+ update: updateSpy,
1040
+ in: inSpy,
1041
+ lt: ltSpy
1042
+ } as any)
1043
+
1044
+ await sweepStaleExecutions()
1045
+
1046
+ expect(adminDb.from).toHaveBeenCalledWith('pipeline_executions')
1047
+ expect(updateSpy).toHaveBeenCalledWith(
1048
+ expect.objectContaining({ status: 'failed' })
1049
+ )
1050
+ expect(inSpy).toHaveBeenCalledWith('status', ['running', 'pending', 'queued'])
1051
+ // lt cutoff must be ~10 minutes ago
1052
+ const cutoffArg = ltSpy.mock.calls[0][1] as string
1053
+ const cutoffMs = new Date(cutoffArg).getTime()
1054
+ const expectedMs = Date.now() - 10 * 60 * 1000
1055
+ expect(Math.abs(cutoffMs - expectedMs)).toBeLessThan(5000)
1056
+ })
1057
+
1058
+ it('logs a warning but does not throw when the sweep update fails', async () => {
1059
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
1060
+ vi.mocked(adminDb.from).mockReturnValue({
1061
+ update: vi.fn().mockReturnThis(),
1062
+ in: vi.fn().mockReturnThis(),
1063
+ lt: vi.fn().mockResolvedValue({ error: { message: 'DB gone' } })
1064
+ } as any)
1065
+
1066
+ await expect(sweepStaleExecutions()).resolves.toBeUndefined()
1067
+ expect(warnSpy).toHaveBeenCalledWith(
1068
+ expect.stringContaining('pipeline_executions stale sweep failed'),
1069
+ 'DB gone'
1070
+ )
1071
+ warnSpy.mockRestore()
1072
+ })
1073
+ })
1074
+ // CIL[END stable_id="~/.framework/tests/unit/pipeline-runner.test.ts:describe_sweepStaleExecutions:block"]
@@ -135,4 +135,19 @@ describe('resolvePrincipal human cache — mid-registration guard', () => {
135
135
  expect(mockPeopleSingle).toHaveBeenCalledTimes(1)
136
136
  })
137
137
  })
138
+
139
+ describe('internal invoke secret', () => {
140
+ it('resolves a bare x-internal-secret to the system principal', async () => {
141
+ vi.stubEnv('INTERNAL_INVOKE_SECRET', 'test-secret')
142
+ const p = await resolvePrincipal({ headers: { 'x-internal-secret': 'test-secret' } })
143
+ expect(p.id).toBe('system')
144
+ expect(p.machineType).toBe('internal')
145
+ })
146
+
147
+ it('does not trust a wrong secret', async () => {
148
+ vi.stubEnv('INTERNAL_INVOKE_SECRET', 'test-secret')
149
+ const p = await resolvePrincipal({ headers: { 'x-internal-secret': 'wrong' } })
150
+ expect(p.id).toBe('anonymous')
151
+ })
152
+ })
138
153
  // CIL[END stable_id="~/.framework/tests/unit/principal-cache.test.ts:module_header:module_header"]
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+
3
+ // F-051: requireSystemContextWithAudit's ctx overload used to return a
4
+ // Netlify response object on denial; handlers returned it as `result`, so
5
+ // createHandler wrapped it as {data:{statusCode:403}} under HTTP 200. It now
6
+ // throws a statusCode-carrying error which the catch path maps to a real
7
+ // HTTP status. These tests exercise the real createHandler end-to-end for
8
+ // security.ts and tests.ts.
9
+
10
+ const principalRef = vi.hoisted(() => ({ principal: null as any }))
11
+
12
+ vi.mock('../../functions/_shared/principal.ts', () => ({
13
+ resolvePrincipal: vi.fn(async () => principalRef.principal),
14
+ isSystemAdmin: (p: any) =>
15
+ !!p?.roles?.some((r: any) => r === 'system_admin' || r?.slug === 'system_admin'),
16
+ getPrincipalDb: () => ({}),
17
+ }))
18
+
19
+ vi.mock('../../functions/_shared/db.ts', () => ({
20
+ adminDb: {
21
+ from: () => {
22
+ const builder: any = {}
23
+ for (const m of [
24
+ 'select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order', 'limit',
25
+ 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte',
26
+ 'lte', 'lt', 'range', 'textSearch',
27
+ ]) {
28
+ builder[m] = () => builder
29
+ }
30
+ builder.single = () => Promise.resolve({ data: null, error: null })
31
+ builder.maybeSingle = () => Promise.resolve({ data: null, error: null })
32
+ builder.then = (res: any, rej: any) =>
33
+ Promise.resolve({ data: [], error: null }).then(res, rej)
34
+ return builder
35
+ },
36
+ rpc: vi.fn(async () => ({ data: null, error: null })),
37
+ auth: { admin: {} },
38
+ },
39
+ getUserDb: vi.fn(() => ({})),
40
+ }))
41
+
42
+ vi.mock('../../functions/_shared/audit.ts', () => ({
43
+ emitAudit: vi.fn(async () => {}),
44
+ emitSpan: vi.fn(async () => {}),
45
+ emitLog: vi.fn(async () => {}),
46
+ emitActivity: vi.fn(async () => {}),
47
+ }))
48
+
49
+ vi.mock('../../functions/_shared/security.ts', () => ({
50
+ isBlockedRequest: vi.fn(async () => null),
51
+ isBlockedIp: vi.fn(async () => false),
52
+ touchPersonGeo: vi.fn(async () => {}),
53
+ }))
54
+
55
+ vi.mock('../../functions/_shared/geo.ts', () => ({
56
+ getClientGeo: vi.fn(() => ({ ip: '127.0.0.1' })),
57
+ }))
58
+
59
+ import { requireSystemContextWithAudit } from '../../functions/_shared/middleware'
60
+ import { handler as securityHandler } from '../../functions/security'
61
+ import { handler as testsHandler } from '../../functions/tests'
62
+
63
+ const memberPrincipal = {
64
+ id: 'p-member',
65
+ type: 'human',
66
+ accountId: 'acct-1',
67
+ roles: ['member'],
68
+ provenance: { sourceType: 'jwt', createdBy: null, invokedAt: new Date().toISOString() },
69
+ }
70
+
71
+ const adminPrincipal = {
72
+ ...memberPrincipal,
73
+ id: 'p-admin',
74
+ roles: ['system_admin'],
75
+ }
76
+
77
+ const event = (queryStringParameters: Record<string, string> = {}) => ({
78
+ headers: {},
79
+ httpMethod: 'GET',
80
+ path: '/api/x',
81
+ queryStringParameters,
82
+ body: null,
83
+ })
84
+
85
+ describe('requireSystemContextWithAudit ctx overload (F-051)', () => {
86
+ it('throws a statusCode 403 error for a non-admin principal', () => {
87
+ const ctx = { principal: memberPrincipal, accountId: 'acct-1' }
88
+ try {
89
+ requireSystemContextWithAudit(ctx as any)
90
+ expect.unreachable('should have thrown')
91
+ } catch (err: any) {
92
+ expect(err).toBeInstanceOf(Error)
93
+ expect(err.statusCode).toBe(403)
94
+ expect(err.message).toBe('System context required')
95
+ }
96
+ })
97
+
98
+ it('throws a statusCode 401 error for an anonymous principal', () => {
99
+ const ctx = { principal: { id: 'anonymous', type: 'machine' }, accountId: null }
100
+ try {
101
+ requireSystemContextWithAudit(ctx as any)
102
+ expect.unreachable('should have thrown')
103
+ } catch (err: any) {
104
+ expect(err.statusCode).toBe(401)
105
+ expect(err.message).toBe('Authentication required')
106
+ }
107
+ })
108
+
109
+ it('returns null and stamps triggeredBy for a system_admin principal', () => {
110
+ const ctx: any = { principal: { ...adminPrincipal }, accountId: 'acct-1' }
111
+ expect(requireSystemContextWithAudit(ctx, 'suite')).toBeNull()
112
+ expect(ctx.triggeredBy).toBe('suite')
113
+ })
114
+ })
115
+
116
+ describe('denied responses are real HTTP errors (F-051)', () => {
117
+ it('tests.ts returns HTTP 403 — not a 200-wrapped response object', async () => {
118
+ principalRef.principal = memberPrincipal
119
+ const res = await testsHandler(event({ action: 'list' }), {})
120
+ expect(res.statusCode).toBe(403)
121
+ const body = JSON.parse(res.body)
122
+ expect(body.error).toBe('System context required')
123
+ expect(body.data).toBeNull()
124
+ // Regression shape: previously {statusCode:200, body:{data:{statusCode:403}}}
125
+ expect(body.data?.statusCode).toBeUndefined()
126
+ })
127
+
128
+ it('security.ts returns HTTP 403 — not a 200-wrapped response object', async () => {
129
+ principalRef.principal = memberPrincipal
130
+ const res = await securityHandler(event({ action: 'blocklist' }), {})
131
+ expect(res.statusCode).toBe(403)
132
+ const body = JSON.parse(res.body)
133
+ expect(body.error).toBe('System context required')
134
+ expect(body.data).toBeNull()
135
+ })
136
+
137
+ it('tests.ts still serves a system_admin principal with HTTP 200', async () => {
138
+ principalRef.principal = adminPrincipal
139
+ const res = await testsHandler(event({ action: 'list' }), {})
140
+ expect(res.statusCode).toBe(200)
141
+ const body = JSON.parse(res.body)
142
+ expect(body.error).toBeNull()
143
+ expect(body.data.data).toEqual([])
144
+ })
145
+ })
@@ -0,0 +1,240 @@
1
+ // Tests for system-cron's per-tick maintenance blocks (F-052).
2
+ // Regression: when get_due_schedules returns zero rows the handler used to
3
+ // return early, skipping due timers, threshold alerts, delivery retries,
4
+ // and log cleanup. These tests exercise the handler with all shared deps
5
+ // mocked; createHandler is stubbed to identity so a fabricated
6
+ // RequestContext flows straight into the handler body.
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
8
+
9
+ const {
10
+ rpcMock,
11
+ fromMock,
12
+ emitAuditMock,
13
+ runPipelineMock,
14
+ sweepMock,
15
+ retryMock,
16
+ } = vi.hoisted(() => ({
17
+ rpcMock: vi.fn(),
18
+ fromMock: vi.fn(),
19
+ emitAuditMock: vi.fn(),
20
+ runPipelineMock: vi.fn(),
21
+ sweepMock: vi.fn(),
22
+ retryMock: vi.fn(),
23
+ }))
24
+
25
+ vi.mock('../../functions/_shared/middleware.ts', () => ({
26
+ createHandler: (fn: any) => fn,
27
+ }))
28
+
29
+ vi.mock('../../functions/_shared/db.ts', () => ({
30
+ adminDb: { rpc: rpcMock, from: fromMock },
31
+ }))
32
+
33
+ vi.mock('../../functions/_shared/audit.ts', () => ({
34
+ emitAudit: emitAuditMock,
35
+ }))
36
+
37
+ vi.mock('../../functions/_shared/pipeline-runner.ts', () => ({
38
+ runPipeline: runPipelineMock,
39
+ sweepStaleExecutions: sweepMock,
40
+ }))
41
+
42
+ vi.mock('../../functions/_shared/retry-deliveries.ts', () => ({
43
+ retryPendingDeliveries: retryMock,
44
+ }))
45
+
46
+ import { handler } from '../../functions/system-cron.ts'
47
+
48
+ function makeCtx() {
49
+ return {
50
+ requestId: 'test-req',
51
+ principal: {
52
+ id: 'system',
53
+ type: 'machine',
54
+ machineType: 'internal',
55
+ accountId: null,
56
+ scopes: ['*:*'],
57
+ isInternal: true,
58
+ },
59
+ db: {},
60
+ accountId: null,
61
+ appId: null,
62
+ query: {},
63
+ requestPath: '/.netlify/functions/system-cron',
64
+ headers: {},
65
+ }
66
+ }
67
+
68
+ // Per-RPC-name responses; anything not listed resolves to an empty list.
69
+ function rpcImpl(overrides: Record<string, any> = {}) {
70
+ return (name: string) => {
71
+ if (name in overrides) {
72
+ const v = overrides[name]
73
+ return v instanceof Error ? Promise.reject(v) : Promise.resolve(v)
74
+ }
75
+ return Promise.resolve({ data: [], error: null })
76
+ }
77
+ }
78
+
79
+ // Chainable thenable query builder; terminal result is keyed by table name.
80
+ function fromImpl(tableData: Record<string, any> = {}) {
81
+ return (table: string) => {
82
+ const result = table in tableData ? tableData[table] : { data: [], error: null }
83
+ const chain: any = {}
84
+ for (const m of ['select', 'eq', 'neq', 'lt', 'gt', 'or', 'in', 'order', 'limit', 'insert', 'update', 'single', 'maybeSingle']) {
85
+ chain[m] = vi.fn(() => chain)
86
+ }
87
+ chain.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
88
+ return chain
89
+ }
90
+ }
91
+
92
+ const RETRY_SUMMARY = {
93
+ outbound_retried: 0,
94
+ outbound_succeeded: 0,
95
+ outbound_failed: 0,
96
+ inbound_retried: 0,
97
+ inbound_succeeded: 0,
98
+ inbound_failed: 0,
99
+ errors: [],
100
+ }
101
+
102
+ describe('system-cron', () => {
103
+ beforeEach(() => {
104
+ vi.clearAllMocks()
105
+ sweepMock.mockResolvedValue(undefined)
106
+ retryMock.mockResolvedValue(RETRY_SUMMARY)
107
+ })
108
+
109
+ it('runs per-tick maintenance when no schedules are due', async () => {
110
+ rpcMock.mockImplementation(rpcImpl({
111
+ get_due_schedules: { data: [], error: null },
112
+ get_due_timers: { data: [], error: null },
113
+ }))
114
+ fromMock.mockImplementation(fromImpl({
115
+ threshold_alerts: { data: [], error: null },
116
+ }))
117
+
118
+ const res = await handler(makeCtx())
119
+ expect(res.statusCode).toBe(200)
120
+
121
+ // Every per-tick block still ran despite the empty schedule list.
122
+ expect(sweepMock).toHaveBeenCalledOnce()
123
+ expect(rpcMock).toHaveBeenCalledWith('get_due_timers', expect.anything())
124
+ expect(fromMock).toHaveBeenCalledWith('threshold_alerts')
125
+ expect(retryMock).toHaveBeenCalledOnce()
126
+
127
+ const body = JSON.parse(res.body)
128
+ expect(body.schedules_due).toBe(0)
129
+ expect(body.executed).toBe(0)
130
+ expect(body.results).toEqual([])
131
+ expect(body.thresholds_evaluated).toBe(0)
132
+ expect(body.deliveries_retried).toEqual(RETRY_SUMMARY)
133
+ })
134
+
135
+ it('executes due timers on a tick with no due schedules', async () => {
136
+ const timer = {
137
+ timer_id: 't1',
138
+ pipeline_id: 'p1',
139
+ account_id: 'a1',
140
+ name: 'nightly',
141
+ execution_count: 3,
142
+ created_by: 'u1',
143
+ app_id: null,
144
+ }
145
+ rpcMock.mockImplementation(rpcImpl({
146
+ get_due_schedules: { data: [], error: null },
147
+ get_due_timers: { data: [timer], error: null },
148
+ update_timer_after_run: { data: null, error: null },
149
+ }))
150
+ fromMock.mockImplementation(fromImpl({
151
+ threshold_alerts: { data: [], error: null },
152
+ }))
153
+ runPipelineMock.mockResolvedValue({ status: 'completed' })
154
+
155
+ const res = await handler(makeCtx())
156
+ expect(res.statusCode).toBe(200)
157
+
158
+ expect(runPipelineMock).toHaveBeenCalledWith(
159
+ 'p1',
160
+ expect.objectContaining({ timer_id: 't1' }),
161
+ expect.objectContaining({ accountId: 'a1' }),
162
+ )
163
+ expect(rpcMock).toHaveBeenCalledWith('update_timer_after_run',
164
+ expect.objectContaining({ p_timer_id: 't1', p_success: true }))
165
+
166
+ const body = JSON.parse(res.body)
167
+ expect(body.schedules_due).toBe(0)
168
+ expect(body.executed).toBe(1)
169
+ expect(body.success).toBe(1)
170
+ expect(body.results[0].scheduleId).toBe('t1')
171
+ expect(retryMock).toHaveBeenCalledOnce()
172
+ })
173
+
174
+ it('still reaches delivery retries when the timer block throws', async () => {
175
+ rpcMock.mockImplementation(rpcImpl({
176
+ get_due_schedules: { data: [], error: null },
177
+ get_due_timers: new Error('rpc down'),
178
+ }))
179
+ fromMock.mockImplementation(fromImpl({
180
+ threshold_alerts: { data: [], error: null },
181
+ }))
182
+
183
+ const res = await handler(makeCtx())
184
+ expect(res.statusCode).toBe(200)
185
+ expect(retryMock).toHaveBeenCalledOnce()
186
+ })
187
+
188
+ it('still processes due schedules and then runs maintenance', async () => {
189
+ const schedule = {
190
+ schedule_id: 's1',
191
+ action_id: 'act1',
192
+ account_id: 'a1',
193
+ machine_principal_id: 'm1',
194
+ delegated_scopes: ['*:*'],
195
+ config: {},
196
+ }
197
+ const action = {
198
+ id: 'act1',
199
+ handler: 'send_email',
200
+ handler_module: 'functions',
201
+ config: {},
202
+ required_scopes: ['*:*'],
203
+ }
204
+ const machine = {
205
+ id: 'm1',
206
+ account_id: 'a1',
207
+ scopes: ['*:*'],
208
+ machine_type: 'internal',
209
+ is_internal: true,
210
+ created_by: 'u1',
211
+ }
212
+
213
+ rpcMock.mockImplementation(rpcImpl({
214
+ get_due_schedules: { data: [schedule], error: null },
215
+ get_due_timers: { data: [], error: null },
216
+ validate_schedule_creator: { data: { is_valid: true }, error: null },
217
+ update_schedule_after_run: { data: null, error: null },
218
+ }))
219
+ fromMock.mockImplementation(fromImpl({
220
+ actions: { data: action, error: null },
221
+ api_keys: { data: machine, error: null },
222
+ threshold_alerts: { data: [], error: null },
223
+ schedule_executions: { data: null, error: null },
224
+ }))
225
+
226
+ const res = await handler(makeCtx())
227
+ expect(res.statusCode).toBe(200)
228
+
229
+ const body = JSON.parse(res.body)
230
+ expect(body.schedules_due).toBe(1)
231
+ expect(body.executed).toBe(1)
232
+ expect(body.success).toBe(1)
233
+ expect(body.results[0].scheduleId).toBe('s1')
234
+
235
+ // Maintenance blocks still ran after schedule processing.
236
+ expect(rpcMock).toHaveBeenCalledWith('get_due_timers', expect.anything())
237
+ expect(fromMock).toHaveBeenCalledWith('threshold_alerts')
238
+ expect(retryMock).toHaveBeenCalledOnce()
239
+ })
240
+ })