mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-14 12:48:03 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df9c20e6c2 | |||
| bbbe145247 | |||
| 387f95187e | |||
| 00dfd2d47a | |||
| d2a6d73aa5 | |||
| 139d76ab57 | |||
| 3021569081 | |||
| a326a6a555 | |||
| dff79319d7 | |||
| 9731048ecd |
@@ -34,6 +34,8 @@ def atomic_write_json(path: str, data: Any, *, indent: Optional[int] = None) ->
|
||||
|
||||
|
||||
def atomic_write_text(path: str, text: str) -> None:
|
||||
if not isinstance(text, str):
|
||||
raise TypeError("atomic_write_text expects a string")
|
||||
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
||||
tmp = f"{path}.tmp.{os.getpid()}"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
|
||||
+15
-11
@@ -38,23 +38,27 @@ def _preview_text(value, limit: int = 200) -> str:
|
||||
return text[:limit]
|
||||
|
||||
|
||||
def _text_field(value) -> str:
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _serialize_image(i: "GalleryImage") -> dict:
|
||||
return {
|
||||
"id": i.id,
|
||||
"filename": i.filename,
|
||||
"filename": _text_field(i.filename),
|
||||
"prompt": _preview_text(i.prompt),
|
||||
"model": i.model or "",
|
||||
"size": i.size or "",
|
||||
"tags": i.tags or "",
|
||||
"model": _text_field(i.model),
|
||||
"size": _text_field(i.size),
|
||||
"tags": _text_field(i.tags),
|
||||
"favorite": bool(i.favorite),
|
||||
"album_id": i.album_id or "",
|
||||
"session_id": i.session_id or "",
|
||||
"album_id": _text_field(i.album_id),
|
||||
"session_id": _text_field(i.session_id),
|
||||
"width": i.width,
|
||||
"height": i.height,
|
||||
"file_size": i.file_size,
|
||||
"taken_at": i.taken_at.isoformat() if i.taken_at else "",
|
||||
"camera_make": i.camera_make or "",
|
||||
"camera_model": i.camera_model or "",
|
||||
"camera_make": _text_field(i.camera_make),
|
||||
"camera_model": _text_field(i.camera_model),
|
||||
"created_at": i.created_at.isoformat() if i.created_at else "",
|
||||
}
|
||||
|
||||
@@ -93,11 +97,11 @@ def cmd_show(args):
|
||||
if not i:
|
||||
fail(f"no image with id {args.id!r}")
|
||||
out = _serialize_image(i)
|
||||
out["prompt_full"] = i.prompt or ""
|
||||
out["ai_tags"] = i.ai_tags or ""
|
||||
out["prompt_full"] = _text_field(i.prompt)
|
||||
out["ai_tags"] = _text_field(i.ai_tags)
|
||||
out["gps_lat"] = i.gps_lat or ""
|
||||
out["gps_lng"] = i.gps_lng or ""
|
||||
out["file_hash"] = i.file_hash or ""
|
||||
out["file_hash"] = _text_field(i.file_hash)
|
||||
emit(out, args)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -108,6 +108,8 @@ def _q(name: str) -> str:
|
||||
|
||||
|
||||
def _split_recipients(value: str) -> list[str]:
|
||||
if not isinstance(value, str):
|
||||
return []
|
||||
return [r.strip() for r in (value or "").split(",") if r.strip()]
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ def _load_items(raw) -> list:
|
||||
items = json.loads(raw)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return []
|
||||
return items if isinstance(items, list) else []
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
return [item for item in items if isinstance(item, dict)]
|
||||
|
||||
|
||||
def _serialize(n: "Note") -> dict:
|
||||
|
||||
@@ -103,6 +103,9 @@ def compute_serve_profiles(system, model, serve_weights_gb=None, serve_quant=Non
|
||||
in the actual serving knobs (n_cpu_moe, KV-cache type, context). serve_quant
|
||||
is the file's quant label (e.g. "Q4_K_M") just for display.
|
||||
"""
|
||||
if not isinstance(system, dict) or not isinstance(model, dict):
|
||||
return []
|
||||
|
||||
vram = float(system.get("gpu_vram_gb") or 0)
|
||||
if vram <= 0:
|
||||
return []
|
||||
|
||||
@@ -18,6 +18,13 @@ DEFAULT_BUDGET = 6000
|
||||
DEFAULT_HEADROOM = 0.85
|
||||
|
||||
|
||||
def _int_or_zero(value) -> int:
|
||||
try:
|
||||
return int(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def compute_input_token_budget(
|
||||
configured: int,
|
||||
context_length: int,
|
||||
@@ -48,8 +55,8 @@ def compute_input_token_budget(
|
||||
- When the window is unknown (context_length <= 0), use the conservative
|
||||
``default`` budget and do NOT scale off the fallback.
|
||||
"""
|
||||
configured = int(configured or 0)
|
||||
context_length = int(context_length or 0)
|
||||
configured = _int_or_zero(configured)
|
||||
context_length = _int_or_zero(context_length)
|
||||
|
||||
if explicit and configured > 0:
|
||||
return min(configured, context_length) if context_length > 0 else configured
|
||||
|
||||
@@ -68,6 +68,8 @@ def read_text_file(path: str) -> str:
|
||||
|
||||
def split_chunks(text: str, size: int = config.CHUNK_SIZE, overlap: int = config.CHUNK_OVERLAP) -> List[str]:
|
||||
"""Split text into overlapping chunks."""
|
||||
if not isinstance(text, str):
|
||||
return []
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return []
|
||||
@@ -87,7 +89,8 @@ def split_chunks(text: str, size: int = config.CHUNK_SIZE, overlap: int = config
|
||||
|
||||
def tokenize(s: str) -> Set[str]:
|
||||
"""Tokenize string into words, excluding stop words."""
|
||||
tokens = re.findall(r"[A-Za-z0-9_\-]+", (s or "").lower())
|
||||
text = s if isinstance(s, str) else ""
|
||||
tokens = re.findall(r"[A-Za-z0-9_\-]+", text.lower())
|
||||
return set(t for t in tokens if t not in config.STOP_WORDS and len(t) > 1)
|
||||
|
||||
def load_personal_index(
|
||||
|
||||
@@ -79,12 +79,18 @@ def check_outbound_url(
|
||||
if not raw_ips:
|
||||
return False, "host does not resolve"
|
||||
|
||||
saw_ip = False
|
||||
for raw in raw_ips:
|
||||
if not isinstance(raw, str):
|
||||
continue
|
||||
try:
|
||||
ip = ipaddress.ip_address(raw.split("%")[0]) # strip IPv6 zone id
|
||||
except ValueError:
|
||||
continue
|
||||
saw_ip = True
|
||||
reason = _classify(ip, block_private=block_private)
|
||||
if reason:
|
||||
return False, reason
|
||||
if not saw_ip:
|
||||
return False, "host does not resolve to an IP"
|
||||
return True, "ok"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
* @returns {HTMLCanvasElement|null}
|
||||
*/
|
||||
export function layerUnionAlpha(w, h, layers) {
|
||||
if (!Array.isArray(layers)) return null;
|
||||
const visible = layers.filter(l => l.visible);
|
||||
if (visible.length < 2) return null;
|
||||
const bgId = visible[0].id;
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
* @returns {{x: number, y: number, guides: Array}}
|
||||
*/
|
||||
export function computeSnap(layer, nx, ny, ctx) {
|
||||
const SNAP_PX = 6 / Math.max(ctx.zoom, 0.0001);
|
||||
const cw = ctx.canvasW, ch = ctx.canvasH;
|
||||
if (!layer || !layer.canvas || !ctx) return { x: nx, y: ny, guides: [] };
|
||||
const zoom = Number.isFinite(Number(ctx.zoom)) ? Number(ctx.zoom) : 1;
|
||||
const SNAP_PX = 6 / Math.max(zoom, 0.0001);
|
||||
const cw = Number(ctx.canvasW) || 0, ch = Number(ctx.canvasH) || 0;
|
||||
const w = layer.canvas.width, h = layer.canvas.height;
|
||||
|
||||
const vTargets = [
|
||||
|
||||
@@ -15,16 +15,21 @@ def test_preview_text_ignores_non_string(monkeypatch):
|
||||
assert cli._preview_text(None) == ""
|
||||
assert cli._preview_text(123) == ""
|
||||
assert cli._preview_text("p" * 250) == "p" * 200
|
||||
assert cli._text_field("ok") == "ok"
|
||||
assert cli._text_field(123) == ""
|
||||
|
||||
|
||||
def test_serialize_image_does_not_crash_on_non_string_prompt(monkeypatch):
|
||||
make_core_db_stub(monkeypatch, models=["GalleryImage", "GalleryAlbum"])
|
||||
cli = load_script("odysseus-gallery")
|
||||
img = SimpleNamespace(
|
||||
id="i1", filename="a.png", prompt=123, model=None, size=None, tags=None,
|
||||
id="i1", filename=123, prompt=123, model=123, size=None, tags=["bad"],
|
||||
favorite=0, album_id=None, session_id=None, width=1, height=1, file_size=1,
|
||||
taken_at=None, camera_make=None, camera_model=None, created_at=None,
|
||||
taken_at=None, camera_make=123, camera_model=None, created_at=None,
|
||||
)
|
||||
out = cli._serialize_image(img)
|
||||
assert out["prompt"] == ""
|
||||
assert out["filename"] == ""
|
||||
assert out["model"] == ""
|
||||
assert out["tags"] == ""
|
||||
assert out["id"] == "i1"
|
||||
|
||||
@@ -48,3 +48,10 @@ def test_recipient_list_rejects_empty_envelope(monkeypatch):
|
||||
assert exc.code == 1
|
||||
else:
|
||||
raise AssertionError("expected empty recipient list to exit")
|
||||
|
||||
|
||||
def test_split_recipients_ignores_non_string_values(monkeypatch):
|
||||
cli = _load_mail_cli(monkeypatch)
|
||||
|
||||
assert cli._split_recipients(None) == []
|
||||
assert cli._split_recipients(["a@example.test"]) == []
|
||||
|
||||
@@ -46,3 +46,25 @@ def test_serialize_keeps_list_note_items(monkeypatch):
|
||||
)
|
||||
|
||||
assert cli._serialize(note)["items"] == [{"text": "done"}]
|
||||
|
||||
|
||||
def test_serialize_skips_invalid_note_item_rows(monkeypatch):
|
||||
make_core_db_stub(monkeypatch, models=["Note"])
|
||||
cli = load_script("odysseus-notes")
|
||||
note = SimpleNamespace(
|
||||
id="n1",
|
||||
title="Checklist",
|
||||
content="",
|
||||
items='[{"text": "done"}, "bad", null, 3]',
|
||||
note_type="checklist",
|
||||
color=None,
|
||||
label=None,
|
||||
pinned=False,
|
||||
archived=False,
|
||||
due_date=None,
|
||||
source=None,
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
assert cli._serialize(note)["items"] == [{"text": "done"}]
|
||||
|
||||
@@ -138,6 +138,16 @@ def test_atomic_write_text_leaves_no_tmp_file(tmp_path):
|
||||
assert _tmp_siblings(tmp_path, "note.txt") == []
|
||||
|
||||
|
||||
def test_atomic_write_text_rejects_non_string_before_tmp_file(tmp_path):
|
||||
target = tmp_path / "note.txt"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
atomic_write_text(str(target), 123)
|
||||
|
||||
assert not target.exists()
|
||||
assert _tmp_siblings(tmp_path, "note.txt") == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# atomic_write_text — failure path: target preserved when replace fails.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -33,6 +33,11 @@ def test_unknown_window_falls_back_to_configured():
|
||||
assert compute_input_token_budget(0, 0, explicit=False) == 6000 # default
|
||||
|
||||
|
||||
def test_invalid_numeric_inputs_fall_back_cleanly():
|
||||
assert compute_input_token_budget("bad", "also bad", explicit=False) == 6000
|
||||
assert compute_input_token_budget("bad", 128000, explicit=True) == int(128000 * 0.85)
|
||||
|
||||
|
||||
def test_is_setting_overridden_reads_raw_saved_file(tmp_path, monkeypatch):
|
||||
import src.settings as settings
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Pin harmonize mask helpers against invalid layer lists.
|
||||
|
||||
Driven through `node --input-type=module`; skips without node.
|
||||
"""
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_REPO = Path(__file__).resolve().parent.parent
|
||||
_HELPER = _REPO / "static" / "js" / "editor" / "harmonize-masks.js"
|
||||
_HAS_NODE = shutil.which("node") is not None
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
|
||||
def test_layer_union_alpha_returns_null_for_non_array_layers():
|
||||
js = f"""
|
||||
import {{ layerUnionAlpha, seamMask, layerBodyMask }} from '{_HELPER.as_posix()}';
|
||||
console.log(JSON.stringify([
|
||||
layerUnionAlpha(10, 10, null),
|
||||
seamMask(10, 10, {{"bad": true}}),
|
||||
layerBodyMask(10, 10, "bad")
|
||||
]));
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
["node", "--input-type=module"],
|
||||
input=js, capture_output=True, text=True, cwd=str(_REPO), timeout=30,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert json.loads(proc.stdout.strip()) == [None, None, None]
|
||||
@@ -1,4 +1,4 @@
|
||||
from src.personal_docs import retrieve_personal_keyword
|
||||
from src.personal_docs import retrieve_personal_keyword, split_chunks
|
||||
|
||||
|
||||
def test_retrieve_personal_keyword_skips_non_dict_rows():
|
||||
@@ -19,3 +19,17 @@ def test_retrieve_personal_keyword_tolerates_missing_chunks_key():
|
||||
index = [{"name": "empty.txt"}, {"name": "doc.txt", "chunks": ["alpha beta gamma"]}]
|
||||
out = retrieve_personal_keyword(index, "beta", k=5)
|
||||
assert out == ["[doc.txt :: chunk 1]\nalpha beta gamma"]
|
||||
|
||||
|
||||
def test_retrieve_personal_keyword_ignores_non_string_text():
|
||||
index = [{"name": "doc.txt", "chunks": [None, ["beta"], "alpha beta gamma"]}]
|
||||
|
||||
assert retrieve_personal_keyword(index, ["beta"], k=5) == []
|
||||
assert retrieve_personal_keyword(index, "beta", k=5) == [
|
||||
"[doc.txt :: chunk 3]\nalpha beta gamma"
|
||||
]
|
||||
|
||||
|
||||
def test_split_chunks_ignores_non_string_text():
|
||||
assert split_chunks(None, size=1000, overlap=200) == []
|
||||
assert split_chunks(["hello"], size=1000, overlap=200) == []
|
||||
|
||||
@@ -28,6 +28,12 @@ def _sys(vram, family="rdna"):
|
||||
return {"backend": "rocm", "gpu_vram_gb": vram, "gpu_family": family}
|
||||
|
||||
|
||||
def test_compute_serve_profiles_ignores_invalid_inputs():
|
||||
assert compute_serve_profiles(None, _DENSE_8B) == []
|
||||
assert compute_serve_profiles(_sys(8), None) == []
|
||||
assert compute_serve_profiles(["bad"], _DENSE_8B) == []
|
||||
|
||||
|
||||
def test_big_moe_on_small_card_offloads_not_fails():
|
||||
"""A 35B MoE can't hold its weights on 16 GB, so the Quality profile must
|
||||
offload experts to CPU (n_cpu_moe > 0) rather than be dropped."""
|
||||
|
||||
@@ -36,6 +36,28 @@ def test_compute_snap_tolerates_non_array_other_layers():
|
||||
assert r["x"] == 10 and r["y"] == 10 and r["guides"] == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
|
||||
def test_compute_snap_tolerates_missing_layer_or_context():
|
||||
js = f"""
|
||||
import {{ computeSnap }} from '{_HELPER.as_posix()}';
|
||||
console.log(JSON.stringify([
|
||||
computeSnap(null, 10, 20, {{ zoom: 1, canvasW: 800, canvasH: 600 }}),
|
||||
computeSnap({{ id: 'L1' }}, 11, 21, {{ zoom: 1, canvasW: 800, canvasH: 600 }}),
|
||||
computeSnap({{ id: 'L1', canvas: {{ width: 100, height: 50 }} }}, 12, 22, null)
|
||||
]));
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
["node", "--input-type=module"],
|
||||
input=js, capture_output=True, text=True, cwd=str(_REPO), timeout=30,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
assert json.loads(proc.stdout.strip()) == [
|
||||
{"x": 10, "y": 20, "guides": []},
|
||||
{"x": 11, "y": 21, "guides": []},
|
||||
{"x": 12, "y": 22, "guides": []},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
|
||||
def test_compute_snap_still_snaps_to_a_layer_edge():
|
||||
other = [{"id": "L2", "visible": True, "offset": {"x": 12, "y": 300},
|
||||
|
||||
@@ -68,3 +68,23 @@ def test_unresolvable_host_blocked():
|
||||
ok, reason = check_outbound_url("http://does-not-resolve.invalid", resolver=PUBLIC)
|
||||
assert ok is False
|
||||
assert "resolve" in reason
|
||||
|
||||
|
||||
def test_resolver_values_must_include_a_parseable_ip():
|
||||
ok, reason = check_outbound_url(
|
||||
"https://example.test",
|
||||
resolver=lambda _host: [None, 123, "not-an-ip"],
|
||||
)
|
||||
|
||||
assert ok is False
|
||||
assert "does not resolve to an IP" in reason
|
||||
|
||||
|
||||
def test_resolver_skips_invalid_values_but_accepts_public_ip():
|
||||
ok, reason = check_outbound_url(
|
||||
"https://example.test",
|
||||
resolver=lambda _host: [None, "not-an-ip", "93.184.216.34"],
|
||||
)
|
||||
|
||||
assert ok is True
|
||||
assert reason == "ok"
|
||||
|
||||
Reference in New Issue
Block a user