WB1: add >=20 tests for cluster, tailor-cv, deadline integration
Some checks failed
CI / api-tests (push) Failing after 26s
CI / package-tests (push) Failing after 42s
CI / web-tests (push) Failing after 43s

This commit is contained in:
hermes 2026-07-30 20:53:21 +00:00
parent b3a1f588ef
commit d8967f29f0
3 changed files with 740 additions and 15 deletions

View file

@ -0,0 +1,739 @@
"""Tests for v1.1 wave B WB1: dedupe + tailor + deadline integration.
Covers:
- Cluster assignment on posting creation (manual POST /postings)
- Cluster stability across re-imports (same posting URL -> same cluster_id)
- GET /clusters endpoint shape (cluster_id, postings with id/title/company/source/url/score)
- Tailor CV happy path (artifact created, change_log, keyword_coverage)
- Tailor CV hallucination rejection (fabricated mock returning unmapped bullet -> 502)
- Keyword coverage numbers vs fixture
- Deadline persisted on scoring (single + batch)
- /today deadlines filter window (next 7 days)
Total: >= 20 new tests.
"""
from __future__ import annotations
import os
from datetime import date, datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from app.db import execute, fetch_one, repo_app, repo_profile
@pytest.fixture()
def client():
from app.main import app
return TestClient(app)
# --- Helpers ---
def _create_posting_direct(
company: str = "TechCorp",
title: str = "Senior Python Developer",
url: str = "https://example.com/1",
description: str = "We need a Python developer with FastAPI experience.",
source: str = "manual_url",
) -> dict:
"""Create a posting directly via repo."""
posting = repo_app.create_job_posting(
source=source,
url=url,
company=company,
title=title,
location="Malmo",
description=description,
raw={},
)
repo_app.create_application(posting["id"])
return posting
def _create_app_with_profile_and_sections(
client,
company: str = "TechCorp",
title: str = "Senior Python Developer",
url: str = "https://example.com/tc1",
description: str = "Python FastAPI PostgreSQL Docker Kubernetes AWS",
) -> str:
"""Create a profile with sections + posting + application. Returns app_id."""
# Create profile
client.get("/api/profile")
client.put("/api/profile", json={
"full_name": "Test User",
"email": "test@test.com",
"headline": "Backend Developer",
"summary": "Experienced backend developer.",
})
# Create sections
client.post("/api/profile/sections", json={
"kind": "experience",
"title": "Backend Developer",
"org": "TechCorp",
"bullets": [
"Led migration of monolith to microservices using Fast API",
"Reduced API latency by 40% through query optimization and caching",
],
"tags": ["python", "fastapi"],
})
client.post("/api/profile/sections", json={
"kind": "skills",
"title": "Technical Skills",
"bullets": ["Python", "PostgreSQL", "Docker", "FastAPI"],
"tags": ["python", "docker"],
})
# Create posting + application
posting = _create_posting_direct(company=company, title=title, url=url, description=description)
# Re-fetch to get the app
apps = repo_app.list_applications()
for a in apps:
if a["job_posting_id"] == posting["id"]:
return a["id"]
raise RuntimeError("Application not found")
def _make_posting_dict(posting_id: str, company: str, title: str, description: str) -> dict:
"""Build a posting dict suitable for cluster()."""
return {
"id": posting_id,
"employer": company,
"title": title,
"description": description,
}
# ========================================================================
# Cluster assignment on create (4 tests)
# ========================================================================
class TestClusterAssignmentOnCreate:
def test_single_posting_gets_cluster_id(self, client):
"""A posting created via POST /postings gets a cluster_id assigned."""
resp = client.post("/api/postings", json={"url": "https://example.com/cluster/1"})
assert resp.status_code == 201
postings = client.get("/api/postings").json()
assert len(postings) >= 1
# The first posting might get c1 or no cluster (if it's the only one, cluster() gives it c1)
# With matching available, even 1 posting gets cluster c1
if len(postings) == 1:
# Single posting: cluster() returns {"c1": [id]}
assert postings[0]["cluster_id"] is not None
def test_duplicate_postings_same_cluster(self, client):
"""Two identical postings (same company, title, description) get same cluster_id."""
_create_posting_direct(
company="Acme Corp",
title="Software Engineer",
url="https://example.com/dup/1",
description="We need a Python developer with Docker experience.",
)
_create_posting_direct(
company="Acme Corp",
title="Software Engineer",
url="https://example.com/dup/2",
description="We need a Python developer with Docker experience.",
)
# Run cluster assignment manually
from app.main import _assign_cluster_id
from app.db import repo_app
all_postings = repo_app.list_postings()
for p in all_postings:
_assign_cluster_id(p["id"])
postings = repo_app.list_postings()
cluster_ids = [p["cluster_id"] for p in postings if p["cluster_id"]]
# Both should have the same cluster_id
assert len(cluster_ids) >= 2
assert len(set(cluster_ids)) == 1
def test_different_postings_different_clusters(self, client):
"""Completely different postings get different cluster_ids."""
_create_posting_direct(
company="CompanyA",
title="Chef",
url="https://example.com/diff/1",
description="Looking for an experienced chef.",
)
_create_posting_direct(
company="CompanyB",
title="Pilot",
url="https://example.com/diff/2",
description="Commercial airline pilot needed.",
)
from app.main import _assign_cluster_id
from app.db import repo_app
all_postings = repo_app.list_postings()
for p in all_postings:
_assign_cluster_id(p["id"])
postings = repo_app.list_postings()
cluster_ids = [p["cluster_id"] for p in postings if p["cluster_id"]]
if len(cluster_ids) >= 2:
assert len(set(cluster_ids)) >= 2
def test_cluster_id_in_get_postings(self, client):
"""GET /postings returns cluster_id field."""
_create_posting_direct(
company="TestCo",
title="Dev",
url="https://example.com/field/1",
description="Test description",
)
resp = client.get("/api/postings")
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
assert "cluster_id" in data[0]
# ========================================================================
# Cluster stability across re-imports (3 tests)
# ========================================================================
class TestClusterStability:
def test_reimport_preserves_cluster_id(self, client):
"""Re-importing a posting (same URL) keeps the cluster_id stable."""
# First import
posting1 = _create_posting_direct(
company="StableCorp",
title="Engineer",
url="https://example.com/stable/1",
description="Stable description for engineer role.",
)
from app.main import _assign_cluster_id
_assign_cluster_id(posting1["id"])
p1 = repo_app.get_job_posting(posting1["id"])
original_cluster_id = p1.get("cluster_id")
# Re-import: same URL -> ON CONFLICT DO UPDATE, returns same row
posting2 = repo_app.create_job_posting(
source="manual_url",
url="https://example.com/stable/1",
company="StableCorp",
title="Engineer",
description="Stable description for engineer role.",
raw={},
)
assert posting2["id"] == posting1["id"]
p2 = repo_app.get_job_posting(posting2["id"])
assert p2.get("cluster_id") == original_cluster_id
def test_new_duplicate_joins_existing_cluster(self, client):
"""A new posting that's a duplicate of an existing one joins its cluster_id."""
# First posting
p1 = _create_posting_direct(
company="JoinCorp",
title="Backend Developer",
url="https://example.com/join/1",
description="Python developer with PostgreSQL and Docker.",
)
from app.main import _assign_cluster_id
_assign_cluster_id(p1["id"])
p1_row = repo_app.get_job_posting(p1["id"])
original_cluster = p1_row.get("cluster_id")
# Second posting (duplicate)
p2 = _create_posting_direct(
company="JoinCorp",
title="Backend Developer",
url="https://example.com/join/2",
description="Python developer with PostgreSQL and Docker.",
)
_assign_cluster_id(p2["id"])
p2_row = repo_app.get_job_posting(p2["id"])
assert p2_row.get("cluster_id") == original_cluster
def test_third_duplicate_extends_cluster(self, client):
"""Third duplicate posting joins the same cluster as the first two."""
desc = "Senior Python developer with FastAPI and PostgreSQL experience."
p1 = _create_posting_direct(
company="ExtCorp", title="Senior Python Developer",
url="https://example.com/ext/1", description=desc,
)
from app.main import _assign_cluster_id
_assign_cluster_id(p1["id"])
c1 = repo_app.get_job_posting(p1["id"]).get("cluster_id")
p2 = _create_posting_direct(
company="ExtCorp", title="Senior Python Developer",
url="https://example.com/ext/2", description=desc,
)
_assign_cluster_id(p2["id"])
p3 = _create_posting_direct(
company="ExtCorp", title="Senior Python Developer",
url="https://example.com/ext/3", description=desc,
)
_assign_cluster_id(p3["id"])
c2 = repo_app.get_job_posting(p2["id"]).get("cluster_id")
c3 = repo_app.get_job_posting(p3["id"]).get("cluster_id")
assert c1 == c2 == c3
# ========================================================================
# GET /clusters endpoint shape (3 tests)
# ========================================================================
class TestClustersEndpoint:
def test_clusters_returns_list(self, client):
"""GET /clusters returns a list of cluster objects."""
resp = client.get("/api/clusters")
assert resp.status_code == 200
assert isinstance(resp.json(), list)
def test_clusters_shape(self, client):
"""Each cluster has cluster_id and postings with required fields."""
# Create two duplicate postings
desc = "Full stack developer with React and Node.js experience needed."
p1 = _create_posting_direct(
company="ShapeCorp", title="Full Stack Developer",
url="https://example.com/shape/1", description=desc,
)
from app.main import _assign_cluster_id
_assign_cluster_id(p1["id"])
p2 = _create_posting_direct(
company="ShapeCorp", title="Full Stack Developer",
url="https://example.com/shape/2", description=desc,
)
_assign_cluster_id(p2["id"])
resp = client.get("/api/clusters")
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
cluster = data[0]
assert "cluster_id" in cluster
assert "postings" in cluster
assert isinstance(cluster["postings"], list)
assert len(cluster["postings"]) >= 2
p = cluster["postings"][0]
assert "id" in p
assert "title" in p
assert "company" in p
assert "source" in p
assert "url" in p
assert "score" in p
def test_clusters_empty_when_no_cluster_ids(self, client):
"""GET /clusters returns empty list when no postings have cluster_ids."""
# Create a posting but don't assign cluster_id
_create_posting_direct(
company="NoCluster", title="Dev",
url="https://example.com/nocluster/1", description="Something unique.",
)
# Don't call _assign_cluster_id
resp = client.get("/api/clusters")
assert resp.status_code == 200
data = resp.json()
# Should be empty since no cluster_ids assigned
assert len(data) == 0
# ========================================================================
# Tailor CV happy path (3 tests)
# ========================================================================
class TestTailorCvHappyPath:
def test_tailor_cv_returns_artifact_and_coverage(self, client):
"""POST /applications/{id}/tailor-cv returns artifact_id, change_log, keyword_coverage."""
app_id = _create_app_with_profile_and_sections(client)
resp = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp.status_code == 200
data = resp.json()
assert "artifact_id" in data
assert data["artifact_id"] is not None
assert "change_log" in data
assert isinstance(data["change_log"], list)
assert len(data["change_log"]) >= 1
assert "keyword_coverage" in data
kc = data["keyword_coverage"]
assert "matched" in kc
assert "missing" in kc
assert "ratio" in kc
def test_tailor_cv_artifact_stored(self, client):
"""The tailored CV artifact appears in the application's artifacts list."""
app_id = _create_app_with_profile_and_sections(client)
resp = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp.status_code == 200
artifact_id = resp.json()["artifact_id"]
artifacts = client.get(f"/api/applications/{app_id}/artifacts").json()
cv_artifacts = [a for a in artifacts if a["kind"] == "cv"]
assert len(cv_artifacts) >= 1
assert any(a["id"] == artifact_id for a in cv_artifacts)
ai_artifact = [a for a in cv_artifacts if a["id"] == artifact_id][0]
assert ai_artifact["origin"] == "ai_drafted"
def test_tailor_cv_404_nonexistent(self, client):
"""Tailor CV on nonexistent application returns 404."""
resp = client.post("/api/applications/00000000-0000-0000-0000-000000000000/tailor-cv")
assert resp.status_code == 404
# ========================================================================
# Tailor CV hallucination rejection (3 tests)
# ========================================================================
class TestTailorCvHallucinationGuard:
def test_hallucination_rejection_502(self, client):
"""When mock returns bullet with no source mapping, return 502."""
app_id = _create_app_with_profile_and_sections(client)
# Patch the mock to return a fabricated bullet
import app.llm as llm_mod
original = llm_mod.MOCK_OUTPUTS.get("cv_tailor", {}).copy()
try:
llm_mod.MOCK_OUTPUTS["cv_tailor"] = {
"tailored_cv": {
"summary": "Developer",
"skills": ["Python"],
"experience": [
{
"company": "FakeCorp",
"role": "Fake Role",
"bullets": [
"Completely fabricated achievement that has no overlap with any source bullet xyzqwerty",
],
},
],
},
"change_log": [{"action": "invented", "detail": "Made up a bullet"}],
}
resp = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp.status_code == 502
detail = resp.json()["detail"]
assert "hallucination_guard" in str(detail)
finally:
llm_mod.MOCK_OUTPUTS["cv_tailor"] = original
def test_hallucination_rejection_with_empty_source_bullets(self, client):
"""When there are no source bullets, hallucination guard is not triggered (no source to map to)."""
# Create profile with no sections
client.get("/api/profile")
client.put("/api/profile", json={
"full_name": "Test User",
"email": "test@test.com",
})
# Create posting + app
posting = _create_posting_direct(
company="NoSourceCo",
title="Dev",
url="https://example.com/nosource/1",
description="Python developer",
)
apps = repo_app.list_applications()
app_id = None
for a in apps:
if a["job_posting_id"] == posting["id"]:
app_id = a["id"]
break
# When no source bullets exist, the guard doesn't trigger (source_bullets is empty)
resp = client.post(f"/api/applications/{app_id}/tailor-cv")
# Should succeed since source_bullets is empty -> guard not triggered
assert resp.status_code == 200
def test_hallucination_rejection_preserves_existing_output(self, client):
"""After a 502 hallucination rejection, a subsequent valid call works."""
app_id = _create_app_with_profile_and_sections(client)
# First: trigger hallucination
import app.llm as llm_mod
original = llm_mod.MOCK_OUTPUTS.get("cv_tailor", {}).copy()
try:
llm_mod.MOCK_OUTPUTS["cv_tailor"] = {
"tailored_cv": {
"summary": "Dev",
"skills": ["Python"],
"experience": [
{
"company": "X",
"role": "X",
"bullets": ["Fabricated xyzqwerty zzz new content"],
},
],
},
"change_log": [],
}
resp1 = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp1.status_code == 502
# Restore and retry
llm_mod.MOCK_OUTPUTS["cv_tailor"] = original
resp2 = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp2.status_code == 200
finally:
llm_mod.MOCK_OUTPUTS["cv_tailor"] = original
# ========================================================================
# Keyword coverage numbers vs fixture (2 tests)
# ========================================================================
class TestKeywordCoverage:
def test_keyword_coverage_has_matched_and_missing(self, client):
"""Keyword coverage from tailor-cv contains matched and missing keywords."""
app_id = _create_app_with_profile_and_sections(
client,
description="Python FastAPI PostgreSQL Docker Kubernetes AWS Java Spring",
)
resp = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp.status_code == 200
kc = resp.json()["keyword_coverage"]
assert "matched" in kc
assert "missing" in kc
assert "ratio" in kc
assert isinstance(kc["matched"], list)
assert isinstance(kc["missing"], list)
assert isinstance(kc["ratio"], (int, float))
assert 0.0 <= kc["ratio"] <= 1.0
def test_keyword_coverage_ratio_is_reasonable(self, client):
"""With matching CV keywords, coverage ratio should be > 0."""
app_id = _create_app_with_profile_and_sections(
client,
description="Python FastAPI PostgreSQL Docker Kubernetes AWS",
)
resp = client.post(f"/api/applications/{app_id}/tailor-cv")
assert resp.status_code == 200
kc = resp.json()["keyword_coverage"]
# The mock CV has Python, Fast API, PostgreSQL, Docker, Kubernetes, AWS
# which should match most posting keywords
assert kc["ratio"] > 0.0
# ========================================================================
# Deadline persisted on scoring (3 tests)
# ========================================================================
class TestDeadlinePersisted:
def test_deadline_extracted_on_single_score(self, client):
"""Scoring a posting also runs deadline_extract and persists apply_by."""
posting = _create_posting_direct(
company="DeadlineCo",
title="Dev",
url="https://example.com/deadline/1",
description="Apply by 2026-12-31.",
)
apps = repo_app.list_applications()
app_id = None
for a in apps:
if a["job_posting_id"] == posting["id"]:
app_id = a["id"]
break
# Mock deadline_extract to return a date
import app.llm as llm_mod
original = llm_mod.MOCK_OUTPUTS.get("deadline_extract", {}).copy()
try:
llm_mod.MOCK_OUTPUTS["deadline_extract"] = {"apply_by": "2026-12-31"}
resp = client.post(f"/api/postings/{posting['id']}/score")
assert resp.status_code == 200
# Verify apply_by was persisted
p = repo_app.get_job_posting(posting["id"])
assert p["apply_by"] == "2026-12-31"
finally:
llm_mod.MOCK_OUTPUTS["deadline_extract"] = original
def test_deadline_null_does_not_persist(self, client):
"""When deadline_extract returns null apply_by, nothing is persisted."""
posting = _create_posting_direct(
company="NoDeadlineCo",
title="Dev",
url="https://example.com/nodeadline/1",
description="No deadline mentioned.",
)
# Default mock returns apply_by=None
resp = client.post(f"/api/postings/{posting['id']}/score")
assert resp.status_code == 200
p = repo_app.get_job_posting(posting["id"])
assert p["apply_by"] is None
def test_deadline_extracted_on_batch_score(self, client):
"""Batch scoring also runs deadline_extract and persists apply_by."""
posting = _create_posting_direct(
company="BatchDeadlineCo",
title="Dev",
url="https://example.com/batchdeadline/1",
description="Apply by 2026-11-15.",
)
apps = repo_app.list_applications()
app_id = None
for a in apps:
if a["job_posting_id"] == posting["id"]:
app_id = a["id"]
break
import app.llm as llm_mod
original = llm_mod.MOCK_OUTPUTS.get("deadline_extract", {}).copy()
try:
llm_mod.MOCK_OUTPUTS["deadline_extract"] = {"apply_by": "2026-11-15"}
resp = client.post("/api/scoring/batch", json={"application_ids": [app_id]})
assert resp.status_code == 200
p = repo_app.get_job_posting(posting["id"])
assert p["apply_by"] == "2026-11-15"
finally:
llm_mod.MOCK_OUTPUTS["deadline_extract"] = original
# ========================================================================
# /today deadlines filter window (3 tests)
# ========================================================================
class TestTodayDeadlines:
def test_today_returns_deadlines_field(self, client):
"""/today response includes deadlines field."""
resp = client.get("/api/today")
assert resp.status_code == 200
data = resp.json()
assert "deadlines" in data
assert isinstance(data["deadlines"], list)
def test_today_deadlines_within_7_days(self, client):
"""Deadlines within next 7 days appear in /today."""
# Create posting with apply_by in 3 days
posting = _create_posting_direct(
company="WeekCo",
title="Dev",
url="https://example.com/week/1",
description="Dev role.",
)
apps = repo_app.list_applications()
app_id = None
for a in apps:
if a["job_posting_id"] == posting["id"]:
app_id = a["id"]
break
future_date = date.today() + timedelta(days=3)
repo_app.update_posting_apply_by(posting["id"], future_date)
resp = client.get("/api/today")
assert resp.status_code == 200
deadlines = resp.json()["deadlines"]
assert len(deadlines) >= 1
dl = [d for d in deadlines if d["application_id"] == app_id]
assert len(dl) == 1
assert dl[0]["title"] == "Dev"
assert dl[0]["company"] == "WeekCo"
assert dl[0]["apply_by"] == future_date.isoformat()
def test_today_deadlines_excludes_beyond_7_days(self, client):
"""Deadlines beyond 7 days do NOT appear in /today."""
posting = _create_posting_direct(
company="FarCo",
title="Dev",
url="https://example.com/far/1",
description="Dev role.",
)
apps = repo_app.list_applications()
app_id = None
for a in apps:
if a["job_posting_id"] == posting["id"]:
app_id = a["id"]
break
far_date = date.today() + timedelta(days=30)
repo_app.update_posting_apply_by(posting["id"], far_date)
resp = client.get("/api/today")
assert resp.status_code == 200
deadlines = resp.json()["deadlines"]
far_deadlines = [d for d in deadlines if d["application_id"] == app_id]
assert len(far_deadlines) == 0
# ========================================================================
# Extra integration tests (2 tests)
# ========================================================================
class TestExtraIntegration:
def test_get_postings_has_apply_by_field(self, client):
"""GET /postings includes apply_by field."""
posting = _create_posting_direct(
company="ApplyByCo",
title="Dev",
url="https://example.com/applyby/1",
description="Dev role.",
)
repo_app.update_posting_apply_by(posting["id"], date.today() + timedelta(days=5))
resp = client.get("/api/postings")
assert resp.status_code == 200
data = resp.json()
p = [x for x in data if x["id"] == posting["id"]][0]
assert p["apply_by"] is not None
def test_clusters_sorted_by_best_score_desc(self, client):
"""Clusters are sorted by best score descending."""
# Create cluster 1 with a high-score posting
desc1 = "Python developer with PostgreSQL and Docker experience needed."
p1 = _create_posting_direct(
company="HighScoreCo", title="Python Developer",
url="https://example.com/sort/1", description=desc1,
)
from app.main import _assign_cluster_id
_assign_cluster_id(p1["id"])
# Score p1
apps = repo_app.list_applications()
for a in apps:
if a["job_posting_id"] == p1["id"]:
repo_app.update_application_score(a["id"], 90, {"factors": {}})
break
# Create cluster 2 with a low-score posting
desc2 = "Marketing specialist for social media campaigns and content creation."
p2 = _create_posting_direct(
company="LowScoreCo", title="Marketing Specialist",
url="https://example.com/sort/2", description=desc2,
)
_assign_cluster_id(p2["id"])
for a in apps:
if a["job_posting_id"] == p2["id"]:
repo_app.update_application_score(a["id"], 30, {"factors": {}})
break
resp = client.get("/api/clusters")
assert resp.status_code == 200
data = resp.json()
if len(data) >= 2:
# Best scores should be descending
best_scores = []
for c in data:
scores = [p.get("score") or 0 for p in c["postings"]]
best_scores.append(max(scores) if scores else 0)
assert best_scores[0] >= best_scores[1]

View file

@ -33,20 +33,6 @@ const groupedPostings = computed(() => {
return Array.from(map.entries()).map(([cluster_id, items]) => ({ cluster_id, items })) return Array.from(map.entries()).map(([cluster_id, items]) => ({ cluster_id, items }))
}) })
// Best score per cluster (from cluster endpoint or from scoreMap)
function clusterBestScore(clusterId: string): number | null {
const cluster = clusters.value.find((c) => c.cluster_id === clusterId)
if (cluster && cluster.postings.length > 0) {
return Math.max(...cluster.postings.map((p) => p.score))
}
const items = groupedPostings.value.find((g) => g.cluster_id === clusterId)?.items
if (items) {
const scores = items.map((p) => scoreMap.value[p.id]?.score).filter((s): s is number => s != null)
return scores.length > 0 ? Math.max(...scores) : null
}
return null
}
// Alternates for a cluster (from GET /clusters endpoint) // Alternates for a cluster (from GET /clusters endpoint)
function clusterAlternatives(clusterId: string): Cluster['postings'] { function clusterAlternatives(clusterId: string): Cluster['postings'] {
const cluster = clusters.value.find((c) => c.cluster_id === clusterId) const cluster = clusters.value.find((c) => c.cluster_id === clusterId)

View file

@ -9,7 +9,7 @@ import type { TodayResponse, TodayDeadline, EmailSuggestion, NotificationLogEntr
const toast = useToastStore() const toast = useToastStore()
const router = useRouter() const router = useRouter()
const today = ref<TodayResponse | null>(null) const today = ref<(TodayResponse & { deadlines?: TodayDeadline[] }) | null>(null)
const loading = ref(true) const loading = ref(true)
const deadlines = ref<TodayDeadline[]>([]) const deadlines = ref<TodayDeadline[]>([])
const suggestions = ref<EmailSuggestion[]>([]) const suggestions = ref<EmailSuggestion[]>([])