mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
Merge remote-tracking branch 'origin/main' into feature/rtl_ui
This commit is contained in:
commit
9d7c513fb7
2
.github/workflows/checks.yml
vendored
2
.github/workflows/checks.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if PRs have conflicts
|
- name: Check if PRs have conflicts
|
||||||
uses: eps1lon/actions-label-merge-conflict@v3
|
uses: eps1lon/actions-label-merge-conflict@v3
|
||||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
if: github.repository == ${{ vars.REPO_MAIN }} && ${{secrets.MERGE_CONFLICT_LABEL_PAT}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: "merge-conflicts"
|
dirtyLabel: "merge-conflicts"
|
||||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@ -95,6 +95,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
3
.github/workflows/deploy-docs.yml
vendored
3
.github/workflows/deploy-docs.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.13'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
cache-dependency-path: 'requirements-docs.txt'
|
cache-dependency-path: 'requirements-docs.txt'
|
||||||
|
|
||||||
@ -118,6 +118,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: ./.github/actions/deploy-to-cloudflare-pages
|
uses: ./.github/actions/deploy-to-cloudflare-pages
|
||||||
|
if: github.repository == ${{ vars.REPO_MAIN }} && ${{secrets.CLOUDFLARE_API_TOKEN}}
|
||||||
with:
|
with:
|
||||||
project_name: "trilium-docs"
|
project_name: "trilium-docs"
|
||||||
comment_body: "📚 Documentation preview is ready"
|
comment_body: "📚 Documentation preview is ready"
|
||||||
|
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@ -77,7 +77,7 @@ jobs:
|
|||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.3.4
|
uses: softprops/action-gh-release@v2.4.0
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
@ -118,7 +118,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.3.4
|
uses: softprops/action-gh-release@v2.4.0
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -127,7 +127,7 @@ jobs:
|
|||||||
path: upload
|
path: upload
|
||||||
|
|
||||||
- name: Publish stable release
|
- name: Publish stable release
|
||||||
uses: softprops/action-gh-release@v2.3.4
|
uses: softprops/action-gh-release@v2.4.0
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
"eslint": "9.37.0",
|
"eslint": "9.37.0",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jsdoc": "4.0.4",
|
"jsdoc": "4.0.5",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
"rcedit": "4.0.1",
|
"rcedit": "4.0.1",
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
|
@ -51,9 +51,9 @@
|
|||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-gpx": "2.2.0",
|
"leaflet-gpx": "2.2.0",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "16.3.0",
|
"marked": "16.4.0",
|
||||||
"mermaid": "11.12.0",
|
"mermaid": "11.12.0",
|
||||||
"mind-elixir": "5.1.1",
|
"mind-elixir": "5.3.2",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"sync_version": "اصدار المزامنه:",
|
"sync_version": "اصدار المزامنه:",
|
||||||
"build_date": "تاريخ الانشاء:",
|
"build_date": "تاريخ الانشاء:",
|
||||||
"build_revision": "مراجعة الاصدار:",
|
"build_revision": "مراجعة الاصدار:",
|
||||||
"data_directory": "مجلد البيانات:"
|
"data_directory": "مجلد البيانات:",
|
||||||
|
"db_version": "اصدار قاعدة البيانات:"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"critical-error": {
|
"critical-error": {
|
||||||
@ -26,6 +27,369 @@
|
|||||||
"save": "حفظ"
|
"save": "حفظ"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"bulk_actions": "اجراءات جماعية"
|
"bulk_actions": "اجراءات جماعية",
|
||||||
|
"available_actions": "الاجراءات المتاحة",
|
||||||
|
"chosen_actions": "الأجراءات المختارة",
|
||||||
|
"execute_bulk_actions": "تنفيذ الأجراءات الجماعية",
|
||||||
|
"bulk_actions_executed": "تم تنفيذ الاجراءات الجماعية بنجاح،",
|
||||||
|
"none_yet": "لايوجد أجراء بعد... اضف اجراء بالنقر على احد الأجراءات المتاحة اعلاه.",
|
||||||
|
"relations": "العلاقات",
|
||||||
|
"notes": "الملاحظات",
|
||||||
|
"other": "أخرى",
|
||||||
|
"affected_notes": "الملاحظات المتأثرة"
|
||||||
|
},
|
||||||
|
"upload_attachments": {
|
||||||
|
"options": "خيارات",
|
||||||
|
"upload": "تحميل"
|
||||||
|
},
|
||||||
|
"attribute_detail": {
|
||||||
|
"name": "الاسم",
|
||||||
|
"value": "قيمة",
|
||||||
|
"promoted": "تمت ترقيته",
|
||||||
|
"promoted_alias": "اسم مستعار",
|
||||||
|
"label_type": "نوع",
|
||||||
|
"text": "نص",
|
||||||
|
"date": "تاريخ",
|
||||||
|
"time": "وقت",
|
||||||
|
"precision": "دفة",
|
||||||
|
"digits": "رقم",
|
||||||
|
"delete": "حذف",
|
||||||
|
"color_type": "لون",
|
||||||
|
"multiplicity": "تعددية",
|
||||||
|
"number": "عدد",
|
||||||
|
"boolean": "منطقي",
|
||||||
|
"url": "عنوان الويب",
|
||||||
|
"inheritable": "قابل للوراثة"
|
||||||
|
},
|
||||||
|
"rename_label": {
|
||||||
|
"to": "الى"
|
||||||
|
},
|
||||||
|
"move_note": {
|
||||||
|
"to": "الى"
|
||||||
|
},
|
||||||
|
"add_relation": {
|
||||||
|
"to": "الى"
|
||||||
|
},
|
||||||
|
"rename_relation": {
|
||||||
|
"to": "الى"
|
||||||
|
},
|
||||||
|
"update_relation_target": {
|
||||||
|
"to": "الى"
|
||||||
|
},
|
||||||
|
"attachments_actions": {
|
||||||
|
"download": "تنزيل"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"week": "أسبوع",
|
||||||
|
"month": "شهر",
|
||||||
|
"year": "سنة",
|
||||||
|
"list": "قائمة",
|
||||||
|
"today": "اليوم",
|
||||||
|
"mon": "الأثنين",
|
||||||
|
"tue": "الثلاثاء",
|
||||||
|
"wed": "الأربعاء",
|
||||||
|
"thu": "الخميس",
|
||||||
|
"fri": "الجمعة",
|
||||||
|
"sat": "السبت",
|
||||||
|
"sun": "الأحد",
|
||||||
|
"january": "يناير",
|
||||||
|
"march": "مارس",
|
||||||
|
"april": "ابريل",
|
||||||
|
"may": "مايو",
|
||||||
|
"june": "يونيو",
|
||||||
|
"july": "يوليو",
|
||||||
|
"august": "أغسطس",
|
||||||
|
"september": "سبتمبر",
|
||||||
|
"october": "اكتوبر",
|
||||||
|
"november": "نوفمبر",
|
||||||
|
"december": "ديسمبر"
|
||||||
|
},
|
||||||
|
"global_menu": {
|
||||||
|
"menu": "القائمة",
|
||||||
|
"options": "خيارات",
|
||||||
|
"advanced": "متقدمة",
|
||||||
|
"logout": "تسجيل خروج",
|
||||||
|
"zoom": "تكبير/تصغير"
|
||||||
|
},
|
||||||
|
"zpetne_odkazy": {
|
||||||
|
"relation": "العلاقة"
|
||||||
|
},
|
||||||
|
"note_icon": {
|
||||||
|
"category": "الفئة:",
|
||||||
|
"search": "بحث:"
|
||||||
|
},
|
||||||
|
"basic_properties": {
|
||||||
|
"language": "اللغة",
|
||||||
|
"editable": "قابل للتعديل"
|
||||||
|
},
|
||||||
|
"book_properties": {
|
||||||
|
"list": "قائمة",
|
||||||
|
"expand": "توسيع",
|
||||||
|
"calendar": "التقويم",
|
||||||
|
"table": "جدول",
|
||||||
|
"board": "لوحة",
|
||||||
|
"grid": "خطوط شبكة",
|
||||||
|
"collapse": "طي"
|
||||||
|
},
|
||||||
|
"file_properties": {
|
||||||
|
"download": "تنزيل",
|
||||||
|
"open": "فتح",
|
||||||
|
"title": "ملف"
|
||||||
|
},
|
||||||
|
"image_properties": {
|
||||||
|
"download": "تنزيل",
|
||||||
|
"open": "فتح",
|
||||||
|
"title": "صورة"
|
||||||
|
},
|
||||||
|
"note_info_widget": {
|
||||||
|
"created": "انشاء",
|
||||||
|
"type": "نوع",
|
||||||
|
"modified": "معدل",
|
||||||
|
"calculate": "حساب"
|
||||||
|
},
|
||||||
|
"note_paths": {
|
||||||
|
"search": "بحث",
|
||||||
|
"archived": "مؤرشف"
|
||||||
|
},
|
||||||
|
"script_executor": {
|
||||||
|
"query": "استعلام",
|
||||||
|
"script": "برنامج نصي"
|
||||||
|
},
|
||||||
|
"search_definition": {
|
||||||
|
"ancestor": "السلف",
|
||||||
|
"limit": "الحد الاقصى",
|
||||||
|
"action": "أجراء",
|
||||||
|
"search_button": "بحث",
|
||||||
|
"debug": "تصحيح الاخطاء"
|
||||||
|
},
|
||||||
|
"ancestor": {
|
||||||
|
"label": "السلف",
|
||||||
|
"depth_label": "العمق"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"limit": "الحد الاقصى"
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"debug": "تصحيح الاخطاء"
|
||||||
|
},
|
||||||
|
"order_by": {
|
||||||
|
"title": "عنوان",
|
||||||
|
"desc": "تنازلي"
|
||||||
|
},
|
||||||
|
"search_string": {
|
||||||
|
"search_prefix": "بحث:"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"title": "مزامنة"
|
||||||
|
},
|
||||||
|
"fonts": {
|
||||||
|
"fonts": "خطوط",
|
||||||
|
"size": "حجم",
|
||||||
|
"serif": "خط ومزخرف",
|
||||||
|
"monospace": "خط بعرض ثابت"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"confirmation": "تأكيد",
|
||||||
|
"cancel": "الغاء",
|
||||||
|
"ok": "نعم"
|
||||||
|
},
|
||||||
|
"delete_notes": {
|
||||||
|
"close": "غلق",
|
||||||
|
"cancel": "الغاء",
|
||||||
|
"ok": "نعم"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"close": "غلق",
|
||||||
|
"export": "تصدير",
|
||||||
|
"export_note_title": "تصدير الملاحظة",
|
||||||
|
"export_status": "حالة التصدير"
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"troubleshooting": "أستكشاف الاخطاء واصلاحها",
|
||||||
|
"other": "أخرى",
|
||||||
|
"title": "ورقة المراجعة السريعة",
|
||||||
|
"noteNavigation": "التنقل بين الملاحظات",
|
||||||
|
"collapseExpand": "طي/توسيع العقدة",
|
||||||
|
"notSet": "غير محدد",
|
||||||
|
"collapseSubTree": "طي الشجرة الفرعية",
|
||||||
|
"tabShortcuts": "أختصارات التبويب",
|
||||||
|
"creatingNotes": "انشاء الملاحظات",
|
||||||
|
"selectNote": "تحديد الملاحظة"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"options": "خيارات",
|
||||||
|
"import": "استيراد"
|
||||||
|
},
|
||||||
|
"include_note": {
|
||||||
|
"label_note": "ملاحظة"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"closeButton": "أغلاق",
|
||||||
|
"okButton": "نعم"
|
||||||
|
},
|
||||||
|
"markdown_import": {
|
||||||
|
"import_button": "أستيراد"
|
||||||
|
},
|
||||||
|
"note_type_chooser": {
|
||||||
|
"templates": "قوالب"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"title": "ترقية",
|
||||||
|
"ok": "نعم",
|
||||||
|
"defaultTitle": "ترقية"
|
||||||
|
},
|
||||||
|
"protected_session_password": {
|
||||||
|
"close_label": "أغلاق"
|
||||||
|
},
|
||||||
|
"revisions": {
|
||||||
|
"delete_button": "حذف",
|
||||||
|
"download_button": "تنزيل",
|
||||||
|
"restore_button": "أستعادة",
|
||||||
|
"preview": "معاينة:"
|
||||||
|
},
|
||||||
|
"sort_child_notes": {
|
||||||
|
"title": "عنوان",
|
||||||
|
"ascending": "تصاعدي",
|
||||||
|
"descending": "تنازلي",
|
||||||
|
"folders": "مجلدات",
|
||||||
|
"sort": "فرز"
|
||||||
|
},
|
||||||
|
"recent_changes": {
|
||||||
|
"undelete_link": "الغاء الحذف"
|
||||||
|
},
|
||||||
|
"edited_notes": {
|
||||||
|
"deleted": "(حذف)"
|
||||||
|
},
|
||||||
|
"note_properties": {
|
||||||
|
"info": "معلومات"
|
||||||
|
},
|
||||||
|
"backend_log": {
|
||||||
|
"refresh": "تحديث"
|
||||||
|
},
|
||||||
|
"max_content_width": {
|
||||||
|
"max_width_unit": "بكسل"
|
||||||
|
},
|
||||||
|
"native_title_bar": {
|
||||||
|
"enabled": "مفعل",
|
||||||
|
"disabled": "معطل"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"theme_label": "السمة",
|
||||||
|
"layout": "تخطيط",
|
||||||
|
"layout-vertical-title": "عمودي",
|
||||||
|
"layout-horizontal-title": "أفقي"
|
||||||
|
},
|
||||||
|
"ui-performance": {
|
||||||
|
"title": "أداء"
|
||||||
|
},
|
||||||
|
"ai_llm": {
|
||||||
|
"progress": "تقدم",
|
||||||
|
"openai_tab": "OpenAI",
|
||||||
|
"actions": "أجراءات",
|
||||||
|
"retry": "أعد المحاولة",
|
||||||
|
"reprocessing_index": "جار اعادة البناء...",
|
||||||
|
"never": "ابدٱ",
|
||||||
|
"agent": {
|
||||||
|
"processing": "جار المعالجة...",
|
||||||
|
"thinking": "جار التفكير...",
|
||||||
|
"loading": "جار التحميل...",
|
||||||
|
"generating": "جار الانشاء..."
|
||||||
|
},
|
||||||
|
"name": "الذكاء الأصطناعي",
|
||||||
|
"openai": "OpenAI",
|
||||||
|
"sources": "مصادر"
|
||||||
|
},
|
||||||
|
"code_auto_read_only_size": {
|
||||||
|
"unit": "حروف"
|
||||||
|
},
|
||||||
|
"code-editor-options": {
|
||||||
|
"title": "محرر"
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"images_section_title": "صور",
|
||||||
|
"max_image_dimensions_unit": "بكسل"
|
||||||
|
},
|
||||||
|
"revisions_snapshot_limit": {
|
||||||
|
"snapshot_number_limit_unit": "لقطات"
|
||||||
|
},
|
||||||
|
"search_engine": {
|
||||||
|
"bing": "Bing",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"google": "جوجل",
|
||||||
|
"save_button": "حفظ"
|
||||||
|
},
|
||||||
|
"heading_style": {
|
||||||
|
"plain": "بسيط"
|
||||||
|
},
|
||||||
|
"text_auto_read_only_size": {
|
||||||
|
"unit": "حروف"
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"language": "لغة",
|
||||||
|
"sunday": "الأحد",
|
||||||
|
"monday": "الأثنين"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"path": "مسار"
|
||||||
|
},
|
||||||
|
"etapi": {
|
||||||
|
"wiki": "ويكي",
|
||||||
|
"created": "تم الأنشاء",
|
||||||
|
"actions": "أجراءات"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"heading": "كلمة السر",
|
||||||
|
"wiki": "ويكي"
|
||||||
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"shortcuts": "أختصارات",
|
||||||
|
"description": "الوصف"
|
||||||
|
},
|
||||||
|
"sync_2": {
|
||||||
|
"timeout_unit": "ميلي ثانية",
|
||||||
|
"note": "ملاحظة",
|
||||||
|
"save": "حفظ",
|
||||||
|
"help": "المساعدة"
|
||||||
|
},
|
||||||
|
"api_log": {
|
||||||
|
"close": "أغلاق"
|
||||||
|
},
|
||||||
|
"bookmark_switch": {
|
||||||
|
"bookmark": "علامة مرجعية"
|
||||||
|
},
|
||||||
|
"editability_select": {
|
||||||
|
"auto": "تلقائي",
|
||||||
|
"read_only": "قراءة-فقط"
|
||||||
|
},
|
||||||
|
"tab_row": {
|
||||||
|
"close": "اغلاق"
|
||||||
|
},
|
||||||
|
"toc": {
|
||||||
|
"options": "خيارات"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"due": {
|
||||||
|
"yesterday": "أمس"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"code_theme": {
|
||||||
|
"title": "المظهر"
|
||||||
|
},
|
||||||
|
"table_view": {
|
||||||
|
"sort-column-ascending": "تصاعدي",
|
||||||
|
"sort-column-descending": "تنازلي",
|
||||||
|
"new-column-relation": "العلاقة"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"close": "اغلاق"
|
||||||
|
},
|
||||||
|
"call_to_action": {
|
||||||
|
"dismiss": "تجاهل"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"percentage": "%"
|
||||||
|
},
|
||||||
|
"clone_to": {
|
||||||
|
"prefix_optional": "بادئة (اختياري)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
apps/client/src/translations/id/translation.json
Normal file
8
apps/client/src/translations/id/translation.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"about": {
|
||||||
|
"title": "Tentang Trilium Notes",
|
||||||
|
"homepage": "Halaman utama:",
|
||||||
|
"app_version": "Versi Aplikasi:",
|
||||||
|
"db_version": "Versi DB:"
|
||||||
|
}
|
||||||
|
}
|
@ -67,7 +67,8 @@
|
|||||||
"switch_to_mobile_version": "モバイル版に切り替え",
|
"switch_to_mobile_version": "モバイル版に切り替え",
|
||||||
"switch_to_desktop_version": "デスクトップ版に切り替え",
|
"switch_to_desktop_version": "デスクトップ版に切り替え",
|
||||||
"configure_launchbar": "ランチャーバーの設定",
|
"configure_launchbar": "ランチャーバーの設定",
|
||||||
"show_shared_notes_subtree": "共有ノートのサブツリーを表示"
|
"show_shared_notes_subtree": "共有ノートのサブツリーを表示",
|
||||||
|
"update_available": "バージョン {{latestVersion}} が利用可能です。クリックしてダウンロードしてください。"
|
||||||
},
|
},
|
||||||
"left_pane_toggle": {
|
"left_pane_toggle": {
|
||||||
"show_panel": "パネルを表示",
|
"show_panel": "パネルを表示",
|
||||||
@ -1290,7 +1291,7 @@
|
|||||||
"cut": "カット",
|
"cut": "カット",
|
||||||
"copy": "コピー",
|
"copy": "コピー",
|
||||||
"copy-link": "リンクをコピー",
|
"copy-link": "リンクをコピー",
|
||||||
"paste": "ペースト",
|
"paste": "貼り付け",
|
||||||
"paste-as-plain-text": "プレーンテキストで貼り付け",
|
"paste-as-plain-text": "プレーンテキストで貼り付け",
|
||||||
"search_online": "{{searchEngine}} で \"{{term}}\" を検索"
|
"search_online": "{{searchEngine}} で \"{{term}}\" を検索"
|
||||||
},
|
},
|
||||||
|
@ -34,15 +34,15 @@
|
|||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.2.1",
|
"electron": "38.2.2",
|
||||||
"@electron-forge/cli": "7.9.0",
|
"@electron-forge/cli": "7.10.0",
|
||||||
"@electron-forge/maker-deb": "7.9.0",
|
"@electron-forge/maker-deb": "7.10.0",
|
||||||
"@electron-forge/maker-dmg": "7.9.0",
|
"@electron-forge/maker-dmg": "7.10.0",
|
||||||
"@electron-forge/maker-flatpak": "7.9.0",
|
"@electron-forge/maker-flatpak": "7.10.0",
|
||||||
"@electron-forge/maker-rpm": "7.9.0",
|
"@electron-forge/maker-rpm": "7.10.0",
|
||||||
"@electron-forge/maker-squirrel": "7.9.0",
|
"@electron-forge/maker-squirrel": "7.10.0",
|
||||||
"@electron-forge/maker-zip": "7.9.0",
|
"@electron-forge/maker-zip": "7.10.0",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.9.0",
|
"@electron-forge/plugin-auto-unpack-natives": "7.10.0",
|
||||||
"prebuild-install": "7.1.3"
|
"prebuild-install": "7.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
"@triliumnext/desktop": "workspace:*",
|
"@triliumnext/desktop": "workspace:*",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.2.1",
|
"electron": "38.2.2",
|
||||||
"fs-extra": "11.3.2"
|
"fs-extra": "11.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
"debounce": "2.2.0",
|
"debounce": "2.2.0",
|
||||||
"debug": "4.4.3",
|
"debug": "4.4.3",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"electron": "38.2.1",
|
"electron": "38.2.2",
|
||||||
"electron-debug": "4.1.0",
|
"electron-debug": "4.1.0",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
@ -105,7 +105,7 @@
|
|||||||
"is-svg": "6.1.0",
|
"is-svg": "6.1.0",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"marked": "16.3.0",
|
"marked": "16.4.0",
|
||||||
"mime-types": "3.0.1",
|
"mime-types": "3.0.1",
|
||||||
"multer": "2.0.2",
|
"multer": "2.0.2",
|
||||||
"normalize-strings": "1.1.1",
|
"normalize-strings": "1.1.1",
|
||||||
|
@ -18,6 +18,134 @@
|
|||||||
"copy-notes-to-clipboard": "نسخ الملاحظات المحددة الى الحافظة",
|
"copy-notes-to-clipboard": "نسخ الملاحظات المحددة الى الحافظة",
|
||||||
"paste-notes-from-clipboard": "لصق الملاحظا من الحافظة الى الملاحظة الحالية",
|
"paste-notes-from-clipboard": "لصق الملاحظا من الحافظة الى الملاحظة الحالية",
|
||||||
"cut-notes-to-clipboard": "قص الملاحظات المحددة الى الحافظة",
|
"cut-notes-to-clipboard": "قص الملاحظات المحددة الى الحافظة",
|
||||||
"select-all-notes-in-parent": "تحديد جميع الملاحظات من مستوى الملاحظة الحالي"
|
"select-all-notes-in-parent": "تحديد جميع الملاحظات من مستوى الملاحظة الحالي",
|
||||||
|
"back-in-note-history": "الانتقال الى الملاحظة السابقة في السجل",
|
||||||
|
"forward-in-note-history": "الانتقال الى الملاحظة التالية في السجل",
|
||||||
|
"scroll-to-active-note": "تمرير شجرة الملاحظات الى الملاحظة النشطة",
|
||||||
|
"search-in-subtree": "البحث عن الملاحظات في الشجرة الفرعية للملاحظة النشطة",
|
||||||
|
"expand-subtree": "توسيع الشجرة الفرعية للملاحظة الحالية",
|
||||||
|
"create-note-into-inbox": "انشاء ملاحظة في صندوق الوارد (اذا كان معرفا) او في ملاحظة اليوم",
|
||||||
|
"move-note-up-in-hierarchy": "نقل الملاحظة للاعلى في التسلسل الهرمي",
|
||||||
|
"move-note-down-in-hierarchy": "نقل الملاحظة للاسفل في التسلسل الهرمي",
|
||||||
|
"edit-note-title": "الانتقال من شجرة الملاحظات إلى تفاصيل الملاحظة وتحرير العنوان",
|
||||||
|
"edit-branch-prefix": "عرض مربع حوار \"تعديل بادئة الفرع\"",
|
||||||
|
"add-note-above-to-the-selection": "اضافة ملاحظة فوق الملاحظة المحددة",
|
||||||
|
"add-note-below-to-selection": "اضافة ملاحظة اسفل الملاحظة المحددة",
|
||||||
|
"duplicate-subtree": "استنساخ الشجرة الفرعية",
|
||||||
|
"tabs-and-windows": "التبويبات والنوافذ",
|
||||||
|
"open-new-tab": "فتح تبويب جديد",
|
||||||
|
"close-active-tab": "غلق التبويب النشط",
|
||||||
|
"reopen-last-tab": "اعادة فتح اخر تبويب مغلق",
|
||||||
|
"activate-next-tab": "تنشيط التبويب الموجود على اليمين",
|
||||||
|
"activate-previous-tab": "تنشيط التبويب الموجود على اليسار",
|
||||||
|
"open-new-window": "فتح نافذة جديدة فارغة",
|
||||||
|
"first-tab": "تنشيط التبويب الاول في القائمة",
|
||||||
|
"second-tab": "تنشيط التبويب الثاني في القائمة",
|
||||||
|
"third-tab": "تنشيط التبويب الثالث في الثائمة",
|
||||||
|
"fourth-tab": "تنشيط التبويب الرابع في القائمة",
|
||||||
|
"fifth-tab": "تنشيط التبويب الخامس في القائمة",
|
||||||
|
"sixth-tab": "تنشيط التبويب السادس في القائمة",
|
||||||
|
"seventh-tab": "تنشيط التبويب السابع في القائمة",
|
||||||
|
"eight-tab": "تنشيط التبويب الثامن في القائمة",
|
||||||
|
"ninth-tab": "تنشيط التبويب التاسع في القائمة",
|
||||||
|
"last-tab": "تنشيط التبويب الاخير في القائمة",
|
||||||
|
"other": "أخرى",
|
||||||
|
"dialogs": "مربعات الحوار"
|
||||||
|
},
|
||||||
|
"setup_sync-from-server": {
|
||||||
|
"note": "ملاحظة:",
|
||||||
|
"password": "كلمة السر",
|
||||||
|
"password-placeholder": "كلمة السر",
|
||||||
|
"back": "رجوع",
|
||||||
|
"server-host-placeholder": "https://<hostname>:<port>",
|
||||||
|
"proxy-server-placeholder": "https://<hostname>:<port>"
|
||||||
|
},
|
||||||
|
"weekdays": {
|
||||||
|
"monday": "الأثنين",
|
||||||
|
"tuesday": "الثلاثاء",
|
||||||
|
"wednesday": "الاربعاء",
|
||||||
|
"thursday": "الخميس",
|
||||||
|
"friday": "الجمعة",
|
||||||
|
"saturday": "السبت",
|
||||||
|
"sunday": "الأحد"
|
||||||
|
},
|
||||||
|
"months": {
|
||||||
|
"january": "يناير",
|
||||||
|
"february": "فبراير",
|
||||||
|
"march": "مارس",
|
||||||
|
"april": "ابريل",
|
||||||
|
"may": "مايو",
|
||||||
|
"june": "يونيو",
|
||||||
|
"july": "يوليو",
|
||||||
|
"august": "أغسطس",
|
||||||
|
"september": "سبتمبر",
|
||||||
|
"october": "أكتوبر",
|
||||||
|
"november": "نوفمبر",
|
||||||
|
"december": "ديسمبر"
|
||||||
|
},
|
||||||
|
"special_notes": {
|
||||||
|
"search_prefix": "بحث:"
|
||||||
|
},
|
||||||
|
"hidden-subtree": {
|
||||||
|
"calendar-title": "تقويم",
|
||||||
|
"bookmarks-title": "العلامات المرجعية",
|
||||||
|
"settings-title": "أعدادات",
|
||||||
|
"options-title": "خيارات",
|
||||||
|
"appearance-title": "المظهر",
|
||||||
|
"shortcuts-title": "أختصارات",
|
||||||
|
"images-title": "صور",
|
||||||
|
"password-title": "كلمة السر",
|
||||||
|
"backup-title": "نسخة أحتياطية",
|
||||||
|
"sync-title": "مزامنة",
|
||||||
|
"other": "أخرى",
|
||||||
|
"advanced-title": "متقدم",
|
||||||
|
"inbox-title": "صندوق الوارد",
|
||||||
|
"spacer-title": "فاصل",
|
||||||
|
"spellcheck-title": "التدقيق الاملائي",
|
||||||
|
"multi-factor-authentication-title": "المصادقة متعددة العوامل"
|
||||||
|
},
|
||||||
|
"tray": {
|
||||||
|
"bookmarks": "العلامات المرجعية"
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"error_title": "خطأ"
|
||||||
|
},
|
||||||
|
"share_theme": {
|
||||||
|
"search_placeholder": "بحث...",
|
||||||
|
"subpages": "الصفحات الفرعية:",
|
||||||
|
"expand": "توسيع"
|
||||||
|
},
|
||||||
|
"hidden_subtree_templates": {
|
||||||
|
"description": "الوصف",
|
||||||
|
"calendar": "التقويم",
|
||||||
|
"table": "جدول",
|
||||||
|
"geolocation": "الموقع الجغرافي",
|
||||||
|
"board": "لوحة",
|
||||||
|
"status": "الحالة",
|
||||||
|
"board_status_done": "تمت"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "تسجيل الدخول",
|
||||||
|
"password": "كلمة السر",
|
||||||
|
"button": "تسجيل الدخول"
|
||||||
|
},
|
||||||
|
"set_password": {
|
||||||
|
"password": "كلمة السر"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"next": "التالي",
|
||||||
|
"title": "تثبيت"
|
||||||
|
},
|
||||||
|
"setup_sync-from-desktop": {
|
||||||
|
"step6-here": "هنا"
|
||||||
|
},
|
||||||
|
"setup_sync-in-progress": {
|
||||||
|
"outstanding-items-default": "غير متوفر"
|
||||||
|
},
|
||||||
|
"share_page": {
|
||||||
|
"parent": "الأصل:"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"duplicate-note-suffix": "(مكرر)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
apps/server/src/assets/translations/id/server.json
Normal file
8
apps/server/src/assets/translations/id/server.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"keyboard_actions": {
|
||||||
|
"back-in-note-history": "Navigasi ke catatan sebelumnya di history",
|
||||||
|
"forward-in-note-history": "Navigasi ke catatan selanjutnya di history",
|
||||||
|
"open-jump-to-note-dialog": "Buka dialog \"Menuju ke catatan\"",
|
||||||
|
"open-command-palette": "Buka palet perintah"
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { BNote } from "../../services/backend_script_entrypoint";
|
import { BNote } from "../../services/backend_script_entrypoint";
|
||||||
|
import cls from "../../services/cls";
|
||||||
import { buildNote } from "../../test/becca_easy_mocking";
|
import { buildNote } from "../../test/becca_easy_mocking";
|
||||||
import { processContent } from "./clipper";
|
import { processContent } from "./clipper";
|
||||||
|
|
||||||
@ -6,7 +7,9 @@ let note!: BNote;
|
|||||||
|
|
||||||
describe("processContent", () => {
|
describe("processContent", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
note = buildNote({});
|
note = buildNote({
|
||||||
|
content: "Hi there"
|
||||||
|
});
|
||||||
note.saveAttachment = () => {};
|
note.saveAttachment = () => {};
|
||||||
vi.mock("../../services/image.js", () => ({
|
vi.mock("../../services/image.js", () => ({
|
||||||
default: {
|
default: {
|
||||||
@ -21,29 +24,29 @@ describe("processContent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("processes basic note", () => {
|
it("processes basic note", () => {
|
||||||
const processed = processContent([], note, "<p>Hello world.</p>");
|
const processed = cls.init(() => processContent([], note, "<p>Hello world.</p>"));
|
||||||
expect(processed).toStrictEqual("<p>Hello world.</p>")
|
expect(processed).toStrictEqual("<p>Hello world.</p>")
|
||||||
});
|
});
|
||||||
|
|
||||||
it("processes plain text", () => {
|
it("processes plain text", () => {
|
||||||
const processed = processContent([], note, "Hello world.");
|
const processed = cls.init(() => processContent([], note, "Hello world."));
|
||||||
expect(processed).toStrictEqual("<p>Hello world.</p>")
|
expect(processed).toStrictEqual("<p>Hello world.</p>")
|
||||||
});
|
});
|
||||||
|
|
||||||
it("replaces images", () => {
|
it("replaces images", () => {
|
||||||
const processed = processContent(
|
const processed = cls.init(() => processContent(
|
||||||
[{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAF0lEQVQoU2P8DwQMBADjqKLRIGAgKggAzHs/0SoYCGwAAAAASUVORK5CYII="}],
|
[{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAF0lEQVQoU2P8DwQMBADjqKLRIGAgKggAzHs/0SoYCGwAAAAASUVORK5CYII="}],
|
||||||
note, `<img src="OKZxZA3MonZJkwFcEhId">`
|
note, `<img src="OKZxZA3MonZJkwFcEhId">`
|
||||||
);
|
));
|
||||||
expect(processed).toStrictEqual(`<img src="api/attachments/foo/image/encodedTitle" >`);
|
expect(processed).toStrictEqual(`<img src="api/attachments/foo/image/encodedTitle" >`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips over non-data images", () => {
|
it("skips over non-data images", () => {
|
||||||
for (const url of [ "foo", "" ]) {
|
for (const url of [ "foo", "" ]) {
|
||||||
const processed = processContent(
|
const processed = cls.init(() => processContent(
|
||||||
[{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl": url}],
|
[{"imageId":"OKZxZA3MonZJkwFcEhId","src":"inline.png","dataUrl": url}],
|
||||||
note, `<img src="OKZxZA3MonZJkwFcEhId">`
|
note, `<img src="OKZxZA3MonZJkwFcEhId">`
|
||||||
);
|
));
|
||||||
expect(processed).toStrictEqual(`<img src="OKZxZA3MonZJkwFcEhId" >`);
|
expect(processed).toStrictEqual(`<img src="OKZxZA3MonZJkwFcEhId" >`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,7 @@ import { beforeAll, describe, expect, it, vi, beforeEach, afterEach } from "vite
|
|||||||
import supertest from "supertest";
|
import supertest from "supertest";
|
||||||
import config from "../../services/config.js";
|
import config from "../../services/config.js";
|
||||||
import { refreshAuth } from "../../services/auth.js";
|
import { refreshAuth } from "../../services/auth.js";
|
||||||
import type { WebSocket } from 'ws';
|
import { sleepFor } from "@triliumnext/commons";
|
||||||
|
|
||||||
// Mock the CSRF protection middleware to allow tests to pass
|
// Mock the CSRF protection middleware to allow tests to pass
|
||||||
vi.mock("../csrf_protection.js", () => ({
|
vi.mock("../csrf_protection.js", () => ({
|
||||||
@ -72,7 +72,11 @@ vi.mock("../../services/options.js", () => ({
|
|||||||
getOptionMap: vi.fn(() => new Map()),
|
getOptionMap: vi.fn(() => new Map()),
|
||||||
createOption: vi.fn(),
|
createOption: vi.fn(),
|
||||||
getOption: vi.fn(() => '0'),
|
getOption: vi.fn(() => '0'),
|
||||||
getOptionOrNull: vi.fn(() => null)
|
getOptionOrNull: vi.fn(() => null),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -499,6 +503,7 @@ describe("LLM API Tests", () => {
|
|||||||
const ws = (await import("../../services/ws.js")).default;
|
const ws = (await import("../../services/ws.js")).default;
|
||||||
|
|
||||||
// Verify thinking message was sent
|
// Verify thinking message was sent
|
||||||
|
await sleepFor(1_000);
|
||||||
expect(ws.sendMessageToAllClients).toHaveBeenCalledWith({
|
expect(ws.sendMessageToAllClients).toHaveBeenCalledWith({
|
||||||
type: 'llm-stream',
|
type: 'llm-stream',
|
||||||
chatNoteId: testChatId,
|
chatNoteId: testChatId,
|
||||||
|
@ -12,7 +12,11 @@ import type { AIService, ChatCompletionOptions, Message } from './ai_interface.j
|
|||||||
vi.mock('../options.js', () => ({
|
vi.mock('../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -110,26 +114,26 @@ describe('AIServiceManager', () => {
|
|||||||
describe('getSelectedProviderAsync', () => {
|
describe('getSelectedProviderAsync', () => {
|
||||||
it('should return the selected provider', async () => {
|
it('should return the selected provider', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
||||||
|
|
||||||
const result = await manager.getSelectedProviderAsync();
|
const result = await manager.getSelectedProviderAsync();
|
||||||
|
|
||||||
expect(result).toBe('openai');
|
expect(result).toBe('openai');
|
||||||
expect(configHelpers.getSelectedProvider).toHaveBeenCalled();
|
expect(configHelpers.getSelectedProvider).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null if no provider is selected', async () => {
|
it('should return null if no provider is selected', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
const result = await manager.getSelectedProviderAsync();
|
const result = await manager.getSelectedProviderAsync();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors and return null', async () => {
|
it('should handle errors and return null', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockRejectedValueOnce(new Error('Config error'));
|
vi.mocked(configHelpers.getSelectedProvider).mockRejectedValueOnce(new Error('Config error'));
|
||||||
|
|
||||||
const result = await manager.getSelectedProviderAsync();
|
const result = await manager.getSelectedProviderAsync();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -141,9 +145,9 @@ describe('AIServiceManager', () => {
|
|||||||
errors: [],
|
errors: [],
|
||||||
warnings: []
|
warnings: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await manager.validateConfiguration();
|
const result = await manager.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,9 +157,9 @@ describe('AIServiceManager', () => {
|
|||||||
errors: ['Missing API key', 'Invalid model'],
|
errors: ['Missing API key', 'Invalid model'],
|
||||||
warnings: []
|
warnings: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await manager.validateConfiguration();
|
const result = await manager.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toContain('There are issues with your AI configuration');
|
expect(result).toContain('There are issues with your AI configuration');
|
||||||
expect(result).toContain('Missing API key');
|
expect(result).toContain('Missing API key');
|
||||||
expect(result).toContain('Invalid model');
|
expect(result).toContain('Invalid model');
|
||||||
@ -167,9 +171,9 @@ describe('AIServiceManager', () => {
|
|||||||
errors: [],
|
errors: [],
|
||||||
warnings: ['Model not optimal']
|
warnings: ['Model not optimal']
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await manager.validateConfiguration();
|
const result = await manager.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -178,21 +182,21 @@ describe('AIServiceManager', () => {
|
|||||||
it('should create and return the selected provider service', async () => {
|
it('should create and return the selected provider service', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
||||||
|
|
||||||
const mockService = {
|
const mockService = {
|
||||||
isAvailable: vi.fn().mockReturnValue(true),
|
isAvailable: vi.fn().mockReturnValue(true),
|
||||||
generateChatCompletion: vi.fn()
|
generateChatCompletion: vi.fn()
|
||||||
};
|
};
|
||||||
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
||||||
|
|
||||||
const result = await manager.getOrCreateAnyService();
|
const result = await manager.getOrCreateAnyService();
|
||||||
|
|
||||||
expect(result).toBe(mockService);
|
expect(result).toBe(mockService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if no provider is selected', async () => {
|
it('should throw error if no provider is selected', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(manager.getOrCreateAnyService()).rejects.toThrow(
|
await expect(manager.getOrCreateAnyService()).rejects.toThrow(
|
||||||
'No AI provider is selected'
|
'No AI provider is selected'
|
||||||
);
|
);
|
||||||
@ -201,7 +205,7 @@ describe('AIServiceManager', () => {
|
|||||||
it('should throw error if selected provider is not available', async () => {
|
it('should throw error if selected provider is not available', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
|
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
|
||||||
|
|
||||||
await expect(manager.getOrCreateAnyService()).rejects.toThrow(
|
await expect(manager.getOrCreateAnyService()).rejects.toThrow(
|
||||||
'Selected AI provider (openai) is not available'
|
'Selected AI provider (openai) is not available'
|
||||||
);
|
);
|
||||||
@ -211,17 +215,17 @@ describe('AIServiceManager', () => {
|
|||||||
describe('isAnyServiceAvailable', () => {
|
describe('isAnyServiceAvailable', () => {
|
||||||
it('should return true if any provider is available', () => {
|
it('should return true if any provider is available', () => {
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
||||||
|
|
||||||
const result = manager.isAnyServiceAvailable();
|
const result = manager.isAnyServiceAvailable();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if no providers are available', () => {
|
it('should return false if no providers are available', () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('');
|
vi.mocked(options.getOption).mockReturnValue('');
|
||||||
|
|
||||||
const result = manager.isAnyServiceAvailable();
|
const result = manager.isAnyServiceAvailable();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -232,18 +236,18 @@ describe('AIServiceManager', () => {
|
|||||||
.mockReturnValueOnce('openai-key')
|
.mockReturnValueOnce('openai-key')
|
||||||
.mockReturnValueOnce('anthropic-key')
|
.mockReturnValueOnce('anthropic-key')
|
||||||
.mockReturnValueOnce(''); // No Ollama URL
|
.mockReturnValueOnce(''); // No Ollama URL
|
||||||
|
|
||||||
const result = manager.getAvailableProviders();
|
const result = manager.getAvailableProviders();
|
||||||
|
|
||||||
expect(result).toEqual(['openai', 'anthropic']);
|
expect(result).toEqual(['openai', 'anthropic']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include already created services', () => {
|
it('should include already created services', () => {
|
||||||
// Mock that OpenAI has API key configured
|
// Mock that OpenAI has API key configured
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
||||||
|
|
||||||
const result = manager.getAvailableProviders();
|
const result = manager.getAvailableProviders();
|
||||||
|
|
||||||
expect(result).toContain('openai');
|
expect(result).toContain('openai');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -255,23 +259,23 @@ describe('AIServiceManager', () => {
|
|||||||
|
|
||||||
it('should generate completion with selected provider', async () => {
|
it('should generate completion with selected provider', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('openai');
|
||||||
|
|
||||||
// Mock the getAvailableProviders to include openai
|
// Mock the getAvailableProviders to include openai
|
||||||
vi.mocked(options.getOption)
|
vi.mocked(options.getOption)
|
||||||
.mockReturnValueOnce('test-api-key') // for availability check
|
.mockReturnValueOnce('test-api-key') // for availability check
|
||||||
.mockReturnValueOnce('') // for anthropic
|
.mockReturnValueOnce('') // for anthropic
|
||||||
.mockReturnValueOnce('') // for ollama
|
.mockReturnValueOnce('') // for ollama
|
||||||
.mockReturnValueOnce('test-api-key'); // for service creation
|
.mockReturnValueOnce('test-api-key'); // for service creation
|
||||||
|
|
||||||
const mockResponse = { content: 'Hello response' };
|
const mockResponse = { content: 'Hello response' };
|
||||||
const mockService = {
|
const mockService = {
|
||||||
isAvailable: vi.fn().mockReturnValue(true),
|
isAvailable: vi.fn().mockReturnValue(true),
|
||||||
generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse)
|
generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse)
|
||||||
};
|
};
|
||||||
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
||||||
|
|
||||||
const result = await manager.generateChatCompletion(messages);
|
const result = await manager.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toBe(mockResponse);
|
expect(result).toBe(mockResponse);
|
||||||
expect(mockService.generateChatCompletion).toHaveBeenCalledWith(messages, {});
|
expect(mockService.generateChatCompletion).toHaveBeenCalledWith(messages, {});
|
||||||
});
|
});
|
||||||
@ -283,28 +287,28 @@ describe('AIServiceManager', () => {
|
|||||||
modelId: 'gpt-4',
|
modelId: 'gpt-4',
|
||||||
fullIdentifier: 'openai:gpt-4'
|
fullIdentifier: 'openai:gpt-4'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock the getAvailableProviders to include openai
|
// Mock the getAvailableProviders to include openai
|
||||||
vi.mocked(options.getOption)
|
vi.mocked(options.getOption)
|
||||||
.mockReturnValueOnce('test-api-key') // for availability check
|
.mockReturnValueOnce('test-api-key') // for availability check
|
||||||
.mockReturnValueOnce('') // for anthropic
|
.mockReturnValueOnce('') // for anthropic
|
||||||
.mockReturnValueOnce('') // for ollama
|
.mockReturnValueOnce('') // for ollama
|
||||||
.mockReturnValueOnce('test-api-key'); // for service creation
|
.mockReturnValueOnce('test-api-key'); // for service creation
|
||||||
|
|
||||||
const mockResponse = { content: 'Hello response' };
|
const mockResponse = { content: 'Hello response' };
|
||||||
const mockService = {
|
const mockService = {
|
||||||
isAvailable: vi.fn().mockReturnValue(true),
|
isAvailable: vi.fn().mockReturnValue(true),
|
||||||
generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse)
|
generateChatCompletion: vi.fn().mockResolvedValueOnce(mockResponse)
|
||||||
};
|
};
|
||||||
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
||||||
|
|
||||||
const result = await manager.generateChatCompletion(messages, {
|
const result = await manager.generateChatCompletion(messages, {
|
||||||
model: 'openai:gpt-4'
|
model: 'openai:gpt-4'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(mockResponse);
|
expect(result).toBe(mockResponse);
|
||||||
expect(mockService.generateChatCompletion).toHaveBeenCalledWith(
|
expect(mockService.generateChatCompletion).toHaveBeenCalledWith(
|
||||||
messages,
|
messages,
|
||||||
{ model: 'gpt-4' }
|
{ model: 'gpt-4' }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -317,7 +321,7 @@ describe('AIServiceManager', () => {
|
|||||||
|
|
||||||
it('should throw error if no provider selected', async () => {
|
it('should throw error if no provider selected', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(manager.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(manager.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'No AI provider is selected'
|
'No AI provider is selected'
|
||||||
);
|
);
|
||||||
@ -330,13 +334,13 @@ describe('AIServiceManager', () => {
|
|||||||
modelId: 'claude-3',
|
modelId: 'claude-3',
|
||||||
fullIdentifier: 'anthropic:claude-3'
|
fullIdentifier: 'anthropic:claude-3'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock that openai is available
|
// Mock that openai is available
|
||||||
vi.mocked(options.getOption)
|
vi.mocked(options.getOption)
|
||||||
.mockReturnValueOnce('test-api-key') // for availability check
|
.mockReturnValueOnce('test-api-key') // for availability check
|
||||||
.mockReturnValueOnce('') // for anthropic
|
.mockReturnValueOnce('') // for anthropic
|
||||||
.mockReturnValueOnce(''); // for ollama
|
.mockReturnValueOnce(''); // for ollama
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
manager.generateChatCompletion(messages, { model: 'anthropic:claude-3' })
|
manager.generateChatCompletion(messages, { model: 'anthropic:claude-3' })
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
@ -348,9 +352,9 @@ describe('AIServiceManager', () => {
|
|||||||
describe('getAIEnabledAsync', () => {
|
describe('getAIEnabledAsync', () => {
|
||||||
it('should return AI enabled status', async () => {
|
it('should return AI enabled status', async () => {
|
||||||
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true);
|
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true);
|
||||||
|
|
||||||
const result = await manager.getAIEnabledAsync();
|
const result = await manager.getAIEnabledAsync();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(configHelpers.isAIEnabled).toHaveBeenCalled();
|
expect(configHelpers.isAIEnabled).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -359,9 +363,9 @@ describe('AIServiceManager', () => {
|
|||||||
describe('getAIEnabled', () => {
|
describe('getAIEnabled', () => {
|
||||||
it('should return AI enabled status synchronously', () => {
|
it('should return AI enabled status synchronously', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(true);
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(true);
|
||||||
|
|
||||||
const result = manager.getAIEnabled();
|
const result = manager.getAIEnabled();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(options.getOptionBool).toHaveBeenCalledWith('aiEnabled');
|
expect(options.getOptionBool).toHaveBeenCalledWith('aiEnabled');
|
||||||
});
|
});
|
||||||
@ -370,17 +374,17 @@ describe('AIServiceManager', () => {
|
|||||||
describe('initialize', () => {
|
describe('initialize', () => {
|
||||||
it('should initialize if AI is enabled', async () => {
|
it('should initialize if AI is enabled', async () => {
|
||||||
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true);
|
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(true);
|
||||||
|
|
||||||
await manager.initialize();
|
await manager.initialize();
|
||||||
|
|
||||||
expect(configHelpers.isAIEnabled).toHaveBeenCalled();
|
expect(configHelpers.isAIEnabled).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize if AI is disabled', async () => {
|
it('should not initialize if AI is disabled', async () => {
|
||||||
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(false);
|
vi.mocked(configHelpers.isAIEnabled).mockResolvedValueOnce(false);
|
||||||
|
|
||||||
await manager.initialize();
|
await manager.initialize();
|
||||||
|
|
||||||
expect(configHelpers.isAIEnabled).toHaveBeenCalled();
|
expect(configHelpers.isAIEnabled).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -388,36 +392,36 @@ describe('AIServiceManager', () => {
|
|||||||
describe('getService', () => {
|
describe('getService', () => {
|
||||||
it('should return service for specified provider', async () => {
|
it('should return service for specified provider', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
||||||
|
|
||||||
const mockService = {
|
const mockService = {
|
||||||
isAvailable: vi.fn().mockReturnValue(true),
|
isAvailable: vi.fn().mockReturnValue(true),
|
||||||
generateChatCompletion: vi.fn()
|
generateChatCompletion: vi.fn()
|
||||||
};
|
};
|
||||||
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
vi.mocked(OpenAIService).mockImplementationOnce(() => mockService as any);
|
||||||
|
|
||||||
const result = await manager.getService('openai');
|
const result = await manager.getService('openai');
|
||||||
|
|
||||||
expect(result).toBe(mockService);
|
expect(result).toBe(mockService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return selected provider service if no provider specified', async () => {
|
it('should return selected provider service if no provider specified', async () => {
|
||||||
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('anthropic');
|
vi.mocked(configHelpers.getSelectedProvider).mockResolvedValueOnce('anthropic');
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
||||||
|
|
||||||
const mockService = {
|
const mockService = {
|
||||||
isAvailable: vi.fn().mockReturnValue(true),
|
isAvailable: vi.fn().mockReturnValue(true),
|
||||||
generateChatCompletion: vi.fn()
|
generateChatCompletion: vi.fn()
|
||||||
};
|
};
|
||||||
vi.mocked(AnthropicService).mockImplementationOnce(() => mockService as any);
|
vi.mocked(AnthropicService).mockImplementationOnce(() => mockService as any);
|
||||||
|
|
||||||
const result = await manager.getService();
|
const result = await manager.getService();
|
||||||
|
|
||||||
expect(result).toBe(mockService);
|
expect(result).toBe(mockService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if specified provider not available', async () => {
|
it('should throw error if specified provider not available', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
|
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
|
||||||
|
|
||||||
await expect(manager.getService('openai')).rejects.toThrow(
|
await expect(manager.getService('openai')).rejects.toThrow(
|
||||||
'Specified provider openai is not available'
|
'Specified provider openai is not available'
|
||||||
);
|
);
|
||||||
@ -427,17 +431,17 @@ describe('AIServiceManager', () => {
|
|||||||
describe('getSelectedProvider', () => {
|
describe('getSelectedProvider', () => {
|
||||||
it('should return selected provider synchronously', () => {
|
it('should return selected provider synchronously', () => {
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('anthropic');
|
vi.mocked(options.getOption).mockReturnValueOnce('anthropic');
|
||||||
|
|
||||||
const result = manager.getSelectedProvider();
|
const result = manager.getSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBe('anthropic');
|
expect(result).toBe('anthropic');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return default provider if none selected', () => {
|
it('should return default provider if none selected', () => {
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('');
|
vi.mocked(options.getOption).mockReturnValueOnce('');
|
||||||
|
|
||||||
const result = manager.getSelectedProvider();
|
const result = manager.getSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBe('openai');
|
expect(result).toBe('openai');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -446,18 +450,18 @@ describe('AIServiceManager', () => {
|
|||||||
it('should return true if provider service is available', () => {
|
it('should return true if provider service is available', () => {
|
||||||
// Mock that OpenAI has API key configured
|
// Mock that OpenAI has API key configured
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key');
|
||||||
|
|
||||||
const result = manager.isProviderAvailable('openai');
|
const result = manager.isProviderAvailable('openai');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if provider service not created', () => {
|
it('should return false if provider service not created', () => {
|
||||||
// Mock that OpenAI has no API key configured
|
// Mock that OpenAI has no API key configured
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('');
|
vi.mocked(options.getOption).mockReturnValueOnce('');
|
||||||
|
|
||||||
const result = manager.isProviderAvailable('openai');
|
const result = manager.isProviderAvailable('openai');
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -467,13 +471,13 @@ describe('AIServiceManager', () => {
|
|||||||
// Since getProviderMetadata only returns metadata for the current active provider,
|
// Since getProviderMetadata only returns metadata for the current active provider,
|
||||||
// and we don't have a current provider set, it should return null
|
// and we don't have a current provider set, it should return null
|
||||||
const result = manager.getProviderMetadata('openai');
|
const result = manager.getProviderMetadata('openai');
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for non-existing provider', () => {
|
it('should return null for non-existing provider', () => {
|
||||||
const result = manager.getProviderMetadata('openai');
|
const result = manager.getProviderMetadata('openai');
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -485,4 +489,4 @@ describe('AIServiceManager', () => {
|
|||||||
expect(manager).toBeDefined();
|
expect(manager).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,11 @@ vi.mock('../../log.js', () => ({
|
|||||||
vi.mock('../../options.js', () => ({
|
vi.mock('../../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -66,14 +70,14 @@ describe('RestChatService', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Get mocked modules
|
// Get mocked modules
|
||||||
mockOptions = (await import('../../options.js')).default;
|
mockOptions = (await import('../../options.js')).default;
|
||||||
mockAiServiceManager = (await import('../ai_service_manager.js')).default;
|
mockAiServiceManager = (await import('../ai_service_manager.js')).default;
|
||||||
mockChatStorageService = (await import('../chat_storage_service.js')).default;
|
mockChatStorageService = (await import('../chat_storage_service.js')).default;
|
||||||
|
|
||||||
restChatService = (await import('./rest_chat_service.js')).default;
|
restChatService = (await import('./rest_chat_service.js')).default;
|
||||||
|
|
||||||
// Setup mock request and response
|
// Setup mock request and response
|
||||||
mockReq = {
|
mockReq = {
|
||||||
params: {},
|
params: {},
|
||||||
@ -81,7 +85,7 @@ describe('RestChatService', () => {
|
|||||||
query: {},
|
query: {},
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
};
|
};
|
||||||
|
|
||||||
mockRes = {
|
mockRes = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn().mockReturnThis(),
|
json: vi.fn().mockReturnThis(),
|
||||||
@ -240,7 +244,7 @@ describe('RestChatService', () => {
|
|||||||
|
|
||||||
it('should handle GET request with stream parameter', async () => {
|
it('should handle GET request with stream parameter', async () => {
|
||||||
mockReq.method = 'GET';
|
mockReq.method = 'GET';
|
||||||
mockReq.query = {
|
mockReq.query = {
|
||||||
stream: 'true',
|
stream: 'true',
|
||||||
useAdvancedContext: 'true',
|
useAdvancedContext: 'true',
|
||||||
showThinking: 'false'
|
showThinking: 'false'
|
||||||
@ -419,4 +423,4 @@ describe('RestChatService', () => {
|
|||||||
expect(mockChatStorageService.getChat).toHaveBeenCalledWith('chat-123');
|
expect(mockChatStorageService.getChat).toHaveBeenCalledWith('chat-123');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,7 +18,11 @@ vi.mock('./configuration_manager.js', () => ({
|
|||||||
vi.mock('../../options.js', () => ({
|
vi.mock('../../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -42,26 +46,26 @@ describe('configuration_helpers', () => {
|
|||||||
describe('getSelectedProvider', () => {
|
describe('getSelectedProvider', () => {
|
||||||
it('should return the selected provider', async () => {
|
it('should return the selected provider', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValueOnce('openai');
|
vi.mocked(optionService.getOption).mockReturnValueOnce('openai');
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedProvider();
|
const result = await configHelpers.getSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBe('openai');
|
expect(result).toBe('openai');
|
||||||
expect(optionService.getOption).toHaveBeenCalledWith('aiSelectedProvider');
|
expect(optionService.getOption).toHaveBeenCalledWith('aiSelectedProvider');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null if no provider is selected', async () => {
|
it('should return null if no provider is selected', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValueOnce('');
|
vi.mocked(optionService.getOption).mockReturnValueOnce('');
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedProvider();
|
const result = await configHelpers.getSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle invalid provider and return null', async () => {
|
it('should handle invalid provider and return null', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValueOnce('invalid-provider');
|
vi.mocked(optionService.getOption).mockReturnValueOnce('invalid-provider');
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedProvider();
|
const result = await configHelpers.getSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBe('invalid-provider' as ProviderType);
|
expect(result).toBe('invalid-provider' as ProviderType);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -69,7 +73,7 @@ describe('configuration_helpers', () => {
|
|||||||
describe('parseModelIdentifier', () => {
|
describe('parseModelIdentifier', () => {
|
||||||
it('should parse model identifier directly', () => {
|
it('should parse model identifier directly', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('openai:gpt-4');
|
const result = configHelpers.parseModelIdentifier('openai:gpt-4');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
modelId: 'gpt-4',
|
modelId: 'gpt-4',
|
||||||
@ -79,7 +83,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle model without provider', () => {
|
it('should handle model without provider', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('gpt-4');
|
const result = configHelpers.parseModelIdentifier('gpt-4');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: 'gpt-4',
|
modelId: 'gpt-4',
|
||||||
fullIdentifier: 'gpt-4'
|
fullIdentifier: 'gpt-4'
|
||||||
@ -88,7 +92,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle empty model string', () => {
|
it('should handle empty model string', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('');
|
const result = configHelpers.parseModelIdentifier('');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: '',
|
modelId: '',
|
||||||
fullIdentifier: ''
|
fullIdentifier: ''
|
||||||
@ -98,7 +102,7 @@ describe('configuration_helpers', () => {
|
|||||||
// Tests for special characters in model names
|
// Tests for special characters in model names
|
||||||
it('should handle model names with periods', () => {
|
it('should handle model names with periods', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('gpt-4.1-turbo-preview');
|
const result = configHelpers.parseModelIdentifier('gpt-4.1-turbo-preview');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: 'gpt-4.1-turbo-preview',
|
modelId: 'gpt-4.1-turbo-preview',
|
||||||
fullIdentifier: 'gpt-4.1-turbo-preview'
|
fullIdentifier: 'gpt-4.1-turbo-preview'
|
||||||
@ -107,7 +111,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle model names with provider prefix and periods', () => {
|
it('should handle model names with provider prefix and periods', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('openai:gpt-4.1-turbo');
|
const result = configHelpers.parseModelIdentifier('openai:gpt-4.1-turbo');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
modelId: 'gpt-4.1-turbo',
|
modelId: 'gpt-4.1-turbo',
|
||||||
@ -117,7 +121,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle model names with multiple colons', () => {
|
it('should handle model names with multiple colons', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('custom:model:v1.2:latest');
|
const result = configHelpers.parseModelIdentifier('custom:model:v1.2:latest');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: 'custom:model:v1.2:latest',
|
modelId: 'custom:model:v1.2:latest',
|
||||||
fullIdentifier: 'custom:model:v1.2:latest'
|
fullIdentifier: 'custom:model:v1.2:latest'
|
||||||
@ -126,7 +130,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle Ollama model names with colons', () => {
|
it('should handle Ollama model names with colons', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('ollama:llama3.1:70b-instruct-q4_K_M');
|
const result = configHelpers.parseModelIdentifier('ollama:llama3.1:70b-instruct-q4_K_M');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
provider: 'ollama',
|
provider: 'ollama',
|
||||||
modelId: 'llama3.1:70b-instruct-q4_K_M',
|
modelId: 'llama3.1:70b-instruct-q4_K_M',
|
||||||
@ -136,7 +140,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle model names with slashes', () => {
|
it('should handle model names with slashes', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('library/mistral:7b-instruct');
|
const result = configHelpers.parseModelIdentifier('library/mistral:7b-instruct');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: 'library/mistral:7b-instruct',
|
modelId: 'library/mistral:7b-instruct',
|
||||||
fullIdentifier: 'library/mistral:7b-instruct'
|
fullIdentifier: 'library/mistral:7b-instruct'
|
||||||
@ -146,7 +150,7 @@ describe('configuration_helpers', () => {
|
|||||||
it('should handle complex model names with special characters', () => {
|
it('should handle complex model names with special characters', () => {
|
||||||
const complexName = 'org/model-v1.2.3:tag@version#variant';
|
const complexName = 'org/model-v1.2.3:tag@version#variant';
|
||||||
const result = configHelpers.parseModelIdentifier(complexName);
|
const result = configHelpers.parseModelIdentifier(complexName);
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: complexName,
|
modelId: complexName,
|
||||||
fullIdentifier: complexName
|
fullIdentifier: complexName
|
||||||
@ -155,7 +159,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle model names with @ symbols', () => {
|
it('should handle model names with @ symbols', () => {
|
||||||
const result = configHelpers.parseModelIdentifier('claude-3.5-sonnet@20241022');
|
const result = configHelpers.parseModelIdentifier('claude-3.5-sonnet@20241022');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: 'claude-3.5-sonnet@20241022',
|
modelId: 'claude-3.5-sonnet@20241022',
|
||||||
fullIdentifier: 'claude-3.5-sonnet@20241022'
|
fullIdentifier: 'claude-3.5-sonnet@20241022'
|
||||||
@ -165,7 +169,7 @@ describe('configuration_helpers', () => {
|
|||||||
it('should not modify or encode special characters', () => {
|
it('should not modify or encode special characters', () => {
|
||||||
const specialChars = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`';
|
const specialChars = 'model!@#$%^&*()_+-=[]{}|;:\'",.<>?/~`';
|
||||||
const result = configHelpers.parseModelIdentifier(specialChars);
|
const result = configHelpers.parseModelIdentifier(specialChars);
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
modelId: specialChars,
|
modelId: specialChars,
|
||||||
fullIdentifier: specialChars
|
fullIdentifier: specialChars
|
||||||
@ -176,7 +180,7 @@ describe('configuration_helpers', () => {
|
|||||||
describe('createModelConfig', () => {
|
describe('createModelConfig', () => {
|
||||||
it('should create model config directly', () => {
|
it('should create model config directly', () => {
|
||||||
const result = configHelpers.createModelConfig('gpt-4', 'openai');
|
const result = configHelpers.createModelConfig('gpt-4', 'openai');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
modelId: 'gpt-4',
|
modelId: 'gpt-4',
|
||||||
@ -186,7 +190,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should handle model with provider prefix', () => {
|
it('should handle model with provider prefix', () => {
|
||||||
const result = configHelpers.createModelConfig('openai:gpt-4');
|
const result = configHelpers.createModelConfig('openai:gpt-4');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
modelId: 'gpt-4',
|
modelId: 'gpt-4',
|
||||||
@ -196,7 +200,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should fallback to openai provider when none specified', () => {
|
it('should fallback to openai provider when none specified', () => {
|
||||||
const result = configHelpers.createModelConfig('gpt-4');
|
const result = configHelpers.createModelConfig('gpt-4');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
modelId: 'gpt-4',
|
modelId: 'gpt-4',
|
||||||
@ -208,27 +212,27 @@ describe('configuration_helpers', () => {
|
|||||||
describe('getDefaultModelForProvider', () => {
|
describe('getDefaultModelForProvider', () => {
|
||||||
it('should return default model for provider', async () => {
|
it('should return default model for provider', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValue('gpt-4');
|
vi.mocked(optionService.getOption).mockReturnValue('gpt-4');
|
||||||
|
|
||||||
const result = await configHelpers.getDefaultModelForProvider('openai');
|
const result = await configHelpers.getDefaultModelForProvider('openai');
|
||||||
|
|
||||||
expect(result).toBe('gpt-4');
|
expect(result).toBe('gpt-4');
|
||||||
expect(optionService.getOption).toHaveBeenCalledWith('openaiDefaultModel');
|
expect(optionService.getOption).toHaveBeenCalledWith('openaiDefaultModel');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined if no default model', async () => {
|
it('should return undefined if no default model', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValue('');
|
vi.mocked(optionService.getOption).mockReturnValue('');
|
||||||
|
|
||||||
const result = await configHelpers.getDefaultModelForProvider('anthropic');
|
const result = await configHelpers.getDefaultModelForProvider('anthropic');
|
||||||
|
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
expect(optionService.getOption).toHaveBeenCalledWith('anthropicDefaultModel');
|
expect(optionService.getOption).toHaveBeenCalledWith('anthropicDefaultModel');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle ollama provider', async () => {
|
it('should handle ollama provider', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValue('llama2');
|
vi.mocked(optionService.getOption).mockReturnValue('llama2');
|
||||||
|
|
||||||
const result = await configHelpers.getDefaultModelForProvider('ollama');
|
const result = await configHelpers.getDefaultModelForProvider('ollama');
|
||||||
|
|
||||||
expect(result).toBe('llama2');
|
expect(result).toBe('llama2');
|
||||||
expect(optionService.getOption).toHaveBeenCalledWith('ollamaDefaultModel');
|
expect(optionService.getOption).toHaveBeenCalledWith('ollamaDefaultModel');
|
||||||
});
|
});
|
||||||
@ -237,27 +241,27 @@ describe('configuration_helpers', () => {
|
|||||||
it('should handle OpenAI model names with periods', async () => {
|
it('should handle OpenAI model names with periods', async () => {
|
||||||
const modelName = 'gpt-4.1-turbo-preview';
|
const modelName = 'gpt-4.1-turbo-preview';
|
||||||
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
||||||
|
|
||||||
const result = await configHelpers.getDefaultModelForProvider('openai');
|
const result = await configHelpers.getDefaultModelForProvider('openai');
|
||||||
|
|
||||||
expect(result).toBe(modelName);
|
expect(result).toBe(modelName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle Anthropic model names with periods and @ symbols', async () => {
|
it('should handle Anthropic model names with periods and @ symbols', async () => {
|
||||||
const modelName = 'claude-3.5-sonnet@20241022';
|
const modelName = 'claude-3.5-sonnet@20241022';
|
||||||
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
||||||
|
|
||||||
const result = await configHelpers.getDefaultModelForProvider('anthropic');
|
const result = await configHelpers.getDefaultModelForProvider('anthropic');
|
||||||
|
|
||||||
expect(result).toBe(modelName);
|
expect(result).toBe(modelName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle Ollama model names with colons and slashes', async () => {
|
it('should handle Ollama model names with colons and slashes', async () => {
|
||||||
const modelName = 'library/llama3.1:70b-instruct-q4_K_M';
|
const modelName = 'library/llama3.1:70b-instruct-q4_K_M';
|
||||||
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
vi.mocked(optionService.getOption).mockReturnValue(modelName);
|
||||||
|
|
||||||
const result = await configHelpers.getDefaultModelForProvider('ollama');
|
const result = await configHelpers.getDefaultModelForProvider('ollama');
|
||||||
|
|
||||||
expect(result).toBe(modelName);
|
expect(result).toBe(modelName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -268,9 +272,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('https://api.openai.com') // openaiBaseUrl
|
.mockReturnValueOnce('https://api.openai.com') // openaiBaseUrl
|
||||||
.mockReturnValueOnce('gpt-4'); // openaiDefaultModel
|
.mockReturnValueOnce('gpt-4'); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getProviderSettings('openai');
|
const result = await configHelpers.getProviderSettings('openai');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
baseUrl: 'https://api.openai.com',
|
baseUrl: 'https://api.openai.com',
|
||||||
@ -283,9 +287,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
||||||
.mockReturnValueOnce('https://api.anthropic.com') // anthropicBaseUrl
|
.mockReturnValueOnce('https://api.anthropic.com') // anthropicBaseUrl
|
||||||
.mockReturnValueOnce('claude-3'); // anthropicDefaultModel
|
.mockReturnValueOnce('claude-3'); // anthropicDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getProviderSettings('anthropic');
|
const result = await configHelpers.getProviderSettings('anthropic');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
apiKey: 'anthropic-key',
|
apiKey: 'anthropic-key',
|
||||||
baseUrl: 'https://api.anthropic.com',
|
baseUrl: 'https://api.anthropic.com',
|
||||||
@ -297,9 +301,9 @@ describe('configuration_helpers', () => {
|
|||||||
vi.mocked(optionService.getOption)
|
vi.mocked(optionService.getOption)
|
||||||
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
||||||
.mockReturnValueOnce('llama2'); // ollamaDefaultModel
|
.mockReturnValueOnce('llama2'); // ollamaDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getProviderSettings('ollama');
|
const result = await configHelpers.getProviderSettings('ollama');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
defaultModel: 'llama2'
|
defaultModel: 'llama2'
|
||||||
@ -308,7 +312,7 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should return empty object for unknown provider', async () => {
|
it('should return empty object for unknown provider', async () => {
|
||||||
const result = await configHelpers.getProviderSettings('unknown' as ProviderType);
|
const result = await configHelpers.getProviderSettings('unknown' as ProviderType);
|
||||||
|
|
||||||
expect(result).toStrictEqual({});
|
expect(result).toStrictEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -316,18 +320,18 @@ describe('configuration_helpers', () => {
|
|||||||
describe('isAIEnabled', () => {
|
describe('isAIEnabled', () => {
|
||||||
it('should return true if AI is enabled', async () => {
|
it('should return true if AI is enabled', async () => {
|
||||||
vi.mocked(optionService.getOptionBool).mockReturnValue(true);
|
vi.mocked(optionService.getOptionBool).mockReturnValue(true);
|
||||||
|
|
||||||
const result = await configHelpers.isAIEnabled();
|
const result = await configHelpers.isAIEnabled();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled');
|
expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if AI is disabled', async () => {
|
it('should return false if AI is disabled', async () => {
|
||||||
vi.mocked(optionService.getOptionBool).mockReturnValue(false);
|
vi.mocked(optionService.getOptionBool).mockReturnValue(false);
|
||||||
|
|
||||||
const result = await configHelpers.isAIEnabled();
|
const result = await configHelpers.isAIEnabled();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled');
|
expect(optionService.getOptionBool).toHaveBeenCalledWith('aiEnabled');
|
||||||
});
|
});
|
||||||
@ -339,9 +343,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.isProviderConfigured('openai');
|
const result = await configHelpers.isProviderConfigured('openai');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -350,9 +354,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('') // openaiApiKey (empty)
|
.mockReturnValueOnce('') // openaiApiKey (empty)
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.isProviderConfigured('openai');
|
const result = await configHelpers.isProviderConfigured('openai');
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -361,9 +365,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
||||||
.mockReturnValueOnce('') // anthropicBaseUrl
|
.mockReturnValueOnce('') // anthropicBaseUrl
|
||||||
.mockReturnValueOnce(''); // anthropicDefaultModel
|
.mockReturnValueOnce(''); // anthropicDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.isProviderConfigured('anthropic');
|
const result = await configHelpers.isProviderConfigured('anthropic');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -371,15 +375,15 @@ describe('configuration_helpers', () => {
|
|||||||
vi.mocked(optionService.getOption)
|
vi.mocked(optionService.getOption)
|
||||||
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
||||||
.mockReturnValueOnce(''); // ollamaDefaultModel
|
.mockReturnValueOnce(''); // ollamaDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.isProviderConfigured('ollama');
|
const result = await configHelpers.isProviderConfigured('ollama');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for unknown provider', async () => {
|
it('should return false for unknown provider', async () => {
|
||||||
const result = await configHelpers.isProviderConfigured('unknown' as ProviderType);
|
const result = await configHelpers.isProviderConfigured('unknown' as ProviderType);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -391,17 +395,17 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getAvailableSelectedProvider();
|
const result = await configHelpers.getAvailableSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBe('openai');
|
expect(result).toBe('openai');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null if no provider selected', async () => {
|
it('should return null if no provider selected', async () => {
|
||||||
vi.mocked(optionService.getOption).mockReturnValueOnce('');
|
vi.mocked(optionService.getOption).mockReturnValueOnce('');
|
||||||
|
|
||||||
const result = await configHelpers.getAvailableSelectedProvider();
|
const result = await configHelpers.getAvailableSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -411,9 +415,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('') // openaiApiKey (empty)
|
.mockReturnValueOnce('') // openaiApiKey (empty)
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getAvailableSelectedProvider();
|
const result = await configHelpers.getAvailableSelectedProvider();
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -427,9 +431,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce('gpt-4'); // openaiDefaultModel
|
.mockReturnValueOnce('gpt-4'); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.validateConfiguration();
|
const result = await configHelpers.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
@ -439,9 +443,9 @@ describe('configuration_helpers', () => {
|
|||||||
|
|
||||||
it('should return warning when AI is disabled', async () => {
|
it('should return warning when AI is disabled', async () => {
|
||||||
vi.mocked(optionService.getOptionBool).mockReturnValue(false);
|
vi.mocked(optionService.getOptionBool).mockReturnValue(false);
|
||||||
|
|
||||||
const result = await configHelpers.validateConfiguration();
|
const result = await configHelpers.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
@ -452,9 +456,9 @@ describe('configuration_helpers', () => {
|
|||||||
it('should return error when no provider selected', async () => {
|
it('should return error when no provider selected', async () => {
|
||||||
vi.mocked(optionService.getOptionBool).mockReturnValue(true);
|
vi.mocked(optionService.getOptionBool).mockReturnValue(true);
|
||||||
vi.mocked(optionService.getOption).mockReturnValue(''); // no aiSelectedProvider
|
vi.mocked(optionService.getOption).mockReturnValue(''); // no aiSelectedProvider
|
||||||
|
|
||||||
const result = await configHelpers.validateConfiguration();
|
const result = await configHelpers.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errors: ['No AI provider selected'],
|
errors: ['No AI provider selected'],
|
||||||
@ -469,9 +473,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('') // openaiApiKey (empty)
|
.mockReturnValueOnce('') // openaiApiKey (empty)
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.validateConfiguration();
|
const result = await configHelpers.validateConfiguration();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
@ -495,9 +499,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getValidModelConfig('openai');
|
const result = await configHelpers.getValidModelConfig('openai');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'openai'
|
provider: 'openai'
|
||||||
@ -511,9 +515,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
.mockReturnValueOnce('anthropic-key') // anthropicApiKey
|
||||||
.mockReturnValueOnce('') // anthropicBaseUrl
|
.mockReturnValueOnce('') // anthropicBaseUrl
|
||||||
.mockReturnValueOnce(''); // anthropicDefaultModel
|
.mockReturnValueOnce(''); // anthropicDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getValidModelConfig('anthropic');
|
const result = await configHelpers.getValidModelConfig('anthropic');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'anthropic'
|
provider: 'anthropic'
|
||||||
@ -526,9 +530,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce(modelName) // ollamaDefaultModel
|
.mockReturnValueOnce(modelName) // ollamaDefaultModel
|
||||||
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
||||||
.mockReturnValueOnce(''); // ollamaDefaultModel
|
.mockReturnValueOnce(''); // ollamaDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getValidModelConfig('ollama');
|
const result = await configHelpers.getValidModelConfig('ollama');
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'ollama'
|
provider: 'ollama'
|
||||||
@ -545,9 +549,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedModelConfig();
|
const result = await configHelpers.getSelectedModelConfig();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'openai'
|
provider: 'openai'
|
||||||
@ -562,9 +566,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // openaiApiKey
|
.mockReturnValueOnce('test-key') // openaiApiKey
|
||||||
.mockReturnValueOnce('') // openaiBaseUrl
|
.mockReturnValueOnce('') // openaiBaseUrl
|
||||||
.mockReturnValueOnce(''); // openaiDefaultModel
|
.mockReturnValueOnce(''); // openaiDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedModelConfig();
|
const result = await configHelpers.getSelectedModelConfig();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'openai'
|
provider: 'openai'
|
||||||
@ -578,9 +582,9 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce(modelName) // ollamaDefaultModel
|
.mockReturnValueOnce(modelName) // ollamaDefaultModel
|
||||||
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
.mockReturnValueOnce('http://localhost:11434') // ollamaBaseUrl
|
||||||
.mockReturnValueOnce(''); // ollamaDefaultModel
|
.mockReturnValueOnce(''); // ollamaDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedModelConfig();
|
const result = await configHelpers.getSelectedModelConfig();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'ollama'
|
provider: 'ollama'
|
||||||
@ -595,13 +599,13 @@ describe('configuration_helpers', () => {
|
|||||||
.mockReturnValueOnce('test-key') // anthropicApiKey
|
.mockReturnValueOnce('test-key') // anthropicApiKey
|
||||||
.mockReturnValueOnce('') // anthropicBaseUrl
|
.mockReturnValueOnce('') // anthropicBaseUrl
|
||||||
.mockReturnValueOnce(''); // anthropicDefaultModel
|
.mockReturnValueOnce(''); // anthropicDefaultModel
|
||||||
|
|
||||||
const result = await configHelpers.getSelectedModelConfig();
|
const result = await configHelpers.getSelectedModelConfig();
|
||||||
|
|
||||||
expect(result).toStrictEqual({
|
expect(result).toStrictEqual({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
provider: 'anthropic'
|
provider: 'anthropic'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,11 @@ import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js';
|
|||||||
vi.mock('../../options.js', () => ({
|
vi.mock('../../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -79,7 +83,7 @@ describe('AnthropicService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Get the mocked Anthropic instance before creating the service
|
// Get the mocked Anthropic instance before creating the service
|
||||||
const AnthropicMock = vi.mocked(Anthropic);
|
const AnthropicMock = vi.mocked(Anthropic);
|
||||||
mockAnthropicInstance = {
|
mockAnthropicInstance = {
|
||||||
@ -122,9 +126,9 @@ describe('AnthropicService', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
AnthropicMock.mockImplementation(() => mockAnthropicInstance);
|
AnthropicMock.mockImplementation(() => mockAnthropicInstance);
|
||||||
|
|
||||||
service = new AnthropicService();
|
service = new AnthropicService();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -144,26 +148,26 @@ describe('AnthropicService', () => {
|
|||||||
it('should return true when AI is enabled and API key exists', () => {
|
it('should return true when AI is enabled and API key exists', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); // API key
|
vi.mocked(options.getOption).mockReturnValueOnce('test-api-key'); // API key
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when AI is disabled', () => {
|
it('should return false when AI is disabled', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when no API key', () => {
|
it('should return false when no API key', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
|
vi.mocked(options.getOption).mockReturnValueOnce(''); // No API key
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -190,9 +194,9 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: 'Hello! How can I help you today?',
|
text: 'Hello! How can I help you today?',
|
||||||
provider: 'Anthropic',
|
provider: 'Anthropic',
|
||||||
@ -214,11 +218,11 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create');
|
const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create');
|
||||||
|
|
||||||
await service.generateChatCompletion(messages);
|
await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
const calledParams = createSpy.mock.calls[0][0] as any;
|
const calledParams = createSpy.mock.calls[0][0] as any;
|
||||||
expect(calledParams.messages).toEqual([
|
expect(calledParams.messages).toEqual([
|
||||||
{ role: 'user', content: 'Hello' }
|
{ role: 'user', content: 'Hello' }
|
||||||
@ -235,12 +239,12 @@ describe('AnthropicService', () => {
|
|||||||
onChunk: vi.fn()
|
onChunk: vi.fn()
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
// Wait for chunks to be processed
|
// Wait for chunks to be processed
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Check that the result exists (streaming logic is complex, so we just verify basic structure)
|
// Check that the result exists (streaming logic is complex, so we just verify basic structure)
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result).toHaveProperty('text');
|
expect(result).toHaveProperty('text');
|
||||||
@ -256,7 +260,7 @@ describe('AnthropicService', () => {
|
|||||||
properties: {}
|
properties: {}
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
baseUrl: 'https://api.anthropic.com',
|
baseUrl: 'https://api.anthropic.com',
|
||||||
@ -267,7 +271,7 @@ describe('AnthropicService', () => {
|
|||||||
tool_choice: { type: 'any' }
|
tool_choice: { type: 'any' }
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock response with tool use
|
// Mock response with tool use
|
||||||
mockAnthropicInstance.messages.create.mockResolvedValueOnce({
|
mockAnthropicInstance.messages.create.mockResolvedValueOnce({
|
||||||
id: 'msg_123',
|
id: 'msg_123',
|
||||||
@ -287,9 +291,9 @@ describe('AnthropicService', () => {
|
|||||||
output_tokens: 25
|
output_tokens: 25
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: '',
|
text: '',
|
||||||
provider: 'Anthropic',
|
provider: 'Anthropic',
|
||||||
@ -312,7 +316,7 @@ describe('AnthropicService', () => {
|
|||||||
|
|
||||||
it('should throw error if service not available', async () => {
|
it('should throw error if service not available', async () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'Anthropic service is not available'
|
'Anthropic service is not available'
|
||||||
);
|
);
|
||||||
@ -326,12 +330,12 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock API error
|
// Mock API error
|
||||||
mockAnthropicInstance.messages.create.mockRejectedValueOnce(
|
mockAnthropicInstance.messages.create.mockRejectedValueOnce(
|
||||||
new Error('API Error: Invalid API key')
|
new Error('API Error: Invalid API key')
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'API Error: Invalid API key'
|
'API Error: Invalid API key'
|
||||||
);
|
);
|
||||||
@ -347,15 +351,15 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Spy on Anthropic constructor
|
// Spy on Anthropic constructor
|
||||||
const AnthropicMock = vi.mocked(Anthropic);
|
const AnthropicMock = vi.mocked(Anthropic);
|
||||||
AnthropicMock.mockClear();
|
AnthropicMock.mockClear();
|
||||||
|
|
||||||
// Create new service to trigger client creation
|
// Create new service to trigger client creation
|
||||||
const newService = new AnthropicService();
|
const newService = new AnthropicService();
|
||||||
await newService.generateChatCompletion(messages);
|
await newService.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(AnthropicMock).toHaveBeenCalledWith({
|
expect(AnthropicMock).toHaveBeenCalledWith({
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
baseURL: 'https://api.anthropic.com',
|
baseURL: 'https://api.anthropic.com',
|
||||||
@ -374,15 +378,15 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Spy on Anthropic constructor
|
// Spy on Anthropic constructor
|
||||||
const AnthropicMock = vi.mocked(Anthropic);
|
const AnthropicMock = vi.mocked(Anthropic);
|
||||||
AnthropicMock.mockClear();
|
AnthropicMock.mockClear();
|
||||||
|
|
||||||
// Create new service to trigger client creation
|
// Create new service to trigger client creation
|
||||||
const newService = new AnthropicService();
|
const newService = new AnthropicService();
|
||||||
await newService.generateChatCompletion(messages);
|
await newService.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(AnthropicMock).toHaveBeenCalledWith({
|
expect(AnthropicMock).toHaveBeenCalledWith({
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
baseURL: 'https://api.anthropic.com',
|
baseURL: 'https://api.anthropic.com',
|
||||||
@ -401,7 +405,7 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock response with mixed content
|
// Mock response with mixed content
|
||||||
mockAnthropicInstance.messages.create.mockResolvedValueOnce({
|
mockAnthropicInstance.messages.create.mockResolvedValueOnce({
|
||||||
id: 'msg_123',
|
id: 'msg_123',
|
||||||
@ -420,9 +424,9 @@ describe('AnthropicService', () => {
|
|||||||
output_tokens: 25
|
output_tokens: 25
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result.text).toBe('Here is the result: The calculation is complete.');
|
expect(result.text).toBe('Here is the result: The calculation is complete.');
|
||||||
expect(result.tool_calls).toHaveLength(1);
|
expect(result.tool_calls).toHaveLength(1);
|
||||||
expect(result.tool_calls![0].function.name).toBe('calculate');
|
expect(result.tool_calls![0].function.name).toBe('calculate');
|
||||||
@ -431,8 +435,8 @@ describe('AnthropicService', () => {
|
|||||||
it('should handle tool results in messages', async () => {
|
it('should handle tool results in messages', async () => {
|
||||||
const messagesWithToolResult: Message[] = [
|
const messagesWithToolResult: Message[] = [
|
||||||
{ role: 'user', content: 'Calculate 5 + 3' },
|
{ role: 'user', content: 'Calculate 5 + 3' },
|
||||||
{
|
{
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: '',
|
||||||
tool_calls: [{
|
tool_calls: [{
|
||||||
id: 'call_123',
|
id: 'call_123',
|
||||||
@ -440,13 +444,13 @@ describe('AnthropicService', () => {
|
|||||||
function: { name: 'calculate', arguments: '{"x": 5, "y": 3}' }
|
function: { name: 'calculate', arguments: '{"x": 5, "y": 3}' }
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
content: '8',
|
content: '8',
|
||||||
tool_call_id: 'call_123'
|
tool_call_id: 'call_123'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
baseUrl: 'https://api.anthropic.com',
|
baseUrl: 'https://api.anthropic.com',
|
||||||
@ -454,11 +458,11 @@ describe('AnthropicService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getAnthropicOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create');
|
const createSpy = vi.spyOn(mockAnthropicInstance.messages, 'create');
|
||||||
|
|
||||||
await service.generateChatCompletion(messagesWithToolResult);
|
await service.generateChatCompletion(messagesWithToolResult);
|
||||||
|
|
||||||
const formattedMessages = (createSpy.mock.calls[0][0] as any).messages;
|
const formattedMessages = (createSpy.mock.calls[0][0] as any).messages;
|
||||||
expect(formattedMessages).toHaveLength(3);
|
expect(formattedMessages).toHaveLength(3);
|
||||||
expect(formattedMessages[2]).toEqual({
|
expect(formattedMessages[2]).toEqual({
|
||||||
@ -471,4 +475,4 @@ describe('AnthropicService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,11 @@ import options from '../../options.js';
|
|||||||
vi.mock('../../options.js', () => ({
|
vi.mock('../../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -82,7 +86,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
|
|
||||||
// Spy on getOpenAIOptions to verify model name is passed correctly
|
// Spy on getOpenAIOptions to verify model name is passed correctly
|
||||||
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
|
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -108,7 +112,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
|
await service.generateChatCompletion([{ role: 'user', content: 'test' }], opts);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -127,7 +131,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
const getOpenAIOptionsSpy = vi.spyOn(providers, 'getOpenAIOptions');
|
||||||
|
|
||||||
const openaiOptions = providers.getOpenAIOptions(opts);
|
const openaiOptions = providers.getOpenAIOptions(opts);
|
||||||
expect(openaiOptions.model).toBe(modelName);
|
expect(openaiOptions.model).toBe(modelName);
|
||||||
});
|
});
|
||||||
@ -153,7 +157,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const service = new OpenAIService();
|
const service = new OpenAIService();
|
||||||
|
|
||||||
// Access the private openai client through the service
|
// Access the private openai client through the service
|
||||||
const client = (service as any).getClient('test-key');
|
const client = (service as any).getClient('test-key');
|
||||||
const createSpy = vi.spyOn(client.chat.completions, 'create');
|
const createSpy = vi.spyOn(client.chat.completions, 'create');
|
||||||
@ -213,7 +217,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const service = new AnthropicService();
|
const service = new AnthropicService();
|
||||||
|
|
||||||
// Access the private anthropic client
|
// Access the private anthropic client
|
||||||
const client = (service as any).getClient('test-key');
|
const client = (service as any).getClient('test-key');
|
||||||
const createSpy = vi.spyOn(client.messages, 'create');
|
const createSpy = vi.spyOn(client.messages, 'create');
|
||||||
@ -278,7 +282,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
|
|
||||||
const ollamaOptions = await providers.getOllamaOptions(opts);
|
const ollamaOptions = await providers.getOllamaOptions(opts);
|
||||||
expect(ollamaOptions.model).toBe(modelName);
|
expect(ollamaOptions.model).toBe(modelName);
|
||||||
|
|
||||||
// Also test with model specified in options
|
// Also test with model specified in options
|
||||||
const optsWithModel: ChatCompletionOptions = {
|
const optsWithModel: ChatCompletionOptions = {
|
||||||
model: 'another/model:v2.0@beta',
|
model: 'another/model:v2.0@beta',
|
||||||
@ -370,7 +374,7 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
describe('Integration with REST API', () => {
|
describe('Integration with REST API', () => {
|
||||||
it('should pass model names correctly through REST chat service', async () => {
|
it('should pass model names correctly through REST chat service', async () => {
|
||||||
const modelName = 'gpt-4.1-turbo-preview@latest';
|
const modelName = 'gpt-4.1-turbo-preview@latest';
|
||||||
|
|
||||||
// Mock the configuration helpers
|
// Mock the configuration helpers
|
||||||
vi.doMock('../config/configuration_helpers.js', () => ({
|
vi.doMock('../config/configuration_helpers.js', () => ({
|
||||||
getSelectedModelConfig: vi.fn().mockResolvedValue({
|
getSelectedModelConfig: vi.fn().mockResolvedValue({
|
||||||
@ -382,8 +386,8 @@ describe('LLM Model Selection with Special Characters', () => {
|
|||||||
|
|
||||||
const { getSelectedModelConfig } = await import('../config/configuration_helpers.js');
|
const { getSelectedModelConfig } = await import('../config/configuration_helpers.js');
|
||||||
const config = await getSelectedModelConfig();
|
const config = await getSelectedModelConfig();
|
||||||
|
|
||||||
expect(config?.model).toBe(modelName);
|
expect(config?.model).toBe(modelName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,11 @@ import { Ollama } from 'ollama';
|
|||||||
vi.mock('../../options.js', () => ({
|
vi.mock('../../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -134,7 +138,7 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Create the mock instance before creating the service
|
// Create the mock instance before creating the service
|
||||||
const OllamaMock = vi.mocked(Ollama);
|
const OllamaMock = vi.mocked(Ollama);
|
||||||
mockOllamaInstance = {
|
mockOllamaInstance = {
|
||||||
@ -191,11 +195,11 @@ describe('OllamaService', () => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
OllamaMock.mockImplementation(() => mockOllamaInstance);
|
OllamaMock.mockImplementation(() => mockOllamaInstance);
|
||||||
|
|
||||||
service = new OllamaService();
|
service = new OllamaService();
|
||||||
|
|
||||||
// Replace the formatter with a mock after construction
|
// Replace the formatter with a mock after construction
|
||||||
(service as any).formatter = {
|
(service as any).formatter = {
|
||||||
formatMessages: vi.fn().mockReturnValue([
|
formatMessages: vi.fn().mockReturnValue([
|
||||||
@ -231,26 +235,26 @@ describe('OllamaService', () => {
|
|||||||
it('should return true when AI is enabled and base URL exists', () => {
|
it('should return true when AI is enabled and base URL exists', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce('http://localhost:11434'); // Base URL
|
vi.mocked(options.getOption).mockReturnValueOnce('http://localhost:11434'); // Base URL
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when AI is disabled', () => {
|
it('should return false when AI is disabled', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when no base URL', () => {
|
it('should return false when no base URL', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
||||||
vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL
|
vi.mocked(options.getOption).mockReturnValueOnce(''); // No base URL
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -275,9 +279,9 @@ describe('OllamaService', () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: 'Hello! How can I help you today?',
|
text: 'Hello! How can I help you today?',
|
||||||
provider: 'ollama',
|
provider: 'ollama',
|
||||||
@ -296,12 +300,12 @@ describe('OllamaService', () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
// Wait for chunks to be processed
|
// Wait for chunks to be processed
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// For streaming, we expect a different response structure
|
// For streaming, we expect a different response structure
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result).toHaveProperty('text');
|
expect(result).toHaveProperty('text');
|
||||||
@ -310,7 +314,7 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should handle tools when enabled', async () => {
|
it('should handle tools when enabled', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockTools = [{
|
const mockTools = [{
|
||||||
name: 'test_tool',
|
name: 'test_tool',
|
||||||
description: 'Test tool',
|
description: 'Test tool',
|
||||||
@ -320,7 +324,7 @@ describe('OllamaService', () => {
|
|||||||
required: []
|
required: []
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
@ -329,18 +333,18 @@ describe('OllamaService', () => {
|
|||||||
tools: mockTools
|
tools: mockTools
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
const chatSpy = vi.spyOn(mockOllamaInstance, 'chat');
|
const chatSpy = vi.spyOn(mockOllamaInstance, 'chat');
|
||||||
|
|
||||||
await service.generateChatCompletion(messages);
|
await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
const calledParams = chatSpy.mock.calls[0][0] as any;
|
const calledParams = chatSpy.mock.calls[0][0] as any;
|
||||||
expect(calledParams.tools).toEqual(mockTools);
|
expect(calledParams.tools).toEqual(mockTools);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if service not available', async () => {
|
it('should throw error if service not available', async () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'Ollama service is not available'
|
'Ollama service is not available'
|
||||||
);
|
);
|
||||||
@ -350,14 +354,14 @@ describe('OllamaService', () => {
|
|||||||
vi.mocked(options.getOption)
|
vi.mocked(options.getOption)
|
||||||
.mockReturnValueOnce('') // Empty base URL for ollamaBaseUrl
|
.mockReturnValueOnce('') // Empty base URL for ollamaBaseUrl
|
||||||
.mockReturnValue(''); // Ensure all subsequent calls return empty
|
.mockReturnValue(''); // Ensure all subsequent calls return empty
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'Ollama service is not available'
|
'Ollama service is not available'
|
||||||
);
|
);
|
||||||
@ -365,19 +369,19 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should handle API errors', async () => {
|
it('should handle API errors', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock API error
|
// Mock API error
|
||||||
mockOllamaInstance.chat.mockRejectedValueOnce(
|
mockOllamaInstance.chat.mockRejectedValueOnce(
|
||||||
new Error('Connection refused')
|
new Error('Connection refused')
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'Connection refused'
|
'Connection refused'
|
||||||
);
|
);
|
||||||
@ -385,30 +389,30 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should create client with custom fetch for debugging', async () => {
|
it('should create client with custom fetch for debugging', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
// Spy on Ollama constructor
|
// Spy on Ollama constructor
|
||||||
const OllamaMock = vi.mocked(Ollama);
|
const OllamaMock = vi.mocked(Ollama);
|
||||||
OllamaMock.mockClear();
|
OllamaMock.mockClear();
|
||||||
|
|
||||||
// Create new service to trigger client creation
|
// Create new service to trigger client creation
|
||||||
const newService = new OllamaService();
|
const newService = new OllamaService();
|
||||||
|
|
||||||
// Replace the formatter with a mock for the new service
|
// Replace the formatter with a mock for the new service
|
||||||
(newService as any).formatter = {
|
(newService as any).formatter = {
|
||||||
formatMessages: vi.fn().mockReturnValue([
|
formatMessages: vi.fn().mockReturnValue([
|
||||||
{ role: 'user', content: 'Hello' }
|
{ role: 'user', content: 'Hello' }
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
||||||
await newService.generateChatCompletion(messages);
|
await newService.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(OllamaMock).toHaveBeenCalledWith({
|
expect(OllamaMock).toHaveBeenCalledWith({
|
||||||
host: 'http://localhost:11434',
|
host: 'http://localhost:11434',
|
||||||
fetch: expect.any(Function)
|
fetch: expect.any(Function)
|
||||||
@ -417,7 +421,7 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should handle tool execution feedback', async () => {
|
it('should handle tool execution feedback', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
@ -426,7 +430,7 @@ describe('OllamaService', () => {
|
|||||||
tools: [{ name: 'test_tool', description: 'Test', parameters: {} }]
|
tools: [{ name: 'test_tool', description: 'Test', parameters: {} }]
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock response with tool call (arguments should be a string for Ollama)
|
// Mock response with tool call (arguments should be a string for Ollama)
|
||||||
mockOllamaInstance.chat.mockResolvedValueOnce({
|
mockOllamaInstance.chat.mockResolvedValueOnce({
|
||||||
message: {
|
message: {
|
||||||
@ -442,9 +446,9 @@ describe('OllamaService', () => {
|
|||||||
},
|
},
|
||||||
done: true
|
done: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result.tool_calls).toEqual([{
|
expect(result.tool_calls).toEqual([{
|
||||||
id: 'call_123',
|
id: 'call_123',
|
||||||
type: 'function',
|
type: 'function',
|
||||||
@ -457,14 +461,14 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should handle mixed text and tool content', async () => {
|
it('should handle mixed text and tool content', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock response with both text and tool calls
|
// Mock response with both text and tool calls
|
||||||
mockOllamaInstance.chat.mockResolvedValueOnce({
|
mockOllamaInstance.chat.mockResolvedValueOnce({
|
||||||
message: {
|
message: {
|
||||||
@ -480,30 +484,30 @@ describe('OllamaService', () => {
|
|||||||
},
|
},
|
||||||
done: true
|
done: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result.text).toBe('Let me help you with that.');
|
expect(result.text).toBe('Let me help you with that.');
|
||||||
expect(result.tool_calls).toHaveLength(1);
|
expect(result.tool_calls).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format messages using the formatter', async () => {
|
it('should format messages using the formatter', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
const formattedMessages = [{ role: 'user', content: 'Hello' }];
|
const formattedMessages = [{ role: 'user', content: 'Hello' }];
|
||||||
(service as any).formatter.formatMessages.mockReturnValueOnce(formattedMessages);
|
(service as any).formatter.formatMessages.mockReturnValueOnce(formattedMessages);
|
||||||
|
|
||||||
const chatSpy = vi.spyOn(mockOllamaInstance, 'chat');
|
const chatSpy = vi.spyOn(mockOllamaInstance, 'chat');
|
||||||
|
|
||||||
await service.generateChatCompletion(messages);
|
await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect((service as any).formatter.formatMessages).toHaveBeenCalled();
|
expect((service as any).formatter.formatMessages).toHaveBeenCalled();
|
||||||
expect(chatSpy).toHaveBeenCalledWith(
|
expect(chatSpy).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -514,23 +518,23 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should handle network errors gracefully', async () => {
|
it('should handle network errors gracefully', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock network error
|
// Mock network error
|
||||||
global.fetch = vi.fn().mockRejectedValueOnce(
|
global.fetch = vi.fn().mockRejectedValueOnce(
|
||||||
new Error('Network error')
|
new Error('Network error')
|
||||||
);
|
);
|
||||||
|
|
||||||
mockOllamaInstance.chat.mockRejectedValueOnce(
|
mockOllamaInstance.chat.mockRejectedValueOnce(
|
||||||
new Error('fetch failed')
|
new Error('fetch failed')
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'fetch failed'
|
'fetch failed'
|
||||||
);
|
);
|
||||||
@ -538,19 +542,19 @@ describe('OllamaService', () => {
|
|||||||
|
|
||||||
it('should validate model availability', async () => {
|
it('should validate model availability', async () => {
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'nonexistent-model',
|
model: 'nonexistent-model',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock model not found error
|
// Mock model not found error
|
||||||
mockOllamaInstance.chat.mockRejectedValueOnce(
|
mockOllamaInstance.chat.mockRejectedValueOnce(
|
||||||
new Error('model "nonexistent-model" not found')
|
new Error('model "nonexistent-model" not found')
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'model "nonexistent-model" not found'
|
'model "nonexistent-model" not found'
|
||||||
);
|
);
|
||||||
@ -561,23 +565,23 @@ describe('OllamaService', () => {
|
|||||||
it('should reuse existing client', async () => {
|
it('should reuse existing client', async () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValue(true);
|
vi.mocked(options.getOptionBool).mockReturnValue(true);
|
||||||
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
vi.mocked(options.getOption).mockReturnValue('http://localhost:11434');
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: 'http://localhost:11434',
|
||||||
model: 'llama2',
|
model: 'llama2',
|
||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOllamaOptions).mockResolvedValue(mockOptions);
|
vi.mocked(providers.getOllamaOptions).mockResolvedValue(mockOptions);
|
||||||
|
|
||||||
const OllamaMock = vi.mocked(Ollama);
|
const OllamaMock = vi.mocked(Ollama);
|
||||||
OllamaMock.mockClear();
|
OllamaMock.mockClear();
|
||||||
|
|
||||||
// Make two calls
|
// Make two calls
|
||||||
await service.generateChatCompletion([{ role: 'user', content: 'Hello' }]);
|
await service.generateChatCompletion([{ role: 'user', content: 'Hello' }]);
|
||||||
await service.generateChatCompletion([{ role: 'user', content: 'Hi' }]);
|
await service.generateChatCompletion([{ role: 'user', content: 'Hi' }]);
|
||||||
|
|
||||||
// Should only create client once
|
// Should only create client once
|
||||||
expect(OllamaMock).toHaveBeenCalledTimes(1);
|
expect(OllamaMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,13 +2,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||||||
import { OpenAIService } from './openai_service.js';
|
import { OpenAIService } from './openai_service.js';
|
||||||
import options from '../../options.js';
|
import options from '../../options.js';
|
||||||
import * as providers from './providers.js';
|
import * as providers from './providers.js';
|
||||||
import type { ChatCompletionOptions, Message } from '../ai_interface.js';
|
import type { Message } from '../ai_interface.js';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('../../options.js', () => ({
|
vi.mock('../../options.js', () => ({
|
||||||
default: {
|
default: {
|
||||||
getOption: vi.fn(),
|
getOption: vi.fn(),
|
||||||
getOptionBool: vi.fn()
|
getOptionBool: vi.fn(),
|
||||||
|
getOptionInt: vi.fn(name => {
|
||||||
|
if (name === "protectedSessionTimeout") return Number.MAX_SAFE_INTEGER;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -53,17 +57,17 @@ describe('OpenAIService', () => {
|
|||||||
describe('isAvailable', () => {
|
describe('isAvailable', () => {
|
||||||
it('should return true when base checks pass', () => {
|
it('should return true when base checks pass', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(true); // AI enabled
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when AI is disabled', () => {
|
it('should return false when AI is disabled', () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
||||||
|
|
||||||
const result = service.isAvailable();
|
const result = service.isAvailable();
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -89,7 +93,7 @@ describe('OpenAIService', () => {
|
|||||||
enableTools: false
|
enableTools: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock the getClient method to return our mock client
|
// Mock the getClient method to return our mock client
|
||||||
const mockCompletion = {
|
const mockCompletion = {
|
||||||
id: 'chatcmpl-123',
|
id: 'chatcmpl-123',
|
||||||
@ -120,9 +124,9 @@ describe('OpenAIService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: 'Hello! How can I help you today?',
|
text: 'Hello! How can I help you today?',
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
@ -144,7 +148,7 @@ describe('OpenAIService', () => {
|
|||||||
stream: true
|
stream: true
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
// Mock the streaming response
|
// Mock the streaming response
|
||||||
const mockStream = {
|
const mockStream = {
|
||||||
[Symbol.asyncIterator]: async function* () {
|
[Symbol.asyncIterator]: async function* () {
|
||||||
@ -162,7 +166,7 @@ describe('OpenAIService', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
chat: {
|
chat: {
|
||||||
completions: {
|
completions: {
|
||||||
@ -172,9 +176,9 @@ describe('OpenAIService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toHaveProperty('stream');
|
expect(result).toHaveProperty('stream');
|
||||||
expect(result.text).toBe('');
|
expect(result.text).toBe('');
|
||||||
expect(result.model).toBe('gpt-3.5-turbo');
|
expect(result.model).toBe('gpt-3.5-turbo');
|
||||||
@ -183,7 +187,7 @@ describe('OpenAIService', () => {
|
|||||||
|
|
||||||
it('should throw error if service not available', async () => {
|
it('should throw error if service not available', async () => {
|
||||||
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
vi.mocked(options.getOptionBool).mockReturnValueOnce(false); // AI disabled
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'OpenAI service is not available'
|
'OpenAI service is not available'
|
||||||
);
|
);
|
||||||
@ -197,7 +201,7 @@ describe('OpenAIService', () => {
|
|||||||
stream: false
|
stream: false
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
chat: {
|
chat: {
|
||||||
completions: {
|
completions: {
|
||||||
@ -207,7 +211,7 @@ describe('OpenAIService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
||||||
|
|
||||||
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
await expect(service.generateChatCompletion(messages)).rejects.toThrow(
|
||||||
'API Error: Invalid API key'
|
'API Error: Invalid API key'
|
||||||
);
|
);
|
||||||
@ -222,7 +226,7 @@ describe('OpenAIService', () => {
|
|||||||
parameters: {}
|
parameters: {}
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const mockOptions = {
|
const mockOptions = {
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
baseUrl: 'https://api.openai.com/v1',
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
@ -233,7 +237,7 @@ describe('OpenAIService', () => {
|
|||||||
tool_choice: 'auto'
|
tool_choice: 'auto'
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const mockCompletion = {
|
const mockCompletion = {
|
||||||
id: 'chatcmpl-123',
|
id: 'chatcmpl-123',
|
||||||
object: 'chat.completion',
|
object: 'chat.completion',
|
||||||
@ -263,9 +267,9 @@ describe('OpenAIService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
||||||
|
|
||||||
await service.generateChatCompletion(messages);
|
await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
const createCall = mockClient.chat.completions.create.mock.calls[0][0];
|
const createCall = mockClient.chat.completions.create.mock.calls[0][0];
|
||||||
expect(createCall.tools).toEqual(mockTools);
|
expect(createCall.tools).toEqual(mockTools);
|
||||||
expect(createCall.tool_choice).toBe('auto');
|
expect(createCall.tool_choice).toBe('auto');
|
||||||
@ -281,7 +285,7 @@ describe('OpenAIService', () => {
|
|||||||
tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }]
|
tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }]
|
||||||
};
|
};
|
||||||
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
vi.mocked(providers.getOpenAIOptions).mockReturnValueOnce(mockOptions);
|
||||||
|
|
||||||
const mockCompletion = {
|
const mockCompletion = {
|
||||||
id: 'chatcmpl-123',
|
id: 'chatcmpl-123',
|
||||||
object: 'chat.completion',
|
object: 'chat.completion',
|
||||||
@ -319,9 +323,9 @@ describe('OpenAIService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
vi.spyOn(service as any, 'getClient').mockReturnValue(mockClient);
|
||||||
|
|
||||||
const result = await service.generateChatCompletion(messages);
|
const result = await service.generateChatCompletion(messages);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
text: '',
|
text: '',
|
||||||
model: 'gpt-3.5-turbo',
|
model: 'gpt-3.5-turbo',
|
||||||
@ -342,4 +346,4 @@ describe('OpenAIService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import log from "./log.js";
|
|
||||||
import dataEncryptionService from "./encryption/data_encryption.js";
|
import dataEncryptionService from "./encryption/data_encryption.js";
|
||||||
import options from "./options.js";
|
|
||||||
import ws from "./ws.js";
|
|
||||||
|
|
||||||
let dataKey: Buffer | null = null;
|
let dataKey: Buffer | null = null;
|
||||||
|
|
||||||
@ -15,11 +12,11 @@ function getDataKey() {
|
|||||||
return dataKey;
|
return dataKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetDataKey() {
|
export function resetDataKey() {
|
||||||
dataKey = null;
|
dataKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProtectedSessionAvailable() {
|
export function isProtectedSessionAvailable() {
|
||||||
return !!dataKey;
|
return !!dataKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,15 +54,8 @@ function touchProtectedSession() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkProtectedSessionExpiration() {
|
export function getLastProtectedSessionOperationDate() {
|
||||||
const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout");
|
return lastProtectedSessionOperationDate;
|
||||||
if (isProtectedSessionAvailable() && lastProtectedSessionOperationDate && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) {
|
|
||||||
resetDataKey();
|
|
||||||
|
|
||||||
log.info("Expiring protected session");
|
|
||||||
|
|
||||||
ws.reloadFrontend("leaving protected session");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -75,6 +65,5 @@ export default {
|
|||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
decryptString,
|
decryptString,
|
||||||
touchProtectedSession,
|
touchProtectedSession
|
||||||
checkProtectedSessionExpiration
|
|
||||||
};
|
};
|
||||||
|
@ -4,9 +4,11 @@ import sqlInit from "./sql_init.js";
|
|||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
import log from "./log.js";
|
import log from "./log.js";
|
||||||
import attributeService from "../services/attributes.js";
|
import attributeService from "../services/attributes.js";
|
||||||
import protectedSessionService from "../services/protected_session.js";
|
|
||||||
import hiddenSubtreeService from "./hidden_subtree.js";
|
import hiddenSubtreeService from "./hidden_subtree.js";
|
||||||
import type BNote from "../becca/entities/bnote.js";
|
import type BNote from "../becca/entities/bnote.js";
|
||||||
|
import options from "./options.js";
|
||||||
|
import { getLastProtectedSessionOperationDate, isProtectedSessionAvailable, resetDataKey } from "./protected_session.js";
|
||||||
|
import ws from "./ws.js";
|
||||||
|
|
||||||
function getRunAtHours(note: BNote): number[] {
|
function getRunAtHours(note: BNote): number[] {
|
||||||
try {
|
try {
|
||||||
@ -64,5 +66,15 @@ sqlInit.dbReady.then(() => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000);
|
setInterval(() => checkProtectedSessionExpiration(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function checkProtectedSessionExpiration() {
|
||||||
|
const protectedSessionTimeout = options.getOptionInt("protectedSessionTimeout");
|
||||||
|
const lastProtectedSessionOperationDate = getLastProtectedSessionOperationDate();
|
||||||
|
if (isProtectedSessionAvailable() && lastProtectedSessionOperationDate && Date.now() - lastProtectedSessionOperationDate > protectedSessionTimeout * 1000) {
|
||||||
|
resetDataKey();
|
||||||
|
log.info("Expiring protected session");
|
||||||
|
ws.reloadFrontend("leaving protected session");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3
docs/README-ZH_CN.md
vendored
3
docs/README-ZH_CN.md
vendored
@ -211,7 +211,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-ZH_TW.md
vendored
3
docs/README-ZH_TW.md
vendored
@ -202,7 +202,8 @@ pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
17
docs/README-ar.md
vendored
17
docs/README-ar.md
vendored
@ -135,7 +135,7 @@ compatible with the latest zadam/trilium version of
|
|||||||
versions of TriliumNext/Trilium have their sync versions incremented which
|
versions of TriliumNext/Trilium have their sync versions incremented which
|
||||||
prevents direct migration.
|
prevents direct migration.
|
||||||
|
|
||||||
## تحدث معنا
|
## 💬تحدث معنا
|
||||||
|
|
||||||
Feel free to join our official conversations. We would love to hear what
|
Feel free to join our official conversations. We would love to hear what
|
||||||
features, suggestions, or issues you may have!
|
features, suggestions, or issues you may have!
|
||||||
@ -157,7 +157,7 @@ Download the binary release for your platform from the [latest release
|
|||||||
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
|
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
|
||||||
and run the `trilium` executable.
|
and run the `trilium` executable.
|
||||||
|
|
||||||
### Linux
|
### لينكس
|
||||||
|
|
||||||
If your distribution is listed in the table below, use your distribution's
|
If your distribution is listed in the table below, use your distribution's
|
||||||
package.
|
package.
|
||||||
@ -179,7 +179,7 @@ interface (which is almost identical to the desktop app).
|
|||||||
Currently only the latest versions of Chrome & Firefox are supported (and
|
Currently only the latest versions of Chrome & Firefox are supported (and
|
||||||
tested).
|
tested).
|
||||||
|
|
||||||
### Mobile
|
### هاتف المحمول
|
||||||
|
|
||||||
To use TriliumNext on a mobile device, you can use a mobile web browser to
|
To use TriliumNext on a mobile device, you can use a mobile web browser to
|
||||||
access the mobile interface of a server installation (see below).
|
access the mobile interface of a server installation (see below).
|
||||||
@ -194,7 +194,7 @@ repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
|
|||||||
disable automatic updates on your server installation (see below) when using
|
disable automatic updates on your server installation (see below) when using
|
||||||
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
|
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
|
||||||
|
|
||||||
### Server
|
### الخادم
|
||||||
|
|
||||||
To install TriliumNext on your own server (including via Docker from
|
To install TriliumNext on your own server (including via Docker from
|
||||||
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
|
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
|
||||||
@ -203,7 +203,7 @@ installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
|
|||||||
|
|
||||||
## 💻 Contribute
|
## 💻 Contribute
|
||||||
|
|
||||||
### Translations
|
### ترجمات
|
||||||
|
|
||||||
If you are a native speaker, help us translate Trilium by heading over to our
|
If you are a native speaker, help us translate Trilium by heading over to our
|
||||||
[Weblate page](https://hosted.weblate.org/engage/trilium/).
|
[Weblate page](https://hosted.weblate.org/engage/trilium/).
|
||||||
@ -213,7 +213,7 @@ Here's the language coverage we have so far:
|
|||||||
[](https://hosted.weblate.org/engage/trilium/)
|
status](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
### Code
|
### كود
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the
|
Download the repository, install dependencies using `pnpm` and then run the
|
||||||
server (available at http://localhost:8080):
|
server (available at http://localhost:8080):
|
||||||
@ -224,7 +224,7 @@ pnpm install
|
|||||||
pnpm run server:start
|
pnpm run server:start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Documentation
|
### التوثيق
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the
|
Download the repository, install dependencies using `pnpm` and then run the
|
||||||
environment required to edit the documentation:
|
environment required to edit the documentation:
|
||||||
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-ca.md
vendored
3
docs/README-ca.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-cs.md
vendored
3
docs/README-cs.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-de.md
vendored
3
docs/README-de.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-el.md
vendored
3
docs/README-el.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-es.md
vendored
3
docs/README-es.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-fa.md
vendored
3
docs/README-fa.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-fi.md
vendored
3
docs/README-fi.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-fr.md
vendored
3
docs/README-fr.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-hr.md
vendored
3
docs/README-hr.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-hu.md
vendored
3
docs/README-hu.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
315
docs/README-id.md
vendored
Normal file
315
docs/README-id.md
vendored
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
# Trilium Notes
|
||||||
|
|
||||||
|

|
||||||
|
\
|
||||||
|

|
||||||
|
\
|
||||||
|
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
|
||||||
|
[](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
|
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) |
|
||||||
|
[Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md)
|
||||||
|
| [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) |
|
||||||
|
[Spanish](./docs/README-es.md)
|
||||||
|
|
||||||
|
Trilium Notes adalah aplikasi pencatatan hierarkis lintas platform yang gratis
|
||||||
|
dan sumber terbuka dengan fokus untuk mengembangkan pengetahuan pribadi yang
|
||||||
|
luas.
|
||||||
|
|
||||||
|
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for
|
||||||
|
quick overview:
|
||||||
|
|
||||||
|
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
|
||||||
|
|
||||||
|
## 📚 Dokumentasi
|
||||||
|
|
||||||
|
**Kunjungi dokumentasi lengkap kami di
|
||||||
|
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
||||||
|
|
||||||
|
Dokumentasi kami tersedia dalam berbagai format:
|
||||||
|
- **Dokumentasi Online**: Telusuri dokumentasi lengkap di
|
||||||
|
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
|
||||||
|
- **Bantuan Dalam Aplikasi**: Tekan `F1` di dalam Trilium untuk mengakses
|
||||||
|
dokumentasi yang sama langsung di aplikasi
|
||||||
|
- **GitHub**: Navigasi melalui [Panduan
|
||||||
|
Pengguna](./docs/User%20Guide/User%20Guide/) di repositori ini
|
||||||
|
|
||||||
|
### Tautan Cepat
|
||||||
|
- [Panduan Memulai](https://docs.triliumnotes.org/)
|
||||||
|
- [Petunjuk
|
||||||
|
Instalasi](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
||||||
|
- [Pengaturan
|
||||||
|
Docker](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
|
||||||
|
- [Upgrading
|
||||||
|
TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
|
||||||
|
- [Basic Concepts and
|
||||||
|
Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
|
||||||
|
- [Patterns of Personal Knowledge
|
||||||
|
Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
|
||||||
|
|
||||||
|
## 🎁 Features
|
||||||
|
|
||||||
|
* Notes can be arranged into arbitrarily deep tree. Single note can be placed
|
||||||
|
into multiple places in the tree (see
|
||||||
|
[cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
|
||||||
|
* Rich WYSIWYG note editor including e.g. tables, images and
|
||||||
|
[math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown
|
||||||
|
[autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
|
||||||
|
* Support for editing [notes with source
|
||||||
|
code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax
|
||||||
|
highlighting
|
||||||
|
* Fast and easy [navigation between
|
||||||
|
notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text
|
||||||
|
search and [note
|
||||||
|
hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
|
||||||
|
* Seamless [note
|
||||||
|
versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
|
||||||
|
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be
|
||||||
|
used for note organization, querying and advanced
|
||||||
|
[scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
|
||||||
|
* UI available in English, German, Spanish, French, Romanian, and Chinese
|
||||||
|
(simplified and traditional)
|
||||||
|
* Direct [OpenID and TOTP
|
||||||
|
integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md)
|
||||||
|
for more secure login
|
||||||
|
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization)
|
||||||
|
with self-hosted sync server
|
||||||
|
* there's a [3rd party service for hosting synchronisation
|
||||||
|
server](https://trilium.cc/paid-hosting)
|
||||||
|
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes
|
||||||
|
to public internet
|
||||||
|
* Strong [note
|
||||||
|
encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with
|
||||||
|
per-note granularity
|
||||||
|
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type
|
||||||
|
"canvas")
|
||||||
|
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and
|
||||||
|
[link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing
|
||||||
|
notes and their relations
|
||||||
|
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
||||||
|
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with
|
||||||
|
location pins and GPX tracks
|
||||||
|
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced
|
||||||
|
showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
|
||||||
|
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
|
||||||
|
* Scales well in both usability and performance upwards of 100 000 notes
|
||||||
|
* Touch optimized [mobile
|
||||||
|
frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for
|
||||||
|
smartphones and tablets
|
||||||
|
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support
|
||||||
|
for user themes
|
||||||
|
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and
|
||||||
|
[Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
|
||||||
|
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy
|
||||||
|
saving of web content
|
||||||
|
* Customizable UI (sidebar buttons, user-defined widgets, ...)
|
||||||
|
* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along
|
||||||
|
with a [Grafana
|
||||||
|
Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json)
|
||||||
|
|
||||||
|
✨ Check out the following third-party resources/communities for more TriliumNext
|
||||||
|
related goodies:
|
||||||
|
|
||||||
|
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party
|
||||||
|
themes, scripts, plugins and more.
|
||||||
|
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
|
||||||
|
|
||||||
|
## ❓Why TriliumNext?
|
||||||
|
|
||||||
|
The original Trilium developer ([Zadam](https://github.com/zadam)) has
|
||||||
|
graciously given the Trilium repository to the community project which resides
|
||||||
|
at https://github.com/TriliumNext
|
||||||
|
|
||||||
|
### ⬆️Migrating from Zadam/Trilium?
|
||||||
|
|
||||||
|
There are no special migration steps to migrate from a zadam/Trilium instance to
|
||||||
|
a TriliumNext/Trilium instance. Simply [install
|
||||||
|
TriliumNext/Trilium](#-installation) as usual and it will use your existing
|
||||||
|
database.
|
||||||
|
|
||||||
|
Versions up to and including
|
||||||
|
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
|
||||||
|
compatible with the latest zadam/trilium version of
|
||||||
|
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later
|
||||||
|
versions of TriliumNext/Trilium have their sync versions incremented which
|
||||||
|
prevents direct migration.
|
||||||
|
|
||||||
|
## 💬 Discuss with us
|
||||||
|
|
||||||
|
Feel free to join our official conversations. We would love to hear what
|
||||||
|
features, suggestions, or issues you may have!
|
||||||
|
|
||||||
|
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous
|
||||||
|
discussions.)
|
||||||
|
- The `General` Matrix room is also bridged to
|
||||||
|
[XMPP](xmpp:discuss@trilium.thisgreat.party?join)
|
||||||
|
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For
|
||||||
|
asynchronous discussions.)
|
||||||
|
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug
|
||||||
|
reports and feature requests.)
|
||||||
|
|
||||||
|
## 🏗 Installation
|
||||||
|
|
||||||
|
### Windows / MacOS
|
||||||
|
|
||||||
|
Download the binary release for your platform from the [latest release
|
||||||
|
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
|
||||||
|
and run the `trilium` executable.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
If your distribution is listed in the table below, use your distribution's
|
||||||
|
package.
|
||||||
|
|
||||||
|
[](https://repology.org/project/triliumnext/versions)
|
||||||
|
|
||||||
|
You may also download the binary release for your platform from the [latest
|
||||||
|
release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the
|
||||||
|
package and run the `trilium` executable.
|
||||||
|
|
||||||
|
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
|
||||||
|
|
||||||
|
### Browser (any OS)
|
||||||
|
|
||||||
|
If you use a server installation (see below), you can directly access the web
|
||||||
|
interface (which is almost identical to the desktop app).
|
||||||
|
|
||||||
|
Currently only the latest versions of Chrome & Firefox are supported (and
|
||||||
|
tested).
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
|
||||||
|
To use TriliumNext on a mobile device, you can use a mobile web browser to
|
||||||
|
access the mobile interface of a server installation (see below).
|
||||||
|
|
||||||
|
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more
|
||||||
|
information on mobile app support.
|
||||||
|
|
||||||
|
If you prefer a native Android app, you can use
|
||||||
|
[TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
|
||||||
|
Report bugs and missing features at [their
|
||||||
|
repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
|
||||||
|
disable automatic updates on your server installation (see below) when using
|
||||||
|
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
To install TriliumNext on your own server (including via Docker from
|
||||||
|
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
|
||||||
|
installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
|
||||||
|
|
||||||
|
|
||||||
|
## 💻 Contribute
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
If you are a native speaker, help us translate Trilium by heading over to our
|
||||||
|
[Weblate page](https://hosted.weblate.org/engage/trilium/).
|
||||||
|
|
||||||
|
Here's the language coverage we have so far:
|
||||||
|
|
||||||
|
[](https://hosted.weblate.org/engage/trilium/)
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
Download the repository, install dependencies using `pnpm` and then run the
|
||||||
|
server (available at http://localhost:8080):
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
|
cd Trilium
|
||||||
|
pnpm install
|
||||||
|
pnpm run server:start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Download the repository, install dependencies using `pnpm` and then run the
|
||||||
|
environment required to edit the documentation:
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
|
cd Trilium
|
||||||
|
pnpm install
|
||||||
|
pnpm edit-docs:edit-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building the Executable
|
||||||
|
Download the repository, install dependencies using `pnpm` and then build the
|
||||||
|
desktop app for Windows:
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
|
cd Trilium
|
||||||
|
pnpm install
|
||||||
|
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details, see the [development
|
||||||
|
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
|
||||||
|
|
||||||
|
### Developer Documentation
|
||||||
|
|
||||||
|
Please view the [documentation
|
||||||
|
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
|
||||||
|
for details. If you have more questions, feel free to reach out via the links
|
||||||
|
described in the "Discuss with us" section above.
|
||||||
|
|
||||||
|
## 👏 Shoutouts
|
||||||
|
|
||||||
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
|
of the application.
|
||||||
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
widget.
|
||||||
|
* [Dosu](https://dosu.dev/) for providing us with the automated responses to
|
||||||
|
GitHub issues and discussions.
|
||||||
|
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
|
||||||
|
|
||||||
|
Trilium would not be possible without the technologies behind it:
|
||||||
|
|
||||||
|
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind
|
||||||
|
text notes. We are grateful for being offered a set of the premium features.
|
||||||
|
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with
|
||||||
|
support for huge amount of languages.
|
||||||
|
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
|
||||||
|
whiteboard used in Canvas notes.
|
||||||
|
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
|
||||||
|
mind map functionality.
|
||||||
|
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
|
||||||
|
maps.
|
||||||
|
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
|
||||||
|
table used in collections.
|
||||||
|
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
|
||||||
|
without real competition.
|
||||||
|
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
|
||||||
|
Used in [relation
|
||||||
|
maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link
|
||||||
|
maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
|
||||||
|
|
||||||
|
## 🤝 Support
|
||||||
|
|
||||||
|
Trilium is built and maintained with [hundreds of hours of
|
||||||
|
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your
|
||||||
|
support keeps it open-source, improves features, and covers costs such as
|
||||||
|
hosting.
|
||||||
|
|
||||||
|
Consider supporting the main developer
|
||||||
|
([eliandoran](https://github.com/eliandoran)) of the application via:
|
||||||
|
|
||||||
|
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
|
||||||
|
- [PayPal](https://paypal.me/eliandoran)
|
||||||
|
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
|
||||||
|
|
||||||
|
## 🔑 License
|
||||||
|
|
||||||
|
Copyright 2017-2025 zadam, Elian Doran, and other contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
later version.
|
3
docs/README-it.md
vendored
3
docs/README-it.md
vendored
@ -260,7 +260,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
2
docs/README-ja.md
vendored
2
docs/README-ja.md
vendored
@ -215,7 +215,7 @@ pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
|
|||||||
## 👏 シャウトアウト
|
## 👏 シャウトアウト
|
||||||
|
|
||||||
* [zadam](https://github.com/zadam) アプリケーションのオリジナルのコンセプトと実装に対して感謝します。
|
* [zadam](https://github.com/zadam) アプリケーションのオリジナルのコンセプトと実装に対して感謝します。
|
||||||
* [Larsa](https://github.com/LarsaSara) アプリケーションアイコンをデザイン。
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) アプリケーションアイコンをデザイン。
|
||||||
* [nriver](https://github.com/nriver) 国際化への取り組み。
|
* [nriver](https://github.com/nriver) 国際化への取り組み。
|
||||||
* [Thomas Frei](https://github.com/thfrei) Canvasへのオリジナルな取り組み。
|
* [Thomas Frei](https://github.com/thfrei) Canvasへのオリジナルな取り組み。
|
||||||
* [antoniotejada](https://github.com/nriver) オリジナルの構文ハイライトウィジェット。
|
* [antoniotejada](https://github.com/nriver) オリジナルの構文ハイライトウィジェット。
|
||||||
|
3
docs/README-ko.md
vendored
3
docs/README-ko.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-md.md
vendored
3
docs/README-md.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-nb_NO.md
vendored
3
docs/README-nb_NO.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-nl.md
vendored
3
docs/README-nl.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-pl.md
vendored
3
docs/README-pl.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-pt.md
vendored
3
docs/README-pt.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-pt_BR.md
vendored
3
docs/README-pt_BR.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-ro.md
vendored
3
docs/README-ro.md
vendored
@ -263,7 +263,8 @@ legăturile descrise în secțiunea „Discutați cu noi” de mai sus.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) pentru conceptul și implementarea originală
|
* [zadam](https://github.com/zadam) pentru conceptul și implementarea originală
|
||||||
a aplicației.
|
a aplicației.
|
||||||
* [Larsa](https://github.com/LarsaSara) pentru pictograma aplicației.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) pentru sistemul de internaționalizare.
|
* [nriver](https://github.com/nriver) pentru sistemul de internaționalizare.
|
||||||
* [Thomas Frei](https://github.com/thfrei) pentru munca sa originală pentru
|
* [Thomas Frei](https://github.com/thfrei) pentru munca sa originală pentru
|
||||||
notițele de tip schiță.
|
notițele de tip schiță.
|
||||||
|
5
docs/README-ru.md
vendored
5
docs/README-ru.md
vendored
@ -261,7 +261,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
@ -307,7 +308,7 @@ Consider supporting the main developer
|
|||||||
|
|
||||||
## 🔑 Лицензия
|
## 🔑 Лицензия
|
||||||
|
|
||||||
Copyright 2017-2025 zadam, Elian Doran, and other contributors
|
Copyright 2017-2025 zadam, Elian Doran и другие авторы
|
||||||
|
|
||||||
Эта программа является бесплатным программным обеспечением: вы можете
|
Эта программа является бесплатным программным обеспечением: вы можете
|
||||||
распространять и/или изменять ее в соответствии с условиями GNU Affero General
|
распространять и/или изменять ее в соответствии с условиями GNU Affero General
|
||||||
|
3
docs/README-sl.md
vendored
3
docs/README-sl.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-sr.md
vendored
3
docs/README-sr.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-tr.md
vendored
3
docs/README-tr.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-uk.md
vendored
3
docs/README-uk.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
3
docs/README-vi.md
vendored
3
docs/README-vi.md
vendored
@ -259,7 +259,8 @@ described in the "Discuss with us" section above.
|
|||||||
|
|
||||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||||
of the application.
|
of the application.
|
||||||
* [Larsa](https://github.com/LarsaSara) for designing the application icon.
|
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||||
|
application icon.
|
||||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||||
|
@ -50,12 +50,12 @@
|
|||||||
"eslint": "9.37.0",
|
"eslint": "9.37.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-playwright": "2.2.2",
|
"eslint-plugin-playwright": "2.2.2",
|
||||||
"eslint-plugin-react-hooks": "6.1.1",
|
"eslint-plugin-react-hooks": "7.0.0",
|
||||||
"happy-dom": "~19.0.0",
|
"happy-dom": "~19.0.0",
|
||||||
"jiti": "2.6.1",
|
"jiti": "2.6.1",
|
||||||
"jsonc-eslint-parser": "2.4.1",
|
"jsonc-eslint-parser": "2.4.1",
|
||||||
"react-refresh": "0.18.0",
|
"react-refresh": "0.18.0",
|
||||||
"rollup-plugin-webpack-stats": "2.1.5",
|
"rollup-plugin-webpack-stats": "2.1.6",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.20.6",
|
||||||
"typescript": "~5.9.0",
|
"typescript": "~5.9.0",
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
"ckeditor5-premium-features": "47.0.0"
|
"ckeditor5-premium-features": "47.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@smithy/middleware-retry": "4.4.0",
|
"@smithy/middleware-retry": "4.4.1",
|
||||||
"@types/jquery": "3.5.33"
|
"@types/jquery": "3.5.33"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"@codemirror/lang-xml": "6.1.0",
|
"@codemirror/lang-xml": "6.1.0",
|
||||||
"@codemirror/legacy-modes": "6.5.2",
|
"@codemirror/legacy-modes": "6.5.2",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.11",
|
||||||
"@codemirror/view": "6.38.4",
|
"@codemirror/view": "6.38.5",
|
||||||
"@fsegurai/codemirror-theme-abcdef": "6.2.2",
|
"@fsegurai/codemirror-theme-abcdef": "6.2.2",
|
||||||
"@fsegurai/codemirror-theme-abyss": "6.2.2",
|
"@fsegurai/codemirror-theme-abyss": "6.2.2",
|
||||||
"@fsegurai/codemirror-theme-android-studio": "6.2.2",
|
"@fsegurai/codemirror-theme-android-studio": "6.2.2",
|
||||||
|
@ -54,3 +54,11 @@ export function trimIndentation(strings: TemplateStringsArray, ...values: any[])
|
|||||||
}
|
}
|
||||||
return output.join("\n");
|
return output.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flushPromises() {
|
||||||
|
return new Promise(setImmediate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleepFor(duration: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, duration));
|
||||||
|
}
|
||||||
|
664
pnpm-lock.yaml
generated
664
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user