Files
jobtrackingapp/tools/summarizer/tests/test_app.py
T
cesnimda e508fb9541
CI and Deploy / test (push) Failing after 52s
CI and Deploy / deploy (push) Has been skipped
test(parser): treat reaped container zombies as terminated
2026-08-31 22:14:37 +02:00

801 lines
30 KiB
Python

import importlib
import io
import json
import os
import subprocess
import sys
import textwrap
import zipfile
from pathlib import Path
from docx import Document
from fastapi.testclient import TestClient
from PIL import Image
from pypdf import PdfWriter
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
def load_app_module(
monkeypatch,
*,
skip_model_load=True,
ollama_model=None,
service_token=None,
external_ai_enabled=False,
routing_mode="local_first",
circuit_threshold=3,
external_prompt_limit=24000,
):
if skip_model_load:
monkeypatch.setenv("AI_SERVICE_SKIP_MODEL_LOAD", "1")
else:
monkeypatch.delenv("AI_SERVICE_SKIP_MODEL_LOAD", raising=False)
monkeypatch.delenv("AI_SERVICE_EAGER_MODEL_LOAD", raising=False)
# Default to keyless so the existing suite is unaffected by a token in the dev shell.
if service_token is None:
monkeypatch.delenv("AI_SERVICE_TOKEN", raising=False)
else:
monkeypatch.setenv("AI_SERVICE_TOKEN", service_token)
if ollama_model is None:
monkeypatch.delenv("OLLAMA_MODEL", raising=False)
else:
monkeypatch.setenv("OLLAMA_MODEL", ollama_model)
if external_ai_enabled:
monkeypatch.setenv("EXTERNAL_AI_ENABLED", "true")
else:
monkeypatch.delenv("EXTERNAL_AI_ENABLED", raising=False)
monkeypatch.setenv("AI_ROUTING_MODE", routing_mode)
monkeypatch.setenv("LOCAL_AI_CIRCUIT_FAILURE_THRESHOLD", str(circuit_threshold))
monkeypatch.setenv("EXTERNAL_AI_MAX_PROMPT_CHARS", str(external_prompt_limit))
monkeypatch.delenv("EXTERNAL_AI_ALLOWED_TASKS", raising=False)
if "app" in sys.modules:
del sys.modules["app"]
module = importlib.import_module("app")
return importlib.reload(module)
def test_health_reports_runtime_without_ollama_and_without_forcing_model_load(monkeypatch):
module = load_app_module(monkeypatch)
client = TestClient(module.app)
response = client.get("/health")
assert response.status_code == 200
payload = response.json()
assert payload["ok"] is True
assert payload["device"] == "cpu"
assert payload["model_loaded"] is False
assert payload["model_disabled"] is True
assert payload["summarize_available"] is False
assert payload["parser_isolated"] is True
assert payload["parser_concurrency"] == 1
assert payload["parser_timeout_seconds"] == 25
assert payload["parser_memory_mib"] == 512
assert "disabled" in payload["model_load_error"].lower()
assert payload["ollama_configured"] is False
assert payload["ollama_model"] is None
assert payload["ollama_installed_models"] == []
assert payload["ollama_loaded_models"] == []
def test_summarize_returns_503_with_explicit_reason_when_model_loading_is_disabled(monkeypatch):
module = load_app_module(monkeypatch)
client = TestClient(module.app)
response = client.post("/summarize", json={"text": "Platform engineering role with APIs and Python experience."})
assert response.status_code == 503
payload = response.json()
assert "disabled" in payload["detail"].lower()
def test_health_reports_ollama_unreachable_when_configured_but_not_available(monkeypatch):
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b")
def boom(path: str):
raise OSError("connection refused")
monkeypatch.setattr(module, "_ollama_json", boom)
client = TestClient(module.app)
response = client.get("/health")
assert response.status_code == 200
payload = response.json()
assert payload["ollama_configured"] is True
assert payload["ollama_reachable"] is False
assert payload["ollama_model"] == "qwen2.5:7b"
assert payload["ollama_model_available"] is False
def test_rewrite_cv_returns_plain_rewritten_text(monkeypatch):
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b")
monkeypatch.setattr(module, "_ollama_generate_text", lambda prompt: "# Professional Summary\nBuilt resilient backend systems.\n\n# Skills\n- C#\n- .NET")
client = TestClient(module.app)
response = client.post("/cv/rewrite", json={
"instruction": "Rewrite this CV into a cleaner master CV.",
"text": "Professional Summary\nBuilt backend systems.",
"max_length": 220,
"min_length": 80,
})
assert response.status_code == 200
payload = response.json()
assert payload["rewritten_text"].startswith("# Professional Summary")
assert "Role summary:" not in payload["rewritten_text"]
def test_classify_block_returns_structured_json(monkeypatch):
module = load_app_module(monkeypatch)
def fake_generate_json(prompt: str):
assert "Senior Platform Engineer" in prompt
return {
"section": "Work Experience",
"confidence": 0.91,
"reason": "job block",
"title": "Senior Platform Engineer",
"company": "Atlas Systems",
"location": "Oslo",
"start": "2019",
"end": "Present",
"bullets": ["Built event-driven APIs and migration tooling."],
"summary": [],
"skills": ["Python", "SQL"],
}
monkeypatch.setattr(module, "_ollama_generate_json", fake_generate_json)
client = TestClient(module.app)
response = client.post("/cv/classify-block", json={"block": "Senior Platform Engineer at Atlas Systems, Oslo, 2019 - Present. Built event-driven APIs and migration tooling."})
assert response.status_code == 200
payload = response.json()
assert payload["section"] == "Work Experience"
assert payload["title"] == "Senior Platform Engineer"
assert payload["company"] == "Atlas Systems"
assert payload["bullets"] == ["Built event-driven APIs and migration tooling."]
assert payload["summary"] == []
assert payload["skills"] == ["Python", "SQL"]
def test_classify_block_supports_projects_section(monkeypatch):
# Phase 2.1-b: Projects and Certifications are now valid classified sections so project blocks
# (e.g. the benchmark CV's JobTrack/InboxIntel) are no longer dropped into "Other".
module = load_app_module(monkeypatch)
def fake_generate_json(prompt: str):
assert "Projects" in prompt # the enum now advertises Projects to the model
return {
"section": "Projects",
"confidence": 0.83,
"reason": "project block",
"title": "JobTrack",
"company": None,
"location": None,
"start": None,
"end": None,
"bullets": ["Full-stack job-application tracker (React, ASP.NET Core, SQLite, Docker)."],
"summary": [],
"skills": [],
}
monkeypatch.setattr(module, "_ollama_generate_json", fake_generate_json)
client = TestClient(module.app)
response = client.post("/cv/classify-block", json={"block": "JobTrack - Full-stack job-application tracker (React, ASP.NET Core, SQLite, Docker)."})
assert response.status_code == 200
payload = response.json()
assert payload["section"] == "Projects"
assert payload["title"] == "JobTrack"
def test_classify_block_defaults_missing_section_to_other(monkeypatch):
module = load_app_module(monkeypatch)
monkeypatch.setattr(module, "_ollama_generate_json", lambda prompt: {"bullets": []})
client = TestClient(module.app)
response = client.post("/cv/classify-block", json={"block": "Miscellaneous profile text"})
assert response.status_code == 200
payload = response.json()
assert payload["section"] == "Other"
assert payload["bullets"] == []
assert payload["summary"] == []
assert payload["skills"] == []
# --- AI provider router -------------------------------------------------------
class _FakeResponse:
def __init__(self, payload):
self._data = json.dumps(payload).encode("utf-8")
def read(self):
return self._data
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def _install_fake_urlopen(monkeypatch, module, response_payload, captured):
def fake_urlopen(req, timeout=None):
captured["url"] = req.full_url
captured["headers"] = {k.lower(): v for k, v in req.header_items()}
captured["body"] = json.loads(req.data.decode("utf-8"))
return _FakeResponse(response_payload)
monkeypatch.setattr(module.urllib_request, "urlopen", fake_urlopen)
def test_provider_defaults_to_ollama_and_is_unchanged(monkeypatch):
monkeypatch.delenv("AI_PROVIDER", raising=False)
monkeypatch.setenv("OLLAMA_BASE_URL", "http://ollama-host:11434")
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b")
assert module.AI_PROVIDER == "ollama"
captured = {}
_install_fake_urlopen(monkeypatch, module, {"response": '{"score": 7}'}, captured)
assert module._ollama_generate_json("hi") == {"score": 7}
assert captured["url"] == "http://ollama-host:11434/api/generate"
assert captured["body"]["model"] == "qwen2.5:7b"
assert captured["body"]["format"] == "json"
assert captured["body"]["options"]["temperature"] == 0.1
def test_local_success_wins_even_when_external_fallback_is_permitted(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "gemini")
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
monkeypatch.setenv("GEMINI_MODEL", "gemini-2.0-flash")
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b", external_ai_enabled=True)
module._external_ai_allowed.set(True)
module._ai_task_type.set("cv-normalize")
calls = []
monkeypatch.setattr(module, "_ollama_generate", lambda *args, **kwargs: calls.append("ollama") or '{"score": 7}')
monkeypatch.setattr(module, "_gemini_generate", lambda *args, **kwargs: calls.append("gemini") or '{"score": 9}')
assert module._ollama_generate_json("hi") == {"score": 7}
assert calls == ["ollama"]
def test_local_failure_uses_permitted_groq_fallback_sequentially(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "groq")
monkeypatch.setenv("GROQ_API_KEY", "test-key")
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b", external_ai_enabled=True)
module._external_ai_allowed.set(True)
module._ai_task_type.set("cv-rewrite")
calls = []
def local_failure(*args, **kwargs):
calls.append("ollama")
raise module.URLError("synthetic local outage")
monkeypatch.setattr(module, "_ollama_generate", local_failure)
monkeypatch.setattr(module, "_groq_generate", lambda *args, **kwargs: calls.append("groq") or "rewritten CV text")
assert module._ollama_generate_text("rewrite this") == "rewritten CV text"
assert calls == ["ollama", "groq"]
assert module._route_state.get()["provider"] == "groq"
assert module._route_state.get()["fallback_reason"] == "local_provider_unavailable"
def test_missing_external_key_never_bypasses_local_failure(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "gemini")
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b", external_ai_enabled=True)
module._external_ai_allowed.set(True)
module._ai_task_type.set("cv-normalize")
monkeypatch.setattr(module, "_ollama_generate", lambda *args, **kwargs: (_ for _ in ()).throw(module.URLError("synthetic outage")))
monkeypatch.setattr(module, "_gemini_generate", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("must not call Gemini")))
from fastapi import HTTPException
try:
module._ollama_generate_json("hi")
except module.HTTPException as ex:
assert ex.status_code == 503
assert "Ollama" in ex.detail
else:
raise AssertionError("expected the local failure")
def test_health_reports_active_provider(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "gemini")
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
module = load_app_module(monkeypatch)
client = TestClient(module.app)
payload = client.get("/health").json()
assert payload["ai_provider"] == "gemini"
assert payload["ai_provider_configured"] is True
assert payload["ai_routing_mode"] == "local_first"
assert payload["local_circuit_open"] is False
def test_external_fallback_requires_admin_gate_and_backend_consent_header(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "gemini")
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b", external_ai_enabled=True)
calls = []
def fake_generate(provider, *args, **kwargs):
calls.append(provider)
if provider == "ollama":
raise module._ProviderFailure("ollama", "provider_unavailable", 503)
return "rewritten externally"
monkeypatch.setattr(module, "_generate_from_provider", fake_generate)
client = TestClient(module.app)
local_response = client.post("/cv/rewrite", json={"instruction": "Rewrite", "text": "Synthetic CV"})
assert local_response.status_code == 503
assert calls == ["ollama"]
assert local_response.headers["X-Ai-Provider"] == "ollama"
calls.clear()
external_response = client.post(
"/cv/rewrite",
json={"instruction": "Rewrite", "text": "Synthetic CV"},
headers={"X-Ai-External-Allowed": "true"},
)
assert external_response.status_code == 200
assert calls == ["ollama", "gemini"]
assert external_response.headers["X-Ai-Provider"] == "gemini"
assert external_response.headers["X-Ai-Model"] == "gemini-2.0-flash"
assert external_response.headers["X-Ai-Fallback-Reason"] == "local_provider_unavailable"
assert external_response.headers["X-Ai-Route-Reason"] == "external_fallback"
def test_invalid_local_json_can_fallback_but_prompt_cost_cap_cannot(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "gemini")
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
module = load_app_module(
monkeypatch,
ollama_model="qwen2.5:7b",
external_ai_enabled=True,
external_prompt_limit=1000,
)
module._external_ai_allowed.set(True)
module._ai_task_type.set("cv-normalize")
calls = []
monkeypatch.setattr(module, "_ollama_generate", lambda *args, **kwargs: calls.append("ollama") or "not json")
monkeypatch.setattr(module, "_gemini_generate", lambda *args, **kwargs: calls.append("gemini") or '{"score": 9}')
assert module._ollama_generate_json("short") == {"score": 9}
assert calls == ["ollama", "gemini"]
assert module._route_state.get()["fallback_reason"] == "local_schema_invalid"
calls.clear()
try:
module._ollama_generate_json("x" * 1001)
except module.HTTPException as ex:
assert ex.status_code == 502
else:
raise AssertionError("expected local schema failure above the external prompt cap")
assert calls == ["ollama"]
assert "external_prompt_limit" in module._route_state.get()["route_reason"]
def test_open_local_circuit_skips_local_only_when_fallback_is_permitted(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "groq")
monkeypatch.setenv("GROQ_API_KEY", "test-key")
module = load_app_module(
monkeypatch,
ollama_model="qwen2.5:7b",
external_ai_enabled=True,
circuit_threshold=1,
)
module._external_ai_allowed.set(True)
module._ai_task_type.set("cv-rewrite")
calls = []
def local_failure(*args, **kwargs):
calls.append("ollama")
raise module.URLError("synthetic local outage")
monkeypatch.setattr(module, "_ollama_generate", local_failure)
monkeypatch.setattr(module, "_groq_generate", lambda *args, **kwargs: calls.append("groq") or "external")
assert module._ollama_generate_text("first") == "external"
assert module._ollama_generate_text("second") == "external"
assert calls == ["ollama", "groq", "groq"]
assert module._route_state.get()["fallback_reason"] == "local_circuit_open"
def test_external_failure_is_clear_and_unapproved_background_task_stays_local(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "gemini")
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
module = load_app_module(monkeypatch, ollama_model="qwen2.5:7b", external_ai_enabled=True)
module._external_ai_allowed.set(True)
calls = []
def provider_failure(provider, *args, **kwargs):
calls.append(provider)
raise module._ProviderFailure(provider, "provider_unavailable", 503)
monkeypatch.setattr(module, "_generate_from_provider", provider_failure)
module._ai_task_type.set("cv-rewrite")
try:
module._ollama_generate_text("allowed")
except module.HTTPException as ex:
assert ex.status_code == 503
assert "Gemini" in ex.detail
else:
raise AssertionError("expected external provider failure")
assert calls == ["ollama", "gemini"]
assert module._route_state.get()["fallback_reason"] == "local_provider_unavailable"
calls.clear()
module._ai_task_type.set("strategy.snapshot")
try:
module._ollama_generate_text("not task-approved")
except module.HTTPException as ex:
assert ex.status_code == 503
assert "Ollama" in ex.detail
else:
raise AssertionError("expected local provider failure")
assert calls == ["ollama"]
assert "task_not_allowed_external" in module._route_state.get()["route_reason"]
def test_external_only_mode_still_requires_explicit_permission_and_task_allowlist(monkeypatch):
monkeypatch.setenv("AI_PROVIDER", "groq")
monkeypatch.setenv("GROQ_API_KEY", "test-key")
module = load_app_module(
monkeypatch,
ollama_model="qwen2.5:7b",
external_ai_enabled=True,
routing_mode="external_only",
)
module._ai_task_type.set("cv-rewrite")
calls = []
monkeypatch.setattr(module, "_ollama_generate", lambda *args, **kwargs: calls.append("ollama") or "local")
monkeypatch.setattr(module, "_groq_generate", lambda *args, **kwargs: calls.append("groq") or "external")
try:
module._ollama_generate_text("without consent")
except module.HTTPException as ex:
assert ex.status_code == 403
else:
raise AssertionError("expected external permission denial")
assert calls == []
module._external_ai_allowed.set(True)
assert module._ollama_generate_text("with consent") == "external"
assert calls == ["groq"]
def test_service_token_rejects_calls_without_the_header(monkeypatch):
module = load_app_module(monkeypatch, service_token="s3cret")
client = TestClient(module.app)
response = client.post("/summarize", json={"text": "Platform engineering role."})
assert response.status_code == 401
assert "token" in response.json()["detail"].lower()
def test_service_token_rejects_a_wrong_header(monkeypatch):
module = load_app_module(monkeypatch, service_token="s3cret")
client = TestClient(module.app)
response = client.post(
"/summarize",
json={"text": "Platform engineering role."},
headers={"X-Ai-Service-Token": "wrong"},
)
assert response.status_code == 401
def test_service_token_allows_the_correct_header(monkeypatch):
module = load_app_module(monkeypatch, service_token="s3cret")
client = TestClient(module.app)
response = client.post(
"/summarize",
json={"text": "Platform engineering role."},
headers={"X-Ai-Service-Token": "s3cret"},
)
# 503 = passed the token gate and reached the handler, which is model-disabled here.
assert response.status_code == 503
def test_health_stays_open_so_probes_and_healthchecks_work(monkeypatch):
module = load_app_module(monkeypatch, service_token="s3cret")
client = TestClient(module.app)
assert client.get("/health").status_code == 200
def test_extract_text_rejects_oversized_upload_before_parsing(monkeypatch):
module = load_app_module(monkeypatch)
monkeypatch.setattr(module, "MAX_EXTRACT_FILE_BYTES", 32)
monkeypatch.setattr(module, "_run_parser_child", lambda extension, data: (_ for _ in ()).throw(AssertionError("parser must not run")))
client = TestClient(module.app)
response = client.post("/extract-text", files={"file": ("large.txt", b"x" * 64, "text/plain")})
assert response.status_code == 400
assert "too large" in response.json()["detail"].lower()
def test_extract_text_rejects_extension_signature_mismatch(monkeypatch):
module = load_app_module(monkeypatch)
client = TestClient(module.app)
response = client.post("/extract-text", files={"file": ("resume.pdf", b"not a pdf", "application/pdf")})
assert response.status_code == 400
assert response.json()["detail"] == "The uploaded file does not match its PDF extension."
def test_extract_text_rejects_binary_plain_text(monkeypatch):
module = load_app_module(monkeypatch)
client = TestClient(module.app)
response = client.post("/extract-text", files={"file": ("resume.txt", b"Name\x00binary", "text/plain")})
assert response.status_code == 400
assert response.json()["detail"] == "The uploaded text file contains binary data."
def test_extract_text_rejects_pdf_over_page_limit(monkeypatch):
load_app_module(monkeypatch)
import parser_child
writer = PdfWriter()
writer.add_blank_page(width=100, height=100)
writer.add_blank_page(width=100, height=100)
payload = io.BytesIO()
writer.write(payload)
try:
parser_child.extract_pdf_text(payload.getvalue(), page_limit=1)
except parser_child.ParserError as exc:
assert exc.code == "pdf_page_limit"
else:
raise AssertionError("Expected the PDF page limit to reject the document")
def test_extract_text_rejects_image_pixel_limit_before_ocr(monkeypatch):
load_app_module(monkeypatch)
import parser_child
monkeypatch.setattr(parser_child, "_ocr_image", lambda image: (_ for _ in ()).throw(AssertionError("OCR must not run")))
payload = io.BytesIO()
Image.new("RGB", (10, 10), "white").save(payload, format="PNG")
try:
parser_child.extract_image_text(payload.getvalue(), pixel_limit=50)
except parser_child.ParserError as exc:
assert exc.code == "image_pixel_limit"
else:
raise AssertionError("Expected the image pixel limit to reject the document")
def test_docx_container_rejects_excessive_entries(monkeypatch):
module = load_app_module(monkeypatch)
monkeypatch.setattr(module, "MAX_DOCX_ENTRIES", 2)
payload = io.BytesIO()
with zipfile.ZipFile(payload, "w") as archive:
archive.writestr("[Content_Types].xml", "types")
archive.writestr("word/document.xml", "document")
archive.writestr("word/styles.xml", "styles")
try:
module._validate_docx_container(payload.getvalue())
except module.HTTPException as exc:
assert exc.status_code == 422
assert exc.detail == "The DOCX contains too many files."
else:
raise AssertionError("Expected the DOCX entry limit to reject the container")
def test_extract_text_does_not_leak_parser_exception(monkeypatch):
module = load_app_module(monkeypatch)
monkeypatch.setattr(module, "_run_parser_child", lambda extension, data: module._raise_parser_error("parser_failed"))
client = TestClient(module.app)
response = client.post("/extract-text", files={"file": ("resume.pdf", b"%PDF-invalid", "application/pdf")})
assert response.status_code == 422
assert response.json()["detail"] == "Document extraction failed."
assert "secret" not in response.text
def test_extraction_normalization_preserves_sections_and_bullets(monkeypatch):
load_app_module(monkeypatch)
import parser_child
normalized = parser_child.normalize_text("PROFILE\n\nEngineer\n• Built APIs\n• Shipped services")
assert normalized.splitlines() == ["PROFILE", "", "Engineer", "• Built APIs", "• Shipped services"]
def test_docx_extraction_preserves_paragraph_and_table_boundaries(monkeypatch):
load_app_module(monkeypatch)
import parser_child
document = Document()
document.add_heading("Technical Skills", level=1)
table = document.add_table(rows=2, cols=2)
table.cell(0, 0).text = "Backend"
table.cell(0, 1).text = "C#, .NET"
table.cell(1, 0).text = "DevOps"
table.cell(1, 1).text = "Docker, Linux"
payload = io.BytesIO()
document.save(payload)
extracted = parser_child.extract_docx_text(payload.getvalue())
assert "Technical Skills" in extracted
assert "Backend | C#, .NET" in extracted
assert "DevOps | Docker, Linux" in extracted
def test_extract_text_runs_in_isolated_child(monkeypatch):
module = load_app_module(monkeypatch)
client = TestClient(module.app)
response = client.post(
"/extract-text",
files={"file": ("resume.md", b"# Ada Lovelace\n\n## Skills\nC#", "text/markdown")},
)
assert response.status_code == 200
assert response.json()["text"] == "# Ada Lovelace\n\n## Skills\nC#"
def test_parser_child_receives_no_provider_secrets(monkeypatch, tmp_path):
module = load_app_module(monkeypatch)
monkeypatch.setenv("GEMINI_API_KEY", "must-not-leak")
monkeypatch.setenv("GROQ_API_KEY", "must-not-leak")
child = tmp_path / "environment_probe.py"
child.write_text(textwrap.dedent("""
import argparse, json, os
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument('--input')
parser.add_argument('--output')
parser.add_argument('--extension')
parser.add_argument('--cpu-seconds')
parser.add_argument('--memory-bytes')
args = parser.parse_args()
leaked = any(os.environ.get(key) for key in ('GEMINI_API_KEY', 'GROQ_API_KEY', 'AI_SERVICE_TOKEN'))
Path(args.output).write_text(json.dumps({'ok': not leaked, 'text': 'safe', 'ocr_used': False, 'page_count': None} if not leaked else {'ok': False, 'code': 'parser_failed'}), encoding='utf-8')
"""), encoding="utf-8")
monkeypatch.setattr(module, "PARSER_CHILD_PATH", child)
assert module._run_parser_child(".txt", b"safe")["text"] == "safe"
def test_parser_child_timeout_is_stable_and_releases_capacity(monkeypatch, tmp_path):
module = load_app_module(monkeypatch)
child = tmp_path / "sleeping_parser.py"
child.write_text("import time\ntime.sleep(30)\n", encoding="utf-8")
monkeypatch.setattr(module, "PARSER_CHILD_PATH", child)
monkeypatch.setattr(module, "PARSER_TIMEOUT_SECONDS", 0.1)
try:
module._run_parser_child(".txt", b"safe")
except module.HTTPException as exc:
assert exc.status_code == 504
assert exc.detail == "Document extraction timed out."
else:
raise AssertionError("Expected the parser deadline to terminate the child")
assert module._parser_capacity.acquire(blocking=False)
module._parser_capacity.release()
def test_parser_timeout_terminates_descendants(monkeypatch, tmp_path):
module = load_app_module(monkeypatch)
pid_path = tmp_path / "descendant.pid"
child = tmp_path / "parser_with_descendant.py"
child.write_text(textwrap.dedent(f"""
import subprocess, sys, time
from pathlib import Path
descendant = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(30)'])
Path({str(pid_path)!r}).write_text(str(descendant.pid), encoding='ascii')
time.sleep(30)
"""), encoding="utf-8")
monkeypatch.setattr(module, "PARSER_CHILD_PATH", child)
monkeypatch.setattr(module, "PARSER_TIMEOUT_SECONDS", 0.2)
try:
module._run_parser_child(".txt", b"safe")
except module.HTTPException as exc:
assert exc.status_code == 504
else:
raise AssertionError("Expected the parser deadline to terminate the process tree")
descendant_pid = int(pid_path.read_text(encoding="ascii"))
if os.name == "nt":
listing = subprocess.run(
["tasklist", "/FI", f"PID eq {descendant_pid}", "/FO", "CSV", "/NH"],
capture_output=True,
text=True,
check=False,
).stdout
assert f'"{descendant_pid}"' not in listing
else:
try:
os.kill(descendant_pid, 0)
except ProcessLookupError:
pass
else:
# Minimal CI containers may not run an init process that promptly
# reaps orphaned children. A zombie has already been terminated and
# cannot execute or retain parser resources, even though kill(pid, 0)
# continues to see its process-table entry.
stat_path = Path(f"/proc/{descendant_pid}/stat")
try:
state = stat_path.read_text(encoding="ascii").split()[2]
except (FileNotFoundError, IndexError, OSError):
state = None
if state == "Z":
return
os.kill(descendant_pid, 9)
raise AssertionError("Parser descendant survived the parent deadline")
def test_parser_temp_cleanup_removes_only_stale_work_directories(monkeypatch, tmp_path):
module = load_app_module(monkeypatch)
parser_root = tmp_path / "parser-root"
stale = parser_root / "work-stale"
recent = parser_root / "work-recent"
unrelated = parser_root / "keep-me"
stale.mkdir(parents=True)
recent.mkdir()
unrelated.mkdir()
old = 1_000_000
os.utime(stale, (old, old))
monkeypatch.setattr(module, "PARSER_TEMP_ROOT", parser_root.resolve())
monkeypatch.setattr(module, "PARSER_STALE_WORK_SECONDS", 300)
monkeypatch.setattr(module, "_parser_temp_ready", False)
assert module._prepare_parser_temp_root() == parser_root.resolve()
assert not stale.exists()
assert recent.exists()
assert unrelated.exists()
def test_cache_purge_requires_service_token_and_clears_content(monkeypatch):
module = load_app_module(monkeypatch, service_token="s3cret")
module.cache["synthetic-key"] = "synthetic-summary"
client = TestClient(module.app)
assert client.delete("/maintenance/cache").status_code == 401
response = client.delete(
"/maintenance/cache",
headers={"X-Ai-Service-Token": "s3cret"},
)
assert response.status_code == 200
assert response.json() == {"cleared": 1}
assert len(module.cache) == 0
def test_endpoints_stay_open_when_no_token_is_configured(monkeypatch):
module = load_app_module(monkeypatch)
client = TestClient(module.app)
# Keyless local dev: reaches the handler (503 model-disabled), not a 401.
response = client.post("/summarize", json={"text": "Platform engineering role."})
assert response.status_code == 503