Compare commits
3 commits
2051880d73
...
e87c707dd9
| Author | SHA1 | Date | |
|---|---|---|---|
| e87c707dd9 | |||
| 7dcdf551fc | |||
| 544a6aeeb3 |
64 changed files with 200 additions and 126 deletions
63
dev-api.py
63
dev-api.py
|
|
@ -14,7 +14,6 @@ import sqlite3
|
||||||
import ssl as ssl_mod
|
import ssl as ssl_mod
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -39,7 +38,7 @@ if str(PEREGRINE_ROOT) not in sys.path:
|
||||||
|
|
||||||
from circuitforge_core.api import make_feedback_router as _make_feedback_router # noqa: E402
|
from circuitforge_core.api import make_feedback_router as _make_feedback_router # noqa: E402
|
||||||
from circuitforge_core.config.settings import load_env as _load_env # noqa: E402
|
from circuitforge_core.config.settings import load_env as _load_env # noqa: E402
|
||||||
from scripts.credential_store import get_credential, set_credential, delete_credential # noqa: E402
|
from scripts.credential_store import get_credential, set_credential # noqa: E402
|
||||||
|
|
||||||
DB_PATH = os.environ.get("STAGING_DB", "/devl/job-seeker/staging.db")
|
DB_PATH = os.environ.get("STAGING_DB", "/devl/job-seeker/staging.db")
|
||||||
|
|
||||||
|
|
@ -738,7 +737,6 @@ def preview_resume_review(job_id: int, body: ResumeReviewBody):
|
||||||
3. render_resume_text() — renders to plain text for the preview panel
|
3. render_resume_text() — renders to plain text for the preview panel
|
||||||
Returns: {preview_text, preview_struct} — struct preserved for the approve step.
|
Returns: {preview_text, preview_struct} — struct preserved for the approve step.
|
||||||
"""
|
"""
|
||||||
import json as _json
|
|
||||||
from scripts.db import get_resume_draft as _get_draft
|
from scripts.db import get_resume_draft as _get_draft
|
||||||
from scripts.resume_optimizer import (
|
from scripts.resume_optimizer import (
|
||||||
apply_review_decisions, frame_skill_gaps, render_resume_text,
|
apply_review_decisions, frame_skill_gaps, render_resume_text,
|
||||||
|
|
@ -759,7 +757,6 @@ def preview_resume_review(job_id: int, body: ResumeReviewBody):
|
||||||
# Step 2: inject gap framing for rejected skills (adjacent / learning)
|
# Step 2: inject gap framing for rejected skills (adjacent / learning)
|
||||||
framings = [f.model_dump() for f in body.gap_framings if f.mode in ("adjacent", "learning")]
|
framings = [f.model_dump() for f in body.gap_framings if f.mode in ("adjacent", "learning")]
|
||||||
if framings:
|
if framings:
|
||||||
db_path_obj = Path(_request_db.get() or DB_PATH)
|
|
||||||
job_row = _get_db().execute(
|
job_row = _get_db().execute(
|
||||||
"SELECT title, company FROM jobs WHERE id=?", (job_id,)
|
"SELECT title, company FROM jobs WHERE id=?", (job_id,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
@ -829,7 +826,6 @@ def approve_resume(job_id: int, body: dict):
|
||||||
saved_resume_id: int | None = None
|
saved_resume_id: int | None = None
|
||||||
if body.get("save_to_library"):
|
if body.get("save_to_library"):
|
||||||
from scripts.db import create_resume as _create_r
|
from scripts.db import create_resume as _create_r
|
||||||
import json as _json2
|
|
||||||
resume_name = (body.get("resume_name") or "").strip() or f"Optimized for job {job_id}"
|
resume_name = (body.get("resume_name") or "").strip() or f"Optimized for job {job_id}"
|
||||||
saved = _create_r(
|
saved = _create_r(
|
||||||
db_path,
|
db_path,
|
||||||
|
|
@ -926,7 +922,7 @@ def create_resume_endpoint(body: dict):
|
||||||
|
|
||||||
@app.post("/api/resumes/import")
|
@app.post("/api/resumes/import")
|
||||||
async def import_resume_endpoint(file: UploadFile, name: str = ""):
|
async def import_resume_endpoint(file: UploadFile, name: str = ""):
|
||||||
import os, tempfile, json as _json
|
import json as _json
|
||||||
from scripts.db import create_resume as _create
|
from scripts.db import create_resume as _create
|
||||||
db_path = Path(_request_db.get() or DB_PATH)
|
db_path = Path(_request_db.get() or DB_PATH)
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
|
|
@ -1128,6 +1124,35 @@ def set_job_resume_endpoint(job_id: int, body: dict):
|
||||||
# context. Avocet then routes these prompts through different local models to
|
# context. Avocet then routes these prompts through different local models to
|
||||||
# compare generation quality against the real Peregrine pipeline.
|
# compare generation quality against the real Peregrine pipeline.
|
||||||
|
|
||||||
|
_SYNTHETIC_JOB = {
|
||||||
|
"id": 0,
|
||||||
|
"title": "Senior Software Engineer",
|
||||||
|
"company": "Acme Corp",
|
||||||
|
"description": (
|
||||||
|
"We are looking for a Senior Software Engineer to join our platform team. "
|
||||||
|
"You will design and build scalable backend services in Python and Go, "
|
||||||
|
"contribute to our event-driven architecture using Kafka and Redis, and "
|
||||||
|
"mentor junior engineers. We value clear communication, strong code review "
|
||||||
|
"practices, and an ownership mindset.\n\n"
|
||||||
|
"Requirements:\n"
|
||||||
|
"- 5+ years of backend engineering experience\n"
|
||||||
|
"- Proficiency in Python or Go; experience with both is a plus\n"
|
||||||
|
"- Solid understanding of distributed systems and API design (REST/gRPC)\n"
|
||||||
|
"- Experience with containerization (Docker/Kubernetes)\n"
|
||||||
|
"- Comfort working in a remote-first, async team environment\n\n"
|
||||||
|
"Nice to have:\n"
|
||||||
|
"- Experience with Kafka or other message-queue systems\n"
|
||||||
|
"- Open-source contributions\n"
|
||||||
|
"- Familiarity with observability tooling (Prometheus, Grafana)\n"
|
||||||
|
),
|
||||||
|
"status": "applied",
|
||||||
|
"cover_letter": "",
|
||||||
|
"raw_output": "",
|
||||||
|
"company_brief": "",
|
||||||
|
"ats_gap_report": "",
|
||||||
|
"talking_points": "",
|
||||||
|
}
|
||||||
|
|
||||||
def _imitate_load_profile():
|
def _imitate_load_profile():
|
||||||
"""Load UserProfile from config/user.yaml, or None if missing."""
|
"""Load UserProfile from config/user.yaml, or None if missing."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -1157,6 +1182,9 @@ def _imitate_cover_letter(db, profile, limit: int) -> dict:
|
||||||
except Exception:
|
except Exception:
|
||||||
corpus = []
|
corpus = []
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
rows = [_SYNTHETIC_JOB]
|
||||||
|
|
||||||
samples = []
|
samples = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
desc = r["description"] or ""
|
desc = r["description"] or ""
|
||||||
|
|
@ -1213,6 +1241,9 @@ def _imitate_company_research(db, profile, limit: int) -> dict:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
rows = [_SYNTHETIC_JOB]
|
||||||
|
|
||||||
samples = []
|
samples = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
jd = (r["description"] or "")[:1500].strip()
|
jd = (r["description"] or "")[:1500].strip()
|
||||||
|
|
@ -1270,6 +1301,10 @@ def _imitate_interview_prep(db, profile, limit: int) -> dict:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
name = profile.name if profile else "the candidate"
|
name = profile.name if profile else "the candidate"
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
rows = [_SYNTHETIC_JOB]
|
||||||
|
|
||||||
samples = []
|
samples = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
system_prompt = (
|
system_prompt = (
|
||||||
|
|
@ -1324,6 +1359,9 @@ def _imitate_ats_resume(db, profile, limit: int) -> dict:
|
||||||
pass
|
pass
|
||||||
resume_block = f"\n## Current Resume\n{resume_text}" if resume_text else ""
|
resume_block = f"\n## Current Resume\n{resume_text}" if resume_text else ""
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
rows = [_SYNTHETIC_JOB]
|
||||||
|
|
||||||
samples = []
|
samples = []
|
||||||
for r in rows:
|
for r in rows:
|
||||||
desc = (r["description"] or "")[:1500].strip()
|
desc = (r["description"] or "")[:1500].strip()
|
||||||
|
|
@ -1462,14 +1500,8 @@ def calendar_push(job_id: int):
|
||||||
# ── Survey endpoints ─────────────────────────────────────────────────────────
|
# ── Survey endpoints ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Module-level imports so tests can patch dev_api.LLMRouter etc.
|
# Module-level imports so tests can patch dev_api.LLMRouter etc.
|
||||||
from scripts.llm_router import LLMRouter
|
from scripts.db import insert_survey_response, get_survey_responses # noqa: E402
|
||||||
from scripts.db import insert_survey_response, get_survey_responses
|
|
||||||
|
|
||||||
from scripts.survey_assistant import (
|
|
||||||
SURVEY_SYSTEM as _SURVEY_SYSTEM,
|
|
||||||
build_text_prompt as _build_text_prompt,
|
|
||||||
build_image_prompt as _build_image_prompt,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/vision/health")
|
@app.get("/api/vision/health")
|
||||||
|
|
@ -2690,7 +2722,7 @@ def config_user():
|
||||||
|
|
||||||
# ── Settings: My Profile endpoints ───────────────────────────────────────────
|
# ── Settings: My Profile endpoints ───────────────────────────────────────────
|
||||||
|
|
||||||
from scripts.user_profile import load_user_profile, save_user_profile
|
from scripts.user_profile import load_user_profile, save_user_profile # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def _user_yaml_path() -> str:
|
def _user_yaml_path() -> str:
|
||||||
|
|
@ -4352,7 +4384,8 @@ def _fetch_cforch_nodes() -> list[dict]:
|
||||||
if not url:
|
if not url:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
import urllib.request, json as _json
|
import urllib.request
|
||||||
|
import json as _json
|
||||||
req = urllib.request.Request(f"{url}/api/nodes", headers={"Accept": "application/json"})
|
req = urllib.request.Request(f"{url}/api/nodes", headers={"Accept": "application/json"})
|
||||||
with urllib.request.urlopen(req, timeout=3) as resp:
|
with urllib.request.urlopen(req, timeout=3) as resp:
|
||||||
data = _json.loads(resp.read())
|
data = _json.loads(resp.read())
|
||||||
|
|
|
||||||
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
[tool.ruff]
|
||||||
|
# app/ is the deprecated Streamlit UI (replaced by Vue+FastAPI).
|
||||||
|
# No new work goes there; exclude from linting rather than accumulate suppressions.
|
||||||
|
exclude = ["app/"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# dev-api.py / dev_api.py (symlink): E702 semicolons in compact Pydantic model
|
||||||
|
# definitions — intentional style for dense data models with many simple fields.
|
||||||
|
"dev-api.py" = ["E702"]
|
||||||
|
"dev_api.py" = ["E702"]
|
||||||
|
|
||||||
|
# finetune_local.py: E402 ML libs (torch, datasets, trl) are imported after
|
||||||
|
# runtime CUDA / Unsloth availability checks — conditional import pattern.
|
||||||
|
"scripts/finetune_local.py" = ["E402", "E741"]
|
||||||
|
|
||||||
|
# scripts/: E402 mid-file imports used for lazy loading or post-env-setup imports.
|
||||||
|
"scripts/task_runner.py" = ["E402"]
|
||||||
|
"scripts/migrate.py" = ["E741"]
|
||||||
|
|
||||||
|
# scrapers/: third-party script; minimal changes policy.
|
||||||
|
"scrapers/companyScraper.py" = ["E722"]
|
||||||
|
|
||||||
|
# tools/: deprecated label tool copy (canonical in avocet); suppress style warnings.
|
||||||
|
"tools/label_tool.py" = ["E741"]
|
||||||
|
|
||||||
|
# tests/: F841 unused variables are the standard mock-patch capture pattern
|
||||||
|
# (e.g., `original_fn = obj.method` before monkeypatching).
|
||||||
|
# E741 ambiguous `l` names and E402 conditional imports are common in test fixtures.
|
||||||
|
# E702 compact `con.commit(); con.close()` is a common SQLite test helper idiom.
|
||||||
|
"tests/**" = ["F841", "E741", "E402", "E702"]
|
||||||
|
"tests/test_wizard_steps.py" = ["F841", "E741", "E402", "E702"]
|
||||||
|
"scripts/test_email_classify.py" = ["E402", "F841"]
|
||||||
|
|
@ -14,7 +14,6 @@ Enhanced features:
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import csv
|
import csv
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from scripts.classifier_adapters import (
|
from scripts.classifier_adapters import (
|
||||||
LABELS,
|
LABELS,
|
||||||
LABEL_DESCRIPTIONS,
|
|
||||||
ClassifierAdapter,
|
ClassifierAdapter,
|
||||||
GLiClassAdapter,
|
GLiClassAdapter,
|
||||||
RerankerAdapter,
|
RerankerAdapter,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ push updates the existing event rather than creating a duplicate.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
|
||||||
import yaml
|
import yaml
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,17 @@ CREATE TABLE IF NOT EXISTS survey_responses (
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CREATE_RESUME_CORRECTIONS = """
|
||||||
|
CREATE TABLE IF NOT EXISTS resume_optimizer_corrections (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_id INTEGER NOT NULL REFERENCES jobs(id),
|
||||||
|
section TEXT NOT NULL,
|
||||||
|
proposed_json TEXT NOT NULL,
|
||||||
|
accepted_json TEXT NOT NULL,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
CREATE_DIGEST_QUEUE = """
|
CREATE_DIGEST_QUEUE = """
|
||||||
CREATE TABLE IF NOT EXISTS digest_queue (
|
CREATE TABLE IF NOT EXISTS digest_queue (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
|
|
@ -205,9 +216,10 @@ def _migrate_db(db_path: Path) -> None:
|
||||||
conn.execute("ALTER TABLE background_tasks ADD COLUMN params TEXT")
|
conn.execute("ALTER TABLE background_tasks ADD COLUMN params TEXT")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # column already exists
|
pass # column already exists
|
||||||
# Ensure references tables exist (CREATE IF NOT EXISTS is idempotent)
|
# Ensure tables that can't be added via ALTER TABLE exist (all idempotent).
|
||||||
conn.execute(CREATE_REFERENCES)
|
conn.execute(CREATE_REFERENCES)
|
||||||
conn.execute(CREATE_JOB_REFERENCES)
|
conn.execute(CREATE_JOB_REFERENCES)
|
||||||
|
conn.execute(CREATE_RESUME_CORRECTIONS)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
@ -223,6 +235,7 @@ def init_db(db_path: Path = DEFAULT_DB) -> None:
|
||||||
conn.execute(CREATE_DIGEST_QUEUE)
|
conn.execute(CREATE_DIGEST_QUEUE)
|
||||||
conn.execute(CREATE_REFERENCES)
|
conn.execute(CREATE_REFERENCES)
|
||||||
conn.execute(CREATE_JOB_REFERENCES)
|
conn.execute(CREATE_JOB_REFERENCES)
|
||||||
|
conn.execute(CREATE_RESUME_CORRECTIONS)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
_migrate_db(db_path)
|
_migrate_db(db_path)
|
||||||
|
|
@ -1241,3 +1254,76 @@ def set_training_exclusion(db_path: Path, job_id: int, excluded: bool) -> None:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Resume optimizer corrections ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_resume_correction(
|
||||||
|
db_path: Path,
|
||||||
|
job_id: int,
|
||||||
|
section: str,
|
||||||
|
proposed: object,
|
||||||
|
accepted: object,
|
||||||
|
) -> None:
|
||||||
|
"""Persist a (proposed, accepted) correction pair from the resume review UI.
|
||||||
|
|
||||||
|
Called when a user edits an LLM-proposed value and accepts it. The pair
|
||||||
|
becomes a supervised fine-tuning (SFT) candidate routed through Avocet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section: 'summary' or 'experience:<title>|<company>'
|
||||||
|
proposed: Original LLM output (string for summary, list for bullets).
|
||||||
|
accepted: User-edited value (same type as proposed).
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
try:
|
||||||
|
conn.execute(
|
||||||
|
"""INSERT INTO resume_optimizer_corrections
|
||||||
|
(job_id, section, proposed_json, accepted_json)
|
||||||
|
VALUES (?, ?, ?, ?)""",
|
||||||
|
(job_id, section, _json.dumps(proposed), _json.dumps(accepted)),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_resume_corrections(
|
||||||
|
db_path: Path,
|
||||||
|
limit: int = 200,
|
||||||
|
job_id: int | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Return pending resume corrections for Avocet export.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Maximum rows to return.
|
||||||
|
job_id: If set, filter to corrections for a specific job.
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
if job_id is not None:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM resume_optimizer_corrections WHERE job_id=? ORDER BY created_at DESC LIMIT ?",
|
||||||
|
(job_id, limit),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM resume_optimizer_corrections ORDER BY created_at DESC LIMIT ?",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": r["id"],
|
||||||
|
"job_id": r["job_id"],
|
||||||
|
"section": r["section"],
|
||||||
|
"proposed": _json.loads(r["proposed_json"]),
|
||||||
|
"accepted": _json.loads(r["accepted_json"]),
|
||||||
|
"created_at": r["created_at"],
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ if not LETTERS_JSONL.exists():
|
||||||
sys.exit(f"ERROR: Dataset not found at {LETTERS_JSONL}\n"
|
sys.exit(f"ERROR: Dataset not found at {LETTERS_JSONL}\n"
|
||||||
"Run: make prepare-training (or: python scripts/prepare_training_data.py)")
|
"Run: make prepare-training (or: python scripts/prepare_training_data.py)")
|
||||||
|
|
||||||
records = [json.loads(l) for l in LETTERS_JSONL.read_text().splitlines() if l.strip()]
|
records = [json.loads(line) for line in LETTERS_JSONL.read_text().splitlines() if line.strip()]
|
||||||
print(f"Loaded {len(records)} training examples.")
|
print(f"Loaded {len(records)} training examples.")
|
||||||
|
|
||||||
# Convert to chat format expected by SFTTrainer
|
# Convert to chat format expected by SFTTrainer
|
||||||
|
|
@ -323,6 +323,6 @@ if gguf_path and gguf_path.exists():
|
||||||
else:
|
else:
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(" Adapter saved (no GGUF produced).")
|
print(" Adapter saved (no GGUF produced).")
|
||||||
print(f" Re-run without --no-gguf to generate a GGUF for Ollama registration.")
|
print(" Re-run without --no-gguf to generate a GGUF for Ollama registration.")
|
||||||
print(f" Adapter path: {adapter_path}")
|
print(f" Adapter path: {adapter_path}")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ def build_prompt(
|
||||||
)
|
)
|
||||||
parts.append(f"{recruiter_note}\n")
|
parts.append(f"{recruiter_note}\n")
|
||||||
|
|
||||||
parts.append(f"Now write a new cover letter for:")
|
parts.append("Now write a new cover letter for:")
|
||||||
parts.append(f" Role: {title}")
|
parts.append(f" Role: {title}")
|
||||||
parts.append(f" Company: {company}")
|
parts.append(f" Company: {company}")
|
||||||
if description:
|
if description:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime
|
||||||
from scripts.integrations.base import IntegrationBase
|
from scripts.integrations.base import IntegrationBase
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import argparse
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from textwrap import dedent
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -348,14 +348,14 @@ def write_compose_override(ports: dict[str, dict]) -> None:
|
||||||
for name, info in to_disable.items():
|
for name, info in to_disable.items():
|
||||||
lines += [
|
lines += [
|
||||||
f" {name}: # adopted — host service on :{info['resolved']}",
|
f" {name}: # adopted — host service on :{info['resolved']}",
|
||||||
f" entrypoint: [\"/bin/sh\", \"-c\", \"sleep infinity\"]",
|
" entrypoint: [\"/bin/sh\", \"-c\", \"sleep infinity\"]",
|
||||||
f" ports: []",
|
" ports: []",
|
||||||
f" healthcheck:",
|
" healthcheck:",
|
||||||
f" test: [\"CMD\", \"true\"]",
|
" test: [\"CMD\", \"true\"]",
|
||||||
f" interval: 1s",
|
" interval: 1s",
|
||||||
f" timeout: 1s",
|
" timeout: 1s",
|
||||||
f" start_period: 0s",
|
" start_period: 0s",
|
||||||
f" retries: 1",
|
" retries: 1",
|
||||||
]
|
]
|
||||||
|
|
||||||
OVERRIDE_YML.write_text("\n".join(lines) + "\n")
|
OVERRIDE_YML.write_text("\n".join(lines) + "\n")
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@ Falls back to empty dict on unrecoverable errors — caller shows the form build
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
import pdfplumber
|
import pdfplumber
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ FastAPI application. Callable directly or via the survey_analyze background task
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,6 @@ def _run_task(db_path: Path, task_id: int, task_type: str, job_id: int,
|
||||||
prioritize_gaps,
|
prioritize_gaps,
|
||||||
rewrite_for_ats,
|
rewrite_for_ats,
|
||||||
hallucination_check,
|
hallucination_check,
|
||||||
render_resume_text,
|
|
||||||
)
|
)
|
||||||
from scripts.user_profile import load_user_profile
|
from scripts.user_profile import load_user_profile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,11 @@ Public API (unchanged — callers do not need to change):
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from circuitforge_core.tasks.scheduler import (
|
from circuitforge_core.tasks.scheduler import (
|
||||||
TaskSpec, # re-export unchanged
|
|
||||||
LocalScheduler as _CoreTaskScheduler,
|
LocalScheduler as _CoreTaskScheduler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import pytest
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from playwright.sync_api import Page, BrowserContext
|
from playwright.sync_api import Page, BrowserContext
|
||||||
|
|
||||||
from tests.e2e.models import ErrorRecord, ModeConfig, diff_errors
|
from tests.e2e.models import ErrorRecord, ModeConfig
|
||||||
from tests.e2e.modes.demo import DEMO
|
from tests.e2e.modes.demo import DEMO
|
||||||
from tests.e2e.modes.cloud import CLOUD
|
from tests.e2e.modes.cloud import CLOUD
|
||||||
from tests.e2e.modes.local import LOCAL
|
from tests.e2e.modes.local import LOCAL
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ from __future__ import annotations
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.e2e.conftest import (
|
from tests.e2e.conftest import (
|
||||||
wait_for_streamlit, get_page_errors, screenshot_on_fail,
|
wait_for_streamlit, screenshot_on_fail,
|
||||||
)
|
)
|
||||||
from tests.e2e.models import ModeConfig, diff_errors
|
from tests.e2e.models import diff_errors
|
||||||
from tests.e2e.pages.home_page import HomePage
|
from tests.e2e.pages.home_page import HomePage
|
||||||
from tests.e2e.pages.job_review_page import JobReviewPage
|
from tests.e2e.pages.job_review_page import JobReviewPage
|
||||||
from tests.e2e.pages.apply_page import ApplyPage
|
from tests.e2e.pages.apply_page import ApplyPage
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ Run: pytest tests/e2e/test_smoke.py --mode=demo
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.e2e.conftest import wait_for_streamlit, get_page_errors, get_console_errors, screenshot_on_fail
|
from tests.e2e.conftest import wait_for_streamlit, screenshot_on_fail
|
||||||
from tests.e2e.models import ModeConfig
|
|
||||||
from tests.e2e.pages.home_page import HomePage
|
from tests.e2e.pages.home_page import HomePage
|
||||||
from tests.e2e.pages.job_review_page import JobReviewPage
|
from tests.e2e.pages.job_review_page import JobReviewPage
|
||||||
from tests.e2e.pages.apply_page import ApplyPage
|
from tests.e2e.pages.apply_page import ApplyPage
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from pathlib import Path
|
|
||||||
import yaml
|
import yaml
|
||||||
from scripts.user_profile import UserProfile
|
from scripts.user_profile import UserProfile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
"""Tests for scripts/backup.py — create, list, restore, and multi-instance support."""
|
"""Tests for scripts/backup.py — create, list, restore, and multi-instance support."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from scripts.backup import (
|
from scripts.backup import (
|
||||||
_decrypt_db_to_bytes,
|
_decrypt_db_to_bytes,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""Tests for BYOK cloud backend detection."""
|
"""Tests for BYOK cloud backend detection."""
|
||||||
import pytest
|
|
||||||
from scripts.byok_guard import is_cloud_backend, cloud_backends
|
from scripts.byok_guard import is_cloud_backend, cloud_backends
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from datetime import timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import pytest
|
from unittest.mock import patch
|
||||||
import os
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_session_is_noop_in_local_mode(monkeypatch):
|
def test_resolve_session_is_noop_in_local_mode(monkeypatch):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
# tests/test_cover_letter.py
|
# tests/test_cover_letter.py
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +88,7 @@ def test_find_similar_letters_returns_top_k():
|
||||||
|
|
||||||
def test_load_corpus_returns_list():
|
def test_load_corpus_returns_list():
|
||||||
"""load_corpus returns a list (empty if LETTERS_DIR absent) without crashing."""
|
"""load_corpus returns a list (empty if LETTERS_DIR absent) without crashing."""
|
||||||
from scripts.generate_cover_letter import load_corpus, LETTERS_DIR
|
from scripts.generate_cover_letter import load_corpus
|
||||||
|
|
||||||
corpus = load_corpus()
|
corpus = load_corpus()
|
||||||
assert isinstance(corpus, list)
|
assert isinstance(corpus, list)
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,6 @@ class TestTaskRunnerCoverLetterParams:
|
||||||
patch("sqlite3.connect") as mock_conn, \
|
patch("sqlite3.connect") as mock_conn, \
|
||||||
patch("scripts.task_runner.generate_cover_letter_fn", mock_generate, create=True):
|
patch("scripts.task_runner.generate_cover_letter_fn", mock_generate, create=True):
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
mock_row = MagicMock()
|
mock_row = MagicMock()
|
||||||
mock_row.__iter__ = lambda s: iter(job.items())
|
mock_row.__iter__ = lambda s: iter(job.items())
|
||||||
mock_row.keys = lambda: job.keys()
|
mock_row.keys = lambda: job.keys()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from email.utils import format_datetime
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
import pytest
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import pytest
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_db_creates_jobs_table(tmp_path):
|
def test_init_db_creates_jobs_table(tmp_path):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Tests for scripts/db_migrate.py — numbered SQL migration runner."""
|
"""Tests for scripts/db_migrate.py — numbered SQL migration runner."""
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import textwrap
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"""Tests for resume library db helpers."""
|
"""Tests for resume library db helpers."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"""IS_DEMO write-block guard tests."""
|
"""IS_DEMO write-block guard tests."""
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Tests for app/components/demo_toolbar.py."""
|
"""Tests for app/components/demo_toolbar.py."""
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pytest
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"""Tests for digest queue API endpoints."""
|
"""Tests for digest queue API endpoints."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"""Tests for new dev-api.py endpoints: stage signals, email sync, signal dismiss."""
|
"""Tests for new dev-api.py endpoints: stage signals, email sync, signal dismiss."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""Tests for interview prep endpoints: research GET/generate/task, contacts GET."""
|
"""Tests for interview prep endpoints: research GET/generate/task, contacts GET."""
|
||||||
import json
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
@ -17,7 +16,6 @@ def client():
|
||||||
|
|
||||||
def test_get_research_found(client):
|
def test_get_research_found(client):
|
||||||
"""Returns research row (minus raw_output) when present."""
|
"""Returns research row (minus raw_output) when present."""
|
||||||
import sqlite3
|
|
||||||
mock_row = {
|
mock_row = {
|
||||||
"job_id": 1,
|
"job_id": 1,
|
||||||
"company_brief": "Acme Corp makes anvils.",
|
"company_brief": "Acme Corp makes anvils.",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
"""Tests for all settings API endpoints added in Tasks 1–8."""
|
"""Tests for all settings API endpoints added in Tasks 1–8."""
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import yaml
|
import yaml
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
# credential_store.py was merged to main repo — no worktree path manipulation needed
|
# credential_store.py was merged to main repo — no worktree path manipulation needed
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import yaml
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
# tests/test_discover.py
|
# tests/test_discover.py
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
SAMPLE_JOB = {
|
SAMPLE_JOB = {
|
||||||
"title": "Customer Success Manager",
|
"title": "Customer Success Manager",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""Unit tests for E2E harness models and helper utilities."""
|
"""Unit tests for E2E harness models and helper utilities."""
|
||||||
import fnmatch
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
import time
|
import time
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"""Tests for the feedback API backend."""
|
"""Tests for the feedback API backend."""
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
# ── mask_pii ──────────────────────────────────────────────────────────────────
|
# ── mask_pii ──────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"""Tests for imap_sync helpers (no live IMAP connection required)."""
|
"""Tests for imap_sync helpers (no live IMAP connection required)."""
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -510,7 +509,7 @@ def test_search_folder_special_gmail_name():
|
||||||
def test_get_existing_message_ids_excludes_null(tmp_path):
|
def test_get_existing_message_ids_excludes_null(tmp_path):
|
||||||
"""NULL message_id rows are excluded from the returned set."""
|
"""NULL message_id rows are excluded from the returned set."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from scripts.db import init_db, insert_job, add_contact
|
from scripts.db import init_db, insert_job
|
||||||
from scripts.imap_sync import _get_existing_message_ids
|
from scripts.imap_sync import _get_existing_message_ids
|
||||||
|
|
||||||
db_path = tmp_path / "test.db"
|
db_path = tmp_path / "test.db"
|
||||||
|
|
@ -980,7 +979,6 @@ def test_scan_todo_label_stage_signal_set_for_non_neutral(tmp_path):
|
||||||
|
|
||||||
def test_scan_todo_label_body_fallback_matches(tmp_path):
|
def test_scan_todo_label_body_fallback_matches(tmp_path):
|
||||||
"""Company name only in body[:300] still triggers a match (body fallback)."""
|
"""Company name only in body[:300] still triggers a match (body fallback)."""
|
||||||
from scripts.db import get_contacts
|
|
||||||
from scripts.imap_sync import _scan_todo_label
|
from scripts.imap_sync import _scan_todo_label
|
||||||
|
|
||||||
db_path = tmp_path / "test.db"
|
db_path = tmp_path / "test.db"
|
||||||
|
|
@ -1110,7 +1108,6 @@ def test_parse_message_large_body_not_truncated():
|
||||||
def test_parse_message_binary_attachment_no_crash():
|
def test_parse_message_binary_attachment_no_crash():
|
||||||
"""Email with binary attachment returns a valid dict without crashing."""
|
"""Email with binary attachment returns a valid dict without crashing."""
|
||||||
from scripts.imap_sync import _parse_message
|
from scripts.imap_sync import _parse_message
|
||||||
import email as _email
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,6 @@ def test_fields_returns_list_of_dicts():
|
||||||
def test_save_and_load_config(tmp_path):
|
def test_save_and_load_config(tmp_path):
|
||||||
"""save_config writes yaml; load_config reads it back."""
|
"""save_config writes yaml; load_config reads it back."""
|
||||||
from scripts.integrations.base import IntegrationBase
|
from scripts.integrations.base import IntegrationBase
|
||||||
import yaml
|
|
||||||
|
|
||||||
class TestIntegration(IntegrationBase):
|
class TestIntegration(IntegrationBase):
|
||||||
name = "savetest"
|
name = "savetest"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
import jwt as pyjwt
|
import jwt as pyjwt
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from unittest.mock import patch
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
import jwt as pyjwt
|
import jwt as pyjwt
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from pathlib import Path
|
|
||||||
import yaml
|
import yaml
|
||||||
from scripts.user_profile import UserProfile
|
from scripts.user_profile import UserProfile
|
||||||
from scripts.generate_llm_config import apply_service_urls
|
from scripts.generate_llm_config import apply_service_urls
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ def test_complete_without_images_skips_vision_service(tmp_path):
|
||||||
"""When images=None, vision_service backend is skipped."""
|
"""When images=None, vision_service backend is skipped."""
|
||||||
import yaml
|
import yaml
|
||||||
from scripts.llm_router import LLMRouter
|
from scripts.llm_router import LLMRouter
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
|
|
||||||
cfg = {
|
cfg = {
|
||||||
"fallback_order": ["vision_service"],
|
"fallback_order": ["vision_service"],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Tests for Peregrine's LLMRouter shim — priority fallback logic."""
|
"""Tests for Peregrine's LLMRouter shim — priority fallback logic."""
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, MagicMock, call
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
@ -54,7 +54,6 @@ def test_uses_local_yaml_when_present():
|
||||||
|
|
||||||
def test_falls_through_to_env_when_no_yamls():
|
def test_falls_through_to_env_when_no_yamls():
|
||||||
"""When no yaml files exist, super().__init__ is called with no args (env-var path)."""
|
"""When no yaml files exist, super().__init__ is called with no args (env-var path)."""
|
||||||
import scripts.llm_router as shim_mod
|
|
||||||
from circuitforge_core.llm import LLMRouter as _CoreLLMRouter
|
from circuitforge_core.llm import LLMRouter as _CoreLLMRouter
|
||||||
|
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
"""Integration tests for messaging endpoints."""
|
"""Integration tests for messaging endpoints."""
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
"""Tests for scripts/preflight.py additions: dual-GPU service table, size warning, VRAM check."""
|
"""Tests for scripts/preflight.py additions: dual-GPU service table, size warning, VRAM check."""
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import yaml
|
import yaml
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
# ── Service table ──────────────────────────────────────────────────────────────
|
# ── Service table ──────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Tests: preflight writes OLLAMA_HOST to .env when Ollama is adopted from host."""
|
"""Tests: preflight writes OLLAMA_HOST to .env when Ollama is adopted from host."""
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
Set CF_RERANKER_MOCK=1 to avoid loading real model weights during tests.
|
Set CF_RERANKER_MOCK=1 to avoid loading real model weights during tests.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
os.environ["CF_RERANKER_MOCK"] = "1"
|
os.environ["CF_RERANKER_MOCK"] = "1"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# tests/test_resume_optimizer.py
|
# tests/test_resume_optimizer.py
|
||||||
"""Tests for scripts/resume_optimizer.py"""
|
"""Tests for scripts/resume_optimizer.py"""
|
||||||
import json
|
import json
|
||||||
import pytest
|
from unittest.mock import patch
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
|
|
||||||
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
"""Unit tests for scripts.resume_sync — format transform between library and profile."""
|
"""Unit tests for scripts.resume_sync — format transform between library and profile."""
|
||||||
import json
|
|
||||||
import pytest
|
|
||||||
from scripts.resume_sync import (
|
from scripts.resume_sync import (
|
||||||
library_to_profile_content,
|
library_to_profile_content,
|
||||||
profile_to_library,
|
profile_to_library,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"""Integration tests for resume library<->profile sync endpoints."""
|
"""Integration tests for resume library<->profile sync endpoints."""
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
"""Tests for /api/resumes/* endpoints."""
|
"""Tests for /api/resumes/* endpoints."""
|
||||||
import json
|
|
||||||
import io
|
import io
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# tests/test_sync.py
|
# tests/test_sync.py
|
||||||
import pytest
|
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
SAMPLE_FM = {
|
SAMPLE_FM = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
|
@ -178,7 +176,6 @@ def test_submit_task_actually_completes(tmp_path):
|
||||||
def test_run_task_enrich_craigslist_success(tmp_path):
|
def test_run_task_enrich_craigslist_success(tmp_path):
|
||||||
"""enrich_craigslist task calls enrich_craigslist_fields and marks completed."""
|
"""enrich_craigslist task calls enrich_craigslist_fields and marks completed."""
|
||||||
from scripts.db import init_db, insert_job, insert_task, get_task_for_job
|
from scripts.db import init_db, insert_job, insert_task, get_task_for_job
|
||||||
from unittest.mock import MagicMock
|
|
||||||
db = tmp_path / "test.db"
|
db = tmp_path / "test.db"
|
||||||
init_db(db)
|
init_db(db)
|
||||||
job_id = insert_job(db, {
|
job_id = insert_job(db, {
|
||||||
|
|
@ -200,7 +197,7 @@ def test_run_task_enrich_craigslist_success(tmp_path):
|
||||||
|
|
||||||
def test_scrape_url_submits_enrich_craigslist_for_craigslist_job(tmp_path):
|
def test_scrape_url_submits_enrich_craigslist_for_craigslist_job(tmp_path):
|
||||||
"""After scrape_url completes for a craigslist job with empty company, enrich_craigslist is queued."""
|
"""After scrape_url completes for a craigslist job with empty company, enrich_craigslist is queued."""
|
||||||
from scripts.db import init_db, insert_job, insert_task, get_task_for_job
|
from scripts.db import init_db, insert_job, insert_task
|
||||||
db = tmp_path / "test.db"
|
db = tmp_path / "test.db"
|
||||||
init_db(db)
|
init_db(db)
|
||||||
job_id = insert_job(db, {
|
job_id = insert_job(db, {
|
||||||
|
|
@ -285,7 +282,7 @@ def test_wizard_generate_null_params_fails(tmp_path):
|
||||||
|
|
||||||
def test_wizard_generate_stores_result_as_json(tmp_path):
|
def test_wizard_generate_stores_result_as_json(tmp_path):
|
||||||
"""wizard_generate stores result JSON in error field on success."""
|
"""wizard_generate stores result JSON in error field on success."""
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
db = tmp_path / "t.db"
|
db = tmp_path / "t.db"
|
||||||
from scripts.db import init_db, insert_task
|
from scripts.db import init_db, insert_task
|
||||||
init_db(db)
|
init_db(db)
|
||||||
|
|
@ -311,7 +308,7 @@ def test_wizard_generate_stores_result_as_json(tmp_path):
|
||||||
|
|
||||||
def test_wizard_generate_feedback_appended_to_prompt(tmp_path):
|
def test_wizard_generate_feedback_appended_to_prompt(tmp_path):
|
||||||
"""feedback and previous_result fields in input_data are appended to the prompt."""
|
"""feedback and previous_result fields in input_data are appended to the prompt."""
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
db = tmp_path / "t.db"
|
db = tmp_path / "t.db"
|
||||||
from scripts.db import init_db, insert_task
|
from scripts.db import init_db, insert_task
|
||||||
init_db(db)
|
init_db(db)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -192,7 +191,6 @@ def test_max_queue_depth_logs_warning(tmp_db, caplog):
|
||||||
"""Queue depth overflow logs a WARNING."""
|
"""Queue depth overflow logs a WARNING."""
|
||||||
import logging
|
import logging
|
||||||
from scripts.db import insert_task
|
from scripts.db import insert_task
|
||||||
from scripts.task_scheduler import TaskSpec
|
|
||||||
|
|
||||||
s = TaskScheduler(tmp_db, _noop_run_task)
|
s = TaskScheduler(tmp_db, _noop_run_task)
|
||||||
s._max_queue_depth = 0 # immediately at limit
|
s._max_queue_depth = 0 # immediately at limit
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import pytest
|
from unittest.mock import patch, MagicMock
|
||||||
import os
|
|
||||||
from unittest.mock import patch, MagicMock, call
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_op_in_local_mode(monkeypatch):
|
def test_no_op_in_local_mode(monkeypatch):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# tests/test_user_profile.py
|
# tests/test_user_profile.py
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile, yaml
|
import yaml
|
||||||
from scripts.user_profile import UserProfile
|
from scripts.user_profile import UserProfile
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from unittest.mock import patch
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from app.wizard.tiers import can_use, tier_label, TIERS, FEATURES, BYOK_UNLOCKABLE
|
from app.wizard.tiers import can_use, tier_label, TIERS, BYOK_UNLOCKABLE
|
||||||
|
|
||||||
|
|
||||||
def test_tiers_list():
|
def test_tiers_list():
|
||||||
|
|
|
||||||
|
|
@ -352,8 +352,8 @@ with tab_fetch:
|
||||||
|
|
||||||
if not accounts:
|
if not accounts:
|
||||||
st.warning(
|
st.warning(
|
||||||
f"No accounts configured. Copy `config/label_tool.yaml.example` → "
|
"No accounts configured. Copy `config/label_tool.yaml.example` → "
|
||||||
f"`config/label_tool.yaml` and add your IMAP accounts.",
|
"`config/label_tool.yaml` and add your IMAP accounts.",
|
||||||
icon="⚠️",
|
icon="⚠️",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -625,7 +625,7 @@ with tab_stats:
|
||||||
st.markdown(f"**{len(labeled)} labeled emails total**")
|
st.markdown(f"**{len(labeled)} labeled emails total**")
|
||||||
|
|
||||||
# Show known labels first, then any custom labels
|
# Show known labels first, then any custom labels
|
||||||
all_display_labels = list(LABELS) + [l for l in counts if l not in LABELS]
|
all_display_labels = list(LABELS) + [lbl for lbl in counts if lbl not in LABELS]
|
||||||
max_count = max(counts.values()) if counts else 1
|
max_count = max(counts.values()) if counts else 1
|
||||||
for lbl in all_display_labels:
|
for lbl in all_display_labels:
|
||||||
if lbl not in counts:
|
if lbl not in counts:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue