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
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+
3
+ // F-065: the stock `generic-http` seed ships
4
+ // `api_token: { source: 'value', value: '' }`. An absent or explicitly-empty
5
+ // credential must mean "call without auth" — the adapter decides whether a
6
+ // key is required. A configured-but-broken env_var entry still throws.
7
+
8
+ vi.mock('../../functions/_shared/middleware.ts', () => ({
9
+ createHandler: (fn: any) => fn,
10
+ }))
11
+
12
+ vi.mock('../../functions/_shared/db.ts', () => ({
13
+ adminDb: {
14
+ from: vi.fn(),
15
+ },
16
+ getUserDb: vi.fn(() => ({})),
17
+ }))
18
+
19
+ vi.mock('../../functions/_shared/audit.ts', () => ({
20
+ emitAudit: vi.fn().mockResolvedValue(undefined),
21
+ emitLog: vi.fn().mockResolvedValue(undefined),
22
+ emitSpan: vi.fn().mockResolvedValue(undefined),
23
+ emitActivity: vi.fn().mockResolvedValue(undefined),
24
+ }))
25
+
26
+ // admin-data pulls in permissions/trigger-engine — only `get`/`update` are
27
+ // used here, and only when record_id / update_record are set.
28
+ vi.mock('../../functions/admin-data.ts', () => ({
29
+ get: vi.fn(),
30
+ update: vi.fn(),
31
+ }))
32
+
33
+ import { adminDb } from '../../functions/_shared/db.ts'
34
+ import { emitLog } from '../../functions/_shared/audit.ts'
35
+ import { execute } from '../../functions/integration-outbound.ts'
36
+
37
+ // Chainable query-builder stub — every filter/terminal method returns the
38
+ // builder except single/maybeSingle/await, which resolve to the result.
39
+ function q(result: { data?: any; error?: any }) {
40
+ const b: any = {}
41
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
42
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte', 'lte', 'range']
43
+ for (const m of chainers) b[m] = vi.fn(() => b)
44
+ b.single = vi.fn(async () => result)
45
+ b.maybeSingle = vi.fn(async () => result)
46
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
47
+ return b
48
+ }
49
+
50
+ let queue: any[] = []
51
+
52
+ const fetchMock = vi.fn()
53
+
54
+ function makeCtx(overrides: any = {}) {
55
+ return {
56
+ requestId: 'req-1',
57
+ principal: {
58
+ id: 'user-1',
59
+ type: 'human',
60
+ accountId: 'acct-1',
61
+ roles: ['system_admin'],
62
+ },
63
+ accountId: 'acct-1',
64
+ db: adminDb,
65
+ query: {},
66
+ ...overrides,
67
+ }
68
+ }
69
+
70
+ function queueIntegration(credentials: any, config: any = { outbound_adapter: 'adapters/generic-webhook-outbound' }) {
71
+ // execute() looks up the account-scoped row first, then the system row.
72
+ queue.push(q({ data: null, error: null })) // tenant miss
73
+ queue.push(q({ data: { id: 'int-1', slug: 'generic-http', credentials, config, is_active: true }, error: null })) // system hit
74
+ queue.push(q({ data: { id: 'del-1' }, error: null })) // webhook_deliveries insert
75
+ queue.push(q({ data: null, error: null })) // webhook_deliveries update
76
+ }
77
+
78
+ beforeEach(() => {
79
+ queue = []
80
+ vi.clearAllMocks()
81
+ vi.mocked(adminDb.from).mockImplementation(() => (queue.length ? queue.shift() : q({ data: null, error: null })))
82
+ fetchMock.mockResolvedValue({
83
+ ok: true,
84
+ status: 200,
85
+ text: async () => '{"ok":true}',
86
+ })
87
+ vi.stubGlobal('fetch', fetchMock)
88
+ })
89
+
90
+ afterEach(() => {
91
+ vi.unstubAllGlobals()
92
+ })
93
+
94
+ describe('integration-outbound optional credentials (F-065)', () => {
95
+ it('sends the request with no Authorization header when api_token is an empty literal', async () => {
96
+ queueIntegration({ api_token: { source: 'value', value: '' } })
97
+
98
+ const result = await execute(makeCtx(), {
99
+ integration_slug: 'generic-http',
100
+ action_config: { url: 'https://hooks.example.com/in' },
101
+ })
102
+
103
+ expect(result.success).toBe(true)
104
+ expect(fetchMock).toHaveBeenCalledOnce()
105
+ const headers = fetchMock.mock.calls[0][1].headers
106
+ expect(headers.Authorization).toBeUndefined()
107
+
108
+ // The skip is still audited — same event name, marked skipped.
109
+ const events = vi.mocked(emitLog).mock.calls.map(c => c[1])
110
+ expect(events).toContain('integration.credential_empty')
111
+ const emptyCall = vi.mocked(emitLog).mock.calls.find(c => c[1] === 'integration.credential_empty')!
112
+ expect(emptyCall[4]).toMatchObject({ skipped: true, field: 'api_token' })
113
+ })
114
+
115
+ it('proceeds unauthenticated when the credentials map has no entry for the field', async () => {
116
+ queueIntegration({})
117
+
118
+ const result = await execute(makeCtx(), {
119
+ integration_slug: 'generic-http',
120
+ action_config: { url: 'https://hooks.example.com/in' },
121
+ })
122
+
123
+ expect(result.success).toBe(true)
124
+ const headers = fetchMock.mock.calls[0][1].headers
125
+ expect(headers.Authorization).toBeUndefined()
126
+ const events = vi.mocked(emitLog).mock.calls.map(c => c[1])
127
+ expect(events).toContain('integration.credential_missing')
128
+ })
129
+
130
+ it('attaches Authorization: Bearer when a literal token is configured', async () => {
131
+ queueIntegration({ api_token: { source: 'value', value: 'tok-123' } })
132
+
133
+ await execute(makeCtx(), {
134
+ integration_slug: 'generic-http',
135
+ action_config: { url: 'https://hooks.example.com/in' },
136
+ })
137
+
138
+ const headers = fetchMock.mock.calls[0][1].headers
139
+ expect(headers.Authorization).toBe('Bearer tok-123')
140
+ })
141
+
142
+ it('still throws when an env_var credential names an unset variable', async () => {
143
+ queueIntegration({ api_token: { source: 'env_var', value: 'DEFINITELY_UNSET_ENV_VAR' } })
144
+
145
+ await expect(
146
+ execute(makeCtx(), {
147
+ integration_slug: 'generic-http',
148
+ action_config: { url: 'https://hooks.example.com/in' },
149
+ })
150
+ ).rejects.toThrow('Environment variable DEFINITELY_UNSET_ENV_VAR for api_token is not set')
151
+ expect(fetchMock).not.toHaveBeenCalled()
152
+ const events = vi.mocked(emitLog).mock.calls.map(c => c[1])
153
+ expect(events).toContain('integration.credential_env_missing')
154
+ })
155
+
156
+ it('openai adapter still fails when no credential resolved', async () => {
157
+ queueIntegration({}, { outbound_adapter: 'adapters/openai-outbound' })
158
+
159
+ await expect(
160
+ execute(makeCtx(), {
161
+ integration_slug: 'openai',
162
+ action_config: { endpoint: '/chat/completions', body: {} },
163
+ })
164
+ ).rejects.toThrow('OpenAI adapter requires api_token credential')
165
+ expect(fetchMock).not.toHaveBeenCalled()
166
+ })
167
+ })
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ vi.mock('../../functions/_shared/db.ts', () => ({
4
+ adminDb: {
5
+ from: vi.fn(),
6
+ },
7
+ getUserDb: vi.fn(() => ({})),
8
+ joins: {
9
+ app: 'app:apps(id, slug, name)',
10
+ createdBy: 'created_by:people(id, full_name)',
11
+ },
12
+ }))
13
+
14
+ vi.mock('../../functions/_shared/audit.ts', () => ({
15
+ emitAudit: vi.fn().mockResolvedValue(undefined),
16
+ emitLog: vi.fn().mockResolvedValue(undefined),
17
+ emitSpan: vi.fn().mockResolvedValue(undefined),
18
+ emitActivity: vi.fn().mockResolvedValue(undefined),
19
+ }))
20
+
21
+ import { adminDb } from '../../functions/_shared/db.ts'
22
+ import { emitLog } from '../../functions/_shared/audit.ts'
23
+ import { update, create } from '../../functions/integrations.ts'
24
+
25
+ // Chainable query-builder stub — every filter/terminal method returns the
26
+ // builder except single/maybeSingle/await, which resolve to the result.
27
+ function q(result: { data?: any; error?: any }) {
28
+ const b: any = {}
29
+ const chainers = ['select', 'eq', 'in', 'is', 'ilike', 'filter', 'not', 'order',
30
+ 'limit', 'update', 'insert', 'upsert', 'delete', 'neq', 'or', 'contains', 'gte', 'lte', 'range']
31
+ for (const m of chainers) b[m] = vi.fn(() => b)
32
+ b.single = vi.fn(async () => result)
33
+ b.maybeSingle = vi.fn(async () => result)
34
+ b.then = (onF: any, onR: any) => Promise.resolve(result).then(onF, onR)
35
+ return b
36
+ }
37
+
38
+ let queue: any[] = []
39
+ let built: any[] = []
40
+
41
+ beforeEach(() => {
42
+ queue = []
43
+ built = []
44
+ vi.clearAllMocks()
45
+ vi.mocked(adminDb.from).mockImplementation(() => {
46
+ const b = queue.length ? queue.shift() : q({ data: null, error: null })
47
+ built.push(b)
48
+ return b
49
+ })
50
+ })
51
+
52
+ // createHandler-wrapped exports invoke the inner handler directly when the
53
+ // first arg carries requestId + principal (the ctx shortcut used by handler()).
54
+ function makeCtx(overrides: any = {}) {
55
+ return {
56
+ requestId: 'req-1',
57
+ principal: {
58
+ id: 'user-1',
59
+ type: 'human',
60
+ accountId: 'acct-1',
61
+ roles: ['system_admin'],
62
+ },
63
+ accountId: 'acct-1',
64
+ db: adminDb,
65
+ query: {},
66
+ ...overrides,
67
+ }
68
+ }
69
+
70
+ describe('integrations update audit redaction (S-005)', () => {
71
+ it('redacts credentials in the audit after payload while writing them to the DB', async () => {
72
+ const integration = { id: 'int-1', name: 'OpenAI' }
73
+ const builder = q({ data: integration, error: null })
74
+ queue.push(builder)
75
+
76
+ const credentials = {
77
+ api_key: { source: 'value', value: 'sk-super-secret' },
78
+ }
79
+
80
+ const result = await update(makeCtx(), {
81
+ id: 'int-1',
82
+ name: 'OpenAI v2',
83
+ credentials,
84
+ })
85
+
86
+ expect(result).toEqual(integration)
87
+
88
+ // The DB write still receives the real credentials.
89
+ const written = builder.update.mock.calls[0][0]
90
+ expect(written.credentials).toEqual(credentials)
91
+ expect(written.is_configured).toBe(true)
92
+ expect(written.name).toBe('OpenAI v2')
93
+
94
+ // The audit payload carries only a redaction marker.
95
+ expect(emitLog).toHaveBeenCalledOnce()
96
+ const [, eventType, target, changes] = vi.mocked(emitLog).mock.calls[0]
97
+ expect(eventType).toBe('integration.updated')
98
+ expect(target).toEqual({ type: 'integration', id: 'int-1' })
99
+ expect(changes.after.credentials).toEqual({ __redacted: true })
100
+ expect(JSON.stringify(changes.after)).not.toContain('sk-super-secret')
101
+ expect(changes.after.name).toBe('OpenAI v2')
102
+ })
103
+
104
+ it('emits no credentials key in the audit payload when credentials are not updated', async () => {
105
+ const builder = q({ data: { id: 'int-2' }, error: null })
106
+ queue.push(builder)
107
+
108
+ await update(makeCtx(), { id: 'int-2', description: 'new desc' })
109
+
110
+ const [, , , changes] = vi.mocked(emitLog).mock.calls[0]
111
+ expect(changes.after).not.toHaveProperty('credentials')
112
+ expect(changes.after.description).toBe('new desc')
113
+ })
114
+
115
+ it('does not leak credentials in the create audit payload', async () => {
116
+ const builder = q({ data: { id: 'int-3' }, error: null })
117
+ queue.push(builder)
118
+
119
+ await create(makeCtx(), {
120
+ name: 'Resend',
121
+ integration_type: 'api',
122
+ provider: 'resend',
123
+ credentials: { api_key: { source: 'value', value: 're_secret' } },
124
+ })
125
+
126
+ const [, , , changes] = vi.mocked(emitLog).mock.calls[0]
127
+ expect(changes.after).not.toHaveProperty('credentials')
128
+ expect(JSON.stringify(changes.after)).not.toContain('re_secret')
129
+ })
130
+ })
@@ -93,6 +93,13 @@ function resolveQuery(q: QueryLog, opts: any): any {
93
93
  if (q.table === 'items') {
94
94
  if (has('update')) return { data: null, error: null }
95
95
  if (has('or')) return { data: opts.staleItems ?? [], error: null }
96
+ // Per-id point reads: a superseded doc has already been archived by
97
+ // the time kbRevoke's execution-time status fence re-reads it.
98
+ const idEq = q.ops.find(([n, a]) => n === 'eq' && a?.[0] === 'id')?.[1]?.[1]
99
+ if (idEq && idEq !== ITEM_ID) {
100
+ const stale = (opts.staleItems ?? []).find((s: any) => s.id === idEq)
101
+ if (stale) return { data: { ...stale, status: 'archived' }, error: null }
102
+ }
96
103
  return { data: opts.record ?? null, error: null }
97
104
  }
98
105
  if (q.table === 'types') {
@@ -273,6 +280,18 @@ describe('kbRevoke — lease clearing', () => {
273
280
  expect(leaseDelete.ops.find(([n]) => n === 'eq')?.[1]).toEqual(['document_id', ITEM_ID])
274
281
  })
275
282
 
283
+ it('skips revocation when the document is currently published — stale queued work fence', async () => {
284
+ const queries = setupDb({ record: makeRecord({ status: 'published' }) })
285
+ const rpcCalls = setupRpc({})
286
+
287
+ const res = await kbRevoke(ctx, { id: ITEM_ID })
288
+
289
+ expect(res).toMatchObject({ ok: true, skipped: true, count: 0 })
290
+ expect(rpcNamed(rpcCalls, 'revoke_embeddings_for_item')).toHaveLength(0)
291
+ expect(queriesWithOp(queries, 'embeddings', 'delete')).toHaveLength(0)
292
+ expect(queriesWithOp(queries, 'document_leases', 'delete')).toHaveLength(0)
293
+ })
294
+
276
295
  it('non-published ingest routes through kbRevoke and clears the lease', async () => {
277
296
  const queries = setupDb({ record: makeRecord({ status: 'draft' }) })
278
297
  setupRpc({})
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ const { mockFrom, mockMaybeSingle } = vi.hoisted(() => ({
4
+ mockFrom: vi.fn(),
5
+ mockMaybeSingle: vi.fn(),
6
+ }))
7
+
8
+ vi.mock('../../functions/_shared/db.ts', () => ({
9
+ adminDb: { from: mockFrom },
10
+ getUserDb: vi.fn(),
11
+ }))
12
+
13
+ vi.mock('../../functions/_shared/middleware.ts', () => ({}))
14
+
15
+ vi.mock('../../functions/_shared/principal.ts', () => ({
16
+ formatPrincipalForAudit: vi.fn(() => null),
17
+ }))
18
+
19
+ const APP_UUID = '5f2c1e9a-3b7d-4a8e-9c6f-1d2e3f4a5b6c'
20
+
21
+ async function loadLogging() {
22
+ vi.resetModules()
23
+ return await import('../../functions/_shared/logging.ts')
24
+ }
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks()
28
+ mockFrom.mockReturnValue({
29
+ select: vi.fn(() => ({
30
+ eq: vi.fn(() => ({ maybeSingle: mockMaybeSingle })),
31
+ })),
32
+ })
33
+ })
34
+
35
+ describe('resolveAppUuid', () => {
36
+ it('returns a uuid input unchanged without a db call', async () => {
37
+ const { resolveAppUuid } = await loadLogging()
38
+ await expect(resolveAppUuid(APP_UUID)).resolves.toBe(APP_UUID)
39
+ expect(mockFrom).not.toHaveBeenCalled()
40
+ })
41
+
42
+ it('resolves a slug via the apps lookup and caches the hit', async () => {
43
+ const { resolveAppUuid } = await loadLogging()
44
+ mockMaybeSingle.mockResolvedValue({ data: { id: APP_UUID }, error: null })
45
+ await expect(resolveAppUuid('demo')).resolves.toBe(APP_UUID)
46
+ await expect(resolveAppUuid('demo')).resolves.toBe(APP_UUID)
47
+ expect(mockFrom).toHaveBeenCalledTimes(1)
48
+ expect(mockFrom).toHaveBeenCalledWith('apps')
49
+ })
50
+
51
+ it('returns null for an unknown slug and caches the miss', async () => {
52
+ const { resolveAppUuid } = await loadLogging()
53
+ mockMaybeSingle.mockResolvedValue({ data: null, error: null })
54
+ await expect(resolveAppUuid('missing-app')).resolves.toBeNull()
55
+ await expect(resolveAppUuid('missing-app')).resolves.toBeNull()
56
+ expect(mockFrom).toHaveBeenCalledTimes(1)
57
+ })
58
+
59
+ it('returns null on lookup error and retries instead of caching', async () => {
60
+ const { resolveAppUuid } = await loadLogging()
61
+ mockMaybeSingle.mockResolvedValue({ data: null, error: { message: 'db down' } })
62
+ await expect(resolveAppUuid('demo')).resolves.toBeNull()
63
+ mockMaybeSingle.mockResolvedValue({ data: { id: APP_UUID }, error: null })
64
+ await expect(resolveAppUuid('demo')).resolves.toBe(APP_UUID)
65
+ expect(mockFrom).toHaveBeenCalledTimes(2)
66
+ })
67
+
68
+ it('returns null for null, undefined, and empty input without a db call', async () => {
69
+ const { resolveAppUuid } = await loadLogging()
70
+ await expect(resolveAppUuid(null)).resolves.toBeNull()
71
+ await expect(resolveAppUuid(undefined)).resolves.toBeNull()
72
+ await expect(resolveAppUuid('')).resolves.toBeNull()
73
+ expect(mockFrom).not.toHaveBeenCalled()
74
+ })
75
+ })
@@ -0,0 +1,120 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/pipeline-runner-background.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Unit tests for the pipeline-runner-background dispatch function"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Verify auth gating, payload validation, and correct runPipeline invocation"]
4
+ // CIL[DOC-CONTEXT module="tests/unit/pipeline-runner-background" layer="test-unit" related_files=".framework/functions/pipeline-runner-background.ts,.framework/functions/_shared/pipeline-runner.ts,.framework/functions/_shared/db.ts"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="test assertions" errors="none" side_effects="Mocks pipeline-runner and db modules"]
6
+ // CIL[DOC-MICRO logic_summary="Stubs INTERNAL_INVOKE_SECRET, mocks runPipeline and sweepStaleExecutions, exercises the raw handler function" notable_decisions="[]"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
10
+
11
+ vi.mock('../../functions/_shared/db.ts', () => ({
12
+ adminDb: { from: vi.fn(), rpc: vi.fn() }
13
+ }))
14
+
15
+ vi.mock('../../functions/_shared/pipeline-runner.ts', () => ({
16
+ runPipeline: vi.fn(),
17
+ sweepStaleExecutions: vi.fn().mockResolvedValue(undefined)
18
+ }))
19
+
20
+ vi.mock('../../functions/_shared/audit.ts', () => ({
21
+ emitAudit: vi.fn().mockResolvedValue(undefined),
22
+ emitSpan: vi.fn().mockResolvedValue(undefined),
23
+ emitLog: vi.fn().mockResolvedValue(undefined)
24
+ }))
25
+
26
+ import { handler } from '../../functions/pipeline-runner-background.ts'
27
+ import { runPipeline } from '../../functions/_shared/pipeline-runner.ts'
28
+ // CIL[END stable_id="~/.framework/tests/unit/pipeline-runner-background.test.ts:module_header:module_header"]
29
+
30
+ function makeEvent(body: any, secret = 'test-secret'): any {
31
+ return {
32
+ httpMethod: 'POST',
33
+ path: '/.netlify/functions/pipeline-runner-background',
34
+ headers: { 'x-internal-secret': secret },
35
+ body: JSON.stringify(body),
36
+ queryStringParameters: {}
37
+ }
38
+ }
39
+
40
+ describe('pipeline-runner-background handler', () => {
41
+ beforeEach(() => {
42
+ vi.clearAllMocks()
43
+ vi.stubEnv('INTERNAL_INVOKE_SECRET', 'test-secret')
44
+ vi.mocked(runPipeline).mockResolvedValue({
45
+ executionId: 'exec-1',
46
+ pipelineId: 'pipe-1',
47
+ status: 'completed',
48
+ stages: [],
49
+ durationMs: 5
50
+ })
51
+ })
52
+
53
+ it('returns 403 when the internal secret is wrong', async () => {
54
+ const res = await handler(makeEvent({ pipeline_id: 'pipe-1' }, 'wrong-secret'), {})
55
+ expect(res.statusCode).toBe(403)
56
+ expect(runPipeline).not.toHaveBeenCalled()
57
+ })
58
+
59
+ it('returns 403 when the internal secret is absent', async () => {
60
+ const event = makeEvent({ pipeline_id: 'pipe-1' })
61
+ delete event.headers['x-internal-secret']
62
+ const res = await handler(event, {})
63
+ expect(res.statusCode).toBe(403)
64
+ expect(runPipeline).not.toHaveBeenCalled()
65
+ })
66
+
67
+ it('returns 400 when pipeline_id is missing from the body', async () => {
68
+ const res = await handler(makeEvent({ trigger_data: {}, account_id: 'acct-1' }), {})
69
+ expect(res.statusCode).toBe(400)
70
+ expect(runPipeline).not.toHaveBeenCalled()
71
+ })
72
+
73
+ it('calls runPipeline with the dispatched pipeline_id and execution_id', async () => {
74
+ const payload = {
75
+ pipeline_id: 'pipe-42',
76
+ trigger_data: { event: 'item_created', entity_id: 'item-9' },
77
+ account_id: 'acct-7',
78
+ execution_id: 'exec-queued-99',
79
+ trigger_id: 'trig-3'
80
+ }
81
+ const res = await handler(makeEvent(payload), {})
82
+
83
+ expect(runPipeline).toHaveBeenCalledOnce()
84
+ const [calledPipelineId, calledTriggerData, calledCtx, calledOpts] =
85
+ vi.mocked(runPipeline).mock.calls[0]
86
+ expect(calledPipelineId).toBe('pipe-42')
87
+ expect(calledTriggerData).toMatchObject({ event: 'item_created', entity_id: 'item-9' })
88
+ expect(calledOpts?.executionId).toBe('exec-queued-99')
89
+ expect(calledCtx.accountId).toBe('acct-7')
90
+
91
+ const body = JSON.parse(res.body)
92
+ expect(body.ok).toBe(true)
93
+ expect(body.execution_id).toBe('exec-1')
94
+ expect(body.status).toBe('completed')
95
+ })
96
+
97
+ it('returns ok:false when the pipeline itself reports failure', async () => {
98
+ vi.mocked(runPipeline).mockResolvedValue({
99
+ executionId: 'exec-1',
100
+ pipelineId: 'pipe-1',
101
+ status: 'failed',
102
+ stages: [],
103
+ durationMs: 5,
104
+ error: 'stage exploded'
105
+ })
106
+ const res = await handler(makeEvent({ pipeline_id: 'pipe-1', execution_id: 'exec-1' }), {})
107
+ const body = JSON.parse(res.body)
108
+ expect(body.ok).toBe(false)
109
+ expect(body.status).toBe('failed')
110
+ })
111
+
112
+ it('returns 500 when runPipeline throws', async () => {
113
+ vi.mocked(runPipeline).mockRejectedValue(new Error('Pipeline not found'))
114
+ const res = await handler(makeEvent({ pipeline_id: 'pipe-1', execution_id: 'exec-1' }), {})
115
+ expect(res.statusCode).toBe(500)
116
+ const body = JSON.parse(res.body)
117
+ expect(body.error).toBe('Pipeline not found')
118
+ expect(body.execution_id).toBe('exec-1')
119
+ })
120
+ })