diff --git a/dev-api.py b/dev-api.py index f74e7bf..f0203bb 100644 --- a/dev-api.py +++ b/dev-api.py @@ -306,7 +306,7 @@ def list_interviews(): sig_placeholders = ",".join("?" * len(job_ids)) excl_placeholders = ",".join("?" * len(SIGNAL_EXCLUDED)) sig_rows = db.execute( - f"SELECT id, job_id, subject, received_at, stage_signal " + f"SELECT id, job_id, subject, received_at, stage_signal, body, from_addr " f"FROM job_contacts " f"WHERE job_id IN ({sig_placeholders}) " f" AND suggestion_dismissed = 0 " @@ -321,6 +321,8 @@ def list_interviews(): "subject": sr["subject"], "received_at": sr["received_at"], "stage_signal": sr["stage_signal"], + "body": sr["body"], + "from_addr": sr["from_addr"], }) db.close() @@ -384,6 +386,34 @@ def dismiss_signal(signal_id: int): return {"ok": True} +# ── POST /api/stage-signals/{id}/reclassify ────────────────────────────── + +VALID_SIGNAL_LABELS = { + 'interview_scheduled', 'offer_received', 'rejected', + 'positive_response', 'survey_received', 'neutral', + 'event_rescheduled', 'unrelated', 'digest', +} + +class ReclassifyBody(BaseModel): + stage_signal: str + +@app.post("/api/stage-signals/{signal_id}/reclassify") +def reclassify_signal(signal_id: int, body: ReclassifyBody): + if body.stage_signal not in VALID_SIGNAL_LABELS: + raise HTTPException(400, f"Invalid label: {body.stage_signal}") + db = _get_db() + result = db.execute( + "UPDATE job_contacts SET stage_signal = ? WHERE id = ?", + (body.stage_signal, signal_id), + ) + rowcount = result.rowcount + db.commit() + db.close() + if rowcount == 0: + raise HTTPException(404, "Signal not found") + return {"ok": True} + + # ── POST /api/jobs/{id}/move ─────────────────────────────────────────────────── STATUS_TIMESTAMP_COL = { diff --git a/tests/test_dev_api_interviews.py b/tests/test_dev_api_interviews.py index 3c061b0..eb30f87 100644 --- a/tests/test_dev_api_interviews.py +++ b/tests/test_dev_api_interviews.py @@ -27,7 +27,9 @@ def tmp_db(tmp_path): subject TEXT, received_at TEXT, stage_signal TEXT, - suggestion_dismissed INTEGER DEFAULT 0 + suggestion_dismissed INTEGER DEFAULT 0, + body TEXT, + from_addr TEXT ); CREATE TABLE background_tasks ( id INTEGER PRIMARY KEY, @@ -73,6 +75,8 @@ def test_interviews_includes_stage_signals(client): assert signals[0]["stage_signal"] == "interview_scheduled" assert signals[0]["subject"] == "Interview confirmed" assert signals[0]["id"] == 10 + assert "body" in signals[0] + assert "from_addr" in signals[0] # neutral signal excluded signal_types = [s["stage_signal"] for s in signals] @@ -157,3 +161,42 @@ def test_dismiss_signal_sets_flag(client, tmp_db): def test_dismiss_signal_404_for_missing_id(client): resp = client.post("/api/stage-signals/9999/dismiss") assert resp.status_code == 404 + + +# ── Body/from_addr in signal response ───────────────────────────────────── + +def test_interviews_signal_includes_body_and_from_addr(client): + resp = client.get("/api/interviews") + assert resp.status_code == 200 + jobs = {j["id"]: j for j in resp.json()} + sig = jobs[1]["stage_signals"][0] + # Fields must exist (may be None when DB column is NULL) + assert "body" in sig + assert "from_addr" in sig + + +# ── POST /api/stage-signals/{id}/reclassify ──────────────────────────────── + +def test_reclassify_signal_updates_label(client, tmp_db): + resp = client.post("/api/stage-signals/10/reclassify", + json={"stage_signal": "positive_response"}) + assert resp.status_code == 200 + assert resp.json() == {"ok": True} + con = sqlite3.connect(tmp_db) + row = con.execute( + "SELECT stage_signal FROM job_contacts WHERE id = 10" + ).fetchone() + con.close() + assert row[0] == "positive_response" + + +def test_reclassify_signal_invalid_label(client): + resp = client.post("/api/stage-signals/10/reclassify", + json={"stage_signal": "not_a_real_label"}) + assert resp.status_code == 400 + + +def test_reclassify_signal_404_for_missing_id(client): + resp = client.post("/api/stage-signals/9999/reclassify", + json={"stage_signal": "neutral"}) + assert resp.status_code == 404