feat: add research and contacts endpoints for interview prep
This commit is contained in:
parent
26484f111c
commit
dc158ba802
2 changed files with 206 additions and 0 deletions
49
dev-api.py
49
dev-api.py
|
|
@ -312,6 +312,55 @@ def cover_letter_task(job_id: int):
|
|||
}
|
||||
|
||||
|
||||
# ── Interview Prep endpoints ─────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/jobs/{job_id}/research")
|
||||
def get_research_brief(job_id: int):
|
||||
from scripts.db import get_research as _get_research
|
||||
row = _get_research(Path(DB_PATH), job_id=job_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="No research found for this job")
|
||||
row.pop("raw_output", None)
|
||||
return row
|
||||
|
||||
|
||||
@app.post("/api/jobs/{job_id}/research/generate")
|
||||
def generate_research(job_id: int):
|
||||
try:
|
||||
from scripts.task_runner import submit_task
|
||||
task_id, is_new = submit_task(db_path=Path(DB_PATH), task_type="company_research", job_id=job_id)
|
||||
return {"task_id": task_id, "is_new": is_new}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/jobs/{job_id}/research/task")
|
||||
def research_task_status(job_id: int):
|
||||
db = _get_db()
|
||||
row = db.execute(
|
||||
"SELECT status, stage, error FROM background_tasks "
|
||||
"WHERE task_type = 'company_research' AND job_id = ? "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(job_id,),
|
||||
).fetchone()
|
||||
db.close()
|
||||
if not row:
|
||||
return {"status": "none", "stage": None, "message": None}
|
||||
return {"status": row["status"], "stage": row["stage"], "message": row["error"]}
|
||||
|
||||
|
||||
@app.get("/api/jobs/{job_id}/contacts")
|
||||
def get_job_contacts(job_id: int):
|
||||
db = _get_db()
|
||||
rows = db.execute(
|
||||
"SELECT id, direction, subject, from_addr, body, received_at "
|
||||
"FROM job_contacts WHERE job_id = ? ORDER BY received_at DESC",
|
||||
(job_id,),
|
||||
).fetchall()
|
||||
db.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ── GET /api/jobs/:id/cover_letter/pdf ───────────────────────────────────────
|
||||
|
||||
@app.get("/api/jobs/{job_id}/cover_letter/pdf")
|
||||
|
|
|
|||
157
tests/test_dev_api_prep.py
Normal file
157
tests/test_dev_api_prep.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"""Tests for interview prep endpoints: research GET/generate/task, contacts GET."""
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
import sys
|
||||
sys.path.insert(0, "/Library/Development/CircuitForge/peregrine/.worktrees/feature-vue-spa")
|
||||
from dev_api import app
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
# ── /api/jobs/{id}/research ─────────────────────────────────────────────────
|
||||
|
||||
def test_get_research_found(client):
|
||||
"""Returns research row (minus raw_output) when present."""
|
||||
row = {
|
||||
"job_id": 1,
|
||||
"company_brief": "Acme Corp makes anvils.",
|
||||
"ceo_brief": "Wile E Coyote",
|
||||
"talking_points": "- Ask about roadrunner containment",
|
||||
"tech_brief": "Python, Rust",
|
||||
"funding_brief": "Series B",
|
||||
"red_flags": None,
|
||||
"accessibility_brief": None,
|
||||
"generated_at": "2026-03-20T12:00:00",
|
||||
"raw_output": "should be stripped",
|
||||
}
|
||||
with patch("scripts.db.get_research", return_value=row):
|
||||
resp = client.get("/api/jobs/1/research")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["company_brief"] == "Acme Corp makes anvils."
|
||||
assert "raw_output" not in data
|
||||
|
||||
|
||||
def test_get_research_not_found(client):
|
||||
"""Returns 404 when no research row exists for job."""
|
||||
with patch("scripts.db.get_research", return_value=None):
|
||||
resp = client.get("/api/jobs/99/research")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ── /api/jobs/{id}/research/generate ────────────────────────────────────────
|
||||
|
||||
def test_generate_research_new_task(client):
|
||||
"""POST generate returns task_id and is_new=True for fresh submission."""
|
||||
with patch("scripts.task_runner.submit_task", return_value=(42, True)):
|
||||
resp = client.post("/api/jobs/1/research/generate")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["task_id"] == 42
|
||||
assert data["is_new"] is True
|
||||
|
||||
|
||||
def test_generate_research_duplicate_task(client):
|
||||
"""POST generate returns is_new=False when task already queued."""
|
||||
with patch("scripts.task_runner.submit_task", return_value=(17, False)):
|
||||
resp = client.post("/api/jobs/1/research/generate")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["is_new"] is False
|
||||
|
||||
|
||||
def test_generate_research_error(client):
|
||||
"""POST generate returns 500 when submit_task raises."""
|
||||
with patch("scripts.task_runner.submit_task", side_effect=Exception("LLM unavailable")):
|
||||
resp = client.post("/api/jobs/1/research/generate")
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
# ── /api/jobs/{id}/research/task ────────────────────────────────────────────
|
||||
|
||||
def test_research_task_none(client):
|
||||
"""Returns status=none when no background task exists for job."""
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.return_value.fetchone.return_value = None
|
||||
with patch("dev_api._get_db", return_value=mock_db):
|
||||
resp = client.get("/api/jobs/1/research/task")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "none"
|
||||
assert data["stage"] is None
|
||||
assert data["message"] is None
|
||||
|
||||
|
||||
def test_research_task_running(client):
|
||||
"""Returns current status/stage/message for an active task."""
|
||||
mock_row = {"status": "running", "stage": "Scraping company site", "error": None}
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.return_value.fetchone.return_value = mock_row
|
||||
with patch("dev_api._get_db", return_value=mock_db):
|
||||
resp = client.get("/api/jobs/1/research/task")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "running"
|
||||
assert data["stage"] == "Scraping company site"
|
||||
assert data["message"] is None
|
||||
|
||||
|
||||
def test_research_task_failed(client):
|
||||
"""Returns message (mapped from error column) for failed task."""
|
||||
mock_row = {"status": "failed", "stage": None, "error": "LLM timeout"}
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.return_value.fetchone.return_value = mock_row
|
||||
with patch("dev_api._get_db", return_value=mock_db):
|
||||
resp = client.get("/api/jobs/1/research/task")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "failed"
|
||||
assert data["message"] == "LLM timeout"
|
||||
|
||||
|
||||
# ── /api/jobs/{id}/contacts ──────────────────────────────────────────────────
|
||||
|
||||
def test_get_contacts_empty(client):
|
||||
"""Returns empty list when job has no contacts."""
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.return_value.fetchall.return_value = []
|
||||
with patch("dev_api._get_db", return_value=mock_db):
|
||||
resp = client.get("/api/jobs/1/contacts")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
|
||||
|
||||
def test_get_contacts_list(client):
|
||||
"""Returns list of contact dicts for job."""
|
||||
mock_rows = [
|
||||
{"id": 1, "direction": "inbound", "subject": "Interview next week",
|
||||
"from_addr": "hr@acme.com", "body": "Hi! We'd like to...", "received_at": "2026-03-19T10:00:00"},
|
||||
{"id": 2, "direction": "outbound", "subject": "Re: Interview next week",
|
||||
"from_addr": None, "body": "Thank you!", "received_at": "2026-03-19T11:00:00"},
|
||||
]
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.return_value.fetchall.return_value = mock_rows
|
||||
with patch("dev_api._get_db", return_value=mock_db):
|
||||
resp = client.get("/api/jobs/1/contacts")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) == 2
|
||||
assert data[0]["direction"] == "inbound"
|
||||
assert data[1]["direction"] == "outbound"
|
||||
|
||||
|
||||
def test_get_contacts_ordered_by_received_at(client):
|
||||
"""Most recent contacts appear first (ORDER BY received_at DESC)."""
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.return_value.fetchall.return_value = []
|
||||
with patch("dev_api._get_db", return_value=mock_db):
|
||||
resp = client.get("/api/jobs/99/contacts")
|
||||
# Verify the SQL contains ORDER BY received_at DESC
|
||||
call_args = mock_db.execute.call_args
|
||||
sql = call_args[0][0]
|
||||
assert "ORDER BY received_at DESC" in sql
|
||||
Loading…
Reference in a new issue