Skip to content

Commit 7b90a65

Browse files
committed
test(rust): mock-server integration proves gnn_augment_tactics wires top-ranked premises into rocq/lean/agda/isabelle/z3
Spawns an in-process axum server on a random port serving fixed /gnn/health and /gnn/rank responses. Asserts that for each of the 5 backends the first tactic from suggest_tactics() is Tactic::Custom { command: "apply", args: ["lemma_foo"] } — the top-ranked premise from the mock. Also asserts GnnClient::health_status() deserializes the richer payload. No Julia installation required. https://claude.ai/code/session_01YPqu7gti4azBach6ZvpRFJ
1 parent 30acdc1 commit 7b90a65

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

tests/gnn_augment_integration.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
4+
//! Integration test: mock HTTP server proves the GNN wire format works and
5+
//! that the 5 key backends (rocq, lean, agda, isabelle, z3) consume
6+
//! `/gnn/rank` and prepend model-derived apply tactics.
7+
//!
8+
//! No Julia installation needed — the mock server is an in-process axum
9+
//! server bound to a random port. Run with:
10+
//!
11+
//! cargo test --test gnn_augment_integration
12+
13+
use axum::{
14+
extract::Json as ExtractJson,
15+
response::Json,
16+
routing::{get, post},
17+
Router,
18+
};
19+
use echidna::{
20+
core::{Goal, ProofState, Term},
21+
gnn::client::{GnnClient, GnnConfig},
22+
provers::{ProverConfig, ProverFactory, ProverKind},
23+
};
24+
use serde_json::{json, Value};
25+
use std::net::SocketAddr;
26+
use tokio::net::TcpListener;
27+
28+
// ─── helpers ────────────────────────────────────────────────────────────────
29+
30+
fn minimal_proof_state() -> ProofState {
31+
let goal = Goal {
32+
id: "g0".to_string(),
33+
target: Term::Const("forall n : nat, n + 0 = n".to_string()),
34+
hypotheses: vec![],
35+
};
36+
ProofState {
37+
goals: vec![goal],
38+
context: echidna::core::Context::default(),
39+
proof_script: vec![],
40+
metadata: std::collections::HashMap::new(),
41+
}
42+
}
43+
44+
/// Spawn a mock GNN server on a random port and return its base URL.
45+
///
46+
/// The server responds:
47+
/// GET /gnn/health → gnn_model_loaded: true, vocab_size: 42, model_path: "/fake/path"
48+
/// POST /gnn/rank → premises ["lemma_foo", "thm_bar", "ax_baz"], scores [0.9, 0.7, 0.5]
49+
async fn spawn_mock_gnn_server() -> String {
50+
let health_handler = get(|| async {
51+
Json(json!({
52+
"status": "ok",
53+
"gnn_model_loaded": true,
54+
"model_path": "/fake/path",
55+
"vocab_size": 42,
56+
"training_records_received": 0u64,
57+
"num_gnn_layers": 4,
58+
"service": "mock",
59+
"version": "test"
60+
}))
61+
});
62+
63+
let rank_handler = post(|_body: ExtractJson<Value>| async {
64+
Json(json!({
65+
"success": true,
66+
"scores": [0.9_f32, 0.7_f32, 0.5_f32],
67+
"premise_names": ["lemma_foo", "thm_bar", "ax_baz"],
68+
"inference_time_ms": 1.0_f32
69+
}))
70+
});
71+
72+
let app = Router::new()
73+
.route("/gnn/health", health_handler)
74+
.route("/gnn/rank", rank_handler);
75+
76+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
77+
let addr: SocketAddr = listener.local_addr().unwrap();
78+
let base_url = format!("http://127.0.0.1:{}", addr.port());
79+
80+
tokio::spawn(async move {
81+
axum::serve(listener, app).await.unwrap();
82+
});
83+
84+
// Give the server a moment to accept connections.
85+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
86+
base_url
87+
}
88+
89+
fn gnn_config(base_url: &str) -> ProverConfig {
90+
ProverConfig {
91+
gnn_api_url: Some(base_url.to_string()),
92+
neural_enabled: true,
93+
..ProverConfig::default()
94+
}
95+
}
96+
97+
// ─── test: GnnClient::health_status() ───────────────────────────────────────
98+
99+
#[tokio::test]
100+
async fn test_health_status_richer_payload() {
101+
let base_url = spawn_mock_gnn_server().await;
102+
let client = GnnClient::with_config(GnnConfig {
103+
api_url: base_url,
104+
timeout_ms: 5000,
105+
..GnnConfig::default()
106+
});
107+
108+
let health = client.health_status().await.expect("health_status should succeed");
109+
110+
assert!(health.gnn_model_loaded, "mock reports model loaded");
111+
assert_eq!(health.model_path.as_deref(), Some("/fake/path"));
112+
assert_eq!(health.vocab_size, 42);
113+
assert_eq!(health.training_records_received, 0);
114+
assert_eq!(health.num_gnn_layers, 4);
115+
assert_eq!(health.service, "mock");
116+
assert_eq!(health.version, "test");
117+
}
118+
119+
// ─── helper: assert top tactic from suggest_tactics ─────────────────────────
120+
121+
async fn assert_top_tactic_is_apply(
122+
kind: ProverKind,
123+
prover_name: &str,
124+
base_url: &str,
125+
) {
126+
let backend = ProverFactory::create(kind, gnn_config(base_url))
127+
.unwrap_or_else(|e| panic!("Failed to create {:?} backend: {}", kind, e));
128+
129+
let state = minimal_proof_state();
130+
let tactics = backend
131+
.suggest_tactics(&state, 10)
132+
.await
133+
.unwrap_or_else(|e| panic!("{} suggest_tactics failed: {}", prover_name, e));
134+
135+
assert!(
136+
!tactics.is_empty(),
137+
"{}: suggest_tactics returned empty list",
138+
prover_name
139+
);
140+
141+
let first = &tactics[0];
142+
match first {
143+
echidna::core::Tactic::Custom { prover, command, args } => {
144+
assert_eq!(
145+
prover.as_str(),
146+
prover_name,
147+
"{}: expected prover name in first tactic",
148+
prover_name
149+
);
150+
assert_eq!(
151+
command.as_str(),
152+
"apply",
153+
"{}: expected 'apply' command in first tactic",
154+
prover_name
155+
);
156+
assert_eq!(
157+
args,
158+
&vec!["lemma_foo".to_string()],
159+
"{}: expected top-ranked premise as argument",
160+
prover_name
161+
);
162+
},
163+
other => {
164+
panic!(
165+
"{}: expected Tactic::Custom(apply lemma_foo) as first tactic, got {:?}",
166+
prover_name, other
167+
);
168+
},
169+
}
170+
}
171+
172+
// ─── per-backend tests ───────────────────────────────────────────────────────
173+
174+
#[tokio::test]
175+
async fn test_rocq_gnn_wires_top_premise() {
176+
let base_url = spawn_mock_gnn_server().await;
177+
assert_top_tactic_is_apply(ProverKind::Rocq, "rocq", &base_url).await;
178+
}
179+
180+
#[tokio::test]
181+
async fn test_lean_gnn_wires_top_premise() {
182+
let base_url = spawn_mock_gnn_server().await;
183+
assert_top_tactic_is_apply(ProverKind::Lean, "lean", &base_url).await;
184+
}
185+
186+
#[tokio::test]
187+
async fn test_agda_gnn_wires_top_premise() {
188+
let base_url = spawn_mock_gnn_server().await;
189+
assert_top_tactic_is_apply(ProverKind::Agda, "agda", &base_url).await;
190+
}
191+
192+
#[tokio::test]
193+
async fn test_isabelle_gnn_wires_top_premise() {
194+
let base_url = spawn_mock_gnn_server().await;
195+
assert_top_tactic_is_apply(ProverKind::Isabelle, "isabelle", &base_url).await;
196+
}
197+
198+
#[tokio::test]
199+
async fn test_z3_gnn_wires_top_premise() {
200+
let base_url = spawn_mock_gnn_server().await;
201+
assert_top_tactic_is_apply(ProverKind::Z3, "z3", &base_url).await;
202+
}

0 commit comments

Comments
 (0)