ui-syncup 0.3.13 → 0.4.0-beta.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/.agents/skills/ai-spec-workflow/SKILL.md +58 -0
- package/.agents/skills/ai-spec-workflow/references/AI_SPECIFICATION_WORKFLOW.md +1434 -0
- package/.agents/skills/ai-spec-workflow/references/templates/design-template.md +729 -0
- package/.agents/skills/ai-spec-workflow/references/templates/requirements-template.md +179 -0
- package/.agents/skills/ai-spec-workflow/references/templates/tasks-template.md +501 -0
- package/.agents/skills/animation-designer/SKILL.md +688 -0
- package/.agents/skills/animation-designer/manifest.yaml +44 -0
- package/.agents/skills/brainstorming/SKILL.md +54 -0
- package/.agents/skills/contract-driven-ui/SKILL.md +270 -0
- package/.agents/skills/dispatching-parallel-agents/SKILL.md +180 -0
- package/.agents/skills/executing-plans/SKILL.md +76 -0
- package/.agents/skills/executing-specs/SKILL.md +53 -0
- package/.agents/skills/finishing-a-development-branch/SKILL.md +200 -0
- package/.agents/skills/github-workflow-automation/SKILL.md +846 -0
- package/.agents/skills/react-best-practices/AGENTS.md +2249 -0
- package/.agents/skills/react-best-practices/README.md +123 -0
- package/.agents/skills/react-best-practices/SKILL.md +121 -0
- package/.agents/skills/react-best-practices/metadata.json +15 -0
- package/.agents/skills/react-best-practices/rules/_sections.md +46 -0
- package/.agents/skills/react-best-practices/rules/_template.md +28 -0
- package/.agents/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.agents/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/.agents/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/.agents/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/react-best-practices/rules/server-cache-react.md +26 -0
- package/.agents/skills/react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/.agents/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/.agents/skills/react-ui-patterns/SKILL.md +289 -0
- package/.agents/skills/receiving-code-review/SKILL.md +213 -0
- package/.agents/skills/requesting-code-review/SKILL.md +105 -0
- package/.agents/skills/requesting-code-review/code-reviewer.md +146 -0
- package/.agents/skills/reviewing-code/SKILL.md +28 -0
- package/.agents/skills/shadcn/SKILL.md +240 -0
- package/.agents/skills/shadcn/agents/openai.yml +5 -0
- package/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/.agents/skills/shadcn/cli.md +255 -0
- package/.agents/skills/shadcn/customization.md +202 -0
- package/.agents/skills/shadcn/evals/evals.json +47 -0
- package/.agents/skills/shadcn/mcp.md +94 -0
- package/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/.agents/skills/shadcn/rules/composition.md +195 -0
- package/.agents/skills/shadcn/rules/forms.md +192 -0
- package/.agents/skills/shadcn/rules/icons.md +101 -0
- package/.agents/skills/shadcn/rules/styling.md +162 -0
- package/.agents/skills/steering-creation/SKILL.md +221 -0
- package/.agents/skills/steering-creation/references/STEERING_CREATION_INSTRUCTION.md +850 -0
- package/.agents/skills/subagent-driven-development/SKILL.md +240 -0
- package/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md +20 -0
- package/.agents/skills/subagent-driven-development/implementer-prompt.md +78 -0
- package/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/.agents/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/.agents/skills/systematic-debugging/SKILL.md +296 -0
- package/.agents/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/.agents/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/.agents/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/.agents/skills/systematic-debugging/find-polluter.sh +63 -0
- package/.agents/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/.agents/skills/systematic-debugging/test-academic.md +14 -0
- package/.agents/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/.agents/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/.agents/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/.agents/skills/test-driven-development/SKILL.md +371 -0
- package/.agents/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/.agents/skills/using-git-worktrees/SKILL.md +217 -0
- package/.agents/skills/using-superpowers/SKILL.md +87 -0
- package/.agents/skills/verification-before-completion/SKILL.md +139 -0
- package/.agents/skills/web-design-guidelines/SKILL.md +36 -0
- package/.agents/skills/writing-plans/SKILL.md +116 -0
- package/.agents/skills/writing-skills/SKILL.md +655 -0
- package/.agents/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/.agents/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/.agents/skills/writing-skills/persuasion-principles.md +187 -0
- package/.agents/skills/writing-skills/render-graphs.js +168 -0
- package/.agents/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/.ai/steering/product.md +51 -0
- package/.ai/steering/structure.md +275 -0
- package/.ai/steering/tech.md +188 -0
- package/.claude/agents/database-architect.md +96 -0
- package/.claude/agents/deployment-pipeline-architect.md +122 -0
- package/.claude/agents/nextjs-expert.md +69 -0
- package/.claude/agents/ui-design-expert.md +106 -0
- package/.claudeignore +69 -0
- package/.dockerignore +8 -0
- package/.env.development +86 -0
- package/.env.example +171 -0
- package/.env.production +139 -0
- package/.env.test +58 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +20 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +23 -0
- package/.github/workflows/ci.yml +64 -0
- package/.github/workflows/release.yml +131 -0
- package/.nvmrc +1 -0
- package/.releaserc.json +18 -0
- package/.vercelignore +73 -0
- package/AGENTS.md +544 -0
- package/CHANGELOG.md +37 -0
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +32 -0
- package/Dockerfile +84 -0
- package/LICENSE +21 -0
- package/README.md +328 -59
- package/SECURITY.md +16 -0
- package/bun.lock +3853 -0
- package/cli/README.md +94 -0
- package/cli/bun.lock +306 -0
- package/cli/index.ts +96 -0
- package/cli/package-lock.json +2157 -0
- package/cli/package.json +30 -0
- package/cli/src/commands/backup.ts +78 -0
- package/cli/src/commands/doctor.ts +82 -0
- package/cli/src/commands/init.ts +234 -0
- package/cli/src/commands/logs.ts +26 -0
- package/cli/src/commands/open.ts +23 -0
- package/cli/src/commands/remove.ts +44 -0
- package/cli/src/commands/restart.ts +21 -0
- package/cli/src/commands/restore.ts +90 -0
- package/cli/src/commands/start.ts +26 -0
- package/cli/src/commands/status.ts +25 -0
- package/cli/src/commands/stop.ts +20 -0
- package/cli/src/commands/upgrade.ts +28 -0
- package/cli/src/lib/docker.ts +40 -0
- package/cli/src/lib/env.ts +42 -0
- package/cli/src/lib/ui.ts +43 -0
- package/cli/tsconfig.json +13 -0
- package/cli/tsup.config.ts +12 -0
- package/components.json +24 -0
- package/docker/README.md +430 -0
- package/docker/compose.dev-minio.yml +30 -0
- package/docker/compose.dev.yml +39 -0
- package/docker/compose.local.yml +84 -0
- package/docker/compose.yml +153 -0
- package/docs/VERSIONING.md +117 -0
- package/docs/database/DRIZZLE_COMMANDS_EXPLAINED.md +1779 -0
- package/docs/database/DRIZZLE_ZOD_POSTGRESQL_INSTRUCTION.md +646 -0
- package/docs/database/MIGRATION_BEST_PRACTICES.md +601 -0
- package/docs/database/MIGRATION_ROLLBACK.md +1080 -0
- package/docs/database/MIGRATION_SYSTEM.md +165 -0
- package/docs/database/MIGRATION_TROUBLESHOOTING.md +881 -0
- package/docs/development/ENVIRONMENT_CONFIG.md +896 -0
- package/docs/development/LOCAL_DEVELOPMENT.md +456 -0
- package/docs/development/REMOTE_DATABASE_SETUP.md +786 -0
- package/docs/development/STORAGE_SETUP.md +207 -0
- package/docs/development/SUPABASE_LOCAL_SETUP.md +178 -0
- package/docs/development/TESTING.md +714 -0
- package/docs/feature-architectures/LOADING_ARCHITECTURE.md +343 -0
- package/docs/feature-architectures/NOTIFICATION_ARCHITECTURE.md +858 -0
- package/docs/feature-architectures/RATE_LIMIT_RESET.md +147 -0
- package/docs/feature-architectures/RBAC.md +1132 -0
- package/docs/feature-architectures/RESOURCE_LIMITS.md +69 -0
- package/docs/feature-architectures/SECURITY.md +284 -0
- package/docs/feature-architectures/WORKSPACES.md +278 -0
- package/docs/plans/admin-setup-wizard-routing-plan.md +623 -0
- package/drizzle/0000_purple_wilson_fisk.sql +360 -0
- package/drizzle/0001_drop_instance_public_url.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +3118 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +13 -0
- package/eslint.config.mjs +44 -0
- package/install.sh +180 -0
- package/next.config.ts +91 -0
- package/package.json +128 -22
- package/playwright.config.ts +70 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +11 -0
- package/public/next.svg +1 -0
- package/public/playground/CPM-101/as-is-image.jpg +0 -0
- package/public/playground/CPM-101/to-be-image.jpg +0 -0
- package/public/playground/TEST-1/LinkedIn-skeleton-screen.png +0 -0
- package/public/playground/TEST-1/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_vuahe90ka1mkx9aepmea.webp +0 -0
- package/public/playground/TEST-1/linkedin_skeletonscreen.jpg +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/__tests__/migrate.integration.test.ts +642 -0
- package/scripts/__tests__/migrate.property.test.ts +1714 -0
- package/scripts/__tests__/migrate.test.ts +536 -0
- package/scripts/admin-reset-password.ts +114 -0
- package/scripts/check-email-queue.ts +99 -0
- package/scripts/check-sessions.ts +50 -0
- package/scripts/db-pull-data.sh +73 -0
- package/scripts/force-verify-email.sh +13 -0
- package/scripts/migrate.ts +693 -0
- package/scripts/process-email-queue.ts +26 -0
- package/scripts/reset-db.ts +47 -0
- package/scripts/reset-rate-limit.sh +26 -0
- package/scripts/reset-remote-db.sql +31 -0
- package/scripts/retry-failed-emails.ts +67 -0
- package/scripts/seed.ts +605 -0
- package/scripts/setup-monitoring.sh +440 -0
- package/scripts/sync-migration-tracking.ts +113 -0
- package/scripts/test-ci-error-handling.sh +237 -0
- package/scripts/test-ci-workflow.sh +200 -0
- package/scripts/test-migration.sh +151 -0
- package/scripts/validate-env.ts +25 -0
- package/scripts/validate-migration-system.ts +566 -0
- package/scripts/verify-ci-status-reporting.sh +206 -0
- package/scripts/verify-user-email.sql +22 -0
- package/scripts/verify-vercel-integration.ts +292 -0
- package/seed_data.md +54 -0
- package/src/app/(protected)/(team)/(routes)/[projectSlug]/error.tsx +89 -0
- package/src/app/(protected)/(team)/(routes)/[projectSlug]/loading.tsx +101 -0
- package/src/app/(protected)/(team)/(routes)/[projectSlug]/page.tsx +91 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/README.md +192 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/error.tsx +58 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/loading.tsx +14 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/not-found.tsx +47 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/page.tsx +91 -0
- package/src/app/(protected)/(team)/projects/page.tsx +16 -0
- package/src/app/(protected)/(team)/team/settings/(section)/instance/page.tsx +52 -0
- package/src/app/(protected)/(team)/team/settings/(section)/integrations/loading.tsx +5 -0
- package/src/app/(protected)/(team)/team/settings/(section)/integrations/page.tsx +23 -0
- package/src/app/(protected)/(team)/team/settings/(section)/members/loading.tsx +5 -0
- package/src/app/(protected)/(team)/team/settings/(section)/members/page.tsx +35 -0
- package/src/app/(protected)/(team)/team/settings/layout.tsx +72 -0
- package/src/app/(protected)/(team)/team/settings/loading.tsx +5 -0
- package/src/app/(protected)/(team)/team/settings/page.tsx +71 -0
- package/src/app/(protected)/dev/auth/README.md +151 -0
- package/src/app/(protected)/dev/auth/page.tsx +590 -0
- package/src/app/(protected)/layout.test.tsx +209 -0
- package/src/app/(protected)/layout.tsx +28 -0
- package/src/app/(protected)/onboarding/page.tsx +27 -0
- package/src/app/(protected)/settings/integrations/page.tsx +23 -0
- package/src/app/(protected)/settings/layout.tsx +26 -0
- package/src/app/(protected)/settings/notifications/page.tsx +26 -0
- package/src/app/(protected)/settings/other/page.tsx +23 -0
- package/src/app/(protected)/settings/page.tsx +23 -0
- package/src/app/(protected)/settings/preferences/page.tsx +23 -0
- package/src/app/(protected)/settings/security/page.tsx +37 -0
- package/src/app/(public)/forgot-password/page.tsx +20 -0
- package/src/app/(public)/invite/project/[token]/error.tsx +50 -0
- package/src/app/(public)/invite/project/[token]/loading.tsx +39 -0
- package/src/app/(public)/invite/project/[token]/page.tsx +156 -0
- package/src/app/(public)/layout.tsx +9 -0
- package/src/app/(public)/privacy-policy/page.tsx +12 -0
- package/src/app/(public)/reset-password/page.tsx +37 -0
- package/src/app/(public)/setup/__tests__/page.test.tsx +30 -0
- package/src/app/(public)/setup/page.tsx +17 -0
- package/src/app/(public)/share/issue/[token]/page.tsx +51 -0
- package/src/app/(public)/sign-in/page.tsx +55 -0
- package/src/app/(public)/sign-up/page.tsx +23 -0
- package/src/app/(public)/verify-email/page.tsx +22 -0
- package/src/app/(public)/verify-email-confirm/page.tsx +40 -0
- package/src/app/api/auth/[...all]/route.ts +6 -0
- package/src/app/api/auth/delete-account/route.ts +134 -0
- package/src/app/api/auth/dev/force-verify/route.ts +180 -0
- package/src/app/api/auth/dev/reset-rate-limit/route.ts +144 -0
- package/src/app/api/auth/dev/sessions/route.ts +172 -0
- package/src/app/api/auth/forgot-password/__tests__/forgot-password.property.test.ts +397 -0
- package/src/app/api/auth/forgot-password/route.ts +277 -0
- package/src/app/api/auth/logout/route.ts +115 -0
- package/src/app/api/auth/me/route.ts +123 -0
- package/src/app/api/auth/providers/__tests__/route.test.ts +236 -0
- package/src/app/api/auth/providers/route.ts +119 -0
- package/src/app/api/auth/resend-verification/route.ts +262 -0
- package/src/app/api/auth/reset-password/__tests__/reset-password.property.test.ts +493 -0
- package/src/app/api/auth/reset-password/__tests__/route.test.ts +284 -0
- package/src/app/api/auth/reset-password/route.ts +251 -0
- package/src/app/api/auth/verify-email/route.ts +232 -0
- package/src/app/api/example-cors/route.ts +61 -0
- package/src/app/api/health/route.ts +14 -0
- package/src/app/api/invite/project/[token]/__tests__/accept-invitation.integration.test.ts +348 -0
- package/src/app/api/invite/project/[token]/decline/route.ts +99 -0
- package/src/app/api/invite/project/[token]/route.ts +269 -0
- package/src/app/api/issues/[issueId]/activities/route.ts +213 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/comments/[commentId]/route.ts +486 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/comments/route.ts +283 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/read/route.ts +242 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/route.ts +534 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/route.ts +514 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/route.ts +161 -0
- package/src/app/api/issues/[issueId]/attachments/route.ts +376 -0
- package/src/app/api/issues/[issueId]/route.ts +516 -0
- package/src/app/api/notifications/[id]/read/route.ts +131 -0
- package/src/app/api/notifications/__tests__/notifications.integration.test.ts +350 -0
- package/src/app/api/notifications/read-all/route.ts +72 -0
- package/src/app/api/notifications/route.ts +148 -0
- package/src/app/api/notifications/unread-count/route.ts +77 -0
- package/src/app/api/projects/[id]/activities/route.ts +174 -0
- package/src/app/api/projects/[id]/invitations/[invitationId]/resend/route.ts +99 -0
- package/src/app/api/projects/[id]/invitations/[invitationId]/route.ts +96 -0
- package/src/app/api/projects/[id]/invitations/route.ts +254 -0
- package/src/app/api/projects/[id]/issues/route.ts +452 -0
- package/src/app/api/projects/[id]/join/route.ts +207 -0
- package/src/app/api/projects/[id]/members/[memberId]/route.ts +364 -0
- package/src/app/api/projects/[id]/members/me/route.ts +121 -0
- package/src/app/api/projects/[id]/members/route.ts +129 -0
- package/src/app/api/projects/[id]/route.ts +476 -0
- package/src/app/api/projects/route.ts +394 -0
- package/src/app/api/setup/admin/route.ts +255 -0
- package/src/app/api/setup/complete/__tests__/route.test.ts +60 -0
- package/src/app/api/setup/complete/route.ts +244 -0
- package/src/app/api/setup/config/route.ts +195 -0
- package/src/app/api/setup/export/route.ts +111 -0
- package/src/app/api/setup/health/route.ts +74 -0
- package/src/app/api/setup/import/route.ts +154 -0
- package/src/app/api/setup/status/route.ts +82 -0
- package/src/app/api/setup/workspace/route.ts +252 -0
- package/src/app/api/teams/[teamId]/export/route.ts +115 -0
- package/src/app/api/teams/[teamId]/invitations/[invitationId]/resend/route.ts +132 -0
- package/src/app/api/teams/[teamId]/invitations/[invitationId]/route.ts +117 -0
- package/src/app/api/teams/[teamId]/invitations/route.ts +363 -0
- package/src/app/api/teams/[teamId]/members/[userId]/route.ts +335 -0
- package/src/app/api/teams/[teamId]/members/route.ts +184 -0
- package/src/app/api/teams/[teamId]/members/search/route.ts +202 -0
- package/src/app/api/teams/[teamId]/route.ts +423 -0
- package/src/app/api/teams/[teamId]/switch/route.ts +140 -0
- package/src/app/api/teams/[teamId]/transfer-ownership/route.ts +212 -0
- package/src/app/api/teams/invitations/[token]/accept/route.ts +140 -0
- package/src/app/api/teams/invitations/by-id/[id]/accept/route.ts +98 -0
- package/src/app/api/teams/invitations/by-id/[id]/decline/route.ts +90 -0
- package/src/app/api/teams/route.ts +278 -0
- package/src/app/api/uploads/media/route.ts +118 -0
- package/src/app/api/uploads/presigned/route.ts +49 -0
- package/src/app/api/user/linked-accounts/route.ts +35 -0
- package/src/app/email-preview/page.tsx +11 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/global-error.tsx +21 -0
- package/src/app/layout.tsx +50 -0
- package/src/app/page.tsx +5 -0
- package/src/components/icons/atlassian-icon.tsx +22 -0
- package/src/components/icons/index.ts +1 -0
- package/src/components/layout/SIDEBAR_LAYOUT_BEST_PRACTICES.md +240 -0
- package/src/components/layout/app-shell-header-store.tsx +20 -0
- package/src/components/layout/app-shell-skeleton.tsx +89 -0
- package/src/components/layout/app-shell-wrapper.tsx +32 -0
- package/src/components/layout/app-shell.test.tsx +155 -0
- package/src/components/layout/app-shell.tsx +100 -0
- package/src/components/shared/headers/app-header-configurator.tsx +42 -0
- package/src/components/shared/headers/app-header.tsx +103 -0
- package/src/components/shared/headers/header-user-menu.tsx +247 -0
- package/src/components/shared/headers/index.ts +44 -0
- package/src/components/shared/headers/page-header.tsx +25 -0
- package/src/components/shared/notifications/__tests__/notification-bell.test.tsx +159 -0
- package/src/components/shared/notifications/__tests__/notification-dropdown.test.tsx +296 -0
- package/src/components/shared/notifications/__tests__/notification-item.test.tsx +328 -0
- package/src/components/shared/notifications/index.ts +45 -0
- package/src/components/shared/notifications/notification-actions.tsx +295 -0
- package/src/components/shared/notifications/notification-bell-button.tsx +77 -0
- package/src/components/shared/notifications/notification-dropdown.tsx +160 -0
- package/src/components/shared/notifications/notification-group-item.tsx +268 -0
- package/src/components/shared/notifications/notification-item.tsx +193 -0
- package/src/components/shared/notifications/notification-load-more.tsx +50 -0
- package/src/components/shared/notifications/notification-panel.tsx +49 -0
- package/src/components/shared/notifications/utils.tsx +127 -0
- package/src/components/shared/permission-guard/index.ts +1 -0
- package/src/components/shared/permission-guard/permission-tooltip.tsx +45 -0
- package/src/components/shared/relative-time.tsx +53 -0
- package/src/components/shared/section-container.tsx +32 -0
- package/src/components/shared/service-status-banner.tsx +121 -0
- package/src/components/shared/settings-sidebar/index.ts +2 -0
- package/src/components/shared/settings-sidebar/team-setting-aside.tsx +97 -0
- package/src/components/shared/settings-sidebar/user-settings-aside.tsx +66 -0
- package/src/components/shared/sidebar/app-sidebar.tsx +146 -0
- package/src/components/shared/sidebar/index.ts +36 -0
- package/src/components/shared/sidebar/sidebar-main.tsx +81 -0
- package/src/components/shared/sidebar/sidebar-project.tsx +61 -0
- package/src/components/shared/sidebar/sidebar-team-avatar.tsx +126 -0
- package/src/components/shared/sidebar/sidebar-team-switcher.tsx +185 -0
- package/src/components/shared/sidebar/type.ts +97 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/avatar-upload.tsx +147 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +244 -0
- package/src/components/ui/image-cropper-dialog.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/optimized-image.tsx +220 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +733 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/config/__tests__/workspace.property.test.ts +40 -0
- package/src/config/auth.ts +62 -0
- package/src/config/integrations.ts +126 -0
- package/src/config/quotas.ts +20 -0
- package/src/config/roles.ts +463 -0
- package/src/config/settings-nav.ts +39 -0
- package/src/config/team-settings-nav.ts +37 -0
- package/src/config/user-settings-nav.ts +42 -0
- package/src/config/version.ts +1 -0
- package/src/config/workspace.ts +64 -0
- package/src/features/annotations/README.md +283 -0
- package/src/features/annotations/api/annotations-api.ts +194 -0
- package/src/features/annotations/api/comments-api.ts +147 -0
- package/src/features/annotations/api/index.ts +71 -0
- package/src/features/annotations/api/save-annotation.ts +150 -0
- package/src/features/annotations/api/schemas.ts +142 -0
- package/src/features/annotations/components/annotated-attachment-view.tsx +576 -0
- package/src/features/annotations/components/annotation-action-sheet.tsx +140 -0
- package/src/features/annotations/components/annotation-annotations-panel.tsx +213 -0
- package/src/features/annotations/components/annotation-box.tsx +539 -0
- package/src/features/annotations/components/annotation-canvas.tsx +534 -0
- package/src/features/annotations/components/annotation-comment-input.tsx +145 -0
- package/src/features/annotations/components/annotation-context-menu.tsx +164 -0
- package/src/features/annotations/components/annotation-drawer.tsx +231 -0
- package/src/features/annotations/components/annotation-layer.tsx +271 -0
- package/src/features/annotations/components/annotation-pin.tsx +318 -0
- package/src/features/annotations/components/annotation-popover.tsx +562 -0
- package/src/features/annotations/components/annotation-thread-panel.tsx +485 -0
- package/src/features/annotations/components/annotation-thread-preview.tsx +195 -0
- package/src/features/annotations/components/annotation-toolbar.tsx +244 -0
- package/src/features/annotations/components/keyboard-shortcuts-modal.tsx +79 -0
- package/src/features/annotations/docs/ANNOTATIONS_ARCHITECTURE.md +67 -0
- package/src/features/annotations/docs/ANNOTATION_SAVE_ARCHITECTURE.md +422 -0
- package/src/features/annotations/docs/ANNOTATION_SAVE_FEATURE.md +408 -0
- package/src/features/annotations/docs/BOX_ANNOTATION_GUIDE.md +542 -0
- package/src/features/annotations/docs/NEXTSTEP.md +28 -0
- package/src/features/annotations/docs/STALE_CLOSURE_FIX.md +344 -0
- package/src/features/annotations/docs/UNDO_REDO_QUICK_START.md +545 -0
- package/src/features/annotations/docs/local_first_canvas_autosave_architecture.md +674 -0
- package/src/features/annotations/examples/complete-example.tsx +266 -0
- package/src/features/annotations/examples/save-annotation-example.tsx +309 -0
- package/src/features/annotations/hooks/__tests__/use-annotation-permissions.property.test.tsx +493 -0
- package/src/features/annotations/hooks/index.ts +36 -0
- package/src/features/annotations/hooks/use-annotation-batch-save.ts +109 -0
- package/src/features/annotations/hooks/use-annotation-comments.ts +353 -0
- package/src/features/annotations/hooks/use-annotation-drafts.ts +137 -0
- package/src/features/annotations/hooks/use-annotation-edit-state.ts +99 -0
- package/src/features/annotations/hooks/use-annotation-history-tracker.ts +159 -0
- package/src/features/annotations/hooks/use-annotation-integration.ts +916 -0
- package/src/features/annotations/hooks/use-annotation-permissions.ts +210 -0
- package/src/features/annotations/hooks/use-annotation-popover.ts +175 -0
- package/src/features/annotations/hooks/use-annotation-save.ts +208 -0
- package/src/features/annotations/hooks/use-annotation-tools.ts +237 -0
- package/src/features/annotations/hooks/use-annotations-with-history.ts +332 -0
- package/src/features/annotations/hooks/use-auto-save.ts +94 -0
- package/src/features/annotations/index.ts +111 -0
- package/src/features/annotations/types/annotation.ts +201 -0
- package/src/features/annotations/types/index.ts +28 -0
- package/src/features/annotations/utils/history-manager.ts +73 -0
- package/src/features/annotations/utils/index.ts +2 -0
- package/src/features/annotations/utils/map-attachments-to-threads.ts +28 -0
- package/src/features/annotations/utils/position-comment-input.ts +136 -0
- package/src/features/annotations/utils/re-sequence-labels.ts +92 -0
- package/src/features/annotations/utils/validate-annotation-label.ts +120 -0
- package/src/features/auth/api/types.ts +101 -0
- package/src/features/auth/components/__tests__/role-gate.test.tsx +448 -0
- package/src/features/auth/components/__tests__/social-login-buttons.test.tsx +313 -0
- package/src/features/auth/components/auth-card.tsx +36 -0
- package/src/features/auth/components/forgot-password-form.tsx +115 -0
- package/src/features/auth/components/index.ts +14 -0
- package/src/features/auth/components/invite-code-input.tsx +155 -0
- package/src/features/auth/components/invited-user-form.tsx +309 -0
- package/src/features/auth/components/onboarding-form.tsx +195 -0
- package/src/features/auth/components/password-strength-indicator.tsx +113 -0
- package/src/features/auth/components/reset-password-form.tsx +138 -0
- package/src/features/auth/components/role-gate.tsx +124 -0
- package/src/features/auth/components/self-registration-choice.tsx +153 -0
- package/src/features/auth/components/sign-in-form.tsx +159 -0
- package/src/features/auth/components/sign-up-form.tsx +158 -0
- package/src/features/auth/components/social-login-buttons.tsx +219 -0
- package/src/features/auth/hooks/__tests__/use-onboarding.test.tsx +109 -0
- package/src/features/auth/hooks/__tests__/use-session.test.tsx +160 -0
- package/src/features/auth/hooks/index.ts +15 -0
- package/src/features/auth/hooks/use-accept-invitation.ts +194 -0
- package/src/features/auth/hooks/use-delete-account.ts +86 -0
- package/src/features/auth/hooks/use-force-verify.ts +89 -0
- package/src/features/auth/hooks/use-forgot-password.ts +144 -0
- package/src/features/auth/hooks/use-link-account.ts +78 -0
- package/src/features/auth/hooks/use-linked-accounts.ts +88 -0
- package/src/features/auth/hooks/use-onboarding.ts +159 -0
- package/src/features/auth/hooks/use-resend-verification.ts +139 -0
- package/src/features/auth/hooks/use-reset-password.ts +151 -0
- package/src/features/auth/hooks/use-reset-rate-limit.ts +56 -0
- package/src/features/auth/hooks/use-self-registration.ts +202 -0
- package/src/features/auth/hooks/use-session.ts +81 -0
- package/src/features/auth/hooks/use-sessions.ts +59 -0
- package/src/features/auth/hooks/use-sign-in.ts +234 -0
- package/src/features/auth/hooks/use-sign-out.ts +88 -0
- package/src/features/auth/hooks/use-sign-up.ts +194 -0
- package/src/features/auth/hooks/use-unlink-account.ts +100 -0
- package/src/features/auth/hooks/use-verify-email-token.ts +125 -0
- package/src/features/auth/index.ts +75 -0
- package/src/features/auth/screens/forgot-password-screen.tsx +33 -0
- package/src/features/auth/screens/index.ts +7 -0
- package/src/features/auth/screens/onboarding-screen.tsx +49 -0
- package/src/features/auth/screens/reset-password-screen.tsx +33 -0
- package/src/features/auth/screens/sign-in-screen.tsx +61 -0
- package/src/features/auth/screens/sign-up-screen.tsx +37 -0
- package/src/features/auth/screens/verify-email-confirm-screen.tsx +286 -0
- package/src/features/auth/screens/verify-email-screen.tsx +146 -0
- package/src/features/auth/types/index.ts +14 -0
- package/src/features/auth/utils/__tests__/validators.test.ts +331 -0
- package/src/features/auth/utils/password-strength.ts +129 -0
- package/src/features/auth/utils/validators.ts +124 -0
- package/src/features/email-preview/actions/render-email.ts +21 -0
- package/src/features/email-preview/screens/email-preview-screen.tsx +81 -0
- package/src/features/folder-scaffold-template/index.ts +0 -0
- package/src/features/instance-settings/components/index.ts +6 -0
- package/src/features/instance-settings/components/instance-settings-form.tsx +180 -0
- package/src/features/instance-settings/components/instance-status-display.tsx +158 -0
- package/src/features/instance-settings/index.ts +7 -0
- package/src/features/instance-settings/screens/index.ts +5 -0
- package/src/features/instance-settings/screens/instance-settings-screen.tsx +59 -0
- package/src/features/issues/README.md +330 -0
- package/src/features/issues/api/create-issue.ts +19 -0
- package/src/features/issues/api/delete-issue.ts +27 -0
- package/src/features/issues/api/get-issue-activities.ts +58 -0
- package/src/features/issues/api/get-issue-details.ts +25 -0
- package/src/features/issues/api/get-project-issues-server.ts +44 -0
- package/src/features/issues/api/get-project-issues.ts +21 -0
- package/src/features/issues/api/index.ts +44 -0
- package/src/features/issues/api/update-issue.ts +31 -0
- package/src/features/issues/api/upload-attachment.ts +81 -0
- package/src/features/issues/components/activity-timeline.tsx +440 -0
- package/src/features/issues/components/canvas-state-indicator.tsx +90 -0
- package/src/features/issues/components/centered-canvas-view.tsx +739 -0
- package/src/features/issues/components/image-selector.tsx +123 -0
- package/src/features/issues/components/image-upload-zone.tsx +262 -0
- package/src/features/issues/components/infinite-canvas-background.tsx +163 -0
- package/src/features/issues/components/inline-editable-select.tsx +173 -0
- package/src/features/issues/components/inline-editable-text.tsx +225 -0
- package/src/features/issues/components/inline-editable-textarea.tsx +219 -0
- package/src/features/issues/components/inline-editable-user-select.tsx +202 -0
- package/src/features/issues/components/issue-deletion-dialog.tsx +142 -0
- package/src/features/issues/components/issue-details-panel.tsx +101 -0
- package/src/features/issues/components/issues-create-dialog.tsx +578 -0
- package/src/features/issues/components/issues-list-filter.tsx +312 -0
- package/src/features/issues/components/issues-list.tsx +151 -0
- package/src/features/issues/components/issues-priority-badge.tsx +77 -0
- package/src/features/issues/components/issues-status-badge.tsx +100 -0
- package/src/features/issues/components/metadata-section.tsx +389 -0
- package/src/features/issues/components/optimized-attachment-view.tsx +528 -0
- package/src/features/issues/components/optimized-image.tsx +257 -0
- package/src/features/issues/components/panel-header.tsx +186 -0
- package/src/features/issues/components/preload.ts +31 -0
- package/src/features/issues/components/priority-selector.tsx +101 -0
- package/src/features/issues/components/responsive-issue-layout-skeleton.tsx +139 -0
- package/src/features/issues/components/responsive-issue-layout.tsx +617 -0
- package/src/features/issues/components/status-selector.tsx +320 -0
- package/src/features/issues/components/type-selector.tsx +102 -0
- package/src/features/issues/components/upload-progress-overlay.tsx +35 -0
- package/src/features/issues/components/uploaded-image-preview.tsx +173 -0
- package/src/features/issues/components/workflow-control.tsx +318 -0
- package/src/features/issues/components/zoom-controls.tsx +150 -0
- package/src/features/issues/config/index.ts +47 -0
- package/src/features/issues/config/options.ts +323 -0
- package/src/features/issues/config/workflow.ts +102 -0
- package/src/features/issues/docs/ARCHITECTURE_DIAGRAM.md +321 -0
- package/src/features/issues/docs/BACKEND_ARCHITECTURE.md +194 -0
- package/src/features/issues/docs/IMAGE_COMPONENTS_ARCHITECTURE.md +363 -0
- package/src/features/issues/docs/IMAGE_COMPONENTS_IMPORTS.md +412 -0
- package/src/features/issues/docs/IMPLEMENTATION_CHECKLIST.md +210 -0
- package/src/features/issues/docs/ROUTE_SETUP_COMPLETE.md +242 -0
- package/src/features/issues/hooks/index.ts +78 -0
- package/src/features/issues/hooks/use-canvas-transform.ts +255 -0
- package/src/features/issues/hooks/use-create-issue.ts +28 -0
- package/src/features/issues/hooks/use-elastic-scroll.ts +296 -0
- package/src/features/issues/hooks/use-issue-activities.ts +71 -0
- package/src/features/issues/hooks/use-issue-delete.ts +84 -0
- package/src/features/issues/hooks/use-issue-details.ts +70 -0
- package/src/features/issues/hooks/use-issue-filters.ts +50 -0
- package/src/features/issues/hooks/use-issue-update.ts +93 -0
- package/src/features/issues/hooks/use-keyboard-shortcuts.ts +104 -0
- package/src/features/issues/hooks/use-optimized-image.ts +228 -0
- package/src/features/issues/hooks/use-project-issues.ts +14 -0
- package/src/features/issues/index.ts +65 -0
- package/src/features/issues/screens/issue-details-screen.tsx +207 -0
- package/src/features/issues/screens/issue-details-skeletons.tsx +295 -0
- package/src/features/issues/screens/issue-share-screen.tsx +56 -0
- package/src/features/issues/types/index.ts +48 -0
- package/src/features/issues/types/issue.ts +291 -0
- package/src/features/issues/utils/filter-issues.ts +141 -0
- package/src/features/issues/utils/index.ts +14 -0
- package/src/features/legal/index.ts +1 -0
- package/src/features/legal/screens/privacy-policy-screen.tsx +307 -0
- package/src/features/notifications/api/get-notifications.ts +58 -0
- package/src/features/notifications/api/get-unread-count.ts +37 -0
- package/src/features/notifications/api/index.ts +35 -0
- package/src/features/notifications/api/mark-all-as-read.ts +37 -0
- package/src/features/notifications/api/mark-as-read.ts +41 -0
- package/src/features/notifications/api/types.ts +109 -0
- package/src/features/notifications/hooks/__tests__/use-notification-subscription.test.ts +206 -0
- package/src/features/notifications/hooks/index.ts +28 -0
- package/src/features/notifications/hooks/use-mark-all-as-read.ts +106 -0
- package/src/features/notifications/hooks/use-mark-as-read.ts +106 -0
- package/src/features/notifications/hooks/use-notification-subscription.ts +244 -0
- package/src/features/notifications/hooks/use-notification-toast.ts +161 -0
- package/src/features/notifications/hooks/use-notifications.ts +80 -0
- package/src/features/notifications/hooks/use-unread-count.ts +60 -0
- package/src/features/notifications/index.ts +48 -0
- package/src/features/notifications/utils/group-notifications.ts +152 -0
- package/src/features/notifications/utils/index.ts +9 -0
- package/src/features/projects/api/create-invitation.ts +45 -0
- package/src/features/projects/api/create-project.ts +64 -0
- package/src/features/projects/api/delete-project.ts +50 -0
- package/src/features/projects/api/get-project-activities.ts +43 -0
- package/src/features/projects/api/get-project-members.ts +53 -0
- package/src/features/projects/api/get-project.ts +49 -0
- package/src/features/projects/api/get-projects.ts +61 -0
- package/src/features/projects/api/index.ts +27 -0
- package/src/features/projects/api/join-project.ts +52 -0
- package/src/features/projects/api/leave-project.ts +51 -0
- package/src/features/projects/api/list-invitations.ts +36 -0
- package/src/features/projects/api/remove-member.ts +60 -0
- package/src/features/projects/api/resend-invitation.ts +36 -0
- package/src/features/projects/api/revoke-invitation.ts +36 -0
- package/src/features/projects/api/types.ts +286 -0
- package/src/features/projects/api/update-member-role.ts +70 -0
- package/src/features/projects/api/update-project.ts +69 -0
- package/src/features/projects/components/__tests__/project-detail-activity-feed.test.tsx +106 -0
- package/src/features/projects/components/__tests__/project-invitation-dialog.test.tsx +211 -0
- package/src/features/projects/components/__tests__/project-member-manager-dialog.test.tsx +254 -0
- package/src/features/projects/components/index.ts +21 -0
- package/src/features/projects/components/project-actions.tsx +248 -0
- package/src/features/projects/components/project-create-dialog.tsx +410 -0
- package/src/features/projects/components/project-detail-activity-feed.tsx +206 -0
- package/src/features/projects/components/project-detail-header.tsx +103 -0
- package/src/features/projects/components/project-detail-overview.tsx +128 -0
- package/src/features/projects/components/project-icon-selector.test.tsx +49 -0
- package/src/features/projects/components/project-icon-selector.tsx +76 -0
- package/src/features/projects/components/project-invitation-dialog.tsx +368 -0
- package/src/features/projects/components/project-issues.tsx +128 -0
- package/src/features/projects/components/project-leave-button.tsx +69 -0
- package/src/features/projects/components/project-list-card.tsx +246 -0
- package/src/features/projects/components/project-list-filters.tsx +320 -0
- package/src/features/projects/components/project-member-manager-dialog.tsx +419 -0
- package/src/features/projects/components/project-settings-dialog.tsx +204 -0
- package/src/features/projects/components/project-stats.tsx +46 -0
- package/src/features/projects/components/project-title-section.tsx +78 -0
- package/src/features/projects/config/icons.ts +91 -0
- package/src/features/projects/hooks/index.ts +28 -0
- package/src/features/projects/hooks/use-create-invitation.ts +83 -0
- package/src/features/projects/hooks/use-create-project.ts +77 -0
- package/src/features/projects/hooks/use-delete-project.ts +84 -0
- package/src/features/projects/hooks/use-join-project.ts +43 -0
- package/src/features/projects/hooks/use-leave-project.ts +84 -0
- package/src/features/projects/hooks/use-project-activities.ts +39 -0
- package/src/features/projects/hooks/use-project-filters.ts +86 -0
- package/src/features/projects/hooks/use-project-invitations.ts +66 -0
- package/src/features/projects/hooks/use-project-members.ts +57 -0
- package/src/features/projects/hooks/use-project.ts +67 -0
- package/src/features/projects/hooks/use-projects.ts +49 -0
- package/src/features/projects/hooks/use-recent-projects.ts +58 -0
- package/src/features/projects/hooks/use-remove-member.ts +89 -0
- package/src/features/projects/hooks/use-resend-invitation.ts +68 -0
- package/src/features/projects/hooks/use-revoke-invitation.ts +71 -0
- package/src/features/projects/hooks/use-team-member-suggestions.ts +133 -0
- package/src/features/projects/hooks/use-update-member-role.ts +92 -0
- package/src/features/projects/hooks/use-update-project.ts +88 -0
- package/src/features/projects/index.ts +91 -0
- package/src/features/projects/screens/index.ts +3 -0
- package/src/features/projects/screens/invitation-acceptance-screen.tsx +320 -0
- package/src/features/projects/screens/project-detail-screen-wrapper.tsx +47 -0
- package/src/features/projects/screens/project-detail-screen.tsx +661 -0
- package/src/features/projects/screens/projects-list-screen.tsx +161 -0
- package/src/features/projects/types/index.ts +59 -0
- package/src/features/projects/utils/format-helpers.ts +16 -0
- package/src/features/projects/utils/index.ts +2 -0
- package/src/features/projects/utils/role-helpers.ts +25 -0
- package/src/features/setup/api/complete-setup.ts +21 -0
- package/src/features/setup/api/create-admin.ts +21 -0
- package/src/features/setup/api/create-first-workspace.ts +21 -0
- package/src/features/setup/api/get-instance-status.ts +12 -0
- package/src/features/setup/api/get-service-health.ts +12 -0
- package/src/features/setup/api/index.ts +44 -0
- package/src/features/setup/api/save-instance-config.ts +21 -0
- package/src/features/setup/api/types.ts +122 -0
- package/src/features/setup/components/__tests__/setup-wizard-ui.test.tsx +362 -0
- package/src/features/setup/components/admin-account-step.tsx +205 -0
- package/src/features/setup/components/first-workspace-step.tsx +120 -0
- package/src/features/setup/components/index.ts +9 -0
- package/src/features/setup/components/instance-config-step.tsx +107 -0
- package/src/features/setup/components/mail-config-step.tsx +205 -0
- package/src/features/setup/components/sample-data-step.tsx +131 -0
- package/src/features/setup/components/service-health-step.tsx +180 -0
- package/src/features/setup/components/service-status-badge.tsx +50 -0
- package/src/features/setup/components/setup-progress.tsx +103 -0
- package/src/features/setup/components/setup-wizard.tsx +169 -0
- package/src/features/setup/hooks/index.ts +12 -0
- package/src/features/setup/hooks/use-complete-setup.ts +23 -0
- package/src/features/setup/hooks/use-create-admin.ts +25 -0
- package/src/features/setup/hooks/use-create-first-workspace.ts +24 -0
- package/src/features/setup/hooks/use-instance-status.ts +21 -0
- package/src/features/setup/hooks/use-save-instance-config.ts +23 -0
- package/src/features/setup/hooks/use-service-health.ts +21 -0
- package/src/features/setup/hooks/use-setup-wizard.ts +152 -0
- package/src/features/setup/hooks/use-workspace-mode.ts +19 -0
- package/src/features/setup/index.ts +30 -0
- package/src/features/setup/screens/index.ts +1 -0
- package/src/features/setup/screens/setup-screen.tsx +40 -0
- package/src/features/setup/types/index.ts +69 -0
- package/src/features/setup/utils/index.ts +78 -0
- package/src/features/team-settings/components/index.ts +39 -0
- package/src/features/team-settings/components/loading-states.tsx +296 -0
- package/src/features/team-settings/components/permission-guard.tsx +23 -0
- package/src/features/team-settings/components/settings-card.tsx +22 -0
- package/src/features/team-settings/components/settings-context-provider.tsx +51 -0
- package/src/features/team-settings/components/settings-error-boundary.tsx +366 -0
- package/src/features/team-settings/components/settings-navigation.tsx +87 -0
- package/src/features/team-settings/components/settings-section.tsx +23 -0
- package/src/features/team-settings/components/team-danger-zone.tsx +275 -0
- package/src/features/team-settings/components/team-information-form.tsx +116 -0
- package/src/features/team-settings/components/team-invitations-list.tsx +463 -0
- package/src/features/team-settings/components/team-members-list.tsx +342 -0
- package/src/features/team-settings/components/team-permission-guard.tsx +56 -0
- package/src/features/team-settings/components/team-setting-integrations.tsx +27 -0
- package/src/features/team-settings/components/team-setting-member.tsx +28 -0
- package/src/features/team-settings/components/team-settings-general.tsx +131 -0
- package/src/features/team-settings/components/transfer-ownership-modal.tsx +164 -0
- package/src/features/team-settings/components/unauthorized-access.tsx +52 -0
- package/src/features/team-settings/hooks/__tests__/use-team-settings.test.tsx +139 -0
- package/src/features/team-settings/hooks/index.ts +1 -0
- package/src/features/team-settings/hooks/use-team-settings.ts +148 -0
- package/src/features/team-settings/hooks/use-transfer-ownership.ts +45 -0
- package/src/features/team-settings/index.ts +25 -0
- package/src/features/team-settings/screens/index.ts +1 -0
- package/src/features/team-settings/screens/team-settings-screen.tsx +33 -0
- package/src/features/team-settings/types/index.ts +78 -0
- package/src/features/team-settings/utils/index.ts +6 -0
- package/src/features/team-settings/utils/mock-data.ts +40 -0
- package/src/features/teams/api/cancel-invitation.ts +25 -0
- package/src/features/teams/api/create-invitation.ts +28 -0
- package/src/features/teams/api/create-team.ts +14 -0
- package/src/features/teams/api/delete-team.ts +19 -0
- package/src/features/teams/api/get-invitations.ts +25 -0
- package/src/features/teams/api/get-team-members.ts +25 -0
- package/src/features/teams/api/get-team.ts +13 -0
- package/src/features/teams/api/get-teams.ts +19 -0
- package/src/features/teams/api/index.ts +49 -0
- package/src/features/teams/api/leave-team.ts +22 -0
- package/src/features/teams/api/remove-member.ts +25 -0
- package/src/features/teams/api/resend-invitation.ts +26 -0
- package/src/features/teams/api/switch-team.ts +13 -0
- package/src/features/teams/api/types.ts +122 -0
- package/src/features/teams/api/update-member-roles.ts +28 -0
- package/src/features/teams/api/update-team.ts +17 -0
- package/src/features/teams/components/create-team-dialog.tsx +105 -0
- package/src/features/teams/hooks/__tests__/cache-invalidation.property.test.tsx +268 -0
- package/src/features/teams/hooks/index.ts +35 -0
- package/src/features/teams/hooks/use-can-manage-members.ts +21 -0
- package/src/features/teams/hooks/use-can-manage-team.ts +21 -0
- package/src/features/teams/hooks/use-cancel-invitation.ts +52 -0
- package/src/features/teams/hooks/use-create-invitation.ts +59 -0
- package/src/features/teams/hooks/use-create-team.ts +38 -0
- package/src/features/teams/hooks/use-delete-team.ts +43 -0
- package/src/features/teams/hooks/use-invitations.ts +31 -0
- package/src/features/teams/hooks/use-leave-team.ts +43 -0
- package/src/features/teams/hooks/use-remove-member.ts +58 -0
- package/src/features/teams/hooks/use-resend-invitation.ts +52 -0
- package/src/features/teams/hooks/use-switch-team.ts +41 -0
- package/src/features/teams/hooks/use-team-members.ts +31 -0
- package/src/features/teams/hooks/use-team-permissions.ts +102 -0
- package/src/features/teams/hooks/use-team.ts +30 -0
- package/src/features/teams/hooks/use-teams.ts +26 -0
- package/src/features/teams/hooks/use-update-member-roles.ts +64 -0
- package/src/features/teams/hooks/use-update-team.ts +47 -0
- package/src/features/teams/index.ts +111 -0
- package/src/features/user-settings/actions/set-password.ts +63 -0
- package/src/features/user-settings/api/index.ts +16 -0
- package/src/features/user-settings/components/__tests__/security-settings.test.tsx +125 -0
- package/src/features/user-settings/components/delete-account-dialog.tsx +185 -0
- package/src/features/user-settings/components/index.ts +13 -0
- package/src/features/user-settings/components/integrations-list.tsx +152 -0
- package/src/features/user-settings/components/notification-preferences.tsx +112 -0
- package/src/features/user-settings/components/other-settings.tsx +126 -0
- package/src/features/user-settings/components/password-section.tsx +297 -0
- package/src/features/user-settings/components/security-settings.tsx +184 -0
- package/src/features/user-settings/components/user-preferences.tsx +146 -0
- package/src/features/user-settings/hooks/index.ts +8 -0
- package/src/features/user-settings/hooks/use-notification-preferences.ts +65 -0
- package/src/features/user-settings/hooks/use-user-preferences.ts +52 -0
- package/src/features/user-settings/index.ts +22 -0
- package/src/features/user-settings/screens/index.ts +11 -0
- package/src/features/user-settings/screens/integrations-screen.tsx +24 -0
- package/src/features/user-settings/screens/notifications-screen.tsx +29 -0
- package/src/features/user-settings/screens/other-settings-screen.tsx +24 -0
- package/src/features/user-settings/screens/setting-preferences-screen.tsx +24 -0
- package/src/features/user-settings/screens/user-settings-screen.tsx +23 -0
- package/src/features/user-settings/types/index.ts +42 -0
- package/src/features/user-settings/utils/index.ts +8 -0
- package/src/hooks/use-long-press.ts +196 -0
- package/src/hooks/use-media-upload.ts +95 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-team.ts +32 -0
- package/src/instrumentation.ts +14 -0
- package/src/lib/__tests__/auth-config.test.ts +166 -0
- package/src/lib/__tests__/config.test.ts +42 -0
- package/src/lib/__tests__/db.test.ts +101 -0
- package/src/lib/__tests__/env.property.test.ts +197 -0
- package/src/lib/__tests__/env.test.ts +177 -0
- package/src/lib/__tests__/health-check.test.ts +87 -0
- package/src/lib/__tests__/oauth-errors.test.ts +184 -0
- package/src/lib/__tests__/oauth-redirect.test.ts +224 -0
- package/src/lib/__tests__/oauth-scopes.test.ts +268 -0
- package/src/lib/__tests__/storage.test.ts +58 -0
- package/src/lib/__tests__/url-validator.test.ts +232 -0
- package/src/lib/api-client.ts +93 -0
- package/src/lib/auth-client.ts +5 -0
- package/src/lib/auth-config.ts +146 -0
- package/src/lib/auth.ts +209 -0
- package/src/lib/config.ts +228 -0
- package/src/lib/cors.ts +96 -0
- package/src/lib/date.ts +16 -0
- package/src/lib/db.ts +88 -0
- package/src/lib/env.ts +489 -0
- package/src/lib/feedback.ts +29 -0
- package/src/lib/health-check.ts +229 -0
- package/src/lib/logger.ts +204 -0
- package/src/lib/oauth-errors.ts +138 -0
- package/src/lib/performance.ts +367 -0
- package/src/lib/query-server.ts +35 -0
- package/src/lib/query.tsx +43 -0
- package/src/lib/resend.ts +36 -0
- package/src/lib/security-headers.ts +165 -0
- package/src/lib/setup-status.ts +34 -0
- package/src/lib/storage.ts +150 -0
- package/src/lib/supabase-client.ts +58 -0
- package/src/lib/testing/test-db.ts +75 -0
- package/src/lib/url-validator.ts +168 -0
- package/src/lib/utils.ts +6 -0
- package/src/mocks/README.md +42 -0
- package/src/mocks/activity.fixtures.ts +281 -0
- package/src/mocks/annotation.fixtures.ts +325 -0
- package/src/mocks/attachment.fixtures.ts +80 -0
- package/src/mocks/email.fixtures.ts +61 -0
- package/src/mocks/index.ts +33 -0
- package/src/mocks/issue.fixtures.ts +364 -0
- package/src/mocks/landing.fixtures.ts +118 -0
- package/src/mocks/notification.fixtures.ts +111 -0
- package/src/mocks/project-activity.fixtures.ts +69 -0
- package/src/mocks/project-invitation.fixtures.ts +95 -0
- package/src/mocks/project-member.fixtures.ts +93 -0
- package/src/mocks/project.fixtures.ts +249 -0
- package/src/mocks/settings.fixtures.ts +56 -0
- package/src/mocks/share.fixtures.ts +48 -0
- package/src/mocks/team-member.fixtures.ts +147 -0
- package/src/mocks/team.fixtures.ts +35 -0
- package/src/mocks/user-settings.fixtures.ts +77 -0
- package/src/mocks/user.fixtures.ts +21 -0
- package/src/providers/theme-provider.tsx +11 -0
- package/src/proxy/__tests__/proxy-setup-gate.test.ts +43 -0
- package/src/proxy.ts +169 -0
- package/src/server/annotations/__tests__/annotation-limit.property.test.ts +291 -0
- package/src/server/annotations/__tests__/attachment-deletion-cascade.property.test.ts +385 -0
- package/src/server/annotations/__tests__/comment-delete-authorization.property.test.ts +419 -0
- package/src/server/annotations/__tests__/sanitize.property.test.ts +302 -0
- package/src/server/annotations/annotation-service.ts +305 -0
- package/src/server/annotations/comment-service.ts +288 -0
- package/src/server/annotations/index.ts +61 -0
- package/src/server/annotations/permission-utils.ts +125 -0
- package/src/server/annotations/sanitize.ts +134 -0
- package/src/server/annotations/types.ts +161 -0
- package/src/server/audit/audit-service.ts +85 -0
- package/src/server/audit/index.ts +11 -0
- package/src/server/audit/types.ts +75 -0
- package/src/server/auth/__tests__/README.md +368 -0
- package/src/server/auth/__tests__/account-linking.integration.test.ts +410 -0
- package/src/server/auth/__tests__/auth-integration.test.ts +811 -0
- package/src/server/auth/__tests__/cookies.test.ts +337 -0
- package/src/server/auth/__tests__/login.property.test.ts +428 -0
- package/src/server/auth/__tests__/oauth-integration.test.ts +555 -0
- package/src/server/auth/__tests__/password.test.ts +194 -0
- package/src/server/auth/__tests__/rate-limiter.test.ts +450 -0
- package/src/server/auth/__tests__/rbac.test.ts +474 -0
- package/src/server/auth/__tests__/session.test.ts +599 -0
- package/src/server/auth/__tests__/signup.property.test.ts +224 -0
- package/src/server/auth/__tests__/token-encryption.test.ts +171 -0
- package/src/server/auth/__tests__/tokens.test.ts +476 -0
- package/src/server/auth/__tests__/verify-email.property.test.ts +372 -0
- package/src/server/auth/cookies.ts +184 -0
- package/src/server/auth/password.ts +94 -0
- package/src/server/auth/rate-limiter.ts +257 -0
- package/src/server/auth/rbac.ts +1168 -0
- package/src/server/auth/session.ts +392 -0
- package/src/server/auth/token-encryption.ts +201 -0
- package/src/server/auth/tokens.ts +397 -0
- package/src/server/db/schema/account.ts +21 -0
- package/src/server/db/schema/annotation-read-status.ts +57 -0
- package/src/server/db/schema/better-auth-verifications.ts +27 -0
- package/src/server/db/schema/email-jobs.ts +24 -0
- package/src/server/db/schema/index.ts +20 -0
- package/src/server/db/schema/instance-settings.ts +42 -0
- package/src/server/db/schema/issue-activities.ts +97 -0
- package/src/server/db/schema/issue-attachments.ts +101 -0
- package/src/server/db/schema/issues.ts +139 -0
- package/src/server/db/schema/notifications.ts +119 -0
- package/src/server/db/schema/project-activities.ts +116 -0
- package/src/server/db/schema/project-invitations.ts +34 -0
- package/src/server/db/schema/project-members.ts +29 -0
- package/src/server/db/schema/projects.ts +46 -0
- package/src/server/db/schema/sessions.ts +17 -0
- package/src/server/db/schema/team-invitations.ts +22 -0
- package/src/server/db/schema/team-members.ts +18 -0
- package/src/server/db/schema/teams.ts +15 -0
- package/src/server/db/schema/user-roles.ts +14 -0
- package/src/server/db/schema/users.ts +17 -0
- package/src/server/db/schema/verification-tokens.ts +15 -0
- package/src/server/email/README.md +258 -0
- package/src/server/email/__tests__/client.property.test.ts +218 -0
- package/src/server/email/__tests__/payload.property.test.ts +205 -0
- package/src/server/email/__tests__/queue.test.ts +407 -0
- package/src/server/email/__tests__/render-template.test.tsx +172 -0
- package/src/server/email/client.ts +70 -0
- package/src/server/email/index.ts +20 -0
- package/src/server/email/providers/console-provider.ts +43 -0
- package/src/server/email/providers/index.ts +5 -0
- package/src/server/email/providers/resend-provider.ts +59 -0
- package/src/server/email/providers/smtp-provider.ts +65 -0
- package/src/server/email/queue.ts +365 -0
- package/src/server/email/render-template.tsx +117 -0
- package/src/server/email/templates/layout.tsx +66 -0
- package/src/server/email/templates/ownership-transfer-email.tsx +75 -0
- package/src/server/email/templates/password-reset-email.tsx +72 -0
- package/src/server/email/templates/project-invitation-email.tsx +70 -0
- package/src/server/email/templates/security-alert-email.tsx +161 -0
- package/src/server/email/templates/team-invitation-email.tsx +68 -0
- package/src/server/email/templates/verification-email.tsx +61 -0
- package/src/server/email/templates/welcome-email.tsx +60 -0
- package/src/server/email/worker.ts +182 -0
- package/src/server/issues/activity-service.ts +422 -0
- package/src/server/issues/attachment-service.ts +379 -0
- package/src/server/issues/index.ts +68 -0
- package/src/server/issues/issue-service.ts +569 -0
- package/src/server/issues/types.ts +233 -0
- package/src/server/notifications/__tests__/notification-service.integration.test.ts +405 -0
- package/src/server/notifications/index.ts +13 -0
- package/src/server/notifications/notification-service.ts +526 -0
- package/src/server/notifications/types.ts +234 -0
- package/src/server/projects/__tests__/activity-logging.integration.test.ts +319 -0
- package/src/server/projects/__tests__/invitation-email.integration.test.ts +258 -0
- package/src/server/projects/__tests__/invitation-service.integration.test.ts +651 -0
- package/src/server/projects/__tests__/member-service.property.test.ts +397 -0
- package/src/server/projects/__tests__/project-service.property.test.ts +116 -0
- package/src/server/projects/activity-service.ts +548 -0
- package/src/server/projects/index.ts +11 -0
- package/src/server/projects/invitation-service.ts +773 -0
- package/src/server/projects/member-service.ts +458 -0
- package/src/server/projects/project-service.ts +491 -0
- package/src/server/projects/schemas.ts +310 -0
- package/src/server/projects/types.ts +166 -0
- package/src/server/projects/utils.ts +128 -0
- package/src/server/setup/__tests__/health-check.property.test.ts +70 -0
- package/src/server/setup/__tests__/sample-data.property.test.ts +82 -0
- package/src/server/setup/__tests__/setup.property.test.ts +95 -0
- package/src/server/setup/health-check-service.ts +294 -0
- package/src/server/setup/index.ts +35 -0
- package/src/server/setup/sample-data-service.ts +233 -0
- package/src/server/setup/setup-service.ts +229 -0
- package/src/server/setup/types.ts +96 -0
- package/src/server/teams/__tests__/export.property.test.ts +542 -0
- package/src/server/teams/__tests__/get-members.integration.test.ts +105 -0
- package/src/server/teams/__tests__/invitation-flow.integration.test.ts +402 -0
- package/src/server/teams/__tests__/invitations.property.test.ts +235 -0
- package/src/server/teams/__tests__/member-management-flow.integration.test.ts +306 -0
- package/src/server/teams/__tests__/members.property.test.ts +180 -0
- package/src/server/teams/__tests__/ownership-transfer.property.test.ts +173 -0
- package/src/server/teams/__tests__/resource-limits.property.test.ts +382 -0
- package/src/server/teams/__tests__/team-context-management.integration.test.ts +396 -0
- package/src/server/teams/__tests__/team-context.property.test.ts +854 -0
- package/src/server/teams/__tests__/team-creation-flow.integration.test.ts +310 -0
- package/src/server/teams/__tests__/team-creation.property.test.ts +280 -0
- package/src/server/teams/errors.ts +396 -0
- package/src/server/teams/export-service.ts +383 -0
- package/src/server/teams/index.ts +10 -0
- package/src/server/teams/invitation-service.ts +708 -0
- package/src/server/teams/member-service.ts +334 -0
- package/src/server/teams/resource-limits.ts +211 -0
- package/src/server/teams/slug.ts +58 -0
- package/src/server/teams/team-context.ts +648 -0
- package/src/server/teams/team-service.ts +660 -0
- package/src/server/teams/types.ts +81 -0
- package/src/server/teams/validation.ts +209 -0
- package/src/styles/globals.css +160 -0
- package/src/types/deployment.ts +39 -0
- package/supabase/config.toml +357 -0
- package/supabase/seed.sql +480 -0
- package/tests/e2e/.gitkeep +0 -0
- package/tests/e2e/QUICK_START.md +98 -0
- package/tests/e2e/README.md +301 -0
- package/tests/e2e/auth.spec.ts +583 -0
- package/tests/e2e/global-setup.ts +23 -0
- package/tests/e2e/global-teardown.ts +23 -0
- package/tests/e2e/helpers/auth-helpers.ts +310 -0
- package/tests/e2e/helpers/test-fixtures.ts +286 -0
- package/tests/e2e/smoke-test.spec.ts +330 -0
- package/tsconfig.json +48 -0
- package/vitest.config.ts +50 -0
- package/vitest.setup.ts +56 -0
- package/dist/index.js +0 -778
|
@@ -0,0 +1,1714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-based tests for database migration runner
|
|
3
|
+
*
|
|
4
|
+
* Tests migration system correctness properties using property-based testing with fast-check.
|
|
5
|
+
*
|
|
6
|
+
* Feature: automated-drizzle-migrations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
10
|
+
import fc from 'fast-check';
|
|
11
|
+
|
|
12
|
+
// Property test configuration
|
|
13
|
+
const PROPERTY_CONFIG = {
|
|
14
|
+
numRuns: 100,
|
|
15
|
+
verbose: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mock console methods to capture log output
|
|
19
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
20
|
+
let consoleInfoSpy: ReturnType<typeof vi.spyOn>;
|
|
21
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
22
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
23
|
+
|
|
24
|
+
const getAllLogMessages = (spy: ReturnType<typeof vi.spyOn>): string[] =>
|
|
25
|
+
spy.mock.calls.map((call: unknown[]) => String(call[0] ?? ''));
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
29
|
+
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
30
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
31
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
consoleLogSpy.mockRestore();
|
|
36
|
+
consoleInfoSpy.mockRestore();
|
|
37
|
+
consoleWarnSpy.mockRestore();
|
|
38
|
+
consoleErrorSpy.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Helper Functions (extracted from migrate.ts for testing)
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validates the DIRECT_URL environment variable
|
|
47
|
+
*/
|
|
48
|
+
function validateEnvironment(directUrl?: string): { valid: boolean; url?: string; error?: string } {
|
|
49
|
+
const DIRECT_URL = directUrl;
|
|
50
|
+
|
|
51
|
+
// Check if DIRECT_URL exists
|
|
52
|
+
if (!DIRECT_URL) {
|
|
53
|
+
return {
|
|
54
|
+
valid: false,
|
|
55
|
+
error: "DIRECT_URL environment variable is not set. Please configure it in your environment or .env.local file.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check if DIRECT_URL is not just whitespace
|
|
60
|
+
if (DIRECT_URL.trim().length === 0) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
error: "DIRECT_URL environment variable is empty. Please provide a valid PostgreSQL connection string.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate PostgreSQL URL format
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(DIRECT_URL);
|
|
70
|
+
|
|
71
|
+
// Check if it's a PostgreSQL URL
|
|
72
|
+
if (!url.protocol.startsWith("postgres")) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
error: `Invalid database URL protocol: ${url.protocol}. Expected 'postgres:' or 'postgresql:'.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if hostname exists
|
|
80
|
+
if (!url.hostname) {
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
error: "Invalid database URL: missing hostname.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { valid: true, url: DIRECT_URL };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Invalid database URL format: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Formats errors with GitHub Actions annotations
|
|
98
|
+
*/
|
|
99
|
+
function formatError(error: unknown, context?: string): string {
|
|
100
|
+
const errorObj = error as any;
|
|
101
|
+
let message = "";
|
|
102
|
+
|
|
103
|
+
if (context) {
|
|
104
|
+
message += `Context: ${context}\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Extract error message
|
|
108
|
+
if (errorObj?.message) {
|
|
109
|
+
message += `Error: ${errorObj.message}\n`;
|
|
110
|
+
} else {
|
|
111
|
+
message += `Error: ${String(error)}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract SQL state if available (PostgreSQL error codes)
|
|
115
|
+
if (errorObj?.code) {
|
|
116
|
+
message += `SQL State: ${errorObj.code}\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract position/line number if available
|
|
120
|
+
if (errorObj?.position) {
|
|
121
|
+
message += `Position: ${errorObj.position}\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Extract constraint name for constraint violations
|
|
125
|
+
if (errorObj?.constraint) {
|
|
126
|
+
message += `Constraint: ${errorObj.constraint}\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Extract table name if available
|
|
130
|
+
if (errorObj?.table) {
|
|
131
|
+
message += `Table: ${errorObj.table}\n`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Extract column name if available
|
|
135
|
+
if (errorObj?.column) {
|
|
136
|
+
message += `Column: ${errorObj.column}\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add troubleshooting guidance
|
|
140
|
+
message += "\nTroubleshooting:\n";
|
|
141
|
+
|
|
142
|
+
if (errorObj?.code === "42P01") {
|
|
143
|
+
message += "- Table does not exist. Ensure migrations are applied in order.\n";
|
|
144
|
+
} else if (errorObj?.code === "23505") {
|
|
145
|
+
message += "- Unique constraint violation. Check for duplicate data.\n";
|
|
146
|
+
} else if (errorObj?.code === "23503") {
|
|
147
|
+
message += "- Foreign key constraint violation. Ensure referenced records exist.\n";
|
|
148
|
+
} else if (errorObj?.code === "42601") {
|
|
149
|
+
message += "- SQL syntax error. Review the migration SQL for syntax issues.\n";
|
|
150
|
+
} else if (errorObj?.message?.includes("timeout")) {
|
|
151
|
+
message += "- Connection timeout. Check network connectivity and database availability.\n";
|
|
152
|
+
} else if (errorObj?.message?.includes("authentication")) {
|
|
153
|
+
message += "- Authentication failed. Verify database credentials in DIRECT_URL.\n";
|
|
154
|
+
} else {
|
|
155
|
+
message += "- Review the error details above and check the migration SQL.\n";
|
|
156
|
+
message += "- Ensure the database is accessible and has sufficient resources.\n";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Simulates logging migration context
|
|
164
|
+
*/
|
|
165
|
+
function logMigrationContext(branch: string, commit: string, environment: string): void {
|
|
166
|
+
console.log(`📋 Migration Context:`);
|
|
167
|
+
console.log(` Branch: ${branch}`);
|
|
168
|
+
console.log(` Commit: ${commit.substring(0, 7)}`);
|
|
169
|
+
console.log(` Environment: ${environment}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Arbitraries for generating test data
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
// Valid PostgreSQL URL arbitrary
|
|
177
|
+
const validPostgresUrlArb = fc.record({
|
|
178
|
+
protocol: fc.constantFrom('postgres:', 'postgresql:'),
|
|
179
|
+
username: fc.stringMatching(/^[a-zA-Z0-9_]{1,20}$/),
|
|
180
|
+
password: fc.stringMatching(/^[a-zA-Z0-9_]{1,20}$/),
|
|
181
|
+
hostname: fc.domain(),
|
|
182
|
+
port: fc.integer({ min: 1024, max: 65535 }),
|
|
183
|
+
database: fc.stringMatching(/^[a-zA-Z0-9_]{1,20}$/),
|
|
184
|
+
}).map(({ protocol, username, password, hostname, port, database }) =>
|
|
185
|
+
`${protocol}//${username}:${password}@${hostname}:${port}/${database}`
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Invalid URL arbitrary
|
|
189
|
+
const invalidUrlArb = fc.oneof(
|
|
190
|
+
fc.constant(''),
|
|
191
|
+
fc.constant(' '),
|
|
192
|
+
fc.constant('not-a-url'),
|
|
193
|
+
fc.constant('http://example.com'),
|
|
194
|
+
fc.constant('mysql://localhost:3306/db'),
|
|
195
|
+
fc.string({ minLength: 1, maxLength: 50 }).filter(s => !s.includes('://')),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// PostgreSQL error code arbitrary
|
|
199
|
+
const postgresErrorCodeArb = fc.constantFrom(
|
|
200
|
+
'42P01', // undefined_table
|
|
201
|
+
'23505', // unique_violation
|
|
202
|
+
'23503', // foreign_key_violation
|
|
203
|
+
'42601', // syntax_error
|
|
204
|
+
'08006', // connection_failure
|
|
205
|
+
'28P01', // invalid_password
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Hex string arbitrary (for migration hashes)
|
|
209
|
+
const hexStringArb = (length: number) =>
|
|
210
|
+
fc.array(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'), {
|
|
211
|
+
minLength: length,
|
|
212
|
+
maxLength: length
|
|
213
|
+
}).map(arr => arr.join(''));
|
|
214
|
+
|
|
215
|
+
// Branch name arbitrary
|
|
216
|
+
const branchNameArb = fc.constantFrom('main', 'develop', 'feature/test', 'hotfix/bug');
|
|
217
|
+
|
|
218
|
+
// Commit SHA arbitrary (40 character hex string)
|
|
219
|
+
const commitShaArb = fc.array(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'), { minLength: 40, maxLength: 40 }).map(arr => arr.join(''));
|
|
220
|
+
|
|
221
|
+
// Environment arbitrary
|
|
222
|
+
const environmentArb = fc.constantFrom('Production', 'Preview', 'Development');
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// Property Tests
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
describe('Migration Runner - Property-Based Tests', () => {
|
|
229
|
+
/**
|
|
230
|
+
* Feature: automated-drizzle-migrations, Property 9: Configuration validation completeness
|
|
231
|
+
* Validates: Requirements 2.1, 2.2
|
|
232
|
+
*
|
|
233
|
+
* For any workflow execution, if the DIRECT_URL environment variable is missing or invalid,
|
|
234
|
+
* the migration system should fail before attempting any database operations.
|
|
235
|
+
*/
|
|
236
|
+
test('Property 9: Configuration validation completeness - missing or invalid DIRECT_URL fails', async () => {
|
|
237
|
+
await fc.assert(
|
|
238
|
+
fc.asyncProperty(
|
|
239
|
+
invalidUrlArb,
|
|
240
|
+
async (invalidUrl) => {
|
|
241
|
+
// Validate environment with invalid URL
|
|
242
|
+
const result = validateEnvironment(invalidUrl);
|
|
243
|
+
|
|
244
|
+
// Verify validation fails
|
|
245
|
+
expect(result.valid).toBe(false);
|
|
246
|
+
expect(result.error).toBeDefined();
|
|
247
|
+
expect(result.error).toBeTruthy();
|
|
248
|
+
expect(typeof result.error).toBe('string');
|
|
249
|
+
expect(result.error!.length).toBeGreaterThan(0);
|
|
250
|
+
|
|
251
|
+
// Verify error message is descriptive
|
|
252
|
+
if (invalidUrl === '' || invalidUrl.trim() === '') {
|
|
253
|
+
expect(result.error).toMatch(/not set|empty/i);
|
|
254
|
+
} else if (!invalidUrl.includes('://')) {
|
|
255
|
+
// Some strings without :// are valid URLs (e.g. "scheme:path"), so they might fail on protocol instead of format
|
|
256
|
+
expect(result.error).toMatch(/invalid.*(url.*format|protocol)/i);
|
|
257
|
+
} else if (!invalidUrl.startsWith('postgres')) {
|
|
258
|
+
expect(result.error).toMatch(/invalid.*protocol|protocol/i);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
),
|
|
262
|
+
PROPERTY_CONFIG
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Property 9 (valid URLs): Valid PostgreSQL URLs pass validation
|
|
268
|
+
*/
|
|
269
|
+
test('Property 9: Configuration validation completeness - valid DIRECT_URL passes', async () => {
|
|
270
|
+
await fc.assert(
|
|
271
|
+
fc.asyncProperty(
|
|
272
|
+
validPostgresUrlArb,
|
|
273
|
+
async (validUrl) => {
|
|
274
|
+
// Validate environment with valid URL
|
|
275
|
+
const result = validateEnvironment(validUrl);
|
|
276
|
+
|
|
277
|
+
// Verify validation succeeds
|
|
278
|
+
expect(result.valid).toBe(true);
|
|
279
|
+
expect(result.url).toBe(validUrl);
|
|
280
|
+
expect(result.error).toBeUndefined();
|
|
281
|
+
}
|
|
282
|
+
),
|
|
283
|
+
PROPERTY_CONFIG
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Feature: automated-drizzle-migrations, Property 7: Error message completeness
|
|
289
|
+
* Validates: Requirements 7.1, 7.2, 7.3, 7.4
|
|
290
|
+
*
|
|
291
|
+
* For any migration failure, the error output should contain sufficient information
|
|
292
|
+
* (SQL error, line number, migration file name) to diagnose the issue without
|
|
293
|
+
* requiring additional database queries.
|
|
294
|
+
*/
|
|
295
|
+
test('Property 7: Error message completeness - SQL errors include diagnostic information', async () => {
|
|
296
|
+
await fc.assert(
|
|
297
|
+
fc.asyncProperty(
|
|
298
|
+
postgresErrorCodeArb,
|
|
299
|
+
fc.string({ minLength: 10, maxLength: 200 }),
|
|
300
|
+
fc.option(fc.integer({ min: 1, max: 1000 }), { nil: undefined }),
|
|
301
|
+
fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }),
|
|
302
|
+
fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }),
|
|
303
|
+
fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }),
|
|
304
|
+
async (errorCode, errorMessage, position, constraint, table, column) => {
|
|
305
|
+
// Create a PostgreSQL-like error object
|
|
306
|
+
const error = {
|
|
307
|
+
message: errorMessage,
|
|
308
|
+
code: errorCode,
|
|
309
|
+
position,
|
|
310
|
+
constraint,
|
|
311
|
+
table,
|
|
312
|
+
column,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Format the error
|
|
316
|
+
const formattedError = formatError(error, 'Migration execution');
|
|
317
|
+
|
|
318
|
+
// Verify error message contains context
|
|
319
|
+
expect(formattedError).toContain('Context: Migration execution');
|
|
320
|
+
|
|
321
|
+
// Verify error message contains the error text
|
|
322
|
+
expect(formattedError).toContain(`Error: ${errorMessage}`);
|
|
323
|
+
|
|
324
|
+
// Verify error message contains SQL state
|
|
325
|
+
expect(formattedError).toContain(`SQL State: ${errorCode}`);
|
|
326
|
+
|
|
327
|
+
// Verify optional fields are included when present
|
|
328
|
+
if (position) {
|
|
329
|
+
expect(formattedError).toContain(`Position: ${position}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (constraint) {
|
|
333
|
+
expect(formattedError).toContain(`Constraint: ${constraint}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (table) {
|
|
337
|
+
expect(formattedError).toContain(`Table: ${table}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (column) {
|
|
341
|
+
expect(formattedError).toContain(`Column: ${column}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Verify troubleshooting guidance is included
|
|
345
|
+
expect(formattedError).toContain('Troubleshooting:');
|
|
346
|
+
|
|
347
|
+
// Verify specific troubleshooting guidance based on error code
|
|
348
|
+
if (errorCode === '42P01') {
|
|
349
|
+
expect(formattedError).toContain('Table does not exist');
|
|
350
|
+
} else if (errorCode === '23505') {
|
|
351
|
+
expect(formattedError).toContain('Unique constraint violation');
|
|
352
|
+
} else if (errorCode === '23503') {
|
|
353
|
+
expect(formattedError).toContain('Foreign key constraint violation');
|
|
354
|
+
} else if (errorCode === '42601') {
|
|
355
|
+
expect(formattedError).toContain('SQL syntax error');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
),
|
|
359
|
+
PROPERTY_CONFIG
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Property 7 (error types): Different error types have appropriate troubleshooting
|
|
365
|
+
*/
|
|
366
|
+
test('Property 7: Error message completeness - error types have specific troubleshooting', async () => {
|
|
367
|
+
await fc.assert(
|
|
368
|
+
fc.asyncProperty(
|
|
369
|
+
fc.oneof(
|
|
370
|
+
fc.constant({ message: 'Connection timeout after 10s', code: '08006' }),
|
|
371
|
+
fc.constant({ message: 'authentication failed for user', code: '28P01' }),
|
|
372
|
+
fc.constant({ message: 'syntax error at or near "CREAT"', code: '42601' }),
|
|
373
|
+
fc.constant({ message: 'relation "users" does not exist', code: '42P01' }),
|
|
374
|
+
),
|
|
375
|
+
async (error) => {
|
|
376
|
+
// Format the error
|
|
377
|
+
const formattedError = formatError(error);
|
|
378
|
+
|
|
379
|
+
// Verify appropriate troubleshooting guidance
|
|
380
|
+
if (error.message.includes('timeout')) {
|
|
381
|
+
expect(formattedError).toMatch(/timeout.*network.*connectivity/i);
|
|
382
|
+
} else if (error.message.includes('authentication')) {
|
|
383
|
+
expect(formattedError).toMatch(/authentication.*credentials/i);
|
|
384
|
+
} else if (error.code === '42601') {
|
|
385
|
+
expect(formattedError).toMatch(/syntax.*review.*SQL/i);
|
|
386
|
+
} else if (error.code === '42P01') {
|
|
387
|
+
expect(formattedError).toMatch(/table.*not exist.*order/i);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
),
|
|
391
|
+
PROPERTY_CONFIG
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Feature: automated-drizzle-migrations, Property 10: Log output completeness
|
|
397
|
+
* Validates: Requirements 3.1, 3.2, 3.3, 3.5
|
|
398
|
+
*
|
|
399
|
+
* For any migration execution, the log output should contain the branch name,
|
|
400
|
+
* commit SHA, count of applied migrations, and execution status for each
|
|
401
|
+
* migration file processed.
|
|
402
|
+
*/
|
|
403
|
+
test('Property 10: Log output completeness - migration context is logged', async () => {
|
|
404
|
+
await fc.assert(
|
|
405
|
+
fc.asyncProperty(
|
|
406
|
+
branchNameArb,
|
|
407
|
+
commitShaArb,
|
|
408
|
+
environmentArb,
|
|
409
|
+
async (branch, commit, environment) => {
|
|
410
|
+
// Clear previous logs
|
|
411
|
+
consoleLogSpy.mockClear();
|
|
412
|
+
|
|
413
|
+
// Log migration context
|
|
414
|
+
logMigrationContext(branch, commit, environment);
|
|
415
|
+
|
|
416
|
+
// Get all log messages
|
|
417
|
+
const logMessages = getAllLogMessages(consoleLogSpy);
|
|
418
|
+
const combinedLog = logMessages.join('\n');
|
|
419
|
+
|
|
420
|
+
// Verify branch name is logged
|
|
421
|
+
expect(combinedLog).toContain(`Branch: ${branch}`);
|
|
422
|
+
|
|
423
|
+
// Verify commit SHA is logged (at least first 7 characters)
|
|
424
|
+
expect(combinedLog).toContain(`Commit: ${commit.substring(0, 7)}`);
|
|
425
|
+
|
|
426
|
+
// Verify environment is logged
|
|
427
|
+
expect(combinedLog).toContain(`Environment: ${environment}`);
|
|
428
|
+
|
|
429
|
+
// Verify context header is present
|
|
430
|
+
expect(combinedLog).toContain('Migration Context');
|
|
431
|
+
}
|
|
432
|
+
),
|
|
433
|
+
PROPERTY_CONFIG
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Property 10 (commit truncation): Commit SHA is consistently truncated to 7 characters
|
|
439
|
+
*/
|
|
440
|
+
test('Property 10: Log output completeness - commit SHA truncation is consistent', async () => {
|
|
441
|
+
await fc.assert(
|
|
442
|
+
fc.asyncProperty(
|
|
443
|
+
branchNameArb,
|
|
444
|
+
commitShaArb,
|
|
445
|
+
environmentArb,
|
|
446
|
+
async (branch, commit, environment) => {
|
|
447
|
+
// Clear previous logs
|
|
448
|
+
consoleLogSpy.mockClear();
|
|
449
|
+
|
|
450
|
+
// Log migration context
|
|
451
|
+
logMigrationContext(branch, commit, environment);
|
|
452
|
+
|
|
453
|
+
// Get all log messages
|
|
454
|
+
const logMessages = getAllLogMessages(consoleLogSpy);
|
|
455
|
+
const combinedLog = logMessages.join('\n');
|
|
456
|
+
|
|
457
|
+
// Extract the logged commit
|
|
458
|
+
const commitMatch = combinedLog.match(/Commit: ([a-f0-9]+)/);
|
|
459
|
+
expect(commitMatch).toBeTruthy();
|
|
460
|
+
|
|
461
|
+
if (commitMatch) {
|
|
462
|
+
const loggedCommit = commitMatch[1];
|
|
463
|
+
|
|
464
|
+
// Verify it's exactly 7 characters
|
|
465
|
+
expect(loggedCommit.length).toBe(7);
|
|
466
|
+
|
|
467
|
+
// Verify it matches the first 7 characters of the original
|
|
468
|
+
expect(loggedCommit).toBe(commit.substring(0, 7));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
),
|
|
472
|
+
PROPERTY_CONFIG
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Property 10 (environment mapping): Branch names map to correct environments
|
|
478
|
+
*/
|
|
479
|
+
test('Property 10: Log output completeness - branch to environment mapping', async () => {
|
|
480
|
+
await fc.assert(
|
|
481
|
+
fc.asyncProperty(
|
|
482
|
+
branchNameArb,
|
|
483
|
+
commitShaArb,
|
|
484
|
+
async (branch, commit) => {
|
|
485
|
+
// Determine expected environment based on branch
|
|
486
|
+
const expectedEnvironment =
|
|
487
|
+
branch === 'main' ? 'Production' :
|
|
488
|
+
branch === 'develop' ? 'Preview' :
|
|
489
|
+
'Development';
|
|
490
|
+
|
|
491
|
+
// Clear previous logs
|
|
492
|
+
consoleLogSpy.mockClear();
|
|
493
|
+
|
|
494
|
+
// Log migration context
|
|
495
|
+
logMigrationContext(branch, commit, expectedEnvironment);
|
|
496
|
+
|
|
497
|
+
// Get all log messages
|
|
498
|
+
const logMessages = getAllLogMessages(consoleLogSpy);
|
|
499
|
+
const combinedLog = logMessages.join('\n');
|
|
500
|
+
|
|
501
|
+
// Verify correct environment is logged
|
|
502
|
+
expect(combinedLog).toContain(`Environment: ${expectedEnvironment}`);
|
|
503
|
+
}
|
|
504
|
+
),
|
|
505
|
+
PROPERTY_CONFIG
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Feature: automated-drizzle-migrations, Property 4: Environment isolation
|
|
511
|
+
* Validates: Requirements 6.1, 6.2, 6.3, 6.4
|
|
512
|
+
*
|
|
513
|
+
* For any push to the develop branch, migrations should only affect the dev database,
|
|
514
|
+
* and for any push to the main branch, migrations should only affect the prod database,
|
|
515
|
+
* with no cross-contamination.
|
|
516
|
+
*/
|
|
517
|
+
test('Property 4: Environment isolation - branch determines database target', async () => {
|
|
518
|
+
await fc.assert(
|
|
519
|
+
fc.asyncProperty(
|
|
520
|
+
fc.constantFrom('main', 'develop'),
|
|
521
|
+
validPostgresUrlArb,
|
|
522
|
+
validPostgresUrlArb,
|
|
523
|
+
async (branch, devUrl, prodUrl) => {
|
|
524
|
+
// Ensure dev and prod URLs are different
|
|
525
|
+
fc.pre(devUrl !== prodUrl);
|
|
526
|
+
|
|
527
|
+
// Determine which URL should be used based on branch
|
|
528
|
+
const expectedUrl = branch === 'main' ? prodUrl : devUrl;
|
|
529
|
+
const unexpectedUrl = branch === 'main' ? devUrl : prodUrl;
|
|
530
|
+
|
|
531
|
+
// Simulate environment variable selection based on branch
|
|
532
|
+
const selectedUrl = branch === 'main' ? prodUrl : devUrl;
|
|
533
|
+
|
|
534
|
+
// Verify correct URL is selected
|
|
535
|
+
expect(selectedUrl).toBe(expectedUrl);
|
|
536
|
+
expect(selectedUrl).not.toBe(unexpectedUrl);
|
|
537
|
+
|
|
538
|
+
// Verify URL validation passes for selected URL
|
|
539
|
+
const result = validateEnvironment(selectedUrl);
|
|
540
|
+
expect(result.valid).toBe(true);
|
|
541
|
+
expect(result.url).toBe(expectedUrl);
|
|
542
|
+
|
|
543
|
+
// Verify the URL contains expected database identifier
|
|
544
|
+
if (branch === 'main') {
|
|
545
|
+
// Production should use PROD_DIRECT_URL
|
|
546
|
+
expect(selectedUrl).toBe(prodUrl);
|
|
547
|
+
} else {
|
|
548
|
+
// Develop/feature branches should use DEV_DIRECT_URL
|
|
549
|
+
expect(selectedUrl).toBe(devUrl);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
),
|
|
553
|
+
PROPERTY_CONFIG
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Property 4 (feature branches): Feature branches use dev database
|
|
559
|
+
*/
|
|
560
|
+
test('Property 4: Environment isolation - feature branches use dev database', async () => {
|
|
561
|
+
await fc.assert(
|
|
562
|
+
fc.asyncProperty(
|
|
563
|
+
fc.stringMatching(/^feature\/[a-z0-9-]{1,30}$/),
|
|
564
|
+
validPostgresUrlArb,
|
|
565
|
+
validPostgresUrlArb,
|
|
566
|
+
async (featureBranch, devUrl, prodUrl) => {
|
|
567
|
+
// Ensure dev and prod URLs are different
|
|
568
|
+
fc.pre(devUrl !== prodUrl);
|
|
569
|
+
|
|
570
|
+
// Feature branches should always use dev database
|
|
571
|
+
const selectedUrl = devUrl; // In workflow, non-main branches use DEV_DIRECT_URL
|
|
572
|
+
|
|
573
|
+
// Verify dev URL is selected
|
|
574
|
+
expect(selectedUrl).toBe(devUrl);
|
|
575
|
+
expect(selectedUrl).not.toBe(prodUrl);
|
|
576
|
+
|
|
577
|
+
// Verify URL validation passes
|
|
578
|
+
const result = validateEnvironment(selectedUrl);
|
|
579
|
+
expect(result.valid).toBe(true);
|
|
580
|
+
}
|
|
581
|
+
),
|
|
582
|
+
PROPERTY_CONFIG
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Property 4 (environment secrets): Different environments use different secrets
|
|
588
|
+
*/
|
|
589
|
+
test('Property 4: Environment isolation - environments use distinct secrets', async () => {
|
|
590
|
+
await fc.assert(
|
|
591
|
+
fc.asyncProperty(
|
|
592
|
+
validPostgresUrlArb,
|
|
593
|
+
validPostgresUrlArb,
|
|
594
|
+
async (devUrl, prodUrl) => {
|
|
595
|
+
// Ensure URLs are different (representing different databases)
|
|
596
|
+
fc.pre(devUrl !== prodUrl);
|
|
597
|
+
|
|
598
|
+
// Simulate GitHub environment secrets
|
|
599
|
+
const previewSecret = devUrl;
|
|
600
|
+
const productionSecret = prodUrl;
|
|
601
|
+
|
|
602
|
+
// Verify secrets are distinct
|
|
603
|
+
expect(previewSecret).not.toBe(productionSecret);
|
|
604
|
+
|
|
605
|
+
// Verify both secrets are valid
|
|
606
|
+
const devResult = validateEnvironment(previewSecret);
|
|
607
|
+
const prodResult = validateEnvironment(productionSecret);
|
|
608
|
+
|
|
609
|
+
expect(devResult.valid).toBe(true);
|
|
610
|
+
expect(prodResult.valid).toBe(true);
|
|
611
|
+
|
|
612
|
+
// Verify they point to different databases
|
|
613
|
+
expect(devResult.url).not.toBe(prodResult.url);
|
|
614
|
+
}
|
|
615
|
+
),
|
|
616
|
+
PROPERTY_CONFIG
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Feature: automated-drizzle-migrations, Property 5: Deployment blocking on failure
|
|
622
|
+
* Validates: Requirements 1.5, 5.3
|
|
623
|
+
*
|
|
624
|
+
* For any migration execution that fails, the subsequent Vercel deployment step
|
|
625
|
+
* should not execute, ensuring that application code is never deployed with an
|
|
626
|
+
* incompatible database schema.
|
|
627
|
+
*/
|
|
628
|
+
test('Property 5: Deployment blocking on failure - failed migrations prevent deployment', async () => {
|
|
629
|
+
await fc.assert(
|
|
630
|
+
fc.asyncProperty(
|
|
631
|
+
fc.integer({ min: 1, max: 10 }),
|
|
632
|
+
fc.boolean(),
|
|
633
|
+
async (exitCode, migrationSuccess) => {
|
|
634
|
+
// Simulate migration execution result
|
|
635
|
+
const migrationExitCode = migrationSuccess ? 0 : exitCode;
|
|
636
|
+
|
|
637
|
+
// Verify exit code indicates success or failure
|
|
638
|
+
const shouldDeploy = migrationExitCode === 0;
|
|
639
|
+
|
|
640
|
+
if (migrationSuccess) {
|
|
641
|
+
// Successful migration should allow deployment
|
|
642
|
+
expect(shouldDeploy).toBe(true);
|
|
643
|
+
expect(migrationExitCode).toBe(0);
|
|
644
|
+
} else {
|
|
645
|
+
// Failed migration should block deployment
|
|
646
|
+
expect(shouldDeploy).toBe(false);
|
|
647
|
+
expect(migrationExitCode).not.toBe(0);
|
|
648
|
+
expect(migrationExitCode).toBeGreaterThan(0);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
),
|
|
652
|
+
PROPERTY_CONFIG
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Property 5 (workflow dependency): Deployment job depends on migration success
|
|
658
|
+
*/
|
|
659
|
+
test('Property 5: Deployment blocking on failure - deployment requires migration success', async () => {
|
|
660
|
+
await fc.assert(
|
|
661
|
+
fc.asyncProperty(
|
|
662
|
+
fc.constantFrom('success', 'failure', 'cancelled'),
|
|
663
|
+
async (migrationStatus) => {
|
|
664
|
+
// Simulate workflow job status
|
|
665
|
+
const migrationJobStatus = migrationStatus;
|
|
666
|
+
|
|
667
|
+
// Determine if deployment should proceed
|
|
668
|
+
const shouldProceedToDeployment = migrationJobStatus === 'success';
|
|
669
|
+
|
|
670
|
+
// Verify deployment only proceeds on success
|
|
671
|
+
if (migrationStatus === 'success') {
|
|
672
|
+
expect(shouldProceedToDeployment).toBe(true);
|
|
673
|
+
} else {
|
|
674
|
+
expect(shouldProceedToDeployment).toBe(false);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
),
|
|
678
|
+
PROPERTY_CONFIG
|
|
679
|
+
);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Property 5 (error propagation): Migration errors propagate to workflow
|
|
684
|
+
*/
|
|
685
|
+
test('Property 5: Deployment blocking on failure - errors propagate correctly', async () => {
|
|
686
|
+
await fc.assert(
|
|
687
|
+
fc.asyncProperty(
|
|
688
|
+
postgresErrorCodeArb,
|
|
689
|
+
fc.string({ minLength: 10, maxLength: 100 }),
|
|
690
|
+
async (errorCode, errorMessage) => {
|
|
691
|
+
// Simulate a migration error
|
|
692
|
+
const error = {
|
|
693
|
+
message: errorMessage,
|
|
694
|
+
code: errorCode,
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
// Format the error
|
|
698
|
+
const formattedError = formatError(error, 'Migration execution');
|
|
699
|
+
|
|
700
|
+
// Verify error is properly formatted for GitHub Actions
|
|
701
|
+
expect(formattedError).toBeTruthy();
|
|
702
|
+
expect(formattedError.length).toBeGreaterThan(0);
|
|
703
|
+
|
|
704
|
+
// Verify error contains diagnostic information
|
|
705
|
+
expect(formattedError).toContain('Error:');
|
|
706
|
+
expect(formattedError).toContain('SQL State:');
|
|
707
|
+
expect(formattedError).toContain('Troubleshooting:');
|
|
708
|
+
|
|
709
|
+
// Simulate exit code for error
|
|
710
|
+
const exitCode = 1;
|
|
711
|
+
|
|
712
|
+
// Verify non-zero exit code blocks deployment
|
|
713
|
+
expect(exitCode).not.toBe(0);
|
|
714
|
+
expect(exitCode).toBeGreaterThan(0);
|
|
715
|
+
}
|
|
716
|
+
),
|
|
717
|
+
PROPERTY_CONFIG
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Feature: automated-drizzle-migrations, Property 1: Migration idempotency
|
|
723
|
+
* Validates: Requirements 4.2, 4.3, 4.4
|
|
724
|
+
*
|
|
725
|
+
* For any set of migration files and any database state, running the migration system
|
|
726
|
+
* multiple times should result in the same final database schema, with previously
|
|
727
|
+
* applied migrations being skipped.
|
|
728
|
+
*/
|
|
729
|
+
test('Property 1: Migration idempotency - running migrations multiple times produces same result', async () => {
|
|
730
|
+
await fc.assert(
|
|
731
|
+
fc.asyncProperty(
|
|
732
|
+
fc.array(
|
|
733
|
+
fc.record({
|
|
734
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
735
|
+
hash: hexStringArb(32),
|
|
736
|
+
appliedAt: fc.date({ min: new Date('2020-01-01'), max: new Date() }),
|
|
737
|
+
}),
|
|
738
|
+
{ minLength: 0, maxLength: 10 }
|
|
739
|
+
),
|
|
740
|
+
fc.array(
|
|
741
|
+
fc.record({
|
|
742
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
743
|
+
hash: hexStringArb(32),
|
|
744
|
+
}),
|
|
745
|
+
{ minLength: 0, maxLength: 10 }
|
|
746
|
+
),
|
|
747
|
+
async (appliedMigrations, pendingMigrations) => {
|
|
748
|
+
// Simulate first migration run
|
|
749
|
+
const firstRunApplied = new Set(appliedMigrations.map(m => m.hash));
|
|
750
|
+
const firstRunPending = pendingMigrations.filter(m => !firstRunApplied.has(m.hash));
|
|
751
|
+
|
|
752
|
+
// After first run, these migrations are now applied
|
|
753
|
+
const afterFirstRun = new Set([...firstRunApplied, ...firstRunPending.map(m => m.hash)]);
|
|
754
|
+
|
|
755
|
+
// Simulate second migration run with same migration files
|
|
756
|
+
const secondRunApplied = afterFirstRun;
|
|
757
|
+
const secondRunPending = pendingMigrations.filter(m => !secondRunApplied.has(m.hash));
|
|
758
|
+
|
|
759
|
+
// After second run, no new migrations should be applied
|
|
760
|
+
const afterSecondRun = new Set([...secondRunApplied, ...secondRunPending.map(m => m.hash)]);
|
|
761
|
+
|
|
762
|
+
// Verify idempotency: second run applies no new migrations
|
|
763
|
+
expect(secondRunPending.length).toBe(0);
|
|
764
|
+
expect(afterSecondRun.size).toBe(afterFirstRun.size);
|
|
765
|
+
|
|
766
|
+
// Verify all migrations from first run are still applied
|
|
767
|
+
firstRunPending.forEach(m => {
|
|
768
|
+
expect(afterSecondRun.has(m.hash)).toBe(true);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Verify the final state is identical
|
|
772
|
+
expect(Array.from(afterSecondRun).sort()).toEqual(Array.from(afterFirstRun).sort());
|
|
773
|
+
}
|
|
774
|
+
),
|
|
775
|
+
PROPERTY_CONFIG
|
|
776
|
+
);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Property 1 (skip logic): Already-applied migrations are skipped
|
|
781
|
+
*/
|
|
782
|
+
test('Property 1: Migration idempotency - already-applied migrations are skipped', async () => {
|
|
783
|
+
await fc.assert(
|
|
784
|
+
fc.asyncProperty(
|
|
785
|
+
fc.array(
|
|
786
|
+
fc.record({
|
|
787
|
+
hash: hexStringArb(32),
|
|
788
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
789
|
+
}),
|
|
790
|
+
{ minLength: 1, maxLength: 5 }
|
|
791
|
+
),
|
|
792
|
+
async (migrations) => {
|
|
793
|
+
// Simulate all migrations already applied
|
|
794
|
+
const appliedHashes = new Set(migrations.map(m => m.hash));
|
|
795
|
+
|
|
796
|
+
// Attempt to run migrations again
|
|
797
|
+
const pendingMigrations = migrations.filter(m => !appliedHashes.has(m.hash));
|
|
798
|
+
|
|
799
|
+
// Verify no migrations are pending
|
|
800
|
+
expect(pendingMigrations.length).toBe(0);
|
|
801
|
+
|
|
802
|
+
// Verify all migrations are marked as applied
|
|
803
|
+
migrations.forEach(m => {
|
|
804
|
+
expect(appliedHashes.has(m.hash)).toBe(true);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
),
|
|
808
|
+
PROPERTY_CONFIG
|
|
809
|
+
);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Property 1 (tracking consistency): Migration tracking table accurately reflects applied migrations
|
|
814
|
+
*/
|
|
815
|
+
test('Property 1: Migration idempotency - tracking table reflects applied state', async () => {
|
|
816
|
+
await fc.assert(
|
|
817
|
+
fc.asyncProperty(
|
|
818
|
+
fc.array(
|
|
819
|
+
fc.record({
|
|
820
|
+
hash: hexStringArb(32),
|
|
821
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
822
|
+
appliedAt: fc.integer({ min: 1000000000000, max: 9999999999999 }), // Unix timestamp in ms
|
|
823
|
+
}),
|
|
824
|
+
{ minLength: 0, maxLength: 10 }
|
|
825
|
+
),
|
|
826
|
+
fc.array(
|
|
827
|
+
fc.record({
|
|
828
|
+
hash: hexStringArb(32),
|
|
829
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
830
|
+
}),
|
|
831
|
+
{ minLength: 0, maxLength: 5 }
|
|
832
|
+
),
|
|
833
|
+
async (trackingTableEntries, newMigrations) => {
|
|
834
|
+
// Ensure new migrations don't overlap with tracking table
|
|
835
|
+
const appliedHashes = new Set(trackingTableEntries.map(m => m.hash));
|
|
836
|
+
const uniqueNewMigrations = newMigrations.filter(m => !appliedHashes.has(m.hash));
|
|
837
|
+
|
|
838
|
+
// Simulate applying new migrations
|
|
839
|
+
const afterApply = [
|
|
840
|
+
...trackingTableEntries,
|
|
841
|
+
...uniqueNewMigrations.map(m => ({
|
|
842
|
+
hash: m.hash,
|
|
843
|
+
filename: m.filename,
|
|
844
|
+
appliedAt: Date.now(),
|
|
845
|
+
})),
|
|
846
|
+
];
|
|
847
|
+
|
|
848
|
+
// Verify tracking table contains all applied migrations
|
|
849
|
+
expect(afterApply.length).toBe(trackingTableEntries.length + uniqueNewMigrations.length);
|
|
850
|
+
|
|
851
|
+
// Verify each applied migration has a tracking entry
|
|
852
|
+
uniqueNewMigrations.forEach(m => {
|
|
853
|
+
const tracked = afterApply.find(t => t.hash === m.hash);
|
|
854
|
+
expect(tracked).toBeDefined();
|
|
855
|
+
expect(tracked?.hash).toBe(m.hash);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// Verify original tracking entries are preserved
|
|
859
|
+
trackingTableEntries.forEach(original => {
|
|
860
|
+
const preserved = afterApply.find(t => t.hash === original.hash);
|
|
861
|
+
expect(preserved).toBeDefined();
|
|
862
|
+
expect(preserved?.appliedAt).toBe(original.appliedAt);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
),
|
|
866
|
+
PROPERTY_CONFIG
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Feature: automated-drizzle-migrations, Property 2: Migration ordering consistency
|
|
872
|
+
* Validates: Requirements 1.3
|
|
873
|
+
*
|
|
874
|
+
* For any set of migration files, the migration system should always execute them
|
|
875
|
+
* in the same chronological order based on their timestamp prefixes, regardless
|
|
876
|
+
* of filesystem ordering.
|
|
877
|
+
*/
|
|
878
|
+
test('Property 2: Migration ordering consistency - migrations execute in timestamp order', async () => {
|
|
879
|
+
await fc.assert(
|
|
880
|
+
fc.asyncProperty(
|
|
881
|
+
fc.array(
|
|
882
|
+
fc.record({
|
|
883
|
+
filename: fc.nat({ max: 9999 }).map(n => `${String(n).padStart(4, '0')}_migration.sql`),
|
|
884
|
+
timestamp: fc.nat({ max: 9999 }),
|
|
885
|
+
}),
|
|
886
|
+
{ minLength: 2, maxLength: 10 }
|
|
887
|
+
),
|
|
888
|
+
async (migrations) => {
|
|
889
|
+
// Extract timestamps from filenames
|
|
890
|
+
const migrationsWithTimestamps = migrations.map(m => {
|
|
891
|
+
const match = m.filename.match(/^(\d{4})_/);
|
|
892
|
+
const timestamp = match ? parseInt(match[1], 10) : 0;
|
|
893
|
+
return { ...m, extractedTimestamp: timestamp };
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Sort by timestamp (simulating migration system behavior)
|
|
897
|
+
const sorted = [...migrationsWithTimestamps].sort((a, b) =>
|
|
898
|
+
a.extractedTimestamp - b.extractedTimestamp
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
// Verify sorted order is chronological
|
|
902
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
903
|
+
expect(sorted[i].extractedTimestamp).toBeGreaterThanOrEqual(sorted[i - 1].extractedTimestamp);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Verify the order is deterministic (same input produces same output)
|
|
907
|
+
const sortedAgain = [...migrationsWithTimestamps].sort((a, b) =>
|
|
908
|
+
a.extractedTimestamp - b.extractedTimestamp
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
expect(sortedAgain.map(m => m.filename)).toEqual(sorted.map(m => m.filename));
|
|
912
|
+
}
|
|
913
|
+
),
|
|
914
|
+
PROPERTY_CONFIG
|
|
915
|
+
);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Property 2 (filesystem independence): Order is independent of filesystem listing
|
|
920
|
+
*/
|
|
921
|
+
test('Property 2: Migration ordering consistency - order independent of filesystem', async () => {
|
|
922
|
+
await fc.assert(
|
|
923
|
+
fc.asyncProperty(
|
|
924
|
+
fc.array(
|
|
925
|
+
fc.nat({ max: 9999 }).map(n => ({
|
|
926
|
+
filename: `${String(n).padStart(4, '0')}_migration.sql`,
|
|
927
|
+
timestamp: n,
|
|
928
|
+
})),
|
|
929
|
+
{ minLength: 2, maxLength: 10 }
|
|
930
|
+
),
|
|
931
|
+
async (migrations) => {
|
|
932
|
+
// Shuffle to simulate different filesystem orderings
|
|
933
|
+
const shuffled1 = [...migrations].sort(() => Math.random() - 0.5);
|
|
934
|
+
const shuffled2 = [...migrations].sort(() => Math.random() - 0.5);
|
|
935
|
+
|
|
936
|
+
// Sort both by timestamp
|
|
937
|
+
const sorted1 = [...shuffled1].sort((a, b) => a.timestamp - b.timestamp);
|
|
938
|
+
const sorted2 = [...shuffled2].sort((a, b) => a.timestamp - b.timestamp);
|
|
939
|
+
|
|
940
|
+
// Verify both produce the same order
|
|
941
|
+
expect(sorted1.map(m => m.filename)).toEqual(sorted2.map(m => m.filename));
|
|
942
|
+
|
|
943
|
+
// Verify order matches timestamp order
|
|
944
|
+
for (let i = 1; i < sorted1.length; i++) {
|
|
945
|
+
expect(sorted1[i].timestamp).toBeGreaterThanOrEqual(sorted1[i - 1].timestamp);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
),
|
|
949
|
+
PROPERTY_CONFIG
|
|
950
|
+
);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Property 2 (timestamp extraction): Timestamps are correctly extracted from filenames
|
|
955
|
+
*/
|
|
956
|
+
test('Property 2: Migration ordering consistency - timestamp extraction is correct', async () => {
|
|
957
|
+
await fc.assert(
|
|
958
|
+
fc.asyncProperty(
|
|
959
|
+
fc.nat({ max: 9999 }),
|
|
960
|
+
fc.stringMatching(/^[a-z_]{1,30}$/),
|
|
961
|
+
async (timestamp, description) => {
|
|
962
|
+
// Create filename with timestamp
|
|
963
|
+
const filename = `${String(timestamp).padStart(4, '0')}_${description}.sql`;
|
|
964
|
+
|
|
965
|
+
// Extract timestamp (simulating migration system)
|
|
966
|
+
const match = filename.match(/^(\d{4})_/);
|
|
967
|
+
expect(match).toBeTruthy();
|
|
968
|
+
|
|
969
|
+
if (match) {
|
|
970
|
+
const extractedTimestamp = parseInt(match[1], 10);
|
|
971
|
+
|
|
972
|
+
// Verify extracted timestamp matches original
|
|
973
|
+
expect(extractedTimestamp).toBe(timestamp);
|
|
974
|
+
|
|
975
|
+
// Verify it's a valid number
|
|
976
|
+
expect(Number.isInteger(extractedTimestamp)).toBe(true);
|
|
977
|
+
expect(extractedTimestamp).toBeGreaterThanOrEqual(0);
|
|
978
|
+
expect(extractedTimestamp).toBeLessThanOrEqual(9999);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
),
|
|
982
|
+
PROPERTY_CONFIG
|
|
983
|
+
);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Feature: automated-drizzle-migrations, Property 6: Migration tracking consistency
|
|
988
|
+
* Validates: Requirements 4.5, 9.2, 9.5
|
|
989
|
+
*
|
|
990
|
+
* For any successfully applied migration, the migration tracking table should contain
|
|
991
|
+
* exactly one entry for that migration, and for any failed migration, the tracking
|
|
992
|
+
* table should not contain an entry.
|
|
993
|
+
*/
|
|
994
|
+
test('Property 6: Migration tracking consistency - successful migrations recorded once', async () => {
|
|
995
|
+
await fc.assert(
|
|
996
|
+
fc.asyncProperty(
|
|
997
|
+
fc.array(
|
|
998
|
+
fc.record({
|
|
999
|
+
hash: hexStringArb(32),
|
|
1000
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1001
|
+
success: fc.constant(true),
|
|
1002
|
+
}),
|
|
1003
|
+
{ minLength: 1, maxLength: 10 }
|
|
1004
|
+
),
|
|
1005
|
+
async (successfulMigrations) => {
|
|
1006
|
+
// Simulate tracking table after successful migrations
|
|
1007
|
+
const trackingTable = successfulMigrations.map(m => ({
|
|
1008
|
+
hash: m.hash,
|
|
1009
|
+
created_at: Date.now(),
|
|
1010
|
+
}));
|
|
1011
|
+
|
|
1012
|
+
// Verify each successful migration has exactly one entry
|
|
1013
|
+
successfulMigrations.forEach(migration => {
|
|
1014
|
+
const entries = trackingTable.filter(t => t.hash === migration.hash);
|
|
1015
|
+
expect(entries.length).toBe(1);
|
|
1016
|
+
expect(entries[0].hash).toBe(migration.hash);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// Verify tracking table size matches number of successful migrations
|
|
1020
|
+
expect(trackingTable.length).toBe(successfulMigrations.length);
|
|
1021
|
+
|
|
1022
|
+
// Verify no duplicate entries
|
|
1023
|
+
const uniqueHashes = new Set(trackingTable.map(t => t.hash));
|
|
1024
|
+
expect(uniqueHashes.size).toBe(trackingTable.length);
|
|
1025
|
+
}
|
|
1026
|
+
),
|
|
1027
|
+
PROPERTY_CONFIG
|
|
1028
|
+
);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Property 6 (failed migrations): Failed migrations are not recorded
|
|
1033
|
+
*/
|
|
1034
|
+
test('Property 6: Migration tracking consistency - failed migrations not recorded', async () => {
|
|
1035
|
+
await fc.assert(
|
|
1036
|
+
fc.asyncProperty(
|
|
1037
|
+
fc.array(
|
|
1038
|
+
fc.record({
|
|
1039
|
+
hash: hexStringArb(32),
|
|
1040
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1041
|
+
success: fc.boolean(),
|
|
1042
|
+
}),
|
|
1043
|
+
{ minLength: 1, maxLength: 10 }
|
|
1044
|
+
),
|
|
1045
|
+
async (migrations) => {
|
|
1046
|
+
// Simulate tracking table with only successful migrations
|
|
1047
|
+
const trackingTable = migrations
|
|
1048
|
+
.filter(m => m.success)
|
|
1049
|
+
.map(m => ({
|
|
1050
|
+
hash: m.hash,
|
|
1051
|
+
created_at: Date.now(),
|
|
1052
|
+
}));
|
|
1053
|
+
|
|
1054
|
+
// Verify successful migrations are recorded
|
|
1055
|
+
const successfulMigrations = migrations.filter(m => m.success);
|
|
1056
|
+
successfulMigrations.forEach(migration => {
|
|
1057
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1058
|
+
expect(entry).toBeDefined();
|
|
1059
|
+
expect(entry?.hash).toBe(migration.hash);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Verify failed migrations are NOT recorded
|
|
1063
|
+
const failedMigrations = migrations.filter(m => !m.success);
|
|
1064
|
+
failedMigrations.forEach(migration => {
|
|
1065
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1066
|
+
expect(entry).toBeUndefined();
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// Verify tracking table only contains successful migrations
|
|
1070
|
+
expect(trackingTable.length).toBe(successfulMigrations.length);
|
|
1071
|
+
}
|
|
1072
|
+
),
|
|
1073
|
+
PROPERTY_CONFIG
|
|
1074
|
+
);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Property 6 (rollback consistency): Rolled back migrations don't appear in tracking
|
|
1079
|
+
*/
|
|
1080
|
+
test('Property 6: Migration tracking consistency - rollback prevents tracking entry', async () => {
|
|
1081
|
+
await fc.assert(
|
|
1082
|
+
fc.asyncProperty(
|
|
1083
|
+
fc.array(
|
|
1084
|
+
fc.record({
|
|
1085
|
+
hash: hexStringArb(32),
|
|
1086
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1087
|
+
}),
|
|
1088
|
+
{ minLength: 1, maxLength: 5 }
|
|
1089
|
+
),
|
|
1090
|
+
fc.integer({ min: 0, max: 4 }),
|
|
1091
|
+
async (migrations, failureIndex) => {
|
|
1092
|
+
// Ensure failure index is valid
|
|
1093
|
+
fc.pre(failureIndex < migrations.length);
|
|
1094
|
+
|
|
1095
|
+
// Simulate migrations up to failure point
|
|
1096
|
+
const appliedBeforeFailure = migrations.slice(0, failureIndex);
|
|
1097
|
+
const failedMigration = migrations[failureIndex];
|
|
1098
|
+
const notAttempted = migrations.slice(failureIndex + 1);
|
|
1099
|
+
|
|
1100
|
+
// Simulate tracking table (only contains migrations before failure)
|
|
1101
|
+
const trackingTable = appliedBeforeFailure.map(m => ({
|
|
1102
|
+
hash: m.hash,
|
|
1103
|
+
created_at: Date.now(),
|
|
1104
|
+
}));
|
|
1105
|
+
|
|
1106
|
+
// Verify migrations before failure are tracked
|
|
1107
|
+
appliedBeforeFailure.forEach(migration => {
|
|
1108
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1109
|
+
expect(entry).toBeDefined();
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// Verify failed migration is NOT tracked (rolled back)
|
|
1113
|
+
const failedEntry = trackingTable.find(t => t.hash === failedMigration.hash);
|
|
1114
|
+
expect(failedEntry).toBeUndefined();
|
|
1115
|
+
|
|
1116
|
+
// Verify migrations after failure are NOT tracked (not attempted)
|
|
1117
|
+
notAttempted.forEach(migration => {
|
|
1118
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1119
|
+
expect(entry).toBeUndefined();
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// Verify tracking table size
|
|
1123
|
+
expect(trackingTable.length).toBe(appliedBeforeFailure.length);
|
|
1124
|
+
}
|
|
1125
|
+
),
|
|
1126
|
+
PROPERTY_CONFIG
|
|
1127
|
+
);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Property 6 (tracking atomicity): Tracking entry is atomic with migration success
|
|
1132
|
+
*/
|
|
1133
|
+
test('Property 6: Migration tracking consistency - tracking is atomic with migration', async () => {
|
|
1134
|
+
await fc.assert(
|
|
1135
|
+
fc.asyncProperty(
|
|
1136
|
+
fc.record({
|
|
1137
|
+
hash: hexStringArb(32),
|
|
1138
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1139
|
+
migrationSuccess: fc.boolean(),
|
|
1140
|
+
trackingSuccess: fc.boolean(),
|
|
1141
|
+
}),
|
|
1142
|
+
async ({ hash, filename, migrationSuccess, trackingSuccess }) => {
|
|
1143
|
+
// Simulate transaction behavior: both must succeed or both must fail
|
|
1144
|
+
const transactionSuccess = migrationSuccess && trackingSuccess;
|
|
1145
|
+
|
|
1146
|
+
// Simulate tracking table state after transaction
|
|
1147
|
+
const trackingTable: Array<{ hash: string; created_at: number }> = [];
|
|
1148
|
+
|
|
1149
|
+
if (transactionSuccess) {
|
|
1150
|
+
// Both migration and tracking succeeded
|
|
1151
|
+
trackingTable.push({ hash, created_at: Date.now() });
|
|
1152
|
+
}
|
|
1153
|
+
// If either failed, tracking table remains empty (rollback)
|
|
1154
|
+
|
|
1155
|
+
// Verify atomicity
|
|
1156
|
+
if (migrationSuccess && trackingSuccess) {
|
|
1157
|
+
// Both succeeded: tracking entry exists
|
|
1158
|
+
expect(trackingTable.length).toBe(1);
|
|
1159
|
+
expect(trackingTable[0].hash).toBe(hash);
|
|
1160
|
+
} else {
|
|
1161
|
+
// Either failed: no tracking entry (rollback)
|
|
1162
|
+
expect(trackingTable.length).toBe(0);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
),
|
|
1166
|
+
PROPERTY_CONFIG
|
|
1167
|
+
);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Feature: automated-drizzle-migrations, Property 3: Transaction atomicity per migration
|
|
1172
|
+
* Validates: Requirements 9.1, 9.3, 9.4
|
|
1173
|
+
*
|
|
1174
|
+
* For any individual migration file, if the migration fails at any point during execution,
|
|
1175
|
+
* the database should return to its exact pre-migration state with no partial changes applied.
|
|
1176
|
+
*/
|
|
1177
|
+
test('Property 3: Transaction atomicity per migration - failed migrations rollback completely', async () => {
|
|
1178
|
+
await fc.assert(
|
|
1179
|
+
fc.asyncProperty(
|
|
1180
|
+
fc.record({
|
|
1181
|
+
migrationHash: hexStringArb(32),
|
|
1182
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1183
|
+
preMigrationState: fc.record({
|
|
1184
|
+
tableCount: fc.integer({ min: 0, max: 50 }),
|
|
1185
|
+
rowCounts: fc.array(fc.integer({ min: 0, max: 1000 }), { maxLength: 10 }),
|
|
1186
|
+
constraints: fc.array(fc.string({ minLength: 5, maxLength: 20 }), { maxLength: 10 }),
|
|
1187
|
+
}),
|
|
1188
|
+
migrationOperations: fc.array(
|
|
1189
|
+
fc.record({
|
|
1190
|
+
type: fc.constantFrom('CREATE_TABLE', 'INSERT', 'ALTER_TABLE', 'CREATE_INDEX'),
|
|
1191
|
+
success: fc.boolean(),
|
|
1192
|
+
}),
|
|
1193
|
+
{ minLength: 1, maxLength: 10 }
|
|
1194
|
+
),
|
|
1195
|
+
}),
|
|
1196
|
+
async ({ migrationHash, filename, preMigrationState, migrationOperations }) => {
|
|
1197
|
+
// Determine if migration succeeds (all operations must succeed)
|
|
1198
|
+
const migrationSucceeds = migrationOperations.every(op => op.success);
|
|
1199
|
+
|
|
1200
|
+
// Simulate database state after migration attempt
|
|
1201
|
+
let postMigrationState;
|
|
1202
|
+
|
|
1203
|
+
if (migrationSucceeds) {
|
|
1204
|
+
// All operations succeeded: state changes are committed
|
|
1205
|
+
postMigrationState = {
|
|
1206
|
+
tableCount: preMigrationState.tableCount + migrationOperations.filter(op => op.type === 'CREATE_TABLE').length,
|
|
1207
|
+
rowCounts: [...preMigrationState.rowCounts],
|
|
1208
|
+
constraints: [...preMigrationState.constraints],
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
// Add rows from INSERT operations
|
|
1212
|
+
const insertCount = migrationOperations.filter(op => op.type === 'INSERT').length;
|
|
1213
|
+
if (insertCount > 0 && postMigrationState.rowCounts.length > 0) {
|
|
1214
|
+
postMigrationState.rowCounts[0] += insertCount;
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
// At least one operation failed: transaction rolled back
|
|
1218
|
+
// Database state should be identical to pre-migration state
|
|
1219
|
+
postMigrationState = {
|
|
1220
|
+
tableCount: preMigrationState.tableCount,
|
|
1221
|
+
rowCounts: [...preMigrationState.rowCounts],
|
|
1222
|
+
constraints: [...preMigrationState.constraints],
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Verify atomicity property
|
|
1227
|
+
if (migrationSucceeds) {
|
|
1228
|
+
// Success: state should have changed
|
|
1229
|
+
const stateChanged =
|
|
1230
|
+
postMigrationState.tableCount !== preMigrationState.tableCount ||
|
|
1231
|
+
postMigrationState.rowCounts.some((count, idx) => count !== preMigrationState.rowCounts[idx]);
|
|
1232
|
+
|
|
1233
|
+
// If there were operations that modify state, state should change
|
|
1234
|
+
const hasCreateTable = migrationOperations.some(op => op.type === 'CREATE_TABLE');
|
|
1235
|
+
const hasInsert = migrationOperations.some(op => op.type === 'INSERT') && preMigrationState.rowCounts.length > 0;
|
|
1236
|
+
const hasStateModifyingOps = hasCreateTable || hasInsert;
|
|
1237
|
+
|
|
1238
|
+
if (hasStateModifyingOps) {
|
|
1239
|
+
expect(stateChanged).toBe(true);
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
// Failure: state must be identical to pre-migration (rollback)
|
|
1243
|
+
expect(postMigrationState.tableCount).toBe(preMigrationState.tableCount);
|
|
1244
|
+
expect(postMigrationState.rowCounts).toEqual(preMigrationState.rowCounts);
|
|
1245
|
+
expect(postMigrationState.constraints).toEqual(preMigrationState.constraints);
|
|
1246
|
+
|
|
1247
|
+
// Verify no partial changes
|
|
1248
|
+
expect(JSON.stringify(postMigrationState)).toBe(JSON.stringify(preMigrationState));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
),
|
|
1252
|
+
PROPERTY_CONFIG
|
|
1253
|
+
);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Property 3 (partial failure): Partial failures trigger complete rollback
|
|
1258
|
+
*/
|
|
1259
|
+
test('Property 3: Transaction atomicity - partial failures rollback all changes', async () => {
|
|
1260
|
+
await fc.assert(
|
|
1261
|
+
fc.asyncProperty(
|
|
1262
|
+
fc.array(
|
|
1263
|
+
fc.record({
|
|
1264
|
+
operation: fc.constantFrom('CREATE', 'INSERT', 'UPDATE', 'DELETE'),
|
|
1265
|
+
success: fc.boolean(),
|
|
1266
|
+
}),
|
|
1267
|
+
{ minLength: 2, maxLength: 10 }
|
|
1268
|
+
),
|
|
1269
|
+
fc.integer({ min: 0, max: 9 }),
|
|
1270
|
+
async (operations, failureIndex) => {
|
|
1271
|
+
// Ensure failure index is valid
|
|
1272
|
+
fc.pre(failureIndex < operations.length);
|
|
1273
|
+
|
|
1274
|
+
// Mark the operation at failureIndex as failed
|
|
1275
|
+
const operationsWithFailure = operations.map((op, idx) => ({
|
|
1276
|
+
...op,
|
|
1277
|
+
success: idx < failureIndex ? true : idx === failureIndex ? false : op.success,
|
|
1278
|
+
}));
|
|
1279
|
+
|
|
1280
|
+
// Simulate transaction execution
|
|
1281
|
+
const migrationSucceeds = operationsWithFailure.every(op => op.success);
|
|
1282
|
+
|
|
1283
|
+
// Count successful operations before failure
|
|
1284
|
+
const successfulOpsBeforeFailure = operationsWithFailure.slice(0, failureIndex).length;
|
|
1285
|
+
|
|
1286
|
+
// Simulate database changes
|
|
1287
|
+
let appliedChanges = 0;
|
|
1288
|
+
|
|
1289
|
+
if (migrationSucceeds) {
|
|
1290
|
+
// All operations succeeded
|
|
1291
|
+
appliedChanges = operationsWithFailure.length;
|
|
1292
|
+
} else {
|
|
1293
|
+
// Transaction failed and rolled back
|
|
1294
|
+
appliedChanges = 0; // All changes rolled back, even successful ones
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Verify rollback behavior
|
|
1298
|
+
if (!migrationSucceeds) {
|
|
1299
|
+
// Even though some operations succeeded before failure,
|
|
1300
|
+
// all changes should be rolled back
|
|
1301
|
+
expect(appliedChanges).toBe(0);
|
|
1302
|
+
|
|
1303
|
+
// If there were successful operations before failure, verify they were rolled back
|
|
1304
|
+
if (successfulOpsBeforeFailure > 0) {
|
|
1305
|
+
// Verify no partial state - successful ops before failure were also rolled back
|
|
1306
|
+
expect(appliedChanges).not.toBe(successfulOpsBeforeFailure);
|
|
1307
|
+
}
|
|
1308
|
+
} else {
|
|
1309
|
+
// All operations succeeded
|
|
1310
|
+
expect(appliedChanges).toBe(operations.length);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
),
|
|
1314
|
+
PROPERTY_CONFIG
|
|
1315
|
+
);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Property 3 (tracking table consistency): Failed migrations don't update tracking table
|
|
1320
|
+
*/
|
|
1321
|
+
test('Property 3: Transaction atomicity - tracking table not updated on failure', async () => {
|
|
1322
|
+
await fc.assert(
|
|
1323
|
+
fc.asyncProperty(
|
|
1324
|
+
fc.record({
|
|
1325
|
+
migrationHash: hexStringArb(32),
|
|
1326
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1327
|
+
migrationSuccess: fc.boolean(),
|
|
1328
|
+
sqlOperations: fc.array(
|
|
1329
|
+
fc.record({
|
|
1330
|
+
sql: fc.string({ minLength: 10, maxLength: 100 }),
|
|
1331
|
+
success: fc.boolean(),
|
|
1332
|
+
}),
|
|
1333
|
+
{ minLength: 1, maxLength: 5 }
|
|
1334
|
+
),
|
|
1335
|
+
}),
|
|
1336
|
+
async ({ migrationHash, filename, migrationSuccess, sqlOperations }) => {
|
|
1337
|
+
// Migration succeeds only if all SQL operations succeed
|
|
1338
|
+
const allOperationsSucceed = sqlOperations.every(op => op.success);
|
|
1339
|
+
const actualMigrationSuccess = migrationSuccess && allOperationsSucceed;
|
|
1340
|
+
|
|
1341
|
+
// Simulate tracking table state
|
|
1342
|
+
const trackingTableBefore: Array<{ hash: string; created_at: number }> = [];
|
|
1343
|
+
const trackingTableAfter: Array<{ hash: string; created_at: number }> = [];
|
|
1344
|
+
|
|
1345
|
+
// Only add to tracking table if migration succeeded
|
|
1346
|
+
if (actualMigrationSuccess) {
|
|
1347
|
+
trackingTableAfter.push({
|
|
1348
|
+
hash: migrationHash,
|
|
1349
|
+
created_at: Date.now(),
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Verify tracking table behavior
|
|
1354
|
+
if (actualMigrationSuccess) {
|
|
1355
|
+
// Success: tracking table should have new entry
|
|
1356
|
+
expect(trackingTableAfter.length).toBe(trackingTableBefore.length + 1);
|
|
1357
|
+
expect(trackingTableAfter.find(t => t.hash === migrationHash)).toBeDefined();
|
|
1358
|
+
} else {
|
|
1359
|
+
// Failure: tracking table should be unchanged (rollback)
|
|
1360
|
+
expect(trackingTableAfter.length).toBe(trackingTableBefore.length);
|
|
1361
|
+
expect(trackingTableAfter.find(t => t.hash === migrationHash)).toBeUndefined();
|
|
1362
|
+
|
|
1363
|
+
// Verify tracking table is identical to before
|
|
1364
|
+
expect(trackingTableAfter).toEqual(trackingTableBefore);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
),
|
|
1368
|
+
PROPERTY_CONFIG
|
|
1369
|
+
);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Property 3 (transaction boundary): Each migration has its own transaction
|
|
1374
|
+
*/
|
|
1375
|
+
test('Property 3: Transaction atomicity - each migration has independent transaction', async () => {
|
|
1376
|
+
await fc.assert(
|
|
1377
|
+
fc.asyncProperty(
|
|
1378
|
+
fc.array(
|
|
1379
|
+
fc.record({
|
|
1380
|
+
hash: hexStringArb(32),
|
|
1381
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1382
|
+
success: fc.boolean(),
|
|
1383
|
+
}),
|
|
1384
|
+
{ minLength: 2, maxLength: 5 }
|
|
1385
|
+
),
|
|
1386
|
+
async (migrations) => {
|
|
1387
|
+
// Simulate tracking table with independent transactions
|
|
1388
|
+
const trackingTable: Array<{ hash: string; created_at: number }> = [];
|
|
1389
|
+
|
|
1390
|
+
// Process each migration in its own transaction
|
|
1391
|
+
for (const migration of migrations) {
|
|
1392
|
+
if (migration.success) {
|
|
1393
|
+
// Transaction succeeded: add to tracking
|
|
1394
|
+
trackingTable.push({
|
|
1395
|
+
hash: migration.hash,
|
|
1396
|
+
created_at: Date.now(),
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
// Transaction failed: don't add to tracking (rollback)
|
|
1400
|
+
// But continue with next migration (independent transaction)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Verify each successful migration is tracked
|
|
1404
|
+
const successfulMigrations = migrations.filter(m => m.success);
|
|
1405
|
+
expect(trackingTable.length).toBe(successfulMigrations.length);
|
|
1406
|
+
|
|
1407
|
+
successfulMigrations.forEach(migration => {
|
|
1408
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1409
|
+
expect(entry).toBeDefined();
|
|
1410
|
+
expect(entry?.hash).toBe(migration.hash);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
// Verify failed migrations are not tracked
|
|
1414
|
+
const failedMigrations = migrations.filter(m => !m.success);
|
|
1415
|
+
failedMigrations.forEach(migration => {
|
|
1416
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1417
|
+
expect(entry).toBeUndefined();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Verify independence: a failed migration doesn't affect previous successful ones
|
|
1421
|
+
if (failedMigrations.length > 0 && successfulMigrations.length > 0) {
|
|
1422
|
+
// Even with failures, successful migrations should still be tracked
|
|
1423
|
+
expect(trackingTable.length).toBeGreaterThan(0);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
),
|
|
1427
|
+
PROPERTY_CONFIG
|
|
1428
|
+
);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Feature: automated-drizzle-migrations, Property 8: Batch migration consistency
|
|
1433
|
+
* Validates: Requirements 8.1, 8.2, 8.3
|
|
1434
|
+
*
|
|
1435
|
+
* For any set of multiple pending migrations, if migration N fails, then migrations
|
|
1436
|
+
* N+1 through N+M should not be executed, and migrations 1 through N-1 should remain
|
|
1437
|
+
* applied in the tracking table.
|
|
1438
|
+
*/
|
|
1439
|
+
test('Property 8: Batch migration consistency - failure halts subsequent migrations', async () => {
|
|
1440
|
+
await fc.assert(
|
|
1441
|
+
fc.asyncProperty(
|
|
1442
|
+
fc.array(
|
|
1443
|
+
fc.record({
|
|
1444
|
+
hash: hexStringArb(32),
|
|
1445
|
+
filename: fc.nat({ max: 9999 }).map(n => `${String(n).padStart(4, '0')}_migration.sql`),
|
|
1446
|
+
timestamp: fc.nat({ max: 9999 }),
|
|
1447
|
+
success: fc.boolean(),
|
|
1448
|
+
}),
|
|
1449
|
+
{ minLength: 2, maxLength: 10 }
|
|
1450
|
+
),
|
|
1451
|
+
async (migrations) => {
|
|
1452
|
+
// Sort migrations by timestamp (chronological order)
|
|
1453
|
+
const sortedMigrations = [...migrations].sort((a, b) => a.timestamp - b.timestamp);
|
|
1454
|
+
|
|
1455
|
+
// Find first failure index
|
|
1456
|
+
const firstFailureIndex = sortedMigrations.findIndex(m => !m.success);
|
|
1457
|
+
|
|
1458
|
+
// Simulate batch execution with halt-on-failure
|
|
1459
|
+
const trackingTable: Array<{ hash: string; created_at: number }> = [];
|
|
1460
|
+
const executedMigrations: string[] = [];
|
|
1461
|
+
|
|
1462
|
+
for (let i = 0; i < sortedMigrations.length; i++) {
|
|
1463
|
+
const migration = sortedMigrations[i];
|
|
1464
|
+
|
|
1465
|
+
// Execute migration
|
|
1466
|
+
executedMigrations.push(migration.hash);
|
|
1467
|
+
|
|
1468
|
+
if (migration.success) {
|
|
1469
|
+
// Success: add to tracking table
|
|
1470
|
+
trackingTable.push({
|
|
1471
|
+
hash: migration.hash,
|
|
1472
|
+
created_at: Date.now(),
|
|
1473
|
+
});
|
|
1474
|
+
} else {
|
|
1475
|
+
// Failure: halt execution (don't process subsequent migrations)
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Verify halt-on-failure behavior
|
|
1481
|
+
if (firstFailureIndex === -1) {
|
|
1482
|
+
// No failures: all migrations should be executed and tracked
|
|
1483
|
+
expect(executedMigrations.length).toBe(sortedMigrations.length);
|
|
1484
|
+
expect(trackingTable.length).toBe(sortedMigrations.length);
|
|
1485
|
+
|
|
1486
|
+
// Verify all migrations are tracked
|
|
1487
|
+
sortedMigrations.forEach(migration => {
|
|
1488
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1489
|
+
expect(entry).toBeDefined();
|
|
1490
|
+
});
|
|
1491
|
+
} else {
|
|
1492
|
+
// Failure occurred: verify halt behavior
|
|
1493
|
+
|
|
1494
|
+
// Migrations before failure should be executed and tracked
|
|
1495
|
+
const migrationsBeforeFailure = sortedMigrations.slice(0, firstFailureIndex);
|
|
1496
|
+
migrationsBeforeFailure.forEach(migration => {
|
|
1497
|
+
expect(executedMigrations).toContain(migration.hash);
|
|
1498
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1499
|
+
expect(entry).toBeDefined();
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// Failed migration should be executed but NOT tracked
|
|
1503
|
+
const failedMigration = sortedMigrations[firstFailureIndex];
|
|
1504
|
+
expect(executedMigrations).toContain(failedMigration.hash);
|
|
1505
|
+
const failedEntry = trackingTable.find(t => t.hash === failedMigration.hash);
|
|
1506
|
+
expect(failedEntry).toBeUndefined();
|
|
1507
|
+
|
|
1508
|
+
// Migrations after failure should NOT be executed
|
|
1509
|
+
const migrationsAfterFailure = sortedMigrations.slice(firstFailureIndex + 1);
|
|
1510
|
+
migrationsAfterFailure.forEach(migration => {
|
|
1511
|
+
expect(executedMigrations).not.toContain(migration.hash);
|
|
1512
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1513
|
+
expect(entry).toBeUndefined();
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// Verify execution count
|
|
1517
|
+
expect(executedMigrations.length).toBe(firstFailureIndex + 1);
|
|
1518
|
+
|
|
1519
|
+
// Verify tracking table only contains successful migrations before failure
|
|
1520
|
+
expect(trackingTable.length).toBe(firstFailureIndex);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
),
|
|
1524
|
+
PROPERTY_CONFIG
|
|
1525
|
+
);
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Property 8 (ordered processing): Batch migrations execute in chronological order
|
|
1530
|
+
*/
|
|
1531
|
+
test('Property 8: Batch migration consistency - migrations execute in order', async () => {
|
|
1532
|
+
await fc.assert(
|
|
1533
|
+
fc.asyncProperty(
|
|
1534
|
+
fc.array(
|
|
1535
|
+
fc.record({
|
|
1536
|
+
hash: hexStringArb(32),
|
|
1537
|
+
timestamp: fc.nat({ max: 9999 }),
|
|
1538
|
+
filename: fc.nat({ max: 9999 }).map(n => `${String(n).padStart(4, '0')}_migration.sql`),
|
|
1539
|
+
}),
|
|
1540
|
+
{ minLength: 2, maxLength: 10 }
|
|
1541
|
+
),
|
|
1542
|
+
async (migrations) => {
|
|
1543
|
+
// Sort by timestamp (simulating batch processing)
|
|
1544
|
+
const sortedMigrations = [...migrations].sort((a, b) => a.timestamp - b.timestamp);
|
|
1545
|
+
|
|
1546
|
+
// Simulate execution order
|
|
1547
|
+
const executionOrder: number[] = [];
|
|
1548
|
+
sortedMigrations.forEach(m => {
|
|
1549
|
+
executionOrder.push(m.timestamp);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
// Verify execution order is chronological
|
|
1553
|
+
for (let i = 1; i < executionOrder.length; i++) {
|
|
1554
|
+
expect(executionOrder[i]).toBeGreaterThanOrEqual(executionOrder[i - 1]);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// Verify order matches sorted order
|
|
1558
|
+
expect(executionOrder).toEqual(sortedMigrations.map(m => m.timestamp));
|
|
1559
|
+
}
|
|
1560
|
+
),
|
|
1561
|
+
PROPERTY_CONFIG
|
|
1562
|
+
);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Property 8 (partial success): Successful migrations before failure remain applied
|
|
1567
|
+
*/
|
|
1568
|
+
test('Property 8: Batch migration consistency - partial success preserves completed migrations', async () => {
|
|
1569
|
+
await fc.assert(
|
|
1570
|
+
fc.asyncProperty(
|
|
1571
|
+
fc.integer({ min: 1, max: 5 }),
|
|
1572
|
+
fc.integer({ min: 1, max: 5 }),
|
|
1573
|
+
fc.array(hexStringArb(32), { minLength: 20, maxLength: 20 }), // Generate enough hashes
|
|
1574
|
+
async (successfulCount, remainingCount, hashes) => {
|
|
1575
|
+
// Ensure we have enough hashes
|
|
1576
|
+
const totalNeeded = successfulCount + 1 + remainingCount;
|
|
1577
|
+
fc.pre(hashes.length >= totalNeeded);
|
|
1578
|
+
|
|
1579
|
+
// Create batch with successful migrations followed by a failure
|
|
1580
|
+
const successfulMigrations = Array.from({ length: successfulCount }, (_, i) => ({
|
|
1581
|
+
hash: hashes[i],
|
|
1582
|
+
timestamp: i,
|
|
1583
|
+
filename: `${String(i).padStart(4, '0')}_migration.sql`,
|
|
1584
|
+
success: true,
|
|
1585
|
+
}));
|
|
1586
|
+
|
|
1587
|
+
const failedMigration = {
|
|
1588
|
+
hash: hashes[successfulCount],
|
|
1589
|
+
timestamp: successfulCount,
|
|
1590
|
+
filename: `${String(successfulCount).padStart(4, '0')}_migration.sql`,
|
|
1591
|
+
success: false,
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
const notExecutedMigrations = Array.from({ length: remainingCount }, (_, i) => ({
|
|
1595
|
+
hash: hashes[successfulCount + 1 + i],
|
|
1596
|
+
timestamp: successfulCount + 1 + i,
|
|
1597
|
+
filename: `${String(successfulCount + 1 + i).padStart(4, '0')}_migration.sql`,
|
|
1598
|
+
success: true,
|
|
1599
|
+
}));
|
|
1600
|
+
|
|
1601
|
+
const allMigrations = [...successfulMigrations, failedMigration, ...notExecutedMigrations];
|
|
1602
|
+
|
|
1603
|
+
// Simulate batch execution with halt-on-failure
|
|
1604
|
+
const trackingTable: Array<{ hash: string; created_at: number }> = [];
|
|
1605
|
+
|
|
1606
|
+
for (const migration of allMigrations) {
|
|
1607
|
+
if (migration.success) {
|
|
1608
|
+
trackingTable.push({
|
|
1609
|
+
hash: migration.hash,
|
|
1610
|
+
created_at: Date.now(),
|
|
1611
|
+
});
|
|
1612
|
+
} else {
|
|
1613
|
+
// Halt on failure
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// Verify successful migrations before failure are tracked
|
|
1619
|
+
expect(trackingTable.length).toBe(successfulCount);
|
|
1620
|
+
|
|
1621
|
+
successfulMigrations.forEach(migration => {
|
|
1622
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1623
|
+
expect(entry).toBeDefined();
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// Verify failed migration is not tracked
|
|
1627
|
+
const failedEntry = trackingTable.find(t => t.hash === failedMigration.hash);
|
|
1628
|
+
expect(failedEntry).toBeUndefined();
|
|
1629
|
+
|
|
1630
|
+
// Verify migrations after failure are not tracked
|
|
1631
|
+
notExecutedMigrations.forEach(migration => {
|
|
1632
|
+
const entry = trackingTable.find(t => t.hash === migration.hash);
|
|
1633
|
+
expect(entry).toBeUndefined();
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
),
|
|
1637
|
+
PROPERTY_CONFIG
|
|
1638
|
+
);
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Property 8 (batch summary): Batch execution reports accurate counts
|
|
1643
|
+
*/
|
|
1644
|
+
test('Property 8: Batch migration consistency - batch summary is accurate', async () => {
|
|
1645
|
+
await fc.assert(
|
|
1646
|
+
fc.asyncProperty(
|
|
1647
|
+
fc.array(
|
|
1648
|
+
fc.record({
|
|
1649
|
+
hash: hexStringArb(32),
|
|
1650
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1651
|
+
success: fc.boolean(),
|
|
1652
|
+
}),
|
|
1653
|
+
{ minLength: 1, maxLength: 10 }
|
|
1654
|
+
),
|
|
1655
|
+
fc.array(
|
|
1656
|
+
fc.record({
|
|
1657
|
+
hash: hexStringArb(32),
|
|
1658
|
+
filename: fc.stringMatching(/^\d{4}_[a-z_]{1,30}\.sql$/),
|
|
1659
|
+
}),
|
|
1660
|
+
{ minLength: 0, maxLength: 5 }
|
|
1661
|
+
),
|
|
1662
|
+
async (pendingMigrations, alreadyAppliedMigrations) => {
|
|
1663
|
+
// Simulate tracking table with already applied migrations
|
|
1664
|
+
const trackingTableBefore = alreadyAppliedMigrations.map(m => ({
|
|
1665
|
+
hash: m.hash,
|
|
1666
|
+
created_at: Date.now() - 1000000,
|
|
1667
|
+
}));
|
|
1668
|
+
|
|
1669
|
+
const appliedHashes = new Set(alreadyAppliedMigrations.map(m => m.hash));
|
|
1670
|
+
|
|
1671
|
+
// Filter out already applied migrations
|
|
1672
|
+
const actuallyPendingMigrations = pendingMigrations.filter(m => !appliedHashes.has(m.hash));
|
|
1673
|
+
|
|
1674
|
+
// Find first failure
|
|
1675
|
+
const firstFailureIndex = actuallyPendingMigrations.findIndex(m => !m.success);
|
|
1676
|
+
|
|
1677
|
+
// Simulate batch execution
|
|
1678
|
+
let migrationsApplied = 0;
|
|
1679
|
+
const migrationsSkipped = alreadyAppliedMigrations.length;
|
|
1680
|
+
let migrationsFailed = 0;
|
|
1681
|
+
|
|
1682
|
+
if (firstFailureIndex === -1) {
|
|
1683
|
+
// No failures: all pending migrations applied
|
|
1684
|
+
migrationsApplied = actuallyPendingMigrations.length;
|
|
1685
|
+
} else {
|
|
1686
|
+
// Failure occurred
|
|
1687
|
+
migrationsApplied = firstFailureIndex;
|
|
1688
|
+
migrationsFailed = 1;
|
|
1689
|
+
// Remaining migrations not attempted (not counted as skipped or failed)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Verify counts
|
|
1693
|
+
expect(migrationsApplied).toBeGreaterThanOrEqual(0);
|
|
1694
|
+
expect(migrationsSkipped).toBeGreaterThanOrEqual(0);
|
|
1695
|
+
expect(migrationsFailed).toBeGreaterThanOrEqual(0);
|
|
1696
|
+
expect(migrationsFailed).toBeLessThanOrEqual(1); // At most one failure (halt-on-failure)
|
|
1697
|
+
|
|
1698
|
+
// Verify total accounting
|
|
1699
|
+
const totalProcessed = migrationsApplied + migrationsSkipped + migrationsFailed;
|
|
1700
|
+
const totalAttempted = actuallyPendingMigrations.length + alreadyAppliedMigrations.length;
|
|
1701
|
+
|
|
1702
|
+
if (firstFailureIndex === -1) {
|
|
1703
|
+
// No failures: all migrations processed
|
|
1704
|
+
expect(totalProcessed).toBe(totalAttempted);
|
|
1705
|
+
} else {
|
|
1706
|
+
// Failure: only migrations up to and including failure are processed
|
|
1707
|
+
expect(migrationsApplied + migrationsFailed).toBe(firstFailureIndex + 1);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
),
|
|
1711
|
+
PROPERTY_CONFIG
|
|
1712
|
+
);
|
|
1713
|
+
});
|
|
1714
|
+
});
|