mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
Three findings from running Qwen3-1.7B against the docker stack: the llm image lost libgomp when the build tools were removed (llama-server needs the OpenMP runtime); the Help prompt let the small model answer from memory instead of the docs, so it now requires search_help before any answer; and help search results called the slug "doc" while get_help_doc's argument is "slug", so the model invented slugs — the names now match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
"""Tests for the help-docs search (Help mode). Uses a fixture corpus, never the real submodule."""
|
|
import json
|
|
from http import HTTPStatus
|
|
|
|
import pytest
|
|
|
|
from modules.ai import help_docs, lib
|
|
|
|
|
|
@pytest.fixture
|
|
def help_corpus(test_directory, monkeypatch):
|
|
"""A small help-docs corpus in a temporary directory."""
|
|
docs = test_directory / 'help-docs'
|
|
(docs / 'system').mkdir(parents=True)
|
|
(docs / 'modules').mkdir()
|
|
(docs / 'index.md').write_text('# Home\n\nWelcome to WROLPi help.\n')
|
|
(docs / 'system/logs.md').write_text(
|
|
'# Logs\n\n## Reading logs\n\nUse journalctl to read service logs on a WROLPi.\n')
|
|
(docs / 'modules/videos.md').write_text(
|
|
'# Videos\n\n## Downloading\n\nVideos are downloaded with yt-dlp into channels.\n'
|
|
+ ('More text about videos. ' * 400)) # > one page
|
|
monkeypatch.setenv('HELP_DOCS_DIR', str(docs))
|
|
help_docs._CACHE.clear()
|
|
try:
|
|
yield docs
|
|
finally:
|
|
help_docs._CACHE.clear()
|
|
|
|
|
|
def test_search_help_scoring(help_corpus):
|
|
"""Title matches outrank body matches; snippets show context."""
|
|
results, total = help_docs.search_help('logs')
|
|
assert total == 1
|
|
assert results[0]['slug'] == 'system/logs'
|
|
assert results[0]['title'] == 'Logs'
|
|
assert results[0]['link'] == '/system/logs/'
|
|
assert 'journalctl' in results[0]['snippet'] or 'Logs' in results[0]['snippet']
|
|
|
|
# 'videos' matches the videos doc far better than any other.
|
|
results, _ = help_docs.search_help('downloading videos')
|
|
assert results[0]['slug'] == 'modules/videos'
|
|
|
|
# No matches.
|
|
results, total = help_docs.search_help('xyzzy')
|
|
assert results == [] and total == 0
|
|
|
|
|
|
def test_help_doc_cache_invalidation(help_corpus):
|
|
"""Edited files are re-parsed; deleted files leave the corpus."""
|
|
results, _ = help_docs.search_help('welcome')
|
|
assert results[0]['slug'] == 'index'
|
|
assert results[0]['link'] == '/'
|
|
|
|
(help_corpus / 'index.md').write_text('# Home\n\nGreetings, traveler.\n')
|
|
import os
|
|
os.utime(help_corpus / 'index.md', (0, 0)) # force a distinct mtime
|
|
results, total = help_docs.search_help('welcome')
|
|
assert total == 0
|
|
results, _ = help_docs.search_help('greetings')
|
|
assert results[0]['slug'] == 'index'
|
|
|
|
(help_corpus / 'system/logs.md').unlink()
|
|
_, total = help_docs.search_help('logs')
|
|
assert total == 0
|
|
|
|
|
|
def test_get_help_doc_refuses_traversal(help_corpus):
|
|
assert help_docs.get_help_doc('../secrets') is None
|
|
assert help_docs.get_help_doc('system/logs') is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ai_help_endpoints(async_client, help_corpus):
|
|
"""Help can be searched and pages read as paged markdown."""
|
|
content = dict(search_str='logs')
|
|
request, response = await async_client.post('/api/ai/help/search', content=json.dumps(content))
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json['total'] == 1
|
|
result = response.json['results'][0]
|
|
assert result['slug'] == 'system/logs'
|
|
|
|
request, response = await async_client.get(f'/api/ai/help/{result["slug"]}')
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json['title'] == 'Logs'
|
|
assert 'journalctl' in response.json['content']
|
|
|
|
# Long pages are paged.
|
|
request, response = await async_client.get('/api/ai/help/modules/videos')
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json['next_offset'] == lib.PAGE_SIZE
|
|
|
|
# Unknown page.
|
|
request, response = await async_client.get('/api/ai/help/no/such/page')
|
|
assert response.status_code == HTTPStatus.NOT_FOUND
|
|
|
|
# Empty search is a clear error.
|
|
request, response = await async_client.post('/api/ai/help/search', content=json.dumps(dict()))
|
|
assert response.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ai_help_not_installed(async_client, monkeypatch, test_directory):
|
|
"""A missing help directory returns empty results with a message, never a 500."""
|
|
monkeypatch.setenv('HELP_DOCS_DIR', str(test_directory / 'nope'))
|
|
help_docs._CACHE.clear()
|
|
|
|
content = dict(search_str='logs')
|
|
request, response = await async_client.post('/api/ai/help/search', content=json.dumps(content))
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json['results'] == []
|
|
assert 'not installed' in response.json['message']
|
|
|
|
request, response = await async_client.get('/api/ai/help/system/logs')
|
|
assert response.status_code == HTTPStatus.NOT_FOUND
|