Skip to content

Commit 066a655

Browse files
Test Userclaude
andcommitted
test(server): add KG fixture with trigger::/pinned:: and on-disk integration tests Refs #2173
AC4 of the Gitea #84 plan (issue #2173) was unverified on main: no KG markdown fixture under terraphim_server/fixtures/haystack/ carried a trigger:: or pinned:: directive, so the parse -> trigger-index -> TF-IDF fallback -> pinned-inclusion pipeline had no on-disk regression coverage (existing coverage elsewhere is in-memory only). Adds the on-disk fixture and a fixture-driven integration test in the surviving consumer crate (terraphim_server): - fixtures/haystack/rust_dependency_management_trigger.md: KG entry with synonyms::, trigger::, and pinned:: true (matches existing haystack directive style). - tests/trigger_index_fixture_test.rs: 4 tests driving the real pipeline from disk: 1. fixture_dir_contains_trigger_and_pinned_directives (AC4 precondition) 2. trigger_index_is_populated_from_disk_fixtures (AC: index non-empty) 3. trigger_only_query_returns_results_via_tfidf_fallback (AC: trigger-only query returns results; +negative control) 4. pinned_entries_present_only_when_include_pinned_true (AC: pinned appear w/ include_pinned=true, absent w/ =false) The plan's API (TriggerIndex, parse_markdown_directives_dir, RoleGraph::load_trigger_index / find_matching_node_ids_with_fallback, MarkdownDirectives{trigger,pinned}) was implemented in the extracted terraphim-core repo and is consumed here as published registry crates terraphim_rolegraph/automata/types 1.20.4. Why a new branch: the prior attempt task/2173-kg-fixture-trigger-pinned-tests is dead -- it modified crates/terraphim_rolegraph/tests/, a path extracted out of this repo (a292086). This branch relocates the fix to terraphim_server, the only in-repo crate that depends on all three core crates and ships the fixture corpus. Verification (all exit 0): - cargo test -p terraphim_server --test trigger_index_fixture_test -> 4 passed - cargo clippy -p terraphim_server --test trigger_index_fixture_test -- -D warnings -> clean - rustfmt --edition 2024 --check -> clean No production code changed. No new dependencies (terraphim_server already depends on terraphim_rolegraph/automata/types/ahash). Cargo.lock untouched. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d3490f9 commit 066a655

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Rust Dependency Management
2+
3+
A knowledge-graph entry covering how Rust projects declare, lock, and audit
4+
third-party crates using Cargo. Trigger- and pinned-based retrieval relies on
5+
the directives below; the body text is ordinary prose so the entry also
6+
participates in normal synonym (Aho-Corasick) matching.
7+
8+
synonyms:: cargo package management, rust dependency, dependency management in rust, cargo crates
9+
trigger:: when managing rust cargo dependencies and crate versions
10+
pinned:: true
11+
12+
## Topics
13+
14+
- `Cargo.toml` manifest and the `[dependencies]` table
15+
- `Cargo.lock` for reproducible, locked builds
16+
- `cargo update` to advance crate versions within semver bounds
17+
- Auditing the dependency tree with `cargo audit`
18+
19+
## See also
20+
21+
- `rust_example.md` for general Rust programming patterns
22+
- `testing_strategies.md` for test-organisation practices
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//! Fixture-driven integration tests for `trigger::` and `pinned::` KG directives.
2+
//!
3+
//! Satisfies AC4 of the Gitea #84 plan (issue #2173): verifies that KG markdown
4+
//! files on disk carrying `trigger::` and `pinned::` directives are correctly
5+
//! parsed by `terraphim_automata` and integrated into the `terraphim_rolegraph`
6+
//! trigger-index + two-pass fallback search.
7+
//!
8+
//! These tests deliberately drive the pipeline from disk (the regression gap
9+
//! the issue names): `parse_markdown_directives_dir` -> deterministic ID
10+
//! assignment -> `RoleGraph::load_trigger_index` ->
11+
//! `find_matching_node_ids_with_fallback`. Existing coverage elsewhere uses
12+
//! in-memory triggers only.
13+
14+
#![cfg(test)]
15+
16+
use std::path::{Path, PathBuf};
17+
18+
use ahash::AHashMap;
19+
use terraphim_automata::parse_markdown_directives_dir;
20+
use terraphim_rolegraph::{DEFAULT_TRIGGER_THRESHOLD, RoleGraph};
21+
use terraphim_types::{RoleName, Thesaurus};
22+
23+
/// Resolve the fixture haystack directory relative to this crate's manifest.
24+
///
25+
/// `terraphim_server` is a workspace member at the repo root, so the haystack
26+
/// lives at `<manifest>/fixtures/haystack`.
27+
fn fixture_haystack_dir() -> PathBuf {
28+
Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/haystack")
29+
}
30+
31+
/// Parse the fixture directory, assign deterministic sequential node IDs to
32+
/// every concept that carries a `trigger::` directive, and return
33+
/// `(triggers_map, pinned_ids)`.
34+
///
35+
/// IDs are assigned in sorted-by-concept-name order so the mapping is stable
36+
/// across runs and independent of `HashMap` iteration order. A concept appears
37+
/// in the map iff it has a non-empty `trigger::`; it is added to `pinned_ids`
38+
/// iff its `MarkdownDirectives::pinned` flag is `true`.
39+
fn build_trigger_data_from_fixtures() -> (AHashMap<u64, String>, Vec<u64>) {
40+
let fixture_path = fixture_haystack_dir();
41+
let parse_result =
42+
parse_markdown_directives_dir(&fixture_path).expect("fixture dir should be parseable");
43+
44+
let mut triggers: AHashMap<u64, String> = AHashMap::new();
45+
let mut pinned_ids: Vec<u64> = Vec::new();
46+
let mut next_id: u64 = 1;
47+
48+
// Sort by concept name for deterministic ID assignment across test runs.
49+
let mut concepts: Vec<_> = parse_result.directives.iter().collect();
50+
concepts.sort_by_key(|(k, _)| k.as_str());
51+
52+
for (concept, directives) in concepts {
53+
if let Some(trigger_text) = &directives.trigger {
54+
let trimmed = trigger_text.trim();
55+
if trimmed.is_empty() {
56+
continue;
57+
}
58+
triggers.insert(next_id, trimmed.to_string());
59+
if directives.pinned {
60+
pinned_ids.push(next_id);
61+
}
62+
// Diagnostic on panic: which concept failed to assign.
63+
let _ = concept;
64+
next_id += 1;
65+
}
66+
}
67+
68+
(triggers, pinned_ids)
69+
}
70+
71+
/// Build a minimal empty `RoleGraph` (no Aho-Corasick synonyms) and load the
72+
/// fixture-derived trigger index into it. Using an empty thesaurus guarantees
73+
/// that `find_matching_node_ids_with_fallback` exercises the TF-IDF fallback
74+
/// path (pass 2) rather than an Aho-Corasick synonym hit.
75+
fn graph_with_fixture_trigger_index() -> RoleGraph {
76+
let thesaurus = Thesaurus::new("test".to_string());
77+
let role = RoleName::from("test-role");
78+
let mut graph = RoleGraph::new_sync(role, thesaurus).expect("new_sync should succeed");
79+
80+
let (triggers, pinned) = build_trigger_data_from_fixtures();
81+
graph.load_trigger_index(triggers, pinned, DEFAULT_TRIGGER_THRESHOLD);
82+
graph
83+
}
84+
85+
#[test]
86+
fn fixture_dir_contains_trigger_and_pinned_directives() {
87+
// AC4 (precondition): at least one fixture file carries trigger:: and
88+
// pinned:: true. If this fails, the fixture
89+
// `rust_dependency_management_trigger.md` was removed or its directives
90+
// were altered.
91+
let (triggers, pinned_ids) = build_trigger_data_from_fixtures();
92+
93+
assert!(
94+
!triggers.is_empty(),
95+
"Expected at least one trigger:: directive in fixture files under \
96+
terraphim_server/fixtures/haystack/; add a .md file with 'trigger::' \
97+
to satisfy AC4 of Gitea #84"
98+
);
99+
assert!(
100+
!pinned_ids.is_empty(),
101+
"Expected at least one pinned:: true directive in fixture files under \
102+
terraphim_server/fixtures/haystack/; add a .md file with 'pinned:: true' \
103+
to satisfy AC4 of Gitea #84"
104+
);
105+
}
106+
107+
#[test]
108+
fn trigger_index_is_populated_from_disk_fixtures() {
109+
// AC4 (1): the trigger index built from on-disk fixtures is non-empty and
110+
// the graph reports a non-empty trigger index.
111+
let graph = graph_with_fixture_trigger_index();
112+
113+
// The fallback only consults the trigger index when it is non-empty, so
114+
// confirm the graph actually ingested the fixture data by observing that
115+
// a trigger-matching query returns a non-empty result (next test) and that
116+
// an unrelated query against an empty graph would be empty. Here we assert
117+
// the positive direction: a known fixture trigger phrase matches.
118+
let results = graph.find_matching_node_ids_with_fallback("cargo dependency management", false);
119+
assert!(
120+
!results.is_empty(),
121+
"trigger index should be populated from fixtures and match a query \
122+
sharing tokens with the fixture's trigger text"
123+
);
124+
}
125+
126+
#[test]
127+
fn trigger_only_query_returns_results_via_tfidf_fallback() {
128+
// AC4 (2): a query that matches ONLY via trigger (no synonym present in the
129+
// empty thesaurus) must still return results through the TF-IDF fallback.
130+
let graph = graph_with_fixture_trigger_index();
131+
132+
// "crate version auditing" shares no synonym with the empty thesaurus but
133+
// overlaps the fixture trigger text semantically. The fallback should fire.
134+
let results = graph.find_matching_node_ids_with_fallback("cargo crate version", false);
135+
assert!(
136+
!results.is_empty(),
137+
"a trigger-only query must return results via the TF-IDF fallback when \
138+
Aho-Corasick finds no synonym matches"
139+
);
140+
141+
// Negative control: a completely unrelated query returns nothing (the
142+
// fallback must not match arbitrarily).
143+
let unrelated = graph.find_matching_node_ids_with_fallback("xyzzy plugh frobnicate", false);
144+
assert!(
145+
unrelated.is_empty(),
146+
"an unrelated query must return no results; got {unrelated:?}"
147+
);
148+
}
149+
150+
#[test]
151+
fn pinned_entries_present_only_when_include_pinned_true() {
152+
// AC4 (3): pinned entries appear in results when include_pinned=true and
153+
// are absent when include_pinned=false.
154+
let graph = graph_with_fixture_trigger_index();
155+
let (_triggers, pinned_ids) = build_trigger_data_from_fixtures();
156+
let a_pinned_id = pinned_ids
157+
.first()
158+
.copied()
159+
.expect("at least one pinned id is required by the fixture precondition");
160+
161+
// Use a query that matches NO synonym and NO trigger token, so the ONLY way
162+
// the pinned id can appear is via the include_pinned path.
163+
let query = "completely unrelated xyzzy plugh";
164+
165+
let with_pinned = graph.find_matching_node_ids_with_fallback(query, true);
166+
assert!(
167+
with_pinned.contains(&a_pinned_id),
168+
"pinned node {a_pinned_id} must appear when include_pinned=true; got {with_pinned:?}"
169+
);
170+
171+
let without_pinned = graph.find_matching_node_ids_with_fallback(query, false);
172+
assert!(
173+
!without_pinned.contains(&a_pinned_id),
174+
"pinned node {a_pinned_id} must NOT appear when include_pinned=false; got {without_pinned:?}"
175+
);
176+
}

0 commit comments

Comments
 (0)