@cachly-dev/mcp-server 0.10.55 → 0.10.57
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/.github/workflows/eas-build-android.yml +58 -0
- package/.github/workflows/eas-update.yml +55 -0
- package/.github/workflows/publish.yml +2 -2
- package/BENCH.md +91 -0
- package/CHANGELOG.md +44 -0
- package/README.md +6 -34
- package/STRATEGY.md +228 -0
- package/apps/travel-chaos-organizer/.env.example +49 -0
- package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
- package/apps/travel-chaos-organizer/Makefile +57 -0
- package/apps/travel-chaos-organizer/README.md +159 -0
- package/apps/travel-chaos-organizer/backend/.env.example +10 -0
- package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
- package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
- package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
- package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
- package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
- package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
- package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
- package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
- package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
- package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
- package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
- package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
- package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
- package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
- package/apps/travel-chaos-organizer/frontend/package.json +44 -0
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
- package/apps/travel-chaos-organizer/landing/index.html +773 -0
- package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
- package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
- package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
- package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
- package/dist/packages/telegram-notify/client.d.ts +19 -0
- package/dist/packages/telegram-notify/client.js +75 -0
- package/dist/packages/telegram-notify/client.js.map +1 -0
- package/dist/src/__tests__/auth.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -0
- package/dist/src/__tests__/confidence.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/src/__tests__/e2e/config.js.map +1 -0
- package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
- package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/src/__tests__/namespace.test.js.map +1 -0
- package/dist/src/__tests__/rerank.test.js +115 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +37 -0
- package/dist/src/bench/fixtures.js +225 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/ckg.js.map +1 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/src/handlers/advanced.js.map +1 -0
- package/dist/{handlers → src/handlers}/brain.js +335 -168
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.js +7 -3
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/{handlers → src/handlers}/team.js +57 -7
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +209 -420
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +55 -0
- package/dist/src/rerank.js +111 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/search.test.d.ts +1 -0
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +226 -0
- package/dist/{tools.js → src/tools.js} +196 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +8 -7
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/server.json +3 -3
- package/smithery.yaml +21 -8
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +130 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +256 -0
- package/src/handlers/brain.ts +345 -172
- package/src/handlers/context.ts +9 -4
- package/src/handlers/fedbrain.ts +21 -7
- package/src/handlers/roadmap.ts +7 -3
- package/src/handlers/tco.ts +347 -0
- package/src/handlers/team.ts +67 -7
- package/src/index.ts +224 -406
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +148 -0
- package/src/tools.ts +210 -0
- package/tsconfig.json +2 -3
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
- /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
- /package/dist/{auth.js → src/auth.js} +0 -0
- /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
- /package/dist/{ckg.js → src/ckg.js} +0 -0
- /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
- /package/dist/{confidence.js → src/confidence.js} +0 -0
- /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
- /package/dist/{embeddings.js → src/embeddings.js} +0 -0
- /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/advanced.js +0 -0
- /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.js +0 -0
- /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.js +0 -0
- /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
- /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{search.js → src/search.js} +0 -0
- /package/dist/{search.test.js → src/search.test.js} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Cachly Redis cache layer (services/cache.py).
|
|
3
|
+
Uses unittest.mock — no real Redis required.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import pytest
|
|
7
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ── No-Redis path ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
def test_cachly_configured_false_when_no_env(monkeypatch):
|
|
13
|
+
monkeypatch.delenv("CACHLY_REDIS_URL", raising=False)
|
|
14
|
+
from app.services import cache as c
|
|
15
|
+
assert c.cachly_configured() is False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_cachly_configured_true_when_env_set(monkeypatch):
|
|
19
|
+
monkeypatch.setenv("CACHLY_REDIS_URL", "redis://:pw@host:6380/0")
|
|
20
|
+
from app.services import cache as c
|
|
21
|
+
assert c.cachly_configured() is True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def test_cache_get_returns_none_when_no_redis(monkeypatch):
|
|
26
|
+
monkeypatch.delenv("CACHLY_REDIS_URL", raising=False)
|
|
27
|
+
import importlib
|
|
28
|
+
from app.services import cache as c
|
|
29
|
+
# Reset module-level state
|
|
30
|
+
c._redis_client = None
|
|
31
|
+
c._redis_available = False
|
|
32
|
+
|
|
33
|
+
result = await c.cache_get("some content")
|
|
34
|
+
assert result is None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_cache_set_no_ops_when_no_redis(monkeypatch):
|
|
39
|
+
monkeypatch.delenv("CACHLY_REDIS_URL", raising=False)
|
|
40
|
+
from app.services import cache as c
|
|
41
|
+
c._redis_client = None
|
|
42
|
+
c._redis_available = False
|
|
43
|
+
|
|
44
|
+
# Should not raise
|
|
45
|
+
await c.cache_set("some content", {"type": "flight"})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── With mocked Redis ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_cache_get_returns_dict_on_hit():
|
|
52
|
+
from app.services import cache as c
|
|
53
|
+
|
|
54
|
+
mock_redis = AsyncMock()
|
|
55
|
+
mock_redis.get = AsyncMock(return_value=json.dumps({"type": "flight", "title": "LH 400"}))
|
|
56
|
+
|
|
57
|
+
c._redis_client = mock_redis
|
|
58
|
+
c._redis_available = True
|
|
59
|
+
|
|
60
|
+
result = await c.cache_get("flight booking text")
|
|
61
|
+
assert result == {"type": "flight", "title": "LH 400"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_cache_get_returns_none_on_miss():
|
|
66
|
+
from app.services import cache as c
|
|
67
|
+
|
|
68
|
+
mock_redis = AsyncMock()
|
|
69
|
+
mock_redis.get = AsyncMock(return_value=None)
|
|
70
|
+
|
|
71
|
+
c._redis_client = mock_redis
|
|
72
|
+
c._redis_available = True
|
|
73
|
+
|
|
74
|
+
result = await c.cache_get("unknown content")
|
|
75
|
+
assert result is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_cache_set_stores_json():
|
|
80
|
+
from app.services import cache as c
|
|
81
|
+
|
|
82
|
+
mock_redis = AsyncMock()
|
|
83
|
+
mock_redis.set = AsyncMock()
|
|
84
|
+
|
|
85
|
+
c._redis_client = mock_redis
|
|
86
|
+
c._redis_available = True
|
|
87
|
+
|
|
88
|
+
parsed = {"type": "hotel", "title": "Hilton Berlin"}
|
|
89
|
+
await c.cache_set("hotel email text", parsed)
|
|
90
|
+
|
|
91
|
+
mock_redis.set.assert_called_once()
|
|
92
|
+
call_args = mock_redis.set.call_args
|
|
93
|
+
assert call_args[1]["ex"] == c.CACHE_TTL
|
|
94
|
+
stored = json.loads(call_args[0][1])
|
|
95
|
+
assert stored["type"] == "hotel"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_cache_get_survives_redis_error():
|
|
100
|
+
from app.services import cache as c
|
|
101
|
+
|
|
102
|
+
mock_redis = AsyncMock()
|
|
103
|
+
mock_redis.get = AsyncMock(side_effect=Exception("connection refused"))
|
|
104
|
+
|
|
105
|
+
c._redis_client = mock_redis
|
|
106
|
+
c._redis_available = True
|
|
107
|
+
|
|
108
|
+
result = await c.cache_get("any content")
|
|
109
|
+
assert result is None # never raises
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_cache_set_survives_redis_error():
|
|
114
|
+
from app.services import cache as c
|
|
115
|
+
|
|
116
|
+
mock_redis = AsyncMock()
|
|
117
|
+
mock_redis.set = AsyncMock(side_effect=Exception("timeout"))
|
|
118
|
+
|
|
119
|
+
c._redis_client = mock_redis
|
|
120
|
+
c._redis_available = True
|
|
121
|
+
|
|
122
|
+
await c.cache_set("any content", {"type": "other"}) # never raises
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cache_key_is_deterministic():
|
|
126
|
+
from app.services import cache as c
|
|
127
|
+
|
|
128
|
+
k1 = c._cache_key("same text")
|
|
129
|
+
k2 = c._cache_key("same text")
|
|
130
|
+
k3 = c._cache_key("different text")
|
|
131
|
+
|
|
132
|
+
assert k1 == k2
|
|
133
|
+
assert k1 != k3
|
|
134
|
+
assert k1.startswith("tco:parse:")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_cache_key_works_for_bytes():
|
|
138
|
+
from app.services import cache as c
|
|
139
|
+
|
|
140
|
+
kb = c._cache_key(b"\x89PNG binary")
|
|
141
|
+
assert kb.startswith("tco:parse:")
|
|
142
|
+
assert len(kb) == len("tco:parse:") + 16
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tests for email drip sequence logic."""
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import patch, AsyncMock
|
|
4
|
+
from sqlalchemy import text
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_drip_dry_run_returns_plan(client, db_session):
|
|
11
|
+
"""Dry run returns what would be sent without sending."""
|
|
12
|
+
r = await client.post("/admin/drip/run?dry_run=true",
|
|
13
|
+
headers={"Authorization": "Basic YWRtaW46Y2hhbmdlbWU="}) # admin:changeme
|
|
14
|
+
# Either 200 (admin auth works in test) or 401 (expected in test env)
|
|
15
|
+
assert r.status_code in (200, 401)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_drip_run_admin_required(client):
|
|
20
|
+
"""Drip run without auth returns 401."""
|
|
21
|
+
r = await client.post("/admin/drip/run")
|
|
22
|
+
assert r.status_code == 401
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_drip_no_users_no_emails(db_session: AsyncSession):
|
|
27
|
+
"""run_drip with no users sends nothing."""
|
|
28
|
+
from app.services.drip import run_drip
|
|
29
|
+
with patch("app.services.email._send", new_callable=AsyncMock) as mock_send:
|
|
30
|
+
result = await run_drip(db_session, dry_run=True)
|
|
31
|
+
mock_send.assert_not_called()
|
|
32
|
+
assert isinstance(result, dict)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
E2E chain test: parse text → inbox → assign to trip.
|
|
3
|
+
Covers the full happy path a user would take.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import patch, AsyncMock
|
|
9
|
+
from httpx import AsyncClient
|
|
10
|
+
from sqlalchemy import text
|
|
11
|
+
from tests.conftest import FAKE_USER_ID
|
|
12
|
+
|
|
13
|
+
# A fixed UUID for the "other user's trip" in cross-user tests
|
|
14
|
+
OTHER_TRIP_ID = "c3d4e5f6-a7b8-9012-cdef-123456789012"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.asyncio
|
|
18
|
+
async def test_full_parse_to_assign_chain(client: AsyncClient, db_session, mock_ollama_flight):
|
|
19
|
+
"""Parse text → lands in inbox → assign to trip → trip has item."""
|
|
20
|
+
# 1. Create a trip
|
|
21
|
+
r = await client.post("/api/v1/trips", json={
|
|
22
|
+
"name": "E2E Test Trip", "description": "Chain test",
|
|
23
|
+
"start_date": "2025-08-01", "end_date": "2025-08-10"
|
|
24
|
+
})
|
|
25
|
+
assert r.status_code == 201
|
|
26
|
+
trip_id = r.json()["id"]
|
|
27
|
+
|
|
28
|
+
# 2. Parse text → inbox (no trip_id)
|
|
29
|
+
with patch("app.services.telemetry.track", new_callable=AsyncMock):
|
|
30
|
+
r = await client.post("/api/v1/parse/text", data={"raw_text": "Lufthansa Buchungsbestaetigung LH400 FRA-JFK"})
|
|
31
|
+
assert r.status_code == 200
|
|
32
|
+
assert r.json()["parsed"]["type"] == "flight"
|
|
33
|
+
|
|
34
|
+
# 3. Fetch inbox
|
|
35
|
+
r = await client.get("/api/v1/inbox?status_filter=pending")
|
|
36
|
+
assert r.status_code == 200
|
|
37
|
+
inbox = r.json()
|
|
38
|
+
assert len(inbox) >= 1
|
|
39
|
+
inbox_id = inbox[0]["id"]
|
|
40
|
+
|
|
41
|
+
# 4. Assign inbox item to trip
|
|
42
|
+
r = await client.post(f"/api/v1/inbox/{inbox_id}/assign",
|
|
43
|
+
json={"trip_id": trip_id, "type": "flight"})
|
|
44
|
+
assert r.status_code == 200
|
|
45
|
+
trip_item_id = r.json()["trip_item_id"]
|
|
46
|
+
assert trip_item_id
|
|
47
|
+
|
|
48
|
+
# 5. Verify item appears in trip timeline
|
|
49
|
+
r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
50
|
+
assert r.status_code == 200
|
|
51
|
+
items = r.json()
|
|
52
|
+
assert any(i["id"] == trip_item_id for i in items)
|
|
53
|
+
|
|
54
|
+
# 6. Inbox item is now "assigned"
|
|
55
|
+
r = await client.get("/api/v1/inbox?status_filter=assigned")
|
|
56
|
+
assert r.status_code == 200
|
|
57
|
+
assigned = [i for i in r.json() if i["id"] == inbox_id]
|
|
58
|
+
assert len(assigned) == 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_cross_user_inbox_assign_blocked(client: AsyncClient, db_session, mock_ollama_flight):
|
|
63
|
+
"""Cannot assign inbox item to another user's trip."""
|
|
64
|
+
# Create a trip owned by a DIFFERENT user with a valid UUID
|
|
65
|
+
await db_session.execute(
|
|
66
|
+
text("INSERT INTO trips (id, user_id, name) VALUES (:id, :uid, :name)"),
|
|
67
|
+
{"id": OTHER_TRIP_ID, "uid": "other-user", "name": "Other Trip"}
|
|
68
|
+
)
|
|
69
|
+
await db_session.commit()
|
|
70
|
+
|
|
71
|
+
# Parse something into inbox
|
|
72
|
+
with patch("app.services.telemetry.track", new_callable=AsyncMock):
|
|
73
|
+
r = await client.post("/api/v1/parse/text", data={"raw_text": "Booking confirmation XYZ123"})
|
|
74
|
+
assert r.status_code == 200
|
|
75
|
+
|
|
76
|
+
r = await client.get("/api/v1/inbox?status_filter=pending")
|
|
77
|
+
inbox_id = r.json()[0]["id"]
|
|
78
|
+
|
|
79
|
+
# Try to assign to other user's trip — must be 403
|
|
80
|
+
r = await client.post(f"/api/v1/inbox/{inbox_id}/assign",
|
|
81
|
+
json={"trip_id": OTHER_TRIP_ID, "type": "other"})
|
|
82
|
+
assert r.status_code == 403
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_trip_search_returns_correct_results(client: AsyncClient, db_session):
|
|
87
|
+
"""Search finds trips by name and description."""
|
|
88
|
+
# Create two trips
|
|
89
|
+
await client.post("/api/v1/trips", json={"name": "Barcelona Sommer", "description": "Strand"})
|
|
90
|
+
await client.post("/api/v1/trips", json={"name": "Winter Wien", "description": "Kultur"})
|
|
91
|
+
|
|
92
|
+
r = await client.get("/api/v1/trips/search?q=Barcelona")
|
|
93
|
+
assert r.status_code == 200
|
|
94
|
+
results = r.json()
|
|
95
|
+
assert any("Barcelona" in t["name"] for t in results)
|
|
96
|
+
assert all("Wien" not in t["name"] for t in results)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_delete_trip_cascades_cleanup(client: AsyncClient, db_session):
|
|
101
|
+
"""Deleting a trip removes it from the list."""
|
|
102
|
+
r = await client.post("/api/v1/trips", json={"name": "Delete Me Trip"})
|
|
103
|
+
trip_id = r.json()["id"]
|
|
104
|
+
|
|
105
|
+
r = await client.delete(f"/api/v1/trips/{trip_id}")
|
|
106
|
+
assert r.status_code == 204
|
|
107
|
+
|
|
108
|
+
r = await client.get("/api/v1/trips")
|
|
109
|
+
assert all(t["id"] != trip_id for t in r.json())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_waitlist_signup_deduplication(client: AsyncClient):
|
|
114
|
+
"""Same email can't sign up twice."""
|
|
115
|
+
email = "e2e-dedup@test.com"
|
|
116
|
+
r1 = await client.post("/api/v1/waitlist", json={"email": email})
|
|
117
|
+
assert r1.status_code in (200, 201)
|
|
118
|
+
|
|
119
|
+
r2 = await client.post("/api/v1/waitlist", json={"email": email})
|
|
120
|
+
# Should succeed (409 or 200 with "already registered") — not 500
|
|
121
|
+
assert r2.status_code < 500
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tests for PDF export endpoint."""
|
|
2
|
+
import uuid
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import patch, AsyncMock
|
|
5
|
+
from httpx import AsyncClient
|
|
6
|
+
from sqlalchemy import text
|
|
7
|
+
from tests.conftest import FAKE_USER_ID
|
|
8
|
+
|
|
9
|
+
EXPORT_TRIP_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
async def test_pdf_export_trip_not_found(client: AsyncClient):
|
|
14
|
+
"""Returns 404 for non-existent trip."""
|
|
15
|
+
r = await client.get(f"/api/v1/trips/{uuid.uuid4()}/export/pdf")
|
|
16
|
+
assert r.status_code == 404
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_pdf_export_returns_pdf(client: AsyncClient, db_session):
|
|
21
|
+
"""Creates a trip and exports it as PDF — verifies content-type."""
|
|
22
|
+
# Create a trip directly in DB using a valid UUID
|
|
23
|
+
await db_session.execute(
|
|
24
|
+
text("INSERT INTO trips (id, user_id, name, start_date, end_date) VALUES (:id, :uid, :name, :sd, :ed)"),
|
|
25
|
+
{"id": EXPORT_TRIP_ID, "uid": FAKE_USER_ID, "name": "Export Test Trip",
|
|
26
|
+
"sd": "2025-06-01", "ed": "2025-06-07"}
|
|
27
|
+
)
|
|
28
|
+
await db_session.commit()
|
|
29
|
+
fake_pdf = b"%PDF-1.4 fake content for test"
|
|
30
|
+
with patch("app.routers.export._build_pdf", return_value=fake_pdf), \
|
|
31
|
+
patch("app.services.telemetry.track", new_callable=AsyncMock):
|
|
32
|
+
r = await client.get(f"/api/v1/trips/{EXPORT_TRIP_ID}/export/pdf")
|
|
33
|
+
assert r.status_code == 200
|
|
34
|
+
assert r.headers["content-type"] == "application/pdf"
|
|
35
|
+
assert len(r.content) > 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
async def test_pdf_export_other_users_trip_returns_404(client: AsyncClient, db_session):
|
|
40
|
+
"""Cannot export another user's trip."""
|
|
41
|
+
other_trip_id = "b2c3d4e5-f6a7-8901-bcde-f12345678901"
|
|
42
|
+
await db_session.execute(
|
|
43
|
+
text("INSERT INTO trips (id, user_id, name) VALUES (:id, :uid, :name)"),
|
|
44
|
+
{"id": other_trip_id, "uid": "other-user-999", "name": "Secret Trip"}
|
|
45
|
+
)
|
|
46
|
+
await db_session.commit()
|
|
47
|
+
r = await client.get(f"/api/v1/trips/{other_trip_id}/export/pdf")
|
|
48
|
+
assert r.status_code == 404
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for /api/v1/inbox
|
|
3
|
+
|
|
4
|
+
Covers:
|
|
5
|
+
list_inbox – empty, pending filter, other status filters
|
|
6
|
+
assign – moves item to trip, updates status to 'assigned', migrates attachments
|
|
7
|
+
reject – sets status to 'rejected', idempotent on 404
|
|
8
|
+
|
|
9
|
+
Run: pytest tests/test_inbox.py -v
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from httpx import AsyncClient
|
|
14
|
+
from tests.conftest import make_trip
|
|
15
|
+
|
|
16
|
+
pytestmark = pytest.mark.asyncio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _create_trip(client: AsyncClient) -> str:
|
|
20
|
+
r = await client.post("/api/v1/trips", json=make_trip())
|
|
21
|
+
assert r.status_code == 201
|
|
22
|
+
return r.json()["id"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def _seed_inbox(client: AsyncClient) -> str:
|
|
26
|
+
"""Parse some text so it lands in inbox (no trip_id → inbox)."""
|
|
27
|
+
r = await client.post(
|
|
28
|
+
"/api/v1/parse/text",
|
|
29
|
+
data={"raw_text": "Flight LH 400 Frankfurt to New York on Aug 15"},
|
|
30
|
+
)
|
|
31
|
+
assert r.status_code == 200
|
|
32
|
+
# fetch inbox to get the id
|
|
33
|
+
inbox = await client.get("/api/v1/inbox")
|
|
34
|
+
assert inbox.status_code == 200
|
|
35
|
+
items = inbox.json()
|
|
36
|
+
assert len(items) > 0
|
|
37
|
+
return items[0]["id"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── list ──────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
async def test_inbox_empty(client):
|
|
43
|
+
r = await client.get("/api/v1/inbox")
|
|
44
|
+
assert r.status_code == 200
|
|
45
|
+
assert r.json() == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def test_inbox_default_status_is_pending(client, mock_ollama_flight):
|
|
49
|
+
await _seed_inbox(client)
|
|
50
|
+
r = await client.get("/api/v1/inbox")
|
|
51
|
+
assert r.status_code == 200
|
|
52
|
+
items = r.json()
|
|
53
|
+
assert all(i["status"] == "pending" for i in items)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def test_inbox_filter_by_status(client, mock_ollama_flight):
|
|
57
|
+
inbox_id = await _seed_inbox(client)
|
|
58
|
+
await client.delete(f"/api/v1/inbox/{inbox_id}")
|
|
59
|
+
|
|
60
|
+
r = await client.get("/api/v1/inbox?status_filter=rejected")
|
|
61
|
+
assert r.status_code == 200
|
|
62
|
+
assert any(i["id"] == inbox_id for i in r.json())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── assign ────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
async def test_assign_creates_trip_item(client, mock_ollama_flight):
|
|
68
|
+
trip_id = await _create_trip(client)
|
|
69
|
+
inbox_id = await _seed_inbox(client)
|
|
70
|
+
|
|
71
|
+
r = await client.post(
|
|
72
|
+
f"/api/v1/inbox/{inbox_id}/assign",
|
|
73
|
+
json={"trip_id": trip_id, "type": "flight"},
|
|
74
|
+
)
|
|
75
|
+
assert r.status_code == 200
|
|
76
|
+
assert "trip_item_id" in r.json()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def test_assign_marks_inbox_as_assigned(client, mock_ollama_flight):
|
|
80
|
+
trip_id = await _create_trip(client)
|
|
81
|
+
inbox_id = await _seed_inbox(client)
|
|
82
|
+
await client.post(f"/api/v1/inbox/{inbox_id}/assign",
|
|
83
|
+
json={"trip_id": trip_id, "type": "flight"})
|
|
84
|
+
|
|
85
|
+
r = await client.get("/api/v1/inbox?status_filter=assigned")
|
|
86
|
+
assert any(i["id"] == inbox_id for i in r.json())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def test_assign_not_found(client):
|
|
90
|
+
r = await client.post(
|
|
91
|
+
"/api/v1/inbox/00000000-0000-0000-0000-000000000000/assign",
|
|
92
|
+
json={"trip_id": "00000000-0000-0000-0000-000000000001", "type": "other"},
|
|
93
|
+
)
|
|
94
|
+
assert r.status_code == 404
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── reject ────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
async def test_reject_inbox_item(client, mock_ollama_flight):
|
|
100
|
+
inbox_id = await _seed_inbox(client)
|
|
101
|
+
r = await client.delete(f"/api/v1/inbox/{inbox_id}")
|
|
102
|
+
assert r.status_code == 204
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def test_reject_not_found(client):
|
|
106
|
+
r = await client.delete("/api/v1/inbox/00000000-0000-0000-0000-000000000000")
|
|
107
|
+
assert r.status_code == 404
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for /api/v1/trips/{trip_id}/items
|
|
3
|
+
|
|
4
|
+
Covers:
|
|
5
|
+
list_items – empty, sorted by event_at, trip isolation
|
|
6
|
+
create_item – happy path, optional fields, missing title → 422
|
|
7
|
+
update_item – partial patch, type change, 404 for wrong trip
|
|
8
|
+
delete_item – removes item, 404 guard
|
|
9
|
+
timeline order – items sorted chronologically regardless of insert order
|
|
10
|
+
|
|
11
|
+
Run: pytest tests/test_items.py -v
|
|
12
|
+
"""
|
|
13
|
+
import pytest
|
|
14
|
+
from httpx import AsyncClient
|
|
15
|
+
from tests.conftest import make_trip, make_item
|
|
16
|
+
|
|
17
|
+
pytestmark = pytest.mark.asyncio
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _create_trip(client: AsyncClient) -> str:
|
|
21
|
+
r = await client.post("/api/v1/trips", json=make_trip())
|
|
22
|
+
assert r.status_code == 201
|
|
23
|
+
return r.json()["id"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def _create_item(client: AsyncClient, trip_id: str, **kwargs) -> dict:
|
|
27
|
+
r = await client.post(f"/api/v1/trips/{trip_id}/items", json=make_item(**kwargs))
|
|
28
|
+
assert r.status_code == 201, r.text
|
|
29
|
+
return r.json()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── list ──────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
async def test_list_items_empty(client):
|
|
35
|
+
trip_id = await _create_trip(client)
|
|
36
|
+
r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
37
|
+
assert r.status_code == 200
|
|
38
|
+
assert r.json() == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def test_list_items_wrong_trip_404(client):
|
|
42
|
+
r = await client.get("/api/v1/trips/00000000-0000-0000-0000-000000000000/items")
|
|
43
|
+
assert r.status_code == 404
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def test_timeline_sorted_by_event_at(client):
|
|
47
|
+
trip_id = await _create_trip(client)
|
|
48
|
+
await _create_item(client, trip_id, title="Late flight", event_at="2024-08-20T18:00:00")
|
|
49
|
+
await _create_item(client, trip_id, title="Early flight", event_at="2024-08-15T06:00:00")
|
|
50
|
+
await _create_item(client, trip_id, title="Mid hotel", event_at="2024-08-17T14:00:00")
|
|
51
|
+
|
|
52
|
+
r = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
53
|
+
titles = [i["title"] for i in r.json()]
|
|
54
|
+
assert titles == ["Early flight", "Mid hotel", "Late flight"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── create ────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
async def test_create_item_happy_path(client):
|
|
60
|
+
trip_id = await _create_trip(client)
|
|
61
|
+
r = await client.post(f"/api/v1/trips/{trip_id}/items", json=make_item(title="LH 400 FRA→JFK"))
|
|
62
|
+
assert r.status_code == 201
|
|
63
|
+
body = r.json()
|
|
64
|
+
assert body["title"] == "LH 400 FRA→JFK"
|
|
65
|
+
assert body["type"] == "flight"
|
|
66
|
+
assert body["trip_id"] == trip_id
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def test_create_item_missing_title_rejected(client):
|
|
70
|
+
trip_id = await _create_trip(client)
|
|
71
|
+
r = await client.post(f"/api/v1/trips/{trip_id}/items", json={"type": "flight"})
|
|
72
|
+
assert r.status_code == 422
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def test_create_item_optional_fields_nullable(client):
|
|
76
|
+
trip_id = await _create_trip(client)
|
|
77
|
+
r = await client.post(f"/api/v1/trips/{trip_id}/items",
|
|
78
|
+
json={"title": "Minimal item", "type": "other"})
|
|
79
|
+
assert r.status_code == 201
|
|
80
|
+
body = r.json()
|
|
81
|
+
assert body["event_at"] is None
|
|
82
|
+
assert body["booking_ref"] is None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def test_create_item_with_parsed_data(client):
|
|
86
|
+
trip_id = await _create_trip(client)
|
|
87
|
+
parsed = {"origin": "FRA", "destination": "JFK", "confidence": 0.95}
|
|
88
|
+
r = await client.post(f"/api/v1/trips/{trip_id}/items",
|
|
89
|
+
json={**make_item(), "parsed_data": parsed})
|
|
90
|
+
assert r.status_code == 201
|
|
91
|
+
assert r.json()["parsed_data"]["origin"] == "FRA"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── update ────────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
async def test_update_item_type(client):
|
|
97
|
+
trip_id = await _create_trip(client)
|
|
98
|
+
item = await _create_item(client, trip_id)
|
|
99
|
+
r = await client.patch(f"/api/v1/trips/{trip_id}/items/{item['id']}",
|
|
100
|
+
json={"type": "hotel"})
|
|
101
|
+
assert r.status_code == 200
|
|
102
|
+
assert r.json()["type"] == "hotel"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def test_update_item_not_found(client):
|
|
106
|
+
trip_id = await _create_trip(client)
|
|
107
|
+
r = await client.patch(
|
|
108
|
+
f"/api/v1/trips/{trip_id}/items/00000000-0000-0000-0000-000000000000",
|
|
109
|
+
json={"title": "Ghost"},
|
|
110
|
+
)
|
|
111
|
+
assert r.status_code == 404
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def test_update_item_empty_body_rejected(client):
|
|
115
|
+
trip_id = await _create_trip(client)
|
|
116
|
+
item = await _create_item(client, trip_id)
|
|
117
|
+
r = await client.patch(f"/api/v1/trips/{trip_id}/items/{item['id']}", json={})
|
|
118
|
+
assert r.status_code == 400
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── delete ────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async def test_delete_item(client):
|
|
124
|
+
trip_id = await _create_trip(client)
|
|
125
|
+
item = await _create_item(client, trip_id)
|
|
126
|
+
r = await client.delete(f"/api/v1/trips/{trip_id}/items/{item['id']}")
|
|
127
|
+
assert r.status_code == 204
|
|
128
|
+
|
|
129
|
+
items = await client.get(f"/api/v1/trips/{trip_id}/items")
|
|
130
|
+
assert items.json() == []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def test_delete_item_not_found(client):
|
|
134
|
+
trip_id = await _create_trip(client)
|
|
135
|
+
r = await client.delete(
|
|
136
|
+
f"/api/v1/trips/{trip_id}/items/00000000-0000-0000-0000-000000000000"
|
|
137
|
+
)
|
|
138
|
+
assert r.status_code == 404
|