### Install Dependencies on macOS Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Use Homebrew to install Lua and required libraries, then verify the installation. ```bash brew install lua luarocks luarocks install luasocket luasec dkjson # Verify lua -e "require('socket'); require('ssl'); require('dkjson'); print('OK')" ``` -------------------------------- ### Install Dependencies on Linux Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Install Lua 5.3 and required modules using apt and luarocks. ```bash sudo apt install lua5.3 liblua5.3-dev luarocks sudo luarocks install luasocket sudo luarocks install luasec OPENSSL_DIR=/usr sudo luarocks install dkjson ``` -------------------------------- ### Quick Start Testing Commands Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Initial commands to navigate to the plugin directory and execute various test suites. ```bash # Navigate to your KOAssistant plugin directory (see paths above) cd ~/Library/Application\ Support/koreader/plugins/koassistant.koplugin # macOS cd ~/.config/koreader/plugins/koassistant.koplugin # Linux # Run unit tests (fast, no API calls) lua tests/run_tests.lua --unit # Run provider connectivity tests lua tests/run_tests.lua # Validate all models (detects constraints, ~1 token per model) lua tests/run_tests.lua --models # Test reasoning/thinking across providers lua tests/run_tests.lua --reasoning # Inspect request structure lua tests/inspect.lua anthropic # Start web UI for interactive testing lua tests/inspect.lua --web ``` -------------------------------- ### Test Workflow with Backup Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Example workflow for transferring settings from an e-reader to a test environment and running tests. ```bash # On your e-reader: Create backup via Settings UI # Copy backup to test machine scp /mnt/onboard/.adds/koreader/koassistant_backups/koassistant_backup_*.koa \ ~/test-env/koassistant_backups/ # On test machine: Restore via Settings UI or run tests cd /path/to/koassistant.koplugin lua tests/run_tests.lua lua tests/inspect.lua --web # Will use your restored API keys ``` -------------------------------- ### Model Validation Output Example Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Example console output showing the results of the --models test mode, including detected model constraints. ```text [openai] Testing 15 models... Pre-check: 1 models not in API list ⚠ o3-pro gpt-5.2 ⚠ CONSTRAINT: max_tokens (default rejected, max_tokens=16 works) gpt-5-mini ⚠ CONSTRAINT: multiple constraints (temp=1.0 + max_tokens=16 works) gpt-4.1 ✓ OK (789ms) Detected Constraints: openai/gpt-5.2: requires max_tokens >= 16 openai/gpt-5-mini: requires temp=1.0 + max_tokens >= 16 ``` -------------------------------- ### Install required Lua dependencies Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Install necessary LuaRocks packages to resolve module not found errors. ```bash luarocks install luasocket luarocks install luasec # macOS sudo luarocks install luasec OPENSSL_DIR=/usr # Linux luarocks install dkjson ``` -------------------------------- ### Initialize Local Configuration Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Use this command to create a local configuration file from the provided sample template. ```bash cp tests/local_config.lua.sample tests/local_config.lua ``` -------------------------------- ### Configure File-based API Keys Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Prepare the local configuration file for API keys during development. ```bash # Navigate to plugin directory (see paths in "KOReader Plugin Paths" above) cp apikeys.lua.sample apikeys.lua # Edit apikeys.lua and add your API keys ``` -------------------------------- ### Run Tests by Platform Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Execute the test suite from the plugin directory on macOS or Linux. ```bash # macOS example: cd ~/Library/Application\ Support/koreader/plugins/koassistant.koplugin lua tests/run_tests.lua # Linux example: cd ~/.config/koreader/plugins/koassistant.koplugin lua tests/run_tests.lua ``` -------------------------------- ### Sample Data and Debug Views Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Functions to fetch and display sample context data or toggle debug visibility. ```javascript async function viewSampleData() { const sampleView = document.getElementById('sample-data-view'); if (sampleView.style.display === 'none') { // Fetch and show sample data try { const response = await fetch('/api/sample-context'); const data = await response.json(); if (data.success && data.sample) { const s = data.sample; let html = `Sample Book Context (used for X-Ray, Recap, Analyze My Notes)

`; html += `Title: ${s.title}
`; html += `Author: ${s.author}
`; html += `Progress: ${s.reading_progress}
`; html += `Chapter: ${s.chapter_title}
`; html += `Last read: ${s.time_since_last_read}

`; html += `Highlights: ${(s.highlights || '').split('\n').length} entries
`; html += `Annotations: ${(s.annotations || '').split('\n\n').length} entries
`; html += `Book text: ${(s.book_text || '').length} chars

`; html += `This data auto-merges when using actions with context extraction flags (use_book_text, use_highlights, etc.)`; sampleView.innerHTML = html; } else { sampleView.innerHTML = 'Error: ' + (data.error || 'Unknown error'); } } catch (e) { sampleView.innerHTML = 'Error loading sample data: ' + e.message; } sampleView.style.display = 'block'; } else { sampleView.style.display = 'none'; } } ``` ```javascript function toggleChatDebug() { const content = document.getElementById('chatDebugContent'); const checkbox = document.getElementById('showDebug'); content.classList.toggle('visible', checkbox.checked); } ``` ```javascript function renderChatView() { const buildResult = state.lastBuild; const responseResult = state.lastResponse; // Only render if we have a response (not just a build) if (!responseResult || !responseResult.response) { // Show placeholder ``` -------------------------------- ### Copying behavior files Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/behaviors.sample/README.md Command to copy a specific behavior file from the sample directory to the active behaviors directory. ```bash cp behaviors.sample/claude_style_standard.md behaviors/ ``` -------------------------------- ### Clone the KOAssistant repository Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Use this command to download the source code, including the test suite, which is excluded from release zips. ```bash git clone https://github.com/zzzsm/koassistant.git ``` -------------------------------- ### Load Configuration Presets Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Defines and applies predefined configuration objects for different plugin behaviors. ```javascript function loadPreset(preset) { const presets = { minimal: { behavior: 'minimal', temperature: 0.7, domain: '', domainId: '', languages: '', thinking: { enabled: false, openai_reasoning_enabled: false, gemini_thinking_enabled: false } }, full: { behavior: 'full', temperature: 0.7, domain: '', domainId: '', languages: '', thinking: { enabled: false, openai_reasoning_enabled: false, gemini_thinking_enabled: false } }, domain: { behavior: 'full', temperature: 0.7, domain: '', domainId: 'first', languages: '', thinking: { enabled: false, openai_reasoning_enabled: false, gemini_thinking_enabled: false } }, thinking: { behavior: 'full', temperature: 1.0, domain: '', domainId: '', languages: '', thinking: { enabled: true, budget: 4096, openai_reasoning_enabled: false, gemini_thinking_enabled: false } }, multilingual: { behavior: 'full', temperature: 0.7, domain: '', domainId: '', languages: 'English, Spanish, Arabic', thinking: { enabled: false, openai_reasoning_enabled: false, gemini_thinking_enabled: false } } ``` -------------------------------- ### Execute Unit Tests via CLI Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Run the full unit test suite or execute a specific test file directly from the repository root. ```bash lua tests/run_tests.lua --unit ``` ```bash lua tests/unit/test_xray_parser.lua ``` -------------------------------- ### Constraint Utilities Usage Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Demonstrates how to use the ConstraintUtils library to retrieve model parameters, reasoning defaults, and parse API error constraints. ```lua local ConstraintUtils = require("tests.lib.constraint_utils") -- Get max temperature for provider (1.0 for Anthropic, 2.0 for others) local max_temp = ConstraintUtils.getMaxTemperature("anthropic") -- Returns 1.0 -- Get default temperature from plugin's Defaults module local default_temp = ConstraintUtils.getDefaultTemperature("openai") -- Returns 0.7 -- Get reasoning defaults (extended thinking budgets, effort levels) local anthropic_reasoning = ConstraintUtils.getReasoningDefaults("anthropic") -- Returns: { budget = 32000, budget_min = 1024, budget_max = 32000, ... } local openai_reasoning = ConstraintUtils.getReasoningDefaults("openai") -- Returns: { effort = "medium", effort_options = { "low", "medium", "high" } } -- Check if model supports capability local supports = ConstraintUtils.supportsCapability("anthropic", "claude-sonnet-4-5", "extended_thinking") -- Returns: true -- Parse constraint errors from API responses local constraint = ConstraintUtils.parseConstraintError("Error: temperature must be 1.0") -- Returns: { type = "temperature", value = 1.0, reason = "..." } -- Build retry config with corrected parameters local new_config = ConstraintUtils.buildRetryConfig(original_config, constraint) ``` -------------------------------- ### Retrieve Plugin Configuration Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Constructs a configuration object by gathering inputs from the UI, including provider settings, behavior overrides, and context-specific data. ```javascript function getConfig() { const behaviorSelect = document.getElementById('behavior-select'); const behaviorId = behaviorSelect.value; const contextType = document.getElementById('context-type').value; const actionId = document.getElementById('action-select').value; // Get action prompt and additional input const actionPrompt = document.getElementById('action-prompt').value; const additionalInput = document.getElementById('additional-input').value; // Build context object for the request const context = { type: contextType, highlighted_text: document.getElementById('highlighted-text').value, book_title: document.getElementById('book-title').value, book_author: document.getElementById('book-author').value, }; // Parse books_info for library context if (contextType === 'library') { const booksList = document.getElementById('books-list').value; const books = booksList.split('\n').filter(l => l.trim()).map(line => { // Parse "Title" by Author format const match = line.match(/"([^"]+)"\s*by\s*(.+)/i); if (match) { return { title: match[1], authors: match[2].trim() }; } return { title: line.trim(), authors: '' }; }); context.books_info = books; } // Get the selected action object if any, with all flag overrides from UI let action = null; if (actionId) { const originalAction = findAction(actionId); if (originalAction) { // Clone action and override all flags from UI action = { ...originalAction }; // Apply overrides from Action Overrides section (always visible when action selected) const section = document.getElementById('action-overrides-section'); if (section.style.display !== 'none') { // Prompt flags action.skip_language_instruction = document.getElementById('flag-skip-language').checked; action.skip_domain = document.getElementById('flag-skip-domain').checked; action.include_book_context = document.getElementById('flag-include-book-context').checked; // Context extraction flags action.use_book_text = document.getElementById('flag-use-book-text').checked; action.use_highlights = document.getElementById('flag-use-highlights').checked; action.use_annotations = document.getElementById('flag-use-annotations').checked; action.use_reading_progress = document.getElementById('flag-use-reading-progress').checked; action.use_reading_stats = document.getElementById('flag-use-reading-stats').checked; action.use_notebook = document.getElementById('flag-use-notebook').checked; } } } // Determine translation language const usePrimary = document.getElementById('translation-use-primary').checked; const userLangs = document.getElementById('user-languages').value; const primaryLang = document.getElementById('primary-language').value || userLangs.split(',')[0]?.trim() || 'English'; let translationLang; if (usePrimary) { translationLang = primaryLang; } else { const translationSelect = document.getElementById('translation-language').value; if (translationSelect === 'custom') { translationLang = document.getElementById('translation-language-custom').value || 'English'; } else { translationLang = translationSelect; } } // Handle behavior - for "custom", send the textarea content as override let customBehavior = null; if (behaviorId === 'custom') { customBehavior = document.getElementById('custom-behavior').value; } return { provider: document.getElementById('provider').value, model: document.getElementById('model').value, behavior: behaviorId === 'custom' ? 'full' : (behaviorId || 'none'), // Base variant custom_behavior: customBehavior, // Override text (only for custom mode) temperature: parseFloat(document.getElementById('temperature').value), max_tokens: parseInt(document.getElementById('max-tokens').value) || 4096, domain: document.getElementById('domain').value, languages: document.getElementById('user-languages').value, primary_language: document.getElementById('primary-language').value || null, translation_language: translationLang, thinking: { // Anthropic extended thinking enabled: document.getElementById('thinking-enabled').checked, budget: ``` -------------------------------- ### Configure LuaRocks paths and run tests Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Export LuaRocks paths to ensure modules are discoverable, then execute the test suite. ```bash eval "$(luarocks --lua-version 5.5 path)" # match your `lua -v`; also 5.4/5.3 lua tests/run_tests.lua --models ``` -------------------------------- ### Manage Provider and Settings Logic Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Functions for fetching and applying plugin settings, including language preferences, behavior selection, and extended thinking configurations. ```javascript customContainer.style.display = 'block'; customTextarea.value = behavior.text || ''; customTextarea.readOnly = true; customTextarea.placeholder = ''; } } else { // None selected customContainer.style.display = 'none'; customTextarea.value = ''; } } async function loadActions() { try { const response = await fetch('/api/actions'); const data = await response.json(); if (data.success && data.actions) { state.actions = data.actions; updateActionDropdown(); } } catch (e) { console.error('Error loading actions:', e); } } async function loadSettings() { try { const response = await fetch('/api/settings'); const data = await response.json(); if (data.success && data.settings) { const s = data.settings; // Apply loaded settings to form if (s.user\_languages) { document.getElementById('user-languages').value = s.user\_languages; onUserLanguagesChange(); // Update primary language dropdown } if (s.primary\_language) { document.getElementById('primary-language').value = s.primary\_language; } document.getElementById('translation-use-primary').checked = s.translation\_use\_primary !== false; // Set translation language - check if value exists in dropdown options const translationSelect = document.getElementById('translation-language'); const translationValue = s.translation\_language || 'English'; const hasOption = Array.from(translationSelect.options).some(opt => opt.value === translationValue); if (hasOption) { translationSelect.value = translationValue; } else { translationSelect.value = 'custom'; document.getElementById('translation-language-custom').value = translationValue; document.getElementById('translation-language-custom').style.display = 'block'; } document.getElementById('translation-language-container').style.display = s.translation\_use\_primary !== false ? 'none' : 'block'; // Behavior - use selected\_behavior (new) or ai\_behavior\_variant (legacy) const behaviorId = s.selected\_behavior || s.ai\_behavior\_variant || 'full'; const behaviorSelect = document.getElementById('behavior-select'); if (behaviorSelect.querySelector(`option[value="${behaviorId}"]`)) { behaviorSelect.value = behaviorId; onBehaviorChange(); } // Temperature if (s.default\_temperature !== undefined) { document.getElementById('temperature').value = s.default\_temperature; updateTempLabel(); } // Extended thinking document.getElementById('thinking-enabled').checked = s.enable\_extended\_thinking || false; if (s.thinking\_budget\_tokens) { document.getElementById('thinking-budget').value = s.thinking\_budget\_tokens; } } } catch (e) { console.error('Error loading settings:', e); } } function onUserLanguagesChange() { const languages = document.getElementById('user-languages').value; const primarySelect = document.getElementById('primary-language'); const translationSelect = document.getElementById('translation-language'); // Parse languages and populate primary language dropdown const langList = languages.split(',').map(l => l.trim()).filter(l => l); primarySelect.innerHTML = '' + langList.map(l => ``).join(''); // Also populate translation language dropdown (languages + custom option) if (langList.length > 0) { translationSelect.innerHTML = langList.map(l => ``).join('') + ''; } else { translationSelect.innerHTML = '' + ''; } } function onTranslationLanguageChange() { const translationSelect = document.getElementById('translation-language'); const customInput = document.getElementById('translation-language-custom'); customInput.style.display = translationSelect.value === 'custom' ? 'block' : 'none'; } function onPrimaryLanguageChange() { // Could be used for any side effects when primary language changes } function updateActionDropdown() { const contextType = document.getElementById('context-type').value; const actionSelect = document.getElementById('action-select'); // Get actions for current context let actions = [...(state.actions[contextType] || [])]; // Include "all" context actions in every dropdown // Include "both" context actions in highlight and book // These may already be in the list from server, but we dedupe below const allContexts = ['highlight', 'book', 'library', 'general']; for (const ctx of allContexts) { if (ctx !== contextType && state.actions[ctx]) { for (const action of state.actions[ctx]) { if (action.context === 'all' || (action.context === 'both' && (contextType === 'highlight' || contextType === 'book'))) { // Add if not already present if (!actions.find(a => a.id === action.id)) { actions.push(action); } } } } } // "None" = raw message with no action processing (edge case testing) // Ask is the default (like plugin behavior when no specific action is chosen) actionSelect.innerHTML = '`) .join(''); // Default to anthropic if available if (state.providers.anthropic) { providerSelect.value = 'anthropic'; } onProviderChange(); // Build compare checkboxes (all unchecked by default) const checksContainer = document.getElementById('compare-checks'); checksContainer.innerHTML = providerIds .map(p => ``) .join(''); } catch (e) { console.error('Error loading providers:', e); showStatus('error', 'Failed to load providers: ' + e.message); } } function onProviderChange() { const provider = document.getElementById('provider').value; const models = state.providers[provider] || []; document.getElementById('model').innerHTML = models.map(m => ``).join(''); // Show/hide reasoning sections based on provider const anthropicSection = document.getElementById('anthropic-thinking-section'); const openaiSection = document.getElementById('openai-reasoning-section'); const geminiSection = document.getElementById('gemini-thinking-section'); // Hide all reasoning sections first anthropicSection.style.display = 'none'; openaiSection.style.display = 'none'; geminiSection.style.display = 'none'; // Show relevant section if (provider === 'anthropic') { anthropicSection.style.display = 'block'; } else if (provider === 'openai') { openaiSection.style.display = 'block'; } else if (provider === 'gemini') { geminiSection.style.display = 'block'; } } async function loadDomains() { try { const response = await fetch('/api/domains'); const data = await response.json(); if (data.success && Array.isArray(data.domains)) { state.domains = data.domains; const domainSelect = document.getElementById('domain-select'); domainSelect.innerHTML = '' + state.domains.map(d => ``).join(''); } } catch (e) { console.error('Error loading domains:', e); } } async function loadBehaviors() { try { const response = await fetch('/api/behaviors'); const data = await response.json(); if (data.success && Array.isArray(data.behaviors)) { state.behaviors = data.behaviors; const behaviorSelect = document.getElementById('behavior-select'); // Build options: None + all behaviors with source indicators + Custom for testing behaviorSelect.innerHTML = '' + state.behaviors.map(b => { const sourceLabel = b.source === 'folder' ? ' (file)' : ''; return ``; }).join('') + ''; // Default to "full" if available const fullBehavior = state.behaviors.find(b => b.id === 'full'); if (fullBehavior) { behaviorSelect.value = 'full'; onBehaviorChange(); } } } catch (e) { console.error('Error loading behaviors:', e); } } function onBehaviorChange() { const behaviorId = document.getElementById('behavior-select').value; const customContainer = document.getElementById('custom-behavior-container'); const customTextarea = document.getElementById('custom-behavior'); if (behaviorId === 'custom') { // Custom mode - editable textarea for testing behavior overrides customContainer.style.display = 'block'; customTextarea.readOnly = false; customTextarea.placeholder = 'Enter custom AI behavior instructions for testing...'; // Don't clear existing custom text } else if (behaviorId && behaviorId !== 'none') { // Selected behavior - show preview (read-only) const behavior = state.behaviors.find(b => b.id === behaviorId); if (behavior) { ``` -------------------------------- ### Define a Custom Behavior File Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/behaviors.sample/README.md Create a markdown file where the filename serves as the ID and the content provides instructions to the AI. ```markdown # My Custom Style You are a helpful assistant who... [Your instructions here] ``` -------------------------------- ### Manage Web UI port conflicts Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Identify processes using the default port and launch the inspection tool on an alternative port. ```bash lsof -i :8080 # Use different port lua tests/inspect.lua --web --port 3000 ``` -------------------------------- ### Apply Configuration Presets Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Updates UI elements based on a selected preset object. Requires DOM elements like 'behavior-select', 'temperature', and 'domain-select' to be present. ```javascript }; const p = presets[preset]; if (!p) return; // Set behavior using dropdown const behaviorSelect = document.getElementById('behavior-select'); if (behaviorSelect.querySelector(`option[value="${p.behavior}"]`)) { behaviorSelect.value = p.behavior; onBehaviorChange(); } document.getElementById('temperature').value = p.temperature; updateTempLabel(); // Handle domain - prefer real domain if available if (p.domainId === 'first' && state.domains.length > 0) { // Use the first available domain document.getElementById('domain-select').value = state.domains[0].id; onDomainChange(); } else if (p.domainId) { document.getElementById('domain-select').value = p.domainId; onDomainChange(); } else { document.getElementById('domain-select').value = ''; document.getElementById('domain').value = p.domain; } document.getElementById('user-languages').value = p.languages; onUserLanguagesChange(); // Update primary/translation language dropdowns // Set reasoning/thinking options for all providers document.getElementById('thinking-enabled').checked = p.thinking.enabled; if (p.thinking.budget) { document.getElementById('thinking-budget').value = p.thinking.budget; } document.getElementById('openai-reasoning-enabled').checked = p.thinking.openai_reasoning_enabled || false; document.getElementById('gemini-thinking-enabled').checked = p.thinking.gemini_thinking_enabled || false; // If thinking preset and not anthropic, switch to anthropic if (preset === 'thinking' && document.getElementById('provider').value !== 'anthropic') { document.getElementById('provider').value = 'anthropic'; onProviderChange(); } } ``` -------------------------------- ### Web UI Inspector Commands Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Commands to launch the browser-based interactive request inspector. ```bash # Start web server (default port 8080) lua tests/inspect.lua --web # Custom port lua tests/inspect.lua --web --port 3000 # Then open http://localhost:8080 ``` -------------------------------- ### Test Suite Directory Structure Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Overview of the project's test directory layout, including runners, mocks, and specific test categories. ```text tests/ ├── run_tests.lua # Test runner ├── inspect.lua # Request inspector (CLI + Web UI) ├── test_config.lua # Config helpers (buildFullConfig) ├── local_config.lua.sample # Local config template ├── fixtures/ │ └── sample_context.lua # Sample context data for tests ├── lib/ │ ├── mock_koreader.lua # KOReader module mocks │ ├── constraint_utils.lua # Plugin constraint utilities wrapper │ ├── request_inspector.lua # Core inspection logic │ ├── terminal_formatter.lua # ANSI colors, formatting │ └── web_server.lua # LuaSocket HTTP server ├── web/ │ └── index.html # Web UI frontend ├── integration/ │ ├── test_full_provider.lua # Comprehensive tests (--full) │ ├── test_model_validation.lua # Model validation (--models) │ └── test_reasoning.lua # Reasoning integration tests (--reasoning) └── unit/ ├── test_action_service.lua # ActionService integration tests (35 tests) ├── test_actions.lua # Placeholder gating, flag cascading tests (65 tests) ├── test_auto_update.lua # Auto-update helper tests (23 tests) ├── test_constants.lua # Context constants, GitHub URLs tests (20 tests) ├── test_constraint_utils.lua # Constraint utilities tests (25 tests) ├── test_export.lua # Export formatting, content modes tests (50 tests) ├── test_loaders.lua # BehaviorLoader, DomainLoader tests (26 tests) ├── test_message_history.lua # Conversation tracking, token estimation tests (54 tests) ├── test_openai_compatible.lua # OpenAI-compatible base class tests (30 tests) ├── test_prompt_building.lua # MessageBuilder, ContextExtractor gating, cache flow (121 tests) ├── test_reasoning.lua # Reasoning capabilities, request injection tests (65 tests) ├── test_response_parser.lua # Provider response parsing tests (42 tests) ├── test_state_management.lua # Context/config state management tests (68 tests) ├── test_streaming_parser.lua # SSE/NDJSON parsing tests (22 tests) ├── test_system_prompts.lua # Behavior, language, domain tests (73 tests) ├── test_templates.lua # Template constants, nudge substitution, action regression (160 tests) └── test_web_search.lua # Web search detection tests (44 tests) ``` -------------------------------- ### Verify Lua module availability Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Check if the ssl.https module is correctly loaded by the Lua interpreter. ```bash lua -e "print(pcall(require,'ssl.https'))" ``` -------------------------------- ### Handle Action Selection and Configuration Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Processes changes to the action dropdown, automatically updating UI fields, prompt templates, and API parameters based on the selected action definition. ```javascript // Find action by ID across all contexts function findAction(actionId) { for (const ctx of ['highlight', 'book', 'library', 'general']) { const actions = state.actions[ctx] || []; const found = actions.find(a => a.id === actionId); if (found) return found; } return null; } function onActionChange() { const actionId = document.getElementById('action-select').value; let contextType = document.getElementById('context-type').value; const actionPromptSection = document.getElementById('action-prompt-section'); if (!actionId) { // No action selected - hide action-related sections document.getElementById('action-overrides-section').style.display = 'none'; actionPromptSection.style.display = 'none'; document.getElementById('action-prompt').value = ''; return; } // Search all contexts for the action (handles compound contexts properly) const action = findAction(actionId); if (action) { // Show action prompt section actionPromptSection.style.display = 'block'; // Auto-switch context type to match action's context (if specific) // This ensures book actions like X-Ray get book context type if (action.context && action.context !== 'all' && action.context !== 'both') { const contextSelect = document.getElementById('context-type'); if (contextSelect.value !== action.context) { contextSelect.value = action.context; onContextTypeChange(); contextType = action.context; // Update local var for any checks below } } // Set behavior if action specifies it if (action.behavior_variant) { const behaviorSelect = document.getElementById('behavior-select'); if (behaviorSelect.querySelector(`option[value="${action.behavior_variant}"]`)) { behaviorSelect.value = action.behavior_variant; onBehaviorChange(); } } // Set temperature if action specifies it if (action.api_params && action.api_params.temperature !== undefined) { document.getElementById('temperature').value = action.api_params.temperature; updateTempLabel(); } // Set thinking if action specifies it if (action.extended_thinking === 'on') { document.getElementById('thinking-enabled').checked = true; if (action.api_params && action.api_params.thinking_budget) { document.getElementById('thinking-budget').value = action.api_params.thinking_budget; } } else if (action.extended_thinking === 'off') { document.getElementById('thinking-enabled').checked = false; } // Set action prompt to the action's template (with placeholders resolved) const promptText = action.template_text || action.prompt || ''; document.getElementById('action-prompt').value = resolvePlaceholders(promptText); // Update action overrides section (always shown for all actions) updateActionOverrides(action); } } ``` -------------------------------- ### Test Runner Usage Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Commands for executing automated tests against providers, including connectivity and model validation. ```bash # Unit tests only (no API calls) lua tests/run_tests.lua --unit # Basic connectivity for all providers lua tests/run_tests.lua # Single provider lua tests/run_tests.lua anthropic # Comprehensive tests (behaviors, temps, domains) lua tests/run_tests.lua anthropic --full # Validate ALL models for a provider (minimal cost) lua tests/run_tests.lua --models openai # Validate all models across all providers lua tests/run_tests.lua --models # Verbose output lua tests/run_tests.lua -v ``` -------------------------------- ### Compare Providers via API Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Sends a POST request to the comparison endpoint and renders the results in a grid layout. Requires at least two providers to be selected via checkbox inputs. ```javascript async function compareProviders() { const checked = document.querySelectorAll('#compare-checks input:checked'); const providers = Array.from(checked).map(c => c.value); if (providers.length < 2) { showStatus('error', 'Select at least 2 providers to compare'); return; } showStatus('loading', 'Comparing providers...'); try { const config = getConfig(); config.providers = providers; const response = await fetch('/api/compare', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); state.compareResults = await response.json(); if (state.compareResults.error) { showStatus('error', state.compareResults.error); return; } // Build comparison view const grid = document.getElementById('output-compare'); grid.innerHTML = providers.map(p => { const result = state.compareResults.results[p] || { error: 'No result' }; return `

${p}

${result.error ? result.error : syntaxHighlight(result.body || {})}
`; }).join(''); showStatus('success', 'Comparison complete'); switchTab('compare'); } catch (e) { showStatus('error', 'Compare failed: ' + e.message); } } ``` -------------------------------- ### Model Audit and Capability Probing Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Commands to probe model capabilities and diff model lists against local snapshots. ```bash eval "$(luarocks --lua-version 5.5 path)" # required first: real HTTP + dkjson # Discovery diff (list GETs only, no generation cost) lua tests/model_audit.lua lua tests/model_audit.lua anthropic gemini # Probe battery on one model (~10-14 live micro-requests, fractions of a cent) lua tests/model_audit.lua --probe anthropic claude-opus-5 # Probe everything the diff found (capped at 10 per run) lua tests/model_audit.lua --probe-new lua tests/model_audit.lua --verbose # full error bodies + ignored-id lists ``` -------------------------------- ### Export Application State Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Serializes current application state into a JSON file and triggers a browser download. ```javascript function exportJSON() { const data = { timestamp: new Date().toISOString(), config: getConfig(), build: state.lastBuild, response: state.lastResponse }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `koassistant-request-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); } ``` -------------------------------- ### Initialize Plugin State and DOM Events Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Defines the global state object and initializes event listeners for UI interactions like dark mode, tab switching, and data loading upon DOM content load. ```javascript // State const state = { providers: {}, domains: [], behaviors: [], // Loaded from /api/behaviors actions: { highlight: [], book: [], library: [], general: [] }, lastBuild: null, lastResponse: null, compareResults: null, conversationHistory: [], // For multi-turn chat originalRequest: null, // Store first request (not overwritten by replies) currentSystemPrompt: null // Stored for follow-up messages }; // Initialize document.addEventListener('DOMContentLoaded', async () => { // Dark mode from localStorage if (localStorage.getItem('darkMode') === 'true') { document.body.classList.add('dark'); } // Tab switching document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => switchTab(tab.dataset.tab)); }); // User languages input - update primary language dropdown document.getElementById('user-languages').addEventListener('input', onUserLanguagesChange); // Translation checkbox - toggle translation language visibility document.getElementById('translation-use-primary').addEventListener('change', () => { const usePrimary = document.getElementById('translation-use-primary').checked; document.getElementById('translation-language-container').style.display = usePrimary ? 'none' : 'block'; }); // Translation language dropdown - show/hide custom input document.getElementById('translation-language').addEventListener('change', onTranslationLanguageChange); // Initial state document.getElementById('translation-language-container').style.display = 'none'; // Load all data await Promise.all([ loadProviders(), loadDomains(), loadBehaviors(), loadActions(), loadSettings() ]); // Initialize context type onContextTypeChange(); }); function toggleDarkMode() { document.body.classList.toggle('dark'); localStorage.setItem('darkMode', document.body.classList.contains('dark')); } function switchTab(tabName) { ``` -------------------------------- ### Request Inspector Usage Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/README.md Commands for visualizing and exporting API requests sent to providers. ```bash # Inspect single provider lua tests/inspect.lua anthropic lua tests/inspect.lua openai --behavior full # Compare providers side-by-side lua tests/inspect.lua --compare anthropic openai gemini # Export as JSON lua tests/inspect.lua --export anthropic > request.json # List providers and presets lua tests/inspect.lua --list # Use presets lua tests/inspect.lua anthropic --preset thinking lua tests/inspect.lua anthropic --preset domain # Custom options lua tests/inspect.lua anthropic --behavior minimal --temp 0.5 lua tests/inspect.lua anthropic --languages "English, Spanish" lua tests/inspect.lua anthropic --thinking 8192 ``` -------------------------------- ### Domain File Template Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/domains.sample/README.md The required structure for a domain file, including the mandatory header and optional metadata in an HTML comment. ```markdown # Domain Name Your domain content here. This text is sent to the AI as context for every message in the conversation. Focus on: - Specialized terminology and concepts - Methodological frameworks - Analytical tools and approaches - Source traditions and references - Technical precision in your field ``` -------------------------------- ### Update Action Dropdown and Context UI Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Populates the action selection menu and manages the visibility of input fields based on the selected context type. ```javascript testing)' + actions.map(a => { const prefix = a.is_custom ? '★ ' : ''; return ``; }).join(''); // Auto-select Ask as default (plugin always uses Ask behavior when no specific action) const askOption = actions.find(a => a.id === 'ask'); if (askOption) { actionSelect.value = 'ask'; } } function onDomainChange() { const domainId = document.getElementById('domain-select').value; const domainTextarea = document.getElementById('domain'); if (domainId) { const domain = state.domains.find(d => d.id === domainId); if (domain) { domainTextarea.value = domain.context; } } else { domainTextarea.value = ''; } } function onContextTypeChange() { const contextType = document.getElementById('context-type').value; // Show/hide context fields document.getElementById('highlight-fields').style.display = contextType === 'highlight' ? 'block' : 'none'; document.getElementById('book-fields').style.display = (contextType === 'book' || contextType === 'highlight') ? 'block' : 'none'; document.getElementById('multi-book-fields').style.display = contextType === 'library' ? 'block' : 'none'; // Update action dropdown to show context-appropriate actions updateActionDropdown(); } ``` -------------------------------- ### API Request Builder Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Asynchronous function to fetch configuration, build a request, and update the UI with the result. ```javascript async function buildRequest() { showStatus('loading', 'Building request...'); try { const config = getConfig(); const response = await fetch('/api/build', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); state.lastBuild = await response.json(); if (state.lastBuild.error) { showStatus('error', state.lastBuild.error); return; } // Update outputs document.getElementById('output-request').innerHTML = syntaxHighlight(state.lastBuild.request?.body || {}); document.getElementById('output-system').innerHTML = syntaxHighlight({ ``` -------------------------------- ### Render Chat Interface Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Generates HTML for chat messages and manages UI state for the chat view, including auto-scrolling and reply section visibility. ```javascript let chatHtml = ''; // Show original user message (only the FIRST one - subsequent messages are in conversationHistory) // After replies, the request body contains ALL messages, so we only want the original const firstUserMessage = userMessages.length > 0 ? userMessages\[0\] : null; if (firstUserMessage) { chatHtml += `
▶ User
${escapeHtml(firstUserMessage)}
`; } else { chatHtml += `
▶ User
No user message found in request
`; } // If we have conversation history, it contains all messages in order (including first response) // Only show assistantText if NO conversation history yet if (state.conversationHistory.length > 0) { // Show all messages from conversation history (first response + all follow-ups) for (const msg of state.conversationHistory) { const roleClass = msg.role === 'user' ? 'user' : 'assistant'; const roleLabel = msg.role === 'user' ? '▶ User' : '◉ KOAssistant'; chatHtml += `
${roleLabel}
${escapeHtml(msg.content)}
`; } } else { // No follow-ups yet - show the first assistant response if (assistantText) { chatHtml += `
◉ KOAssistant
${escapeHtml(assistantText)}
`; } else { chatHtml += `
◉ KOAssistant
(No response text - check Response tab for details)
`; } } const chatMessagesEl = document.getElementById('chatMessages'); chatMessagesEl.innerHTML = chatHtml; // Auto-scroll chat to bottom const chatView = document.getElementById('output-chat'); chatView.scrollTop = chatView.scrollHeight; // Show reply section if we have any response (allows continuing even if parsing failed) const replySection = document.getElementById('chatReplySection'); if (responseResult?.response || state.conversationHistory.length > 0) { replySection.style.display = 'block'; } else { replySection.style.display = 'none'; } ``` -------------------------------- ### Expanded Editor Modal Operations Source: https://github.com/zeeyado/koassistant.koplugin/blob/main/tests/web/index.html Logic for opening, closing, saving, and inserting placeholders within the expanded editor modal. ```javascript let expandedEditorTargetId = null; function openExpandedEditor(targetId, title) { expandedEditorTargetId = targetId; const targetTextarea = document.getElementById(targetId); const modalTextarea = document.getElementById('expandedEditorTextarea'); const saveBtn = document.getElementById('expandedEditorSave'); const cancelBtn = document.getElementById('expandedEditorCancel'); const placeholderPicker = document.getElementById('placeholderPicker'); document.getElementById('expandedEditorTitle').textContent = title || 'Edit'; modalTextarea.value = targetTextarea.value; // Preserve readonly state from target const isReadonly = targetTextarea.readOnly; modalTextarea.readOnly = isReadonly; if (isReadonly) { document.getElementById('expandedEditorTitle').textContent = (title || 'View') + ' (read-only)'; saveBtn.style.display = 'none'; cancelBtn.textContent = 'Close'; } else { saveBtn.style.display = ''; cancelBtn.textContent = 'Cancel'; } // Show placeholder picker only for prompt-related fields (not readonly) const showPlaceholders = !isReadonly && ( targetId === 'action-prompt' || targetId === 'additional-input' || targetId === 'domain' || targetId === 'custom-behavior' ); placeholderPicker.style.display = showPlaceholders ? 'block' : 'none'; document.getElementById('expandedEditorModal').classList.add('visible'); modalTextarea.focus(); } function insertPlaceholder() { const select = document.getElementById('placeholderSelect'); const placeholder = select.value; const textarea = document.getElementById('expandedEditorTextarea'); const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; // Insert placeholder at cursor position textarea.value = text.substring(0, start) + placeholder + text.substring(end); // Move cursor to end of inserted placeholder const newCursorPos = start + placeholder.length; textarea.selectionStart = newCursorPos; textarea.selectionEnd = newCursorPos; textarea.focus(); } function closeExpandedEditor() { document.getElementById('expandedEditorModal').classList.remove('visible'); expandedEditorTargetId = null; } function saveExpandedEditor() { if (expandedEditorTargetId) { document.getElementById(expandedEditorTargetId).value = document.getElementById('expandedEditorTextarea').value; } closeExpandedEditor(); } ``` -------------------------------- ### Add an AI translation disclaimer Source: https://github.com/zeeyado/koassistant.koplugin/wiki/Contributing-to-this-Wiki Include this block at the top of any wiki page translated using AI tools to inform readers and encourage community improvements. ```markdown > This page was translated from the English version with AI assistance. > Native speakers: feel free to improve the phrasing! ```