diff --git a/src/job_research/apec/query_derivation.py b/src/job_research/apec/query_derivation.py index 921ec7e..868184d 100644 --- a/src/job_research/apec/query_derivation.py +++ b/src/job_research/apec/query_derivation.py @@ -5,14 +5,6 @@ def _normalize_term(raw_term: str) -> str: return " ".join(raw_term.split()) -def _normalize_constraint(raw_term: str) -> str: - term = _normalize_term(raw_term) - if term.lower().endswith(" only"): - return term[:-5].rstrip() - - return term - - def derive_apec_queries(profile: CandidateProfileOutput) -> list[str]: queries: list[str] = [] seen: set[str] = set() @@ -35,28 +27,22 @@ def derive_apec_queries(profile: CandidateProfileOutput) -> list[str]: support_terms = [_normalize_term(term) for term in profile.strengths] support_terms.extend(_normalize_term(term) for term in profile.skills_to_emphasize) - support_terms.extend(_normalize_constraint(term) for term in profile.constraints) - support_terms = [term for term in support_terms if term] - if support_terms: - for target_role in unique_roles[:2]: - add_query(target_role) + for target_role in unique_roles: + add_query(target_role) + if len(queries) == 5: + return queries - if unique_roles: - primary_role = unique_roles[0] - for term in support_terms: - add_query(f"{primary_role} {term}") - if len(queries) == 5: - break - else: - for term in support_terms: - add_query(term) - if len(queries) == 5: - break + if unique_roles: + primary_role = unique_roles[0] + for term in support_terms: + add_query(f"{primary_role} {term}") + if len(queries) == 5: + break else: - for target_role in unique_roles[:5]: - add_query(target_role) + for term in support_terms: + add_query(term) if len(queries) == 5: break diff --git a/src/job_research/cli.py b/src/job_research/cli.py index cf868ad..9a96701 100644 --- a/src/job_research/cli.py +++ b/src/job_research/cli.py @@ -108,10 +108,8 @@ def fetch_apec( data_root: Path = typer.Option( Path("data"), "--data-root", - exists=True, file_okay=False, dir_okay=True, - readable=True, help="Directory containing candidate-profile.yaml and Apec run artifacts.", ), ) -> None: @@ -211,11 +209,20 @@ def fetch_apec( listing_errors=listing_errors, ) + artifact_write_errors: list[str] = [] + try: _write_yaml(paths["listings"], [listing.model_dump(mode="json") for listing in deduplicated_listings]) + except OSError as exc: # pragma: no cover - defensive boundary + artifact_write_errors.append(f"listings.yaml: {exc}") + + try: _write_yaml(paths["run_meta"], run_meta.model_dump(mode="json")) except OSError as exc: # pragma: no cover - defensive boundary - typer.echo(f"Unable to write Apec run artifacts: {exc}", err=True) + artifact_write_errors.append(f"run-meta.yaml: {exc}") + + if artifact_write_errors: + typer.echo(f"Unable to write Apec run artifacts: {'; '.join(artifact_write_errors)}", err=True) raise typer.Exit(code=1) typer.echo( diff --git a/tests/apec/test_query_derivation.py b/tests/apec/test_query_derivation.py index 2b814b0..e8d3475 100644 --- a/tests/apec/test_query_derivation.py +++ b/tests/apec/test_query_derivation.py @@ -22,9 +22,9 @@ def test_derive_apec_queries_preserves_order_dedupes_and_caps_at_five() -> None: assert queries == [ "Data Engineer", "Analytics Engineer", - "Data Engineer Python", - "Data Engineer SQL", - "Data Engineer BigQuery", + "BI Engineer", + "Junior Data Platform Engineer", + "ML Engineer", ] @@ -51,7 +51,7 @@ def test_derive_apec_queries_uses_up_to_five_target_roles_when_no_support_terms_ ] -def test_derive_apec_queries_includes_constraints_in_support_term_queries() -> None: +def test_derive_apec_queries_uses_support_terms_without_constraints() -> None: profile = CandidateProfileOutput( target_roles=["Data Engineer"], strengths=["Python"], @@ -65,6 +65,4 @@ def test_derive_apec_queries_includes_constraints_in_support_term_queries() -> N "Data Engineer", "Data Engineer Python", "Data Engineer BigQuery", - "Data Engineer CDI", - "Data Engineer France", ] diff --git a/tests/test_apec_cli.py b/tests/test_apec_cli.py index bb6889a..1c2487e 100644 --- a/tests/test_apec_cli.py +++ b/tests/test_apec_cli.py @@ -95,21 +95,19 @@ def _snapshot_meta( def test_fetch_apec_fails_cleanly_when_candidate_profile_is_missing(tmp_path, monkeypatch) -> None: - data_root = tmp_path / "data" - data_root.mkdir() - class SpyApecAdapter: def __init__(self, max_listings: int = 50) -> None: raise AssertionError("adapter should not be created when the profile is missing") + monkeypatch.chdir(tmp_path) monkeypatch.setattr("job_research.cli.ApecAdapter", SpyApecAdapter) - result = CliRunner().invoke(app, ["fetch-apec", "--data-root", str(data_root)]) + result = CliRunner().invoke(app, ["fetch-apec"]) assert result.exit_code == 1 assert "candidate-profile.yaml" in result.stderr assert "Traceback" not in result.stderr - assert not (data_root / "apec").exists() + assert not (tmp_path / "data").exists() def test_fetch_apec_fails_cleanly_when_candidate_profile_yaml_is_malformed( @@ -341,6 +339,14 @@ def test_fetch_apec_reports_final_artifact_write_failures_cleanly( assert adapter.search_calls == [["Role From YAML"]] assert adapter.fetch_calls == ["https://example.test/job/123"] + run_dir = data_root / "apec" / "runs" / "2026-06-01T10-00-00Z" + if failing_filename == "listings.yaml": + assert not (run_dir / "listings.yaml").exists() + assert (run_dir / "run-meta.yaml").exists() + else: + assert (run_dir / "listings.yaml").exists() + assert not (run_dir / "run-meta.yaml").exists() + def test_fetch_apec_reads_profile_and_writes_run_artifacts(tmp_path, monkeypatch) -> None: data_root = tmp_path / "data"