From 0932c95f403372a141cbb7264a6fa05663036273 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Wed, 1 Jul 2026 00:05:16 -0400 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Batch modularize oversized validation and acceptance helpers","authority":"manual"} --- .../competitor_strength_profile.rs | 456 +---------------- .../competitor_strength_profile/boundaries.rs | 147 ++++++ .../competitor_strength_profile/openviking.rs | 73 +++ .../competitor_strength_profile/qmd.rs | 91 ++++ .../summary_terms.rs | 138 ++++++ .../src/docs/tests_put_validation.rs | 448 +---------------- .../tests_put_validation/core_rejections.rs | 120 +++++ .../docs/tests_put_validation/english_gate.rs | 68 +++ .../tests_put_validation/source_library.rs | 95 ++++ .../tests_put_validation/typed_source_refs.rs | 107 ++++ .../docs/tests_put_validation/write_policy.rs | 57 +++ .../src/docs/tests_search_validation.rs | 422 +--------------- .../filter_building.rs | 106 ++++ .../request_validation.rs | 226 +++++++++ .../tests_search_validation/sparse_mode.rs | 52 ++ .../docs/tests_search_validation/support.rs | 42 ++ .../acceptance/structured_field_retrieval.rs | 467 +----------------- .../structured_facts.rs | 140 ++++++ .../structured_field_retrieval/support.rs | 360 ++++++++++++++ .../acceptance/trace_admin_observability.rs | 10 +- .../trace_admin_observability/helpers.rs | 411 +-------------- .../helpers/assertions.rs | 47 ++ .../helpers/inserts.rs | 259 ++++++++++ .../trace_admin_observability/helpers/seed.rs | 81 +++ .../helpers/setup.rs | 44 ++ 25 files changed, 2290 insertions(+), 2177 deletions(-) create mode 100644 apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/boundaries.rs create mode 100644 apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/openviking.rs create mode 100644 apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/qmd.rs create mode 100644 apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/summary_terms.rs create mode 100644 packages/elf-service/src/docs/tests_put_validation/core_rejections.rs create mode 100644 packages/elf-service/src/docs/tests_put_validation/english_gate.rs create mode 100644 packages/elf-service/src/docs/tests_put_validation/source_library.rs create mode 100644 packages/elf-service/src/docs/tests_put_validation/typed_source_refs.rs create mode 100644 packages/elf-service/src/docs/tests_put_validation/write_policy.rs create mode 100644 packages/elf-service/src/docs/tests_search_validation/filter_building.rs create mode 100644 packages/elf-service/src/docs/tests_search_validation/request_validation.rs create mode 100644 packages/elf-service/src/docs/tests_search_validation/sparse_mode.rs create mode 100644 packages/elf-service/src/docs/tests_search_validation/support.rs create mode 100644 packages/elf-service/tests/acceptance/structured_field_retrieval/structured_facts.rs create mode 100644 packages/elf-service/tests/acceptance/structured_field_retrieval/support.rs create mode 100644 packages/elf-service/tests/acceptance/trace_admin_observability/helpers/assertions.rs create mode 100644 packages/elf-service/tests/acceptance/trace_admin_observability/helpers/inserts.rs create mode 100644 packages/elf-service/tests/acceptance/trace_admin_observability/helpers/seed.rs create mode 100644 packages/elf-service/tests/acceptance/trace_admin_observability/helpers/setup.rs diff --git a/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile.rs b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile.rs index 2829183c..adc9eaa3 100644 --- a/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile.rs +++ b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile.rs @@ -1,6 +1,11 @@ +mod boundaries; +mod openviking; +mod qmd; +mod summary_terms; + use std::fs; -use color_eyre::{Result, eyre}; +use color_eyre::Result; use serde_json::Value; use crate::support; @@ -15,14 +20,14 @@ fn qmd_openviking_strength_profile_report_preserves_claim_boundaries() -> Result let benchmarking_index = fs::read_to_string(support::benchmarking_index_path()?)?; let iteration_direction = fs::read_to_string(support::iteration_direction_report_path()?)?; - assert_strength_profile_summary(&report); - assert_strength_profile_terms(&report)?; - assert_qmd_strength_profile(&report)?; - assert_qmd_wrong_result_diagnosis(&report)?; - assert_openviking_strength_profile(&report)?; - assert_strength_profile_json_claim_boundaries(&report)?; - assert_strength_profile_markdown_boundaries(&markdown); - assert_operator_facing_strength_profile_boundaries( + summary_terms::assert_strength_profile_summary(&report); + summary_terms::assert_strength_profile_terms(&report)?; + qmd::assert_qmd_strength_profile(&report)?; + qmd::assert_qmd_wrong_result_diagnosis(&report)?; + openviking::assert_openviking_strength_profile(&report)?; + boundaries::assert_strength_profile_json_claim_boundaries(&report)?; + boundaries::assert_strength_profile_markdown_boundaries(&markdown); + boundaries::assert_operator_facing_strength_profile_boundaries( &readme, &benchmarking_index, &iteration_direction, @@ -30,436 +35,3 @@ fn qmd_openviking_strength_profile_report_preserves_claim_boundaries() -> Result Ok(()) } - -fn assert_strength_profile_summary(report: &Value) { - assert_eq!( - report.pointer("/schema").and_then(Value::as_str), - Some("elf.competitor_strength_profile_report/v1") - ); - assert_eq!( - report.pointer("/summary/qmd/retrieval_quality").and_then(Value::as_str), - Some("tie") - ); - assert_eq!( - report.pointer("/summary/qmd/local_query_transparency").and_then(Value::as_str), - Some("not_tested") - ); - assert_eq!( - report.pointer("/summary/qmd/local_replayability").and_then(Value::as_str), - Some("not_tested") - ); - assert_eq!( - report.pointer("/summary/qmd/overall_outcome").and_then(Value::as_str), - Some("not_tested") - ); - assert_eq!( - report.pointer("/summary/openviking/overall_outcome").and_then(Value::as_str), - Some("not_tested") - ); - assert_eq!( - report - .pointer("/qmd_strength_profile/win_tie_loss_summary/elf_win") - .and_then(Value::as_u64), - Some(0) - ); - assert_eq!( - report.pointer("/qmd_strength_profile/win_tie_loss_summary/tie").and_then(Value::as_u64), - Some(3) - ); - assert_eq!( - report - .pointer("/qmd_strength_profile/win_tie_loss_summary/elf_loss") - .and_then(Value::as_u64), - Some(0) - ); - assert_eq!( - report - .pointer("/qmd_strength_profile/win_tie_loss_summary/not_tested") - .and_then(Value::as_u64), - Some(5) - ); - assert_eq!( - report - .pointer("/openviking_context_trajectory_profile/win_tie_loss_summary/not_tested") - .and_then(Value::as_u64), - Some(5) - ); - assert_eq!( - report - .pointer("/openviking_context_trajectory_profile/win_tie_loss_summary/elf_win") - .and_then(Value::as_u64), - Some(1) - ); -} - -fn assert_strength_profile_terms(report: &Value) -> Result<()> { - let result_terms = support::array_at(report, "/result_type_terms")?; - let coverage_terms = support::array_at(report, "/coverage_status_terms")?; - let outcome_terms = support::array_at(report, "/outcome_terms")?; - let actual_result_terms = support::string_array_at(report, "/result_type_terms")?; - let actual_coverage_terms = support::string_array_at(report, "/coverage_status_terms")?; - - assert_eq!( - actual_result_terms, - [ - "pass", - "wrong_result", - "blocked", - "incomplete", - "lifecycle_fail", - "not_encoded", - "unsupported_claim", - ] - .map(str::to_owned) - ); - assert_eq!( - actual_coverage_terms, - [ - "pass", - "wrong_result", - "blocked", - "incomplete", - "lifecycle_fail", - "not_encoded", - "unsupported", - "unsupported_claim", - ] - .map(str::to_owned) - ); - assert!(!result_terms.iter().any(|term| term.as_str() == Some("unsupported"))); - assert!(!result_terms.iter().any(|term| term.as_str() == Some("partial"))); - assert!(!coverage_terms.iter().any(|term| term.as_str() == Some("partial"))); - assert!(result_terms.iter().any(|term| term.as_str() == Some("unsupported_claim"))); - assert!(coverage_terms.iter().any(|term| term.as_str() == Some("unsupported"))); - - assert_value_in_terms(report, "/summary/qmd/overall_outcome", outcome_terms)?; - assert_value_in_terms(report, "/summary/openviking/overall_outcome", outcome_terms)?; - - for scenario in support::array_at(report, "/qmd_strength_profile/scenario_outcomes")? { - assert_value_in_terms(scenario, "/result_type", result_terms)?; - assert_value_in_terms(scenario, "/elf_status", coverage_terms)?; - assert_value_in_terms(scenario, "/qmd_status", coverage_terms)?; - } - for scenario in - support::array_at(report, "/openviking_context_trajectory_profile/scenario_outcomes")? - { - assert_value_in_terms(scenario, "/result_type", result_terms)?; - assert_value_in_terms(scenario, "/openviking_status", coverage_terms)?; - assert_value_in_terms(scenario, "/elf_equivalent_status", coverage_terms)?; - } - - Ok(()) -} - -fn assert_value_in_terms(value: &Value, pointer: &str, terms: &[Value]) -> Result<()> { - let actual = value - .pointer(pointer) - .and_then(Value::as_str) - .ok_or_else(|| eyre::eyre!("missing string at {pointer}"))?; - - assert!( - terms.iter().any(|term| term.as_str() == Some(actual)), - "{actual} at {pointer} is not declared in the report term list" - ); - - Ok(()) -} - -fn assert_qmd_strength_profile(report: &Value) -> Result<()> { - let qmd_scenarios = support::array_at(report, "/qmd_strength_profile/scenario_outcomes")?; - let local_transparency = - support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-local-query-transparency")?; - let retrieval = support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-retrieval-quality")?; - let rerank_controls = support::find_by_field( - qmd_scenarios, - "/scenario_id", - "qmd-expansion-fusion-rerank-controls", - )?; - let stale_isolation = - support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-stale-context-isolation")?; - let lifecycle = - support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-update-delete-cold-start")?; - let operator_debug = - support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-operator-debug-evidence")?; - let replayability = - support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-local-replayability")?; - let wrong_result = - support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-wrong-result-diagnosis")?; - - assert_eq!(qmd_scenarios.len(), 8); - assert_eq!(retrieval.pointer("/elf_outcome").and_then(Value::as_str), Some("tie")); - assert_eq!( - local_transparency.pointer("/elf_outcome").and_then(Value::as_str), - Some("not_tested") - ); - assert_eq!( - local_transparency.pointer("/result_type").and_then(Value::as_str), - Some("not_encoded") - ); - assert_eq!( - rerank_controls.pointer("/result_type").and_then(Value::as_str), - Some("not_encoded") - ); - assert_eq!(stale_isolation.pointer("/result_type").and_then(Value::as_str), Some("pass")); - assert_eq!(stale_isolation.pointer("/elf_outcome").and_then(Value::as_str), Some("tie")); - assert_eq!(lifecycle.pointer("/result_type").and_then(Value::as_str), Some("pass")); - assert_eq!(lifecycle.pointer("/elf_outcome").and_then(Value::as_str), Some("tie")); - assert_eq!(operator_debug.pointer("/result_type").and_then(Value::as_str), Some("not_encoded")); - assert_eq!(operator_debug.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); - assert_eq!(replayability.pointer("/result_type").and_then(Value::as_str), Some("not_encoded")); - assert_eq!(replayability.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); - assert_eq!( - wrong_result.pointer("/evidence_class").and_then(Value::as_str), - Some("research_gate") - ); - assert_eq!(wrong_result.pointer("/result_type").and_then(Value::as_str), Some("not_encoded")); - - Ok(()) -} - -fn assert_qmd_wrong_result_diagnosis(report: &Value) -> Result<()> { - let taxonomy = - support::array_at(report, "/qmd_strength_profile/wrong_result_diagnosis/taxonomy")?; - let absent = support::find_by_field(taxonomy, "/class", "evidence_absent")?; - let dropped = support::find_by_field(taxonomy, "/class", "retrieved_but_dropped")?; - let narrated = support::find_by_field(taxonomy, "/class", "selected_but_not_narrated")?; - let lifecycle = - support::find_by_field(taxonomy, "/class", "contradicted_by_lifecycle_evidence")?; - - assert_eq!(absent.pointer("/coverage").and_then(Value::as_str), Some("observed")); - assert_eq!( - dropped.pointer("/coverage").and_then(Value::as_str), - Some("not_observed_candidate_trace_missing") - ); - assert_eq!(narrated.pointer("/coverage").and_then(Value::as_str), Some("observed")); - assert_eq!(lifecycle.pointer("/coverage").and_then(Value::as_str), Some("observed")); - - let qmd_diagnosis_jobs = - support::array_at(report, "/qmd_strength_profile/wrong_result_diagnosis/jobs")?; - let delete_job = - support::find_by_field(qmd_diagnosis_jobs, "/job_id", "memory-evolution-delete-ttl-001")?; - - assert_eq!(qmd_diagnosis_jobs.len(), 6); - assert_eq!(delete_job.pointer("/qmd_status").and_then(Value::as_str), Some("wrong_result")); - assert!(support::array_contains_str(delete_job, "/missing_evidence", "delete-tombstone")?); - assert!( - delete_job - .pointer("/diagnosis") - .and_then(Value::as_str) - .is_some_and(|diagnosis| diagnosis.contains("typed wrong_result")) - ); - - Ok(()) -} - -fn assert_openviking_strength_profile(report: &Value) -> Result<()> { - let openviking_scenarios = - support::array_at(report, "/openviking_context_trajectory_profile/scenario_outcomes")?; - let trajectory = support::find_by_field( - openviking_scenarios, - "/scenario_id", - "openviking-staged-retrieval-trajectory", - )?; - let precondition = support::find_by_field( - openviking_scenarios, - "/scenario_id", - "openviking-evidence-bearing-retrieval-precondition", - )?; - let local_embed_setup = support::find_by_field( - openviking_scenarios, - "/scenario_id", - "openviking-local-embed-setup", - )?; - let missed_terms = support::find_by_field( - openviking_scenarios, - "/scenario_id", - "openviking-missed-expected-terms-evidence", - )?; - let hierarchy = support::find_by_field( - openviking_scenarios, - "/scenario_id", - "openviking-hierarchy-selection", - )?; - let recursive_expansion = support::find_by_field( - openviking_scenarios, - "/scenario_id", - "openviking-recursive-context-expansion", - )?; - - assert_eq!(openviking_scenarios.len(), 6); - assert_eq!( - trajectory.pointer("/evidence_class").and_then(Value::as_str), - Some("fixture_backed") - ); - assert_eq!(trajectory.pointer("/result_type").and_then(Value::as_str), Some("blocked")); - assert_eq!(trajectory.pointer("/openviking_status").and_then(Value::as_str), Some("blocked")); - assert_eq!(local_embed_setup.pointer("/result_type").and_then(Value::as_str), Some("pass")); - assert_eq!( - local_embed_setup.pointer("/elf_outcome").and_then(Value::as_str), - Some("not_tested") - ); - assert_eq!(local_embed_setup.pointer("/typed_blocker"), Some(&Value::Null)); - assert_eq!(precondition.pointer("/result_type").and_then(Value::as_str), Some("wrong_result")); - assert_eq!(precondition.pointer("/elf_outcome").and_then(Value::as_str), Some("elf_win")); - assert_eq!( - precondition.pointer("/typed_blocker").and_then(Value::as_str), - Some("output_missed_expected_terms") - ); - assert_eq!(missed_terms.pointer("/result_type").and_then(Value::as_str), Some("wrong_result")); - assert_eq!(missed_terms.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); - assert_eq!(hierarchy.pointer("/result_type").and_then(Value::as_str), Some("blocked")); - assert_eq!(hierarchy.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); - assert_eq!( - recursive_expansion.pointer("/result_type").and_then(Value::as_str), - Some("blocked") - ); - assert_eq!( - recursive_expansion.pointer("/elf_outcome").and_then(Value::as_str), - Some("not_tested") - ); - - Ok(()) -} - -fn assert_strength_profile_json_claim_boundaries(report: &Value) -> Result<()> { - assert!(support::array_contains_str( - report, - "/claim_boundaries", - "ELF does not broadly beat qmd; it ties encoded retrieval and lifecycle correctness, keeps qmd query transparency as not_tested for comparative scoring, and leaves replayability not_tested." - )?); - assert!(support::array_contains_str( - report, - "/claim_boundaries", - "qmd expansion, fusion, and rerank superiority remains not_tested because the current qmd paths use --no-rerank and do not score internals." - )?); - assert!(support::array_contains_str( - report, - "/claim_boundaries", - "ELF does not beat OpenViking on context trajectory; OpenViking trajectory strengths remain blocked/not_tested behind a wrong_result same-corpus output precondition and missing staged artifacts." - )?); - assert!(support::array_contains_str( - report, - "/claim_boundaries", - "Research_gate and blocked fixture records are follow-up gates, not pass evidence." - )?); - assert!(support::array_contains_str( - report, - "/claim_boundaries", - "Missing equivalent surfaces are encoded as unsupported, blocked, or not_encoded rather than fake losses." - )?); - - Ok(()) -} - -fn assert_strength_profile_markdown_boundaries(markdown: &str) { - assert!( - markdown.contains( - "| Wrong-result diagnosis | `research_gate` | `not_encoded` | `not_tested` |" - ) - ); - assert!( - markdown.contains("ELF ties qmd on the current encoded retrieval-correctness surfaces") - ); - assert!(markdown.contains("qmd remains the local retrieval-debug UX reference")); - assert!(markdown.contains("not scored as comparative ELF wins or losses")); - assert!(markdown.contains("ELF currently wins only the equivalent OpenViking same-corpus")); - assert!(markdown.contains("Do not claim ELF broadly beats qmd")); - assert!(markdown.contains( - "Do not claim ELF beats OpenViking on staged retrieval, hierarchy, or recursive" - )); - assert!(markdown.contains( - "Do not turn `research_gate`, `blocked`, `not_encoded`, or `unsupported` surfaces" - )); - assert!(markdown.contains("no pass evidence is claimed")); - assert!(markdown.contains("typed `wrong_result` state")); -} - -fn assert_operator_facing_strength_profile_boundaries( - readme: &str, - benchmarking_index: &str, - iteration_direction: &str, -) { - assert!(readme.contains("Full-suite live real-world adapter sweep after XY-926")); - assert!(readme.contains("all 55 checked-in jobs across 13 suites")); - assert!(readme.contains("ELF now live-scores capture/write-policy")); - assert!(readme.contains("consolidation proposal review")); - assert!(readme.contains("knowledge-page rebuild/lint")); - assert!(readme.contains("operator-debugging fixtures")); - assert!(!readme.contains("memory-evolution wrong results")); - assert!(readme.contains("Live temporal reconciliation after XY-905")); - assert!(readme.contains("now reports ELF live `memory_evolution` as 6/6 pass")); - assert!(readme.contains("broad qmd, Graphiti/Zep, mem0/OpenMemory, Letta")); - assert!(readme.contains("production-ops operator boundaries")); - assert!(readme.contains("core/archival live adapter gap")); - assert!( - support::collapse_whitespace(readme).contains("blocked context-trajectory measurement") - ); - assert!( - readme - .contains("consolidation, knowledge, capture, and core/archival typed non-pass states") - ); - assert!(readme.contains("operator-debug trace hydration")); - assert!(readme.contains("qmd remains the local retrieval-debug UX reference")); - assert!(readme.contains("broad ELF-over-qmd")); - assert!(readme.contains("qmd and OpenViking Strength-Profile Report - June 11, 2026")); - assert!(benchmarking_index.contains("2026-06-11-qmd-openviking-strength-profile-report.md")); - assert!( - benchmarking_index.contains("separates qmd retrieval quality from debug/replay ergonomics") - ); - assert!(benchmarking_index.contains("preserves XY-928 OpenViking")); - assert!( - benchmarking_index - .contains("context-trajectory surfaces as blocked/not-tested until scored staged") - ); - assert!( - iteration_direction - .contains("ELF and qmd are tied on the encoded live retrieval, work-resume, and") - ); - assert!(iteration_direction.contains("ELF does not yet beat qmd's local retrieval-debug")); - - assert_iteration_direction_current_measurement_counts(iteration_direction); - - assert!(iteration_direction.contains( - "ELF beats OpenViking on context trajectory. The scenario is encoded as blocked" - )); - assert!( - iteration_direction - .contains("Do not promote a reference project into a win/loss claim until") - ); -} - -fn assert_iteration_direction_current_measurement_counts(markdown: &str) { - for expected in [ - "| Jobs | `55` |", - "| Encoded suites | `15` |", - "| Blocked | `6` |", - "| Mean score | `0.891` |", - "| Evidence coverage | `123/123` |", - "| Source-ref coverage | `123/123` |", - "| Quote coverage | `123/123` |", - "| Expected evidence recall | `115/115` |", - "| `blocked` | `7` |", - "| `not_encoded` | `5` |", - "`live_baseline_only`, `fixture_backed`, and `research_gate`", - "`blocked` for fixture-backed trajectory gates", - ] { - assert!(markdown.contains(expected), "missing iteration-direction text: {expected}"); - } - for stale in [ - "| Jobs | `40` |", - "| Encoded suites | `11` |", - "| Jobs | `50` |", - "| Encoded suites | `14` |", - "| Mean score | `0.950` |", - "| Mean score | `0.900` |", - "| Evidence coverage | `88/88` |", - "| Evidence coverage | `115/115` |", - "| Expected evidence recall | `80/80` |", - "| Expected evidence recall | `107/107` |", - "| `blocked` | `5` |", - "| `not_encoded` | `7` |", - "`live_baseline_only` plus `research_gate`", - ] { - assert!(!markdown.contains(stale), "stale iteration-direction text: {stale}"); - } -} diff --git a/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/boundaries.rs b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/boundaries.rs new file mode 100644 index 00000000..1292a800 --- /dev/null +++ b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/boundaries.rs @@ -0,0 +1,147 @@ +use color_eyre::Result; +use serde_json::Value; + +use crate::support; + +pub(crate) fn assert_strength_profile_json_claim_boundaries(report: &Value) -> Result<()> { + assert!(support::array_contains_str( + report, + "/claim_boundaries", + "ELF does not broadly beat qmd; it ties encoded retrieval and lifecycle correctness, keeps qmd query transparency as not_tested for comparative scoring, and leaves replayability not_tested." + )?); + assert!(support::array_contains_str( + report, + "/claim_boundaries", + "qmd expansion, fusion, and rerank superiority remains not_tested because the current qmd paths use --no-rerank and do not score internals." + )?); + assert!(support::array_contains_str( + report, + "/claim_boundaries", + "ELF does not beat OpenViking on context trajectory; OpenViking trajectory strengths remain blocked/not_tested behind a wrong_result same-corpus output precondition and missing staged artifacts." + )?); + assert!(support::array_contains_str( + report, + "/claim_boundaries", + "Research_gate and blocked fixture records are follow-up gates, not pass evidence." + )?); + assert!(support::array_contains_str( + report, + "/claim_boundaries", + "Missing equivalent surfaces are encoded as unsupported, blocked, or not_encoded rather than fake losses." + )?); + + Ok(()) +} + +pub(crate) fn assert_strength_profile_markdown_boundaries(markdown: &str) { + assert!( + markdown.contains( + "| Wrong-result diagnosis | `research_gate` | `not_encoded` | `not_tested` |" + ) + ); + assert!( + markdown.contains("ELF ties qmd on the current encoded retrieval-correctness surfaces") + ); + assert!(markdown.contains("qmd remains the local retrieval-debug UX reference")); + assert!(markdown.contains("not scored as comparative ELF wins or losses")); + assert!(markdown.contains("ELF currently wins only the equivalent OpenViking same-corpus")); + assert!(markdown.contains("Do not claim ELF broadly beats qmd")); + assert!(markdown.contains( + "Do not claim ELF beats OpenViking on staged retrieval, hierarchy, or recursive" + )); + assert!(markdown.contains( + "Do not turn `research_gate`, `blocked`, `not_encoded`, or `unsupported` surfaces" + )); + assert!(markdown.contains("no pass evidence is claimed")); + assert!(markdown.contains("typed `wrong_result` state")); +} + +pub(crate) fn assert_operator_facing_strength_profile_boundaries( + readme: &str, + benchmarking_index: &str, + iteration_direction: &str, +) { + assert!(readme.contains("Full-suite live real-world adapter sweep after XY-926")); + assert!(readme.contains("all 55 checked-in jobs across 13 suites")); + assert!(readme.contains("ELF now live-scores capture/write-policy")); + assert!(readme.contains("consolidation proposal review")); + assert!(readme.contains("knowledge-page rebuild/lint")); + assert!(readme.contains("operator-debugging fixtures")); + assert!(!readme.contains("memory-evolution wrong results")); + assert!(readme.contains("Live temporal reconciliation after XY-905")); + assert!(readme.contains("now reports ELF live `memory_evolution` as 6/6 pass")); + assert!(readme.contains("broad qmd, Graphiti/Zep, mem0/OpenMemory, Letta")); + assert!(readme.contains("production-ops operator boundaries")); + assert!(readme.contains("core/archival live adapter gap")); + assert!( + support::collapse_whitespace(readme).contains("blocked context-trajectory measurement") + ); + assert!( + readme + .contains("consolidation, knowledge, capture, and core/archival typed non-pass states") + ); + assert!(readme.contains("operator-debug trace hydration")); + assert!(readme.contains("qmd remains the local retrieval-debug UX reference")); + assert!(readme.contains("broad ELF-over-qmd")); + assert!(readme.contains("qmd and OpenViking Strength-Profile Report - June 11, 2026")); + assert!(benchmarking_index.contains("2026-06-11-qmd-openviking-strength-profile-report.md")); + assert!( + benchmarking_index.contains("separates qmd retrieval quality from debug/replay ergonomics") + ); + assert!(benchmarking_index.contains("preserves XY-928 OpenViking")); + assert!( + benchmarking_index + .contains("context-trajectory surfaces as blocked/not-tested until scored staged") + ); + assert!( + iteration_direction + .contains("ELF and qmd are tied on the encoded live retrieval, work-resume, and") + ); + assert!(iteration_direction.contains("ELF does not yet beat qmd's local retrieval-debug")); + + assert_iteration_direction_current_measurement_counts(iteration_direction); + + assert!(iteration_direction.contains( + "ELF beats OpenViking on context trajectory. The scenario is encoded as blocked" + )); + assert!( + iteration_direction + .contains("Do not promote a reference project into a win/loss claim until") + ); +} + +fn assert_iteration_direction_current_measurement_counts(markdown: &str) { + for expected in [ + "| Jobs | `55` |", + "| Encoded suites | `15` |", + "| Blocked | `6` |", + "| Mean score | `0.891` |", + "| Evidence coverage | `123/123` |", + "| Source-ref coverage | `123/123` |", + "| Quote coverage | `123/123` |", + "| Expected evidence recall | `115/115` |", + "| `blocked` | `7` |", + "| `not_encoded` | `5` |", + "`live_baseline_only`, `fixture_backed`, and `research_gate`", + "`blocked` for fixture-backed trajectory gates", + ] { + assert!(markdown.contains(expected), "missing iteration-direction text: {expected}"); + } + for stale in [ + "| Jobs | `40` |", + "| Encoded suites | `11` |", + "| Jobs | `50` |", + "| Encoded suites | `14` |", + "| Mean score | `0.950` |", + "| Mean score | `0.900` |", + "| Evidence coverage | `88/88` |", + "| Evidence coverage | `115/115` |", + "| Expected evidence recall | `80/80` |", + "| Expected evidence recall | `107/107` |", + "| `blocked` | `5` |", + "| `not_encoded` | `7` |", + "`live_baseline_only` plus `research_gate`", + ] { + assert!(!markdown.contains(stale), "stale iteration-direction text: {stale}"); + } +} diff --git a/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/openviking.rs b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/openviking.rs new file mode 100644 index 00000000..d6b010ca --- /dev/null +++ b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/openviking.rs @@ -0,0 +1,73 @@ +use color_eyre::Result; +use serde_json::Value; + +use crate::support; + +pub(crate) fn assert_openviking_strength_profile(report: &Value) -> Result<()> { + let openviking_scenarios = + support::array_at(report, "/openviking_context_trajectory_profile/scenario_outcomes")?; + let trajectory = support::find_by_field( + openviking_scenarios, + "/scenario_id", + "openviking-staged-retrieval-trajectory", + )?; + let precondition = support::find_by_field( + openviking_scenarios, + "/scenario_id", + "openviking-evidence-bearing-retrieval-precondition", + )?; + let local_embed_setup = support::find_by_field( + openviking_scenarios, + "/scenario_id", + "openviking-local-embed-setup", + )?; + let missed_terms = support::find_by_field( + openviking_scenarios, + "/scenario_id", + "openviking-missed-expected-terms-evidence", + )?; + let hierarchy = support::find_by_field( + openviking_scenarios, + "/scenario_id", + "openviking-hierarchy-selection", + )?; + let recursive_expansion = support::find_by_field( + openviking_scenarios, + "/scenario_id", + "openviking-recursive-context-expansion", + )?; + + assert_eq!(openviking_scenarios.len(), 6); + assert_eq!( + trajectory.pointer("/evidence_class").and_then(Value::as_str), + Some("fixture_backed") + ); + assert_eq!(trajectory.pointer("/result_type").and_then(Value::as_str), Some("blocked")); + assert_eq!(trajectory.pointer("/openviking_status").and_then(Value::as_str), Some("blocked")); + assert_eq!(local_embed_setup.pointer("/result_type").and_then(Value::as_str), Some("pass")); + assert_eq!( + local_embed_setup.pointer("/elf_outcome").and_then(Value::as_str), + Some("not_tested") + ); + assert_eq!(local_embed_setup.pointer("/typed_blocker"), Some(&Value::Null)); + assert_eq!(precondition.pointer("/result_type").and_then(Value::as_str), Some("wrong_result")); + assert_eq!(precondition.pointer("/elf_outcome").and_then(Value::as_str), Some("elf_win")); + assert_eq!( + precondition.pointer("/typed_blocker").and_then(Value::as_str), + Some("output_missed_expected_terms") + ); + assert_eq!(missed_terms.pointer("/result_type").and_then(Value::as_str), Some("wrong_result")); + assert_eq!(missed_terms.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); + assert_eq!(hierarchy.pointer("/result_type").and_then(Value::as_str), Some("blocked")); + assert_eq!(hierarchy.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); + assert_eq!( + recursive_expansion.pointer("/result_type").and_then(Value::as_str), + Some("blocked") + ); + assert_eq!( + recursive_expansion.pointer("/elf_outcome").and_then(Value::as_str), + Some("not_tested") + ); + + Ok(()) +} diff --git a/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/qmd.rs b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/qmd.rs new file mode 100644 index 00000000..fbfb6eb7 --- /dev/null +++ b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/qmd.rs @@ -0,0 +1,91 @@ +use color_eyre::Result; +use serde_json::Value; + +use crate::support; + +pub(crate) fn assert_qmd_strength_profile(report: &Value) -> Result<()> { + let qmd_scenarios = support::array_at(report, "/qmd_strength_profile/scenario_outcomes")?; + let local_transparency = + support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-local-query-transparency")?; + let retrieval = support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-retrieval-quality")?; + let rerank_controls = support::find_by_field( + qmd_scenarios, + "/scenario_id", + "qmd-expansion-fusion-rerank-controls", + )?; + let stale_isolation = + support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-stale-context-isolation")?; + let lifecycle = + support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-update-delete-cold-start")?; + let operator_debug = + support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-operator-debug-evidence")?; + let replayability = + support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-local-replayability")?; + let wrong_result = + support::find_by_field(qmd_scenarios, "/scenario_id", "qmd-wrong-result-diagnosis")?; + + assert_eq!(qmd_scenarios.len(), 8); + assert_eq!(retrieval.pointer("/elf_outcome").and_then(Value::as_str), Some("tie")); + assert_eq!( + local_transparency.pointer("/elf_outcome").and_then(Value::as_str), + Some("not_tested") + ); + assert_eq!( + local_transparency.pointer("/result_type").and_then(Value::as_str), + Some("not_encoded") + ); + assert_eq!( + rerank_controls.pointer("/result_type").and_then(Value::as_str), + Some("not_encoded") + ); + assert_eq!(stale_isolation.pointer("/result_type").and_then(Value::as_str), Some("pass")); + assert_eq!(stale_isolation.pointer("/elf_outcome").and_then(Value::as_str), Some("tie")); + assert_eq!(lifecycle.pointer("/result_type").and_then(Value::as_str), Some("pass")); + assert_eq!(lifecycle.pointer("/elf_outcome").and_then(Value::as_str), Some("tie")); + assert_eq!(operator_debug.pointer("/result_type").and_then(Value::as_str), Some("not_encoded")); + assert_eq!(operator_debug.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); + assert_eq!(replayability.pointer("/result_type").and_then(Value::as_str), Some("not_encoded")); + assert_eq!(replayability.pointer("/elf_outcome").and_then(Value::as_str), Some("not_tested")); + assert_eq!( + wrong_result.pointer("/evidence_class").and_then(Value::as_str), + Some("research_gate") + ); + assert_eq!(wrong_result.pointer("/result_type").and_then(Value::as_str), Some("not_encoded")); + + Ok(()) +} + +pub(crate) fn assert_qmd_wrong_result_diagnosis(report: &Value) -> Result<()> { + let taxonomy = + support::array_at(report, "/qmd_strength_profile/wrong_result_diagnosis/taxonomy")?; + let absent = support::find_by_field(taxonomy, "/class", "evidence_absent")?; + let dropped = support::find_by_field(taxonomy, "/class", "retrieved_but_dropped")?; + let narrated = support::find_by_field(taxonomy, "/class", "selected_but_not_narrated")?; + let lifecycle = + support::find_by_field(taxonomy, "/class", "contradicted_by_lifecycle_evidence")?; + + assert_eq!(absent.pointer("/coverage").and_then(Value::as_str), Some("observed")); + assert_eq!( + dropped.pointer("/coverage").and_then(Value::as_str), + Some("not_observed_candidate_trace_missing") + ); + assert_eq!(narrated.pointer("/coverage").and_then(Value::as_str), Some("observed")); + assert_eq!(lifecycle.pointer("/coverage").and_then(Value::as_str), Some("observed")); + + let qmd_diagnosis_jobs = + support::array_at(report, "/qmd_strength_profile/wrong_result_diagnosis/jobs")?; + let delete_job = + support::find_by_field(qmd_diagnosis_jobs, "/job_id", "memory-evolution-delete-ttl-001")?; + + assert_eq!(qmd_diagnosis_jobs.len(), 6); + assert_eq!(delete_job.pointer("/qmd_status").and_then(Value::as_str), Some("wrong_result")); + assert!(support::array_contains_str(delete_job, "/missing_evidence", "delete-tombstone")?); + assert!( + delete_job + .pointer("/diagnosis") + .and_then(Value::as_str) + .is_some_and(|diagnosis| diagnosis.contains("typed wrong_result")) + ); + + Ok(()) +} diff --git a/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/summary_terms.rs b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/summary_terms.rs new file mode 100644 index 00000000..6d4f81a0 --- /dev/null +++ b/apps/elf-eval/tests/real_world_job_benchmark/competitor_strength_profile/summary_terms.rs @@ -0,0 +1,138 @@ +use color_eyre::{Result, eyre}; +use serde_json::Value; + +use crate::support; + +pub(crate) fn assert_strength_profile_summary(report: &Value) { + assert_eq!( + report.pointer("/schema").and_then(Value::as_str), + Some("elf.competitor_strength_profile_report/v1") + ); + assert_eq!( + report.pointer("/summary/qmd/retrieval_quality").and_then(Value::as_str), + Some("tie") + ); + assert_eq!( + report.pointer("/summary/qmd/local_query_transparency").and_then(Value::as_str), + Some("not_tested") + ); + assert_eq!( + report.pointer("/summary/qmd/local_replayability").and_then(Value::as_str), + Some("not_tested") + ); + assert_eq!( + report.pointer("/summary/qmd/overall_outcome").and_then(Value::as_str), + Some("not_tested") + ); + assert_eq!( + report.pointer("/summary/openviking/overall_outcome").and_then(Value::as_str), + Some("not_tested") + ); + assert_eq!( + report + .pointer("/qmd_strength_profile/win_tie_loss_summary/elf_win") + .and_then(Value::as_u64), + Some(0) + ); + assert_eq!( + report.pointer("/qmd_strength_profile/win_tie_loss_summary/tie").and_then(Value::as_u64), + Some(3) + ); + assert_eq!( + report + .pointer("/qmd_strength_profile/win_tie_loss_summary/elf_loss") + .and_then(Value::as_u64), + Some(0) + ); + assert_eq!( + report + .pointer("/qmd_strength_profile/win_tie_loss_summary/not_tested") + .and_then(Value::as_u64), + Some(5) + ); + assert_eq!( + report + .pointer("/openviking_context_trajectory_profile/win_tie_loss_summary/not_tested") + .and_then(Value::as_u64), + Some(5) + ); + assert_eq!( + report + .pointer("/openviking_context_trajectory_profile/win_tie_loss_summary/elf_win") + .and_then(Value::as_u64), + Some(1) + ); +} + +pub(crate) fn assert_strength_profile_terms(report: &Value) -> Result<()> { + let result_terms = support::array_at(report, "/result_type_terms")?; + let coverage_terms = support::array_at(report, "/coverage_status_terms")?; + let outcome_terms = support::array_at(report, "/outcome_terms")?; + let actual_result_terms = support::string_array_at(report, "/result_type_terms")?; + let actual_coverage_terms = support::string_array_at(report, "/coverage_status_terms")?; + + assert_eq!( + actual_result_terms, + [ + "pass", + "wrong_result", + "blocked", + "incomplete", + "lifecycle_fail", + "not_encoded", + "unsupported_claim", + ] + .map(str::to_owned) + ); + assert_eq!( + actual_coverage_terms, + [ + "pass", + "wrong_result", + "blocked", + "incomplete", + "lifecycle_fail", + "not_encoded", + "unsupported", + "unsupported_claim", + ] + .map(str::to_owned) + ); + assert!(!result_terms.iter().any(|term| term.as_str() == Some("unsupported"))); + assert!(!result_terms.iter().any(|term| term.as_str() == Some("partial"))); + assert!(!coverage_terms.iter().any(|term| term.as_str() == Some("partial"))); + assert!(result_terms.iter().any(|term| term.as_str() == Some("unsupported_claim"))); + assert!(coverage_terms.iter().any(|term| term.as_str() == Some("unsupported"))); + + assert_value_in_terms(report, "/summary/qmd/overall_outcome", outcome_terms)?; + assert_value_in_terms(report, "/summary/openviking/overall_outcome", outcome_terms)?; + + for scenario in support::array_at(report, "/qmd_strength_profile/scenario_outcomes")? { + assert_value_in_terms(scenario, "/result_type", result_terms)?; + assert_value_in_terms(scenario, "/elf_status", coverage_terms)?; + assert_value_in_terms(scenario, "/qmd_status", coverage_terms)?; + } + for scenario in + support::array_at(report, "/openviking_context_trajectory_profile/scenario_outcomes")? + { + assert_value_in_terms(scenario, "/result_type", result_terms)?; + assert_value_in_terms(scenario, "/openviking_status", coverage_terms)?; + assert_value_in_terms(scenario, "/elf_equivalent_status", coverage_terms)?; + } + + Ok(()) +} + +fn assert_value_in_terms(value: &Value, pointer: &str, terms: &[Value]) -> Result<()> { + let actual = value + .pointer(pointer) + .and_then(Value::as_str) + .ok_or_else(|| eyre::eyre!("missing string at {pointer}"))?; + + assert!( + terms.iter().any(|term| term.as_str() == Some(actual)), + "{actual} at {pointer} is not declared in the report term list" + ); + + Ok(()) +} diff --git a/packages/elf-service/src/docs/tests_put_validation.rs b/packages/elf-service/src/docs/tests_put_validation.rs index 124e16e6..0ead206a 100644 --- a/packages/elf-service/src/docs/tests_put_validation.rs +++ b/packages/elf-service/src/docs/tests_put_validation.rs @@ -1,443 +1,5 @@ -use crate::docs::{self, DocType, DocsPutRequest, Error}; -use elf_domain::writegate::{WritePolicy, WritePolicyAudit, WriteSpan}; - -#[test] -fn validate_docs_put_rejects_invalid_doc_type() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "invalid", - "ts": "2026-02-25T12:00:00Z", - }), - content: "Hello world.".to_string(), - }) - .expect_err("Expected invalid doc_type to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("doc_type")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_missing_source_ref() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Knowledge.as_str().to_string()), - title: None, - write_policy: None, - source_ref: serde_json::json!({"schema":"doc_source_ref/v1", "doc_type":"knowledge"}), - content: "Hello world.".to_string(), - }) - .expect_err("Expected missing source_ref.ts to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("source_ref[\"ts\"]")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_non_object_source_ref() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: None, - write_policy: None, - source_ref: serde_json::json!("legacy-shape"), - content: "Hello world.".to_string(), - }) - .expect_err("Expected non-object source_ref to be rejected."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("source_ref must be a JSON object")) - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_mismatched_request_and_source_ref_doc_type() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Chat.as_str().to_string()), - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - }), - content: "Hello world.".to_string(), - }) - .expect_err("Expected mismatched doc_type to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("match")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_wrong_source_ref_schema() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "note_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - }), - content: "Hello world.".to_string(), - }) - .expect_err("Expected wrong source_ref.schema to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("doc_source_ref/v1")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_chat_source_ref_with_missing_thread_metadata() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Chat.as_str().to_string()), - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "chat", - "ts": "2026-02-25T12:00:00Z", - }), - content: "Hello world.".to_string(), - }) - .expect_err("Expected chat source_ref to require thread_id/role."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("thread_id")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_search_source_ref_with_missing_domain() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Search.as_str().to_string()), - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "search", - "ts": "2026-02-25T12:00:00Z", - "query": "test", - "url": "https://example.com", - }), - content: "Hello world.".to_string(), - }) - .expect_err("Expected search source_ref to require domain."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("domain")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_rejects_dev_source_ref_with_multiple_identifiers() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Dev.as_str().to_string()), - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "dev", - "ts": "2026-02-25T12:00:00Z", - "repo": "hack-ink/ELF", - "commit_sha": "9f0a3f4c4eb58bfcf4a5f4f9d0c7be0e13c2f8d19", - "issue_number": 123, - }), - content: "Hello world.".to_string(), - }) - .expect_err("Expected dev source_ref to enforce exactly one identifier field."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("exactly one of commit_sha, pr_number, or issue_number")) - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_uses_source_ref_doc_type_when_request_doc_type_is_absent() { - let resolved_doc_type = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: None, - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "chat", - "ts": "2026-02-25T12:00:00Z", - "thread_id": "thread-1", - "role": "assistant" - }), - content: "Hello world.".to_string(), - }) - .expect("Expected valid source_ref to resolve doc_type."); - - assert_eq!(resolved_doc_type.doc_type, DocType::Chat); -} - -#[test] -fn validate_docs_put_accepts_source_library_article_metadata() { - let validated = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Knowledge.as_str().to_string()), - title: Some("Saved article".to_string()), - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - "source_kind": "article", - "canonical_uri": "https://example.com/research/source-library", - "captured_at": "2026-02-25T12:10:00Z", - "source_created_at": "2026-02-24T09:00:00Z", - "trust_label": "public_web", - "author": "Example Author", - "handle": "example-author", - "excerpt_locator": { - "quote": { - "exact": "Source libraries preserve long-form evidence." - }, - "position": { - "start": 0, - "end": 48 - } - } - }), - content: - "Source libraries preserve long-form evidence. Agents can hydrate exact excerpts later." - .to_string(), - }) - .expect("Expected source library metadata to be accepted."); - - assert_eq!(validated.doc_type, DocType::Knowledge); -} - -#[test] -fn validate_docs_put_rejects_incomplete_source_library_metadata() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Knowledge.as_str().to_string()), - title: Some("Saved article".to_string()), - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - "source_kind": "article", - "captured_at": "2026-02-25T12:10:00Z", - "trust_label": "public_web" - }), - content: "Source libraries preserve long-form evidence.".to_string(), - }) - .expect_err("Expected canonical_uri to be required for source library metadata."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("canonical_uri")), - other => panic!("Unexpected error: {other:?}"), - } - - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Knowledge.as_str().to_string()), - title: Some("Saved thread".to_string()), - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - "source_kind": "social_thread", - "canonical_uri": "https://example.com/thread/123", - "captured_at": "2026-02-25T12:10:00Z", - "trust_label": "public_web" - }), - content: "The thread says source libraries need social captures.".to_string(), - }) - .expect_err("Expected social_thread source_kind to require chat doc_type."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("requires doc_type=chat")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_applies_write_policy_and_includes_audit() { - let validated = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Knowledge.as_str().to_string()), - title: None, - write_policy: Some(WritePolicy { - exclusions: vec![WriteSpan { start: 6, end: 35 }], - redactions: vec![], - }), - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - }), - content: "Hello sk-abcdefghijklmnopqrstuvwxyz!".to_string(), - }) - .expect("Expected valid write policy transformation."); - let expected_audit = WritePolicyAudit { - exclusions: vec![WriteSpan { start: 6, end: 35 }], - ..Default::default() - }; - - assert_eq!(validated.content, "Hello !".to_string()); - assert_eq!(validated.write_policy_audit.unwrap_or_default(), expected_audit); -} - -#[test] -fn validate_docs_put_rejects_secret_after_write_policy() { - let err = docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: Some(DocType::Knowledge.as_str().to_string()), - title: None, - write_policy: Some(WritePolicy { exclusions: vec![], redactions: vec![] }), - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - }), - content: "Hello sk-abcdefghijklmnopqrstuvwxyz!".to_string(), - }) - .expect_err("Expected secret-bearing content to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("contains secrets")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_put_allows_doc_source_ref_v1_and_rejects_free_text() { - docs::validate_docs_put(&DocsPutRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: Some("English title".to_string()), - write_policy: None, - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - "notes": "English only." - }), - content: "English content.".to_string(), - }) - .expect("Expected doc_source_ref/v1 source_ref to be accepted."); - - let err = docs::validate_docs_put(&DocsPutRequest { - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - "notes": "\u{4f60}\u{597d}\u{4e16}\u{754c}" - }), - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: Some("English title".to_string()), - write_policy: None, - content: "English content.".to_string(), - }) - .expect_err("Expected non-English free-text in source_ref."); - - match err { - Error::NonEnglishInput { field } => assert_eq!(field, "$.source_ref[\"notes\"]"), - other => panic!("Unexpected error: {other:?}"), - } - - let err = docs::validate_docs_put(&DocsPutRequest { - source_ref: serde_json::json!({ - "schema": "doc_source_ref/v1", - "doc_type": "knowledge", - "ts": "2026-02-25T12:00:00Z", - "ref": "\u{4f60}\u{597d}\u{4e16}\u{754c}" - }), - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - scope: "project_shared".to_string(), - doc_type: None, - title: Some("English title".to_string()), - write_policy: None, - content: "English content.".to_string(), - }) - .expect_err("Expected identifier lane with non-Latin text to be rejected."); - - match err { - Error::NonEnglishInput { field } => assert_eq!(field, "$.source_ref[\"ref\"]"), - other => panic!("Unexpected error: {other:?}"), - } -} +mod core_rejections; +mod english_gate; +mod source_library; +mod typed_source_refs; +mod write_policy; diff --git a/packages/elf-service/src/docs/tests_put_validation/core_rejections.rs b/packages/elf-service/src/docs/tests_put_validation/core_rejections.rs new file mode 100644 index 00000000..ce2e217c --- /dev/null +++ b/packages/elf-service/src/docs/tests_put_validation/core_rejections.rs @@ -0,0 +1,120 @@ +use crate::docs::{self, DocType, DocsPutRequest, Error}; + +#[test] +fn validate_docs_put_rejects_invalid_doc_type() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "invalid", + "ts": "2026-02-25T12:00:00Z", + }), + content: "Hello world.".to_string(), + }) + .expect_err("Expected invalid doc_type to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("doc_type")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_rejects_missing_source_ref() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Knowledge.as_str().to_string()), + title: None, + write_policy: None, + source_ref: serde_json::json!({"schema":"doc_source_ref/v1", "doc_type":"knowledge"}), + content: "Hello world.".to_string(), + }) + .expect_err("Expected missing source_ref.ts to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("source_ref[\"ts\"]")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_rejects_non_object_source_ref() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: None, + write_policy: None, + source_ref: serde_json::json!("legacy-shape"), + content: "Hello world.".to_string(), + }) + .expect_err("Expected non-object source_ref to be rejected."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("source_ref must be a JSON object")) + }, + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_rejects_mismatched_request_and_source_ref_doc_type() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Chat.as_str().to_string()), + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + }), + content: "Hello world.".to_string(), + }) + .expect_err("Expected mismatched doc_type to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("match")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_rejects_wrong_source_ref_schema() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "note_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + }), + content: "Hello world.".to_string(), + }) + .expect_err("Expected wrong source_ref.schema to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("doc_source_ref/v1")), + other => panic!("Unexpected error: {other:?}"), + } +} diff --git a/packages/elf-service/src/docs/tests_put_validation/english_gate.rs b/packages/elf-service/src/docs/tests_put_validation/english_gate.rs new file mode 100644 index 00000000..80fc2aed --- /dev/null +++ b/packages/elf-service/src/docs/tests_put_validation/english_gate.rs @@ -0,0 +1,68 @@ +use crate::docs::{self, DocsPutRequest, Error}; + +#[test] +fn validate_docs_put_allows_doc_source_ref_v1_and_rejects_free_text() { + docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: Some("English title".to_string()), + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + "notes": "English only." + }), + content: "English content.".to_string(), + }) + .expect("Expected doc_source_ref/v1 source_ref to be accepted."); + + let err = docs::validate_docs_put(&DocsPutRequest { + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + "notes": "\u{4f60}\u{597d}\u{4e16}\u{754c}" + }), + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: Some("English title".to_string()), + write_policy: None, + content: "English content.".to_string(), + }) + .expect_err("Expected non-English free-text in source_ref."); + + match err { + Error::NonEnglishInput { field } => assert_eq!(field, "$.source_ref[\"notes\"]"), + other => panic!("Unexpected error: {other:?}"), + } + + let err = docs::validate_docs_put(&DocsPutRequest { + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + "ref": "\u{4f60}\u{597d}\u{4e16}\u{754c}" + }), + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: Some("English title".to_string()), + write_policy: None, + content: "English content.".to_string(), + }) + .expect_err("Expected identifier lane with non-Latin text to be rejected."); + + match err { + Error::NonEnglishInput { field } => assert_eq!(field, "$.source_ref[\"ref\"]"), + other => panic!("Unexpected error: {other:?}"), + } +} diff --git a/packages/elf-service/src/docs/tests_put_validation/source_library.rs b/packages/elf-service/src/docs/tests_put_validation/source_library.rs new file mode 100644 index 00000000..2f296278 --- /dev/null +++ b/packages/elf-service/src/docs/tests_put_validation/source_library.rs @@ -0,0 +1,95 @@ +use crate::docs::{self, DocType, DocsPutRequest, Error}; + +#[test] +fn validate_docs_put_accepts_source_library_article_metadata() { + let validated = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Knowledge.as_str().to_string()), + title: Some("Saved article".to_string()), + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + "source_kind": "article", + "canonical_uri": "https://example.com/research/source-library", + "captured_at": "2026-02-25T12:10:00Z", + "source_created_at": "2026-02-24T09:00:00Z", + "trust_label": "public_web", + "author": "Example Author", + "handle": "example-author", + "excerpt_locator": { + "quote": { + "exact": "Source libraries preserve long-form evidence." + }, + "position": { + "start": 0, + "end": 48 + } + } + }), + content: + "Source libraries preserve long-form evidence. Agents can hydrate exact excerpts later." + .to_string(), + }) + .expect("Expected source library metadata to be accepted."); + + assert_eq!(validated.doc_type, DocType::Knowledge); +} + +#[test] +fn validate_docs_put_rejects_incomplete_source_library_metadata() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Knowledge.as_str().to_string()), + title: Some("Saved article".to_string()), + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + "source_kind": "article", + "captured_at": "2026-02-25T12:10:00Z", + "trust_label": "public_web" + }), + content: "Source libraries preserve long-form evidence.".to_string(), + }) + .expect_err("Expected canonical_uri to be required for source library metadata."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("canonical_uri")), + other => panic!("Unexpected error: {other:?}"), + } + + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Knowledge.as_str().to_string()), + title: Some("Saved thread".to_string()), + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + "source_kind": "social_thread", + "canonical_uri": "https://example.com/thread/123", + "captured_at": "2026-02-25T12:10:00Z", + "trust_label": "public_web" + }), + content: "The thread says source libraries need social captures.".to_string(), + }) + .expect_err("Expected social_thread source_kind to require chat doc_type."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("requires doc_type=chat")), + other => panic!("Unexpected error: {other:?}"), + } +} diff --git a/packages/elf-service/src/docs/tests_put_validation/typed_source_refs.rs b/packages/elf-service/src/docs/tests_put_validation/typed_source_refs.rs new file mode 100644 index 00000000..452637bb --- /dev/null +++ b/packages/elf-service/src/docs/tests_put_validation/typed_source_refs.rs @@ -0,0 +1,107 @@ +use crate::docs::{self, DocType, DocsPutRequest, Error}; + +#[test] +fn validate_docs_put_rejects_chat_source_ref_with_missing_thread_metadata() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Chat.as_str().to_string()), + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "chat", + "ts": "2026-02-25T12:00:00Z", + }), + content: "Hello world.".to_string(), + }) + .expect_err("Expected chat source_ref to require thread_id/role."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("thread_id")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_rejects_search_source_ref_with_missing_domain() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Search.as_str().to_string()), + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "search", + "ts": "2026-02-25T12:00:00Z", + "query": "test", + "url": "https://example.com", + }), + content: "Hello world.".to_string(), + }) + .expect_err("Expected search source_ref to require domain."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("domain")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_rejects_dev_source_ref_with_multiple_identifiers() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Dev.as_str().to_string()), + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "dev", + "ts": "2026-02-25T12:00:00Z", + "repo": "hack-ink/ELF", + "commit_sha": "9f0a3f4c4eb58bfcf4a5f4f9d0c7be0e13c2f8d19", + "issue_number": 123, + }), + content: "Hello world.".to_string(), + }) + .expect_err("Expected dev source_ref to enforce exactly one identifier field."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("exactly one of commit_sha, pr_number, or issue_number")) + }, + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_put_uses_source_ref_doc_type_when_request_doc_type_is_absent() { + let resolved_doc_type = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: None, + title: None, + write_policy: None, + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "chat", + "ts": "2026-02-25T12:00:00Z", + "thread_id": "thread-1", + "role": "assistant" + }), + content: "Hello world.".to_string(), + }) + .expect("Expected valid source_ref to resolve doc_type."); + + assert_eq!(resolved_doc_type.doc_type, DocType::Chat); +} diff --git a/packages/elf-service/src/docs/tests_put_validation/write_policy.rs b/packages/elf-service/src/docs/tests_put_validation/write_policy.rs new file mode 100644 index 00000000..3ff8d37b --- /dev/null +++ b/packages/elf-service/src/docs/tests_put_validation/write_policy.rs @@ -0,0 +1,57 @@ +use crate::docs::{self, DocType, DocsPutRequest, Error}; +use elf_domain::writegate::{WritePolicy, WritePolicyAudit, WriteSpan}; + +#[test] +fn validate_docs_put_applies_write_policy_and_includes_audit() { + let validated = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Knowledge.as_str().to_string()), + title: None, + write_policy: Some(WritePolicy { + exclusions: vec![WriteSpan { start: 6, end: 35 }], + redactions: vec![], + }), + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + }), + content: "Hello sk-abcdefghijklmnopqrstuvwxyz!".to_string(), + }) + .expect("Expected valid write policy transformation."); + let expected_audit = WritePolicyAudit { + exclusions: vec![WriteSpan { start: 6, end: 35 }], + ..Default::default() + }; + + assert_eq!(validated.content, "Hello !".to_string()); + assert_eq!(validated.write_policy_audit.unwrap_or_default(), expected_audit); +} + +#[test] +fn validate_docs_put_rejects_secret_after_write_policy() { + let err = docs::validate_docs_put(&DocsPutRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + scope: "project_shared".to_string(), + doc_type: Some(DocType::Knowledge.as_str().to_string()), + title: None, + write_policy: Some(WritePolicy { exclusions: vec![], redactions: vec![] }), + source_ref: serde_json::json!({ + "schema": "doc_source_ref/v1", + "doc_type": "knowledge", + "ts": "2026-02-25T12:00:00Z", + }), + content: "Hello sk-abcdefghijklmnopqrstuvwxyz!".to_string(), + }) + .expect_err("Expected secret-bearing content to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("contains secrets")), + other => panic!("Unexpected error: {other:?}"), + } +} diff --git a/packages/elf-service/src/docs/tests_search_validation.rs b/packages/elf-service/src/docs/tests_search_validation.rs index 500eab4a..58001b0d 100644 --- a/packages/elf-service/src/docs/tests_search_validation.rs +++ b/packages/elf-service/src/docs/tests_search_validation.rs @@ -1,418 +1,4 @@ -use qdrant_client::qdrant::{ - DatetimeRange, Filter, condition::ConditionOneOf, r#match::MatchValue, -}; -use time::{OffsetDateTime, format_description::well_known::Rfc3339}; - -use crate::docs::{ - self, DocType, DocsSearchL0Filters, DocsSearchL0Request, DocsSparseMode, Error, - tests::{self, PROJECT_ID, TENANT_ID}, -}; - -fn first_datetime_range(filter: &Filter, key: &str) -> Option { - for condition in &filter.must { - if let Some(ConditionOneOf::Field(field)) = condition.condition_one_of.as_ref() { - if field.key != key { - continue; - } - - if let Some(range) = field.datetime_range.as_ref() { - return Some(*range); - } - } - } - - None -} - -fn first_match_value(filter: &Filter, key: &str) -> Option { - for condition in &filter.must { - if let Some(ConditionOneOf::Field(field)) = condition.condition_one_of.as_ref() { - if field.key != key { - continue; - } - - if let Some(r#match) = field.r#match.as_ref() { - let Some(match_value) = r#match.match_value.as_ref() else { - continue; - }; - - return match match_value { - MatchValue::Keyword(value) => Some(value.clone()), - _ => None, - }; - } - } - } - - None -} - -#[test] -fn docs_search_l0_requires_chat_doc_type_for_thread_id() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "thread".to_string(), - scope: None, - status: None, - doc_type: Some("search".to_string()), - sparse_mode: None, - domain: None, - repo: None, - agent_id: None, - thread_id: Some("thread-1".to_string()), - updated_after: None, - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected thread_id to require doc_type=chat."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("thread_id requires")), - other => panic!("Unexpected error: {other:?}"), - } - - docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "thread".to_string(), - scope: None, - status: None, - doc_type: Some("chat".to_string()), - sparse_mode: None, - domain: None, - repo: None, - agent_id: None, - thread_id: Some("thread-1".to_string()), - updated_after: None, - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect("Expected thread_id filter to be accepted for chat."); -} - -#[test] -fn validate_docs_search_l0_defaults_status_and_filters_dates() { - let filters = docs::validate_docs_search_l0(&tests::test_request_with_query("hello world")) - .expect("valid request"); - - assert_eq!(filters.status, "active"); - - let bad_dates = DocsSearchL0Request { - updated_after: Some("2026-02-25T12:00:00Z".to_string()), - updated_before: Some("2026-02-25T11:00:00Z".to_string()), - sparse_mode: None, - domain: None, - repo: None, - ..tests::test_request_with_query("status") - }; - let err = docs::validate_docs_search_l0(&bad_dates) - .expect_err("Expected bad date order to be rejected."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("earlier")); - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_search_l0_rejects_invalid_status() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "status".to_string(), - scope: None, - status: Some("archived".to_string()), - doc_type: None, - sparse_mode: None, - domain: None, - repo: None, - agent_id: None, - thread_id: None, - updated_after: None, - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected invalid status to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("status")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_search_l0_rejects_invalid_datetime_format() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "status".to_string(), - scope: None, - status: None, - doc_type: None, - sparse_mode: None, - domain: None, - repo: None, - agent_id: None, - thread_id: None, - updated_after: Some("2026-02-25T12:00:00".to_string()), - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected invalid RFC3339 datetime to be rejected."); - - match err { - Error::InvalidRequest { message } => assert!(message.contains("RFC3339")), - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn build_doc_search_filter_applies_status_and_requested_filters() { - let filters = DocsSearchL0Filters { - scope: Some("project_shared".to_string()), - status: "deleted".to_string(), - doc_type: Some(DocType::Chat), - sparse_mode: DocsSparseMode::Auto, - domain: None, - repo: None, - agent_id: Some("owner".to_string()), - thread_id: Some("thread-7".to_string()), - updated_after: Some( - OffsetDateTime::parse("2026-02-20T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), - ), - updated_before: Some( - OffsetDateTime::parse("2026-02-28T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), - ), - ts_gte: Some( - OffsetDateTime::parse("2026-01-01T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), - ), - ts_lte: Some( - OffsetDateTime::parse("2026-12-31T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), - ), - }; - let filter = docs::build_doc_search_filter( - TENANT_ID, - PROJECT_ID, - "requester", - &["agent_private".to_string(), "project_shared".to_string()], - &filters, - ); - - assert_eq!(first_match_value(&filter, "tenant_id").as_deref(), Some("tenant")); - assert_eq!(first_match_value(&filter, "status").as_deref(), Some("deleted")); - assert_eq!(first_match_value(&filter, "scope").as_deref(), Some("project_shared")); - assert_eq!(first_match_value(&filter, "doc_type").as_deref(), Some("chat")); - assert_eq!(first_match_value(&filter, "agent_id").as_deref(), Some("owner")); - assert_eq!(first_match_value(&filter, "thread_id").as_deref(), Some("thread-7")); - assert_eq!(first_match_value(&filter, "domain").as_deref(), None); - assert_eq!(first_match_value(&filter, "repo").as_deref(), None); - - let datetime_range = first_datetime_range(&filter, "updated_at") - .expect("Expected datetime filter for updated_at."); - let after = - OffsetDateTime::parse("2026-02-20T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); - let before = - OffsetDateTime::parse("2026-02-28T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); - let lt = datetime_range.lt.as_ref().expect("Expected datetime filter .lt value."); - let gt = datetime_range.gt.as_ref().expect("Expected datetime filter .gt value."); - - assert_eq!(lt.seconds, before.unix_timestamp()); - assert_eq!(lt.nanos, before.nanosecond() as i32); - assert_eq!(gt.seconds, after.unix_timestamp()); - assert_eq!(gt.nanos, after.nanosecond() as i32); - assert!(datetime_range.gte.is_none()); - assert!(datetime_range.lte.is_none()); - - let doc_ts_range = - first_datetime_range(&filter, "doc_ts").expect("Expected datetime filter for doc_ts."); - let gte = doc_ts_range.gte.as_ref().expect("Expected datetime filter .gte value."); - let lte = doc_ts_range.lte.as_ref().expect("Expected datetime filter .lte value."); - let doc_ts_gte = - OffsetDateTime::parse("2026-01-01T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); - let doc_ts_lte = - OffsetDateTime::parse("2026-12-31T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); - - assert_eq!(gte.seconds, doc_ts_gte.unix_timestamp()); - assert_eq!(gte.nanos, doc_ts_gte.nanosecond() as i32); - assert_eq!(lte.seconds, doc_ts_lte.unix_timestamp()); - assert_eq!(lte.nanos, doc_ts_lte.nanosecond() as i32); - assert!(doc_ts_range.gt.is_none()); - assert!(doc_ts_range.lt.is_none()); -} - -#[test] -fn validate_docs_search_l0_rejects_invalid_doc_ts_order() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "status".to_string(), - scope: None, - status: None, - doc_type: None, - sparse_mode: None, - domain: None, - repo: None, - agent_id: None, - thread_id: None, - updated_after: None, - updated_before: None, - ts_gte: Some("2026-02-25T12:00:00Z".to_string()), - ts_lte: Some("2026-02-25T11:00:00Z".to_string()), - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected bad doc_ts order to be rejected."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("earlier")); - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_search_l0_rejects_invalid_sparse_mode() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "status".to_string(), - scope: None, - status: None, - doc_type: None, - sparse_mode: Some("invalid".to_string()), - domain: None, - repo: None, - agent_id: None, - thread_id: None, - updated_after: None, - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected invalid sparse mode to be rejected."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("sparse_mode")); - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_search_l0_rejects_domain_without_doc_type_search() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "status".to_string(), - scope: None, - status: None, - doc_type: None, - sparse_mode: None, - domain: Some("example.com".to_string()), - repo: None, - agent_id: None, - thread_id: None, - updated_after: None, - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected domain without doc_type=search to be rejected."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("doc_type=search")); - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_search_l0_rejects_repo_without_doc_type_dev() { - let err = docs::validate_docs_search_l0(&DocsSearchL0Request { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - caller_agent_id: "agent".to_string(), - read_profile: "private_plus_project".to_string(), - query: "status".to_string(), - scope: None, - status: None, - doc_type: None, - sparse_mode: None, - domain: None, - repo: Some("hack-ink/ELF".to_string()), - agent_id: None, - thread_id: None, - updated_after: None, - updated_before: None, - ts_gte: None, - ts_lte: None, - top_k: None, - candidate_k: None, - explain: None, - }) - .expect_err("Expected repo without doc_type=dev to be rejected."); - - match err { - Error::InvalidRequest { message } => { - assert!(message.contains("doc_type=dev")); - }, - other => panic!("Unexpected error: {other:?}"), - } -} - -#[test] -fn validate_docs_search_l0_default_sparse_mode() { - let filters = docs::validate_docs_search_l0(&tests::test_request_with_query("status")) - .expect("valid request"); - - assert!(matches!(filters.sparse_mode, DocsSparseMode::Auto)); -} - -#[test] -fn should_enable_sparse_auto_uses_symbol_cues() { - assert!(docs::should_enable_sparse_auto("https://example.com/search?q=abc")); - assert!(!docs::should_enable_sparse_auto("how to debug a timeout")); -} +mod filter_building; +mod request_validation; +mod sparse_mode; +mod support; diff --git a/packages/elf-service/src/docs/tests_search_validation/filter_building.rs b/packages/elf-service/src/docs/tests_search_validation/filter_building.rs new file mode 100644 index 00000000..054606e9 --- /dev/null +++ b/packages/elf-service/src/docs/tests_search_validation/filter_building.rs @@ -0,0 +1,106 @@ +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; + +use crate::docs::{ + self, DocType, DocsSearchL0Filters, DocsSearchL0Request, DocsSparseMode, Error, + tests::{self, PROJECT_ID, TENANT_ID, tests_search_validation::support}, +}; + +#[test] +fn validate_docs_search_l0_defaults_status_and_filters_dates() { + let filters = docs::validate_docs_search_l0(&tests::test_request_with_query("hello world")) + .expect("valid request"); + + assert_eq!(filters.status, "active"); + + let bad_dates = DocsSearchL0Request { + updated_after: Some("2026-02-25T12:00:00Z".to_string()), + updated_before: Some("2026-02-25T11:00:00Z".to_string()), + sparse_mode: None, + domain: None, + repo: None, + ..tests::test_request_with_query("status") + }; + let err = docs::validate_docs_search_l0(&bad_dates) + .expect_err("Expected bad date order to be rejected."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("earlier")); + }, + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn build_doc_search_filter_applies_status_and_requested_filters() { + let filters = DocsSearchL0Filters { + scope: Some("project_shared".to_string()), + status: "deleted".to_string(), + doc_type: Some(DocType::Chat), + sparse_mode: DocsSparseMode::Auto, + domain: None, + repo: None, + agent_id: Some("owner".to_string()), + thread_id: Some("thread-7".to_string()), + updated_after: Some( + OffsetDateTime::parse("2026-02-20T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), + ), + updated_before: Some( + OffsetDateTime::parse("2026-02-28T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), + ), + ts_gte: Some( + OffsetDateTime::parse("2026-01-01T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), + ), + ts_lte: Some( + OffsetDateTime::parse("2026-12-31T00:00:00Z", &Rfc3339).expect("Invalid timestamp."), + ), + }; + let filter = docs::build_doc_search_filter( + TENANT_ID, + PROJECT_ID, + "requester", + &["agent_private".to_string(), "project_shared".to_string()], + &filters, + ); + + assert_eq!(support::first_match_value(&filter, "tenant_id").as_deref(), Some("tenant")); + assert_eq!(support::first_match_value(&filter, "status").as_deref(), Some("deleted")); + assert_eq!(support::first_match_value(&filter, "scope").as_deref(), Some("project_shared")); + assert_eq!(support::first_match_value(&filter, "doc_type").as_deref(), Some("chat")); + assert_eq!(support::first_match_value(&filter, "agent_id").as_deref(), Some("owner")); + assert_eq!(support::first_match_value(&filter, "thread_id").as_deref(), Some("thread-7")); + assert_eq!(support::first_match_value(&filter, "domain").as_deref(), None); + assert_eq!(support::first_match_value(&filter, "repo").as_deref(), None); + + let datetime_range = support::first_datetime_range(&filter, "updated_at") + .expect("Expected datetime filter for updated_at."); + let after = + OffsetDateTime::parse("2026-02-20T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); + let before = + OffsetDateTime::parse("2026-02-28T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); + let lt = datetime_range.lt.as_ref().expect("Expected datetime filter .lt value."); + let gt = datetime_range.gt.as_ref().expect("Expected datetime filter .gt value."); + + assert_eq!(lt.seconds, before.unix_timestamp()); + assert_eq!(lt.nanos, before.nanosecond() as i32); + assert_eq!(gt.seconds, after.unix_timestamp()); + assert_eq!(gt.nanos, after.nanosecond() as i32); + assert!(datetime_range.gte.is_none()); + assert!(datetime_range.lte.is_none()); + + let doc_ts_range = support::first_datetime_range(&filter, "doc_ts") + .expect("Expected datetime filter for doc_ts."); + let gte = doc_ts_range.gte.as_ref().expect("Expected datetime filter .gte value."); + let lte = doc_ts_range.lte.as_ref().expect("Expected datetime filter .lte value."); + let doc_ts_gte = + OffsetDateTime::parse("2026-01-01T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); + let doc_ts_lte = + OffsetDateTime::parse("2026-12-31T00:00:00Z", &Rfc3339).expect("Invalid timestamp."); + + assert_eq!(gte.seconds, doc_ts_gte.unix_timestamp()); + assert_eq!(gte.nanos, doc_ts_gte.nanosecond() as i32); + assert_eq!(lte.seconds, doc_ts_lte.unix_timestamp()); + assert_eq!(lte.nanos, doc_ts_lte.nanosecond() as i32); + assert!(doc_ts_range.gt.is_none()); + assert!(doc_ts_range.lt.is_none()); +} diff --git a/packages/elf-service/src/docs/tests_search_validation/request_validation.rs b/packages/elf-service/src/docs/tests_search_validation/request_validation.rs new file mode 100644 index 00000000..74559a22 --- /dev/null +++ b/packages/elf-service/src/docs/tests_search_validation/request_validation.rs @@ -0,0 +1,226 @@ +use crate::docs::{ + self, DocsSearchL0Request, Error, + tests::{PROJECT_ID, TENANT_ID}, +}; + +#[test] +fn docs_search_l0_requires_chat_doc_type_for_thread_id() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "thread".to_string(), + scope: None, + status: None, + doc_type: Some("search".to_string()), + sparse_mode: None, + domain: None, + repo: None, + agent_id: None, + thread_id: Some("thread-1".to_string()), + updated_after: None, + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected thread_id to require doc_type=chat."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("thread_id requires")), + other => panic!("Unexpected error: {other:?}"), + } + + docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "thread".to_string(), + scope: None, + status: None, + doc_type: Some("chat".to_string()), + sparse_mode: None, + domain: None, + repo: None, + agent_id: None, + thread_id: Some("thread-1".to_string()), + updated_after: None, + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect("Expected thread_id filter to be accepted for chat."); +} + +#[test] +fn validate_docs_search_l0_rejects_invalid_status() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "status".to_string(), + scope: None, + status: Some("archived".to_string()), + doc_type: None, + sparse_mode: None, + domain: None, + repo: None, + agent_id: None, + thread_id: None, + updated_after: None, + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected invalid status to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("status")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_search_l0_rejects_invalid_datetime_format() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "status".to_string(), + scope: None, + status: None, + doc_type: None, + sparse_mode: None, + domain: None, + repo: None, + agent_id: None, + thread_id: None, + updated_after: Some("2026-02-25T12:00:00".to_string()), + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected invalid RFC3339 datetime to be rejected."); + + match err { + Error::InvalidRequest { message } => assert!(message.contains("RFC3339")), + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_search_l0_rejects_invalid_doc_ts_order() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "status".to_string(), + scope: None, + status: None, + doc_type: None, + sparse_mode: None, + domain: None, + repo: None, + agent_id: None, + thread_id: None, + updated_after: None, + updated_before: None, + ts_gte: Some("2026-02-25T12:00:00Z".to_string()), + ts_lte: Some("2026-02-25T11:00:00Z".to_string()), + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected bad doc_ts order to be rejected."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("earlier")); + }, + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_search_l0_rejects_domain_without_doc_type_search() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "status".to_string(), + scope: None, + status: None, + doc_type: None, + sparse_mode: None, + domain: Some("example.com".to_string()), + repo: None, + agent_id: None, + thread_id: None, + updated_after: None, + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected domain without doc_type=search to be rejected."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("doc_type=search")); + }, + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_search_l0_rejects_repo_without_doc_type_dev() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "status".to_string(), + scope: None, + status: None, + doc_type: None, + sparse_mode: None, + domain: None, + repo: Some("hack-ink/ELF".to_string()), + agent_id: None, + thread_id: None, + updated_after: None, + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected repo without doc_type=dev to be rejected."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("doc_type=dev")); + }, + other => panic!("Unexpected error: {other:?}"), + } +} diff --git a/packages/elf-service/src/docs/tests_search_validation/sparse_mode.rs b/packages/elf-service/src/docs/tests_search_validation/sparse_mode.rs new file mode 100644 index 00000000..056b1376 --- /dev/null +++ b/packages/elf-service/src/docs/tests_search_validation/sparse_mode.rs @@ -0,0 +1,52 @@ +use crate::docs::{ + self, DocsSearchL0Request, DocsSparseMode, Error, + tests::{self, PROJECT_ID, TENANT_ID}, +}; + +#[test] +fn validate_docs_search_l0_rejects_invalid_sparse_mode() { + let err = docs::validate_docs_search_l0(&DocsSearchL0Request { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + caller_agent_id: "agent".to_string(), + read_profile: "private_plus_project".to_string(), + query: "status".to_string(), + scope: None, + status: None, + doc_type: None, + sparse_mode: Some("invalid".to_string()), + domain: None, + repo: None, + agent_id: None, + thread_id: None, + updated_after: None, + updated_before: None, + ts_gte: None, + ts_lte: None, + top_k: None, + candidate_k: None, + explain: None, + }) + .expect_err("Expected invalid sparse mode to be rejected."); + + match err { + Error::InvalidRequest { message } => { + assert!(message.contains("sparse_mode")); + }, + other => panic!("Unexpected error: {other:?}"), + } +} + +#[test] +fn validate_docs_search_l0_default_sparse_mode() { + let filters = docs::validate_docs_search_l0(&tests::test_request_with_query("status")) + .expect("valid request"); + + assert!(matches!(filters.sparse_mode, DocsSparseMode::Auto)); +} + +#[test] +fn should_enable_sparse_auto_uses_symbol_cues() { + assert!(docs::should_enable_sparse_auto("https://example.com/search?q=abc")); + assert!(!docs::should_enable_sparse_auto("how to debug a timeout")); +} diff --git a/packages/elf-service/src/docs/tests_search_validation/support.rs b/packages/elf-service/src/docs/tests_search_validation/support.rs new file mode 100644 index 00000000..1243b9ad --- /dev/null +++ b/packages/elf-service/src/docs/tests_search_validation/support.rs @@ -0,0 +1,42 @@ +use qdrant_client::qdrant::{ + DatetimeRange, Filter, condition::ConditionOneOf, r#match::MatchValue, +}; + +pub(crate) fn first_datetime_range(filter: &Filter, key: &str) -> Option { + for condition in &filter.must { + if let Some(ConditionOneOf::Field(field)) = condition.condition_one_of.as_ref() { + if field.key != key { + continue; + } + + if let Some(range) = field.datetime_range.as_ref() { + return Some(*range); + } + } + } + + None +} + +pub(crate) fn first_match_value(filter: &Filter, key: &str) -> Option { + for condition in &filter.must { + if let Some(ConditionOneOf::Field(field)) = condition.condition_one_of.as_ref() { + if field.key != key { + continue; + } + + if let Some(r#match) = field.r#match.as_ref() { + let Some(match_value) = r#match.match_value.as_ref() else { + continue; + }; + + return match match_value { + MatchValue::Keyword(value) => Some(value.clone()), + _ => None, + }; + } + } + } + + None +} diff --git a/packages/elf-service/tests/acceptance/structured_field_retrieval.rs b/packages/elf-service/tests/acceptance/structured_field_retrieval.rs index eb218cef..6fa89494 100644 --- a/packages/elf-service/tests/acceptance/structured_field_retrieval.rs +++ b/packages/elf-service/tests/acceptance/structured_field_retrieval.rs @@ -1,466 +1,3 @@ -use std::{ - collections::HashMap, - sync::{Arc, atomic::AtomicUsize}, -}; +pub(crate) mod support; -use qdrant_client::{ - Payload, - qdrant::{Document, PointStruct, UpsertPointsBuilder, Vector}, -}; -use serde_json::Value; -use sqlx::PgExecutor; -use time::OffsetDateTime; -use uuid::Uuid; - -use crate::acceptance::{self, SpyExtractor, StubEmbedding}; -use elf_config::ProviderConfig; -use elf_service::{BoxFuture, ElfService, Providers, RerankProvider, Result, SearchRequest}; -use elf_storage::qdrant::{BM25_MODEL, BM25_VECTOR_NAME, DENSE_VECTOR_NAME}; -use elf_testkit::TestDatabase; - -struct TestContext { - service: ElfService, - test_db: TestDatabase, - embedding_version: String, -} - -struct UpsertPointArgs<'a> { - chunk_id: Uuid, - note_id: Uuid, - chunk_index: i32, - start_offset: i32, - end_offset: i32, - text: &'a str, - dense: Vec, -} - -struct KeywordRerank { - keyword: &'static str, -} -impl RerankProvider for KeywordRerank { - fn rerank<'a>( - &'a self, - _cfg: &'a ProviderConfig, - _query: &'a str, - docs: &'a [String], - ) -> BoxFuture<'a, Result>> { - let keyword = self.keyword; - - Box::pin(async move { - Ok(docs.iter().map(|doc| if doc.contains(keyword) { 1.0 } else { 0.1 }).collect()) - }) - } -} - -fn vec_text_zeros() -> String { - let mut buf = String::with_capacity(2 + (4_096 * 2)); - - buf.push('['); - - for i in 0..4_096 { - if i > 0 { - buf.push(','); - } - - buf.push('0'); - } - - buf.push(']'); - - buf -} - -fn build_payload( - note_id: Uuid, - chunk_id: Uuid, - chunk_index: i32, - start_offset: i32, - end_offset: i32, -) -> Payload { - let mut payload = Payload::new(); - - payload.insert("note_id", note_id.to_string()); - payload.insert("chunk_id", chunk_id.to_string()); - payload.insert("chunk_index", Value::from(chunk_index)); - payload.insert("start_offset", Value::from(start_offset)); - payload.insert("end_offset", Value::from(end_offset)); - payload.insert("tenant_id", "t"); - payload.insert("project_id", "p"); - payload.insert("agent_id", "a"); - payload.insert("scope", "agent_private"); - payload.insert("status", "active"); - - payload -} - -fn build_vectors(text: &str, dense: Vec) -> HashMap { - let mut vectors = HashMap::new(); - - vectors.insert(DENSE_VECTOR_NAME.to_string(), Vector::from(dense)); - vectors.insert( - BM25_VECTOR_NAME.to_string(), - Vector::from(Document::new(text.to_string(), BM25_MODEL)), - ); - - vectors -} - -async fn setup_context(test_name: &str) -> Option { - let Some(test_db) = acceptance::test_db().await else { - eprintln!("Skipping {test_name}; set ELF_PG_DSN to run this test."); - - return None; - }; - let Some(qdrant_url) = acceptance::test_qdrant_url() else { - eprintln!("Skipping {test_name}; set ELF_QDRANT_URL to run this test."); - - return None; - }; - let providers = Providers::new( - Arc::new(StubEmbedding { vector_dim: 4_096 }), - Arc::new(KeywordRerank { keyword: "ZEBRA" }), - Arc::new(SpyExtractor { - calls: Arc::new(AtomicUsize::new(0)), - payload: serde_json::json!({ "notes": [] }), - }), - ); - let collection = test_db.collection_name("elf_acceptance"); - let docs_collection = test_db.collection_name("elf_acceptance_docs"); - let cfg = acceptance::test_config( - test_db.dsn().to_string(), - qdrant_url, - 4_096, - collection, - docs_collection, - ); - let service = - acceptance::build_service(cfg, providers).await.expect("Failed to build service."); - - acceptance::reset_db(&service.db.pool).await.expect("Failed to reset test database."); - acceptance::reset_qdrant_collection( - &service.qdrant.client, - &service.qdrant.collection, - service.qdrant.vector_dim, - ) - .await - .expect("Failed to reset Qdrant collection."); - - let embedding_version = format!( - "{}:{}:{}", - service.cfg.providers.embedding.provider_id, - service.cfg.providers.embedding.model, - service.cfg.storage.qdrant.vector_dim - ); - - Some(TestContext { service, test_db, embedding_version }) -} - -async fn insert_note<'e, E>(executor: E, note_id: Uuid, note_text: &str, embedding_version: &str) -where - E: PgExecutor<'e>, -{ - let now = OffsetDateTime::now_utc(); - - sqlx::query( - "\ -INSERT INTO memory_notes ( - note_id, - tenant_id, - project_id, - agent_id, - scope, - type, - key, - text, - importance, - confidence, - status, - created_at, - updated_at, - expires_at, - embedding_version, - source_ref, - hit_count, - last_hit_at -) -VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13, - $14, - $15, - $16, - $17, - $18 -)", - ) - .bind(note_id) - .bind("t") - .bind("p") - .bind("a") - .bind("agent_private") - .bind("fact") - .bind(Option::::None) - .bind(note_text) - .bind(0.4_f32) - .bind(0.9_f32) - .bind("active") - .bind(now) - .bind(now) - .bind(Option::::None) - .bind(embedding_version) - .bind(serde_json::json!({})) - .bind(0_i64) - .bind(Option::::None) - .execute(executor) - .await - .expect("Failed to insert memory note."); -} - -#[allow(clippy::too_many_arguments)] -async fn insert_chunk<'e, E>( - executor: E, - chunk_id: Uuid, - note_id: Uuid, - chunk_index: i32, - start_offset: i32, - end_offset: i32, - text: &str, - embedding_version: &str, -) where - E: PgExecutor<'e>, -{ - sqlx::query( - "\ -INSERT INTO memory_note_chunks ( - chunk_id, - note_id, - chunk_index, - start_offset, - end_offset, - text, - embedding_version -) -VALUES ($1, $2, $3, $4, $5, $6, $7)", - ) - .bind(chunk_id) - .bind(note_id) - .bind(chunk_index) - .bind(start_offset) - .bind(end_offset) - .bind(text) - .bind(embedding_version) - .execute(executor) - .await - .expect("Failed to insert chunk metadata."); -} - -async fn insert_chunk_embedding<'e, E>(executor: E, chunk_id: Uuid, embedding_version: &str) -where - E: PgExecutor<'e>, -{ - let vec_text = vec_text_zeros(); - - sqlx::query( - "\ -INSERT INTO note_chunk_embeddings (chunk_id, embedding_version, embedding_dim, vec) -VALUES ($1, $2, $3, $4::text::vector)", - ) - .bind(chunk_id) - .bind(embedding_version) - .bind(4_096_i32) - .bind(vec_text.as_str()) - .execute(executor) - .await - .expect("Failed to insert chunk embedding."); -} - -async fn insert_fact_field_row<'e, E>(executor: E, field_id: Uuid, note_id: Uuid, fact_text: &str) -where - E: PgExecutor<'e>, -{ - sqlx::query( - "\ -INSERT INTO memory_note_fields (field_id, note_id, field_kind, item_index, text) -VALUES ($1, $2, $3, $4, $5)", - ) - .bind(field_id) - .bind(note_id) - .bind("fact") - .bind(0_i32) - .bind(fact_text) - .execute(executor) - .await - .expect("Failed to insert note field."); -} - -async fn insert_fact_field_embedding<'e, E>(executor: E, field_id: Uuid, embedding_version: &str) -where - E: PgExecutor<'e>, -{ - let vec_text = vec_text_zeros(); - - sqlx::query( - "\ -INSERT INTO note_field_embeddings (field_id, embedding_version, embedding_dim, vec) -VALUES ($1, $2, $3, $4::text::vector)", - ) - .bind(field_id) - .bind(embedding_version) - .bind(4_096_i32) - .bind(vec_text.as_str()) - .execute(executor) - .await - .expect("Failed to insert field embedding."); -} - -async fn upsert_point(service: &ElfService, args: UpsertPointArgs<'_>) { - let payload = build_payload( - args.note_id, - args.chunk_id, - args.chunk_index, - args.start_offset, - args.end_offset, - ); - let vectors = build_vectors(args.text, args.dense); - let point = PointStruct::new(args.chunk_id.to_string(), vectors, payload); - - service - .qdrant - .client - .upsert_points( - UpsertPointsBuilder::new(service.qdrant.collection.clone(), vec![point]).wait(true), - ) - .await - .expect("Failed to upsert Qdrant point."); -} - -#[tokio::test] -#[ignore = "Requires external Postgres and Qdrant. Set ELF_PG_DSN and ELF_QDRANT_URL to run."] -async fn structured_fact_field_can_surface_note_and_marks_matched_fields() { - let Some(context) = - setup_context("structured_fact_field_can_surface_note_and_marks_matched_fields").await - else { - return; - }; - let query = "alpha unique"; - - for i in 0..20 { - let note_id = Uuid::new_v4(); - let chunk_id = Uuid::new_v4(); - let text = format!("Confuser {i}: {query}."); - - insert_note(&context.service.db.pool, note_id, &text, &context.embedding_version).await; - insert_chunk( - &context.service.db.pool, - chunk_id, - note_id, - 0, - 0, - text.len() as i32, - &text, - &context.embedding_version, - ) - .await; - upsert_point( - &context.service, - UpsertPointArgs { - chunk_id, - note_id, - chunk_index: 0, - start_offset: 0, - end_offset: text.len() as i32, - text: &text, - dense: vec![0.0_f32; 4_096], - }, - ) - .await; - } - - let structured_note_id = Uuid::new_v4(); - let structured_chunk_id = Uuid::new_v4(); - let structured_chunk_text = "ZEBRA chunk text does not include the query."; - - insert_note( - &context.service.db.pool, - structured_note_id, - "This note is generic.", - &context.embedding_version, - ) - .await; - insert_chunk( - &context.service.db.pool, - structured_chunk_id, - structured_note_id, - 0, - 0, - structured_chunk_text.len() as i32, - structured_chunk_text, - &context.embedding_version, - ) - .await; - insert_chunk_embedding( - &context.service.db.pool, - structured_chunk_id, - &context.embedding_version, - ) - .await; - upsert_point( - &context.service, - UpsertPointArgs { - chunk_id: structured_chunk_id, - note_id: structured_note_id, - chunk_index: 0, - start_offset: 0, - end_offset: structured_chunk_text.len() as i32, - text: structured_chunk_text, - dense: vec![1.0_f32; 4_096], - }, - ) - .await; - - let field_id = Uuid::new_v4(); - - insert_fact_field_row(&context.service.db.pool, field_id, structured_note_id, query).await; - insert_fact_field_embedding(&context.service.db.pool, field_id, &context.embedding_version) - .await; - - let response = context - .service - .search_raw(SearchRequest { - tenant_id: "t".to_string(), - project_id: "p".to_string(), - agent_id: "a".to_string(), - token_id: None, - read_profile: "private_only".to_string(), - payload_level: Default::default(), - query: query.to_string(), - top_k: Some(1), - candidate_k: Some(10), - filter: None, - record_hits: Some(false), - ranking: None, - }) - .await - .expect("Search failed."); - let item = response.items.first().expect("Expected search result."); - - assert_eq!(item.note_id, structured_note_id); - assert!( - item.explain.r#match.matched_fields.iter().any(|field| field == "facts"), - "Expected matched_fields to include facts; got {:?}", - item.explain.r#match.matched_fields - ); - - context.test_db.cleanup().await.expect("Failed to cleanup test database."); -} +mod structured_facts; diff --git a/packages/elf-service/tests/acceptance/structured_field_retrieval/structured_facts.rs b/packages/elf-service/tests/acceptance/structured_field_retrieval/structured_facts.rs new file mode 100644 index 00000000..152483fd --- /dev/null +++ b/packages/elf-service/tests/acceptance/structured_field_retrieval/structured_facts.rs @@ -0,0 +1,140 @@ +use uuid::Uuid; + +use crate::acceptance::structured_field_retrieval::support::{self, TestContext, UpsertPointArgs}; +use elf_service::SearchRequest; + +#[tokio::test] +#[ignore = "Requires external Postgres and Qdrant. Set ELF_PG_DSN and ELF_QDRANT_URL to run."] +async fn structured_fact_field_can_surface_note_and_marks_matched_fields() { + let Some(context) = + support::setup_context("structured_fact_field_can_surface_note_and_marks_matched_fields") + .await + else { + return; + }; + let query = "alpha unique"; + + insert_confuser_notes(&context, query).await; + + let structured_note_id = insert_structured_fact_note(&context, query).await; + let response = context + .service + .search_raw(SearchRequest { + tenant_id: "t".to_string(), + project_id: "p".to_string(), + agent_id: "a".to_string(), + token_id: None, + read_profile: "private_only".to_string(), + payload_level: Default::default(), + query: query.to_string(), + top_k: Some(1), + candidate_k: Some(10), + filter: None, + record_hits: Some(false), + ranking: None, + }) + .await + .expect("Search failed."); + let item = response.items.first().expect("Expected search result."); + + assert_eq!(item.note_id, structured_note_id); + assert!( + item.explain.r#match.matched_fields.iter().any(|field| field == "facts"), + "Expected matched_fields to include facts; got {:?}", + item.explain.r#match.matched_fields + ); + + context.test_db.cleanup().await.expect("Failed to cleanup test database."); +} + +async fn insert_confuser_notes(context: &TestContext, query: &str) { + for i in 0..20 { + let note_id = Uuid::new_v4(); + let chunk_id = Uuid::new_v4(); + let text = format!("Confuser {i}: {query}."); + + support::insert_note(&context.service.db.pool, note_id, &text, &context.embedding_version) + .await; + support::insert_chunk( + &context.service.db.pool, + chunk_id, + note_id, + 0, + 0, + text.len() as i32, + &text, + &context.embedding_version, + ) + .await; + support::upsert_point( + &context.service, + UpsertPointArgs { + chunk_id, + note_id, + chunk_index: 0, + start_offset: 0, + end_offset: text.len() as i32, + text: &text, + dense: vec![0.0_f32; 4_096], + }, + ) + .await; + } +} + +async fn insert_structured_fact_note(context: &TestContext, query: &str) -> Uuid { + let structured_note_id = Uuid::new_v4(); + let structured_chunk_id = Uuid::new_v4(); + let structured_chunk_text = "ZEBRA chunk text does not include the query."; + + support::insert_note( + &context.service.db.pool, + structured_note_id, + "This note is generic.", + &context.embedding_version, + ) + .await; + support::insert_chunk( + &context.service.db.pool, + structured_chunk_id, + structured_note_id, + 0, + 0, + structured_chunk_text.len() as i32, + structured_chunk_text, + &context.embedding_version, + ) + .await; + support::insert_chunk_embedding( + &context.service.db.pool, + structured_chunk_id, + &context.embedding_version, + ) + .await; + support::upsert_point( + &context.service, + UpsertPointArgs { + chunk_id: structured_chunk_id, + note_id: structured_note_id, + chunk_index: 0, + start_offset: 0, + end_offset: structured_chunk_text.len() as i32, + text: structured_chunk_text, + dense: vec![1.0_f32; 4_096], + }, + ) + .await; + + let field_id = Uuid::new_v4(); + + support::insert_fact_field_row(&context.service.db.pool, field_id, structured_note_id, query) + .await; + support::insert_fact_field_embedding( + &context.service.db.pool, + field_id, + &context.embedding_version, + ) + .await; + + structured_note_id +} diff --git a/packages/elf-service/tests/acceptance/structured_field_retrieval/support.rs b/packages/elf-service/tests/acceptance/structured_field_retrieval/support.rs new file mode 100644 index 00000000..3e28282e --- /dev/null +++ b/packages/elf-service/tests/acceptance/structured_field_retrieval/support.rs @@ -0,0 +1,360 @@ +use std::{ + collections::HashMap, + sync::{Arc, atomic::AtomicUsize}, +}; + +use qdrant_client::{ + Payload, + qdrant::{Document, PointStruct, UpsertPointsBuilder, Vector}, +}; +use serde_json::Value; +use sqlx::PgExecutor; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::acceptance::{self, SpyExtractor, StubEmbedding}; +use elf_config::ProviderConfig; +use elf_service::{BoxFuture, ElfService, Providers, RerankProvider, Result}; +use elf_storage::qdrant::{BM25_MODEL, BM25_VECTOR_NAME, DENSE_VECTOR_NAME}; +use elf_testkit::TestDatabase; + +pub(crate) struct TestContext { + pub(crate) service: ElfService, + pub(crate) test_db: TestDatabase, + pub(crate) embedding_version: String, +} + +pub(crate) struct UpsertPointArgs<'a> { + pub(crate) chunk_id: Uuid, + pub(crate) note_id: Uuid, + pub(crate) chunk_index: i32, + pub(crate) start_offset: i32, + pub(crate) end_offset: i32, + pub(crate) text: &'a str, + pub(crate) dense: Vec, +} + +struct KeywordRerank { + keyword: &'static str, +} +impl RerankProvider for KeywordRerank { + fn rerank<'a>( + &'a self, + _cfg: &'a ProviderConfig, + _query: &'a str, + docs: &'a [String], + ) -> BoxFuture<'a, Result>> { + let keyword = self.keyword; + + Box::pin(async move { + Ok(docs.iter().map(|doc| if doc.contains(keyword) { 1.0 } else { 0.1 }).collect()) + }) + } +} + +pub(crate) async fn setup_context(test_name: &str) -> Option { + let Some(test_db) = acceptance::test_db().await else { + eprintln!("Skipping {test_name}; set ELF_PG_DSN to run this test."); + + return None; + }; + let Some(qdrant_url) = acceptance::test_qdrant_url() else { + eprintln!("Skipping {test_name}; set ELF_QDRANT_URL to run this test."); + + return None; + }; + let providers = Providers::new( + Arc::new(StubEmbedding { vector_dim: 4_096 }), + Arc::new(KeywordRerank { keyword: "ZEBRA" }), + Arc::new(SpyExtractor { + calls: Arc::new(AtomicUsize::new(0)), + payload: serde_json::json!({ "notes": [] }), + }), + ); + let collection = test_db.collection_name("elf_acceptance"); + let docs_collection = test_db.collection_name("elf_acceptance_docs"); + let cfg = acceptance::test_config( + test_db.dsn().to_string(), + qdrant_url, + 4_096, + collection, + docs_collection, + ); + let service = + acceptance::build_service(cfg, providers).await.expect("Failed to build service."); + + acceptance::reset_db(&service.db.pool).await.expect("Failed to reset test database."); + acceptance::reset_qdrant_collection( + &service.qdrant.client, + &service.qdrant.collection, + service.qdrant.vector_dim, + ) + .await + .expect("Failed to reset Qdrant collection."); + + let embedding_version = format!( + "{}:{}:{}", + service.cfg.providers.embedding.provider_id, + service.cfg.providers.embedding.model, + service.cfg.storage.qdrant.vector_dim + ); + + Some(TestContext { service, test_db, embedding_version }) +} + +pub(crate) async fn insert_note<'e, E>( + executor: E, + note_id: Uuid, + note_text: &str, + embedding_version: &str, +) where + E: PgExecutor<'e>, +{ + let now = OffsetDateTime::now_utc(); + + sqlx::query( + "\ +INSERT INTO memory_notes ( + note_id, + tenant_id, + project_id, + agent_id, + scope, + type, + key, + text, + importance, + confidence, + status, + created_at, + updated_at, + expires_at, + embedding_version, + source_ref, + hit_count, + last_hit_at +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18 +)", + ) + .bind(note_id) + .bind("t") + .bind("p") + .bind("a") + .bind("agent_private") + .bind("fact") + .bind(Option::::None) + .bind(note_text) + .bind(0.4_f32) + .bind(0.9_f32) + .bind("active") + .bind(now) + .bind(now) + .bind(Option::::None) + .bind(embedding_version) + .bind(serde_json::json!({})) + .bind(0_i64) + .bind(Option::::None) + .execute(executor) + .await + .expect("Failed to insert memory note."); +} + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn insert_chunk<'e, E>( + executor: E, + chunk_id: Uuid, + note_id: Uuid, + chunk_index: i32, + start_offset: i32, + end_offset: i32, + text: &str, + embedding_version: &str, +) where + E: PgExecutor<'e>, +{ + sqlx::query( + "\ +INSERT INTO memory_note_chunks ( + chunk_id, + note_id, + chunk_index, + start_offset, + end_offset, + text, + embedding_version +) +VALUES ($1, $2, $3, $4, $5, $6, $7)", + ) + .bind(chunk_id) + .bind(note_id) + .bind(chunk_index) + .bind(start_offset) + .bind(end_offset) + .bind(text) + .bind(embedding_version) + .execute(executor) + .await + .expect("Failed to insert chunk metadata."); +} + +pub(crate) async fn insert_chunk_embedding<'e, E>( + executor: E, + chunk_id: Uuid, + embedding_version: &str, +) where + E: PgExecutor<'e>, +{ + let vec_text = vec_text_zeros(); + + sqlx::query( + "\ +INSERT INTO note_chunk_embeddings (chunk_id, embedding_version, embedding_dim, vec) +VALUES ($1, $2, $3, $4::text::vector)", + ) + .bind(chunk_id) + .bind(embedding_version) + .bind(4_096_i32) + .bind(vec_text.as_str()) + .execute(executor) + .await + .expect("Failed to insert chunk embedding."); +} + +pub(crate) async fn insert_fact_field_row<'e, E>( + executor: E, + field_id: Uuid, + note_id: Uuid, + fact_text: &str, +) where + E: PgExecutor<'e>, +{ + sqlx::query( + "\ +INSERT INTO memory_note_fields (field_id, note_id, field_kind, item_index, text) +VALUES ($1, $2, $3, $4, $5)", + ) + .bind(field_id) + .bind(note_id) + .bind("fact") + .bind(0_i32) + .bind(fact_text) + .execute(executor) + .await + .expect("Failed to insert note field."); +} + +pub(crate) async fn insert_fact_field_embedding<'e, E>( + executor: E, + field_id: Uuid, + embedding_version: &str, +) where + E: PgExecutor<'e>, +{ + let vec_text = vec_text_zeros(); + + sqlx::query( + "\ +INSERT INTO note_field_embeddings (field_id, embedding_version, embedding_dim, vec) +VALUES ($1, $2, $3, $4::text::vector)", + ) + .bind(field_id) + .bind(embedding_version) + .bind(4_096_i32) + .bind(vec_text.as_str()) + .execute(executor) + .await + .expect("Failed to insert field embedding."); +} + +pub(crate) async fn upsert_point(service: &ElfService, args: UpsertPointArgs<'_>) { + let payload = build_payload( + args.note_id, + args.chunk_id, + args.chunk_index, + args.start_offset, + args.end_offset, + ); + let vectors = build_vectors(args.text, args.dense); + let point = PointStruct::new(args.chunk_id.to_string(), vectors, payload); + + service + .qdrant + .client + .upsert_points( + UpsertPointsBuilder::new(service.qdrant.collection.clone(), vec![point]).wait(true), + ) + .await + .expect("Failed to upsert Qdrant point."); +} + +fn vec_text_zeros() -> String { + let mut buf = String::with_capacity(2 + (4_096 * 2)); + + buf.push('['); + + for i in 0..4_096 { + if i > 0 { + buf.push(','); + } + + buf.push('0'); + } + + buf.push(']'); + + buf +} + +fn build_payload( + note_id: Uuid, + chunk_id: Uuid, + chunk_index: i32, + start_offset: i32, + end_offset: i32, +) -> Payload { + let mut payload = Payload::new(); + + payload.insert("note_id", note_id.to_string()); + payload.insert("chunk_id", chunk_id.to_string()); + payload.insert("chunk_index", Value::from(chunk_index)); + payload.insert("start_offset", Value::from(start_offset)); + payload.insert("end_offset", Value::from(end_offset)); + payload.insert("tenant_id", "t"); + payload.insert("project_id", "p"); + payload.insert("agent_id", "a"); + payload.insert("scope", "agent_private"); + payload.insert("status", "active"); + + payload +} + +fn build_vectors(text: &str, dense: Vec) -> HashMap { + let mut vectors = HashMap::new(); + + vectors.insert(DENSE_VECTOR_NAME.to_string(), Vector::from(dense)); + vectors.insert( + BM25_VECTOR_NAME.to_string(), + Vector::from(Document::new(text.to_string(), BM25_MODEL)), + ); + + vectors +} diff --git a/packages/elf-service/tests/acceptance/trace_admin_observability.rs b/packages/elf-service/tests/acceptance/trace_admin_observability.rs index 34591da8..bea713c2 100644 --- a/packages/elf-service/tests/acceptance/trace_admin_observability.rs +++ b/packages/elf-service/tests/acceptance/trace_admin_observability.rs @@ -2,9 +2,13 @@ mod helpers; pub(crate) use helpers::{ PROJECT_ID, TENANT_ID, TraceAdminObservabilityFixture, VisibilityTraceFixtureIds, - assert_trace_admin_visibility_cross_scope, insert_trace, insert_trace_candidate, - insert_trace_item, insert_trace_stage, insert_trace_stage_item, - seed_visibility_and_recent_list_traces, setup_service, trace_recent_list_page, + assertions::assert_trace_admin_visibility_cross_scope, + inserts::{ + insert_trace, insert_trace_candidate, insert_trace_item, insert_trace_stage, + insert_trace_stage_item, + }, + seed::{seed_visibility_and_recent_list_traces, trace_recent_list_page}, + setup::setup_service, }; use time::OffsetDateTime; diff --git a/packages/elf-service/tests/acceptance/trace_admin_observability/helpers.rs b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers.rs index d35c863a..a1382c68 100644 --- a/packages/elf-service/tests/acceptance/trace_admin_observability/helpers.rs +++ b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers.rs @@ -1,15 +1,11 @@ -use std::sync::{Arc, atomic::AtomicUsize}; +pub(crate) mod assertions; +pub(crate) mod inserts; +pub(crate) mod seed; +pub(crate) mod setup; -use serde_json::Value; -use sqlx::PgPool; -use time::{Duration, OffsetDateTime}; use uuid::Uuid; -use crate::acceptance::{self, SpyExtractor, StubEmbedding, StubRerank}; -use elf_service::{ - ElfService, Providers, SearchExplainRequest, TraceGetRequest, TraceRecentListRequest, - TraceRecentListResponse, TraceTrajectoryGetRequest, search::TraceReplayCandidate, -}; +use elf_service::ElfService; use elf_testkit::TestDatabase; pub(crate) const TENANT_ID: &str = "tenant_admin_scope"; @@ -27,400 +23,3 @@ pub(crate) struct VisibilityTraceFixtureIds { pub(crate) trace_three: Uuid, pub(crate) item_two: Uuid, } - -pub(crate) async fn setup_service(test_name: &str) -> Option { - let Some(test_db) = acceptance::test_db().await else { - eprintln!("Skipping {test_name}; set ELF_PG_DSN to run this test."); - - return None; - }; - let Some(qdrant_url) = acceptance::test_qdrant_url() else { - eprintln!("Skipping {test_name}; set ELF_QDRANT_URL to run this test."); - - return None; - }; - let collection = test_db.collection_name("elf_acceptance"); - let docs_collection = test_db.collection_name("elf_acceptance_docs"); - let cfg = acceptance::test_config( - test_db.dsn().to_string(), - qdrant_url, - 4_096, - collection, - docs_collection, - ); - let extractor = SpyExtractor { - calls: Arc::new(AtomicUsize::new(0)), - payload: serde_json::json!({ "notes": [] }), - }; - let providers = Providers::new( - Arc::new(StubEmbedding { vector_dim: 4_096 }), - Arc::new(StubRerank), - Arc::new(extractor), - ); - let service = - acceptance::build_service(cfg, providers).await.expect("Failed to build service."); - - acceptance::reset_db(&service.db.pool).await.expect("Failed to reset test database."); - - Some(TraceAdminObservabilityFixture { service, test_db }) -} - -pub(crate) async fn insert_trace( - executor: &PgPool, - trace_id: Uuid, - agent_id: &str, - read_profile: &str, - query: &str, - created_at: OffsetDateTime, -) { - sqlx::query( - "\ -INSERT INTO search_traces ( - trace_id, - tenant_id, - project_id, - agent_id, - read_profile, - query, - expansion_mode, - expanded_queries, - allowed_scopes, - candidate_count, - top_k, - config_snapshot, - trace_version, - created_at, - expires_at -) - VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13, - $14, - $15 - )", - ) - .bind(trace_id) - .bind(TENANT_ID) - .bind(PROJECT_ID) - .bind(agent_id) - .bind(read_profile) - .bind(query) - .bind("full") - .bind(serde_json::json!([query])) - .bind(serde_json::json!(["agent_private", "project_shared", "org_shared"])) - .bind(10_i32) - .bind(5_i32) - .bind(serde_json::json!({ "test": true })) - .bind(TRACE_VERSION) - .bind(created_at) - .bind(created_at + Duration::minutes(60)) - .execute(executor) - .await - .expect("Failed to insert trace."); -} - -pub(crate) async fn insert_trace_item( - executor: &PgPool, - item_id: Uuid, - trace_id: Uuid, - note_id: Uuid, - chunk_id: Uuid, - rank: i32, -) { - sqlx::query( - "\ -INSERT INTO search_trace_items ( - item_id, - trace_id, - note_id, - chunk_id, - rank, - final_score, - explain -) -VALUES ($1, $2, $3, $4, $5, $6, $7)", - ) - .bind(item_id) - .bind(trace_id) - .bind(note_id) - .bind(chunk_id) - .bind(rank) - .bind(1.0_f32) - .bind(serde_json::json!({ - "match": { "matched_terms": [], "matched_fields": [] }, - "ranking": { - "schema": "search_ranking_explain/v2", - "policy_id": "ranking_v2:test", - "final_score": 1.0, - "terms": [] - } - })) - .execute(executor) - .await - .expect("Failed to insert trace item."); -} - -pub(crate) async fn insert_trace_stage( - executor: &PgPool, - stage_id: Uuid, - trace_id: Uuid, - stage_order: i32, - stage_name: &str, - created_at: OffsetDateTime, -) { - sqlx::query( - "\ -INSERT INTO search_trace_stages ( - stage_id, - trace_id, - stage_order, - stage_name, - stage_payload, - created_at -) -VALUES ($1, $2, $3, $4, $5, $6)", - ) - .bind(stage_id) - .bind(trace_id) - .bind(stage_order) - .bind(stage_name) - .bind(serde_json::json!({ - "stage_name": stage_name, - "metrics": { "items": 0 } - })) - .bind(created_at) - .execute(executor) - .await - .expect("Failed to insert trace stage."); -} - -pub(crate) async fn insert_trace_stage_item( - executor: &PgPool, - item_id: Uuid, - stage_id: Uuid, - note_id: Uuid, - chunk_id: Uuid, - metrics: Value, -) { - sqlx::query( - "\ -INSERT INTO search_trace_stage_items ( - id, - stage_id, - item_id, - note_id, - chunk_id, - metrics -) -VALUES ($1, $2, $3, $4, $5, $6)", - ) - .bind(Uuid::new_v4()) - .bind(stage_id) - .bind(item_id) - .bind(note_id) - .bind(chunk_id) - .bind(metrics) - .execute(executor) - .await - .expect("Failed to insert trace stage item."); -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn insert_trace_candidate( - executor: &PgPool, - candidate_id: Uuid, - trace_id: Uuid, - note_id: Uuid, - chunk_id: Uuid, - rank: i32, - retrieval_rank: i32, - retrieval_score: f32, - created_at: OffsetDateTime, -) { - sqlx::query( - "\ -INSERT INTO search_trace_candidates ( - candidate_id, - trace_id, - note_id, - chunk_id, - chunk_index, - snippet, - candidate_snapshot, - retrieval_rank, - rerank_score, - note_scope, - note_importance, - note_updated_at, - note_hit_count, - note_last_hit_at, - created_at, - expires_at -) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)", - ) - .bind(candidate_id) - .bind(trace_id) - .bind(note_id) - .bind(chunk_id) - .bind(rank) - .bind("trace candidate snippet") - .bind({ - let candidate_snapshot = TraceReplayCandidate { - note_id, - chunk_id, - chunk_index: rank, - snippet: "trace candidate snippet".to_string(), - retrieval_rank: retrieval_rank as u32, - retrieval_score: Some(retrieval_score), - rerank_score: retrieval_score, - note_scope: "agent_private".to_string(), - note_importance: 0.6, - note_updated_at: created_at, - note_hit_count: 12, - note_last_hit_at: None, - diversity_selected: None, - diversity_selected_rank: None, - diversity_selected_reason: None, - diversity_skipped_reason: None, - diversity_nearest_selected_note_id: None, - diversity_similarity: None, - diversity_mmr_score: None, - diversity_missing_embedding: None, - }; - - serde_json::to_value(candidate_snapshot) - .expect("Failed to serialize trace replay candidate.") - }) - .bind(retrieval_rank) - .bind(retrieval_score) - .bind("agent_private") - .bind(0.6_f32) - .bind(created_at) - .bind(12_i64) - .bind(Option::::None) - .bind(created_at) - .bind(created_at + Duration::minutes(90)) - .execute(executor) - .await - .expect("Failed to insert trace candidate."); -} - -pub(crate) async fn seed_visibility_and_recent_list_traces( - service: &ElfService, - now: OffsetDateTime, -) -> VisibilityTraceFixtureIds { - let trace_one = Uuid::new_v4(); - let trace_two = Uuid::new_v4(); - let trace_three = Uuid::new_v4(); - let item_one = Uuid::new_v4(); - let item_two = Uuid::new_v4(); - let item_three = Uuid::new_v4(); - let note_one = Uuid::new_v4(); - let note_two = Uuid::new_v4(); - let note_three = Uuid::new_v4(); - let chunk_one = Uuid::new_v4(); - let chunk_two = Uuid::new_v4(); - let chunk_three = Uuid::new_v4(); - - insert_trace(&service.db.pool, trace_one, "agent_one", "private_only", "one", now).await; - insert_trace( - &service.db.pool, - trace_two, - "agent_two", - "private_only", - "two", - now - Duration::seconds(10), - ) - .await; - insert_trace( - &service.db.pool, - trace_three, - "agent_three", - "private_only", - "three", - now - Duration::seconds(20), - ) - .await; - insert_trace_item(&service.db.pool, item_one, trace_one, note_one, chunk_one, 1).await; - insert_trace_item(&service.db.pool, item_two, trace_two, note_two, chunk_two, 1).await; - insert_trace_item(&service.db.pool, item_three, trace_three, note_three, chunk_three, 1).await; - - VisibilityTraceFixtureIds { trace_one, trace_two, trace_three, item_two } -} - -pub(crate) async fn trace_recent_list_page( - service: &ElfService, - cursor_created_at: Option, - cursor_trace_id: Option, -) -> TraceRecentListResponse { - service - .trace_recent_list(TraceRecentListRequest { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - agent_id: "admin_agent".to_string(), - limit: Some(2), - cursor_created_at, - cursor_trace_id, - agent_id_filter: None, - read_profile: None, - created_after: None, - created_before: None, - }) - .await - .expect("Failed to list recent traces.") -} - -pub(crate) async fn assert_trace_admin_visibility_cross_scope( - service: &ElfService, - trace_id: Uuid, - item_id: Uuid, -) { - let cross_agent_trace_get = service - .trace_get(TraceGetRequest { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - agent_id: "different_agent".to_string(), - trace_id, - }) - .await - .expect("Expected cross-agent trace lookup to bypass agent ownership filtering."); - - assert_eq!(cross_agent_trace_get.trace.trace_id, trace_id); - assert_eq!(cross_agent_trace_get.trace.agent_id, "agent_two"); - - let cross_agent_trajectory = service - .trace_trajectory_get(TraceTrajectoryGetRequest { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - agent_id: "different_agent".to_string(), - trace_id, - }) - .await - .expect("Expected cross-agent trajectory lookup to bypass agent ownership filtering."); - - assert_eq!(cross_agent_trajectory.trace.trace_id, trace_id); - - let cross_agent_item = service - .search_explain(SearchExplainRequest { - tenant_id: TENANT_ID.to_string(), - project_id: PROJECT_ID.to_string(), - agent_id: "different_agent".to_string(), - result_handle: item_id, - }) - .await - .expect("Expected cross-agent trace-item lookup to bypass agent ownership filtering."); - - assert_eq!(cross_agent_item.item.result_handle, item_id); -} diff --git a/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/assertions.rs b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/assertions.rs new file mode 100644 index 00000000..7c89e678 --- /dev/null +++ b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/assertions.rs @@ -0,0 +1,47 @@ +use uuid::Uuid; + +use crate::acceptance::trace_admin_observability::helpers::{PROJECT_ID, TENANT_ID}; +use elf_service::{ElfService, SearchExplainRequest, TraceGetRequest, TraceTrajectoryGetRequest}; + +pub(crate) async fn assert_trace_admin_visibility_cross_scope( + service: &ElfService, + trace_id: Uuid, + item_id: Uuid, +) { + let cross_agent_trace_get = service + .trace_get(TraceGetRequest { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + agent_id: "different_agent".to_string(), + trace_id, + }) + .await + .expect("Expected cross-agent trace lookup to bypass agent ownership filtering."); + + assert_eq!(cross_agent_trace_get.trace.trace_id, trace_id); + assert_eq!(cross_agent_trace_get.trace.agent_id, "agent_two"); + + let cross_agent_trajectory = service + .trace_trajectory_get(TraceTrajectoryGetRequest { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + agent_id: "different_agent".to_string(), + trace_id, + }) + .await + .expect("Expected cross-agent trajectory lookup to bypass agent ownership filtering."); + + assert_eq!(cross_agent_trajectory.trace.trace_id, trace_id); + + let cross_agent_item = service + .search_explain(SearchExplainRequest { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + agent_id: "different_agent".to_string(), + result_handle: item_id, + }) + .await + .expect("Expected cross-agent trace-item lookup to bypass agent ownership filtering."); + + assert_eq!(cross_agent_item.item.result_handle, item_id); +} diff --git a/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/inserts.rs b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/inserts.rs new file mode 100644 index 00000000..d99042d7 --- /dev/null +++ b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/inserts.rs @@ -0,0 +1,259 @@ +use serde_json::Value; +use sqlx::PgPool; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; + +use crate::acceptance::trace_admin_observability::helpers::{PROJECT_ID, TENANT_ID, TRACE_VERSION}; +use elf_service::search::TraceReplayCandidate; + +pub(crate) async fn insert_trace( + executor: &PgPool, + trace_id: Uuid, + agent_id: &str, + read_profile: &str, + query: &str, + created_at: OffsetDateTime, +) { + sqlx::query( + "\ +INSERT INTO search_traces ( + trace_id, + tenant_id, + project_id, + agent_id, + read_profile, + query, + expansion_mode, + expanded_queries, + allowed_scopes, + candidate_count, + top_k, + config_snapshot, + trace_version, + created_at, + expires_at +) + VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + )", + ) + .bind(trace_id) + .bind(TENANT_ID) + .bind(PROJECT_ID) + .bind(agent_id) + .bind(read_profile) + .bind(query) + .bind("full") + .bind(serde_json::json!([query])) + .bind(serde_json::json!(["agent_private", "project_shared", "org_shared"])) + .bind(10_i32) + .bind(5_i32) + .bind(serde_json::json!({ "test": true })) + .bind(TRACE_VERSION) + .bind(created_at) + .bind(created_at + Duration::minutes(60)) + .execute(executor) + .await + .expect("Failed to insert trace."); +} + +pub(crate) async fn insert_trace_item( + executor: &PgPool, + item_id: Uuid, + trace_id: Uuid, + note_id: Uuid, + chunk_id: Uuid, + rank: i32, +) { + sqlx::query( + "\ +INSERT INTO search_trace_items ( + item_id, + trace_id, + note_id, + chunk_id, + rank, + final_score, + explain +) +VALUES ($1, $2, $3, $4, $5, $6, $7)", + ) + .bind(item_id) + .bind(trace_id) + .bind(note_id) + .bind(chunk_id) + .bind(rank) + .bind(1.0_f32) + .bind(serde_json::json!({ + "match": { "matched_terms": [], "matched_fields": [] }, + "ranking": { + "schema": "search_ranking_explain/v2", + "policy_id": "ranking_v2:test", + "final_score": 1.0, + "terms": [] + } + })) + .execute(executor) + .await + .expect("Failed to insert trace item."); +} + +pub(crate) async fn insert_trace_stage( + executor: &PgPool, + stage_id: Uuid, + trace_id: Uuid, + stage_order: i32, + stage_name: &str, + created_at: OffsetDateTime, +) { + sqlx::query( + "\ +INSERT INTO search_trace_stages ( + stage_id, + trace_id, + stage_order, + stage_name, + stage_payload, + created_at +) +VALUES ($1, $2, $3, $4, $5, $6)", + ) + .bind(stage_id) + .bind(trace_id) + .bind(stage_order) + .bind(stage_name) + .bind(serde_json::json!({ + "stage_name": stage_name, + "metrics": { "items": 0 } + })) + .bind(created_at) + .execute(executor) + .await + .expect("Failed to insert trace stage."); +} + +pub(crate) async fn insert_trace_stage_item( + executor: &PgPool, + item_id: Uuid, + stage_id: Uuid, + note_id: Uuid, + chunk_id: Uuid, + metrics: Value, +) { + sqlx::query( + "\ +INSERT INTO search_trace_stage_items ( + id, + stage_id, + item_id, + note_id, + chunk_id, + metrics +) +VALUES ($1, $2, $3, $4, $5, $6)", + ) + .bind(Uuid::new_v4()) + .bind(stage_id) + .bind(item_id) + .bind(note_id) + .bind(chunk_id) + .bind(metrics) + .execute(executor) + .await + .expect("Failed to insert trace stage item."); +} + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn insert_trace_candidate( + executor: &PgPool, + candidate_id: Uuid, + trace_id: Uuid, + note_id: Uuid, + chunk_id: Uuid, + rank: i32, + retrieval_rank: i32, + retrieval_score: f32, + created_at: OffsetDateTime, +) { + sqlx::query( + "\ +INSERT INTO search_trace_candidates ( + candidate_id, + trace_id, + note_id, + chunk_id, + chunk_index, + snippet, + candidate_snapshot, + retrieval_rank, + rerank_score, + note_scope, + note_importance, + note_updated_at, + note_hit_count, + note_last_hit_at, + created_at, + expires_at +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)", + ) + .bind(candidate_id) + .bind(trace_id) + .bind(note_id) + .bind(chunk_id) + .bind(rank) + .bind("trace candidate snippet") + .bind({ + let candidate_snapshot = TraceReplayCandidate { + note_id, + chunk_id, + chunk_index: rank, + snippet: "trace candidate snippet".to_string(), + retrieval_rank: retrieval_rank as u32, + retrieval_score: Some(retrieval_score), + rerank_score: retrieval_score, + note_scope: "agent_private".to_string(), + note_importance: 0.6, + note_updated_at: created_at, + note_hit_count: 12, + note_last_hit_at: None, + diversity_selected: None, + diversity_selected_rank: None, + diversity_selected_reason: None, + diversity_skipped_reason: None, + diversity_nearest_selected_note_id: None, + diversity_similarity: None, + diversity_mmr_score: None, + diversity_missing_embedding: None, + }; + + serde_json::to_value(candidate_snapshot) + .expect("Failed to serialize trace replay candidate.") + }) + .bind(retrieval_rank) + .bind(retrieval_score) + .bind("agent_private") + .bind(0.6_f32) + .bind(created_at) + .bind(12_i64) + .bind(Option::::None) + .bind(created_at) + .bind(created_at + Duration::minutes(90)) + .execute(executor) + .await + .expect("Failed to insert trace candidate."); +} diff --git a/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/seed.rs b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/seed.rs new file mode 100644 index 00000000..1f4cc428 --- /dev/null +++ b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/seed.rs @@ -0,0 +1,81 @@ +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; + +use crate::acceptance::trace_admin_observability::helpers::{ + PROJECT_ID, TENANT_ID, VisibilityTraceFixtureIds, inserts, +}; +use elf_service::{ElfService, TraceRecentListRequest, TraceRecentListResponse}; + +pub(crate) async fn seed_visibility_and_recent_list_traces( + service: &ElfService, + now: OffsetDateTime, +) -> VisibilityTraceFixtureIds { + let trace_one = Uuid::new_v4(); + let trace_two = Uuid::new_v4(); + let trace_three = Uuid::new_v4(); + let item_one = Uuid::new_v4(); + let item_two = Uuid::new_v4(); + let item_three = Uuid::new_v4(); + let note_one = Uuid::new_v4(); + let note_two = Uuid::new_v4(); + let note_three = Uuid::new_v4(); + let chunk_one = Uuid::new_v4(); + let chunk_two = Uuid::new_v4(); + let chunk_three = Uuid::new_v4(); + + inserts::insert_trace(&service.db.pool, trace_one, "agent_one", "private_only", "one", now) + .await; + inserts::insert_trace( + &service.db.pool, + trace_two, + "agent_two", + "private_only", + "two", + now - Duration::seconds(10), + ) + .await; + inserts::insert_trace( + &service.db.pool, + trace_three, + "agent_three", + "private_only", + "three", + now - Duration::seconds(20), + ) + .await; + inserts::insert_trace_item(&service.db.pool, item_one, trace_one, note_one, chunk_one, 1).await; + inserts::insert_trace_item(&service.db.pool, item_two, trace_two, note_two, chunk_two, 1).await; + inserts::insert_trace_item( + &service.db.pool, + item_three, + trace_three, + note_three, + chunk_three, + 1, + ) + .await; + + VisibilityTraceFixtureIds { trace_one, trace_two, trace_three, item_two } +} + +pub(crate) async fn trace_recent_list_page( + service: &ElfService, + cursor_created_at: Option, + cursor_trace_id: Option, +) -> TraceRecentListResponse { + service + .trace_recent_list(TraceRecentListRequest { + tenant_id: TENANT_ID.to_string(), + project_id: PROJECT_ID.to_string(), + agent_id: "admin_agent".to_string(), + limit: Some(2), + cursor_created_at, + cursor_trace_id, + agent_id_filter: None, + read_profile: None, + created_after: None, + created_before: None, + }) + .await + .expect("Failed to list recent traces.") +} diff --git a/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/setup.rs b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/setup.rs new file mode 100644 index 00000000..3e16f171 --- /dev/null +++ b/packages/elf-service/tests/acceptance/trace_admin_observability/helpers/setup.rs @@ -0,0 +1,44 @@ +use std::sync::{Arc, atomic::AtomicUsize}; + +use crate::acceptance::{ + self, SpyExtractor, StubEmbedding, StubRerank, + trace_admin_observability::helpers::TraceAdminObservabilityFixture, +}; +use elf_service::Providers; + +pub(crate) async fn setup_service(test_name: &str) -> Option { + let Some(test_db) = acceptance::test_db().await else { + eprintln!("Skipping {test_name}; set ELF_PG_DSN to run this test."); + + return None; + }; + let Some(qdrant_url) = acceptance::test_qdrant_url() else { + eprintln!("Skipping {test_name}; set ELF_QDRANT_URL to run this test."); + + return None; + }; + let collection = test_db.collection_name("elf_acceptance"); + let docs_collection = test_db.collection_name("elf_acceptance_docs"); + let cfg = acceptance::test_config( + test_db.dsn().to_string(), + qdrant_url, + 4_096, + collection, + docs_collection, + ); + let extractor = SpyExtractor { + calls: Arc::new(AtomicUsize::new(0)), + payload: serde_json::json!({ "notes": [] }), + }; + let providers = Providers::new( + Arc::new(StubEmbedding { vector_dim: 4_096 }), + Arc::new(StubRerank), + Arc::new(extractor), + ); + let service = + acceptance::build_service(cfg, providers).await.expect("Failed to build service."); + + acceptance::reset_db(&service.db.pool).await.expect("Failed to reset test database."); + + Some(TraceAdminObservabilityFixture { service, test_db }) +}