mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 07:52:20 +02:00
chore(publication): close pre-integration release blockers
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
[
|
||||
{
|
||||
"name": "microsoft/Phi-mini-MoE-instruct",
|
||||
"provider": "test",
|
||||
"parameter_count": "2B",
|
||||
"parameters_raw": 2000000000,
|
||||
"quantization": "F16",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-01",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "Qwen/Qwen2.5-3B-Instruct-AWQ",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "AWQ-4bit",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-02",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "Qwen/Qwen2.5-3B-Instruct",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "F16",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-03",
|
||||
"gguf_sources": [
|
||||
{
|
||||
"repo": "test/three-GGUF",
|
||||
"file": "three-Q4_K_M.gguf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Qwen/Qwen3.5-9B",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "F16",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-04",
|
||||
"gguf_sources": [
|
||||
{
|
||||
"repo": "unsloth/Qwen3.5-9B-GGUF",
|
||||
"file": "Qwen3.5-9B-Q4_K_M.gguf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Qwen/Qwen3.6-27B",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "F16",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-05",
|
||||
"gguf_sources": [
|
||||
{
|
||||
"repo": "unsloth/Qwen3.6-27B-GGUF",
|
||||
"file": "Qwen3.6-27B-Q4_K_M.gguf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Qwen/Qwen3.6-35B-A3B",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "F16",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-06",
|
||||
"gguf_sources": [
|
||||
{
|
||||
"repo": "unsloth/Qwen3.6-35B-A3B-GGUF",
|
||||
"file": "Qwen3.6-35B-A3B-UD-Q4_K_M.gguf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "google/gemma-4-12B-it",
|
||||
"provider": "test",
|
||||
"parameter_count": "12B",
|
||||
"parameters_raw": 12000000000,
|
||||
"quantization": "F16",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-07",
|
||||
"gguf_sources": [
|
||||
{
|
||||
"repo": "unsloth/gemma-4-12B-it-GGUF",
|
||||
"file": "test-Q4_K_M.gguf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "google/gemma-4-12B-it-qat-int4",
|
||||
"provider": "test",
|
||||
"parameter_count": "12B",
|
||||
"parameters_raw": 12000000000,
|
||||
"quantization": "QAT-INT4",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-08",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "google/gemma-4-12B-it-qat-int8",
|
||||
"provider": "test",
|
||||
"parameter_count": "12B",
|
||||
"parameters_raw": 12000000000,
|
||||
"quantization": "QAT-INT8",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-09",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "test/native-NVFP4",
|
||||
"provider": "test",
|
||||
"parameter_count": "12B",
|
||||
"parameters_raw": 12000000000,
|
||||
"quantization": "NVFP4",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-10",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "test/native-FP8",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "FP8",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-11",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "mlx-community/test-4bit",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "mlx-4bit",
|
||||
"context_length": 4096,
|
||||
"release_date": "2026-01-12",
|
||||
"gguf_sources": []
|
||||
},
|
||||
{
|
||||
"name": "test/standalone-GGUF",
|
||||
"provider": "test",
|
||||
"parameter_count": "3B",
|
||||
"parameters_raw": 3000000000,
|
||||
"quantization": "Q4_K_M",
|
||||
"context_length": 4096,
|
||||
"release_date": null,
|
||||
"gguf_sources": [],
|
||||
"is_gguf": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Authored ranking inputs; factual identifiers from regression tests are selectors.
|
||||
|
||||
All sizes, dates and capabilities are synthetic, not statements about Hub models.
|
||||
No copied production-catalog rows or descriptions are included.
|
||||
|
||||
The fixture is opt-in: a module imports ``publication_catalog`` and requests it
|
||||
explicitly (``pytestmark = pytest.mark.usefixtures("publication_catalog")`` or
|
||||
a test argument). It yields the authored input rows so tests can derive their
|
||||
expectations from the input instead of restating it.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
FIXTURE_PATH = Path(__file__).parent / "fixtures/hwfit_publication_models.json"
|
||||
|
||||
|
||||
def authored_rows():
|
||||
return json.loads(FIXTURE_PATH.read_text())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def publication_catalog(monkeypatch):
|
||||
from services.hwfit import models, hf_discovery
|
||||
|
||||
rows = authored_rows()
|
||||
# Exercise the real merge/normalization path, independent of user caches.
|
||||
monkeypatch.setattr(models, "_models_cache", None)
|
||||
monkeypatch.setattr(models, "_load_model_file", lambda path: rows if str(path) == models.model_catalog_path() else [])
|
||||
monkeypatch.setattr(hf_discovery, "load_cached_hf_collection_models", lambda: [])
|
||||
monkeypatch.setattr(hf_discovery, "load_cached_mlx_community_models", lambda: [])
|
||||
yield authored_rows()
|
||||
@@ -0,0 +1,127 @@
|
||||
// Behavioral browser checks for the accepted publication surface.
|
||||
const { chromium } = require('playwright');
|
||||
const { readFileSync } = require('fs');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
(async () => {
|
||||
const origin = process.env.ODYSSEUS_TEST_STATIC_ORIGIN;
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
const mediaRequests = [];
|
||||
const errors = [];
|
||||
page.on('request', req => { if (/\.(webm|mp4)(?:$|\?)/.test(req.url())) mediaRequests.push(req.url()); });
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(origin + '/website/index.html');
|
||||
assert.equal(await page.locator('video, .preview-panel, .sec-bg-tint').count(), 0);
|
||||
assert.equal(await page.locator('.feature-description').count(), 8);
|
||||
for (const description of await page.locator('.feature-description .desc').allTextContents()) assert(description.trim());
|
||||
for (const width of [1280, 390]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
assert(await page.locator('.feature-description').first().isVisible());
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
}
|
||||
assert.deepEqual(mediaRequests, []);
|
||||
assert.deepEqual(errors, []);
|
||||
|
||||
const manifest = await (await page.request.get(origin + '/static/manifest.json')).json();
|
||||
assert.equal(manifest.display, 'standalone');
|
||||
assert(!manifest.icons);
|
||||
for (const filename of ['index.html', 'login.html']) {
|
||||
const html = await (await page.request.get(origin + '/static/' + filename)).text();
|
||||
assert(!/<link[^>]+rel=["']apple-touch-icon/.test(html));
|
||||
}
|
||||
|
||||
// A small host DOM runs the actual export function and markdown renderer.
|
||||
await page.route('**/publication-harness', route => route.fulfill({ contentType: 'text/html', body: '<!DOCTYPE html><body><textarea id="doc-editor-textarea"></textarea><select id="doc-language-select"><option value="markdown">Markdown</option><option value="text">Text</option><option value="html">HTML</option></select><div id="theme-grid"></div><select id="theme-font-select"><option value="mono">Fira Code</option><option value="sans">Sans</option></select><select id="theme-density-select"><option value="comfortable">Comfortable</option></select></body>' }));
|
||||
await page.route('**/api/**', route => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ fonts: {}, value: null }) }));
|
||||
await page.goto(origin + '/publication-harness');
|
||||
const source = readFileSync('static/js/document.js', 'utf8');
|
||||
const fn = source.match(/\n async function exportAsPdf\(\) \{(.*?)\n \}\n/s)[0];
|
||||
const result = await page.evaluate(async ({ origin, fn }) => {
|
||||
const module = await import(origin + '/static/js/markdown.js');
|
||||
const markdownModule = module.default || module;
|
||||
const _isRichTextLang = lang => lang === 'html';
|
||||
const _isDocxLang = () => false;
|
||||
const _richTextExportCss = () => 'p { color: black; }';
|
||||
const _getExportBaseName = () => 'Print test';
|
||||
const activeDocId = 'test';
|
||||
const errors = [];
|
||||
const uiModule = { showError: message => errors.push(message) };
|
||||
const originalRender = markdownModule.renderMath;
|
||||
const prints = [];
|
||||
markdownModule.renderMath = async container => {
|
||||
await originalRender(container);
|
||||
const frame = document.getElementById('doc-browser-print-frame');
|
||||
frame.contentWindow.print = () => {
|
||||
prints.push({ body: frame.contentDocument.body.innerHTML, title: frame.contentDocument.title, links: [...frame.contentDocument.querySelectorAll('link')].map(l => l.href) });
|
||||
frame.contentWindow.dispatchEvent(new Event('afterprint'));
|
||||
};
|
||||
};
|
||||
const run = eval('(' + fn.trim() + ')');
|
||||
const textarea = document.getElementById('doc-editor-textarea');
|
||||
textarea.value = 'Formula $E = mc^2$ here.';
|
||||
await run();
|
||||
document.getElementById('doc-language-select').value = 'text';
|
||||
textarea.value = '<script>parent.unsafe = true</script>& literal';
|
||||
await run();
|
||||
document.getElementById('doc-language-select').value = 'html';
|
||||
textarea.value = '<p>Rich content</p><script>parent.unsafe = true</script><img src="data:,x" onerror="parent.unsafe = true">';
|
||||
await run();
|
||||
const oldPrint = window.print;
|
||||
window.print = undefined;
|
||||
await run();
|
||||
window.print = oldPrint;
|
||||
return { prints, errors, unsafe: !!window.unsafe, frameRemaining: !!document.getElementById('doc-browser-print-frame') };
|
||||
}, { origin, fn });
|
||||
assert.equal(result.prints.length, 3);
|
||||
assert(result.prints[0].body.includes('class="katex"'));
|
||||
assert(!result.prints[0].body.includes('ody-math-pending'));
|
||||
assert(result.prints[0].links.some(url => url.includes('/katex/')));
|
||||
assert.equal(result.prints[0].title, 'Print test');
|
||||
assert(result.prints[1].body.includes('<script>'));
|
||||
assert(result.prints[2].body.includes('Rich content'));
|
||||
assert.equal(result.unsafe, false);
|
||||
assert.equal(result.frameRemaining, false);
|
||||
assert.deepEqual(result.errors, ['Browser printing is unavailable.']);
|
||||
|
||||
// Actual theme module: default, legacy local/server selections and custom theme preferences.
|
||||
const fonts = await page.evaluate(async origin => {
|
||||
localStorage.setItem('odysseus-theme', JSON.stringify({ name: 'dark', colors: {}, font: 'gohu' }));
|
||||
localStorage.setItem('odysseus-custom-themes', JSON.stringify({ old: { font: 'GohuFont' } }));
|
||||
const theme = await import(origin + '/static/js/theme.js');
|
||||
theme.applyFontDensity(null, null);
|
||||
const fallback = document.documentElement.style.getPropertyValue('--font-family');
|
||||
theme.applyFontDensity('gohu', null);
|
||||
const legacy = document.documentElement.style.getPropertyValue('--font-family');
|
||||
theme.initThemeUI();
|
||||
const selected = document.getElementById('theme-font-select').value;
|
||||
const saved = theme.getSaved().font;
|
||||
const custom = theme.getCustomThemes().old.font;
|
||||
theme.applyFontDensity('sans', null);
|
||||
const sans = document.documentElement.style.getPropertyValue('--font-family');
|
||||
return { fallback, legacy, selected, saved, custom, sans };
|
||||
}, origin);
|
||||
assert.equal(fonts.fallback, "'Fira Code', monospace");
|
||||
assert.equal(fonts.legacy, fonts.fallback);
|
||||
assert.equal(fonts.selected, 'mono');
|
||||
assert.equal(fonts.saved, 'mono');
|
||||
assert.equal(fonts.custom, 'mono');
|
||||
assert(fonts.sans.includes('system-ui'));
|
||||
|
||||
// Verify the real early bootstrap handles persisted legacy preferences.
|
||||
const appHtml = readFileSync('static/index.html', 'utf8');
|
||||
const bootstrap = [...appHtml.matchAll(/<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/g)].map(m => m[1]).find(code => code.includes('Apply font early'));
|
||||
assert(bootstrap);
|
||||
const early = await page.evaluate(async code => {
|
||||
const theme = await import('/static/js/theme.js');
|
||||
localStorage.setItem('odysseus-theme', JSON.stringify({ name: 'dark', colors: theme.THEMES.dark, font: 'gohu' }));
|
||||
eval(code);
|
||||
return document.documentElement.style.getPropertyValue('--font-family');
|
||||
}, bootstrap);
|
||||
assert.equal(early, fonts.fallback);
|
||||
console.log(JSON.stringify({ website: true, fonts: true, print: true, pwa: true }));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
@@ -89,7 +89,7 @@ a status code. `scripts/odysseus-smoke --areas` prints the current list.
|
||||
| Email | the fixture inbox lists, opens with its body, and the unread count drops on mark-read |
|
||||
| Memory | a fact is listed, found by search, and gone after delete |
|
||||
| Uploads | an attachment reads back byte for byte |
|
||||
| Cookbook | hardware is detected and recommendations come back sized against it; state persists |
|
||||
| Cookbook | hardware is detected; a fresh install's recommendation request returns the explicit empty-catalog Rescan guidance; state persists |
|
||||
| Settings | a preference written on one session is still there after a new login |
|
||||
|
||||
## What is not covered, and why
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
"""Cookbook: hardware is detected and the recommendations are sized against it.
|
||||
"""Cookbook: hardware is detected and a fresh install says how to get a catalog.
|
||||
|
||||
What the README advertises here is hardware-aware recommendation, and
|
||||
that is exactly the part that runs offline. Downloading and serving a
|
||||
model is left to the gap list: it needs tmux, a GPU runtime and several
|
||||
gigabytes over the network.
|
||||
No model catalog ships with the app: the bundled lists are empty, and
|
||||
rows only arrive when Rescan sends `refresh_catalog` while online. So a
|
||||
fresh data dir that was never refreshed must answer a recommendation
|
||||
request with an explicit empty-catalog message, not an empty table or a
|
||||
server error. Ranking itself is covered by the unit tests against an
|
||||
authored fixture catalog. Downloading and serving a model is left to the
|
||||
gap list: it needs tmux, a GPU runtime and several gigabytes over the
|
||||
network.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.constants import DATA_DIR
|
||||
|
||||
SYSTEM_PATH = "/api/hwfit/system"
|
||||
MODELS_PATH = "/api/hwfit/models"
|
||||
STATE_PATH = "/api/cookbook/state"
|
||||
@@ -14,6 +24,10 @@ GPUS_PATH = "/api/cookbook/gpus"
|
||||
|
||||
STATE_MARKER = "odysseusSmokeMarker"
|
||||
|
||||
# Everything that can give the instance a catalog: the user catalog and the
|
||||
# two caches a Rescan writes, all under the instance's data dir.
|
||||
CATALOG_FILES = ("hf_models.json", "hf_collection_models.json", "mlx_community_models.json")
|
||||
|
||||
|
||||
def test_hardware_is_detected(client):
|
||||
response = client.get(SYSTEM_PATH)
|
||||
@@ -28,14 +42,24 @@ def test_hardware_is_detected(client):
|
||||
assert gpus.json().get("ok") is True, gpus.text
|
||||
|
||||
|
||||
def test_recommendations_fit_the_detected_hardware(client):
|
||||
def test_fresh_install_explains_how_to_populate_the_catalog(client):
|
||||
catalog_dir = Path(DATA_DIR) / "hwfit"
|
||||
present = [name for name in CATALOG_FILES if (catalog_dir / name).exists()]
|
||||
if present:
|
||||
pytest.skip(f"not a fresh install: {catalog_dir} already holds {present}")
|
||||
|
||||
response = client.get(MODELS_PATH)
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
system = body.get("system") or {}
|
||||
assert system.get("cpu_name"), body
|
||||
recommended = body.get("models") or body.get("recommendations") or []
|
||||
assert recommended, f"no model recommendation for this hardware: {list(body)}"
|
||||
assert body.get("models") == [], body
|
||||
error = body.get("error") or ""
|
||||
assert "Model catalog is empty" in error, body
|
||||
assert "Rescan" in error, body
|
||||
# A plain request never refreshes, so nothing was fetched behind our back.
|
||||
assert "catalog_refresh" not in body, body
|
||||
assert not any((catalog_dir / name).exists() for name in CATALOG_FILES)
|
||||
|
||||
|
||||
def test_cookbook_state_persists(client):
|
||||
|
||||
@@ -60,7 +60,6 @@ def test_login_page_static_assets_resolve_under_mount_path():
|
||||
mounted_login_url = "https://example.test/odysseus/login"
|
||||
for relative_asset, mounted_path in (
|
||||
("static/manifest.json", "/odysseus/static/manifest.json"),
|
||||
("static/icons/icon-192.png", "/odysseus/static/icons/icon-192.png"),
|
||||
(
|
||||
"static/fonts/FiraCode-Regular.woff2",
|
||||
"/odysseus/static/fonts/FiraCode-Regular.woff2",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Repository asset ownership guards for issues #1335 and #6175.
|
||||
|
||||
Public Markdown and landing-page media belong in website/, while shared
|
||||
README/packaging imagery belongs in assets/branding/. Images in either managed
|
||||
root must be referenced by tracked text, and every tracked website video must
|
||||
be referenced by the site's entry point.
|
||||
optional README/packaging imagery belongs in assets/branding/. Retained images
|
||||
in either managed root must be referenced by tracked text, and every retained
|
||||
website video must be referenced by the site's entry point. Publication can
|
||||
operate with no imagery or videos.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
@@ -41,7 +42,8 @@ def _tracked(*paths_under):
|
||||
return None
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
return [REPO / line for line in out.stdout.splitlines() if line.strip()]
|
||||
return [REPO / line for line in out.stdout.splitlines()
|
||||
if line.strip() and (REPO / line).is_file()]
|
||||
|
||||
|
||||
def test_no_orphan_documentation_or_branding_images():
|
||||
@@ -49,9 +51,6 @@ def test_no_orphan_documentation_or_branding_images():
|
||||
if managed_files is None:
|
||||
pytest.skip("not a git checkout")
|
||||
managed_images = [p for p in managed_files if p.suffix.lower() in IMAGE_EXTS]
|
||||
assert any("assets/branding" in p.as_posix() for p in managed_images), (
|
||||
"expected assets/branding/ to contain the shared project imagery"
|
||||
)
|
||||
|
||||
# All tracked text we might reference an image from.
|
||||
all_tracked = _tracked(".") or []
|
||||
@@ -94,7 +93,6 @@ def test_pages_site_owns_its_entrypoint_and_media():
|
||||
assert text.startswith("---\nlayout: default\n---\n"), guide
|
||||
|
||||
website_videos = [p for p in website_files if p.suffix.lower() in VIDEO_EXTS]
|
||||
assert website_videos, "expected website/ to contain the landing-page videos"
|
||||
|
||||
entrypoint = (REPO / "website/index.html").read_text(encoding="utf-8")
|
||||
unreferenced = [
|
||||
|
||||
@@ -8,9 +8,15 @@ like Apple Silicon (GGUF-only recommendations) while datacenter CDNA and
|
||||
unknown-family AMD are left untouched, and that CUDA is unchanged.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from services.hwfit import hardware
|
||||
from services.hwfit.fit import rank_models
|
||||
from services.hwfit.models import get_models
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
# Rank authored inputs rather than publication catalog snapshots.
|
||||
pytestmark = pytest.mark.usefixtures("publication_catalog")
|
||||
|
||||
|
||||
def _rocm_system(family="rdna", ram_gb=32.0, vram_gb=16.0):
|
||||
@@ -170,6 +176,8 @@ def test_sort_by_newest_orders_by_release_date():
|
||||
"gpu_family": "rdna", "gpu_count": 1, "available_ram_gb": 22.0, "total_ram_gb": 31.0}
|
||||
res = rank_models(sys, sort="newest", limit=50)
|
||||
dated = [r.get("release_date") for r in res if r.get("release_date")]
|
||||
assert len(set(dated)) > 1, "authored inputs must exercise date ordering"
|
||||
assert any(not r.get("release_date") for r in res), "authored inputs must include an undated row"
|
||||
# dates present must be in descending order
|
||||
assert dated == sorted(dated, reverse=True), "release dates not descending"
|
||||
# any undated entries must come after all dated ones
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import pytest
|
||||
|
||||
from services.hwfit.fit import rank_models
|
||||
from services.hwfit.models import get_models, is_prequantized
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
# Rank authored inputs rather than publication catalog snapshots.
|
||||
pytestmark = pytest.mark.usefixtures("publication_catalog")
|
||||
|
||||
|
||||
def _8gb_vram_system():
|
||||
@@ -14,38 +20,63 @@ def _8gb_vram_system():
|
||||
}
|
||||
|
||||
|
||||
def test_gemma4_12b_in_catalog():
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
assert "google/gemma-4-12B-it" in catalog, "gemma-4-12B-it missing from catalog"
|
||||
GEMMA = "google/gemma-4-12B-it"
|
||||
|
||||
|
||||
def test_gemma4_12b_has_gguf_source():
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
entry = catalog["google/gemma-4-12B-it"]
|
||||
assert entry.get("gguf_sources"), "gemma-4-12B-it has no gguf_sources"
|
||||
repos = [s["repo"] for s in entry["gguf_sources"]]
|
||||
assert "unsloth/gemma-4-12B-it-GGUF" in repos
|
||||
def _input_row(rows, name):
|
||||
return next(r for r in rows if r["name"] == name)
|
||||
|
||||
|
||||
def _qat_rows(rows):
|
||||
return [r for r in rows if r["name"].startswith(GEMMA + "-qat-")]
|
||||
|
||||
|
||||
def test_gemma4_12b_user_catalog_row_wins_the_merge(publication_catalog, monkeypatch):
|
||||
"""A dynamic cache listing the same repo must not replace or duplicate the
|
||||
user-catalog row; rows only the cache knows are still merged in."""
|
||||
from services.hwfit import hf_discovery
|
||||
|
||||
monkeypatch.setattr(hf_discovery, "load_cached_hf_collection_models", lambda: [
|
||||
{"name": GEMMA, "quantization": "F16", "gguf_sources": [{"repo": "test/cache-GGUF", "file": "cache.gguf"}]},
|
||||
{"name": "test/cache-only", "quantization": "F16", "gguf_sources": []},
|
||||
])
|
||||
merged = get_models()
|
||||
names = [m["name"] for m in merged]
|
||||
|
||||
assert names.count(GEMMA) == 1
|
||||
assert "test/cache-only" in names
|
||||
entry = next(m for m in merged if m["name"] == GEMMA)
|
||||
assert entry["gguf_sources"] == _input_row(publication_catalog, GEMMA)["gguf_sources"]
|
||||
|
||||
|
||||
def test_gemma4_12b_ranked_row_carries_its_gguf_source(publication_catalog):
|
||||
hit = next(r for r in rank_models(_8gb_vram_system(), search="gemma-4-12B-it", limit=20) if r["name"] == GEMMA)
|
||||
assert hit["gguf_sources"] == _input_row(publication_catalog, GEMMA)["gguf_sources"]
|
||||
# The F16 input does not fit 8 GB, so the fit falls back to a GGUF quant.
|
||||
assert hit["quant"] == "Q4_K_M"
|
||||
|
||||
|
||||
def test_gemma4_12b_rank_models_returns_it_for_8gb_vram():
|
||||
results = rank_models(_8gb_vram_system(), search="gemma-4-12B-it", limit=20)
|
||||
names = [r["name"] for r in results]
|
||||
assert "google/gemma-4-12B-it" in names, "rank_models did not return gemma-4-12B-it for 8 GB VRAM"
|
||||
assert GEMMA in names, "rank_models did not return gemma-4-12B-it for 8 GB VRAM"
|
||||
|
||||
|
||||
def test_gemma4_12b_qat_entries_in_catalog():
|
||||
def test_gemma4_12b_qat_entries_rank_with_their_native_quant(publication_catalog):
|
||||
qat = _qat_rows(publication_catalog)
|
||||
assert len(qat) == 2
|
||||
ranked = {r["name"]: r for r in rank_models(_8gb_vram_system(), search="gemma-4-12B-it-qat", limit=20)}
|
||||
for row in qat:
|
||||
assert ranked[row["name"]]["quant"] == row["quantization"]
|
||||
|
||||
|
||||
def test_gemma4_12b_qat_entries_are_prequantized(publication_catalog):
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
assert "google/gemma-4-12B-it-qat-int4" in catalog
|
||||
assert "google/gemma-4-12B-it-qat-int8" in catalog
|
||||
for row in _qat_rows(publication_catalog):
|
||||
assert is_prequantized(catalog[row["name"]])
|
||||
|
||||
|
||||
def test_gemma4_12b_qat_entries_are_prequantized():
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
assert is_prequantized(catalog["google/gemma-4-12B-it-qat-int4"])
|
||||
assert is_prequantized(catalog["google/gemma-4-12B-it-qat-int8"])
|
||||
|
||||
|
||||
def test_gemma4_12b_qat_entries_have_no_gguf():
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
assert catalog["google/gemma-4-12B-it-qat-int4"]["gguf_sources"] == []
|
||||
assert catalog["google/gemma-4-12B-it-qat-int8"]["gguf_sources"] == []
|
||||
def test_gemma4_12b_qat_entries_have_no_gguf(publication_catalog):
|
||||
ranked = {r["name"]: r for r in rank_models(_8gb_vram_system(), search="gemma-4-12B-it-qat", limit=20)}
|
||||
for row in _qat_rows(publication_catalog):
|
||||
assert ranked[row["name"]]["gguf_sources"] == []
|
||||
|
||||
@@ -3,8 +3,33 @@
|
||||
The handler did `n = int(gpu_count)` with no guard, so `?gpu_count=abc` (or any
|
||||
non-integer) raised ValueError -> HTTP 500. A malformed count is now ignored,
|
||||
matching how the neighbouring gpu_group param is already parsed.
|
||||
|
||||
The count parsing runs only after the empty-catalog early return, so these
|
||||
tests rank the authored publication catalog against a fixed two-GPU system and
|
||||
spy on rank_models to prove the parsing path actually ran.
|
||||
"""
|
||||
import functools
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from routes.hwfit_routes import setup_hwfit_routes
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
SYSTEM = {
|
||||
"has_gpu": True,
|
||||
"backend": "cuda",
|
||||
"gpu_name": "Synthetic GPU",
|
||||
"gpu_vram_gb": 24.0,
|
||||
"gpu_count": 2,
|
||||
"gpus": [
|
||||
{"index": 0, "name": "Synthetic GPU", "vram_gb": 12.0},
|
||||
{"index": 1, "name": "Synthetic GPU", "vram_gb": 12.0},
|
||||
],
|
||||
"gpu_groups": [{"name": "Synthetic GPU", "vram_each": 12.0, "count": 2, "indices": [0, 1], "vram_total": 24.0}],
|
||||
"available_ram_gb": 32.0,
|
||||
"total_ram_gb": 32.0,
|
||||
}
|
||||
|
||||
|
||||
def _get_models():
|
||||
@@ -15,24 +40,80 @@ def _get_models():
|
||||
raise AssertionError("hwfit /models route not found")
|
||||
|
||||
|
||||
def test_non_numeric_gpu_count_does_not_raise():
|
||||
@pytest.fixture
|
||||
def ranked_systems(publication_catalog, monkeypatch):
|
||||
"""Fix detection and record every system the handler hands to rank_models."""
|
||||
from services.hwfit import fit, hardware
|
||||
|
||||
monkeypatch.setattr(hardware, "detect_system", lambda **_: deepcopy(SYSTEM))
|
||||
calls = []
|
||||
real_rank = fit.rank_models
|
||||
|
||||
@functools.wraps(real_rank)
|
||||
def spy(system, **kwargs):
|
||||
calls.append(deepcopy(system))
|
||||
return real_rank(system, **kwargs)
|
||||
|
||||
monkeypatch.setattr(fit, "rank_models", spy)
|
||||
return calls
|
||||
|
||||
|
||||
def _assert_ranked(result, ranked_systems):
|
||||
assert isinstance(result, dict)
|
||||
assert "error" not in result
|
||||
assert isinstance(result["models"], list) and result["models"]
|
||||
# Reaching rank_models means the empty-catalog return was not taken and the
|
||||
# manual-hardware and gpu_count parsing ran first.
|
||||
assert len(ranked_systems) == 1
|
||||
assert ranked_systems[0] == result["system"]
|
||||
return result["system"]
|
||||
|
||||
|
||||
def test_non_numeric_gpu_count_does_not_raise(ranked_systems):
|
||||
handler = _get_models()
|
||||
# Previously raised ValueError (HTTP 500); now degrades to a normal ranking.
|
||||
result = handler(gpu_count="abc")
|
||||
assert isinstance(result, dict)
|
||||
system = _assert_ranked(handler(gpu_count="abc"), ranked_systems)
|
||||
# Ignored like an omitted count: the auto pool keeps both GPUs and no
|
||||
# explicit-count GPU-only pin is applied.
|
||||
assert system["detected_gpu_count"] == 2
|
||||
assert system["active_group"]["use_count"] == 2
|
||||
assert system["gpu_count"] == 2
|
||||
assert "gpu_only" not in system
|
||||
|
||||
|
||||
def test_numeric_gpu_count_still_accepted():
|
||||
def test_numeric_gpu_count_still_accepted(ranked_systems):
|
||||
handler = _get_models()
|
||||
result = handler(gpu_count="0")
|
||||
assert isinstance(result, dict)
|
||||
system = _assert_ranked(handler(gpu_count="0"), ranked_systems)
|
||||
# 0 switches to RAM-only ranking.
|
||||
assert system["detected_gpu_count"] == 2
|
||||
assert system["has_gpu"] is False
|
||||
assert system["gpu_count"] == 0
|
||||
assert system["gpu_only"] is False
|
||||
assert "active_group" not in system
|
||||
|
||||
|
||||
def test_non_numeric_manual_gpu_count_does_not_raise():
|
||||
def test_non_numeric_manual_gpu_count_does_not_raise(ranked_systems):
|
||||
# manual_gpu_count is the other count param on this endpoint (the hardware
|
||||
# simulator in _apply_manual_hardware). A non-numeric value must also degrade
|
||||
# (default to 1) rather than 500, so the endpoint's count parsing is fully
|
||||
# covered.
|
||||
handler = _get_models()
|
||||
result = handler(manual_mode="gpu", manual_gpu_count="abc")
|
||||
assert isinstance(result, dict)
|
||||
system = _assert_ranked(handler(manual_mode="gpu", manual_gpu_count="abc"), ranked_systems)
|
||||
assert system["manual_hardware"] is True
|
||||
assert system["gpu_name"] == "Simulated CUDA GPU"
|
||||
assert system["gpu_count"] == 1
|
||||
assert system["gpu_groups"][0]["count"] == 1
|
||||
assert system["detected_gpu_count"] == 1
|
||||
|
||||
|
||||
def test_empty_catalog_returns_before_count_parsing(ranked_systems, monkeypatch):
|
||||
# Control: without catalog rows the handler stops before the parsing above,
|
||||
# which is why the tests in this module must rank the authored catalog.
|
||||
from services.hwfit import models
|
||||
|
||||
monkeypatch.setattr(models, "get_models", lambda: [])
|
||||
result = _get_models()(gpu_count="abc", manual_mode="gpu", manual_gpu_count="abc")
|
||||
assert result["models"] == []
|
||||
assert "Model catalog is empty" in result["error"]
|
||||
assert ranked_systems == []
|
||||
assert "detected_gpu_count" not in result["system"]
|
||||
|
||||
+17
-10
@@ -6,9 +6,15 @@ guarantee that non-macOS (Linux/Windows) detection is unchanged.
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from services.hwfit import hardware
|
||||
from services.hwfit.fit import rank_models
|
||||
from services.hwfit.models import get_models
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
# Rank authored inputs rather than publication catalog snapshots.
|
||||
pytestmark = pytest.mark.usefixtures("publication_catalog")
|
||||
|
||||
|
||||
def _metal_system(ram_gb=16.0, vram_gb=10.7):
|
||||
@@ -78,19 +84,20 @@ def test_only_gguf_or_mlx_models_recommended_on_metal():
|
||||
assert unservable == [], f"{len(unservable)} non-servable models on Metal, e.g. {unservable[:3]}"
|
||||
|
||||
|
||||
def test_qwen_catalog_entries_point_at_verified_gguf_repos():
|
||||
"""Qwen GGUF-looking Cookbook rows must download GGUF repos, not the base
|
||||
safetensors repositories."""
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
def test_qwen_gguf_rows_rank_with_their_gguf_downloads_on_metal(publication_catalog):
|
||||
"""Qwen GGUF-looking Cookbook rows must download the GGUF repo/file their
|
||||
catalog row names, not the base safetensors repository."""
|
||||
expected = {
|
||||
"Qwen/Qwen3.5-9B": ("unsloth/Qwen3.5-9B-GGUF", "Qwen3.5-9B-Q4_K_M.gguf"),
|
||||
"Qwen/Qwen3.6-27B": ("unsloth/Qwen3.6-27B-GGUF", "Qwen3.6-27B-Q4_K_M.gguf"),
|
||||
"Qwen/Qwen3.6-35B-A3B": ("unsloth/Qwen3.6-35B-A3B-GGUF", "Qwen3.6-35B-A3B-UD-Q4_K_M.gguf"),
|
||||
r["name"]: r["gguf_sources"]
|
||||
for r in publication_catalog
|
||||
if r["name"].startswith("Qwen/Qwen3.") and r["gguf_sources"]
|
||||
}
|
||||
assert len(expected) == 3
|
||||
ranked = {r["name"]: r for r in rank_models(_metal_system(), search="Qwen/Qwen3.", limit=50)}
|
||||
|
||||
for model_name, (repo, filename) in expected.items():
|
||||
sources = catalog[model_name].get("gguf_sources") or []
|
||||
assert any(src.get("repo") == repo and src.get("file") == filename for src in sources)
|
||||
for model_name, sources in expected.items():
|
||||
assert ranked[model_name]["gguf_sources"] == sources
|
||||
assert all(src["repo"] != model_name and src["file"].endswith(".gguf") for src in ranked[model_name]["gguf_sources"])
|
||||
|
||||
|
||||
def test_safetensors_models_still_recommended_on_cuda():
|
||||
|
||||
@@ -7,9 +7,15 @@ notably that "metal" is honoured (Apple Silicon is GGUF-only via llama.cpp /
|
||||
Ollama) instead of being silently coerced to CUDA.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from routes.hwfit_routes import _apply_manual_hardware, _MANUAL_BACKENDS
|
||||
from services.hwfit.fit import rank_models
|
||||
from services.hwfit.models import get_models
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
# Rank authored inputs rather than publication catalog snapshots.
|
||||
pytestmark = pytest.mark.usefixtures("publication_catalog")
|
||||
|
||||
|
||||
def test_no_manual_mode_leaves_system_untouched():
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import pytest
|
||||
|
||||
from services.hwfit.fit import analyze_model, rank_models
|
||||
from services.hwfit.models import (
|
||||
get_models,
|
||||
infer_quantization_from_name,
|
||||
is_prequantized,
|
||||
)
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
# Rank authored inputs rather than publication catalog snapshots.
|
||||
pytestmark = pytest.mark.usefixtures("publication_catalog")
|
||||
|
||||
|
||||
def _dual_5060ti_system():
|
||||
@@ -35,7 +41,7 @@ def test_infers_native_hf_quant_formats_from_repo_names():
|
||||
|
||||
def test_nvfp4_catalog_quant_is_preserved():
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
model = catalog["txn545/Qwen3.5-122B-A10B-NVFP4"]
|
||||
model = catalog["test/native-NVFP4"]
|
||||
|
||||
assert model["quantization"] == "NVFP4"
|
||||
assert is_prequantized(model)
|
||||
@@ -43,7 +49,7 @@ def test_nvfp4_catalog_quant_is_preserved():
|
||||
|
||||
def test_nvfp4_search_result_is_not_gguf_or_cpu_offload():
|
||||
catalog = {m["name"]: m for m in get_models()}
|
||||
model = catalog["txn545/Qwen3.5-122B-A10B-NVFP4"]
|
||||
model = catalog["test/native-NVFP4"]
|
||||
|
||||
fit = analyze_model(model, _dual_5060ti_system())
|
||||
assert fit["quant"] == "NVFP4"
|
||||
@@ -51,10 +57,10 @@ def test_nvfp4_search_result_is_not_gguf_or_cpu_offload():
|
||||
|
||||
results = rank_models(
|
||||
_dual_5060ti_system(),
|
||||
search="Qwen3.5-122B-A10B-NVFP4",
|
||||
search="native-NVFP4",
|
||||
limit=10,
|
||||
)
|
||||
hit = next(r for r in results if r["name"] == "txn545/Qwen3.5-122B-A10B-NVFP4")
|
||||
hit = next(r for r in results if r["name"] == "test/native-NVFP4")
|
||||
assert hit["quant"] == "NVFP4"
|
||||
assert hit["run_mode"] != "cpu_offload"
|
||||
|
||||
|
||||
@@ -6,8 +6,14 @@ FP8 safetensors repos — must be filtered out on Windows so the Cookbook does
|
||||
not recommend models the user cannot actually serve.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from services.hwfit.fit import rank_models
|
||||
from services.hwfit.models import get_models
|
||||
from tests.hwfit_publication_fixtures import publication_catalog # noqa: F401
|
||||
|
||||
# Rank authored inputs rather than publication catalog snapshots.
|
||||
pytestmark = pytest.mark.usefixtures("publication_catalog")
|
||||
|
||||
|
||||
def _windows_system(ram_gb=32.0, vram_gb=16.0):
|
||||
|
||||
@@ -368,7 +368,7 @@ def test_detached_container_math_typesets_with_the_real_renderer(node_available)
|
||||
|
||||
mdToHtml defers math to a document-scoped flush, which cannot reach a
|
||||
detached node, so the export has to typeset its own container before
|
||||
handing it to html2pdf. This is that container: pending spans in, real
|
||||
handing it to browser printing. This is that container: pending spans in, real
|
||||
KaTeX markup out, no .katex-error and nothing left pending.
|
||||
"""
|
||||
out = _run_node(
|
||||
@@ -400,7 +400,7 @@ def test_detached_container_math_typesets_with_the_real_renderer(node_available)
|
||||
assert "ody-math-pending" not in out["written"]
|
||||
|
||||
|
||||
def test_pdf_export_typesets_its_container_before_html2pdf():
|
||||
def test_pdf_export_typesets_its_container_before_print():
|
||||
"""Ordering in a call site, so pin the call site. No node needed."""
|
||||
source = document_source()
|
||||
match = re.search(r"\n async function exportAsPdf\(\) \{(.*?)\n \}\n", source, re.S)
|
||||
@@ -410,7 +410,7 @@ def test_pdf_export_typesets_its_container_before_html2pdf():
|
||||
render = "await markdownModule.renderMath(container);"
|
||||
assert render in body, "the export never typesets its detached container"
|
||||
assert body.index("container.innerHTML = html;") < body.index(render)
|
||||
assert body.index(render) < body.index("window.html2pdf()")
|
||||
assert body.index(render) < body.index("frame.contentWindow.print()")
|
||||
|
||||
|
||||
def test_md_to_html_renders_inline_once_katex_is_loaded(node_available):
|
||||
|
||||
@@ -8,6 +8,6 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
def test_pdf_backed_documents_do_not_offer_destructive_html_pdf_export():
|
||||
source = document_source()
|
||||
|
||||
assert "if (!isForm) {" in source
|
||||
assert "label: _isDocxLang(lang) ? 'Convert to PDF' : 'Print as PDF'" in source
|
||||
assert "if (!isForm && (_isDocxLang(lang) || typeof window.print === 'function')) {" in source
|
||||
assert "label: _isDocxLang(lang) ? 'Convert to PDF' : 'Print / save PDF'" in source
|
||||
assert "destroy the original page layout, images, and form structure" in source
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""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']
|
||||
@@ -1,11 +1,8 @@
|
||||
"""Regression guard for the README title presentation.
|
||||
|
||||
Originally (#1390) the README opened with an ASCII-art banner that had to live
|
||||
inside a ``` code fence, otherwise GitHub's markdown collapsed its leading
|
||||
whitespace and box-drawing rules and rendered it misaligned. The README refresh
|
||||
(#4306) dropped that banner in favour of a centered wordmark image, so the guard
|
||||
now pins the wordmark identity instead, while still catching the original failure
|
||||
mode if an un-fenced ASCII banner is ever reintroduced.
|
||||
The original ASCII-art banner needed a code fence to preserve whitespace.
|
||||
Publication now opens with a centered text heading after omitting the wordmark
|
||||
artwork. Keep the title identity and the historical ASCII-fencing guard.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,11 +19,9 @@ def _fenced_segments(text: str):
|
||||
return parts[1::2]
|
||||
|
||||
|
||||
def test_readme_opens_with_wordmark_title():
|
||||
# The README must still open with a recognizable Odysseus title: now the
|
||||
# centered wordmark image rather than an H1 / ASCII banner.
|
||||
def test_readme_opens_with_text_title():
|
||||
head = "\n".join(README.read_text(encoding="utf-8").splitlines()[:15])
|
||||
assert 'alt="Odysseus"' in head, "README must open with the Odysseus wordmark image"
|
||||
assert head.startswith('<h1 align="center">Odysseus</h1>'), "README must open with the Odysseus text title"
|
||||
|
||||
|
||||
def test_reintroduced_ascii_banner_stays_fenced():
|
||||
|
||||
Reference in New Issue
Block a user