spine-framework 0.3.144 → 0.4.0

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 (451) hide show
  1. package/.env.example +76 -0
  2. package/.framework/.spine-manifest.json +19 -0
  3. package/.framework/cli/commands/agents.ts +61 -33
  4. package/.framework/cli/commands/auth.ts +60 -28
  5. package/.framework/cli/commands/create-app.ts +212 -80
  6. package/.framework/cli/commands/dev.ts +184 -68
  7. package/.framework/cli/commands/doctor.ts +324 -152
  8. package/.framework/cli/commands/generate.ts +119 -68
  9. package/.framework/cli/commands/init.ts +141 -242
  10. package/.framework/cli/commands/install-app.ts +1336 -314
  11. package/.framework/cli/commands/items.ts +31 -29
  12. package/.framework/cli/commands/migrate.ts +299 -156
  13. package/.framework/cli/commands/migrations.ts +62 -30
  14. package/.framework/cli/commands/pipelines.ts +31 -29
  15. package/.framework/cli/commands/status.ts +105 -45
  16. package/.framework/cli/commands/system.ts +47 -42
  17. package/.framework/cli/commands/test.ts +161 -102
  18. package/.framework/cli/commands/upgrade.ts +260 -0
  19. package/.framework/cli/commands/validate-app.ts +517 -161
  20. package/.framework/cli/context.ts +117 -85
  21. package/.framework/cli/env-loader.ts +74 -43
  22. package/.framework/cli/index.ts +70 -46
  23. package/.framework/cli/lib/project-files.ts +404 -0
  24. package/.framework/docs/guides/testing-guide.md +1 -1
  25. package/.framework/docs/runtime-types.md +72 -72
  26. package/.framework/functions/_shared/access.ts +84 -0
  27. package/.framework/functions/_shared/agent-runner.ts +5051 -1026
  28. package/.framework/functions/_shared/app-manifest.ts +214 -94
  29. package/.framework/functions/_shared/ast.ts +214 -0
  30. package/.framework/functions/_shared/audit.ts +91 -55
  31. package/.framework/functions/_shared/cil.ts +368 -127
  32. package/.framework/functions/_shared/db.ts +337 -68
  33. package/.framework/functions/_shared/funnel.ts +228 -0
  34. package/.framework/functions/_shared/geo.ts +105 -0
  35. package/.framework/functions/_shared/hash-api-key.ts +94 -17
  36. package/.framework/functions/_shared/index.ts +22 -224
  37. package/.framework/functions/_shared/kb.ts +832 -58
  38. package/.framework/functions/_shared/llm.ts +118 -18
  39. package/.framework/functions/_shared/logging.ts +317 -19
  40. package/.framework/functions/_shared/middleware.ts +701 -131
  41. package/.framework/functions/_shared/notify.ts +70 -0
  42. package/.framework/functions/_shared/outcomes.ts +34 -19
  43. package/.framework/functions/_shared/permissions.ts +501 -315
  44. package/.framework/functions/_shared/pipeline-runner.ts +704 -155
  45. package/.framework/functions/_shared/principal.ts +436 -263
  46. package/.framework/functions/_shared/rate-limit.ts +233 -0
  47. package/.framework/functions/_shared/resolve-credentials.ts +86 -19
  48. package/.framework/functions/_shared/resolve-ids.ts +203 -139
  49. package/.framework/functions/_shared/retry-deliveries.ts +51 -16
  50. package/.framework/functions/_shared/schema-utils.ts +602 -521
  51. package/.framework/functions/_shared/security.ts +345 -0
  52. package/.framework/functions/_shared/testing.ts +197 -172
  53. package/.framework/functions/_shared/tracing.ts +351 -21
  54. package/.framework/functions/_shared/trigger-engine.ts +425 -136
  55. package/.framework/functions/_shared/webhook-registration.ts +71 -70
  56. package/.framework/functions/_shared/webhook-registry.ts +126 -83
  57. package/.framework/functions/account-nodes.ts +30 -30
  58. package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
  59. package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
  60. package/.framework/functions/adapters/openai-outbound.ts +15 -16
  61. package/.framework/functions/admin-data.ts +596 -173
  62. package/.framework/functions/admin-funnel.ts +64 -60
  63. package/.framework/functions/admin-install-funnel.ts +84 -66
  64. package/.framework/functions/admin-signals.ts +69 -51
  65. package/.framework/functions/ai-agents.ts +431 -106
  66. package/.framework/functions/ai-interactions.ts +186 -112
  67. package/.framework/functions/api-keys.ts +107 -107
  68. package/.framework/functions/apps.ts +154 -136
  69. package/.framework/functions/auth.ts +83 -65
  70. package/.framework/functions/cil-agent-ingest.ts +189 -232
  71. package/.framework/functions/cil-agent.ts +433 -177
  72. package/.framework/functions/cil-callmap.ts +642 -430
  73. package/.framework/functions/cil-config.ts +139 -135
  74. package/.framework/functions/cil-context.ts +214 -127
  75. package/.framework/functions/cil-doc.ts +587 -458
  76. package/.framework/functions/cil-embeddings.ts +453 -364
  77. package/.framework/functions/cil-graph.ts +427 -142
  78. package/.framework/functions/cil-ingest.ts +1418 -1154
  79. package/.framework/functions/cil-pipeline.ts +276 -21
  80. package/.framework/functions/cil-project.ts +659 -132
  81. package/.framework/functions/cil-validate.ts +198 -164
  82. package/.framework/functions/debug-auth.ts +57 -55
  83. package/.framework/functions/deliveries.ts +80 -21
  84. package/.framework/functions/embeddings.ts +1023 -318
  85. package/.framework/functions/eval-harness.ts +146 -108
  86. package/.framework/functions/integration-outbound.ts +205 -75
  87. package/.framework/functions/integration-routes.ts +128 -23
  88. package/.framework/functions/integrations.ts +125 -100
  89. package/.framework/functions/invites.ts +1095 -460
  90. package/.framework/functions/item-progress.ts +158 -102
  91. package/.framework/functions/kb.ts +34 -10
  92. package/.framework/functions/logs.ts +133 -133
  93. package/.framework/functions/observability.ts +152 -122
  94. package/.framework/functions/pipeline-executions.ts +147 -132
  95. package/.framework/functions/pipelines.ts +134 -119
  96. package/.framework/functions/prompt-configs.ts +96 -80
  97. package/.framework/functions/roles.ts +112 -82
  98. package/.framework/functions/security.ts +450 -0
  99. package/.framework/functions/system-cron.ts +117 -80
  100. package/.framework/functions/system.ts +158 -98
  101. package/.framework/functions/tests.ts +29 -29
  102. package/.framework/functions/timers.ts +94 -95
  103. package/.framework/functions/trigger-scheduler.ts +10 -0
  104. package/.framework/functions/triggers.ts +179 -149
  105. package/.framework/functions/types.ts +137 -125
  106. package/.framework/mcp/index.ts +133 -0
  107. package/.framework/mcp/load-env.ts +60 -0
  108. package/.framework/mcp/shared/app-manifest.ts +275 -0
  109. package/.framework/mcp/shared/db.ts +477 -0
  110. package/.framework/mcp/shared/kb.ts +118 -0
  111. package/.framework/mcp/tools/architect.ts +431 -0
  112. package/.framework/mcp/tools/cil/context.ts +345 -0
  113. package/.framework/mcp/tools/cil/graph.ts +242 -0
  114. package/.framework/mcp/tools/cil/ingest.ts +1217 -0
  115. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
  116. package/.framework/mcp/tools/cil/pipeline.ts +767 -0
  117. package/.framework/mcp/tools/cil/project.ts +791 -0
  118. package/.framework/mcp/tools/cil/retrieval.ts +336 -0
  119. package/.framework/mcp/tools/cil/search-core.ts +454 -0
  120. package/.framework/mcp/tools/cil/search.ts +97 -0
  121. package/.framework/mcp/tools/cil/suggest.ts +186 -0
  122. package/.framework/mcp/tools/cil/validate.ts +1369 -0
  123. package/.framework/mcp/tools/kb/delete.ts +139 -0
  124. package/.framework/mcp/tools/kb/index.ts +52 -0
  125. package/.framework/mcp/tools/kb/ingest.ts +84 -0
  126. package/.framework/mcp/tools/kb/list-types.ts +102 -0
  127. package/.framework/mcp/tools/kb/publish.ts +100 -0
  128. package/.framework/mcp/tools/kb/reindex.ts +86 -0
  129. package/.framework/mcp/tools/kb/revoke.ts +85 -0
  130. package/.framework/mcp/tools/kb/search.ts +101 -0
  131. package/.framework/mcp/tools/kb/upsert.ts +125 -0
  132. package/.framework/mcp/tools/observability.ts +510 -0
  133. package/.framework/migrations/001_core_schema.sql +15 -0
  134. package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
  135. package/.framework/migrations/003_core_system_data.sql +16 -1
  136. package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
  137. package/.framework/migrations/005_core_observability.sql +17 -1
  138. package/.framework/migrations/006_security_hardening_rls.sql +15 -0
  139. package/.framework/migrations/007_kb_article_list.sql +17 -1
  140. package/.framework/migrations/008_items_target.sql +15 -0
  141. package/.framework/migrations/009_roles_slug_unique.sql +15 -0
  142. package/.framework/migrations/010_auth_user_trigger.sql +15 -0
  143. package/.framework/migrations/011_spine_system_account.sql +16 -0
  144. package/.framework/migrations/012_service_role_grants.sql +16 -0
  145. package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
  146. package/.framework/migrations/014_apps_required_roles.sql +16 -0
  147. package/.framework/migrations/015_types_kind_slug_unique.sql +20 -0
  148. package/.framework/migrations/016_cil_types.sql +281 -0
  149. package/.framework/migrations/017_cil_stable_ids.sql +37 -0
  150. package/.framework/migrations/018_runtime_tables.sql +129 -0
  151. package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
  152. package/.framework/migrations/020_schema_reconciliation.sql +67 -0
  153. package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
  154. package/.framework/migrations/032_cil_status_migration.sql +73 -0
  155. package/.framework/migrations/033_cil_versioning.sql +73 -0
  156. package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
  157. package/.framework/migrations/035_rate_limits.sql +55 -0
  158. package/.framework/migrations/036_demo_record_flags.sql +72 -0
  159. package/.framework/migrations/037_kb_types_canonical.sql +244 -0
  160. package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
  161. package/.framework/migrations/042_registration_hierarchy.sql +309 -0
  162. package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
  163. package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
  164. package/.framework/migrations/045_security_critical_rls.sql +139 -0
  165. package/.framework/migrations/046_security_missing_rls.sql +79 -0
  166. package/.framework/migrations/047_people_updated_by.sql +27 -0
  167. package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
  168. package/.framework/migrations/049_security_blocklist.sql +148 -0
  169. package/.framework/migrations/050_notifications.sql +49 -0
  170. package/.framework/migrations/051_message_agent_fields.sql +74 -0
  171. package/.framework/migrations/052_registration_security.sql +421 -0
  172. package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
  173. package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
  174. package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
  175. package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
  176. package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
  177. package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
  178. package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
  179. package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
  180. package/.framework/migrations/061_fk_dedup.sql +32 -0
  181. package/.framework/public/spine-logo.svg +10 -3
  182. package/.framework/scripts/app-install-cli.ts +190 -93
  183. package/.framework/scripts/assemble-frontend.sh +127 -44
  184. package/.framework/scripts/assemble-functions.sh +128 -30
  185. package/.framework/scripts/boundary-check.sh +6 -2
  186. package/.framework/scripts/build-manifest.sh +24 -68
  187. package/.framework/scripts/check-core-integrity.sh +15 -76
  188. package/.framework/scripts/checksum-manifest.mts +343 -0
  189. package/.framework/scripts/cil-migrate-markers.ts +403 -0
  190. package/.framework/scripts/fix-cil-markers.mjs +176 -0
  191. package/.framework/scripts/verify-integrity.sh +21 -57
  192. package/.framework/scripts/verify-manifest.mts +305 -0
  193. package/.framework/seeds/types/funnel_signal_use.json +74 -21
  194. package/.framework/seeds/types/spine_instance.json +73 -21
  195. package/.framework/src/App.tsx +118 -72
  196. package/.framework/src/apps/admin/index.tsx +855 -58
  197. package/.framework/src/components/AppWrapper.tsx +43 -45
  198. package/.framework/src/components/CustomAppLoader.tsx +160 -116
  199. package/.framework/src/components/admin/AdminListPage.tsx +46 -44
  200. package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
  201. package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
  202. package/.framework/src/components/admin/PageInstructions.tsx +98 -2
  203. package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
  204. package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
  205. package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
  206. package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
  207. package/.framework/src/components/app-sidebar.tsx +42 -45
  208. package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
  209. package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
  210. package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
  211. package/.framework/src/components/kb/KBArticleList.tsx +251 -10
  212. package/.framework/src/components/layout/AppShell.tsx +51 -45
  213. package/.framework/src/components/layout/Header.tsx +42 -43
  214. package/.framework/src/components/layout/Layout.tsx +46 -46
  215. package/.framework/src/components/layout/Sidebar.tsx +196 -96
  216. package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
  217. package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
  218. package/.framework/src/components/runtime/DataFilters.tsx +49 -74
  219. package/.framework/src/components/runtime/DataHeader.tsx +53 -32
  220. package/.framework/src/components/runtime/DataListPage.tsx +51 -155
  221. package/.framework/src/components/runtime/DataStats.tsx +66 -44
  222. package/.framework/src/components/runtime/DataTable.tsx +49 -149
  223. package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
  224. package/.framework/src/components/runtime/index.ts +119 -16
  225. package/.framework/src/components/search-form.tsx +46 -29
  226. package/.framework/src/components/shared/AgentView.tsx +65 -48
  227. package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
  228. package/.framework/src/components/shared/InviteManager.tsx +207 -97
  229. package/.framework/src/components/shared/MemberList.tsx +97 -0
  230. package/.framework/src/components/shared/SchemaFields.tsx +107 -86
  231. package/.framework/src/components/ui/DataTable.tsx +80 -68
  232. package/.framework/src/components/ui/Form.tsx +157 -146
  233. package/.framework/src/components/ui/ItemCard.tsx +74 -67
  234. package/.framework/src/components/ui/ItemListView.tsx +73 -68
  235. package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
  236. package/.framework/src/components/ui/Modal.tsx +78 -68
  237. package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
  238. package/.framework/src/components/ui/accordion.tsx +85 -86
  239. package/.framework/src/components/ui/alert-dialog.tsx +197 -184
  240. package/.framework/src/components/ui/alert.tsx +105 -94
  241. package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
  242. package/.framework/src/components/ui/avatar.tsx +122 -107
  243. package/.framework/src/components/ui/badge.tsx +60 -55
  244. package/.framework/src/components/ui/breadcrumb.tsx +127 -128
  245. package/.framework/src/components/ui/button-group.tsx +86 -72
  246. package/.framework/src/components/ui/button.tsx +71 -50
  247. package/.framework/src/components/ui/calendar.tsx +72 -57
  248. package/.framework/src/components/ui/card.tsx +136 -129
  249. package/.framework/src/components/ui/carousel.tsx +226 -107
  250. package/.framework/src/components/ui/chart.tsx +252 -147
  251. package/.framework/src/components/ui/checkbox.tsx +43 -44
  252. package/.framework/src/components/ui/collapsible.tsx +86 -71
  253. package/.framework/src/components/ui/combobox.tsx +282 -249
  254. package/.framework/src/components/ui/command.tsx +158 -146
  255. package/.framework/src/components/ui/context-menu.tsx +239 -240
  256. package/.framework/src/components/ui/dialog.tsx +174 -157
  257. package/.framework/src/components/ui/direction.tsx +72 -56
  258. package/.framework/src/components/ui/drawer.tsx +168 -169
  259. package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
  260. package/.framework/src/components/ui/empty.tsx +131 -120
  261. package/.framework/src/components/ui/field.tsx +205 -173
  262. package/.framework/src/components/ui/hover-card.tsx +71 -72
  263. package/.framework/src/components/ui/input-group.tsx +148 -133
  264. package/.framework/src/components/ui/input-otp.tsx +99 -86
  265. package/.framework/src/components/ui/input.tsx +43 -41
  266. package/.framework/src/components/ui/item.tsx +207 -185
  267. package/.framework/src/components/ui/kbd.tsx +57 -56
  268. package/.framework/src/components/ui/label.tsx +45 -41
  269. package/.framework/src/components/ui/menubar.tsx +268 -236
  270. package/.framework/src/components/ui/native-select.tsx +85 -86
  271. package/.framework/src/components/ui/navigation-menu.tsx +166 -156
  272. package/.framework/src/components/ui/pagination.tsx +131 -141
  273. package/.framework/src/components/ui/popover.tsx +125 -128
  274. package/.framework/src/components/ui/progress.tsx +47 -44
  275. package/.framework/src/components/ui/radio-group.tsx +60 -55
  276. package/.framework/src/components/ui/resizable.tsx +85 -72
  277. package/.framework/src/components/ui/scroll-area.tsx +57 -58
  278. package/.framework/src/components/ui/select.tsx +182 -169
  279. package/.framework/src/components/ui/separator.tsx +42 -43
  280. package/.framework/src/components/ui/sheet.tsx +170 -159
  281. package/.framework/src/components/ui/sidebar.tsx +395 -398
  282. package/.framework/src/components/ui/skeleton.tsx +42 -43
  283. package/.framework/src/components/ui/slider.tsx +61 -44
  284. package/.framework/src/components/ui/sonner.tsx +44 -44
  285. package/.framework/src/components/ui/spinner.tsx +47 -42
  286. package/.framework/src/components/ui/switch.tsx +56 -42
  287. package/.framework/src/components/ui/table-primitives.tsx +272 -132
  288. package/.framework/src/components/ui/table.tsx +140 -142
  289. package/.framework/src/components/ui/tabs.tsx +110 -93
  290. package/.framework/src/components/ui/textarea.tsx +43 -44
  291. package/.framework/src/components/ui/toggle-group.tsx +87 -71
  292. package/.framework/src/components/ui/toggle.tsx +56 -56
  293. package/.framework/src/components/ui/tooltip.tsx +99 -85
  294. package/.framework/src/contexts/AppContext.tsx +166 -114
  295. package/.framework/src/contexts/AuthContext.tsx +232 -137
  296. package/.framework/src/hooks/use-mobile.ts +43 -44
  297. package/.framework/src/hooks/useApi.ts +185 -172
  298. package/.framework/src/hooks/useAppPath.ts +31 -29
  299. package/.framework/src/hooks/useApps.ts +77 -42
  300. package/.framework/src/hooks/useEntityList.ts +66 -29
  301. package/.framework/src/hooks/useEntityRecord.ts +76 -32
  302. package/.framework/src/hooks/useForm.ts +62 -30
  303. package/.framework/src/hooks/useListSchema.ts +60 -43
  304. package/.framework/src/hooks/useSchemaRecord.ts +115 -95
  305. package/.framework/src/lib/api.ts +258 -134
  306. package/.framework/src/lib/registration.ts +181 -0
  307. package/.framework/src/lib/supabase.ts +118 -81
  308. package/.framework/src/lib/utils.ts +266 -266
  309. package/.framework/src/main.tsx +43 -44
  310. package/.framework/src/pages/DashboardPage.tsx +44 -43
  311. package/.framework/src/pages/NoAppPage.tsx +50 -0
  312. package/.framework/src/pages/NotFoundPage.tsx +32 -29
  313. package/.framework/src/pages/SetupPage.tsx +34 -33
  314. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
  315. package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
  316. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
  317. package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
  318. package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
  319. package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
  320. package/.framework/src/pages/admin/AppsPage.tsx +45 -42
  321. package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
  322. package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
  323. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
  324. package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
  325. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
  326. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
  327. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
  328. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
  329. package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
  330. package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
  331. package/.framework/src/pages/admin/LogsPage.tsx +87 -137
  332. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
  333. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
  334. package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
  335. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
  336. package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
  337. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
  338. package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
  339. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
  340. package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
  341. package/.framework/src/pages/admin/RolesPage.tsx +47 -164
  342. package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
  343. package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
  344. package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
  345. package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
  346. package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
  347. package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
  348. package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
  349. package/.framework/src/pages/admin/TimersPage.tsx +44 -42
  350. package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
  351. package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
  352. package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
  353. package/.framework/src/pages/admin/TypesPage.tsx +45 -42
  354. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
  355. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
  356. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
  357. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
  358. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
  359. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
  360. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
  361. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
  362. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
  363. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
  364. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
  365. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
  366. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  367. package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
  368. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
  369. package/.framework/src/pages/auth/LoginPage.tsx +38 -32
  370. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
  371. package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
  372. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
  373. package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
  374. package/.framework/src/types/auth.ts +101 -93
  375. package/.framework/src/types/types.ts +283 -262
  376. package/.framework/templates/project.json +87 -0
  377. package/.framework/templates/spine.config.neutral.json +7 -0
  378. package/.framework/tests/api/api-surface.test.ts +381 -0
  379. package/.framework/tests/fixtures/seed.ts +427 -0
  380. package/.framework/tests/fixtures/teardown.ts +228 -0
  381. package/.framework/tests/reporter.ts +351 -0
  382. package/.framework/tests/setup.ts +135 -0
  383. package/.framework/tests/ui/ui-sweep.ts +278 -0
  384. package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
  385. package/.framework/tests/unit/agent-runner.test.ts +2389 -0
  386. package/.framework/tests/unit/ai-interactions.test.ts +81 -0
  387. package/.framework/tests/unit/app-manifest.test.ts +359 -0
  388. package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
  389. package/.framework/tests/unit/cil-determinism.test.ts +85 -0
  390. package/.framework/tests/unit/core-isolation.test.ts +153 -0
  391. package/.framework/tests/unit/funnel-signals.test.ts +135 -0
  392. package/.framework/tests/unit/install-app.test.ts +1759 -0
  393. package/.framework/tests/unit/invites-security.test.ts +572 -0
  394. package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
  395. package/.framework/tests/unit/kb-search.test.ts +119 -0
  396. package/.framework/tests/unit/migrate.test.ts +509 -0
  397. package/.framework/tests/unit/notify.test.ts +136 -0
  398. package/.framework/tests/unit/permissions.test.ts +269 -0
  399. package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
  400. package/.framework/tests/unit/principal-cache.test.ts +138 -0
  401. package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
  402. package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
  403. package/.framework/tests/unit/schema-utils.test.ts +225 -0
  404. package/.framework/tests/unit/trigger-engine.test.ts +513 -0
  405. package/.framework/tests/unit/validate-app.test.ts +527 -0
  406. package/README.md +34 -3
  407. package/bin/spine-framework.cjs +1 -0
  408. package/config/tailwind.config.cjs +7 -0
  409. package/config/tsconfig.functions.json +42 -0
  410. package/config/tsconfig.json +37 -9
  411. package/config/vite.config.ts +31 -2
  412. package/netlify.toml +31 -3
  413. package/package.json +46 -29
  414. package/scripts/assemble-frontend.sh +127 -44
  415. package/scripts/assemble-functions.sh +127 -29
  416. package/scripts/boundary-check.sh +6 -2
  417. package/scripts/build-manifest.sh +24 -68
  418. package/scripts/checksum-manifest.mts +345 -0
  419. package/scripts/dev-clean.sh +86 -0
  420. package/scripts/dev-netlify.sh +49 -0
  421. package/scripts/netlify-dev-wrapper.sh +2 -0
  422. package/scripts/verify-install.ts +358 -0
  423. package/scripts/verify-integrity.sh +21 -57
  424. package/scripts/verify-manifest.mts +305 -0
  425. package/templates/project/gitignore +45 -0
  426. package/{config → templates/project}/package-lock.json +22335 -9366
  427. package/vitest.config.ts +46 -0
  428. package/.framework/public/spine.config.json +0 -91
  429. package/config/deno.lock +0 -108
  430. package/config/tsconfig.node.tsbuildinfo +0 -1
  431. package/config/tsconfig.tsbuildinfo +0 -1
  432. package/config/vite.config.d.ts +0 -2
  433. package/custom/.devin/AGENT.md +0 -357
  434. package/custom/.devin/skills/install-app/SKILL.md +0 -39
  435. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
  436. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
  437. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
  438. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
  439. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
  440. package/custom/.devin/workflows/app-install.yaml +0 -83
  441. package/custom/README.md +0 -79
  442. package/docs/README.md +0 -15
  443. package/scripts/app-install-cli.ts +0 -286
  444. package/scripts/check-core-integrity.sh +0 -82
  445. package/scripts/ingest-chunks.cjs +0 -202
  446. package/scripts/kb-chunk-parser.cjs +0 -312
  447. package/scripts/kb-chunk-parser.ts +0 -330
  448. package/scripts/kb-ingest-docs.ts +0 -203
  449. package/scripts/load-test-app-install.ts +0 -484
  450. package/scripts/test-login.js +0 -33
  451. package/spine.config.json +0 -91
@@ -0,0 +1,509 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/tests/unit/migrate.test.ts"]
2
+ // CIL[DOC-MACRO purpose="Import Vitest and filesystem fixture utilities for isolated CLI tests."]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Import Vitest and filesystem fixture utilities for isolated CLI tests."]
4
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="none" errors="none" side_effects="none"]
6
+ // CIL[DOC-MICRO logic_summary="Import Vitest and filesystem fixture utilities for isolated CLI tests." notable_decisions=""]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="experimental"]
9
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
11
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
12
+ import { tmpdir } from 'node:os'
13
+ import { join } from 'node:path'
14
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:module_header:module_header"]
15
+
16
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:pgstate:constant" type="constant" identity="pgState" parent="none"]
17
+ // CIL[DOC-MACRO purpose="Initialize hoisted state for isolated Postgres client mocks."]
18
+ // CIL[DOC-RESPONSIBILITY responsibility="Initialize hoisted state for isolated Postgres client mocks."]
19
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
20
+ // CIL[DOC-CONTRACT inputs="none" outputs="Mock client instances, handler, and connect error state" errors="none" side_effects="Initializes mock state"]
21
+ // CIL[DOC-MICRO logic_summary="Initialize hoisted state for isolated Postgres client mocks." notable_decisions=""]
22
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
23
+ // CIL[DOC-STABILITY stability="experimental"]
24
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
25
+ const pgState = vi.hoisted(() => ({
26
+ instances: [] as any[],
27
+ handler: null as null | ((sql: string, params?: any[]) => any),
28
+ connectError: null as null | Error,
29
+ }))
30
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:pgstate:constant"]
31
+
32
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:pg_mock:block" type="block" identity="pg_mock" parent="none"]
33
+ // CIL[DOC-MACRO purpose="Register a fake Postgres client that records SQL and never connects to a database."]
34
+ // CIL[DOC-RESPONSIBILITY responsibility="Register a fake Postgres client that records SQL and never connects to a database."]
35
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
36
+ // CIL[DOC-CONTRACT inputs="pgState" outputs="Vitest module mock" errors="none" side_effects="Registers a mock"]
37
+ // CIL[DOC-MICRO logic_summary="Register a fake Postgres client that records SQL and never connects to a database." notable_decisions=""]
38
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
39
+ // CIL[DOC-STABILITY stability="experimental"]
40
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
41
+ vi.mock('pg', () => ({
42
+ default: {
43
+ Client: class {
44
+ config: any
45
+ calls: { sql: string; params?: any[] }[] = []
46
+ ended = false
47
+ constructor(config: any) {
48
+ this.config = config
49
+ pgState.instances.push(this)
50
+ }
51
+ async connect() {
52
+ if (pgState.connectError) throw pgState.connectError
53
+ }
54
+ async end() {
55
+ this.ended = true
56
+ }
57
+ async query(sql: string, params?: any[]) {
58
+ this.calls.push({ sql, params })
59
+ if (pgState.handler) return pgState.handler(sql, params)
60
+ return { rows: [] }
61
+ }
62
+ },
63
+ },
64
+ }))
65
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:pg_mock:block"]
66
+
67
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:orig_cwd:constant" type="constant" identity="ORIG_CWD" parent="none"]
68
+ // CIL[DOC-MACRO purpose="Capture the original working directory for fixture restoration."]
69
+ // CIL[DOC-RESPONSIBILITY responsibility="Capture the original working directory for fixture restoration."]
70
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
71
+ // CIL[DOC-CONTRACT inputs="none" outputs="Original cwd string" errors="none" side_effects="Reads process cwd"]
72
+ // CIL[DOC-MICRO logic_summary="Capture the original working directory for fixture restoration." notable_decisions=""]
73
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
74
+ // CIL[DOC-STABILITY stability="experimental"]
75
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
76
+ const ORIG_CWD = process.cwd()
77
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:orig_cwd:constant"]
78
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:env_keys:constant" type="constant" identity="ENV_KEYS" parent="none"]
79
+ // CIL[DOC-MACRO purpose="List environment variables saved and restored by migration tests."]
80
+ // CIL[DOC-RESPONSIBILITY responsibility="List environment variables saved and restored by migration tests."]
81
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
82
+ // CIL[DOC-CONTRACT inputs="none" outputs="Readonly environment-key list" errors="none" side_effects="none"]
83
+ // CIL[DOC-MICRO logic_summary="List environment variables saved and restored by migration tests." notable_decisions=""]
84
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
85
+ // CIL[DOC-STABILITY stability="experimental"]
86
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
87
+ const ENV_KEYS = ['SUPABASE_URL', 'SUPABASE_DB_PASSWORD'] as const
88
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:env_keys:constant"]
89
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:savedenv:variable" type="variable" identity="savedEnv" parent="none"]
90
+ // CIL[DOC-MACRO purpose="Hold saved environment values for fixture cleanup."]
91
+ // CIL[DOC-RESPONSIBILITY responsibility="Hold saved environment values for fixture cleanup."]
92
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
93
+ // CIL[DOC-CONTRACT inputs="none" outputs="Mutable saved environment map" errors="none" side_effects="Initializes local state"]
94
+ // CIL[DOC-MICRO logic_summary="Hold saved environment values for fixture cleanup." notable_decisions=""]
95
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
96
+ // CIL[DOC-STABILITY stability="experimental"]
97
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
98
+ let savedEnv: Record<string, string | undefined> = {}
99
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:savedenv:variable"]
100
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:workdir:variable" type="variable" identity="workdir" parent="none"]
101
+ // CIL[DOC-MACRO purpose="Hold the temporary project root for migration fixtures."]
102
+ // CIL[DOC-RESPONSIBILITY responsibility="Hold the temporary project root for migration fixtures."]
103
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
104
+ // CIL[DOC-CONTRACT inputs="none" outputs="Mutable fixture path" errors="none" side_effects="Initializes local state"]
105
+ // CIL[DOC-MICRO logic_summary="Hold the temporary project root for migration fixtures." notable_decisions=""]
106
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
107
+ // CIL[DOC-STABILITY stability="experimental"]
108
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
109
+ let workdir = ''
110
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:workdir:variable"]
111
+
112
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:mig:function" type="function" identity="mig" parent="none"]
113
+ // CIL[DOC-MACRO purpose="Write a framework SQL fixture inside the temporary project."]
114
+ // CIL[DOC-RESPONSIBILITY responsibility="Write a framework SQL fixture inside the temporary project."]
115
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
116
+ // CIL[DOC-CONTRACT inputs="name: string, sql: string" outputs="void" errors="none" side_effects="Creates fixture directories and writes a fixture"]
117
+ // CIL[DOC-MICRO logic_summary="Write a framework SQL fixture inside the temporary project." notable_decisions=""]
118
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
119
+ // CIL[DOC-STABILITY stability="experimental"]
120
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
121
+ function mig(name: string, sql: string) {
122
+ const dir = join(workdir, '.framework/migrations')
123
+ mkdirSync(dir, { recursive: true })
124
+ writeFileSync(join(dir, name), sql)
125
+ }
126
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:mig:function"]
127
+
128
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:custommig:function" type="function" identity="customMig" parent="none"]
129
+ // CIL[DOC-MACRO purpose="Write a custom SQL fixture inside the temporary project."]
130
+ // CIL[DOC-RESPONSIBILITY responsibility="Write a custom SQL fixture inside the temporary project."]
131
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
132
+ // CIL[DOC-CONTRACT inputs="name: string, sql: string" outputs="void" errors="none" side_effects="Creates fixture directories and writes a fixture"]
133
+ // CIL[DOC-MICRO logic_summary="Write a custom SQL fixture inside the temporary project." notable_decisions=""]
134
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
135
+ // CIL[DOC-STABILITY stability="experimental"]
136
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
137
+ function customMig(name: string, sql: string) {
138
+ const dir = join(workdir, 'custom/migrations')
139
+ mkdirSync(dir, { recursive: true })
140
+ writeFileSync(join(dir, name), sql)
141
+ }
142
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:custommig:function"]
143
+
144
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:freshmodule:function" type="function" identity="freshModule" parent="none"]
145
+ // CIL[DOC-MACRO purpose="Reset the module cache and import migration code against the current fixture cwd."]
146
+ // CIL[DOC-RESPONSIBILITY responsibility="Reset the module cache and import migration code against the current fixture cwd."]
147
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
148
+ // CIL[DOC-CONTRACT inputs="none" outputs="Promise of the migration module" errors="none" side_effects="Resets Vitest module cache and imports source"]
149
+ // CIL[DOC-MICRO logic_summary="Reset the module cache and import migration code against the current fixture cwd." notable_decisions=""]
150
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
151
+ // CIL[DOC-STABILITY stability="experimental"]
152
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
153
+ async function freshModule() {
154
+ vi.resetModules()
155
+ return await import('../../cli/commands/migrate.ts')
156
+ }
157
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:freshmodule:function"]
158
+
159
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:setenv:function" type="function" identity="setEnv" parent="none"]
160
+ // CIL[DOC-MACRO purpose="Set fake hosted Supabase credentials for isolated migration execution tests."]
161
+ // CIL[DOC-RESPONSIBILITY responsibility="Set fake hosted Supabase credentials for isolated migration execution tests."]
162
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
163
+ // CIL[DOC-CONTRACT inputs="none" outputs="void" errors="none" side_effects="Sets fixture environment variables"]
164
+ // CIL[DOC-MICRO logic_summary="Set fake hosted Supabase credentials for isolated migration execution tests." notable_decisions=""]
165
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
166
+ // CIL[DOC-STABILITY stability="experimental"]
167
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
168
+ function setEnv() {
169
+ process.env.SUPABASE_URL = 'https://abc123.supabase.co'
170
+ process.env.SUPABASE_DB_PASSWORD = 'p@ss:w/eird?chars'
171
+ }
172
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:setenv:function"]
173
+
174
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:fixture_setup:block" type="block" identity="fixture_setup" parent="none"]
175
+ // CIL[DOC-MACRO purpose="Register temporary-project setup with fake clients and muted console output."]
176
+ // CIL[DOC-RESPONSIBILITY responsibility="Register temporary-project setup with fake clients and muted console output."]
177
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
178
+ // CIL[DOC-CONTRACT inputs="Vitest lifecycle and mock state" outputs="Fixture hook" errors="none" side_effects="Registers a hook that creates a temp directory, changes cwd, and installs spies"]
179
+ // CIL[DOC-MICRO logic_summary="Register temporary-project setup with fake clients and muted console output." notable_decisions=""]
180
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
181
+ // CIL[DOC-STABILITY stability="experimental"]
182
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
183
+ beforeEach(() => {
184
+ savedEnv = {}
185
+ for (const k of ENV_KEYS) savedEnv[k] = process.env[k]
186
+ workdir = mkdtempSync(join(tmpdir(), 'spine-migrate-test-'))
187
+ process.chdir(workdir)
188
+ pgState.instances = []
189
+ pgState.handler = null
190
+ pgState.connectError = null
191
+ vi.spyOn(console, 'log').mockImplementation(() => {})
192
+ vi.spyOn(console, 'error').mockImplementation(() => {})
193
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
194
+ vi.spyOn(process.stdout, 'write').mockImplementation((() => true) as any)
195
+ })
196
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:fixture_setup:block"]
197
+
198
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:fixture_cleanup:block" type="block" identity="fixture_cleanup" parent="none"]
199
+ // CIL[DOC-MACRO purpose="Register cleanup that restores cwd, environment, and spies and removes the created fixture."]
200
+ // CIL[DOC-RESPONSIBILITY responsibility="Register cleanup that restores cwd, environment, and spies and removes the created fixture."]
201
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
202
+ // CIL[DOC-CONTRACT inputs="Fixture state" outputs="Cleanup hook" errors="none" side_effects="Registers fixture cleanup"]
203
+ // CIL[DOC-MICRO logic_summary="Register cleanup that restores cwd, environment, and spies and removes the created fixture." notable_decisions=""]
204
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
205
+ // CIL[DOC-STABILITY stability="experimental"]
206
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
207
+ afterEach(() => {
208
+ process.chdir(ORIG_CWD)
209
+ rmSync(workdir, { recursive: true, force: true })
210
+ for (const k of ENV_KEYS) {
211
+ if (savedEnv[k] === undefined) delete process.env[k]
212
+ else process.env[k] = savedEnv[k]
213
+ }
214
+ vi.restoreAllMocks()
215
+ })
216
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:fixture_cleanup:block"]
217
+
218
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:collection_suite:block" type="block" identity="collection_suite" parent="none"]
219
+ // CIL[DOC-MACRO purpose="Register framework-before-custom ordering, framework-only, and file-filter tests."]
220
+ // CIL[DOC-RESPONSIBILITY responsibility="Register framework-before-custom ordering, framework-only, and file-filter tests."]
221
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
222
+ // CIL[DOC-CONTRACT inputs="Fixture helpers and imported migration module" outputs="Vitest assertions" errors="none" side_effects="Registers isolated fixture tests"]
223
+ // CIL[DOC-MICRO logic_summary="Register framework-before-custom ordering, framework-only, and file-filter tests." notable_decisions=""]
224
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
225
+ // CIL[DOC-STABILITY stability="experimental"]
226
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
227
+ describe('collectMigrations', () => {
228
+ it('returns framework then custom migrations sorted lexically', async () => {
229
+ const { collectMigrations } = await freshModule()
230
+ mig('002_b.sql', 'SELECT 2;')
231
+ mig('001_a.sql', 'SELECT 1;')
232
+ customMig('003_c.sql', 'SELECT 3;')
233
+ const files = collectMigrations(false)
234
+ expect(files.map(f => f.name)).toEqual(['001_a.sql', '002_b.sql', '003_c.sql'])
235
+ })
236
+
237
+ it('excludes custom migrations when frameworkOnly is true', async () => {
238
+ const { collectMigrations } = await freshModule()
239
+ mig('001_a.sql', 'SELECT 1;')
240
+ customMig('002_b.sql', 'SELECT 2;')
241
+ const files = collectMigrations(true)
242
+ expect(files.map(f => f.name)).toEqual(['001_a.sql'])
243
+ })
244
+
245
+ it('ignores non-sql and dotfiles', async () => {
246
+ const { collectMigrations } = await freshModule()
247
+ mig('001_a.sql', 'SELECT 1;')
248
+ mig('notes.txt', 'x')
249
+ mig('.hidden.sql', 'SELECT 0;')
250
+ const files = collectMigrations(false)
251
+ expect(files.map(f => f.name)).toEqual(['001_a.sql'])
252
+ })
253
+ })
254
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:collection_suite:block"]
255
+
256
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:sql_parser_suite:block" type="block" identity="sql_parser_suite" parent="none"]
257
+ // CIL[DOC-MACRO purpose="Register quoted-literal, comment, dollar-quote, and trailing-statement SQL parser tests."]
258
+ // CIL[DOC-RESPONSIBILITY responsibility="Register quoted-literal, comment, dollar-quote, and trailing-statement SQL parser tests."]
259
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
260
+ // CIL[DOC-CONTRACT inputs="Imported splitSqlStatements" outputs="Vitest assertions" errors="none" side_effects="Registers parser tests"]
261
+ // CIL[DOC-MICRO logic_summary="Register quoted-literal, comment, dollar-quote, and trailing-statement SQL parser tests." notable_decisions=""]
262
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
263
+ // CIL[DOC-STABILITY stability="experimental"]
264
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
265
+ describe('splitSqlStatements', () => {
266
+ it('keeps semicolons inside single-quoted literals in one statement', async () => {
267
+ const { splitSqlStatements } = await freshModule()
268
+ const stmts = splitSqlStatements("INSERT INTO t VALUES ('a;b'); SELECT 1;")
269
+ expect(stmts).toEqual(["INSERT INTO t VALUES ('a;b');", 'SELECT 1;'])
270
+ })
271
+
272
+ it('drops line and block comments including their semicolons', async () => {
273
+ const { splitSqlStatements } = await freshModule()
274
+ const stmts = splitSqlStatements('-- comment ; here\nSELECT 1; /* block ; */ SELECT 2;')
275
+ expect(stmts).toEqual(['SELECT 1;', 'SELECT 2;'])
276
+ })
277
+
278
+ it('preserves dollar-quoted function bodies verbatim', async () => {
279
+ const { splitSqlStatements } = await freshModule()
280
+ const sql = 'CREATE FUNCTION f() RETURNS void AS $$ BEGIN RAISE NOTICE \'x;y\'; END; $$ LANGUAGE plpgsql; SELECT 3;'
281
+ const stmts = splitSqlStatements(sql)
282
+ expect(stmts.length).toBe(2)
283
+ expect(stmts[0]).toContain('$$ BEGIN')
284
+ expect(stmts[0]).toContain("'x;y'")
285
+ expect(stmts[1]).toBe('SELECT 3;')
286
+ })
287
+
288
+ it('appends a semicolon to a trailing statement without one', async () => {
289
+ const { splitSqlStatements } = await freshModule()
290
+ expect(splitSqlStatements('SELECT 1')).toEqual(['SELECT 1;'])
291
+ })
292
+ })
293
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:sql_parser_suite:block"]
294
+
295
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/migrate.test.ts:execution_suite:block" type="block" identity="execution_suite" parent="none"]
296
+ // CIL[DOC-MACRO purpose="Register mocked migration execution tests for credentials, URL validation, tracking, transactions, abort, and cleanup."]
297
+ // CIL[DOC-RESPONSIBILITY responsibility="Register mocked migration execution tests for credentials, URL validation, tracking, transactions, abort, and cleanup."]
298
+ // CIL[DOC-CONTEXT module="tests/unit" layer="test-unit" related_files=".framework/cli/commands/migrate.ts"]
299
+ // CIL[DOC-CONTRACT inputs="Mock client and fixture helpers" outputs="Vitest assertions" errors="none" side_effects="Registers fake-database execution tests"]
300
+ // CIL[DOC-MICRO logic_summary="Register mocked migration execution tests for credentials, URL validation, tracking, transactions, abort, and cleanup." notable_decisions=""]
301
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
302
+ // CIL[DOC-STABILITY stability="experimental"]
303
+ // CIL[DOC-AUDIENCE audience="framework contributors"]
304
+ describe('runMigrations', () => {
305
+ it('returns null when credentials are missing', async () => {
306
+ const { runMigrations } = await freshModule()
307
+ delete process.env.SUPABASE_URL
308
+ delete process.env.SUPABASE_DB_PASSWORD
309
+ mig('001_a.sql', 'SELECT 1;')
310
+ expect(await runMigrations()).toBeNull()
311
+ expect(pgState.instances.length).toBe(0)
312
+ })
313
+
314
+ it('throws a credential-free error for an invalid SUPABASE_URL', async () => {
315
+ const { runMigrations } = await freshModule()
316
+ process.env.SUPABASE_URL = 'not-a-url'
317
+ process.env.SUPABASE_DB_PASSWORD = 'supersecretvalue'
318
+ mig('001_a.sql', 'SELECT 1;')
319
+ await expect(runMigrations()).rejects.toThrow(/SUPABASE_URL/)
320
+ try {
321
+ await runMigrations()
322
+ } catch (err: any) {
323
+ expect(err.message).not.toContain('supersecretvalue')
324
+ }
325
+ expect(pgState.instances.length).toBe(0)
326
+ })
327
+
328
+ it.each([
329
+ 'http://abc123.supabase.co',
330
+ 'postgres://abc123.supabase.co',
331
+ 'https://abc123.supabase.co:5432',
332
+ 'https://abc123.supabase.co/rest',
333
+ 'https://abc123.supabase.co/?q=1',
334
+ 'https://abc123.supabase.co/#frag',
335
+ 'https://abc123.evil.co',
336
+ 'https://abc123.supabase.co.evil.com',
337
+ 'https://.supabase.co',
338
+ ])('rejects non-hosted SUPABASE_URL %s before creating a client', async (url) => {
339
+ const { runMigrations } = await freshModule()
340
+ process.env.SUPABASE_URL = url
341
+ process.env.SUPABASE_DB_PASSWORD = 'dbpass'
342
+ mig('001_a.sql', 'SELECT 1;')
343
+ await expect(runMigrations()).rejects.toThrow(/SUPABASE_URL/)
344
+ expect(pgState.instances.length).toBe(0)
345
+ })
346
+
347
+ it('rejects credential-bearing SUPABASE_URL values without leaking the secret', async () => {
348
+ const { runMigrations } = await freshModule()
349
+ process.env.SUPABASE_URL = 'https://user:s3cr3t-in-url@abc123.supabase.co'
350
+ process.env.SUPABASE_DB_PASSWORD = 'dbpass'
351
+ mig('001_a.sql', 'SELECT 1;')
352
+ let message = ''
353
+ try {
354
+ await runMigrations()
355
+ } catch (err: any) {
356
+ message = err.message
357
+ }
358
+ expect(message).toMatch(/SUPABASE_URL/)
359
+ expect(message).not.toContain('s3cr3t-in-url')
360
+ expect(pgState.instances.length).toBe(0)
361
+ })
362
+
363
+ it('accepts a trailing-slash hosted SUPABASE_URL', async () => {
364
+ const { runMigrations } = await freshModule()
365
+ process.env.SUPABASE_URL = 'https://abc123.supabase.co/'
366
+ process.env.SUPABASE_DB_PASSWORD = 'dbpass'
367
+ mig('001_a.sql', 'SELECT 1;')
368
+ await runMigrations()
369
+ expect(pgState.instances[0].config.host).toBe('db.abc123.supabase.co')
370
+ })
371
+
372
+ it('returns zero counts without connecting when no migration files exist', async () => {
373
+ mkdirSync(join(workdir, '.framework/migrations'), { recursive: true })
374
+ const { runMigrations } = await freshModule()
375
+ setEnv()
376
+ expect(await runMigrations()).toEqual({ applied: 0, skipped: 0, failed: 0 })
377
+ expect(pgState.instances.length).toBe(0)
378
+ })
379
+
380
+ it('passes the password inside the client config object, never a URI', async () => {
381
+ const { runMigrations } = await freshModule()
382
+ setEnv()
383
+ mig('001_a.sql', 'SELECT 1;')
384
+ await runMigrations()
385
+ const cfg = pgState.instances[0].config
386
+ expect(cfg).toMatchObject({
387
+ user: 'postgres',
388
+ password: 'p@ss:w/eird?chars',
389
+ host: 'db.abc123.supabase.co',
390
+ port: 5432,
391
+ database: 'postgres',
392
+ })
393
+ expect(cfg).not.toHaveProperty('connectionString')
394
+ })
395
+
396
+ it('applies each file in a transaction and records the tracking row before COMMIT', async () => {
397
+ const { runMigrations } = await freshModule()
398
+ setEnv()
399
+ mig('001_a.sql', 'SELECT 1; SELECT 2;')
400
+ const result = await runMigrations()
401
+ expect(result).toEqual({ applied: 1, skipped: 0, failed: 0 })
402
+ const client = pgState.instances[0]
403
+ const sqls = client.calls.map((c: any) => c.sql)
404
+ expect(sqls[0]).toContain('CREATE TABLE IF NOT EXISTS _spine_migrations')
405
+ expect(sqls[1]).toBe('SELECT filename FROM _spine_migrations')
406
+ const beginIdx = sqls.indexOf('BEGIN')
407
+ const insertIdx = sqls.findIndex(s => s.includes('INSERT INTO _spine_migrations'))
408
+ const commitIdx = sqls.indexOf('COMMIT')
409
+ expect(beginIdx).toBeGreaterThan(-1)
410
+ expect(insertIdx).toBeGreaterThan(beginIdx)
411
+ expect(commitIdx).toBeGreaterThan(insertIdx)
412
+ const insertCall = client.calls[insertIdx]
413
+ expect(insertCall.params).toEqual(['001_a.sql'])
414
+ expect(client.ended).toBe(true)
415
+ })
416
+
417
+ it('skips already-tracked files without executing their SQL', async () => {
418
+ const { runMigrations } = await freshModule()
419
+ setEnv()
420
+ mig('001_a.sql', 'SELECT 1;')
421
+ mig('002_b.sql', 'SELECT 2;')
422
+ pgState.handler = (sql: string) => {
423
+ if (sql === 'SELECT filename FROM _spine_migrations') {
424
+ return { rows: [{ filename: '001_a.sql' }] }
425
+ }
426
+ return { rows: [] }
427
+ }
428
+ const result = await runMigrations()
429
+ expect(result).toEqual({ applied: 1, skipped: 1, failed: 0 })
430
+ const client = pgState.instances[0]
431
+ expect(client.calls.filter((c: any) => c.sql === 'BEGIN').length).toBe(1)
432
+ expect(client.calls.some((c: any) => c.sql === 'SELECT 1;')).toBe(false)
433
+ })
434
+
435
+ it('stops at the first file error: rollback, one failed count, no later files or tracking insert', async () => {
436
+ const { runMigrations } = await freshModule()
437
+ setEnv()
438
+ mig('001_a.sql', 'SELECT 1;')
439
+ mig('002_b.sql', 'SELECT 2; BOOM;')
440
+ mig('003_c.sql', 'SELECT 3;')
441
+ pgState.handler = (sql: string) => {
442
+ if (sql === 'BOOM;') throw new Error('statement exploded')
443
+ return { rows: [] }
444
+ }
445
+ const result = await runMigrations()
446
+ expect(result).toEqual({ applied: 1, skipped: 0, failed: 1 })
447
+ const client = pgState.instances[0]
448
+ const sqls = client.calls.map((c: any) => c.sql)
449
+ expect(sqls.filter(s => s === 'BEGIN').length).toBe(2)
450
+ expect(sqls.filter(s => s === 'ROLLBACK').length).toBe(1)
451
+ expect(sqls.filter(s => s === 'SELECT 3;').length).toBe(0)
452
+ const inserts = client.calls.filter((c: any) => c.sql.includes('INSERT INTO _spine_migrations'))
453
+ expect(inserts.length).toBe(1)
454
+ expect(inserts[0].params).toEqual(['001_a.sql'])
455
+ expect(client.ended).toBe(true)
456
+ })
457
+
458
+ it('propagates tracking-table CREATE failure and runs no migrations', async () => {
459
+ const { runMigrations } = await freshModule()
460
+ setEnv()
461
+ mig('001_a.sql', 'SELECT 1;')
462
+ pgState.handler = (sql: string) => {
463
+ if (sql.includes('CREATE TABLE IF NOT EXISTS _spine_migrations')) {
464
+ throw new Error('cannot create tracking table')
465
+ }
466
+ return { rows: [] }
467
+ }
468
+ await expect(runMigrations()).rejects.toThrow('cannot create tracking table')
469
+ const client = pgState.instances[0]
470
+ expect(client.calls.some((c: any) => c.sql === 'BEGIN')).toBe(false)
471
+ expect(client.ended).toBe(true)
472
+ })
473
+
474
+ it('propagates tracking SELECT failure and runs no migrations', async () => {
475
+ const { runMigrations } = await freshModule()
476
+ setEnv()
477
+ mig('001_a.sql', 'SELECT 1;')
478
+ pgState.handler = (sql: string) => {
479
+ if (sql === 'SELECT filename FROM _spine_migrations') {
480
+ throw new Error('cannot read tracking state')
481
+ }
482
+ return { rows: [] }
483
+ }
484
+ await expect(runMigrations()).rejects.toThrow('cannot read tracking state')
485
+ const client = pgState.instances[0]
486
+ expect(client.calls.some((c: any) => c.sql === 'BEGIN')).toBe(false)
487
+ expect(client.ended).toBe(true)
488
+ })
489
+
490
+ it('propagates read errors and still closes the client', async () => {
491
+ const { runMigrations } = await freshModule()
492
+ setEnv()
493
+ const dir = join(workdir, '.framework/migrations')
494
+ mkdirSync(dir, { recursive: true })
495
+ mkdirSync(join(dir, 'broken.sql'))
496
+ await expect(runMigrations()).rejects.toThrow()
497
+ expect(pgState.instances[0].ended).toBe(true)
498
+ })
499
+
500
+ it('ends the client when connect itself succeeds but a later error occurs', async () => {
501
+ const { runMigrations } = await freshModule()
502
+ setEnv()
503
+ mig('001_a.sql', 'SELECT 1;')
504
+ pgState.handler = () => { throw new Error('mid-flight failure') }
505
+ await expect(runMigrations()).rejects.toThrow('mid-flight failure')
506
+ expect(pgState.instances[0].ended).toBe(true)
507
+ })
508
+ })
509
+ // CIL[END stable_id="~/.framework/tests/unit/migrate.test.ts:execution_suite:block"]
@@ -0,0 +1,136 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/notify.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Unit tests for _shared/notify.ts resolveNotificationRecipients"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Assert role:<slug> entries resolve to account-scoped people ids, UUIDs pass through, and malformed/unknown entries warn and are skipped"]
4
+ // CIL[DOC-CONTEXT module="tests/unit/notify" layer="test-unit" related_files=".framework/functions/_shared/notify.ts,.framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts"]
5
+ // CIL[DOC-CONTRACT inputs="mocked adminDb people queries" outputs="expect assertions on resolved id lists and console.warn" errors="none" side_effects="registers vi.mock factories"]
6
+ // CIL[DOC-MICRO logic_summary="Mocks adminDb.from('people') with a thenable supabase-style chain so the awaited .eq('role.slug', ...) call resolves fixture rows" notable_decisions="[]"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="test maintainers"]
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
11
+ import type { CoreContext } from '../../functions/_shared/middleware.ts'
12
+
13
+ vi.mock('../../functions/_shared/db.ts', () => ({
14
+ adminDb: { from: vi.fn(), rpc: vi.fn() }
15
+ }))
16
+
17
+ import { adminDb } from '../../functions/_shared/db.ts'
18
+ import { resolveNotificationRecipients } from '../../functions/_shared/notify.ts'
19
+ // CIL[END stable_id="~/.framework/tests/unit/notify.test.ts:module_header:module_header"]
20
+
21
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/notify.test.ts:makeCtx:function" type="function" identity="makeCtx" parent="none"]
22
+ // CIL[DOC-MACRO purpose="Create a test CoreContext scoped to acct-test"]
23
+ // CIL[DOC-CONTRACT inputs="overrides: Partial<CoreContext>" outputs="CoreContext object" errors="none" side_effects="none"]
24
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
25
+ // CIL[DOC-STABILITY stability="stable"]
26
+ function makeCtx(overrides: Partial<CoreContext> = {}): CoreContext {
27
+ return {
28
+ principal: {
29
+ id: 'system', type: 'machine', accountId: null, scopes: ['*:*'],
30
+ provenance: { sourceType: 'manual', createdBy: null, invokedAt: new Date().toISOString() }
31
+ },
32
+ accountId: 'acct-test',
33
+ db: {},
34
+ requestId: 'req-test',
35
+ ...overrides
36
+ }
37
+ }
38
+ // CIL[END stable_id="~/.framework/tests/unit/notify.test.ts:makeCtx:function"]
39
+
40
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/notify.test.ts:mockPeople:function" type="function" identity="mockPeople" parent="none"]
41
+ // CIL[DOC-MACRO purpose="Mock adminDb.from('people') to resolve fixture rows from the awaited terminal .eq"]
42
+ // CIL[DOC-RESPONSIBILITY responsibility="Return a supabase-style thenable query builder for people role-join lookups"]
43
+ // CIL[DOC-CONTRACT inputs="rows: any[] | null, error?: any" outputs="configured adminDb.from mock" errors="none" side_effects="Mocks adminDb.from"]
44
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
45
+ // CIL[DOC-STABILITY stability="stable"]
46
+ const UUID_A = '11111111-1111-4111-8111-111111111111'
47
+ const UUID_B = '22222222-2222-4222-8222-222222222222'
48
+
49
+ function mockPeople(result: { data: any[] | null; error?: any }) {
50
+ const q: any = {
51
+ select: vi.fn(() => q),
52
+ eq: vi.fn(() => q),
53
+ in: vi.fn(() => q),
54
+ single: vi.fn().mockResolvedValue({ data: result.data?.[0] ?? null, error: result.error ?? null }),
55
+ maybeSingle: vi.fn().mockResolvedValue({ data: result.data?.[0] ?? null, error: result.error ?? null }),
56
+ then: (onF: any, onR: any) =>
57
+ Promise.resolve({ data: result.data, error: result.error ?? null }).then(onF, onR)
58
+ }
59
+ vi.mocked(adminDb.from).mockImplementation((table: string) => {
60
+ if (table === 'people') return q
61
+ return {
62
+ select: vi.fn().mockReturnThis(),
63
+ eq: vi.fn().mockReturnThis(),
64
+ single: vi.fn().mockResolvedValue({ data: null, error: null }),
65
+ maybeSingle: vi.fn().mockResolvedValue({ data: null, error: null })
66
+ } as any
67
+ })
68
+ }
69
+ // CIL[END stable_id="~/.framework/tests/unit/notify.test.ts:mockPeople:function"]
70
+
71
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/notify.test.ts:describe_resolveNotificationRecipients:block" type="block" identity="describe_resolveNotificationRecipients" parent="none"]
72
+ // CIL[DOC-MACRO purpose="resolveNotificationRecipients behavior suite"]
73
+ // CIL[DOC-RESPONSIBILITY responsibility="Verify role slug joins, UUID passthrough, warn-and-skip semantics, and dedupe"]
74
+ // CIL[DOC-CONTRACT inputs="recipient entry lists" outputs="resolved people.id lists" errors="none" side_effects="console.warn on skipped entries"]
75
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
76
+ // CIL[DOC-STABILITY stability="stable"]
77
+ describe('resolveNotificationRecipients', () => {
78
+ beforeEach(() => {
79
+ vi.clearAllMocks()
80
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
81
+ })
82
+
83
+ it('resolves role:<slug> entries to people ids in the account', async () => {
84
+ mockPeople({ data: [{ id: 'person-1' }, { id: 'person-2' }] })
85
+ const ids = await resolveNotificationRecipients(makeCtx(), ['role:member_admin'])
86
+ expect(ids).toEqual(['person-1', 'person-2'])
87
+ expect(adminDb.from).toHaveBeenCalledWith('people')
88
+ })
89
+
90
+ it('passes UUID-shaped recipients through unchanged', async () => {
91
+ mockPeople({ data: [] })
92
+ const ids = await resolveNotificationRecipients(makeCtx(), [UUID_A, UUID_B])
93
+ expect(ids).toEqual([UUID_A, UUID_B])
94
+ expect(adminDb.from).not.toHaveBeenCalledWith('people')
95
+ })
96
+
97
+ it('warns and skips a role that matches no active people', async () => {
98
+ mockPeople({ data: [] })
99
+ const ids = await resolveNotificationRecipients(makeCtx(), ['role:no_such_role'])
100
+ expect(ids).toEqual([])
101
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('role:no_such_role'))
102
+ })
103
+
104
+ it('warns and skips when the role lookup query errors', async () => {
105
+ mockPeople({ data: null, error: { message: 'join exploded' } })
106
+ const ids = await resolveNotificationRecipients(makeCtx(), ['role:member_admin'])
107
+ expect(ids).toEqual([])
108
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('role:member_admin'))
109
+ })
110
+
111
+ it('warns and skips recipients that are neither role: handles nor UUIDs', async () => {
112
+ mockPeople({ data: [] })
113
+ const ids = await resolveNotificationRecipients(makeCtx(), ['member_admin', 'not-a-uuid'])
114
+ expect(ids).toEqual([])
115
+ expect(console.warn).toHaveBeenCalledTimes(2)
116
+ })
117
+
118
+ it('skips non-string entries with a warning', async () => {
119
+ mockPeople({ data: [] })
120
+ const ids = await resolveNotificationRecipients(makeCtx(), [42, null] as any[])
121
+ expect(ids).toEqual([])
122
+ expect(console.warn).toHaveBeenCalledTimes(2)
123
+ })
124
+
125
+ it('mixes role resolution and UUID passthrough, deduping results', async () => {
126
+ mockPeople({ data: [{ id: UUID_A }, { id: 'person-9' }] })
127
+ const ids = await resolveNotificationRecipients(makeCtx(), ['role:member_admin', UUID_A, UUID_B])
128
+ expect(ids).toEqual([UUID_A, 'person-9', UUID_B])
129
+ })
130
+
131
+ it('returns an empty list for empty or missing recipients', async () => {
132
+ expect(await resolveNotificationRecipients(makeCtx(), [])).toEqual([])
133
+ expect(await resolveNotificationRecipients(makeCtx(), undefined as any)).toEqual([])
134
+ })
135
+ })
136
+ // CIL[END stable_id="~/.framework/tests/unit/notify.test.ts:describe_resolveNotificationRecipients:block"]