From 50cc916d18c0cf6e5bf920d93cbfc83a8d827dec Mon Sep 17 00:00:00 2001 From: clover caruso Date: Tue, 8 Sep 2026 20:30:37 -0700 Subject: [PATCH] fix: refresh automatic titles when moving outlines Update page and metadata title caches in the same revision as outline coordinates. Preserve explicit titles, content identities and historical revisions. Verified against three native OneNote controls and independent cold reopens, interruption and offline lost-reply tests, 3,692 ASan executions, full core/offline/Python suites, Clippy and both iOS targets. Assisted-by: gpt-6-astra --- corpus/outline-edit/automatic-title/README.md | 29 ++++ .../before/notebook/TitleControl.one | Bin 0 -> 21400 bytes .../automatic-title/before/read/hierarchy.xml | 2 + .../automatic-title/before/read/page-001.xml | 2 + .../automatic-title/before/read/page-003.xml | 2 + .../automatic-title/before/read/page-004.xml | 2 + .../candidate/TitleControl.one | Bin 0 -> 23640 bytes .../cold/notebook/TitleControl.one | Bin 0 -> 23640 bytes .../automatic-title/cold/read/hierarchy.xml | 2 + .../automatic-title/cold/read/page-000.xml | 2 + .../automatic-title/cold/read/page-001.xml | 2 + .../automatic-title/cold/read/page-002.xml | 2 + .../outline-edit/automatic-title/fixture.json | 1 + .../native/cold/notebook/TitleControl.one | 1 + .../native/cold/read/hierarchy.xml | 2 + .../native/cold/read/page-002.xml | 2 + .../native/cold/read/page-003.xml | 2 + .../native/cold/read/page-004.xml | 2 + .../native/notebook/TitleControl.one | Bin 0 -> 29336 bytes .../automatic-title/native/read/hierarchy.xml | 2 + .../automatic-title/native/read/page-000.xml | 2 + .../automatic-title/native/read/page-001.xml | 2 + .../automatic-title/native/read/page-003.xml | 2 + .../automatic-title/provenance.json | 116 +++++++++++++ crates/onestore-offline/tests/sync/outline.rs | 75 ++++++++ crates/onestore/src/outline.rs | 17 +- crates/onestore/tests/outline.rs | 163 ++++++++++++++++++ fuzz/fuzz_targets/paragraph.rs | 10 ++ tools/native/outline-title.ps1 | 48 ++++++ tools/test_outline_edit.py | 33 ++++ 30 files changed, 520 insertions(+), 5 deletions(-) create mode 100644 corpus/outline-edit/automatic-title/README.md create mode 100644 corpus/outline-edit/automatic-title/before/notebook/TitleControl.one create mode 100644 corpus/outline-edit/automatic-title/before/read/hierarchy.xml create mode 100644 corpus/outline-edit/automatic-title/before/read/page-001.xml create mode 100644 corpus/outline-edit/automatic-title/before/read/page-003.xml create mode 100644 corpus/outline-edit/automatic-title/before/read/page-004.xml create mode 100644 corpus/outline-edit/automatic-title/candidate/TitleControl.one create mode 100644 corpus/outline-edit/automatic-title/cold/notebook/TitleControl.one create mode 100644 corpus/outline-edit/automatic-title/cold/read/hierarchy.xml create mode 100644 corpus/outline-edit/automatic-title/cold/read/page-000.xml create mode 100644 corpus/outline-edit/automatic-title/cold/read/page-001.xml create mode 100644 corpus/outline-edit/automatic-title/cold/read/page-002.xml create mode 100644 corpus/outline-edit/automatic-title/fixture.json create mode 120000 corpus/outline-edit/automatic-title/native/cold/notebook/TitleControl.one create mode 100644 corpus/outline-edit/automatic-title/native/cold/read/hierarchy.xml create mode 100644 corpus/outline-edit/automatic-title/native/cold/read/page-002.xml create mode 100644 corpus/outline-edit/automatic-title/native/cold/read/page-003.xml create mode 100644 corpus/outline-edit/automatic-title/native/cold/read/page-004.xml create mode 100644 corpus/outline-edit/automatic-title/native/notebook/TitleControl.one create mode 100644 corpus/outline-edit/automatic-title/native/read/hierarchy.xml create mode 100644 corpus/outline-edit/automatic-title/native/read/page-000.xml create mode 100644 corpus/outline-edit/automatic-title/native/read/page-001.xml create mode 100644 corpus/outline-edit/automatic-title/native/read/page-003.xml create mode 100644 corpus/outline-edit/automatic-title/provenance.json create mode 100644 tools/native/outline-title.ps1 diff --git a/corpus/outline-edit/automatic-title/README.md b/corpus/outline-edit/automatic-title/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d12b244ebdd2906f209576760c541ff2fe3ebf07 --- /dev/null +++ b/corpus/outline-edit/automatic-title/README.md @@ -0,0 +1,29 @@ +# Automatic titles after outline movement + +Three native pages distinguish vertical ordering, horizontal ordering and an +explicit title. Moving the second outline ahead of the first changes the two +automatic titles to `Second 🦋 é`; the explicit title remains unchanged. + +`before` is the native source. `native` records the corresponding COM moves and +an independent cold reopen. `candidate` is the Rust edit; `cold` is its independent +OneNote reopen. Cold captures preserve exact active graphs. Rust movement retains +all text identities, content, formatting and history. Identical artifacts link +to one copy; `provenance.json` records the native runs and clone teardown. + +The Rust regression originally left both automatic-title fields unchanged. +Position edits now update the existing page and metadata caches within the same +revision as the coordinates. The offline regression additionally changes the +remote title-producing text, loses the publication reply and reopens the cache +before confirmation and a dependent edit. + +Run `tools.test_outline_edit.OutlineEditTest.test_automatic_titles_follow_native_and_rust_outline_movement` +through Python unittest after building the workspace examples with all features. +The Rust test `native_automatic_and_explicit_titles_follow_outline_movement` can +export a new candidate directory through `ONESTORE_OUTLINE_TITLE_OUTPUT`. + +To regenerate the native controls, copy `fixture.json` and the synthetic section +from `corpus/native-delete/deletion-05/before` into a new input notebook directory. +Capture it with `tools/native_runner.py --author tools/native/pages.ps1 +--collect-notebook`. Capture that result's notebook with +`--author tools/native/outline-title.ps1 --collect-notebook`, then independently +cold-open both the native result and the Rust candidate without an author script. diff --git a/corpus/outline-edit/automatic-title/before/notebook/TitleControl.one b/corpus/outline-edit/automatic-title/before/notebook/TitleControl.one new file mode 100644 index 0000000000000000000000000000000000000000..b29efe83f94312361f3e77e6754f11adf0c9907d GIT binary patch literal 21400 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 literal 0 HcmV?d00001 diff --git a/corpus/outline-edit/automatic-title/before/read/hierarchy.xml b/corpus/outline-edit/automatic-title/before/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..fc93eb0778808de69cf3d5307bfb9dd450894e13 --- /dev/null +++ b/corpus/outline-edit/automatic-title/before/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/before/read/page-001.xml b/corpus/outline-edit/automatic-title/before/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..14027d9a74ab9ee14a9c32ab2f5454b3e88f4915 --- /dev/null +++ b/corpus/outline-edit/automatic-title/before/read/page-001.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/before/read/page-003.xml b/corpus/outline-edit/automatic-title/before/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..677b0132259a3cf8bed5426f06a2f598846f481c --- /dev/null +++ b/corpus/outline-edit/automatic-title/before/read/page-003.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/before/read/page-004.xml b/corpus/outline-edit/automatic-title/before/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..87aaadbf74c8d772aeecff926d8ca33af9d47d26 --- /dev/null +++ b/corpus/outline-edit/automatic-title/before/read/page-004.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/candidate/TitleControl.one b/corpus/outline-edit/automatic-title/candidate/TitleControl.one new file mode 100644 index 0000000000000000000000000000000000000000..884cec54059c0cbacd87a91f7be00f8941d18f1a GIT binary patch literal 23640 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 Ch7lV8 literal 0 HcmV?d00001 diff --git a/corpus/outline-edit/automatic-title/cold/notebook/TitleControl.one b/corpus/outline-edit/automatic-title/cold/notebook/TitleControl.one new file mode 100644 index 0000000000000000000000000000000000000000..7c18e9b97196cc84c8623e8e1bcd0950c92cadb8 GIT binary patch literal 23640 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 + diff --git a/corpus/outline-edit/automatic-title/cold/read/page-000.xml b/corpus/outline-edit/automatic-title/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b40442d60db1cf787076775f245b6cd52101fa7 --- /dev/null +++ b/corpus/outline-edit/automatic-title/cold/read/page-000.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/cold/read/page-001.xml b/corpus/outline-edit/automatic-title/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..dd27ba5829a56dd160c7cb5d07b8f2dfb2f54cc0 --- /dev/null +++ b/corpus/outline-edit/automatic-title/cold/read/page-001.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/cold/read/page-002.xml b/corpus/outline-edit/automatic-title/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..543bc61d4760baac2e20fa3d42ae46d6382c2502 --- /dev/null +++ b/corpus/outline-edit/automatic-title/cold/read/page-002.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/fixture.json b/corpus/outline-edit/automatic-title/fixture.json new file mode 100644 index 0000000000000000000000000000000000000000..da384c090ad4c9543b224f1eeb40de7b7aed3583 --- /dev/null +++ b/corpus/outline-edit/automatic-title/fixture.json @@ -0,0 +1 @@ +{"sections": ["TitleControl.one"], "pages": [{"section": "TitleControl.one", "xml": "Vertical firstSecond \ud83e\udd8b e\u0301"}, {"section": "TitleControl.one", "xml": "Horizontal firstSecond \ud83e\udd8b e\u0301"}, {"section": "TitleControl.one", "xml": "Explicit titleExplicit titleSecond \ud83e\udd8b e\u0301"}]} \ No newline at end of file diff --git a/corpus/outline-edit/automatic-title/native/cold/notebook/TitleControl.one b/corpus/outline-edit/automatic-title/native/cold/notebook/TitleControl.one new file mode 120000 index 0000000000000000000000000000000000000000..c2ccdfd7698b3d0ff28d305952cd0b9aaaf7fcbb --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/cold/notebook/TitleControl.one @@ -0,0 +1 @@ +../../notebook/TitleControl.one \ No newline at end of file diff --git a/corpus/outline-edit/automatic-title/native/cold/read/hierarchy.xml b/corpus/outline-edit/automatic-title/native/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..db0e819665e073c38d325b8cc121e9292e4f94d7 --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/native/cold/read/page-002.xml b/corpus/outline-edit/automatic-title/native/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..8219b1b55f4a0250899b3b251476fe2b986e710f --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/cold/read/page-002.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/native/cold/read/page-003.xml b/corpus/outline-edit/automatic-title/native/cold/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..d06176fd09b2314ce79c511ea036d80511d223cd --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/cold/read/page-003.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/native/cold/read/page-004.xml b/corpus/outline-edit/automatic-title/native/cold/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb9eb371e4ebb4b29ac03593ac2dfb7c53d067c7 --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/cold/read/page-004.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/native/notebook/TitleControl.one b/corpus/outline-edit/automatic-title/native/notebook/TitleControl.one new file mode 100644 index 0000000000000000000000000000000000000000..d7c68995a31f0bceeb86c4587d93dad51d811cd9 GIT binary patch literal 29336 zcmeHP2Y3|K_Mgr6&2C5_5lA*6garu%h#2WFY(hd$fFPlW2m)#l1VYq+A`2#hCNu>E zo{wIniWqzdQZyhcR zk3?6rgM{0enfR5`^b1A$j4u09U!AOv^mjh{L{E2C+PT#~|IlZ8+p&`?A6t8*;8Cf@ zjsFUw=U0%!jU{{-9|!Jz4s5${`QrPd(ia7~hhiEmOm}GMncDN4UfP7 z(x3woJj-Ts^f>cVR>1zk_G4S^?zXH}Yz(q|$;Sa72W~wFM2RWN%nlnK9#_-Ku_qd;ABSjBnf&_q_axz0GV+(SLU z{kb8TZ{S|rOS4V>%cxKfJ&@S*lXd;$@Zs4N`czYZ5|EXVt?yUr9m(PLp z{H?hTy2o6>yzwVMJHuaVc9br@bO$)VTv9 zAvhkHIRk`zIYF$EeW4k3dl3bTTW5s6-qe+{>|Ea3j%iz?v68SWsO4D*j|T05E*B_V z_2OHHa?fU(9(?zBvlgqH7?Gz&wMb!6px~kcEX^F`{zDrfmmh?2X@zm&LU3N35WLwg z1lJ}MVCsg|?&4@kLv^%5|8OBD)-Kow+J!2>V#n!QtI*Zf>DP8qvzgIp*(tH}&eYGd zIFF0nvldu=+U(Wqw|^~vs_pcg)Y&Nu|9y7x(Qc9Ye@v9>Q#F^BSYsowP(xaQ_aA8& z9O%E=+KjyuhS?I{oR(3k(~_g@zW6GP^$+PHCe}awRqsFk)4zLta&&aUyKSm&eJN<& zkig6HWc~B`gFO$afYz7V9NgN6n#?mGQt*PoL&ckZh-Mvol_Jf6C8 zHUJOmm1;Ne^cV!Cre2%+&ZoS22mb2WKlE&eSKr>X;e#FL_M*>4Ny5a_8WUJ&A_3#4 z0pb`TsAD4`Gz{?{jR_2vN_?&{f$JiI&mkAsjU<~ta{loCc4~C~RQVxHY0Y{fj#cvg z_&DI>z`e`?xj5tUgRg(+h)B>sl=~C*1?V3IehyaVe*N*rAL!pm&_C4pgFHz0Eq$h4 zjvao*`TDXWANQGDJsUhLb%}btotZ&K+RLW7)Ki*s) z7X%$+OuL}r&xV;3r%m0Jr`zAL7j4u`$56|IBtkydKmP9Ziv9lvk9WSRt2BC1jSq9Y zQ^;ipH6Zk;iC;=G`9XIspeT_nW z5z+pXvI~7u4Yyqx>`lhLuFAujOeiw z1QUd~X+t&aBD%s{Fux@MB_~}8D6@0I>{(TTO%Q~wPS4|>8K*XG*u3cjGqW=K_ZuAf zR7U2AEcC~r_&BRmj`wk5tkO7P)0bnMREv`5aXM+_PVgVTh$sRZLCAG}J`pZBj@g9? zlL$Hz4^s$I2|5vI2>b}<5X>c*M=+n@`EbGhC&8O`!Bl~sT_8aVy2p6Hbnt^ZB$@z% z6VQ3$(gZAkSSyd&kOy8!E}#ZDV+fI_kR9)JiG_#G`CPDgXSDc zMG8-`S|bR@lFK0cL30CoeG$>a_W;-!CD#WPZTi(4pXb`tYJB(LsBzEq?2b|J9fVsW zm@n{&V4e71Unk!Z?*xJg1lDk2!g#u3?d&94_&kav-k5dp;UH<@RB6(z;lq)gQ_zom zeZZF_&I7gevDV#39Pu7)0>n8))r8W~x2|ORFu+HjA2?z_zmYMKT?Y=!8aU+1ftiu9 zO=2P&#x`sOnvvvmV{g3~x*^-(kF#B$oAR}ebq>10XYc?#2e0D^k9`Szz}#bNzgE4i z^jEPa-0PnCqrfNFVi=~lMzP-&P@DX@Vb4=MGd^glBOf>Bj)~Fi>3N~u;?Ihja=}j) z;Bl;fQ5}pEb*B=XqcIlIRX$!2#Ks%h>3908I^HQx%3)*1730PphFao2A586H@8s?N z242!W3wlul$x6?aC-9i9a3}nFl$@XfGtf`K{{2#%5XUeqlYU(I!{sm54$`N+g^3+? z0mjygn?(PsAnBV#<8uWsUi}j2Ub>)y=+%2;H-@Ynux3-s_^H+HKtHQ9jNMstONQ<8 z(pbMI8W+r;{4CJJb%9s5_iB_druT(^e`v1Ltx?PppvUSAlb(-Wk=>%pDML}Jzwq0S zXMrAVG7hqJ_;bqnIh*nm`mVoFvpu6^pT+i0THeW@iVhyQ&}&uh%&1nmBALEl9|wFK zC?f~d^h;`j_MKj&Q|U-<=?M%3R@!SUBqRMvR@NuKVFQ9#0wd+wD9=n+69M#rVImb| z2na$erU7IKPm)HELHAs%7mLtu5y4IL2ko{q_3rq#pC-Iyf0mLWE2%}{n@2q8KsS8FRk`dP0dmS{~7!?x>)Ia(8*P5?t}%VClP zNtO| zP?WYY(yF0*vrhe#`Zd}drZtPk7vC!fYW-n}icO$I53LPbOYHUiwuROX8^Hhi<{7Ic zi7uxrT5E(?L!zsi)>54i5?y2j&?wz>Da`@&T1jh#n3Be7)f#IQYg5o#u`!5k`r<^y z3SYVt9PqaOv67Gw*HFe|u`eL5v0v|CW$xD>Z~TE?f`qt+8h?-n9b@Rx#VxWAjc|T6 ztV+Z)HPf4ecWO!08lY`m);ki~7an-yEn|n}Bj+qLKdxuqGF5HsR>U>_SQoN>#pzA0 zXCy^!es6TrRqqta7073rxJN?7bN!JpE;p9!2=3G5UP;NX0(}Sx`nTF}<@&cLJ$vr% z#A|h`%x=3#*C1ly-aQLIId>)O+@81wctjj!7925xXaWdMz|F)p zpxCsf2pYT?qgW&@OFgzh$8W@exgrI~RWNdQ{P$-R7bn$nPHwno#;L`N>Wh^f<^T z`N@tN%-B37KiM8b1#nL?x(W>@JVwco+h{OhK8Qw_T#KPj-RYSY?msephTFr?_ozDa zhn+Pz)L5jdq3!>D{2BPXl)RDq!<2b)f0!~)=?_!W-ase(VQRW3@|4d5IOwX4x>cOM zYgd@I)>-?dK2y(a0^U>mA}7A{cf9i9Thsa!CL4oCKi)6w?*;o%DQBn5AI5jq`u%=6 z7Dm&7dBHS6^v?(Ztz{85pHqxj;Y)Xx1K#XC?2nk*+j2w?JN;Mpu%iGRX1st$hZz-M z4znAg#MlB?83G^)r2TA>!3;4h?PoI#0g&sGV+sy4LU?wVO*EKADb5{c;|%^#X~ZVb zYnCCvpAeG6th*uHN(jke*1=%K&Lz>6WH4g8lIUt?uoRC+ki!fHjYyZ$VFqrI_YfNa zV}?%6dsyFKg}N+t`k=uGTSV$K%wWYf;`68x6MU(>9PqaO;T1-LKTP?a1FaC|4|~Yu zU}f&tA8-7@i8Unn!_@eLJm?s88(ho`ta8b@q$uRzq%pPg!8^4?Di|gB!+M$=Bf5lb zHzv+c`Dn=6iwm-=eS=D>+SaY`haEOudbHiTL9LcF4n5TT@lmgOr%IPw*4jG$M4cU561LRXG_vq~TRSMnww`WM_`|xH z?iqg=4Z&qD7T?F&3$+-3lpsa61N`Dg0tBF1(MB0Mwrq*{~|J_I$tB7%zb2%t>`Uq7dLtIkV< zKd|Pp9%xY#J-5cE$eLqKdiEDWx+DwPbf#)Bc^><-lR}Z~Y&AbI1f8?T&OxvB%&nU| zW9hZsX^T5Wfl}@(D&IO}gs=$&5f0>x+WIH|ml*V5cDb`{BvR}_nI-j=i zpRLyC9a^#=_fRIjw z=Dn+E4{|)!tT;c`&a5~;<}}OBk8LB{ijEayA~54ir#uXX0wo^ZXlRxl-H>ZL^!=(% zZYHSs%dlcWH#E3TW``P!R5iGz5BF*Wf0>eJa(|gJPwp>M<|+MUYT6y>guhHpH$|TE zIROVtkm4HHDmc zGJhFK7?#2YTCMy=Oq&U($*<`9-tV)i_AEt#w2E7FWJBIDz8s+;Sz5`^$ z8eh5t98g>TM245RKcunew{W4um*$;D3>9`*6kXw$hrM%q&QXNL+#Z|9J0EB`*gMPc z9&sqqiQSgz&LzA@ELv^g9|EM%`w^?~Y_Fqj>HTKdCx}dx>_6h(8=y9^R7TE%r5-bp- zQ3hhRk+DFp9pc%Ga!C0CBE;c)7Aq!`B<$X`D9%2=V^N%rc3EU+vWwdne{#D`#lEOc zG*eGZzZhYWiGH6rbk7{$#m9krn*+B)zbNf_LdZw>%i(vxC&KPW{i5{ed7%ekw`KZ8 z3FmpCUx?rDz!@LtW)-QbXZ-?g!$&9jK%rkigy5u*_c#UWm0iD}rd{wCS+4uX<=<>j zexKzVlJkr+Svw~|kD$jOD9@mAJ)%zAcdCsanV0x+W=!h112u5ulF8#8{x<25h+A)v z&cmqb4IBNfm?VQ$)R?c3&!iq@pi?Kb*cX{x*E4 zPdw)0(-#=ad_O)8-0K`r(-$De;h{rp6td{n$3<_Kk3f7HkXNOTj~A2ECB(-iKLC&0 zmrG6)nYTkui=)5&zg&LSaCZgysTQJ<%THyDiQC)FCp~_!pI|4l*ae@Om@M(DZo-p!R^5Pe~b@6>hCoOt*B|X41y`H|`TpD`4`|rx)$7FO8 zd)SqJJk#sxd(pRs-qPAde7!i_4;AYrBxwJ|;oHYrLj5;<)@RYxJ)lUwUItwo)_;A- zdjXHsvV_l1{r(bPFUt33xxVS?yU=6eUTAruUK@Q4ZR!KZe>kK;d3wY*d>0ycoYTmt znV25%S3Ogs?%{up@Y8Pto~E$scdCtqSAM6Ouiuo;moC?QrDO8z^Ft>G`KJ%S_@Hsl ze2)Ggspy+fR#6V|?*=x98jEt&zEql2zlXsW<^@xI$;W}an*(a{QocW8{lhCBWn-Tf zl3uH*YBTfC&WqoDvQh~WT2cwE&CI_;FP6AuwLuI^C4RA*EwpLjOZD2j=v%#a_Y?C` N=i|Vw + diff --git a/corpus/outline-edit/automatic-title/native/read/page-000.xml b/corpus/outline-edit/automatic-title/native/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..48a2ab148379f8dd2b33312f7100cf84f6a5b639 --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/read/page-000.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/native/read/page-001.xml b/corpus/outline-edit/automatic-title/native/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..5334eb46b1e11826af3c1e04aa34c67fee59c7b4 --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/read/page-001.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/native/read/page-003.xml b/corpus/outline-edit/automatic-title/native/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..9d6adce81eaa9f312c05bbe4a9b99da177d4557a --- /dev/null +++ b/corpus/outline-edit/automatic-title/native/read/page-003.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/outline-edit/automatic-title/provenance.json b/corpus/outline-edit/automatic-title/provenance.json new file mode 100644 index 0000000000000000000000000000000000000000..ceecd4a3f008ddc0efbbf367ca9121dc367af0b2 --- /dev/null +++ b/corpus/outline-edit/automatic-title/provenance.json @@ -0,0 +1,116 @@ +{ + "outline-title-native-before": { + "capture": "evidence/m10/outline-title-native-before", + "machine": { + "name": "m6-cfe84d87", + "hostname": "ONE-M6-CFE84D87" + }, + "run": { + "notebook": "/Users/clo/dev/one/evidence/m10/outline-title-input", + "expected_pages": -1, + "author": "tools/native/pages.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "dfbf0e016aa4c8486ecea9f39768c633de05ac0db8214139052ca2e9c43bbd02", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } + }, + "teardown": { + "absent": true + } + }, + "outline-title-native-after": { + "capture": "evidence/m10/outline-title-native-after", + "machine": { + "name": "m6-49c5ad30", + "hostname": "ONE-M6-49C5AD30" + }, + "run": { + "notebook": "/Users/clo/dev/one/evidence/m10/outline-title-native-before/notebook", + "expected_pages": -1, + "author": "evidence/m10/outline-title-move.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "9d8779a521fb60ff3063d320babe742deeda1ca7e5cf89f54f5617473e28b998", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } + }, + "teardown": { + "absent": true + } + }, + "outline-title-native-cold": { + "capture": "evidence/m10/outline-title-native-cold", + "machine": { + "name": "m6-3512bf26", + "hostname": "ONE-M6-3512BF26" + }, + "run": { + "notebook": "/Users/clo/dev/one/evidence/m10/outline-title-native-after/notebook", + "expected_pages": 5, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } + }, + "teardown": { + "absent": true + } + }, + "outline-title-rust-cold": { + "capture": "evidence/m10/outline-title-rust-cold", + "machine": { + "name": "m6-61cabd16", + "hostname": "ONE-M6-61CABD16" + }, + "run": { + "notebook": "/Users/clo/dev/one/evidence/m10/outline-title-rust-candidate", + "expected_pages": 3, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } + }, + "teardown": { + "absent": true + } + } +} diff --git a/crates/onestore-offline/tests/sync/outline.rs b/crates/onestore-offline/tests/sync/outline.rs index 87e6e1156c054bfc53acb7ad9b0d4ae8edc96ba5..bd021e0c0b77f555bf664bdee9fa603ddedbcee9 100644 --- a/crates/onestore-offline/tests/sync/outline.rs +++ b/crates/onestore-offline/tests/sync/outline.rs @@ -108,6 +108,81 @@ fn layout_and_dependent_text_survive_reopen_and_independent_remote_formatting() } } +#[test] +fn rebased_outline_movement_uses_remote_title_text_and_confirms_after_reopen() { + let (source, sid, outline, _, text_id) = fixture(); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let (_, page) = document.pages().unwrap()[0]; + let space = &document.spaces[&sid]; + let metadata = space.revisions[&space.contexts[&ExGuid::default()]].roots[&2]; + let second = onestore::Insertion::outline(page, 144.0, 36.0, "Second", "Author").unwrap(); + let source = PreparedEdit::insert(&source, sid, &second) + .unwrap() + .as_bytes() + .to_vec(); + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("titles.sqlite"); + let cache = Replica::create(&path, &source).unwrap(); + let movement = cache + .outline( + &source, + sid, + outline, + Change::Position { x: 216.0, y: 72.0 }, + ) + .unwrap() + .unwrap(); + let local = cache.snapshot().unwrap(); + assert_eq!(node(&local, sid, metadata)["kind"]["title"], "Second"); + let dependent = cache + .edit_text(&local, sid, text_id, 0..0, "Local ") + .unwrap() + .unwrap(); + drop(cache); + let remote = + PreparedEdit::text(&source, sid, second.text_object(), 0..6, "Remote second 🐈").unwrap(); + let mut server = Server::new(remote.as_bytes()); + server.fault = Fault::UnknownAfter; + let cache = Replica::open(&path).unwrap(); + assert!(cache.sync_once(&mut server).is_err()); + assert!(matches!( + cache.status(movement).unwrap(), + Some(EditStatus::AwaitingConfirmation { .. }) + )); + assert_eq!( + node(&server.durable, sid, metadata)["kind"]["title"], + "Original 🦀 é" + ); + assert_eq!( + node(&server.visible, sid, metadata)["kind"]["title"], + "Remote second 🐈" + ); + drop(cache); + for id in [movement, dependent] { + let cache = Replica::open(&path).unwrap(); + assert!( + matches!(cache.sync_once(&mut server).unwrap(), Some((actual, EditStatus::Published { .. })) if actual == id) + ); + assert_eq!( + node(&server.durable, sid, metadata)["kind"]["title"], + "Remote second 🐈" + ); + assert_eq!( + node(&server.durable, sid, page)["kind"]["alternate_title"], + "Remote second 🐈" + ); + } + let cache = Replica::open(&path).unwrap(); + assert!(cache.pending().unwrap().is_empty()); + assert_eq!( + node(&cache.snapshot().unwrap(), sid, text_id)["kind"]["text"], + "Local Original 🦀 é" + ); + assert_eq!(server.publications, 2); +} + #[test] fn competing_layout_requires_current_review_and_preserves_dependent_edits() { let (source, sid, outline, _, text_id) = fixture(); diff --git a/crates/onestore/src/outline.rs b/crates/onestore/src/outline.rs index 3c4f01f7cf20aed1e605190ac87eea4ace672711..c58fe5569c1d023b63b4943689972f39becbf974 100644 --- a/crates/onestore/src/outline.rs +++ b/crates/onestore/src/outline.rs @@ -2,7 +2,7 @@ use crate::{ Error, ExGuid, RevisionIndex, Store, create::current_timestamps, document::{Document, Kind}, - edit::editable_parents, + edit::{editable_parents, update_title}, write::{PropertyObject, write_revision}, }; use serde::{Deserialize, Serialize}; @@ -13,6 +13,7 @@ use std::collections::{BTreeMap, BTreeSet}; #[serde(deny_unknown_fields)] pub enum OutlineEdit { /// Moves an ordinary page outline to coordinates measured in points. + /// Refreshes automatic page titles when the leading outline changes. Position { x: f32, y: f32 }, /// Changes an ordinary outline's maximum width in points, at least 36. /// `user_set` distinguishes an explicit width from an automatic layout hint. @@ -56,7 +57,7 @@ impl OutlineEdit { let store = Store::parse(source)?; let index = RevisionIndex::parse(&store)?; index.validate_current()?; - let document = Document::parse(&index)?; + let mut document = Document::parse(&index)?; let pages: Vec<_> = document .pages()? .into_iter() @@ -65,9 +66,12 @@ impl OutlineEdit { let [page] = pages.as_slice() else { return Err(invalid("Outline editing requires a single active page")); }; - let semantic = &document.spaces[&space]; - let view = &semantic.revisions[&semantic.contexts[&ExGuid::default()]]; - let parents = editable_parents(view, &pages, object)?; + let mut semantic = document.spaces.remove(&space).unwrap(); + let view = semantic + .revisions + .remove(&semantic.contexts[&ExGuid::default()]) + .unwrap(); + let parents = editable_parents(&view, &pages, object)?; let node = &view.nodes[&object]; match self { Self::Collapsed(_) => { @@ -128,6 +132,9 @@ impl OutlineEdit { changed.insert(id, ancestor); } } + if matches!(self, Self::Position { .. }) { + update_title(&store, raw, view, &pages, &mut changed)?; + } Ok(changed) }) } diff --git a/crates/onestore/tests/outline.rs b/crates/onestore/tests/outline.rs index 94be3a4363fbff3f9aa083cd98f53e77cb942bcb..d3f9e9238d2587eea35aa08220bf10bf92c02907 100644 --- a/crates/onestore/tests/outline.rs +++ b/crates/onestore/tests/outline.rs @@ -271,6 +271,146 @@ fn resizing_a_native_reserved_width_preserves_content() { } } +#[test] +fn outline_movement_updates_both_automatic_title_fields_without_changing_content() { + let source = onestore::create_section("titles.one", "First", "Author").unwrap(); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let (sid, page) = document.pages().unwrap()[0]; + let insertion = + onestore::Insertion::outline(page, 144.0, 144.0, "Second 🦋 é", "Author").unwrap(); + let source = PreparedEdit::insert(&source, sid, &insertion) + .unwrap() + .as_bytes() + .to_vec(); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let space = &document.spaces[&sid]; + let rid = space.contexts[&ExGuid::default()]; + let before = &space.revisions[&rid]; + for (x, y, expected) in [ + (144.0, 0.0, "Second 🦋 é"), + (0.0, 36.0, "Second 🦋 é"), + (144.0, 36.0, "First"), + (0.0, 144.0, "First"), + ] { + let edit = PreparedEdit::outline(&source, sid, insertion.object(), Edit::Position { x, y }) + .unwrap(); + let saved = Store::parse(edit.as_bytes()).unwrap(); + let saved_index = RevisionIndex::parse(&saved).unwrap(); + saved_index.validate_current().unwrap(); + let saved_document = Document::parse(&saved_index).unwrap(); + let saved_space = &saved_document.spaces[&sid]; + let after = &saved_space.revisions[&saved_space.contexts[&ExGuid::default()]]; + let metadata = before.roots[&2]; + assert!( + matches!(&after.nodes[&metadata].kind, Kind::Metadata { title: Some(title), .. } if title == expected) + ); + assert!( + matches!(&after.nodes[&page].kind, Kind::Page { alternate_title: Some(title), .. } if title == expected) + ); + assert_eq!( + before.nodes.keys().collect::>(), + after.nodes.keys().collect::>() + ); + for (oid, node) in &before.nodes { + if ![page, metadata, insertion.object()].contains(oid) { + assert_eq!( + serde_json::to_value(node).unwrap(), + serde_json::to_value(&after.nodes[oid]).unwrap() + ); + } + } + assert_eq!( + format!("{:?}", index.resolve(sid, rid).unwrap()), + format!("{:?}", saved_index.resolve(sid, rid).unwrap()) + ); + assert_eq!( + PreparedEdit::outline( + edit.as_bytes(), + sid, + insertion.object(), + Edit::Position { x, y } + ) + .unwrap() + .as_bytes(), + edit.as_bytes() + ); + } +} + +#[test] +fn native_automatic_and_explicit_titles_follow_outline_movement() { + let source = include_bytes!( + "../../../corpus/outline-edit/automatic-title/before/notebook/TitleControl.one" + ); + let store = Store::parse(source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let mut candidate = source.to_vec(); + let pages = document.pages().unwrap(); + assert_eq!(pages.len(), 3); + for (sid, page) in pages { + let space = &document.spaces[&sid]; + let view = &space.revisions[&space.contexts[&ExGuid::default()]]; + let Kind::Metadata { + title: Some(title), .. + } = &view.nodes[&view.roots[&2]].kind + else { + panic!() + }; + let outline = *view.nodes[&page].children.iter().find(|oid| { + view.nodes[oid].children.iter().any(|paragraph| view.nodes[paragraph].content.iter().any(|text| { + matches!(&view.nodes[text].kind, Kind::RichText { text, .. } if text.starts_with("Second")) + })) + }).unwrap(); + let change = Edit::Position { + x: 36.0, + y: if title == "Horizontal first" { + 108.0 + } else { + 72.0 + }, + }; + candidate = PreparedEdit::outline(&candidate, sid, outline, change) + .unwrap() + .as_bytes() + .to_vec(); + let store = Store::parse(&candidate).unwrap(); + let saved = RevisionIndex::parse(&store).unwrap(); + saved.validate_current().unwrap(); + let document = Document::parse(&saved).unwrap(); + let space = &document.spaces[&sid]; + let after = &space.revisions[&space.contexts[&ExGuid::default()]]; + let expected = if title == "Explicit title" { + "Explicit title" + } else { + "Second 🦋 é" + }; + assert!( + matches!(&after.nodes[&after.roots[&2]].kind, Kind::Metadata { title: Some(title), .. } if title == expected) + ); + for (oid, node) in &view.nodes { + assert_eq!(node.children, after.nodes[oid].children); + assert_eq!(node.content, after.nodes[oid].content); + if matches!(node.kind, Kind::RichText { .. }) { + assert_eq!( + serde_json::to_value(node).unwrap(), + serde_json::to_value(&after.nodes[oid]).unwrap() + ); + } + } + } + if let Some(output) = std::env::var_os("ONESTORE_OUTLINE_TITLE_OUTPUT") { + let output = std::path::PathBuf::from(output); + assert!(output.is_absolute()); + std::fs::create_dir(&output).unwrap(); + std::fs::write(output.join("TitleControl.one"), candidate).unwrap(); + } +} + #[test] fn outline_publication_interruptions_reopen_as_complete_old_or_new_layout() { let source = onestore::create_section("layout.one", "Before 🦀 after", "Author").unwrap(); @@ -286,6 +426,11 @@ fn outline_publication_interruptions_reopen_as_complete_old_or_new_layout() { .find(|id| matches!(view.nodes[id].kind, Kind::Outline { .. })) .unwrap(); let paragraph = view.nodes[&outline].children[0]; + let second = onestore::Insertion::outline(page, 144.0, 36.0, "Second", "Author").unwrap(); + let source = PreparedEdit::insert(&source, sid, &second) + .unwrap() + .as_bytes() + .to_vec(); for (object, operation) in [ (outline, Edit::Position { x: 216.0, y: 72.0 }), ( @@ -344,6 +489,11 @@ fn repeated_geometry_changes_and_expansion_match_an_independent_model() { .unwrap(); let paragraph = view.nodes[&outline].children[0]; let text = view.nodes[¶graph].content[0]; + let second = onestore::Insertion::outline(page, 144.0, 36.0, "Second", "Author").unwrap(); + let original = PreparedEdit::insert(&original, sid, &second) + .unwrap() + .as_bytes() + .to_vec(); for seed in 1..=16_u64 { let mut rng = seed; let mut source = original.clone(); @@ -388,6 +538,19 @@ fn repeated_geometry_changes_and_expansion_match_an_independent_model() { let document = Document::parse(&index).unwrap(); let space = &document.spaces[&sid]; let current = &space.revisions[&space.contexts[&ExGuid::default()]]; + let x = layout["x"].as_f64().unwrap(); + let y = layout["y"].as_f64().unwrap(); + let title = if y < 36.0 || (y == 36.0 && x <= 144.0) { + "Before 🦀 é" + } else { + "Second" + }; + assert!( + matches!(¤t.nodes[¤t.roots[&2]].kind, Kind::Metadata { title: Some(actual), .. } if actual == title) + ); + assert!( + matches!(¤t.nodes[&page].kind, Kind::Page { alternate_title: Some(actual), .. } if actual == title) + ); assert_eq!( serde_json::to_value(¤t.nodes[&outline].layout).unwrap(), layout diff --git a/fuzz/fuzz_targets/paragraph.rs b/fuzz/fuzz_targets/paragraph.rs index dbb663bf828dd4e32e13aee1b59fb24635a7db19..5d9d4d801966225ce38a15848bb0ddb24aab6ca3 100644 --- a/fuzz/fuzz_targets/paragraph.rs +++ b/fuzz/fuzz_targets/paragraph.rs @@ -33,6 +33,8 @@ static SOURCE: LazyLock<(Vec, ExGuid, ExGuid)> = LazyLock::new(|| { .with_formatting(4..7, &[A::Italic(true), A::Color(Some([12, 34, 56]))]) .unwrap(); let edited = PreparedEdit::insert(&source, sid, &intent).unwrap(); + let second = Insertion::outline(page, 144.0, 36.0, "Fixed second", "Author").unwrap(); + let edited = PreparedEdit::insert(edited.as_bytes(), sid, &second).unwrap(); (edited.as_bytes().to_vec(), sid, outline) }); @@ -187,6 +189,14 @@ fuzz_target!(|input: &[u8]| { let after_document = Document::parse(&after_index).unwrap(); let space = &after_document.spaces[sid]; let after_view = &space.revisions[&space.contexts[&ExGuid::default()]]; + let layout = &after_view.nodes[outline].layout; + if layout.y.unwrap() > 36.0 || (layout.y == Some(36.0) && layout.x.unwrap() > 144.0) { + let (_, page) = after_document.pages().unwrap()[0]; + assert!(matches!(&after_view.nodes[&after_view.roots[&2]].kind, + Kind::Metadata { title: Some(title), .. } if title == "Fixed second")); + assert!(matches!(&after_view.nodes[&page].kind, + Kind::Page { alternate_title: Some(title), .. } if title == "Fixed second")); + } if let Some(expected) = expected_layout { assert_eq!( serde_json::to_value(&after_view.nodes[outline].layout).unwrap(), diff --git a/tools/native/outline-title.ps1 b/tools/native/outline-title.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..87299b07f00b9476ab4d2d0bb98d513314fbd2a0 --- /dev/null +++ b/tools/native/outline-title.ps1 @@ -0,0 +1,48 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$app = New-Object -ComObject OneNote.Application +$notebook = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0) + $section = '' + $app.OpenHierarchy('TitleControl.one', $notebook, [ref]$section, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + do { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 3) { break } + Start-Sleep -Milliseconds 200 + } while ([DateTime]::UtcNow -lt $deadline) + if ($pages.Count -ne 3) { throw 'Expected three outline title controls.' } + $inputs = Join-Path $Root 'inputs' + [void](New-Item -ItemType Directory -Path $inputs) + foreach ($page in $pages) { + $name = $page.GetAttribute('name') + if ($name -notin @('Vertical first', 'Horizontal first', 'Explicit title')) { throw 'Unexpected control title.' } + $content = '' + $app.GetPageContent($page.GetAttribute('ID'), [ref]$content, 1, 1) + [xml]$xml = $content + $outlines = @($xml.SelectNodes('/*/*[local-name()="Outline"]') | Where-Object { $_.InnerText -match 'Second' }) + if ($outlines.Count -ne 1) { throw 'Expected one movable outline.' } + $outline = $outlines[0] + $position = $outline.SelectSingleNode('./*[local-name()="Position"]') + $position.SetAttribute('x', '36') + $position.SetAttribute('y', $(if ($name -eq 'Horizontal first') { '108' } else { '72' })) + foreach ($child in @($xml.DocumentElement.ChildNodes)) { + if ($child -ne $outline -and $child.LocalName -notin @('QuickStyleDef', 'TagDef')) { + [void]$xml.DocumentElement.RemoveChild($child) + } + } + [IO.File]::WriteAllText((Join-Path $inputs ($name + '.xml')), $xml.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($xml.OuterXml, [DateTime]::MinValue, 1, $false) + } + $app.SyncHierarchy($notebook) +} finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/tools/test_outline_edit.py b/tools/test_outline_edit.py index 4a02c4778c67229a5af116b6159993e0c25868ec..84ba019a8acb28898344219cb151c65c3e71a3d8 100644 --- a/tools/test_outline_edit.py +++ b/tools/test_outline_edit.py @@ -19,6 +19,39 @@ compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] class OutlineEditTest(unittest.TestCase): + def test_automatic_titles_follow_native_and_rust_outline_movement(self): + fixture = FIXTURE / 'automatic-title' + models = {} + with TemporaryDirectory() as temporary: + for phase, notebook, capture in [ + ('before', fixture / 'before/notebook', fixture / 'before/read'), + ('native', fixture / 'native/notebook', fixture / 'native/read'), + ('native-cold', fixture / 'native/cold/notebook', fixture / 'native/cold/read'), + ('rust', fixture / 'candidate', fixture / 'cold/read'), + ('rust-cold', fixture / 'cold/notebook', fixture / 'cold/read'), + ]: + folder = Path(temporary) / phase + shutil.copytree(capture, folder / 'read') + compare(notebook, folder / 'read') + subprocess.run([EXPORTER, notebook / 'TitleControl.one', folder / 'model'], check=True) + document = json.loads((folder / 'model/document.json').read_text()) + models[phase] = list(ordered_pages(document)) + self.assertEqual(len(models[phase]), 3) + for phase, pages in models.items(): + expected = ['Vertical first', 'Horizontal first', 'Explicit title'] if phase == 'before' else ['Second 🦋 é', 'Second 🦋 é', 'Explicit title'] + self.assertEqual([r['nodes'][r['roots']['2']]['kind']['title'] for _, _, r, _ in pages], expected) + for before, after in [('native', 'native-cold'), ('rust', 'rust-cold')]: + for (sid, _, a, page), (saved_sid, _, b, saved_page) in zip(models[before], models[after], strict=True): + self.assertEqual((sid, page), (saved_sid, saved_page)) + self.assertEqual(dict(walk(a, page)), dict(walk(b, page))) + for (sid, _, a, page), (saved_sid, _, b, saved_page) in zip(models['before'], models['rust'], strict=True): + self.assertEqual((sid, page), (saved_sid, saved_page)) + for oid, node in walk(a, page): + self.assertEqual(node['children'], b['nodes'][oid]['children']) + self.assertEqual(node['content'], b['nodes'][oid]['content']) + if node['kind']['type'] == 'RichText': + self.assertEqual(node, b['nodes'][oid]) + def test_rust_subtree_moves_and_deletions_survive_cold_native_reopen(self): for name, pages in [ ('rust-tree/ordinary', 15), ('rust-tree/groups-cells', 12), -- 2.54.0