1- //! Cross-version upgrade: prove the CURRENT binary handles a GENUINE old-format
2- //! (internal schema v3) graph minted by omnigraph 0.7.2 — not a v4 -shaped graph
3- //! with a rewound stamp. Two things the stamp-rewind stand-in
1+ //! Cross-version upgrade: prove the CURRENT binary handles GENUINE old-format
2+ //! graphs minted by released binaries — not a current -shaped graph with a
3+ //! rewound stamp. Two things the stamp-rewind stand-in
44//! (`sub_current_graph_is_refused_then_rebuilt_via_export_import`) cannot prove:
55//!
66//! 1. the open-refusal fires on the REAL on-disk v3 shape (lineage in
99//! 2. the documented `export → init → load` rebuild round-trips the data,
1010//! including a `Vector` column, off a genuine v3 export.
1111//!
12- //! Gated: requires `OMNIGRAPH_OLD_BIN` (an absolute path to a 0.7.2 `omnigraph`
13- //! binary). Skips gracefully when unset — the same convention as the S3 and
14- //! system-e2e gates — so the default `cargo test --workspace` stays green
15- //! without it. CI sets it in the `crossversion_upgrade` job (see
16- //! `docs/dev/testing.md`).
12+ //! The v3 case uses `OMNIGRAPH_OLD_BIN` (0.7.2). The immediate-predecessor v4
13+ //! case uses `OMNIGRAPH_PREVIOUS_BIN` (0.8.1) and also proves that binary
14+ //! refuses a v5 graph. Each case skips only when its variable is unset; a set
15+ //! but invalid path fails loudly.
1716
1817mod support;
1918
@@ -39,6 +38,17 @@ fn old_bin() -> Option<PathBuf> {
3938 Some ( path)
4039}
4140
41+ fn previous_bin ( ) -> Option < PathBuf > {
42+ let path = PathBuf :: from ( std:: env:: var_os ( "OMNIGRAPH_PREVIOUS_BIN" ) ?) ;
43+ assert ! (
44+ path. exists( ) ,
45+ "OMNIGRAPH_PREVIOUS_BIN is set but does not exist: {} \
46+ (unset it to skip, or point it at a real 0.8.1 omnigraph binary)",
47+ path. display( ) ,
48+ ) ;
49+ Some ( path)
50+ }
51+
4252/// Run the OLD (0.7.2) binary hermetically (no developer `~/.omnigraph`).
4353fn run_old ( bin : & Path , args : & [ & str ] ) -> std:: process:: Output {
4454 Command :: new ( bin)
@@ -65,6 +75,15 @@ fn nonblank_lines(bytes: &[u8]) -> usize {
6575 . count ( )
6676}
6777
78+ fn exported_row_with_slug ( bytes : & [ u8 ] , slug : & str ) -> serde_json:: Value {
79+ String :: from_utf8_lossy ( bytes)
80+ . lines ( )
81+ . filter ( |line| !line. trim ( ) . is_empty ( ) )
82+ . map ( |line| serde_json:: from_str :: < serde_json:: Value > ( line) . expect ( "valid export JSONL" ) )
83+ . find ( |row| row[ "data" ] [ "slug" ] . as_str ( ) == Some ( slug) )
84+ . unwrap_or_else ( || panic ! ( "export must contain slug '{slug}'" ) )
85+ }
86+
6887#[ test]
6988fn current_binary_refuses_and_rebuilds_a_genuine_v3_graph ( ) {
7089 let Some ( old) = old_bin ( ) else {
@@ -91,7 +110,14 @@ fn current_binary_refuses_and_rebuilds_a_genuine_v3_graph() {
91110 "load" ,
92111 & run_old (
93112 & old,
94- & [ "load" , "--mode" , "overwrite" , "--data" , data. to_str ( ) . unwrap ( ) , og] ,
113+ & [
114+ "load" ,
115+ "--mode" ,
116+ "overwrite" ,
117+ "--data" ,
118+ data. to_str ( ) . unwrap ( ) ,
119+ og,
120+ ] ,
95121 ) ,
96122 ) ;
97123
@@ -124,8 +150,14 @@ fn current_binary_refuses_and_rebuilds_a_genuine_v3_graph() {
124150 ) ;
125151
126152 // 4. The CURRENT binary rebuilds: fresh init + load the v3 export.
127- let new_graph = temp. path ( ) . join ( "new-v4.omni" ) ;
128- output_success ( cli ( ) . arg ( "init" ) . arg ( "--schema" ) . arg ( & schema) . arg ( & new_graph) ) ;
153+ let new_graph = temp. path ( ) . join ( "new-current.omni" ) ;
154+ output_success (
155+ cli ( )
156+ . arg ( "init" )
157+ . arg ( "--schema" )
158+ . arg ( & schema)
159+ . arg ( & new_graph) ,
160+ ) ;
129161 output_success (
130162 cli ( )
131163 . arg ( "load" )
@@ -141,7 +173,7 @@ fn current_binary_refuses_and_rebuilds_a_genuine_v3_graph() {
141173 assert_eq ! (
142174 nonblank_lines( & export. stdout) ,
143175 nonblank_lines( & reexport. stdout) ,
144- "row count must round-trip v3 → v4 " ,
176+ "row count must round-trip v3 → current " ,
145177 ) ;
146178 let rebuilt = String :: from_utf8_lossy ( & reexport. stdout ) ;
147179 assert ! (
@@ -153,3 +185,97 @@ fn current_binary_refuses_and_rebuilds_a_genuine_v3_graph() {
153185 "the rebuilt graph must preserve node data, got: {rebuilt}" ,
154186 ) ;
155187}
188+
189+ #[ test]
190+ fn v5_refuses_and_rebuilds_genuine_v4_and_v4_refuses_v5 ( ) {
191+ let Some ( previous) = previous_bin ( ) else {
192+ eprintln ! (
193+ "skipping immediate-predecessor upgrade test: OMNIGRAPH_PREVIOUS_BIN is not set to a 0.8.1 binary"
194+ ) ;
195+ return ;
196+ } ;
197+
198+ let temp = tempdir ( ) . unwrap ( ) ;
199+ let old_graph = temp. path ( ) . join ( "old-v4.omni" ) ;
200+ let schema = fixture ( "search.pg" ) ;
201+ let data = fixture ( "search.jsonl" ) ;
202+ let old_uri = old_graph. to_str ( ) . unwrap ( ) ;
203+
204+ assert_ok (
205+ "v4 init" ,
206+ & run_old (
207+ & previous,
208+ & [ "init" , "--schema" , schema. to_str ( ) . unwrap ( ) , old_uri] ,
209+ ) ,
210+ ) ;
211+ assert_ok (
212+ "v4 load" ,
213+ & run_old (
214+ & previous,
215+ & [
216+ "load" ,
217+ "--mode" ,
218+ "overwrite" ,
219+ "--data" ,
220+ data. to_str ( ) . unwrap ( ) ,
221+ old_uri,
222+ ] ,
223+ ) ,
224+ ) ;
225+ assert ! (
226+ !old_graph. join( "_graph_commits.lance" ) . exists( ) ,
227+ "a genuine v4 graph keeps graph lineage inside __manifest" ,
228+ ) ;
229+
230+ let export = run_old ( & previous, & [ "export" , old_uri] ) ;
231+ assert_ok ( "v4 export" , & export) ;
232+ let jsonl = temp. path ( ) . join ( "v4.jsonl" ) ;
233+ std:: fs:: write ( & jsonl, & export. stdout ) . unwrap ( ) ;
234+
235+ let refusal = output_failure ( cli ( ) . arg ( "snapshot" ) . arg ( & old_graph) ) ;
236+ let stderr = String :: from_utf8_lossy ( & refusal. stderr ) ;
237+ assert ! ( stderr. contains( "0.8.x" ) , "got: {stderr}" ) ;
238+ assert ! ( stderr. contains( "export" ) , "got: {stderr}" ) ;
239+
240+ let new_graph = temp. path ( ) . join ( "new-v5.omni" ) ;
241+ output_success (
242+ cli ( )
243+ . arg ( "init" )
244+ . arg ( "--schema" )
245+ . arg ( & schema)
246+ . arg ( & new_graph) ,
247+ ) ;
248+ output_success (
249+ cli ( )
250+ . arg ( "load" )
251+ . arg ( "--mode" )
252+ . arg ( "overwrite" )
253+ . arg ( "--data" )
254+ . arg ( & jsonl)
255+ . arg ( & new_graph) ,
256+ ) ;
257+ let reexport = output_success ( cli ( ) . arg ( "export" ) . arg ( & new_graph) ) ;
258+ assert_eq ! (
259+ nonblank_lines( & export. stdout) ,
260+ nonblank_lines( & reexport. stdout)
261+ ) ;
262+ let original_ml_intro = exported_row_with_slug ( & export. stdout , "ml-intro" ) ;
263+ let rebuilt_ml_intro = exported_row_with_slug ( & reexport. stdout , "ml-intro" ) ;
264+ assert_eq ! (
265+ rebuilt_ml_intro[ "data" ] [ "embedding" ] , original_ml_intro[ "data" ] [ "embedding" ] ,
266+ "v4 → v5 rebuild must preserve vector values, not merely row count"
267+ ) ;
268+
269+ let reverse = run_old ( & previous, & [ "snapshot" , new_graph. to_str ( ) . unwrap ( ) ] ) ;
270+ assert ! (
271+ !reverse. status. success( ) ,
272+ "a v4 binary must refuse a genuine v5 graph"
273+ ) ;
274+ let reverse_stderr = String :: from_utf8_lossy ( & reverse. stderr ) ;
275+ assert ! (
276+ reverse_stderr. contains( "upgrade omnigraph" )
277+ || reverse_stderr. contains( "newer" )
278+ || reverse_stderr. contains( "expects v4" ) ,
279+ "unexpected reverse-refusal message: {reverse_stderr}" ,
280+ ) ;
281+ }
0 commit comments