spine-framework 0.3.145 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +76 -0
- package/.framework/.spine-manifest.json +19 -0
- package/.framework/cli/commands/agents.ts +61 -33
- package/.framework/cli/commands/auth.ts +60 -28
- package/.framework/cli/commands/create-app.ts +212 -80
- package/.framework/cli/commands/dev.ts +184 -68
- package/.framework/cli/commands/doctor.ts +324 -152
- package/.framework/cli/commands/generate.ts +119 -68
- package/.framework/cli/commands/init.ts +141 -242
- package/.framework/cli/commands/install-app.ts +1336 -314
- package/.framework/cli/commands/items.ts +31 -29
- package/.framework/cli/commands/migrate.ts +299 -156
- package/.framework/cli/commands/migrations.ts +62 -30
- package/.framework/cli/commands/pipelines.ts +31 -29
- package/.framework/cli/commands/status.ts +105 -45
- package/.framework/cli/commands/system.ts +47 -42
- package/.framework/cli/commands/test.ts +161 -102
- package/.framework/cli/commands/upgrade.ts +260 -0
- package/.framework/cli/commands/validate-app.ts +517 -161
- package/.framework/cli/context.ts +117 -85
- package/.framework/cli/env-loader.ts +74 -43
- package/.framework/cli/index.ts +70 -46
- package/.framework/cli/lib/project-files.ts +404 -0
- package/.framework/docs/guides/testing-guide.md +1 -1
- package/.framework/docs/runtime-types.md +72 -72
- package/.framework/functions/_shared/access.ts +84 -0
- package/.framework/functions/_shared/agent-runner.ts +5067 -1026
- package/.framework/functions/_shared/app-manifest.ts +214 -94
- package/.framework/functions/_shared/ast.ts +214 -0
- package/.framework/functions/_shared/audit.ts +91 -55
- package/.framework/functions/_shared/cil.ts +368 -127
- package/.framework/functions/_shared/db.ts +337 -68
- package/.framework/functions/_shared/funnel.ts +228 -0
- package/.framework/functions/_shared/geo.ts +105 -0
- package/.framework/functions/_shared/hash-api-key.ts +94 -17
- package/.framework/functions/_shared/index.ts +22 -224
- package/.framework/functions/_shared/kb.ts +832 -58
- package/.framework/functions/_shared/llm.ts +118 -18
- package/.framework/functions/_shared/logging.ts +317 -19
- package/.framework/functions/_shared/middleware.ts +701 -131
- package/.framework/functions/_shared/notify.ts +70 -0
- package/.framework/functions/_shared/outcomes.ts +34 -19
- package/.framework/functions/_shared/permissions.ts +501 -315
- package/.framework/functions/_shared/pipeline-runner.ts +704 -155
- package/.framework/functions/_shared/principal.ts +436 -263
- package/.framework/functions/_shared/rate-limit.ts +233 -0
- package/.framework/functions/_shared/resolve-credentials.ts +86 -19
- package/.framework/functions/_shared/resolve-ids.ts +203 -139
- package/.framework/functions/_shared/retry-deliveries.ts +51 -16
- package/.framework/functions/_shared/schema-utils.ts +602 -521
- package/.framework/functions/_shared/security.ts +345 -0
- package/.framework/functions/_shared/testing.ts +197 -172
- package/.framework/functions/_shared/tracing.ts +351 -21
- package/.framework/functions/_shared/trigger-engine.ts +425 -136
- package/.framework/functions/_shared/webhook-registration.ts +71 -70
- package/.framework/functions/_shared/webhook-registry.ts +126 -83
- package/.framework/functions/account-nodes.ts +30 -30
- package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
- package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
- package/.framework/functions/adapters/openai-outbound.ts +15 -16
- package/.framework/functions/admin-data.ts +596 -173
- package/.framework/functions/admin-funnel.ts +64 -60
- package/.framework/functions/admin-install-funnel.ts +84 -66
- package/.framework/functions/admin-signals.ts +69 -51
- package/.framework/functions/ai-agents.ts +431 -106
- package/.framework/functions/ai-interactions.ts +186 -112
- package/.framework/functions/api-keys.ts +107 -107
- package/.framework/functions/apps.ts +154 -136
- package/.framework/functions/auth.ts +83 -65
- package/.framework/functions/cil-agent-ingest.ts +189 -232
- package/.framework/functions/cil-agent.ts +433 -177
- package/.framework/functions/cil-callmap.ts +642 -430
- package/.framework/functions/cil-config.ts +139 -135
- package/.framework/functions/cil-context.ts +214 -127
- package/.framework/functions/cil-doc.ts +587 -458
- package/.framework/functions/cil-embeddings.ts +453 -364
- package/.framework/functions/cil-graph.ts +427 -142
- package/.framework/functions/cil-ingest.ts +1418 -1154
- package/.framework/functions/cil-pipeline.ts +276 -21
- package/.framework/functions/cil-project.ts +659 -132
- package/.framework/functions/cil-validate.ts +198 -164
- package/.framework/functions/debug-auth.ts +57 -55
- package/.framework/functions/deliveries.ts +80 -21
- package/.framework/functions/embeddings.ts +1023 -318
- package/.framework/functions/eval-harness.ts +146 -108
- package/.framework/functions/integration-outbound.ts +205 -75
- package/.framework/functions/integration-routes.ts +128 -23
- package/.framework/functions/integrations.ts +125 -100
- package/.framework/functions/invites.ts +1095 -460
- package/.framework/functions/item-progress.ts +158 -102
- package/.framework/functions/kb.ts +34 -10
- package/.framework/functions/logs.ts +133 -133
- package/.framework/functions/observability.ts +152 -122
- package/.framework/functions/pipeline-executions.ts +147 -132
- package/.framework/functions/pipelines.ts +134 -119
- package/.framework/functions/prompt-configs.ts +96 -80
- package/.framework/functions/roles.ts +112 -82
- package/.framework/functions/security.ts +450 -0
- package/.framework/functions/system-cron.ts +117 -80
- package/.framework/functions/system.ts +158 -98
- package/.framework/functions/tests.ts +29 -29
- package/.framework/functions/timers.ts +94 -95
- package/.framework/functions/trigger-scheduler.ts +10 -0
- package/.framework/functions/triggers.ts +179 -149
- package/.framework/functions/types.ts +137 -125
- package/.framework/mcp/index.ts +133 -0
- package/.framework/mcp/load-env.ts +60 -0
- package/.framework/mcp/shared/app-manifest.ts +275 -0
- package/.framework/mcp/shared/db.ts +477 -0
- package/.framework/mcp/shared/kb.ts +118 -0
- package/.framework/mcp/tools/architect.ts +431 -0
- package/.framework/mcp/tools/cil/context.ts +345 -0
- package/.framework/mcp/tools/cil/graph.ts +242 -0
- package/.framework/mcp/tools/cil/ingest.ts +1217 -0
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
- package/.framework/mcp/tools/cil/pipeline.ts +767 -0
- package/.framework/mcp/tools/cil/project.ts +791 -0
- package/.framework/mcp/tools/cil/retrieval.ts +336 -0
- package/.framework/mcp/tools/cil/search-core.ts +454 -0
- package/.framework/mcp/tools/cil/search.ts +97 -0
- package/.framework/mcp/tools/cil/suggest.ts +186 -0
- package/.framework/mcp/tools/cil/validate.ts +1369 -0
- package/.framework/mcp/tools/kb/delete.ts +139 -0
- package/.framework/mcp/tools/kb/index.ts +52 -0
- package/.framework/mcp/tools/kb/ingest.ts +84 -0
- package/.framework/mcp/tools/kb/list-types.ts +102 -0
- package/.framework/mcp/tools/kb/publish.ts +100 -0
- package/.framework/mcp/tools/kb/reindex.ts +86 -0
- package/.framework/mcp/tools/kb/revoke.ts +85 -0
- package/.framework/mcp/tools/kb/search.ts +101 -0
- package/.framework/mcp/tools/kb/upsert.ts +125 -0
- package/.framework/mcp/tools/observability.ts +510 -0
- package/.framework/migrations/001_core_schema.sql +15 -0
- package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
- package/.framework/migrations/003_core_system_data.sql +16 -1
- package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
- package/.framework/migrations/005_core_observability.sql +17 -1
- package/.framework/migrations/006_security_hardening_rls.sql +15 -0
- package/.framework/migrations/007_kb_article_list.sql +17 -1
- package/.framework/migrations/008_items_target.sql +15 -0
- package/.framework/migrations/009_roles_slug_unique.sql +15 -0
- package/.framework/migrations/010_auth_user_trigger.sql +15 -0
- package/.framework/migrations/011_spine_system_account.sql +16 -0
- package/.framework/migrations/012_service_role_grants.sql +16 -0
- package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
- package/.framework/migrations/014_apps_required_roles.sql +16 -0
- package/.framework/migrations/015_types_kind_slug_unique.sql +15 -0
- package/.framework/migrations/016_cil_types.sql +281 -0
- package/.framework/migrations/017_cil_stable_ids.sql +37 -0
- package/.framework/migrations/018_runtime_tables.sql +129 -0
- package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
- package/.framework/migrations/020_schema_reconciliation.sql +67 -0
- package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
- package/.framework/migrations/032_cil_status_migration.sql +73 -0
- package/.framework/migrations/033_cil_versioning.sql +73 -0
- package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
- package/.framework/migrations/035_rate_limits.sql +55 -0
- package/.framework/migrations/036_demo_record_flags.sql +72 -0
- package/.framework/migrations/037_kb_types_canonical.sql +244 -0
- package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
- package/.framework/migrations/042_registration_hierarchy.sql +309 -0
- package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
- package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
- package/.framework/migrations/045_security_critical_rls.sql +139 -0
- package/.framework/migrations/046_security_missing_rls.sql +79 -0
- package/.framework/migrations/047_people_updated_by.sql +27 -0
- package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
- package/.framework/migrations/049_security_blocklist.sql +148 -0
- package/.framework/migrations/050_notifications.sql +49 -0
- package/.framework/migrations/051_message_agent_fields.sql +74 -0
- package/.framework/migrations/052_registration_security.sql +421 -0
- package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
- package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
- package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
- package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
- package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
- package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
- package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
- package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
- package/.framework/migrations/061_fk_dedup.sql +32 -0
- package/.framework/public/spine-logo.svg +10 -3
- package/.framework/scripts/app-install-cli.ts +190 -93
- package/.framework/scripts/assemble-frontend.sh +127 -44
- package/.framework/scripts/assemble-functions.sh +128 -30
- package/.framework/scripts/boundary-check.sh +6 -2
- package/.framework/scripts/build-manifest.sh +24 -68
- package/.framework/scripts/check-core-integrity.sh +15 -76
- package/.framework/scripts/checksum-manifest.mts +343 -0
- package/.framework/scripts/cil-migrate-markers.ts +403 -0
- package/.framework/scripts/fix-cil-markers.mjs +176 -0
- package/.framework/scripts/verify-integrity.sh +21 -57
- package/.framework/scripts/verify-manifest.mts +305 -0
- package/.framework/seeds/types/funnel_signal_use.json +74 -21
- package/.framework/seeds/types/spine_instance.json +73 -21
- package/.framework/src/App.tsx +118 -72
- package/.framework/src/apps/admin/index.tsx +855 -58
- package/.framework/src/components/AppWrapper.tsx +43 -45
- package/.framework/src/components/CustomAppLoader.tsx +160 -116
- package/.framework/src/components/admin/AdminListPage.tsx +46 -44
- package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
- package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
- package/.framework/src/components/admin/PageInstructions.tsx +98 -2
- package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
- package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
- package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
- package/.framework/src/components/app-sidebar.tsx +42 -45
- package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
- package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
- package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
- package/.framework/src/components/kb/KBArticleList.tsx +251 -10
- package/.framework/src/components/layout/AppShell.tsx +51 -45
- package/.framework/src/components/layout/Header.tsx +42 -43
- package/.framework/src/components/layout/Layout.tsx +46 -46
- package/.framework/src/components/layout/Sidebar.tsx +196 -96
- package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
- package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
- package/.framework/src/components/runtime/DataFilters.tsx +49 -74
- package/.framework/src/components/runtime/DataHeader.tsx +53 -32
- package/.framework/src/components/runtime/DataListPage.tsx +51 -155
- package/.framework/src/components/runtime/DataStats.tsx +66 -44
- package/.framework/src/components/runtime/DataTable.tsx +49 -149
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
- package/.framework/src/components/runtime/index.ts +119 -16
- package/.framework/src/components/search-form.tsx +46 -29
- package/.framework/src/components/shared/AgentView.tsx +65 -48
- package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
- package/.framework/src/components/shared/InviteManager.tsx +207 -97
- package/.framework/src/components/shared/MemberList.tsx +97 -0
- package/.framework/src/components/shared/SchemaFields.tsx +107 -86
- package/.framework/src/components/ui/DataTable.tsx +80 -68
- package/.framework/src/components/ui/Form.tsx +157 -146
- package/.framework/src/components/ui/ItemCard.tsx +74 -67
- package/.framework/src/components/ui/ItemListView.tsx +73 -68
- package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
- package/.framework/src/components/ui/Modal.tsx +78 -68
- package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
- package/.framework/src/components/ui/accordion.tsx +85 -86
- package/.framework/src/components/ui/alert-dialog.tsx +197 -184
- package/.framework/src/components/ui/alert.tsx +105 -94
- package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
- package/.framework/src/components/ui/avatar.tsx +122 -107
- package/.framework/src/components/ui/badge.tsx +60 -55
- package/.framework/src/components/ui/breadcrumb.tsx +127 -128
- package/.framework/src/components/ui/button-group.tsx +86 -72
- package/.framework/src/components/ui/button.tsx +71 -50
- package/.framework/src/components/ui/calendar.tsx +72 -57
- package/.framework/src/components/ui/card.tsx +136 -129
- package/.framework/src/components/ui/carousel.tsx +226 -107
- package/.framework/src/components/ui/chart.tsx +252 -147
- package/.framework/src/components/ui/checkbox.tsx +43 -44
- package/.framework/src/components/ui/collapsible.tsx +86 -71
- package/.framework/src/components/ui/combobox.tsx +282 -249
- package/.framework/src/components/ui/command.tsx +158 -146
- package/.framework/src/components/ui/context-menu.tsx +239 -240
- package/.framework/src/components/ui/dialog.tsx +174 -157
- package/.framework/src/components/ui/direction.tsx +72 -56
- package/.framework/src/components/ui/drawer.tsx +168 -169
- package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
- package/.framework/src/components/ui/empty.tsx +131 -120
- package/.framework/src/components/ui/field.tsx +205 -173
- package/.framework/src/components/ui/hover-card.tsx +71 -72
- package/.framework/src/components/ui/input-group.tsx +148 -133
- package/.framework/src/components/ui/input-otp.tsx +99 -86
- package/.framework/src/components/ui/input.tsx +43 -41
- package/.framework/src/components/ui/item.tsx +207 -185
- package/.framework/src/components/ui/kbd.tsx +57 -56
- package/.framework/src/components/ui/label.tsx +45 -41
- package/.framework/src/components/ui/menubar.tsx +268 -236
- package/.framework/src/components/ui/native-select.tsx +85 -86
- package/.framework/src/components/ui/navigation-menu.tsx +166 -156
- package/.framework/src/components/ui/pagination.tsx +131 -141
- package/.framework/src/components/ui/popover.tsx +125 -128
- package/.framework/src/components/ui/progress.tsx +47 -44
- package/.framework/src/components/ui/radio-group.tsx +60 -55
- package/.framework/src/components/ui/resizable.tsx +85 -72
- package/.framework/src/components/ui/scroll-area.tsx +57 -58
- package/.framework/src/components/ui/select.tsx +182 -169
- package/.framework/src/components/ui/separator.tsx +42 -43
- package/.framework/src/components/ui/sheet.tsx +170 -159
- package/.framework/src/components/ui/sidebar.tsx +395 -398
- package/.framework/src/components/ui/skeleton.tsx +42 -43
- package/.framework/src/components/ui/slider.tsx +61 -44
- package/.framework/src/components/ui/sonner.tsx +44 -44
- package/.framework/src/components/ui/spinner.tsx +47 -42
- package/.framework/src/components/ui/switch.tsx +56 -42
- package/.framework/src/components/ui/table-primitives.tsx +272 -132
- package/.framework/src/components/ui/table.tsx +140 -142
- package/.framework/src/components/ui/tabs.tsx +110 -93
- package/.framework/src/components/ui/textarea.tsx +43 -44
- package/.framework/src/components/ui/toggle-group.tsx +87 -71
- package/.framework/src/components/ui/toggle.tsx +56 -56
- package/.framework/src/components/ui/tooltip.tsx +99 -85
- package/.framework/src/contexts/AppContext.tsx +166 -114
- package/.framework/src/contexts/AuthContext.tsx +232 -137
- package/.framework/src/hooks/use-mobile.ts +43 -44
- package/.framework/src/hooks/useApi.ts +185 -172
- package/.framework/src/hooks/useAppPath.ts +31 -29
- package/.framework/src/hooks/useApps.ts +77 -42
- package/.framework/src/hooks/useEntityList.ts +66 -29
- package/.framework/src/hooks/useEntityRecord.ts +76 -32
- package/.framework/src/hooks/useForm.ts +62 -30
- package/.framework/src/hooks/useListSchema.ts +60 -43
- package/.framework/src/hooks/useSchemaRecord.ts +115 -95
- package/.framework/src/lib/api.ts +258 -134
- package/.framework/src/lib/registration.ts +181 -0
- package/.framework/src/lib/supabase.ts +118 -81
- package/.framework/src/lib/utils.ts +266 -266
- package/.framework/src/main.tsx +43 -44
- package/.framework/src/pages/DashboardPage.tsx +44 -43
- package/.framework/src/pages/NoAppPage.tsx +50 -0
- package/.framework/src/pages/NotFoundPage.tsx +32 -29
- package/.framework/src/pages/SetupPage.tsx +34 -33
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
- package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
- package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
- package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
- package/.framework/src/pages/admin/AppsPage.tsx +45 -42
- package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
- package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
- package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
- package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
- package/.framework/src/pages/admin/LogsPage.tsx +87 -137
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
- package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
- package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
- package/.framework/src/pages/admin/RolesPage.tsx +47 -164
- package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
- package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
- package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
- package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
- package/.framework/src/pages/admin/TimersPage.tsx +44 -42
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
- package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
- package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
- package/.framework/src/pages/admin/TypesPage.tsx +45 -42
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
- package/.framework/src/pages/auth/LoginPage.tsx +38 -32
- package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
- package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
- package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
- package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
- package/.framework/src/types/auth.ts +101 -93
- package/.framework/src/types/types.ts +283 -262
- package/.framework/templates/project.json +87 -0
- package/.framework/templates/spine.config.neutral.json +7 -0
- package/.framework/tests/api/api-surface.test.ts +381 -0
- package/.framework/tests/fixtures/seed.ts +427 -0
- package/.framework/tests/fixtures/teardown.ts +228 -0
- package/.framework/tests/reporter.ts +351 -0
- package/.framework/tests/setup.ts +135 -0
- package/.framework/tests/ui/ui-sweep.ts +278 -0
- package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
- package/.framework/tests/unit/agent-runner.test.ts +2389 -0
- package/.framework/tests/unit/ai-interactions.test.ts +81 -0
- package/.framework/tests/unit/app-manifest.test.ts +359 -0
- package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
- package/.framework/tests/unit/cil-determinism.test.ts +85 -0
- package/.framework/tests/unit/core-isolation.test.ts +153 -0
- package/.framework/tests/unit/funnel-signals.test.ts +135 -0
- package/.framework/tests/unit/install-app.test.ts +1759 -0
- package/.framework/tests/unit/invites-security.test.ts +572 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
- package/.framework/tests/unit/kb-search.test.ts +119 -0
- package/.framework/tests/unit/migrate.test.ts +509 -0
- package/.framework/tests/unit/notify.test.ts +136 -0
- package/.framework/tests/unit/permissions.test.ts +269 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
- package/.framework/tests/unit/principal-cache.test.ts +138 -0
- package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
- package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
- package/.framework/tests/unit/schema-utils.test.ts +225 -0
- package/.framework/tests/unit/trigger-engine.test.ts +513 -0
- package/.framework/tests/unit/validate-app.test.ts +527 -0
- package/README.md +34 -3
- package/bin/spine-framework.cjs +1 -0
- package/config/tailwind.config.cjs +7 -0
- package/config/tsconfig.functions.json +42 -0
- package/config/tsconfig.json +37 -9
- package/config/vite.config.ts +31 -2
- package/netlify.toml +31 -3
- package/package.json +46 -29
- package/scripts/assemble-frontend.sh +127 -44
- package/scripts/assemble-functions.sh +127 -29
- package/scripts/boundary-check.sh +6 -2
- package/scripts/build-manifest.sh +24 -68
- package/scripts/checksum-manifest.mts +345 -0
- package/scripts/dev-clean.sh +86 -0
- package/scripts/dev-netlify.sh +49 -0
- package/scripts/netlify-dev-wrapper.sh +2 -0
- package/scripts/verify-install.ts +358 -0
- package/scripts/verify-integrity.sh +21 -57
- package/scripts/verify-manifest.mts +305 -0
- package/templates/project/gitignore +45 -0
- package/{config → templates/project}/package-lock.json +22335 -9366
- package/vitest.config.ts +46 -0
- package/.framework/public/spine.config.json +0 -91
- package/config/deno.lock +0 -108
- package/config/tsconfig.node.tsbuildinfo +0 -1
- package/config/tsconfig.tsbuildinfo +0 -1
- package/config/vite.config.d.ts +0 -2
- package/custom/.devin/AGENT.md +0 -357
- package/custom/.devin/skills/install-app/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
- package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
- package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
- package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
- package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
- package/custom/.devin/workflows/app-install.yaml +0 -83
- package/custom/README.md +0 -79
- package/docs/README.md +0 -15
- package/scripts/app-install-cli.ts +0 -286
- package/scripts/check-core-integrity.sh +0 -82
- package/scripts/ingest-chunks.cjs +0 -202
- package/scripts/kb-chunk-parser.cjs +0 -312
- package/scripts/kb-chunk-parser.ts +0 -330
- package/scripts/kb-ingest-docs.ts +0 -203
- package/scripts/load-test-app-install.ts +0 -484
- package/scripts/test-login.js +0 -33
- 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"]
|