Files
odysseus/tests/test_publication_plan_b.py
T

151 lines
7.2 KiB
Python

"""Publication behavior: cold catalogs, server QR, notices, and removed resources."""
import asyncio
import base64
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
ROOT = Path(__file__).resolve().parents[1]
def test_retained_artifacts_match_distributed_provenance_and_notices():
ledger = json.loads((ROOT / 'THIRD_PARTY_PROVENANCE.json').read_text())
assert len(ledger['retained']) == 6
for entry in ledger['retained']:
assert entry['source_records']
assert hashlib.sha256((ROOT / entry['notice']).read_bytes()).hexdigest() == entry['notice_sha256']
for artifact in entry['artifacts']:
data = (ROOT / artifact['path']).read_bytes()
assert len(data) == artifact['size_bytes']
assert hashlib.sha256(data).hexdigest() == artifact['sha256']
def test_removed_resource_ledger_has_no_runtime_references():
ledger = (ROOT / 'PUBLICATION_ASSET_DECISIONS.md').read_text()
removed = re.findall(r'^- SAN-\d+: `([^`]+)`$', ledger, re.M)
assert len(removed) == 21
# Inspect tracked text, including files omitted by parity-audit exclusions.
tracked = subprocess.check_output(['git', 'ls-files', '-z'], cwd=ROOT).decode().split('\0')
for path in removed:
assert not (ROOT / path).exists()
for name in tracked:
if name == "PUBLICATION_ASSET_DECISIONS.md":
continue # Historical ledger entries are intentionally non-resource references.
file = ROOT / name
if not file.is_file():
continue
try:
content = file.read_text()
except UnicodeError:
continue
assert Path(path).name not in content, (path, name)
def test_cold_catalog_and_refresh_use_mutable_data(monkeypatch, tmp_path):
from services.hwfit import models, hf_discovery as discovery
from src import constants
monkeypatch.setattr(constants, 'DATA_DIR', str(tmp_path))
monkeypatch.setattr(models, '_models_cache', None)
monkeypatch.setattr(discovery, 'MLX_COMMUNITY_CACHE', tmp_path / 'hwfit/mlx_community_models.json')
monkeypatch.setattr(discovery, 'HF_COLLECTION_MODELS_CACHE', tmp_path / 'hwfit/hf_collection_models.json')
for file in ['hf_models.json', 'mlx_community_models.json']:
assert (ROOT / 'services/hwfit/data' / file).read_text() == '[]\n'
assert models.get_models() == []
row = {'name': 'test/runtime', 'parameter_count': '3B', 'quantization': 'F16'}
monkeypatch.setattr(discovery, 'fetch_collection_models', lambda source: [dict(row, name='mlx-community/test' if source['mlx_only'] else row['name'])] if source.get('mlx_only') else [row])
assert models.refresh_dynamic_catalogs(force=True) == {'mlx_community': 1, 'hf_collections': 1}
assert {r['name'] for r in models.get_models()} == {'test/runtime', 'mlx-community/test'}
cached = json.loads(discovery.HF_COLLECTION_MODELS_CACHE.read_text())
assert cached['source'] and cached['fetched_at'] and cached['count'] == 1
# Offline cache loading must work without a fetch.
models.reset_model_cache()
monkeypatch.setattr(discovery, 'fetch_collection_models', Mock(side_effect=OSError('offline')))
assert len(models.get_models()) == 2
user_file = Path(models.model_catalog_path())
user_file.write_text(json.dumps([dict(row, name='test/user')]))
models.reset_model_cache()
assert len(models.get_models()) == 3
def test_empty_catalog_route_gives_real_refresh_guidance(monkeypatch):
from services.hwfit import models, hardware
from routes.hwfit_routes import setup_hwfit_routes
monkeypatch.setattr(hardware, 'detect_system', lambda **kwargs: {'backend': 'cpu_x86'})
monkeypatch.setattr(models, 'get_models', lambda: [])
monkeypatch.setattr(models, 'refresh_dynamic_catalogs', Mock(side_effect=OSError('offline')))
endpoint = next(r.endpoint for r in setup_hwfit_routes().routes if r.path.endswith('/models'))
result = endpoint(refresh_catalog=True)
assert result['models'] == []
assert 'Rescan' in result['error'] and 'offline' in result['error']
assert result['catalog_refresh'] == {'error': 'offline'}
def test_2fa_setup_still_generates_a_server_png():
from routes.auth_routes import setup_auth_routes
auth = Mock()
auth.get_username_for_token.return_value = 'alice'
auth.totp_enabled.return_value = False
auth.totp_generate_secret.return_value = 'JBSWY3DPEHPK3PXP'
auth.totp_get_provisioning_uri.return_value = 'otpauth://totp/test:alice?secret=JBSWY3DPEHPK3PXP&issuer=test'
endpoint = next(r.endpoint for r in setup_auth_routes(auth).routes if r.path.endswith('/2fa/setup'))
result = asyncio.run(endpoint(SimpleNamespace(cookies={})))
assert result['uri'].startswith('otpauth://')
prefix, data = result['qr_code'].split(',', 1)
assert prefix == 'data:image/png;base64'
assert base64.b64decode(data).startswith(b'\x89PNG\r\n\x1a\n')
def test_distribution_paths_include_all_notices():
for name in ['Odysseus.spec', 'build-windows-portable.ps1', 'build-macos-app.sh', 'Dockerfile']:
text = (ROOT / name).read_text()
assert 'licenses' in text and 'THIRD_PARTY_PROVENANCE.json' in text and 'ACKNOWLEDGMENTS.md' in text
ignore = (ROOT / '.dockerignore').read_text()
assert '!ACKNOWLEDGMENTS.md' in ignore
manifest = json.loads((ROOT / 'static/manifest.json').read_text())
assert not manifest.get('icons')
assert manifest['display'] == 'standalone'
def test_browser_publication_behaviors():
"""Real Chromium DOM: text-only website, saved fonts, escaped print/math."""
if not shutil.which('node'):
pytest.skip('Node is required')
probe = subprocess.run(['node', '-e', "require.resolve('playwright')"], cwd=ROOT, capture_output=True)
if probe.returncode:
pytest.skip('Playwright is required')
result = subprocess.run(['node', str(ROOT / 'tests/publication_plan_b_browser.cjs')], cwd=ROOT, capture_output=True, text=True, timeout=60)
assert result.returncode == 0, result.stdout + result.stderr
output = json.loads(result.stdout)
assert output == {'website': True, 'fonts': True, 'print': True, 'pwa': True}
def test_service_worker_activation_purges_previous_asset_cache():
result = subprocess.run(['node', '-e', r"""
const fs = require('fs');
const vm = require('vm');
const handlers = {};
const removed = [];
let pending;
const source = fs.readFileSync('static/sw.js', 'utf8');
const current = source.match(/const CACHE_NAME = '([^']+)'/)[1];
vm.runInNewContext(source, {
self: { addEventListener: (event, callback) => handlers[event] = callback, clients: { claim: () => Promise.resolve() } },
caches: { keys: async () => ['previous-cache', current], delete: async key => { removed.push(key); return true; } },
});
handlers.activate({ waitUntil: promise => pending = promise });
pending.then(() => process.stdout.write(JSON.stringify(removed))).catch(error => { console.error(error); process.exit(1); });
"""], cwd=ROOT, text=True, capture_output=True, timeout=10)
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == ['previous-cache']