### 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 `
${result.error ? result.error : syntaxHighlight(result.body || {})}