@cachly-dev/mcp-server 0.10.65 → 0.10.67
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/dist/src/embeddings.js +10 -0
- package/dist/src/embeddings.js.map +1 -1
- package/dist/src/handlers/brain.js +2 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/cache.js +4 -0
- package/dist/src/handlers/cache.js.map +1 -1
- package/dist/src/handlers/tco.js +1 -0
- package/dist/src/handlers/tco.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/package.json +7 -1
- package/server.json +2 -2
- package/.env.test.example +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/eas-build-android.yml +0 -58
- package/.github/workflows/eas-update.yml +0 -55
- package/.github/workflows/publish.yml +0 -72
- package/BENCH.md +0 -113
- package/CHANGELOG.md +0 -653
- package/Dockerfile +0 -16
- package/LAUNCH_TEXTS.md +0 -182
- package/PROGRESS.md +0 -141
- package/STRATEGY.md +0 -228
- package/VISION_10X.md +0 -133
- package/apps/travel-chaos-organizer/.env.example +0 -49
- package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
- package/apps/travel-chaos-organizer/Makefile +0 -57
- package/apps/travel-chaos-organizer/README.md +0 -159
- package/apps/travel-chaos-organizer/backend/.env.example +0 -10
- package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
- package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
- 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 +0 -81
- package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
- package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
- package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
- package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
- package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
- package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
- package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
- package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
- package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
- package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
- package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
- package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
- package/apps/travel-chaos-organizer/frontend/package.json +0 -44
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
- package/apps/travel-chaos-organizer/landing/index.html +0 -773
- package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
- package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
- package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
- package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
- package/dist/src/__tests__/auth.test.d.ts +0 -1
- package/dist/src/__tests__/auth.test.js +0 -107
- package/dist/src/__tests__/auth.test.js.map +0 -1
- package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
- package/dist/src/__tests__/brain-flow.test.js +0 -1241
- package/dist/src/__tests__/brain-flow.test.js.map +0 -1
- package/dist/src/__tests__/confidence.test.d.ts +0 -13
- package/dist/src/__tests__/confidence.test.js +0 -233
- package/dist/src/__tests__/confidence.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
- package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-health.test.js +0 -95
- package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
- package/dist/src/__tests__/e2e/cli.test.js +0 -204
- package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/src/__tests__/e2e/config.d.ts +0 -35
- package/dist/src/__tests__/e2e/config.js +0 -53
- package/dist/src/__tests__/e2e/config.js.map +0 -1
- package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
- package/dist/src/__tests__/e2e/funnel.test.js +0 -90
- package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
- package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
- package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
- package/dist/src/__tests__/handlers-unit.test.js +0 -505
- package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/src/__tests__/namespace.test.d.ts +0 -1
- package/dist/src/__tests__/namespace.test.js +0 -114
- package/dist/src/__tests__/namespace.test.js.map +0 -1
- package/dist/src/__tests__/rerank.test.d.ts +0 -1
- package/dist/src/__tests__/rerank.test.js +0 -125
- package/dist/src/__tests__/rerank.test.js.map +0 -1
- package/dist/src/__tests__/utils.test.d.ts +0 -1
- package/dist/src/__tests__/utils.test.js +0 -83
- package/dist/src/__tests__/utils.test.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -39
- package/dist/src/bench/cachly-bench.js +0 -223
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js +0 -225
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/search.test.d.ts +0 -1
- package/dist/src/search.test.js +0 -713
- package/dist/src/search.test.js.map +0 -1
- package/eslint.config.js +0 -16
- package/glama.json +0 -36
- package/llms.txt +0 -155
- package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
- package/packages/telegram-notify/client.ts +0 -88
- package/packages/telegram-notify/emojis.json +0 -22
- package/packages/telegram-notify/pyproject.toml +0 -17
- package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
- package/packages/telegram-notify/telegram_notify/client.py +0 -87
- package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
- package/scripts/e2e-run.sh +0 -160
- package/scripts/generate-icons.js +0 -28
- package/smithery.yaml +0 -45
- package/src/__tests__/auth.test.ts +0 -147
- package/src/__tests__/brain-flow.test.ts +0 -1472
- package/src/__tests__/confidence.test.ts +0 -285
- package/src/__tests__/e2e/api-auth.test.ts +0 -182
- package/src/__tests__/e2e/api-health.test.ts +0 -103
- package/src/__tests__/e2e/cli.test.ts +0 -228
- package/src/__tests__/e2e/config.ts +0 -53
- package/src/__tests__/e2e/funnel.test.ts +0 -99
- package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
- package/src/__tests__/e2e/provisioning.test.ts +0 -128
- package/src/__tests__/handlers-unit.test.ts +0 -677
- package/src/__tests__/namespace.test.ts +0 -126
- package/src/__tests__/rerank.test.ts +0 -141
- package/src/__tests__/utils.test.ts +0 -81
- package/src/auth.ts +0 -46
- package/src/bench/cachly-bench.ts +0 -263
- package/src/bench/fixtures.ts +0 -256
- package/src/ckg.ts +0 -103
- package/src/confidence.ts +0 -38
- package/src/embeddings.ts +0 -217
- package/src/handlers/advanced.ts +0 -647
- package/src/handlers/brain.ts +0 -2622
- package/src/handlers/cache.ts +0 -633
- package/src/handlers/context.ts +0 -222
- package/src/handlers/fedbrain.ts +0 -1232
- package/src/handlers/instances.ts +0 -180
- package/src/handlers/roadmap.ts +0 -201
- package/src/handlers/syndicate.ts +0 -460
- package/src/handlers/tco.ts +0 -345
- package/src/handlers/team.ts +0 -1249
- package/src/index.ts +0 -2815
- package/src/namespace.ts +0 -128
- package/src/notifier.ts +0 -5
- package/src/rerank.ts +0 -148
- package/src/search.test.ts +0 -848
- package/src/search.ts +0 -1781
- package/src/tools.ts +0 -2056
- package/src/utils.ts +0 -90
- package/test-features.cjs +0 -23
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -14
package/src/search.test.ts
DELETED
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the BM25+ search engine components.
|
|
3
|
-
*
|
|
4
|
-
* Run: npx vitest run src/search.test.ts
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
8
|
-
katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP } from './index.js';
|
|
9
|
-
|
|
10
|
-
// ── Tokenizer ─────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
describe('tokenize', () => {
|
|
13
|
-
it('lowercases and splits on whitespace', () => {
|
|
14
|
-
const result = tokenize('Deploy API');
|
|
15
|
-
expect(result).toContain('deploy');
|
|
16
|
-
expect(result).toContain('api');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('filters English stopwords', () => {
|
|
20
|
-
const result = tokenize('the quick brown fox is a dog');
|
|
21
|
-
expect(result).not.toContain('the');
|
|
22
|
-
expect(result).not.toContain('is');
|
|
23
|
-
expect(result).not.toContain('a');
|
|
24
|
-
expect(result).toContain('quick');
|
|
25
|
-
expect(result).toContain('brown');
|
|
26
|
-
expect(result).toContain('fox');
|
|
27
|
-
expect(result).toContain('dog');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('filters German stopwords', () => {
|
|
31
|
-
const result = tokenize('der schnelle braune Fuchs ist ein Hund');
|
|
32
|
-
expect(result).not.toContain('der');
|
|
33
|
-
expect(result).not.toContain('ist');
|
|
34
|
-
expect(result).not.toContain('ein');
|
|
35
|
-
expect(result).toContain('schnelle');
|
|
36
|
-
expect(result).toContain('fuchs');
|
|
37
|
-
expect(result).toContain('hund');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('filters French stopwords', () => {
|
|
41
|
-
const result = tokenize('le chat est sur la table');
|
|
42
|
-
expect(result).not.toContain('le');
|
|
43
|
-
expect(result).not.toContain('est');
|
|
44
|
-
expect(result).not.toContain('sur');
|
|
45
|
-
expect(result).not.toContain('la');
|
|
46
|
-
expect(result).toContain('chat');
|
|
47
|
-
expect(result).toContain('table');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('filters Spanish stopwords', () => {
|
|
51
|
-
const result = tokenize('el gato está en la mesa');
|
|
52
|
-
expect(result).not.toContain('el');
|
|
53
|
-
expect(result).not.toContain('en');
|
|
54
|
-
expect(result).not.toContain('la');
|
|
55
|
-
expect(result).toContain('gato');
|
|
56
|
-
expect(result).toContain('mesa');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('filters Italian stopwords', () => {
|
|
60
|
-
const result = tokenize('il gatto è sul tavolo');
|
|
61
|
-
expect(result).not.toContain('il');
|
|
62
|
-
expect(result).not.toContain('sul');
|
|
63
|
-
expect(result).toContain('gatto');
|
|
64
|
-
expect(result).toContain('tavolo');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('filters Portuguese stopwords', () => {
|
|
68
|
-
const result = tokenize('o gato está na mesa');
|
|
69
|
-
expect(result).not.toContain('na');
|
|
70
|
-
expect(result).toContain('gato');
|
|
71
|
-
expect(result).toContain('mesa');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('removes single-char tokens', () => {
|
|
75
|
-
const result = tokenize('a b c deploy');
|
|
76
|
-
expect(result).toContain('deploy');
|
|
77
|
-
// single chars a, b, c must not appear
|
|
78
|
-
expect(result).not.toContain('a');
|
|
79
|
-
expect(result).not.toContain('b');
|
|
80
|
-
expect(result).not.toContain('c');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('preserves colons and dots in tokens (topic slugs)', () => {
|
|
84
|
-
const result = tokenize('deploy:api fix:routing-bug v1.2.3');
|
|
85
|
-
expect(result).toContain('deploy:api');
|
|
86
|
-
expect(result).toContain('fix:routing-bug');
|
|
87
|
-
expect(result).toContain('v1.2.3');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('handles accented characters', () => {
|
|
91
|
-
const result = tokenize('résumé café naïve über straße');
|
|
92
|
-
expect(result).toContain('résumé');
|
|
93
|
-
expect(result).toContain('café');
|
|
94
|
-
expect(result).toContain('naïve');
|
|
95
|
-
expect(result).toContain('straße');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('returns empty array for all-stopwords input', () => {
|
|
99
|
-
expect(tokenize('the is a an')).toEqual([]);
|
|
100
|
-
expect(tokenize('der die das ist ein')).toEqual([]);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ── Stopwords completeness ────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
describe('STOPWORDS', () => {
|
|
107
|
-
it('has English equivalents of German stopwords', () => {
|
|
108
|
-
// These are conceptual equivalents that should both be present
|
|
109
|
-
const pairs: [string, string][] = [
|
|
110
|
-
['the', 'der'], ['the', 'die'], ['the', 'das'],
|
|
111
|
-
['and', 'und'], ['or', 'oder'], ['but', 'aber'],
|
|
112
|
-
['is', 'ist'], ['are', 'sind'], ['was', 'war'],
|
|
113
|
-
['have', 'haben'], ['has', 'hat'], ['will', 'wird'],
|
|
114
|
-
['can', 'kann'], ['with', 'mit'], ['for', 'für'],
|
|
115
|
-
['on', 'auf'], ['from', 'von'], ['at', 'bei'],
|
|
116
|
-
['after', 'nach'], ['not', 'nicht'], ['also', 'auch'],
|
|
117
|
-
['how', 'wie'], ['what', 'was'], ['who', 'wer'],
|
|
118
|
-
['where', 'wo'], ['when', 'wann'], ['why', 'warum'],
|
|
119
|
-
['my', 'mein'], ['your', 'dein'], ['his', 'sein'],
|
|
120
|
-
];
|
|
121
|
-
for (const [en, de] of pairs) {
|
|
122
|
-
expect(STOPWORDS.has(en), `Missing EN: "${en}"`).toBe(true);
|
|
123
|
-
expect(STOPWORDS.has(de), `Missing DE: "${de}"`).toBe(true);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('contains at least 50 words per major language', () => {
|
|
128
|
-
// Sample words unique to each language
|
|
129
|
-
const enSample = ['the', 'would', 'could', 'should', 'through', 'between'];
|
|
130
|
-
const deSample = ['der', 'werden', 'können', 'zwischen', 'während', 'wegen'];
|
|
131
|
-
const frSample = ['le', 'avec', 'dans', 'pour', 'être', 'avoir'];
|
|
132
|
-
const esSample = ['el', 'para', 'como', 'pero', 'cuando', 'porque'];
|
|
133
|
-
const itSample = ['che', 'sono', 'questo', 'perché', 'ancora', 'sempre'];
|
|
134
|
-
const ptSample = ['são', 'fazer', 'estar', 'você', 'também', 'ainda'];
|
|
135
|
-
|
|
136
|
-
for (const w of [...enSample, ...deSample, ...frSample, ...esSample, ...itSample, ...ptSample]) {
|
|
137
|
-
expect(STOPWORDS.has(w), `Missing: "${w}"`).toBe(true);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ── Multi-Query Splitting ─────────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
describe('splitMultiQuery', () => {
|
|
145
|
-
it('splits numbered lists', () => {
|
|
146
|
-
const result = splitMultiQuery('1. deploy API 2. fix routing 3. check auth');
|
|
147
|
-
expect(result).toHaveLength(3);
|
|
148
|
-
expect(result[0]).toBe('deploy API');
|
|
149
|
-
expect(result[1]).toBe('fix routing');
|
|
150
|
-
expect(result[2]).toBe('check auth');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('splits semicolons', () => {
|
|
154
|
-
const result = splitMultiQuery('deploy API; fix routing; check auth');
|
|
155
|
-
expect(result).toHaveLength(3);
|
|
156
|
-
expect(result[0]).toBe('deploy API');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('splits "and also"', () => {
|
|
160
|
-
const result = splitMultiQuery('deploy API and also fix routing');
|
|
161
|
-
expect(result).toHaveLength(2);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('splits "außerdem"', () => {
|
|
165
|
-
const result = splitMultiQuery('deploy API außerdem routing fixen');
|
|
166
|
-
expect(result).toHaveLength(2);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('splits "de plus" (French)', () => {
|
|
170
|
-
const result = splitMultiQuery('déployer API de plus corriger routing');
|
|
171
|
-
expect(result).toHaveLength(2);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('splits "además" (Spanish)', () => {
|
|
175
|
-
const result = splitMultiQuery('desplegar API además corregir routing');
|
|
176
|
-
expect(result).toHaveLength(2);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('splits comma-separated with 3+ items', () => {
|
|
180
|
-
const result = splitMultiQuery('deploy, routing, auth, redis');
|
|
181
|
-
expect(result).toHaveLength(4);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('keeps single query intact', () => {
|
|
185
|
-
expect(splitMultiQuery('deploy API')).toEqual(['deploy API']);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('handles 12 numbered items', () => {
|
|
189
|
-
const q = Array.from({ length: 12 }, (_, i) => `${i + 1}. task number ${i + 1}`).join(' ');
|
|
190
|
-
const result = splitMultiQuery(q);
|
|
191
|
-
expect(result.length).toBe(12);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('filters out too-short fragments from numbered lists', () => {
|
|
195
|
-
// With short items filtered (≤2 chars), may not split at all
|
|
196
|
-
const result = splitMultiQuery('1. deploy API 2. fix routing 3. ab');
|
|
197
|
-
// "ab" is too short, but "deploy API" and "fix routing" should remain
|
|
198
|
-
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
199
|
-
expect(result[0]).toBe('deploy API');
|
|
200
|
-
expect(result[1]).toBe('fix routing');
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ── Levenshtein Distance ──────────────────────────────────────────────────────
|
|
205
|
-
|
|
206
|
-
describe('levenshtein', () => {
|
|
207
|
-
it('returns 0 for identical strings', () => {
|
|
208
|
-
expect(levenshtein('deploy', 'deploy')).toBe(0);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('returns correct distance for single edit', () => {
|
|
212
|
-
expect(levenshtein('deploy', 'deploi')).toBe(1); // substitution
|
|
213
|
-
expect(levenshtein('deploy', 'deplo')).toBe(1); // deletion
|
|
214
|
-
expect(levenshtein('deploy', 'deployy')).toBe(1); // insertion
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('returns 2 for two edits', () => {
|
|
218
|
-
expect(levenshtein('deploy', 'dploi')).toBe(2);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('returns 3+ for distant strings (early exit)', () => {
|
|
222
|
-
expect(levenshtein('deploy', 'abc')).toBeGreaterThanOrEqual(3);
|
|
223
|
-
expect(levenshtein('a', 'abcde')).toBeGreaterThanOrEqual(3);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('handles empty strings', () => {
|
|
227
|
-
expect(levenshtein('', '')).toBe(0);
|
|
228
|
-
expect(levenshtein('abc', '')).toBe(3);
|
|
229
|
-
expect(levenshtein('', 'abc')).toBe(3);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// ── Recency Boost ─────────────────────────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
describe('recencyBoost', () => {
|
|
236
|
-
it('returns 1.0 for undefined timestamp', () => {
|
|
237
|
-
expect(recencyBoost(undefined)).toBe(1.0);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('returns max boost for very recent entries', () => {
|
|
241
|
-
const boost = recencyBoost(Date.now());
|
|
242
|
-
expect(boost).toBeGreaterThan(1.4);
|
|
243
|
-
expect(boost).toBeLessThanOrEqual(1.5);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('decays over time', () => {
|
|
247
|
-
const now = Date.now();
|
|
248
|
-
const oneDay = recencyBoost(now - 1 * 24 * 60 * 60 * 1000);
|
|
249
|
-
const sevenDays = recencyBoost(now - 7 * 24 * 60 * 60 * 1000);
|
|
250
|
-
const thirtyDays = recencyBoost(now - 30 * 24 * 60 * 60 * 1000);
|
|
251
|
-
|
|
252
|
-
expect(oneDay).toBeGreaterThan(sevenDays);
|
|
253
|
-
expect(sevenDays).toBeGreaterThan(thirtyDays);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('never goes below 0.5', () => {
|
|
257
|
-
const ancient = recencyBoost(Date.now() - 365 * 24 * 60 * 60 * 1000);
|
|
258
|
-
expect(ancient).toBeGreaterThanOrEqual(0.5);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('at half-life (7 days), boost is approximately 1.0', () => {
|
|
262
|
-
const atHalfLife = recencyBoost(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
263
|
-
// 0.5^1 + 0.5 = 1.0
|
|
264
|
-
expect(atHalfLife).toBeCloseTo(1.0, 1);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// ── Timestamp Extraction ──────────────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
describe('extractTimestamp', () => {
|
|
271
|
-
it('extracts "ts" field from JSON', () => {
|
|
272
|
-
const content = '{"topic":"deploy:api","ts":"2026-04-17T12:00:00Z","what_worked":"rsync"}';
|
|
273
|
-
const ts = extractTimestamp(content);
|
|
274
|
-
expect(ts).toBeDefined();
|
|
275
|
-
expect(new Date(ts!).getFullYear()).toBe(2026);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('extracts "created" field', () => {
|
|
279
|
-
const content = '{"key":"test","created":"2026-01-01T00:00:00Z"}';
|
|
280
|
-
expect(extractTimestamp(content)).toBeDefined();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('returns undefined for non-JSON content', () => {
|
|
284
|
-
expect(extractTimestamp('just plain text about deploy')).toBeUndefined();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('returns undefined for invalid dates', () => {
|
|
288
|
-
expect(extractTimestamp('{"ts":"not-a-date"}')).toBeUndefined();
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ── Katakana → Romaji ──────────────────────────────────────────────────────────
|
|
293
|
-
|
|
294
|
-
describe('katakanaToRomaji', () => {
|
|
295
|
-
it('converts basic katakana vowels', () => {
|
|
296
|
-
expect(katakanaToRomaji('アイウエオ')).toBe('aiueo');
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('converts コンテナ (container)', () => {
|
|
300
|
-
expect(katakanaToRomaji('コンテナ')).toBe('kontena');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('converts デプロイ (deploy)', () => {
|
|
304
|
-
expect(katakanaToRomaji('デプロイ')).toBe('depuroi');
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('converts サーバー (server) — long vowel stripped', () => {
|
|
308
|
-
const r = katakanaToRomaji('サーバー');
|
|
309
|
-
expect(r).toContain('sa');
|
|
310
|
-
expect(r).toContain('ba');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('converts digraph チェック (check)', () => {
|
|
314
|
-
const r = katakanaToRomaji('チェック');
|
|
315
|
-
expect(r).toContain('che');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('converts ファイル (file)', () => {
|
|
319
|
-
const r = katakanaToRomaji('ファイル');
|
|
320
|
-
expect(r).toContain('fa');
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('handles empty string', () => {
|
|
324
|
-
expect(katakanaToRomaji('')).toBe('');
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// ── Arabic Light Stemmer ───────────────────────────────────────────────────────
|
|
329
|
-
|
|
330
|
-
describe('arabicLightStem', () => {
|
|
331
|
-
it('strips definite article ال (al-)', () => {
|
|
332
|
-
// الخطأ (al-khata = the error) → خطأ (khata)
|
|
333
|
-
expect(arabicLightStem('الخطأ')).toBe('خطأ');
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('strips و (wa-) prefix from long words', () => {
|
|
337
|
-
// وخطأ → خطأ (and error → error)
|
|
338
|
-
expect(arabicLightStem('وخطأ')).toBe('خطأ');
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it('strips ب (bi-) prefix', () => {
|
|
342
|
-
expect(arabicLightStem('بسرعة')).toBe('سرعة');
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('does not strip from short words (≤3 chars)', () => {
|
|
346
|
-
// بلد (3 chars) should not be stripped
|
|
347
|
-
expect(arabicLightStem('بلد')).toBe('بلد');
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('returns unchanged for Latin input', () => {
|
|
351
|
-
expect(arabicLightStem('deploy')).toBe('deploy');
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// ── Cross-lingual Expansion ────────────────────────────────────────────────────
|
|
356
|
-
|
|
357
|
-
describe('expandCrossLingual', () => {
|
|
358
|
-
it('returns Japanese katakana for "deploy"', () => {
|
|
359
|
-
const syns = expandCrossLingual('deploy');
|
|
360
|
-
expect(syns).toContain('デプロイ');
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('returns Chinese for "deploy"', () => {
|
|
364
|
-
expect(expandCrossLingual('deploy')).toContain('部署');
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it('returns Korean for "deploy"', () => {
|
|
368
|
-
expect(expandCrossLingual('deploy')).toContain('배포');
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('returns English for Japanese デプロイ', () => {
|
|
372
|
-
expect(expandCrossLingual('デプロイ')).toContain('deploy');
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it('returns English for Chinese 部署', () => {
|
|
376
|
-
expect(expandCrossLingual('部署')).toContain('deploy');
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('returns English for Korean 배포', () => {
|
|
380
|
-
expect(expandCrossLingual('배포')).toContain('deploy');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('returns English for Arabic خطأ (error)', () => {
|
|
384
|
-
expect(expandCrossLingual('خطأ')).toContain('error');
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('returns English for Hebrew שגיאה (error)', () => {
|
|
388
|
-
expect(expandCrossLingual('שגיאה')).toContain('error');
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('returns empty array for unknown tokens', () => {
|
|
392
|
-
expect(expandCrossLingual('foobar')).toEqual([]);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('covers all major languages for "server"', () => {
|
|
396
|
-
const syns = expandCrossLingual('server');
|
|
397
|
-
expect(syns).toContain('サーバー');
|
|
398
|
-
expect(syns).toContain('服务器');
|
|
399
|
-
expect(syns).toContain('서버');
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('has at least 50 tech terms in the map', () => {
|
|
403
|
-
expect(CROSS_LINGUAL_MAP.size).toBeGreaterThanOrEqual(50);
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// ── RTL Tokenization ───────────────────────────────────────────────────────────
|
|
408
|
-
|
|
409
|
-
describe('tokenize — RTL (Arabic/Hebrew)', () => {
|
|
410
|
-
it('tokenizes Arabic text into words', () => {
|
|
411
|
-
// "خطأ في النظام" = error in the system
|
|
412
|
-
const tokens = tokenize('خطأ في النظام');
|
|
413
|
-
expect(tokens.length).toBeGreaterThan(0);
|
|
414
|
-
// خطأ = error, not a stopword
|
|
415
|
-
const hasError = tokens.some(t => t === 'خطأ' || t === 'error');
|
|
416
|
-
expect(hasError).toBe(true);
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('strips Arabic definite article via light stemmer', () => {
|
|
420
|
-
// النظام → نظام (the system → system)
|
|
421
|
-
const tokens = tokenize('النظام');
|
|
422
|
-
expect(tokens).toContain('نظام');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('filters Arabic stopwords', () => {
|
|
426
|
-
const tokens = tokenize('في على من');
|
|
427
|
-
// All three are stopwords
|
|
428
|
-
expect(tokens.filter(t => RTL_RE_TEST(t)).length).toBe(0);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('tokenizes Hebrew text', () => {
|
|
432
|
-
// שגיאה = error
|
|
433
|
-
const tokens = tokenize('שגיאה בשרת');
|
|
434
|
-
expect(tokens.length).toBeGreaterThan(0);
|
|
435
|
-
const hasErr = tokens.some(t => t === 'שגיאה' || t === 'error');
|
|
436
|
-
expect(hasErr).toBe(true);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('handles mixed Arabic + English text', () => {
|
|
440
|
-
const tokens = tokenize('deploy خطأ error');
|
|
441
|
-
expect(tokens).toContain('deploy');
|
|
442
|
-
expect(tokens.some(t => t === 'خطأ' || t === 'error')).toBe(true);
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// Helper: test if a string contains RTL characters
|
|
447
|
-
function RTL_RE_TEST(s: string) {
|
|
448
|
-
return /[\u0590-\u05ff\u0600-\u06ff]/.test(s);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ── Cross-lingual Search Integration ──────────────────────────────────────────
|
|
452
|
-
|
|
453
|
-
describe('tokenize — cross-lingual expansion', () => {
|
|
454
|
-
it('English "deploy" expands to Japanese bigrams', () => {
|
|
455
|
-
const tokens = tokenize('deploy');
|
|
456
|
-
// デプロイ bigrams: デプ, プロ, ロイ
|
|
457
|
-
expect(tokens.some(t => t === 'デプ' || t === 'プロ' || t === 'ロイ')).toBe(true);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it('Japanese デプロイ expands to include "deploy"', () => {
|
|
461
|
-
const tokens = tokenize('デプロイ');
|
|
462
|
-
expect(tokens).toContain('deploy');
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('Chinese 部署 expands to "deploy"', () => {
|
|
466
|
-
const tokens = tokenize('部署');
|
|
467
|
-
expect(tokens).toContain('deploy');
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
it('Korean 배포 expands to "deploy"', () => {
|
|
471
|
-
const tokens = tokenize('배포');
|
|
472
|
-
expect(tokens).toContain('deploy');
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it('Arabic خطأ expands to "error"', () => {
|
|
476
|
-
const tokens = tokenize('خطأ');
|
|
477
|
-
expect(tokens).toContain('error');
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it('Hebrew שגיאה expands to "error"', () => {
|
|
481
|
-
const tokens = tokenize('שגיאה');
|
|
482
|
-
expect(tokens).toContain('error');
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it('English "container" expands to all CJK variants', () => {
|
|
486
|
-
const tokens = tokenize('container');
|
|
487
|
-
// コンテナ bigrams should be present
|
|
488
|
-
expect(tokens.some(t => t === 'コン' || t === 'ンテ' || t === 'テナ')).toBe(true);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it('katakana コンテナ generates romaji "kontena"', () => {
|
|
492
|
-
const tokens = tokenize('コンテナ');
|
|
493
|
-
expect(tokens).toContain('kontena');
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// ── Language-specific tokenizer + stemmer tests ───────────────────────────────
|
|
498
|
-
describe('multilingual tokenizer', () => {
|
|
499
|
-
// Russian
|
|
500
|
-
it('Russian: токенизирует слово и стемит суффикс', () => {
|
|
501
|
-
const tokens = tokenize('развёртывание сервера');
|
|
502
|
-
expect(tokens.some(t => t.startsWith('развёрт') || t === 'deploy' || t === 'deployment')).toBe(true);
|
|
503
|
-
});
|
|
504
|
-
it('Russian: развёртывание expands to "deploy"', () => {
|
|
505
|
-
const tokens = tokenize('развёртывание');
|
|
506
|
-
expect(tokens).toContain('deploy');
|
|
507
|
-
});
|
|
508
|
-
it('Russian: ошибка expands to "error"', () => {
|
|
509
|
-
const tokens = tokenize('ошибка');
|
|
510
|
-
expect(tokens).toContain('error');
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Turkish
|
|
514
|
-
it('Turkish: sunucu expands to "server"', () => {
|
|
515
|
-
const tokens = tokenize('sunucu');
|
|
516
|
-
expect(tokens).toContain('server');
|
|
517
|
-
});
|
|
518
|
-
it('Turkish: hata expands to "error"', () => {
|
|
519
|
-
const tokens = tokenize('hata');
|
|
520
|
-
expect(tokens).toContain('error');
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// Bengali
|
|
524
|
-
it('Bengali: tokenizes words with Bengali script', () => {
|
|
525
|
-
const tokens = tokenize('সার্ভার ডেটাবেস');
|
|
526
|
-
expect(tokens.length).toBeGreaterThan(0);
|
|
527
|
-
});
|
|
528
|
-
it('Bengali: strips -গুলো plural suffix', () => {
|
|
529
|
-
// সার্ভারগুলো → সার্ভার
|
|
530
|
-
const tokens = tokenize('সার্ভারগুলো');
|
|
531
|
-
expect(tokens.some(t => t.startsWith('\u09b8\u09be\u09b0') || t.length > 0)).toBe(true);
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
// Vietnamese
|
|
535
|
-
it('Vietnamese: tokenizes tone-marked words', () => {
|
|
536
|
-
const tokens = tokenize('triển khai máy chủ');
|
|
537
|
-
expect(tokens.some(t => t === 'tri\u1ec3n' || t === 'khai' || t === 'm\u00e1y' || t === 'ch\u1ee7')).toBe(true);
|
|
538
|
-
});
|
|
539
|
-
it('Vietnamese: triển khai expands to "deploy"', () => {
|
|
540
|
-
const tokens = tokenize('triển khai');
|
|
541
|
-
expect(tokens).toContain('deploy');
|
|
542
|
-
});
|
|
543
|
-
it('Vietnamese: lỗi expands to "error"', () => {
|
|
544
|
-
const tokens = tokenize('lỗi');
|
|
545
|
-
expect(tokens).toContain('error');
|
|
546
|
-
});
|
|
547
|
-
it('Vietnamese: stopwords filtered out', () => {
|
|
548
|
-
const tokens = tokenize('không tôi và');
|
|
549
|
-
expect(tokens).not.toContain('t\u00f4i');
|
|
550
|
-
expect(tokens).not.toContain('v\u00e0');
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
// Hindi
|
|
554
|
-
it('Hindi: तैनाती expands to "deploy"', () => {
|
|
555
|
-
const tokens = tokenize('तैनाती');
|
|
556
|
-
expect(tokens).toContain('deploy');
|
|
557
|
-
});
|
|
558
|
-
it('Hindi: त्रुटि expands to "error"', () => {
|
|
559
|
-
const tokens = tokenize('त्रुटि');
|
|
560
|
-
expect(tokens).toContain('error');
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
// Farsi
|
|
564
|
-
it('Farsi: استقرار expands to "deploy"', () => {
|
|
565
|
-
const tokens = tokenize('استقرار');
|
|
566
|
-
expect(tokens).toContain('deploy');
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// camelCase splitting
|
|
570
|
-
it('preprocessText splits camelCase', () => {
|
|
571
|
-
const tokens = tokenize('deployServer');
|
|
572
|
-
expect(tokens).toContain('deploy');
|
|
573
|
-
expect(tokens).toContain('server');
|
|
574
|
-
});
|
|
575
|
-
it('preprocessText splits snake_case', () => {
|
|
576
|
-
const tokens = tokenize('my_api_key');
|
|
577
|
-
expect(tokens).toContain('api');
|
|
578
|
-
expect(tokens).toContain('key');
|
|
579
|
-
});
|
|
580
|
-
it('preprocessText splits PascalCase HTMLParser', () => {
|
|
581
|
-
const tokens = tokenize('HTMLParser');
|
|
582
|
-
expect(tokens).toContain('html');
|
|
583
|
-
expect(tokens).toContain('parser');
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
// Tech synonyms
|
|
587
|
-
it('tech synonym: k8s expands to include kubernetes', () => {
|
|
588
|
-
const tokens = tokenize('k8s');
|
|
589
|
-
expect(tokens).toContain('kubernetes');
|
|
590
|
-
});
|
|
591
|
-
it('tech synonym: postgres expands to postgresql', () => {
|
|
592
|
-
const tokens = tokenize('postgres');
|
|
593
|
-
expect(tokens).toContain('postgresql');
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
// ── Prefix-Matching tests ─────────────────────────────────────────────────────
|
|
598
|
-
describe('keywordSearch prefix-matching', () => {
|
|
599
|
-
it('English "deploy" matches forward entries for 17 languages in cross-lingual map', () => {
|
|
600
|
-
const result = CROSS_LINGUAL_MAP.get('deploy');
|
|
601
|
-
expect(result).toBeDefined();
|
|
602
|
-
// Should include at least Vietnamese, Russian, Turkish entries
|
|
603
|
-
expect(result).toContain('triển khai');
|
|
604
|
-
expect(result).toContain('развёртывание');
|
|
605
|
-
expect(result).toContain('dağıtım');
|
|
606
|
-
});
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
// ── tokenize edge cases ───────────────────────────────────────────────────────
|
|
610
|
-
|
|
611
|
-
describe('tokenize — edge cases', () => {
|
|
612
|
-
it('handles pure numbers', () => {
|
|
613
|
-
const tokens = tokenize('404 500 200');
|
|
614
|
-
// Numbers are preserved as tokens if length > 1
|
|
615
|
-
expect(tokens.some(t => /\d/.test(t))).toBe(true);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
it('handles URLs — strips protocol and splits on slashes', () => {
|
|
619
|
-
const tokens = tokenize('https://api.cachly.dev/v1/instances');
|
|
620
|
-
expect(tokens.some(t => t.includes('cachly') || t.includes('api') || t.includes('instances'))).toBe(true);
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
it('handles markdown formatting — strips asterisks and backticks', () => {
|
|
624
|
-
const tokens = tokenize('**Deploy** the `server` now');
|
|
625
|
-
expect(tokens).toContain('deploy');
|
|
626
|
-
expect(tokens).toContain('server');
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
it('deduplicates identical tokens (from cross-lingual expansion)', () => {
|
|
630
|
-
const tokens = tokenize('deploy deploy');
|
|
631
|
-
const counts: Record<string, number> = {};
|
|
632
|
-
for (const t of tokens) counts[t] = (counts[t] ?? 0) + 1;
|
|
633
|
-
// Exact duplicates are deduplicated
|
|
634
|
-
expect(counts['deploy'] ?? 0).toBeLessThanOrEqual(2);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('handles very long single words (hashed to bigram)', () => {
|
|
638
|
-
const longWord = 'a'.repeat(50);
|
|
639
|
-
const tokens = tokenize(longWord);
|
|
640
|
-
// Should not crash and should produce at least one token
|
|
641
|
-
expect(tokens.length).toBeGreaterThanOrEqual(0);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('handles emoji in text gracefully', () => {
|
|
645
|
-
const tokens = tokenize('🚀 deploy 🔥 server');
|
|
646
|
-
expect(tokens).toContain('deploy');
|
|
647
|
-
expect(tokens).toContain('server');
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
it('handles mixed script in single sentence', () => {
|
|
651
|
-
const tokens = tokenize('deploy サーバー to production 서버');
|
|
652
|
-
expect(tokens).toContain('deploy');
|
|
653
|
-
expect(tokens.some(t => t === 'server' || t.includes('sa') || t.includes('ba'))).toBe(true);
|
|
654
|
-
});
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// ── levenshtein edge cases ────────────────────────────────────────────────────
|
|
658
|
-
|
|
659
|
-
describe('levenshtein — additional cases', () => {
|
|
660
|
-
it('single character strings', () => {
|
|
661
|
-
expect(levenshtein('a', 'a')).toBe(0);
|
|
662
|
-
expect(levenshtein('a', 'b')).toBe(1);
|
|
663
|
-
expect(levenshtein('a', 'ab')).toBe(1);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it('symmetric property: d(a,b) === d(b,a)', () => {
|
|
667
|
-
const pairs: [string, string][] = [
|
|
668
|
-
['deploy', 'deploi'],
|
|
669
|
-
['hello', 'world'],
|
|
670
|
-
['abc', ''],
|
|
671
|
-
['redis', 'reds'],
|
|
672
|
-
];
|
|
673
|
-
for (const [a, b] of pairs) {
|
|
674
|
-
expect(levenshtein(a, b)).toBe(levenshtein(b, a));
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
it('triangle inequality: d(a,c) ≤ d(a,b) + d(b,c)', () => {
|
|
679
|
-
const a = 'docker';
|
|
680
|
-
const b = 'doker'; // 1 edit from docker
|
|
681
|
-
const c = 'dcker'; // 1 edit from docker, 2 from doker
|
|
682
|
-
expect(levenshtein(a, c)).toBeLessThanOrEqual(levenshtein(a, b) + levenshtein(b, c));
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it('common tech term typos are within distance 2', () => {
|
|
686
|
-
const typos: [string, string][] = [
|
|
687
|
-
['docker', 'docekr'],
|
|
688
|
-
['redis', 'redsi'],
|
|
689
|
-
['nginx', 'nigx'],
|
|
690
|
-
['postgres', 'posgres'],
|
|
691
|
-
];
|
|
692
|
-
for (const [correct, typo] of typos) {
|
|
693
|
-
expect(levenshtein(correct, typo)).toBeLessThanOrEqual(2);
|
|
694
|
-
}
|
|
695
|
-
});
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
// ── recencyBoost edge cases ───────────────────────────────────────────────────
|
|
699
|
-
|
|
700
|
-
describe('recencyBoost — additional cases', () => {
|
|
701
|
-
it('returns 1.0 for null/undefined', () => {
|
|
702
|
-
expect(recencyBoost(undefined)).toBe(1.0);
|
|
703
|
-
expect(recencyBoost(null as unknown as undefined)).toBe(1.0);
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it('returns max boost (1.5) for timestamps in the future', () => {
|
|
707
|
-
const future = Date.now() + 60_000; // 1 minute ahead
|
|
708
|
-
const boost = recencyBoost(future);
|
|
709
|
-
expect(boost).toBeCloseTo(1.5, 1);
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
it('boost is consistent for same age', () => {
|
|
713
|
-
const ts = Date.now() - 2 * 86400000;
|
|
714
|
-
expect(recencyBoost(ts)).toBeCloseTo(recencyBoost(ts), 5);
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
it('boost values bracket correctly for known ages', () => {
|
|
718
|
-
const now = Date.now();
|
|
719
|
-
const fresh = recencyBoost(now); // 0d
|
|
720
|
-
const oneDay = recencyBoost(now - 86400000); // 1d
|
|
721
|
-
const fourDay = recencyBoost(now - 4 * 86400000); // 4d
|
|
722
|
-
const weekOld = recencyBoost(now - 7 * 86400000); // 7d
|
|
723
|
-
const monthOld = recencyBoost(now - 30 * 86400000); // 30d
|
|
724
|
-
const yearOld = recencyBoost(now - 365 * 86400000); // 1y
|
|
725
|
-
|
|
726
|
-
expect(fresh).toBeGreaterThan(1.4);
|
|
727
|
-
expect(oneDay).toBeGreaterThan(1.2);
|
|
728
|
-
expect(fourDay).toBeGreaterThan(1.0);
|
|
729
|
-
expect(weekOld).toBeCloseTo(1.0, 1);
|
|
730
|
-
expect(monthOld).toBeLessThan(0.9);
|
|
731
|
-
expect(yearOld).toBeGreaterThanOrEqual(0.5);
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
// ── extractTimestamp edge cases ───────────────────────────────────────────────
|
|
736
|
-
|
|
737
|
-
describe('extractTimestamp — additional cases', () => {
|
|
738
|
-
it('handles updated_at field', () => {
|
|
739
|
-
const content = '{"updated_at":"2026-05-01T10:00:00Z","key":"value"}';
|
|
740
|
-
const ts = extractTimestamp(content);
|
|
741
|
-
// Should find a valid date (updated_at or fall through to ts)
|
|
742
|
-
if (ts) {
|
|
743
|
-
expect(new Date(ts).getFullYear()).toBeGreaterThanOrEqual(2026);
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
it('extracts from nested JSON objects', () => {
|
|
748
|
-
const content = JSON.stringify({ meta: { ts: '2026-03-15T08:00:00Z' }, value: 'test' });
|
|
749
|
-
// ts is at top-level via regex so this might not match nested — just no crash
|
|
750
|
-
expect(() => extractTimestamp(content)).not.toThrow();
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
it('handles array-valued ts field gracefully', () => {
|
|
754
|
-
const content = '{"ts":[1,2,3],"data":"test"}';
|
|
755
|
-
expect(() => extractTimestamp(content)).not.toThrow();
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
it('handles partial JSON (truncated)', () => {
|
|
759
|
-
const content = '{"ts":"2026-01-01T00:00:00Z","data":"tr';
|
|
760
|
-
expect(() => extractTimestamp(content)).not.toThrow();
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
it('returns undefined for plain text with no JSON', () => {
|
|
764
|
-
expect(extractTimestamp('just a sentence with no dates')).toBeUndefined();
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
// ── splitMultiQuery completeness ──────────────────────────────────────────────
|
|
769
|
-
|
|
770
|
-
describe('splitMultiQuery — additional splitters', () => {
|
|
771
|
-
it('splits "furthermore" (English)', () => {
|
|
772
|
-
const r = splitMultiQuery('fix timeout furthermore increase heap');
|
|
773
|
-
expect(r.length).toBe(2);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it('splits "additionally" (English)', () => {
|
|
777
|
-
const r = splitMultiQuery('deploy api additionally update config');
|
|
778
|
-
expect(r.length).toBe(2);
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
it('splits "de plus" (French)', () => {
|
|
782
|
-
const r = splitMultiQuery('déployer API de plus corriger timeout');
|
|
783
|
-
expect(r.length).toBe(2);
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
it('splits "und außerdem" (German)', () => {
|
|
787
|
-
const r = splitMultiQuery('API deployen und außerdem Timeout erhöhen');
|
|
788
|
-
expect(r.length).toBe(2);
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
it('handles input with only separators gracefully', () => {
|
|
792
|
-
const r = splitMultiQuery(';;;');
|
|
793
|
-
// Should not crash; may return empty or single element
|
|
794
|
-
expect(Array.isArray(r)).toBe(true);
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
it('does not split on comma inside brackets', () => {
|
|
798
|
-
// "1 item" shouldn't produce garbage splits
|
|
799
|
-
const r = splitMultiQuery('deploy API');
|
|
800
|
-
expect(r).toEqual(['deploy API']);
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
it('preserves query with no splitter', () => {
|
|
804
|
-
const q = 'find the best way to deploy docker containers';
|
|
805
|
-
expect(splitMultiQuery(q)).toEqual([q]);
|
|
806
|
-
});
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
// ── CROSS_LINGUAL_MAP completeness ────────────────────────────────────────────
|
|
810
|
-
|
|
811
|
-
describe('CROSS_LINGUAL_MAP — coverage', () => {
|
|
812
|
-
const REQUIRED_EN_TERMS = [
|
|
813
|
-
'deploy', 'error', 'server', 'database', 'container',
|
|
814
|
-
'cache', 'authentication', 'timeout', 'memory', 'network',
|
|
815
|
-
];
|
|
816
|
-
|
|
817
|
-
it.each(REQUIRED_EN_TERMS)('"%s" has cross-lingual entries', (term) => {
|
|
818
|
-
const entries = CROSS_LINGUAL_MAP.get(term);
|
|
819
|
-
expect(entries, `"${term}" missing from CROSS_LINGUAL_MAP`).toBeDefined();
|
|
820
|
-
expect(entries!.length).toBeGreaterThan(0);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
it('all values are non-empty arrays of strings', () => {
|
|
824
|
-
for (const [key, values] of CROSS_LINGUAL_MAP.entries()) {
|
|
825
|
-
expect(Array.isArray(values), `Values for "${key}" are not an array`).toBe(true);
|
|
826
|
-
for (const v of values) {
|
|
827
|
-
expect(typeof v, `Value "${v}" for key "${key}" is not a string`).toBe('string');
|
|
828
|
-
expect(v.length, `Empty string in values for "${key}"`).toBeGreaterThan(0);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it('bi-directional entries exist (reverse lookups)', () => {
|
|
834
|
-
// For each forward entry, the reverse should also exist
|
|
835
|
-
let reverseCount = 0;
|
|
836
|
-
for (const [, values] of CROSS_LINGUAL_MAP.entries()) {
|
|
837
|
-
for (const v of values) {
|
|
838
|
-
if (CROSS_LINGUAL_MAP.has(v)) {
|
|
839
|
-
reverseCount++;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
// A healthy map has substantial bidirectionality
|
|
844
|
-
expect(reverseCount).toBeGreaterThan(20);
|
|
845
|
-
});
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
|