1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//! #383 — the `fraiseql watch` loop against a real filesystem.
//!
//! `watch::run` shipped with only pure URL-formatting unit tests; nothing
//! executed the loop. This suite drives it end to end with real file events:
//!
//! - the initial compile writes the artifact;
//! - a BROKEN save reports the failure and leaves the previous good artifact in place (the
//! acceptance criterion watch exists for — a typo must not destroy the served schema);
//! - a subsequent fixed save recompiles, so one bad save never wedges the loop.
//!
//! No database and no server: `reload_url` is `None`, which skips the live
//! reload leg (covered by its own unit tests + the admin-route tests in
//! fraiseql-server).
#![allow(clippy::unwrap_used, clippy::panic)] // Reason: test code — panics are acceptable
use std::{path::Path, time::Duration};
use serde_json::json;
/// A minimal valid authoring schema whose one type is named `type_name`.
fn valid_schema(type_name: &str) -> String {
json!({
"types": [{
"name": type_name,
"sql_source": "v_thing",
"fields": [{"name": "id", "type": "ID", "nullable": false}]
}],
"queries": [{
"name": "things",
"return_type": type_name,
"returns_list": true,
"sql_source": "v_thing"
}]
})
.to_string()
}
/// Poll until `predicate` holds on the output file, up to ~10s.
async fn wait_for(out: &Path, predicate: impl Fn(&str) -> bool) -> String {
for _ in 0..200 {
if let Ok(content) = std::fs::read_to_string(out) {
if predicate(&content) {
return content;
}
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
panic!("output file never reached the expected state: {}", out.display());
}
#[tokio::test(flavor = "multi_thread")]
async fn watch_recompiles_and_a_broken_save_preserves_the_artifact() {
let dir = tempfile::TempDir::new().unwrap();
let input = dir.path().join("schema.json");
let output = dir.path().join("schema.compiled.json");
std::fs::write(&input, valid_schema("Thing")).unwrap();
let input_s = input.to_str().unwrap().to_string();
let output_s = output.to_str().unwrap().to_string();
let watcher = tokio::spawn(async move {
// No reload URL and no database: pure compile-on-change loop.
fraiseql_cli::commands::watch::run(&input_s, &output_s, None, None, None).await
});
// 1. Initial compile writes the artifact.
let first = wait_for(&output, |c| c.contains("\"Thing\"")).await;
// 2. A broken save must NOT clobber the good artifact. Wait past the debounce + compile window
// before asserting the file is unchanged.
std::fs::write(&input, "{ this is not json").unwrap();
tokio::time::sleep(Duration::from_millis(1500)).await;
let after_broken = std::fs::read_to_string(&output).unwrap();
assert_eq!(
after_broken, first,
"a failed compile must leave the previous good artifact untouched"
);
// 3. A fixed save recompiles — one bad save never wedges the loop.
std::fs::write(&input, valid_schema("Widget")).unwrap();
wait_for(&output, |c| c.contains("\"Widget\"")).await;
watcher.abort();
}