From d48945258367e00b2866e112f0440c0e7223135d Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 24 Jul 2026 08:51:30 +0000 Subject: [PATCH] Improve troubleshooting and compatible setting handling --- CONTRIBUTING.md | 5 + _tools/inspect-troubleshooting-docs.ts | 138 +++++ .../inspect-troubleshooting-docs.unit.spec.ts | 17 + docs/settings.md | 6 + docs/troubleshooting.md | 13 +- docs/tweak_mismatch_dialogue.png | Bin 47151 -> 32256 bytes package.json | 1 + .../ModuleResolveMismatchedTweaks.ts | 47 +- ...ModuleResolveMismatchedTweaks.unit.spec.ts | 125 +++- .../SettingDialogue/LiveSyncSetting.ts | 3 +- .../features/SettingDialogue/PaneAdvanced.ts | 4 +- .../features/SettingDialogue/SettingPane.ts | 1 + test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/dialog-mounts.ts | 553 +++++++++++++++++- updates.md | 3 + 15 files changed, 868 insertions(+), 50 deletions(-) create mode 100644 _tools/inspect-troubleshooting-docs.ts create mode 100644 _tools/inspect-troubleshooting-docs.unit.spec.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1ca3b9f..a97333b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,11 @@ Before submitting a pull request, you must run verification scripts locally to e ```bash npm run test:unit ``` +- When changing the troubleshooting or recovery guides, inspect their current English UI labels and local references: + ```bash + npm run inspect:troubleshooting + ``` + This read-only Inspector prints JSON containing `ok`, `checkedFiles`, `checkedLocalReferences`, and `errors`, and exits unsuccessfully when a contract is stale. If you have the capability and a suitable environment (such as Linux and Docker), running the CLI End-to-End (E2E) tests is also highly appreciated. Instructions are detailed in [devs.md](devs.md). If you cannot run E2E tests locally, please explicitly ask to run the tests on the CI by stating 'Please run CI tests' in your pull request description. diff --git a/_tools/inspect-troubleshooting-docs.ts b/_tools/inspect-troubleshooting-docs.ts new file mode 100644 index 00000000..e7de1545 --- /dev/null +++ b/_tools/inspect-troubleshooting-docs.ts @@ -0,0 +1,138 @@ +import { access, readFile } from "node:fs/promises"; +import { dirname, relative, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +type InspectionError = { + check: "current-label" | "local-reference" | "retired-label"; + file: string; + detail: string; +}; + +export type TroubleshootingDocsInspection = { + ok: boolean; + checkedFiles: string[]; + checkedLocalReferences: number; + errors: InspectionError[]; +}; + +const guidePaths = ["docs/troubleshooting.md", "docs/recovery.md", "docs/tips/p2p-sync-tips.md"] as const; +const messageCataloguePath = "src/common/messagesJson/en.json"; +const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu; + +function repositoryRootFromThisFile(): string { + return resolve(dirname(fileURLToPath(import.meta.url)), ".."); +} + +function normaliseReferenceTarget(rawTarget: string): string { + const withoutAngles = rawTarget.startsWith("<") && rawTarget.endsWith(">") ? rawTarget.slice(1, -1) : rawTarget; + return decodeURIComponent(withoutAngles); +} + +function isExternalReference(target: string): boolean { + return /^(?:https?:|mailto:|obsidian:)/u.test(target); +} + +async function inspectLocalReferences( + repositoryRoot: string, + documentPath: string, + document: string, + errors: InspectionError[] +): Promise { + let checked = 0; + for (const match of document.matchAll(markdownLinkPattern)) { + const rawTarget = match[1]; + if (!rawTarget) continue; + const target = normaliseReferenceTarget(rawTarget); + if (isExternalReference(target) || target.startsWith("#")) continue; + + const [pathPart] = target.split("#", 1); + if (!pathPart) continue; + checked++; + const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart); + try { + await access(referencedPath); + } catch { + errors.push({ + check: "local-reference", + file: documentPath, + detail: `Missing local reference: ${relative(repositoryRoot, referencedPath)}`, + }); + } + } + return checked; +} + +export async function inspectTroubleshootingDocs( + repositoryRoot = repositoryRootFromThisFile() +): Promise { + const errors: InspectionError[] = []; + const documents = new Map(); + for (const guidePath of guidePaths) { + documents.set(guidePath, await readFile(resolve(repositoryRoot, guidePath), "utf8")); + } + + const troubleshooting = documents.get("docs/troubleshooting.md")!; + const catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record< + string, + string + >; + const requiredMessageKeys = [ + "TweakMismatchResolve.Action.UseConfigured", + "TweakMismatchResolve.Action.UseMine", + "TweakMismatchResolve.Action.UseRemote", + "TweakMismatchResolve.Action.Dismiss", + "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown", + ] as const; + + for (const messageKey of requiredMessageKeys) { + const label = catalogue[messageKey]; + if (!label) { + errors.push({ + check: "current-label", + file: messageCataloguePath, + detail: `The English message catalogue does not define ${messageKey}.`, + }); + continue; + } + if (!troubleshooting.includes(label)) { + errors.push({ + check: "current-label", + file: "docs/troubleshooting.md", + detail: `The guide does not include the current UI label '${label}'.`, + }); + } + } + + for (const retiredLabel of ["`Update with mine`", "`Use configured`", "`Sync settings via Markdown files`"]) { + if (troubleshooting.includes(retiredLabel)) { + errors.push({ + check: "retired-label", + file: "docs/troubleshooting.md", + detail: `The guide still includes the retired label ${retiredLabel}.`, + }); + } + } + + let checkedLocalReferences = 0; + for (const [guidePath, document] of documents) { + checkedLocalReferences += await inspectLocalReferences(repositoryRoot, guidePath, document, errors); + } + + return { + ok: errors.length === 0, + checkedFiles: [...guidePaths], + checkedLocalReferences, + errors, + }; +} + +async function runCli(): Promise { + const result = await inspectTroubleshootingDocs(); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.ok) process.exitCode = 1; +} + +const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; +if (invokedPath === import.meta.url) { + await runCli(); +} diff --git a/_tools/inspect-troubleshooting-docs.unit.spec.ts b/_tools/inspect-troubleshooting-docs.unit.spec.ts new file mode 100644 index 00000000..f184a162 --- /dev/null +++ b/_tools/inspect-troubleshooting-docs.unit.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { inspectTroubleshootingDocs } from "./inspect-troubleshooting-docs"; + +describe("troubleshooting documentation contract", () => { + it("uses current English UI labels and resolves every local guide reference", async () => { + const result = await inspectTroubleshootingDocs(); + + expect(result.checkedFiles).toEqual([ + "docs/troubleshooting.md", + "docs/recovery.md", + "docs/tips/p2p-sync-tips.md", + ]); + expect(result.checkedLocalReferences).toBeGreaterThan(0); + expect(result.errors).toEqual([]); + expect(result.ok).toBe(true); + }); +}); diff --git a/docs/settings.md b/docs/settings.md index 4dd9521c..16af6883 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -959,6 +959,12 @@ If disabled(toggled), chunks will be split on the UI thread (Previous behaviour) Setting key: processSmallFilesInUIThread If enabled, the file under 1kb will be processed in the UI thread. +#### Automatically align compatible chunk settings + +Setting key: autoAcceptCompatibleTweak + +Current releases enable this by default when the differences are limited to compatible chunk settings. The side with the newer recorded modification time is used for the chunk hash algorithm, chunk size, or splitter version; the remote value is used when neither side has a recorded time or the times are equal. No dialogue or database reconstruction is required. Existing content remains readable, but changing these values can reduce chunk reuse. Turn this off to review compatible differences manually. Any difference which also involves an incompatible setting always requires an explicit decision. + ### 8. Compatibility (Trouble addressed) #### Do not check configuration mismatch before replication diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index be623d3f..41812ed0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -61,9 +61,14 @@ If the log reports missing chunks or a size mismatch: Some settings must match across devices. LiveSync pauses synchronisation when the local and remote values differ rather than propagating an unexpected change silently. -- Choose `Update with mine` only when this device's setting is the intended shared value. -- Choose `Use configured` to accept the value already stored for the synchronisation group. -- `Dismiss` postpones the decision, but synchronisation remains paused until it is resolved. +Current releases automatically align compatible settings which control how new chunks are created, by default and where possible. This applies to the chunk hash algorithm, chunk size, and splitter version. Existing content remains readable across these choices, although using different choices can reduce chunk reuse and increase storage or transfer work. An explicit opt-out retains the manual review. A mismatch involving encryption, path obfuscation, file-name case handling, or any combination which includes one of those settings always remains a manual decision. + +The available actions depend on when the mismatch is found: + +- While checking a remote profile, `Use configured settings` accepts the shared values already stored in that remote. `Dismiss` leaves this device's settings unchanged. +- For a mismatch found before synchronisation, `Apply settings to this device` accepts the remote values. Choose `Update remote database settings` only when this device's values are intended to become the shared values. +- When the change requires local or remote reconstruction, the action itself states that Fetch or Rebuild will follow. Make sure that the intended authoritative copy is available before choosing it. +- `Dismiss` postpones a mismatch found before synchronisation. Synchronisation remains paused until the mismatch is resolved. ![Configuration mismatch dialogue](tweak_mismatch_dialogue.png) @@ -75,7 +80,7 @@ Historic defect notices and renamed controls are retained in the [0.25 release h Generate an encrypted Setup URI from a working device. This preserves the intended remote profiles and selections while allowing the additional device to keep its own device-specific name. Store the URI and its passphrase separately. -For deliberate setting changes during normal use, use `Sync settings via Markdown files` under `Sync settings`. +For deliberate setting changes during normal use, use `Sync Settings via Markdown` under `Sync settings`. ### Choose a Setup URI passphrase diff --git a/docs/tweak_mismatch_dialogue.png b/docs/tweak_mismatch_dialogue.png index 073a999be8fbdd8e47fd21221f3445a9834b023e..d25566cada62529216f7393b3cc07999510e2e40 100644 GIT binary patch literal 32256 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 literal 47151 zcmdqJXH-*d7_O-w3P=$Y0RaIKkzND@>53HTMWq+%AiaYDqEZC`rT1Qx-aCZeLhmIZ zbfhIffDl5G6Tb7Ev)0V4S@Uyd&5ta|PT2L`?|z=^zHh#3YbsNcF_PW5af4Fjt%B~2 z8~?BqUjL916P7S9jXoj#xap~@EPtb7?9nda%`Lmv8n18MsE#GSvbarnf5+{uq34Yo zcYCg1HwRpQTi>|R*sG%O`n{joVI~RWK_3=R!rLD4#PoGx#1lqi%kRG)21hc|ZopQa z=-k(sG00PjemL`6=LsXAFT48L$M2?HF|_2rzl6qwJ{cXxuk-}Q`X@nK+X3*ktP_8v z5qvRoksIa!LysM}TnZIIc7{jn!yrEz)4LGoAY1-&<)*#eh>eTcTTB4HVh#(Eefr z$#frr5I5$3>#!q|=wz0zcQ9gx&!lM(>%HJ6?ms*L&m$jmb8VN&HHswtZ_jj6r3BY;=4-ILTe3oI3+57MWlj5$ay__zg)Eir2->6@&>oFZqoKz@CZ2cb^M3!?G{+8+UGk zj?dPUL6S?@ZIoRzUiw0ztC5wAO+n*$xYV@;XbMz5u$?`shJmnwA(#`ezhSbM{rFqT z6U8E-Ea2|h!Q${pio`(s$&&G6ZAn=xToQL%L`H07!W%v#3#j)wIWC@t7WTM$;aiUJ zHTnwN>RBg$Fo!BYcX}L_QG5+MWcK_fpA~DM^k_7|xear^jJSHi7Gf~#)aotgyHnmn zlb!aOO#)FS*Q6Gnxix24?0x=8GEz@TQ*w*fAc*!=PQPeZHd7m!b- z<=a_-a9VL7PJglV!)u+_7T@2Ai5)BrLH(z<;3eQO?|qvG@k=92uI)QG)I|zpl{?aI zDnSd9Jq{Yt$dsKfpDs$2I_tH9$TU*<#>Tev$@q7o{-3V9p^;{#heA#F1S4-uM zE-S^hX=2WCyFHQb6ka9$VzkV6F*QL^wl--MY5KAT3iAWJZ<`0!4@L)CWaulXa=+98 zUVK`6;(K=H`?=36A8ragQp2wYH7}1XRdN2<$i>k~@&Khp?c; zPVu@PW%`gx`Qt?{-9Z&&my$uvb~8O%bdW3b;*NPs7iZ=MLSp9|FJ%2-Ah1XyduzI8 z{59yz<*Md^1w7OqT2i>|JkNZkG{c$g?_OAs8)6OIOH3$l_`A{K1OM6H@(%DFk0_-^|re@>6P$Z3Gkgcw)2w{j0Q~4n_uZR#_=iBz_Qn; zJIR$a@6pe3jYimhPa(@=a_jr@&I#5ENO;QnirAJvyaD@c_YJca&I++j zKX-GiXbwZ1hM9TP!@`da8(}S`zAG!`t(r_I5NH2qPchYyWBcu;52-gv9DC@}&wQA? zb~m>J&O;Fa43Fb;YFl@{Dqk64V;iAUnf}8cT`jwWc&gLS_#CesnZ|!ld7!#21^Il-H*QDGCvL`${e0 ztbqO(4pR%xjc0TnnmIy`qqN;c`$5Q#N=J((6c#n9jzy2F+PJU4aJyTu8b#0f5*u<~ z6qX8b`=<}nZNsdKVsgm7Yy19Zm#I-_kcJ!zo$LuU7l~xCNd57fcODv$Tv^JcyWilv zFp@7weX&$5LjW!s~A(V+fDx#!i@P_YQ4Od=cYh@|K9Vhy;cMnF>hwbHJ>gEZ}$ z116mIE>D*heBa`I5y9|2{gtEbz2yo_D+ZYz<120^GjiCj46KwE>NFUb^~s!rM@Ef% z;C+#DkUsjWA*j&0yzeF(#$P;1{$WJ^)z4;B*GSGV4OvRRmNd?-GNMz&%KUsT*%Wwb z#k%u{lX<`A3Y_}JB))$aq!5ue;fmAeihGU`sFwrB~^T5+kf^ zrdSVldyptF;}IP!-4(PWi?^)6r*lSC5N4+NKO2{DbA8+_B^JyI-Ri*|;5_;f0Ua*P z=r4^;$Ku{0E%N9Uk3)_QmTIaE-(V_lPg#6-xISruiIQnD$PIQ>+oRJd-O~cA3PxEbYbI)H|RbW?3I7!O!7j>3IDSCOTh(Vqe-#3IRb5Wk<>galH{h{3m|q=;|j zZv!iP|ZKRJY4uzk(W5X9J&j`;ay@vm0H@)Ta? zICw}RihS-iACIlYsxO#d>?~X8S($5x>0(+JMfB@(jr}CJR_bdR$kR_m-3hV9wzf84 zDdp~7N$rs|>|SZ*_9f!PN^SvkG_@}Q4t6$ydM6)xl4&zWs?g7UgZe-xTWRp)K1(Zj zk4IGq!S;0$E(K<(G6YGC;#UXIgH$DkcpW1HfrUdmDeLt|s~f~YhTWE(WNWkCQciew zR(g^-9^2&vk+qzU(jw=ya%#%leexEk@aA8}AZUxrRjID0**knJwkpG49$(DTx96nAJCjj*m?rIm{W&)G9Y7J{U;iPv1%U@bGQEQ<~0C zCu)s<(2dT^{rL9P($E!MU5Zd^+sCc+W$>61dVk~= zS=m=z3ild^$-G5XJ(hS5jI7TxX3%8u@WtOI$O)rI0eoacz>!c~DJOc}AgiX%3(~O~ zCav1+rpVy9F}JsP5ME#4xD#V`TouU9s(v|$v2&Ft&Aba1#T;Uz5@Sq(E$zmD^7?=Q z@7+Z4%joa;4!ngN@=^yhDlF3Kte=+3FhXYB;Iw9EHUiHJmWcf4*=DVlRG0U1RW0lL zMW8#b3b(l@)E zBr43@sz9Bu?x^OI(ms)_N+M67eV1gAmhu9Cj?<8MedlBFu6dZ*8j7d9emiaDq}o55 zcJBM0c9~O^WnUr_No-yCi~{tY{EY0U_Y4+8p@sD4f;kaF@s{s6^=KZ%7N#yTx-#}r zzTl(Ym#V|i>wU=z z+^`!xV6Z@tCVinMhMM{;jhW)d$)PXwGZ5l(H6*Le^yc8pw^UjqAEW)wq8p)^c$UM; z>HwO|+3|9ArdW2#yp~V0R z=k_gIL)OBt^_1(@!6W|G61~<#f&lHjt^O|Ej|RRYC5*z{tp~gzzvX6r3(MQ}<#c(F zYdx8;uweLFVJg{lwp@Q!WVN!oXcbS{&iznG#I?gg%hh9NH+JHd=D_Jpt>7gTcQh&v zyKrL&7&#r2V0P=^2-Of*i8$l>XCR%Tv!K!B5rHXC0@Yy7Ej)6=RaPMG|T zAXd_%pfaPiSA9~ZehG97uqquCWq=7`__2WwNG^81mo*Bjx-t6k{AgD@KwG@ANeK^U z30&fxI$+ApN><(ZF%`60NqiBIYmnu2sf{{mYV-i8Tqx$YuFU~F<|Dhr+!|e=!tVsY z>C$v5#RDA&cmdinK!Tua7&DV}np9xF6MmCUi!}78j@p}yoDBf%uE~$Cy_a&ORZ9riHSbbX|1KEoPhv{J|x|AmW;+sKt zCAJ?la)~(Fu$#*Yo#D45h_I5_Dll$~4LnHTCnm>aT7Zb&;m35_N4BE zEpZmHnTc;N8)$g5@>lES<*yGXUpzYFVD8HJmfKyCC7a#TU?>mRutFRB$nU9t@WGkx>zh{1B#J5L!A`%B&)?tkC?BEQX-AofB^|Vp>md!(8 zoyH~Ea1195=d;3%+ht=@z~)WXOz821P}_V#&>nY7By57iGk23mL#Us8XKWd;$AUWu)omCL%R-$x*T+5hzp`l?+Wf>pzh&Gb%w^IlU3K;i0jS@Gjbj5 zdO^Q&e3wlu&+@hfk*HSB@r)WI%#JN2;>rKyU$lwQ_1Yk8yFPKInRFO^QCK&Dg^;m$C$d8YUX1aY?WCDc8aj%(OWhf{Vf^!_Wx7OzhT zB@?+C)t9-TuS7m)pl9T2JwIWO)~{P5M3_=wS08(u)cji_3l^`=EI2|RvfA@;Zs6>? z%!Ls>3;yd3o+osj$1X`hgZg)q8uF>6I__U6PuFBp|M z@ef{SGkHD@J<87QG)Gqd5N1Xut3LCWiL@aG{Hd!bujgmDs7Zm-%Vz9^vPpjXbc z>Hyq~^q9v7tk{5)_zd^Pn}ar8wNODf@yd5)p?Jm%Hj36T7!JUk)yf%T-*iNvL>GQ{ z>_EZcd`zgNi!$(u(6Q@o-OIOvy`clJ?YklLVfRLP;TA3YBSOpfQJSEtZglOu$@o-byYy0ryZ3vgu*hT;XkT zo?@;^>BV{>fw3O9<}5*T3&VmTwFhePq2J}M!Yz8YXcw6*7{6e;J*(XK)gfi-WQD0P z`^={@+j0U<-RdRg+gamsE=l3`xR+GmPPm{Fd{XoVkS46pCaAmVBoX-Nf)c9X1EJ3f zcj)8ii8_)tV$oSYVJYf4`S7FdF%K&3trRL=Af8GAQSHZnI0(BHr5MQnf z-=EC~2$a>?WP}N6eRPio?gR?*+CCi|I|*$wDCuQ3tW|J0BmKG2tr%5$P&%BP{Ia`6 zC1CRf1-p@60p%i7p!ojobd~k))Inn6>#zh+s0xmohFxHElOYe#8lHI<&@E&N`kl^ScsM-(x0 z4OmXQC=3M4pHWqPn&78!Weu7%LJf;pT#82)7)D&#U`U< z>__(a(E&yIvLg?XV+vNaFNkN~eT%m8^DRX8pcLMiG|7 zUki(W-ZtkSSw{C3zIbZh;=2&U>zbyC@sc+|>jw2DxHIPY*SOs<)ERhQ8pkOmG?dr$ zq|}!);4d+=kL6f2sF`l~Ik#XRHN6rkGrJbxS6CNk)&^Mk&gS?RMt=Oyf(vzALg7|w zuxG`1dgJ4U0J7KEs&nOHRb{Zkz;J+05j@YD_D!`z1iN{rmN702q&NEC`I|iYi%AoA zG&=uGrgB!YP{(9pZu%RymZgyERrpJXbUun3_{=^eoHc=!-8n1IitezBg{zBdgYuje|B_>?-`NsnSFP!N=H5ig``2n=$Kbtec=WLcf%EURtz~i#5K#m z@9|E3DW2;+KC%6s>t7FfH2$!go6#h`ieZ0Yf zQPj2%7JVg2Q&&x`9!pO5bbLMU@!&)OQ{1L)nq@O_Ra2YMBVu&n#+~Ps(ySw7&)vh- zwEKP*J^J*mZnr=F2h1#S=*r?ze(O=c&aM6_%D`BDJtSg2j{L)95SW5uFjXBe@Spo{Dcdy?{VxCLc- zk~=BEi`C#8guY{f`$i`#4(YAyUN!b&8|~gfBgPWwQAQ;=cKB;x;KF?FF8HfMK@!#D z7&pUio?Eqg@dM7jI8M~5@_mYLYqCG=n|``VaA-)(+(J)jNGh$!@FZth(oS$WfL~^3 zPz(oqJnweVHlNnNmr9B2WtuP2{dMqKn7!SAP}l zY5#ykX#nnr?wMCVV9&2&rZvV*iiQo#|M9m>zeUqpoE2C-ENsiY_6w!j-d0yOO#1Ot zKJp{zhU5E`jm0wSXS`&YqsF9nd`B+de;ZRAv;A``rNAFDHrMm4L#{8Jbw}xDuZ)rr zPskfTQH3a?g&TC;{m*zRhpbA20?7oLAM=XsEJ<#zbEuU-teAco_-ZTxb$%1BeZa1g zSvF8sJeeiwLAUZ$B!>n=@h_T>wuiH|0D(s%#l=}(>H?2W`Y6HzA8v=Y*gEX!7*w?# zFMDpd*pxatJUQ7p7%U3Kiyv=YkfI zD)nr;tL~ZS7P3pe6Ppcl5)VgpM0lTBuGE=0p_B8S=}O{t z^0vBe_6bBQa4W+5iezHs&nh2G4RTd56K)Cv-ELWOzAIg%f?RmF!m?$Y(@e;ZPjPW8 zB2U&|(`+-iO4eCQ(p~H7Z0~%)Z{H9jWottML|*<|=_@Avy8oQ_ZBueE$9E5b+jj^H zDR2I->ffm7Um5=Yv0X@rOT4$DIcQ9#EO%vnux~XgbGc_&kq(5Mv@^u-Hx;9EjB=|! z5hZ2+Rxcqx;XW^qFNDyLSUmV_%z@rg>u|4B)+9o?1Dm(>-Whx@vrUcF#r8wHPY>Qy>|DaRp zK^K}EZTfjD8s~i;;6NiG{{C->qovyrN#NZ;sz7bVJX}6m(`C(vH-Sl>&6h@iK&%uZ za24oCPS{?6dyV1rGaNIjZ8ZwPrvSPeaeu0HM~OF3;rsr@d{kEmmwI!DFnYaxA|(p?%%K_%KKy{YQR=0;og6fB1xhd1Ny84-MXZ zTh>N*N90ToGv?c#;qr|kM>MUGEbyIw6=+3(T@)@5n|`?Hj<|Miarv| z<)tNRhTBfNml)bSF!alDo2pT8dkoxC_E=r(6lZS)XF9W=!V z`w%^qp&;ALIh5fRi{EI*9d-n4@|0~{8C~`OI=eTba<9Trp*G$3wo9oC!mpOvC)?OX zqUcfffY5?AM)w**{p$w(tDL1uA@x*qg--L-%$iyw+`_+m^hEAFR3H0-TrR=lZ?+$| z_;74&kvmIrn0@MlVJ7V?ckp`eFE60UqNf6Qa_V6V()bhLYn61KKc{rZTvU1@Z3xOO z#DY#$|NWSci-jQrgN4cDl_El8nyRpwb196!+}(z0dw@{QJ!jP8QodUDj?s<@sIgwl z>uauV7Spa1{^mCL{-qVWRlxI~^c_AU^N~FOQcm>&EIx2#zVb+R&mg7p-h6b~)Go)b24-#91RA)zn(8Jm z-*gr3=@1=3z_NxZT`>CSE-1ydrHZoIZBh^4;pLy7HgZ7LFH8~3TsO~I>xw%2|M_4Uf7)1jAgzcsHf>rdrVJ6S@&k_~8t8cyV#4zEbW(@&*EWl>)Keg_9=~<;K^6lhGWo+x>YqyFYyfwQc0GFzbgXQ|>3PXam)#$*p~W!pZc z*wfDy<^1}i9A^-q$4WjVt(xKVRa9~Mb^8lBA66Uc?NW2t$_L5zr&A3?vd66R7GYk8 zQ_nH;k(cjEoHhnIU5WQ5RKYLSqnSG6BN~?7{?Q{NVSOp2|4fXUmhsUby*~vl@9M$gznLY=xLwQy5){#SXT1WyY%d!CrUR+9Kk_iXLRWS>5Gl9*sl ztke0;lJ@r34Jy3Sf;QRL#|~axa_$dk=zc2gmzB9szI@B;aF_RaLg@|iL!wtfwnjY= z$BUSfU%WiA4sbR(Tipgfl5D@2^rU84$2mJ$86Csk*U+5j+hEpRe}YTHWx|4Udwt!t zVok~D;orF7moWu^?iLdFv*6P>7s*pXh5jS)9pRMTOJpFECVIFT?+>|={B*gALGxiH zR9U9R?`72wMU=lHcMfce`&2u8gi0}Wwue0IYbO+UFte9zWG_1 zdisY1Z3|_~wN!jaAx$x80s8dQb&o{2&-|?}-Y#Lu?1y}ylIWF3-3C|TT&u_Vo3MJ$ zDJypmHy3nNlJm=?u%Gn90u`0zn(38K6?xf7KV#LEAKYfk`!JT-*ojZux)sm5W1mMV zt?AeIiu2+8U)7t=f$?yri!nJDA0232(mzE#6K!GsA_WV?|2V*(#|yhJeN6=!7xjeI z)#MYLg$HPQ0WkDWy#WAj6mJb$UEB(@VWOn(1+pkiQ~3G0bGV0~dC*JgU5z$Vz^ zRa5M5ZM=Sj?q!~QLMBYEFWg`~s$Ye{J)3*fVYopXsGTWhen-`)$gs|DNLAz? z(GQ|qlT>ma3U%KZO5hQ}TAQ>^jDH_!#0!!lxE(- zXZ?yGuC{@tcOB3FkxK|pJQR>}rZ;X-!QL!zK+Zb!yig<<=kD;P>D=mPd|H%KwVr}6 z?r%#dncO7OU||bSPZTOmEixxF@XGppt_Z4pVj|aXB0(tQgNSXUPc9zf)g-@s>y7P; zg~_L@An%obeE+HJX_&+4T^$j@x&$D%CjPeiDN zrptY0wGTVEDA5U{e>*??W>(J46spo4KpOgSHC7$u5n1RGMoGY7;rrpqm^XQ6zf(Tz ztI8#8XEmKa;AcOt&~8Xj(@pKIcbw8_6`0v(ll*RRCzj()ydnqJknp!J&(t&Obi2!S z6qrbGi(c>TDXXbT(HYa>7coO~T>{KORx93Mc_9NUc%Ee#vm~%~BQ+C3M^77eQSNEA zYU9&mBnP*zO<7e9bz4kPTW$^Ov7n$0MV{~``lfg73m{sidv_u+fR?dP;`j+j+ zOT#Mil}&1ScAv(i4`sm?fzkf(V``4FB%(L5`9W+VT zfUgdwwSMasg?5GveYDwqLIJkw5aq0f$%2m119B&?1ZEgkI|zd?7_cbedKOJYmTJ}< z%i!+Ex)tu^WScS*HdPlazluPP@96znUu-2vb2zKol}7qXcHxNpPxm8HZ=-XT1r`Aa!eQawOi8d~bM-hx_y%u1B1c6abIcw}xFi#Hb4Sz_X(NivUxzSXi!p7dvlJ z3LK82?)zr>^}cEPd>r@=x)3R-lv+}rvnkY-kKmy_15(B=8EtbID%tmrtmcmc=M{xw zh4)J~FRQ5&zirJ0*y1ij_@NUw=e-p}g6KVg&((O{f8!nf7BKl!0<{%9sx?1mNbE;a zh8J|A9Vn+$sPk|ZBCpS_t9}?UtH~EyjAm~Ue?*rhY>{i@IX^K`>itg(@L^7Y4dZpE z65;WmTbEYx|1)BRYJI{leRJS4HemJMhr1Pt@y*EX*8CW`o^WjE-vMZ%K8yfa&6Xb{ z>@tDP#x1A&2Z<-I{6f@lp@iePR3ac3)vkzrAm(gfrMx#uP{ zzodXIEvVHc1pJzts*S`fNQ1Zs+s-Gr_S`DnmWoS%84+oYv<;P$}*?GEwXg<0qJ*=?LF@z%8o=;c&r&gJ&S z9r)iKszE~s(kP**LbaW{F3FhYbkE|R+=EidPNYqQMs)RF{7%~WCwV^66CevSnN0$Ps|ZHRG#9R)t; zP}^PpFD^Y8TDw;I^k_K_8B(7+YOcJ#FMg$f`kd35GZ-?gG%vth3oQS*cWSr@I@#!P z*z^egwx&zGW0EZneC>oI`#QTl+Q{k0OFBT1nm!lyylSqo$-U;8 zbOH53`?`;w0h1cLPpzI`dhJon6G(N(BLW0nS5?y@Y`?dp$lj)ak_cb31%844$UT55 zw~*(^gHTAoCm=Q7nd!3tHO7F$devc8wHJ`^1B1NRY_Fc*NciSv7*$AfrALy~n72Ey z_mv|oU}5TA2bP<#P6ds3b;6e0K}pZ!gvcZK?>rn?F#ODw94G%$#CGgY&4HC3B~j@e zwfk&bL;0=XO< zrh2TV_{6t(wFp-|?-{p4YFIlfpSVKyCkr3!-*_wR4e%?+be-sB_*E?Hk7Nl-CM7!*}S+2?ZnyjBIH{ew(gdU%e+Nzhi6Bzm`#jK2iL!Q`po z@5Vd+Cfhr`E9I7J$S%-kB;Vpz!6i`cBvG-yZ#i&yO4z>T2c0-!OOa2+69g=syc{od z8dnN!klWkG3K@t@+aZFR-$;ln3;)gXyrg!$b7zB8ylOq1vVx1QiPd9-J17Tk@)1Nr zMzc$WGObGJerEibLaeR^@sD`IS2+rasn#u`+mT!J3Bt=JWJRlnjJ_-G>Af~-qWsHNvjz%frtn(-mf*du#dGf0JM;AiDB1ARUNNfrj`)8; zpB6PrV#E0Ibfh$LVUh_mR$77czU?9?p*`C*OGZXTv=^)XMqK9ez~!5?__V^6?1_u7ueD?PH^o& z&&gaZ)WTC}a0l1EeOYKY3!ntBwl!ZXJKQsG4%~2QJ7l>IWw;05m6~k7_UTm8n{LGx zc>EE)PKQ*Q&xo)wG7`vCX=O*tvP(KYM4z0#nJ|T9x3^@tg0N%lS9``4p2g=C`mylE zQu&#{v=`2MnpcNw@Nb^D)iH^{h~*{>F~PH6!5pyqs&PBb(K?W`R#ZegukPxohiZ|x zK0HmGDRPWq2M0C9ji(dh6yhELM0M%1E5f{qven1Ke2gD)_xMYXVPHnmS3%Qwq{mkz{4ORgXzFC-$Q_3z#4M0o7 ze%+WEZ63UK&d9Nazj0v(>$l7g^)Yr`+ZQJRdS1_jZFy#-OQYrUrt3j~5xm+K@PqC< zhc|bcjvD~&q3dOG%!mpmS<(EbkG3^0M%i5cn&bYSUv7=p$bx$a86%UZ?NJCzD}Dw` zuySn-4P zPLq&U5N^8_{%fGW=}l03qj9kYuPaac1?xPZ7Ll(dyCX?}8O9onFFtKP3li!x*7I5R zCPC1PY}JcH6L+N>+%*`*&({5$CIMVD%FbP(GwqPz}gnS0S*nVPaU&XG*WUS0t8f))p|CD30!W$;&B@*nHRasbvykP zicwpL0ik9Mf|tH^_Z$Evgo0YIs6}Wa{$&|71F)9Z+*CDny3mxaA468ZivZ!fT5E5q zx3G?cg#L4y9~20=O&{x~C48l133;GYdR-jaqL-z36BRkp3!=^JQvTv%oy?8gbH$+ss0)U3%oj~ec) z+pTTGTe1a6ib}R`06nW(w`mZ0I*>ax=yCGN{J9H7a#vb~r2qEx1>=6EKBzPk z?+o7*!EYzhjdX4z=YCOLXcoWgw&T zCY#CfE=^l5sLfhIzkN#8-t-Mlzrx$HXs@vJ7Z^`iR{q@2ExKp`gy2y)naf6wOHN+r z9$+ro6ap;=MyRW(%2rSYP6`zpH8xCWt%AN}gH}tONWtirB7Q*RVPnt@EnS5&=+%C| z6qKOXZzOm4TpiB^34yQ`_{}MD73VY?;Uw_+LLmKoor;zCX44z1`I9a4eZv6tuP%m+9C0*)QYLi&q^tfxHp zjLD{D0c<{QTpss1Dw-!mAWk#aL%zq?cIXum;$ZLvc48Y_DY+B6bb(CCCW68IKlA!VeL4rd|ml15U2tgh|YoYh*%I zqxiR6PF2?Dsp=qY?C3p`&yJ5(rOuv(NDfZfhMyk>wB}Z5vx-ALHqSl@xkOIjv)r$P zMq@G?zOqhxuu5XwlYOgZn>9kT%-z_z*|2Dg^>2$8eD!>#7IbZN52~^;{C<8hw@f2a zdwDzPi{0bHhSzp-DM}U-z$2hmYY!e)8PHNjc?K;jZglE68@mN$RxI)zj}DN@J~@70 zxf)XC)a?W^tbInP&#ew^LF#f{CzQif!*b4~Hk9Nszh;Vn7Z$1dO@mZPT2l;-0b%u< z_2MgMU>!&--?+3*l|G}Y1-K)G>=cD55k&lvD;@5)==p1ii`H-D?*<1Z0}pUMs3?scq{QH<0zQbMuFP{FuOZ zP3F0Ae)i|apFp^l4y#vR^F)ehT2Io9q17NMFtc|zLv<}XaeR?RvTs4iO)r+s_~0=r zPhpFsG)*1gM>nTTowr{;zhN#23aJIWVz7N|Hz8+v!S+8)YPfmcU!YcGNxH z`q@o>V}Z)?i)GRLTMwh?1kcq-$8DCNEEv}P{guww!O&9IwvB!130b?btc`v2x7*~{ zTi+JjLK(W_U83j53f%Yk^;8M|BX!RxI)3-!EN`8Ptn9OSa9}HbJH14eJ^;f%uYVvI zWqD{@s&2$5kfbWUVo&7i-gRle={W81alKR6AU3+irt&4|V7TDV1B0OIcCOGRJfs=f zz7=7^0p|NTqxo^DI|;wXo+JaDDAjQH+0E-rN_)BR-i zAaj-oc}fsJDm^F`oOj9`-K*v>;nRYrnI}I-_(C;Yyu2KQ3ZH)WD0(+zUR7oaoYii` zHeewj1YnR-0v#Y{@T~gsQ|SjCiUrPlx7Ez+;FGuosc-j3SJA_ct~R#2BEn>22W?AuypKyJseaBCBGV^Ax8Jxa(pdv~Qhz{frXi+7! zx%CP6IWh&ktM-tZe~KZ1cJL=GdoROhfY z3geO;u@stJvZ#%-w1B3jvhEKT1eU*;_fi8@tN=(qJ;}@qvP`<4WM(kccHnAfyEOI$ z)o+YI@3T3UH|rJMtv_}C)^HOtPUB3h#&+#FPY3He))^(X3>>82T;NMHu*&kg^snS{ z>z!=2!Yn9X+!QaPH8AY-_{iPaN@4i1mZ$5Gc4@A@AWIfn?7H@rXxhdlp3lIHf3FQeIS?ZFarnc<8bTQ^L;}iTNH9$in#ZXc95BHRd%&?%X$UqnpmJq@rnLy^WIC8o=czj(O+1SZ$DyPME< z9>NggyVKLgT|b%DsMjSx@tc#TYcbk;oW$%T-LETDz?t-|ndG&iMg3ZmlRl zbhaGk(r2}0J6WLVg3KSS)z=Th3KVDf)tQ@Jj0eYJ25c8e!cMvm@DLFoA~978!Ei`R zao$!1WQT9L8Yoxd2GAYRz^h$R{7e2=xMl9gN@C2L7>R(NFzra)9y5^@9^M)HotcHQ&aTQ+ zL)~%5)(O;ET$0y8Y^$sKo3j#=KM4)50@-_n+8YmE<{?edr~l>uBX#R+tot)gg7%CF zt+F)pa{VxlLjkLl^q)=p@V^bPH7CYcb28qq`9J>ug#U|iqtIxWlr?D?{}YB540-l7 z`X0oOUYq5a2k?h^ z6f9d0D=&Rw-+aDwmv8jhfJa^PCGp9sRgk+kk$+O@T{5=F;-#c}9ZhHg$FdtQkP0L zzyr7T9-B|m%9lx&l>azrL7uw|54^Lov)F+7!>$`IR{4UpqhORb!S(MmZ8tV%cJCEt zy~l+BeHJJQ0P0|tHYdV~fa?}Tc6=5cjE9~_aWgfV19w$nnXfCxe*`Wv2VUI7P2;NO zzhpOy4`}^P^41{Lc|^E9#TIhn9aqX6ie7`_+G`#;Zjw+gi2!VF=c@8Qtcy%LLaR+N@4F@`i5Od2I`(WWF4 zRU-rRU~XRTo_8b9V@CX-pFf)-aU8jCO;TC((cIEw0&x1u!o>3ebkDQxaUtH5u(wTa z2kjH`RS+2>;frTeY4$1u3?>8VH4>GaBFO&3Q`rn@6<=;Pq7&kO`5>4cuY6BF2T}lq zG5nHbiFQMec@yt3wjaRBWKvz;TBxydNPZHo4<(W9~O2a2m&>4c>+E>q@2|Z`V-3R;ZxB=RCO>lFa>CVDkd&?W@Q< zAKK0@;yeU8O+_*`>a>dSj}yPyAc8&s8vc8=$CY_BBOfR7(rV!7xn*MdIZBoFJ@Mjd zB>FF}6@qZMWwhg;8M%dVBS{4oTC2WVH8&e}QxUj+?mqZy!l~fm5At-XD*Z<2X%ND<`re(%?mQ~5VjkoePue9JQ>2W@ljA+cOseiE;c$IAhu_bA z|He>(Q_4uA+JT`y+DsEw&2Ok6;=wf|P#Z#29^b=2Ivy~6jyl5;nepMhvAN8f`UM2# zfd&>)Iv@to)Ym`h4Z%!IKyIu=BzE>JGf=I^@QjRORt`tojw!ozn4Q?dIFz6r_TJn! zx$Kf_^t?fGGvk;Q12lqTz^h>X6Rt3(bjAl~7yW=1n!WMoh)TJ4S~ADlZoxR*5&(M2 zLw;8TxSeUrv-ATU16LvnD1zK35U%qaDjuBAi2Ph;6bxk>e3k>6#j%DY#M z0!@`zP;huWEwCRuI=vt?jZGBaGVf^gnMt{ON*ey5SdqLeoK!3K+WJjSlt*~N*QsaU zg#ZtO{68*c80!YJldUcFbk+cQw@FnpY+}T@8eT|2(9ONR=U556jkKZa&>q08W=*LSVESR z7r>{5MLDS8i|6H*cuNBnB2udnEP8WaSy0%z!C3o$8*O`ELy#(H>@07Zd$Bk8g$ zg@#kzd2PU=l^(C0IVx!XKq-CwzdzAj@-I8PFxNH?e%YsI4ZhVFV2v+DU?wH6LJ10l z7)oA=r#J;U1|NmMDX#)75i$#y$7D)x-%$Wu;4Q8j*BFen)jWEiO(=0n^)8#TOcyrZ zqIHFcjALI>Nky0AbZNlC(Udwd&Ch%tqVdK70i_-nK2AFIW^%=wUJ z-lfdwo!#=*Ef&hMh31oQ^Orj?Zy?eHo6@z-bnd3STYJ$0Jdd`z`w#vChE*`zo-=A^ zJ0^k12D^3np+|UONmz(=&JY9;zO~wKGbZFc?t{QU1)GKc{+;{MKyix{!{ga!c9zDe z{Jx24?aPe3?)%5z{D5bU9l$-7DWz%L`2f#fzXw#iC=2>L`=!BI@o~u3Whar55JAR5ZJ%6SFXFkYQ5Zk4Ob!3xT7L;PhRPo^Y98-XlqQ?-qd)CtRi;B`tJn zn&1_$(#KRtMY)Y?=LoT$#O^wn@W!PShBz?w$Mj8%w!Na0!P=&A{S7r>lC#A|tF;0W zYG118sa=A=eEd1dVKp$E{CcK>@c0Sr8ot$<>GNiVwzm(uL>~co9s@7iMCJK>i+fph z2$0-1zt`DM61&d05I)>Qasw8UM@P7t&<=_Ir98(E_q-W=j!;jIH*BAt@jA2uOr710 zz`^xNwD^%QP#TeSXNU=7-f4O{Vr8T%K@7CR@EEVYs^1x%-^9a1Ufmc;Z2~5vi%n40 zl%e$dgq;=Wv3Exw4AoH7ir)dW#`PPdV~KAo%a}+gyW1#gGd2?xpjW~N-v%i|?}OQP zc?4%gFEc`W0d;|%8czs@YQ0HbH`x0m9v4pWnyvQ}l=s6%x}H9b-SG!cj0CDTmmL9( zUXsMFyT4eHG(+PYg~LrqO0cTmk-ToGX{^Xd5Z9Ymm?#Lw$sv;0kWSu9{Yp^Wg-Oj) z0)0l}(vuv|&Yb4Y$r+O2VtGt{JMeb47-CV-gU^kWTawH-zu#>m#O!vfIevE`BPL3)u5Zw67UOkQB^JV)7-L*hE?#C!G4 zqOWZ?K@6RdgwV;vjq-JUFV1Vc9vL#Ei<#feF*0Ro9FOz56M2fLCljLI5Wl?__rwqX zZCpJ{M88O1W-P$?C9h7#!)mSu_^`=0HGAcg+Nann=duN(vS`A>Mt;xF$1hN^Gc1UC z;}AK^L7tfs(m6cqMkxb3Mu`@NTB+Pg3TO?l8NVTuQ^|hSJI~BXFXp03j|?4L?u*FE zbuW*+HF_h!iONAOm0hHK`MZ%<-qPwPma%@-8W7POWzeJ2(%{)@3(%a((RfL3arcQ! z6{Rv1hcXNkfjru1hlT_1ZhKK&J^5^V=;wncQ;GXSQf+lfUYB?Ci3#>Rq92%jUjA{S*J1e1UQ68PTzXNI37WJIMf<2>Rz*Hr^m`F#n`kp0gMVJ`EneANQ&)CUxB|J*nLHqDfc{44-A%$4si=oyPrY7{AIH}v^k9K6&vs*wGvV2xh~lo zp7o)#wr6r~aiZSchLPTUN*hB2If+pGJ^8(wca z!B2>?!d_iV%;aB<7=!Tvi4>YTTvj~Ri$h5y1ue}ev7O*TitE0_?BW-RG3MD?Zp=}yd)9i z8E0k6SO~Qry>)oh!!vA&Z>M&0s>tzA%uK>iKk409meM2gHSR07;sPLh@4Y$Qu~ zP&7rN@<1Rn?{i)!j2*Uh?-lY3qrs>16bj~|nXz*efJ_Sv2uy!xDrU)yPqVyHB{?(r zxtGABy(pvPIV2PZ9t6iu_@(;)lp#uu z|EB@*|Bs>aGi74!rc>*`S#T6-k<8Yr&tL-`SPqvaVDEP>zpx5)6uSP6gfIOY2^UtW zjWk&YJ|+Osq!k#w06$@w#4}$}p?6zC@6r{2U17HW_tD(rt=u;Cw%?`b$hI$lY#qIY zzv`$Q$%i6Z%{Iv8_j%pBx2okIa~2anGf}%fCV`}nwK-rfI%hN= z!r~SJt}z`hB3p)RCrhoTLg-Pr@D)@x)bl8T1rT;HqL=XZeX?H0zj@Fm<2{i!i)BB zIT@&3L;sy@gm81*CV&X)G`Q=gSjyG_a!IWfb+2Mxp%^2z`J>+WfoG#o#c-~AambVL z3h{fj`XcQJ;+J>*ZHFIsZ}Y4{A9*}V5Wa9oX<@LYL)4$E@@*vegB5D62_#p)nyoR4 zO_5?_tP5)bIa@AqN$#!iU{gs${S2`K9(up|L}H?4!+6ai@Ah~J1WlaJ5vM?(5@-+F zUwBFhvO>|H0d!_ySj;cR>^}l$v27n#~s5CQ5W`(j9)*pAGT#GU8 zQOBDE$T;5`as7pNRNqk;`vgf)3_uJ5zbP=~TDC>d#MDSn5cT0RLw?!E`mZ~H4UW>2zzWN&5u0SjnBS&wWmb$@h>D0JchX4U z;j%urV9aONAvon*iPcVTX6EnHdMeuur`&h+Z&){bt4s%>X8ytlRc04a zUQB?Vlrm~D`_;e6XcmC!O4p-K?uFQCFWk8zz#vN08`Oh=rA&if5#wiAj{EM1(PnFa zi|eYral4Xrz1wnc5VF}7g&x)3efg9XBhY!3O1Oe;0A;JDW(k|xdqc_~#XWU5Qd9TFTKM;yXsI>9Jv21DbS%cm2) zjlrbMgnn}FWDWQ~3)P5&R%8}F)ucsqyUcoWF+t6b=6prpWRr zpMrvL%6hS`qvV8@qZa=}4{`6Y$rduCy%#^p@D6J%mxGzGc8G)HG{?VzcTulw_6Xvs zG?D+-`yO)Kzklm~>~BF*a8F(_KYB+9Os1T?@UP!>6>9@RiK{y8!4Ih zDjun3fpqrBkJ#oYF50E+WBKs6V!90{SQQL)2*YK(yxp}IRT zvHV>XJXWkGOE7*Dvzd!6(vfC`ITZ$}H4t>@D0{yuzG2@c)L9r@Gn&jzMz(;-E(m#oCyM36cQkLL85Z`!RmG1)}gs%P+GJMqdwdx9b8Wn|UMKW_owBNWSvV9c@E=;_fZ4Rbu6` zoAAxvCj^RR**iK0`F^BINsQCUk4S!LA~^kaUIo0m;Y}Rlt3KE?m1>IDEuV z>3L+htJO}AtaYj{+Q6h@+Isj64G`st{S40vJZU&)&}@C3=yOIFsYhhx})wr-oP{r|JP5R_(GgF8k zV+jh5kV+utsg)>_X+7`V7Ficoq&Brm*|MEXTcKu7!pm1#x{XGnWj`=K$W{GG+&cWu zkI0Rgn1M)TE45i(m1N|wMPnc1*W&cB1F}K!dRAaz>gwuii`LhK!RuW{5L|3f+r`>ad!8+ z9%k%Kba~MrcIjdVHojmF*SXJnoRs=qG$)A{$?;&+Dd9G17Y9wLxdB&>r=)ap=vK>w z(UwB9DS>*1*L!8?K(CRgsZkGF2Zn&Wx<0j_xov`~x?)V1>Bw`@c&@X_Ihh&r93!EZ zQOWyO?a9S!qR1;%a*jAR)olvs!Q`M7Ns@AuU7U;9aXLkM->8Up=`dllPT+oRjA~mXh^E4Q!;g23J z0)3_;Z|3<4cdA-~`V|!{lek2k3%|2cx)s>>;GKhGJqok)reiHQ)oo#FLv5C3(Y=dKJQBxc_zZ=XaJu{rC_s zE8=I2iB@PM%aYxXt2uYZ9905Cn?h2K)TVakj0}<{ScXEFsLvjo<0mg+WRgeeZWI*{ zaH#XUoF{j63%yJ(*a_?{$^NkS*%zDX!(?z>0Dch77)~bFkJ0BrMc4l?78UjG!@DS- zB{MJ|Ps0CiOV;*+!q{$PP}=$8Oe?Q(?7dfc>sPL;4H54h!&8byb4z##?W6lS^|+>Op1}STx(WKWXnLHlFg4c@C7rZc{3DyKRl^H*2EUjz~O(A9Di< z_C+C|V(P6I!d)!@ExA2RN(sbBCY@BJw{aDK^yN7~fa=yQbxI@xTBO|hb`?1FweJFx zU>jKcbHiu%x8|pB!5M4@I$PRt@F#cQU%R`O5!zUID6)a&=yyr#|D_inlkODvxzYtN zuth6;sPwElbcZ{pAFxQy!@p5&uDP5 z*+G}NMjk<*+#{t4<5Z;GKYXNyym|-`T#uF-o8()Di_osOrU2 z$6;t_=ny8rrOsamzNrgPiDNiqj0naoq9-BSo3c)uR(}P2!^y{(;>5<^&3?54Wgl0q z6n(&&R_bJcQ75tn))z4w8&wiH-VVSX*atz#K(CYW8R$UyW5L|PM~%h%rxTV645}G| zl`?Rk&hAu!OG_T3EDFdqB*0&QlXQhWyyS#+46Xub0`!T<)r#G;N<2kXBHiZ_ia!4G zyHHsjEv(K|qiYec8V5LX5IB-qw;32fO0zl{_wDGKhlw1_+1u8S5X=hvgw`qT=%qdl ztk`p$1G-YGFci48WNl6u9hSgKmMZLEu3S+tlEW?4>OJ7Na1Kp>op=gpq+so!t-upIBkQ1#68XXCbGF2IEOGPU=|b* zXy5nUhiKRwfrVafPME`gNa8CTOCN+&ak&$0*SXpVR9%XgmWQerC6x!tAi&7fKJYny zIP{eB6QHJVv~6{j82v4S3mDr-(>*mXlJ)^R%#{W24rgY$_vy0ufU^cX6Is|~6~$S` zGiU#&9=3g-bCLA-xSLiMhN~N_mqS_hKt1|C80I{f(IQ&mwNDPvcQ``LGB0E2Tto6r z!WQ7XC?!4?yoxEjS;|8ZCzhnbXp{R8WED}s8c$fYZlJ&#yv}lrR_x5crI5alEU9A=s+V`Nv@erkzlUmJfYP}U_>yKn0bw@jIC4-j$v%@L${y& z*81WwcWh%FJ9wx)xeoX0NnGjRVN%MKC(v!@RbNp;W!N0{JVA-8q=91M0 ztY6Gjh3nLi#~>G9Pxcxo>F-Qv6*2b@{){e>$ho!TYUAR-MC@PNFYLtiH>hm18~`(0G%wZ`+g+5VK5i`DuH#?y#aEVDY%(uwo>*MwCva;RPT00}MlBTR*Pk1+ z3Hq_EF<*?*u{P5X=LfRs@LKQG&6;XXm$`205mD*7Ee*O_HfMVuk9g~c)0XO|ZR|8M zR4+p48JC*RbX=VT&SoL~*c0u^t9`zg@wyGBZMpzsrtb}%w!V11wbNvPeKC8pWiY$7 zbnu{F`NZvR@k+WC*70%bnQ)v+Aa?0XE{fNuffst4N_*h!AL(@|XE?49pu7qkif{pO zoe-IZRxh`)CYH^xXY~!>R@)5pKCg2LzF>Bd1+sr5&CZ8MLj1^GEKiOHrhS>Cz+E8$ zixZ85#Bo8^9qh*N_ek##5Q!AvAv|LnuWI$i??FJL;><#4dBf|Zn? zt4v0~j6coCkLYHAZhGr5$B>if7|u;O`GZti?+^HJSqhXB;D3QDLMd8P^Z zOhuQcBLJB{f(Nwdk~jMrf%<{debL2GPDRSI&Mx}8z3(2;SjbZQPw~}|Z>E=grb8rr zeArH6B3>f*Xeu^0ofkCw=y$TBIB~=dq2}0bblf82*;pgXKZJiJ8`M3uUJEvTuc+J2 zN{Jzb0KN<%%J+2st#FDo;=H+h0T!Iqh&g=#zp{7yw9_#9#jY=jviXRzHTtYFJ<3AG zrIZs*B1-wTr7B>x6N=t)y~Viy8#~Lb*rM!WtEqHn(eJJO6XNSd3A@FdG5u>-p2KO? zSE_hZv*57mjU z{?R5xq6v`oL-%XN3|yX%wVyVLU{l%Qi9qe64I$x;C%;V;NRNU%S@Dl8I9b{_V8t@- z<)}R|@zdK~yOi|06#*H_O|(P&uYP=cc*f2of{XxS4~Ubtw6l_`{xCzRgaKND1s*cy z9Bj5QV|gv0$mb+KZpV0D%nrlNxH*Dxv(Y&TTVwKfhWiZdrZRahc|{Gf26D{>o9{|b zetccrV@~h5j9Stii!duqWC9cKyxT;E;b?Gd~sLwc}UN>8tfo;W=6Wm&JV zZlT3BukEdU!n4=Hoa$T_JEuN{zX#wXA)OQtNDgB26{;b#|YIN=etBdfkw5L}O)@W|vW1Nd-T;bD|WW&1+ z5<8d69*Mglr5UwNYKC(7r_q>zMdWzkPD{rMCj~uu`rj6$s z%f*O0L4^m)z zvs9W|dj=5=YClbrvKxm!Nns;E0OxiDeN#vg!|?r+C!DuV!xthxn+Fe}lQ_T_i>lg~ zo51@cun23Ih>aBL0M4aqfqF_dp-encTwaRx=C8jAkA}%jD;ByfFPTeH4eJd*rl8YHcLb!O<>$=i>hGr8F z|0~k{-mg(t@KmK{yB$C1PSPTWdYa{8Uc$9!yO>>+Ef$2l+1B&B^@1o~bhKA#grxC{ zQ14wr<|Xr8%d25j@tKweBQaytY>;?5%!;kv>}Tj_n4 zB$MWe_j#7hn~E*}15ZAun5Qg9p!5;aUKRW}EO3;zsW3-f@sN>>&efVqmYN0^^-p26_O-TgXkF8LXJipI>1B({ND z7Aw3l1^I)iN(*z=JZ|Vo!zT)KeXw9>pEpX=b}_B$f=pXnqu`&scYpq%{m5Hv*K)x| zza1q%d;EL+GhIq^%BT3`e;%hq&VCUhRY&PxtLl2~ZxM4aCQ#((iCdqF^3VEJRx=q~@j6 z#9=jOmJ8qzOn zN9s+x32|&)FSZIEv0v!W+Ts?pZDe|1P~E#IN#uz!P=7l6jFkn)(>27T%+KpKc}swa zbOEs8z3xUCxHZoFYW%vo<@qO!r++@69#viYAAa3j@A~ARDNI`TXP0S2_iSg){h(j$ z%5yUE8!ZOw60IOI6U9<^c-H8*)8L(5j6^x|ppg#d(wlQwLz;@F-W*s&Vz-deDwH^u zmtV%^cO1i74)5X#PW@~h-&`^wil|hBu9}_`YZ4L-$W?1QMfkvpWomqnTohuLaBhC> ztKv0871Cqz8;zA%^XDAXV9eFo8a=sTlfZ@;0l;ia`sy|v-dx8=c{dltE6Uy)M(7!0 z)46zn%jZlqn!&v9%#YPrmpETa(ikU_x9Lj0!$Rglp5Mm! z_>x>p+e49@21e@+kQtrY(m_RXf1=q_=5ZNvoMt#Mk#g~3QbWw}OPJ3?n{j$y0B*9` zBV3_tcDDCP-di$w$c(b5jz*d3<4mnL$@s5e)UgM)MdEnk|8{-+Z&AGbPC=#aBvL}A zJLuVgyJ+o_!^J*bd4s#K=dAz9_c6fgL1hA0b!fC%-ecgkIOE&C>s=qg`VQP4b94s& zDMnXkO^69rAZ$pY72XC$nhhXAySGo^U;T?&k3*y|!smAH`F(y5IIsf>3n2-D9{Od^ ztEqqSvR?P}Ow%rD$-wS8vv^qy{2ygF^z40Mw@=Yt-?{9T;2Umatd{AlPII~OnQqI_RxY$iB zW%|>4`sW8}&l%d>^kH{HzuFtz?N3>i2f}~wW0d&MT?Oo#3U$3B`}QLTKip{IPTNoP z`d%+g?^h!rBA$_LJaoA;C5=~eWo!JTSJqTnX8WGTUDU#vm{U6|7qMLS&>=>?9x*-Z zlDdMZLYaPM*ly$7g>c-v&F;TH3P0eXr<@~9FQlZ8I<5{+{7{u*&8Om$5F~1mF1xzWI9)}YJ2rs++t})nW z8s};%5J1FC0v2f|ou&@mS950ZU!)^R5Z26TWoxP2c3byfo`koZ0{K&m2zT)LE0XDd zVC+vRFEvlFcgq3MRM_wgepozEzvREhUpB!^SWvmX6P)s9v$?>!-XUYo4Pu}_3biLd zoH7mG2y^Z8P>u~)uv~=Ugkc|nFCEMbVA37=7^yLKSL>4!>`SJ`v)5ER{~@jWC)qBN z_J=cW;6V?yPijap^xM4%tcaLk&#?*mo_ENKm1yI)Xs?>!;WmFPI^AS7UxE$36j#&_ z;$wif?=CM8Z#R;*Q|W7w%JN1J4oEOP`wiScps`IY-Q=yA>y*|2ZU%V5C&q+bPB`ws z_kZojrf=@(RxKKwz;IZQa@am(o6J3IyTu#cYDN#eL71bs@VG($?SbkTY#}fy!=yzifIPH-VH7bQ&Pov0uAYTYQ+6zZ^ZnSa0K|t za?%|ue^93DP4j4`g5P?-unf3@q}M!^(W)M>v}NeG&Rgk(t+H4%c-cS1aT^{5D`6EG z%rmY`YyX3OaaY3&+eJ*=RE+{n`tI(qGsF7sbKOSnDXs1hR`ADrwVh%17}9qx2 zYqwQk9v_Qs-m=AC-mJFK@_q_sDvs7}a|HRS-%SKUk0t0PV7zT@HxjY>i_4l=?CY{{ z3F8P!y#qOCn9TEqse?ViZX&8t9*;&1{o_X=} zbw}U^;boIv)}SV@wzL@2Iz%9q6%r$JdUso;QKO@qb^-tgb)&gb?6ofzSDc%5= zPUdSB`t>e@yys?<8W8Ah$D5a85|IX=aPyax{b;o1gZN$FG7n;{V10x&%){2-PI3Yx z&tlZVaQ!C458MJH8~pOdy1^dfB)bZiUbp~*pV5ZHaY{E#3}qKA%cKI2lcuKwJ7yTE z;{y3&Rxq~P71^VB0E%<%tt@c}PZ;SbjM zwFHy$Hm)QjJK2jQfjRo(VTw0%RyLUf`i^`JEN7o8u}sO^e%G$;wk>_h$9Or*?Q07V z`p3KjbWgeCZ(oRQakEP#5bq1gy4>^>Gu;*)buY$c-Y(1*W;6E^mK{UeQtRMXQ-kpSgi??u&f08)8$xRk|u0auA^f z^Q%uM2u!xnCe-LL{JL9~qqu^$Ube8^kGnj&2!V$hZoB^px$bhvu|oekg*5tO9;dZj zH8aQf;f2n1xbvn#vj~X+8G$rCFL0^gnJ}Q+JD{&&evPB@>#osSWR4K`{Q8_Z?rJI% zB37zXK3}*%FrGC~=tr z@&%kr&zuI#K7pRX*xCSI(Y@J}`7Zae1sFhUl_nK$0TlS1zchw0a4PhE9ykS=CEXF} zLSwb=7=)l50ktygPRnaKBX|ev(Rbev*D?%2X^kUNARKvV0>SJx`0wT3~f29+v zYFJf(2$Z}(FiLQ5N8t@boM(M@n|r;OvsW~&wt?qA2LhT&??%LVb0!rIv} znx&XGnl_^0{CPofbW6bXNkZ*{g)M$VI{;HR-t+;bW8U`)^96PiseXsiuNm>7b9oC;K zK0okx#@sMR8{u|p>>5BZuLF-AQ)WM9#zJNuHU}tv7WXkAIOaIPG5v#H%?&8yZ)cKt zlH)jm>ON~FK#4U?VD{Rs5|cUgK%Jf^;>~?xxrv!N`EddQ&@2`rw4I$YoP|GD4fAd< z{w_qiclm_udJKYQVj&sH=ppTm8#~tAUpKrvHPL+=tk$6*TLW}`2@AQn$f+*I3n*kz zVZ5ea%vMh-AH|d4V(Er{M!?nwJ3ZTgH%tNv@~eDCxy7EZ5phFv``tAZ1;JOug*_>c z0`46oOy>lH1M!2x9{3eCpBhFNU`lo?ynJ2Hlj2Qe8Q?VobVsEh_r{{5#b7pBeJ%F> zGUto{Ac}#++n;4pQezWFCfVhEpv&dh>B0F zM9l03*6Vk^{aQ4smzJ?b2IX3wQ)cIQ%HWH#jtrh2BVsJU-i<-Lhsc6>&##!TSaTuV0U8cQ}glvzQZf%ZS3c+KHqldqCj&q&NBOjg8lu99Wv2k%=LSpYe$>`_MbhB2Y z0@#g;Z-lsP1$Xo=PAkw{V_WO@uNe{()f4?9oPwUw$SoS{M+ROOr8&aYQRDHEAt0c> z#z3Po@L4Rrie^vCq167T9qE`%O~!x!MQOx>=Q;9iniGznWF4*nN|qnhs#Oc5k*~HpNK4~u+dYv zt@$xlIex_+{0E($>aZmg-h79{k6wlky|Vs^L{ArNDvlsOo9BGaed3hRIGCF9X+#fo z6TE|b|K+{f1^`?0Bu%S6q>U`OUji}h-tZ-WdHk6Z8eHI#%YXJFQZE`QB*y;52VdXe zleM3h^&Davzo}Rb5TObRT&%(LGDK7ZXSe*l*3tm0# zhX1nsnl-*ik8G5w9(9m>`pTL$Wgq#)Rpk85q=tM90M-nV(X;Xt&+@a7_fmc#OL5ib zHd_Q8!pr2*K_uhP`M;-%5qph}yp$+xM8DL6X)l~61I3-h>Y zP?6yOy0ss_<6DC8}()cY34gZRu>t{h-fdcR^TmDLXCY+Z?XbA2@3Hgh!skMZ6 zkwnPSNtv_{H~89WNL-N&!o3IB3oq+0kB(kl&!kkovgWR=kl1-@9IU`-%%8I&Nkw)7 zi5((z-ZazKFj7zeBuV{-(P!cw8+55GsNHB2<{DJa;fr*@f)F3)^&^69fCH!c@d=1D z16?2&hR)pifi&m__`^vWj!|tK$t+A^deI*ml!N!h+KOjj8a&_CjlIb0 zZRQF4C#UWPFqeK=`AGSX@4@>!V-ieE!U47%RW+?(4A}?tJ`I;~VV6;F8-Ra5bQ|0t z8=X0k%&9l0tls`6`9Zr=wJY_&5Nd9y89ErDt3tsU8d_h_jxeuWt`Gbs# zQvE{OJZ0B+2<=;*p9I(R_C+{&5RiX34nji{C(C=vh@7UcB(~fJWZ}y1Nmq}14+ffL z;G>m2yHyize>K!Z?9nwa-?ixFGM{ljXyEAA81iNfCOeVg9(C^DFO&czB*fo^cr_|^ zPjF!L)g1)1;@cqC!Xvo8zxdvAi0Y6n;}mB(Ur@VbFGeDZek79xV{aIZW38imoWBOF zAj3^87Jp$Y2q6etX-IXNbiYodO0P>NT>#&-o>nzX66f~7{~IV>Rc@{MPcOv&i+>LT{%_Y<5F$`n>u;rsPf7%zLh@do z+CL6A)Z1j@4yZlhpAJuaFOZ|r?Nyio4ubk04nkund}sA91i|rt3qh0~w6Lu_yYL?~ z*b>eZV)xE}t9-J@U!7cYW3~Ig)w#LPedIR`p z^TS7xx^=rDy9sj=w#vG$z@8rJ+x-OIZNwu0K;2&{zu~@QwrVS53MwKW*1v4LzM5YZ zzM8GFBE2Z4yzj$=?bA=2?rqE#*9Oj|*KMn_8lprUv!AXOgZtZZ0D5q_Wp#c4`V{E%u|16qBV6B7a z1@M_wJ5-YT1`N0J0{quqy2|Cr9#I?a2jifnDPv?eAW`a*+SoT7f&I_`2;ji_j|NVF z`y0ppaTKU#IKgbAZ!Vy_+P}G^CkThowVlfI3A-AaUG`OJ^l_T%fqQqDLYvt8-vE|> z5T)x)$1&L%C(z+=>k7Tvnj~&?=)TT99!#2!J``?CBpZ6cV2Cry60>kD#E>L~gvnU&ZeszYS%9Gn<#^$W{Lf-C$CNL}Cen1_ z4$pq>EG`U?_Zk5&b+_(MTj0*@55s+fj(fnoaV%G%SSuLz8uL9_P_OUXwr*O~_gk;~ zQx|xFYqmN$4HL*}*##X7Ztdcfow>4*G_Ezghqk^c)Xam}N@5aqp>DK~w+ zUn@7(Flx)|B(mT`dEmYReX{?RH?hzHkTW+Dy=6(!2HltZcMhmj@}dsCTo;FIYsW{4 zrEKK-_OvPYK|}D?zOD1TOr~(|qO{VhG2nLzwNGpoGNq1`rQo|iV4^o|4(v<0*I~hL z#ryM$rS(|)K1QI68fGBWY@we7LVn=kx#b0`OD-o8if97+3&soh@24+5Q-{3WK*CXFY-@JNO+M7y`!BGlDpencq~@lEqF z23{a2+fQlPD%GzQcsEgy*E>{iD!Z~OUm8j?%yw`aRR2h8zYK?EMvrZdIRGB>`jk~= zn{}NVy8lQ=Vu$)r(k3`?KA)te26?-puk((>@`w~uIjSv*Z?QNi#G0wU0+c=h%4iY2MoOg1Y!>YRc zyxixM*1^%a$TLpBPD2V03`m##0Gj_=iHZbd^voI*R%QuxrBV>AimC4pv~NGXNiksv z|9tj&)*qxNq*MIKr?)o78nG}|g*cG7rb5ZR4p!#Ok+1TXSlj^brA0r8>F$bgx@i2P z!PBj({XV-wVedvt+ZKf>*C#=IR(+QdOVW1D2JH^>HdV#glu{jsejH)lsm0}Rw;31g za@1!UAe7aJE^=~FHeVH}TXV|D>%GY3KNi6%ay;E!d$2bq;gEJU`wHZ*>^i1GnwWX# z2K(h;oSS~D!iH=Uj3&DF$DEX!S(tSr^d=%6^>fm#@1=__hh@jpOD9e;G^mrT$&kGt zShgr664ckBa!Hcl6b$4k*%TB>QVBx|UHe<+TC^wh=yWmUJcnfpv39O60W>q`pak<+ ze6!mozEEa&S&(1$fn&SCiUj311ROlS5~3owUJeT)cNLC3*1`HiM*PQC)D!5sykQMA z2P}D6b#l(+UY(SnYR&a=P}Nu^&~f??S5tz6*fz*^;n336%l2FfWwgL!EG|ZD2P8aP z(IF!n2z)rQ`D36i4jUQT&}3lK{0NT|ma8&?uPY5C#-AetL3=Ax;15QVK<>*p_2O+h zm2!a`2c&Xp%nA+t^NCNMdoS>3KHEAu46$XM5N*JXX;Doe@cSA9T zBFF2buG9T*{(46IAU)Zs4=abC z?L&u+Z1=47FGnd%C+~+ z-7-jbHwZYw07KV+zyK3?3i;@!KS|ycXw4l&x)ol z=bpFd%V+Ztc1xVTPjuq<5b(9S&tPx%qWseV|AMjx`{YMj?PN`ORvM=Krr0Brk!a1jkU_W6}Pl zJe%l*U_S`kZC%4;Q-W?s0a)4=65o79DO(_zk>xC#UjN^H27Qo(e{Aj z;iU{(GW3ye+q9Z$8s2h_{9;AXW@wu4kNTit9okTFHJ(4~t+!GXsj(sef_6`Z%qmFB zVTwsvS=2&CG4SjJ&b$2Uv7QEA{yi_U{ROn@fO^JCMJNoy&e}4iSy5}4>QYDZOKWI{ zR>)iEbx_l<93HB5o=)}Hl$+#VeR?j@7s4xRgXo3^WsV_=5#UTry}rh?4^u6LT`{3$ z3~!FM7VIoMx6=vEy)6fn(7KxJ01U5a$FJ4QYGbN%N+G7B1{qo_S8)ad^@Li`;W;Sp z%kNpGsfZj1Q$h#ON1go2tETBnBpt8$Q4ky%CcN*lui?|xBzfA~*78pJvOtad ziM=L3o^1KlWi8q>WzeO-wpl(CFW1$6jpm`!82-Ayo2DK=GBstld4(-;K`!0Ab3=z% z5pCe1pHCPK<`NTEIo7|vIA6GQiSeU_=j32@4c1{r-+kYAwzBfMiqUsjOgsRpjS1m* zR^dQ*cNxq-Het>Lqe8f3^_2CVXGATAbv;-YQ#o0R9aBc4|}s0t-+%Q5oKq9QxM9mkhX< zxIfolUe0b&(ngtOnbE@zkT9ihQ$EUvHXpjcoP%y31 z{@@DENU!~^F)=iRy9FZ7`}X=vK1JS7#-kE2jbEFMhe03r%Ie zJW%ANGOlPXF9{H|zMe9jx5lyZe(<2jdU!}e%`OX?Yu0Kl8>-yNDUPTMEuhA=!L6O7 znUIB)R~{sbDc7s*1uCipR7E`0iu=Z3!B@wsEsHFNQew!nJrN`rWH&U~A@XDhmjaR# z2kx_YpIW>udU`e%4Z_ur*;s9VCYod9jY4xU6__>w-AS(0zYX&LSa)K^&bUmbyc>F{ zNX#w6eikwok%~+8I)ZnTqXSNKM11`O7TVG%vP<<;ZaAiF(FLzgdvT+Iox%5{aCbt` zw$X_M9_MXG5#~oUZbnSw5#P*q(E7muX;%C~-Ii~sF$&KZ*B(|8?XY=GlTEJqN&`qL zz-R^FOoDme07&az>V-U!LV!{Ja*qN+%X30TqsZbBFbA+DIyuU-wz&a%(chG>E_SFe~Kc9qwWABlQ` zDSP^IR;&mE`#4z*qLes>QJYmST?)j!v%hpe-e^XR*)!M$e-x3G-=&_JOL`h%LjGY5P0$p*oWwo!q`^20O-8z{I|K6XrEGvn* z1&ZPI3wf}BEu6)?jimRzeYE(+^xm>U=CJQ=Vf@jf8r=8~bDbe^`JCb_GfUjtO(#!p zl8&|OfzjjO9^Hu{5R>wvkEXXp$Zz&Tn}jdg#?NI^X}MV`ndxb?2OhaS>n6VyzPZ?k zzus#FG7^l?#;z;D`8SuGoDaKtZ$AWRoVB$($$FUr1 zGr0xc|5;&rbN>42O;<0}Ra z^JI-q+||n8!Cv8sIF9R*Y8AZ{4Mys)hzz92z#}pxzYh^ zA3c%u7+%3f;wJX;;Pni!rgHz>N1#G`JIYSxsA;PZl4CMiZYN_9b9P9UHQ6ZUNC z>wIH_GL&TB!mqJSvD%Qkb#`iQK+xT7OPjurIHgn*i+Y+qcWExLwQ#w=c8gK%4c^Be zvhrMnP)UwgQ{2m3Th~FFTUq^Z0dfkTofRVX&>>W}!Y>PXL*wcX-HzLy7V){MziZ~< zSQ+0~&z${BKNti_MZNF>z=$nDcW4o7uuN{Zkh>{Hq?roVLKgHmM|RG1nS}(ai$2je zH*P0A?GYiPU*r7ot>n@E{Lz(FdefnjJ6`K?dZ3R0@`Y1pgvQmy$U7zFZdExOoyKs2 zaLiKBd(x&&e(TF)giGHf!wNd?>Bi}&&UGN*VjI&%Cb6h#g^J^rEv>gKOl($NH4ybu zOfA3K(xskPnN9Z|<2A$AEXc?$RDg)zFAjlRtICi^^>poIAgnRW{nHzrG_7O&Uzk=` zVjUY7faS9m4d&f>J2QfBjxnT$4g*sa3EFfWk zWmU|XNm9gmyw0Vl7Y(osOoDiVpu z4wwk~m9eu23|sbdyZYwHn?vI$Flg=sD6sLU*u=Tgk1DVQ5R~^)0aq%Ok)v$h@G@p~ zlTwJesxNmwoseI3fFqT4fWv%V2_9-b)jUh^1^_|xHwhqxEa58Kj`h=x5()ne~ORRU1VaRrZ_Q4k`u^&Hh@0H^Z zr(UNRyz^>)=E$R>;|CkJ{Pd7V#VQWYru$hPGQe5}vei$*nT*rkCL*OyEE>oPrJ8_W zV0EhByNOFAUY%3p-5oQW+~u4z&?`zeY5wkqwViH6DpNEb9X_C49RDFwcx&CU!LQ!O zZhjoNPr7yt#g7~UZT{DkD47k#Ke8e^ldunR8QYC$x>xwIxj@TEtEz$to}0FD=H^mefIV5{tD6A z|m{R#8oWYg#)Br9rpD%8ln^+Si!Hq&}xxBi?opaAys%?P{3KE zOu@$by+(~v56sWOFC`W;Z)|g5=m^h}ktV7vf1AQjbMq^6boh(fxJ#z*1(uii6jL%$ za#(CRc{MQvJD;?~YZW@ZvIn*g=6$SB z*ZO`cBqKXHvGs-i);IfT&!e^oKVqjyxS^|E7a%cREdfA4{dvQJh&f3T)k(5mCBceJ zKG$bPhUC8W*8OuA;>_ekFMCJYQ%5fXc7Q!>zUhEWhP`~v(Ta%2S0l$JhuW_f1=u$* z#@wDObM*3pHSAedyitZSEJ?m%mlMCFvvw)WCx_`oPZM+_VwCjc7F5l>QqwrZxM`nT zdm3;xI0?L8{^*YrWfa~%vPustFe$hydiQJ$)R%oDBk>7b@N|N1>kBkD%VPbdYFhSQ z))Da&S|GdY)f2hg8~Q;-iMK|74bsZbQ-S4n<&XyCM)H&_@uGvz7`_WXc;6WPud44K zB31Uk4)G8F$b$b3{LEVfu-|d?n~YFzV)Na1eAy+hm0#&~t-fszEG$KIJxMjo-~pX5 zYo9#WQk#o5)=v_EKd#d@TQS)3t$DCz+1hB>@6lSM6lY*JSPcO&wYfsLi&hIF(c2aS z(vY$aq3p~yN1JaVTLbM)>_3NN5I#Azt(u(1W;GHmCwLg)uO8DZihSJX8dZw-Hg|X{ zrnHb7CCz-9+Bh=`TvgmmAD&%~G_d|7q7+yIvVZUbmnh( z1fJf_ut^IVdprPsR(Ejz(M~?O)#e2QY>U9<(aj8*nJF7sMWMPmdviDhDWRIP;N;qe zC#mH1q>aTcpgw0B7blY-@sR4NIN|4>E(q)m!tuM1Tzxk&vyu>tbrY)t#Y%LN{h=1P z)$`G?8@W0;D~zCPIH@jX!}oh-#W`1|$HI}-P1nJ&(`zpF(cH#B$t^n&-5B8j-){@o z3}L6Yx5LxZO#S%TD17K&^LCXy4+HEHmjte<12;qCBpl)nhBVUbSN908Rzvf}yVn%z zNo$dVd(rF6oGN~sbU9G{>4KzzP$Px&E72fQV>!@E_9?UzvZf22WZkBkTDC&^PUKvK zn9l8qYUkc$Rg)v!>oFhu-P66~bXdIpOiJ1A$9k@P7 z>{Qf@4g&+i8BXYwyJ)JMNmc-nqvGwX&(#4iskf-pIz#4LfR&tLSiwIW9fQU~s5Ti) zAv5x+S7=t7XJ2j@72VTS3!@jSKP_G$o^#j(*4D?ayK0^&?rr1O@hK1*CGzP!gT>_S zWR(Sz-GPEHsgr~VC)1VV^l&X8W)ZhyEQ;Dx0uWJ@CP%zlf>KJlk`?8V#@r#F+BzZa^Cx5`+Cm{ldM9;T20Fld6xj6@?04F=*%&=OT~~qbIWXtu4~a@x>m;&&iH>x+p!h z7G3!}+kely`vr<028H$nD$64upuLAtC5GkA2T*k%30}zlqCe}cFiqt(o^JUDYcuSb z2e*0q&SZ{Hf`;R&O$T|b9pKBAq&TRr9VhBS4q01|YPiu#>9#sNJ6*|0R zSCPwVV%Hlkd&Z}?-4H@B5=QUTib2(i?+vWyeV~lgZ;tagqW&b6y0#A0W1_N$jwVg- z^(4NPXyh&=2go-uIK&O#7=knH%J72*IKNyezjbCae?9^VnCL0ramc>o0w3ASQ8O}p z(;U0D2!J2;PQ&FA%3Kp5S&fvBlIp7eOz*WZyKWduqw?SK5u6PfrQ>XeN+p9N)~)zf zI2!aNon`Q0t09QW;(jrT*xILoa01Vx8)&V@x;L+{7+?1OGRHSq!ZpG|T30_%Lz2wz zK{JWwbJdO&!%5)A0nC(n+*@sqtsWl+&3;9SJq#GN(xFh>=Eh(rOBfQ17SY63{So13 zakmq{H(AVA|Fo=Tg)jcix^V#yDNee*NO2${T)ToP!HD;WBZ%k4 zT10cn)?i3BHQT$hKAgo zm?pnV8>YB98RDn0PBu8dkxR@CQ=&NuuzSzvi-Y}=m#Keb%s>au$K+rdIwf&Y{;g#O zoPJYJ4rNf+XSozHn#A1t!uJ=6VfA=4)%_xzgopuB%>0ILs*(L)=HnSP1Q0`MR;_r` zSKo((fwa&}icc@T^^eWw=doC3XrMSAh?xw8%sxrivC-}chakylh4>32N!Y7g2f67c zPz5`N7hes29n}A|k-e(==>12p$4wEk z3o=Ap0#%Vy4Y@)=J%M-ml$0Rtx$}!&`}d|xV8mrHQWa1`^_4jtW6hZvyLWmV)q%3x z1^uGkB`PnWW~wM73e}IigMx!s*sV``O_C7f2mc{Q%6tVpsc8swe9s0ypO|MWD z{|SG^iW62k)Bp#b5;RfX5BJV_g;=C{qCq%Asly$Ifa6@TBzL-x_bKkP#fj8MT>`x9 zpO1NqFMATRK2+x>1?M(Depl=BCq#n-Bc-hEhNU7ZRs#dX30jXTnQ3?EPVf?lWC#rV z>k>VfN#{R*L@+H~tV7*1wct9{k8nvcCX1-{^)~d3yI9qhI?bY1rZ6xx>nzgwXakHa zd47D7g4|S}NDIxRghdT>TG3-u{jtIsWRG}R&Cd^xtfQ4W`wTlh?~~R$vtPNUr!NB; zK-Q2Et!oe>Zqo#f(DlpYcOCWR>9jnZ}OWsIzV@6P=)^&BR@4D5&F zDt+!Fn=;DSNj~6Pgk!?*bf6AxK;VNf=x^=z(50k_{R{o|o{3Bm>;wbK*a6PSs{(QR z*ebDMRa}1m3PODSyEq{MtT~D@tzu*jI$CE;8wC~Ja*x+v4G@ywsSt{tPUjJ> z2WZQKE?@}RHR8TIZO-jP1C`02B%80>n(mXtv;0Zm6@2MgV1SDEwEW6)04N5s^2>!k(uH_gAi~n$c;+rk$+TeEZaf@rc zP?A(?_2j9TZvXE|k6Ds;zsZW)@X}K`R|O!`Ct_3I{tvY3gz?rzNmcD<^D1wxINe6S zLceLQtvmpsE9w`g3STGvS5Q*3e+T&iMjqTNlmtOwmR%e&jNQIiGZfbL&!)sR{Po1q1;5p+nG=yZff-*VAV7pk_%hN>L#5S+Ylle^HNR^-)*=t~#1S8f zHdS#Y6TDv*Q0DX-on1t}yrj87y;7RTtcF;w5x9{F7maSr%Mv@h`)j$TK)l=Zee9@4 z>j6&sz3G)wnYl|GE!*?7km=Y?rOqn_|D4>G40qaVLF{yyc|k_C*UL2sNRQUWjr1=< z&tXL$e{FKu9=9yKGkWo(P#Orf&v8l}+X>h<01(Q7v9?Hd^TZEm>YQcvdlePY@gx#b zV_(q~Hs%P|^|E|LWeW#IABR};3K*N`+-hgBBXx~bt6+NYR$v=Dk~p%MAwJTOVwa*Y zFFCASmmktV@?j3!iyt*13+A$q3YG``&^|U_xa3gF8#f<6oOVB-%&YyZ5l_d)SVMvl zcmN`$(%j-$`=UEb@&vburLznt5XD_fJBf)3+ANc$wH3ROUN)%Q*Sxe#R-`a`pj!ta zYn1Fy4}KA)o<|LtxSu;1#bj$YXUAWoW+nI`<5g!#^)ui}^#@<}e-^-kK8+o51^zGjqZ!We7-~qa$28FV`Lt=&4 z%xaf8;1$W4xen?KZE_@WRcQo{HWzV8zTE&aR+m?-6iAv#ukk8;#?QmL=41D8l&~4G zjx$NX$tK`4#KiCGARv4jwyH|3@QBniHnYGdt?ipcl(eJLnL=66FTlf6_b1lMe%@{s zNV;jT>Gn5jkqs4-aTj8CDK~^Yj_0eHR#^>e6VST*0WiQLY)b3syWo;RCDQH_ELtrX!n131N`3iA=+ zcI%~;4k86+6I{TDSZF+vmJ^#UN^XH3_KgUSYVv)fTvjD89N1#kKb#bubXXbCf^z)* z^W^`Vchdypzl7&AAe^k;z})|bDM0hMDG(rt@SCSQc^n7N>o-`8lf0Nncc0TVR~QJJ z6t$$cO>`Yh6?2(z8a-%BK%bZQTb2UmXr4-&hyUfJU`_Ti%i++B!Hc$GpC(rMrLEdU zTQ|=!EQNEKl25VFL3-CIRMO?S?Yr`3J7rYpSU#M?^H`7!b)i3>aXjH z2XAS=UWh8_jN!KDkz>TQ033-1dFqKKQ)tqMeQ1=zzBcx66YU zhUnCvbdOW!ler=r^l4?2D|p->7{nv5V@nmXjx7@HtLV3#v-xws?KoB~009I|W4`BT zgWpa0>m2RO3xL@_UlbXJyoTP(Xe>eF1g(6phm z;93Bsi||G{v76s=!gMcL#OM|UG)i)?El)5W69w#A6rf~Zs9;AXSm=Vb&gSqDnsW~r z?w;d`GU2z1Hr%e}EmfK%4+i<7|-k z^cHQrT+-qR=x`sfXZQB~>V)r-Waed2 zdPtRK4kTzvnshEnq|AP6bO1mj=hhg|D#lN7Jo##wNVI36$yEI6FX62$)+A5%l$1I2 zC?O<|wY#(28Os9TKHL~nn!QHBJyGTS3+O>N5G9zJJX(Oek-UGqZ!xR6g^!Mh2;tYD z*aQolG*N$=jz!rWa=pejH9=-A|$ z=j{+!X)$|%9;}vq9(H1W-ujhGJ?sw2V@pDsZ!(%Tse*Q?^*d4&U)sz}&XDu&0}q`w zj#ZN?cAMLJZ}W-=DP7_9IXALR?5dUYERQCX5ce+?W6MmMAtRl{FnOf z!ZPOm_HBO7)=bud@@8l!{lHFmIc{e=XBsI9laBKd(v7)ph&#Z*G(fb6jkGPa{f6fxcp8dom|l=&IN7{!=7!=zUU z%F5z4nUWKF;9^roxjPv$C@kqhL0u0SDR|Bh&ZL)%o* zWd0G+Ko5i>FS?e@6q_R`_W;d~-d_ThrB=UE0zilu7~>l|ccFLk+A-X%x;aB$VxYOq z>q85iaWlB=nK1rB;wUrg_N=%rN*$WylR#x600pmmgyd&$niIHG)2Y?Uz9(HBVZZ)W zFmcIVhmmEU4hRNWSo!qA=qVQREAF$R4dB(AMQ72&PnUxqE0JcBA*8@^TFVuIMG*{@)J;#fkG^xfFQbOwXxwXw!P?XSgQ*O zR1uoD_PKx5@w3AZ&!h=T$_G$`4FHk}_{hCRTy$(`3=2YW)$+4A=*CIM-QF}6A9GOQ z5$cE{fu;>j$GI)V%jNn3kc3h`(rD8P(Hm0Cy5jjA;D7{iwezcaOm3WySa0@0@vO*H zr+vAHQ_-v$-W^TLJeFS+~Q!?DbpMfFh0+!6!fcH&<6nGAzq#X0Z`F3S&GAR)`0tt=b{^L zB(5duAzQWOvtI{Wal2H{PX+M!J_r1oABz<@{yh~fKjJW3S0|~!YYVVz>SDU3z!W@% zCNk0>fX-CDeRf9j;8 zaNxsi!lcB)HqEzbqr|8RU0zN~O=^S42CPqaW^eXuri?kJ!{jO|dkFj*6rd)OT0n_M zqGQGYVA&>23g(u4&(bA%$SMaj7EYJ^E*(088T2KDVtC@D->$V6^$>BDmD?vhHQkEd zZ@XPgDr>C12m&r9JNXD3U7~U6?v}erW1-eRI=N2E?Y@f&4@e=oM0DbF&V?1)Q{kh_ z*H{0PiH5AQeU@!Qv9h~?6bbMbL1H`!4qZ|w*^XMOUKHB$vMqgLpEnq6$)+H3(TMH@ zj*tS$?n$gD1n>=C>$p4Nu>wPGL@0)o;jkXkjvMkhA^+aJ1cz${^cj@BfSVN_z8-2$ vWiE;X3wT)ezaBawNMFW>zyk~E) { const items = Object.keys( TweakValuesShouldMatchedTemplate @@ -64,33 +62,17 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { ); if (!hasOnlyCompatibleLossyMismatches) return undefined; + let autoAcceptCompatibleTweak = this.settings.autoAcceptCompatibleTweak; if (this.settings.autoAcceptCompatibleTweak === undefined) { - if (this._hasNotifiedAutoAcceptCompatibleUndefined) { - return undefined; - } - this._hasNotifiedAutoAcceptCompatibleUndefined = true; - const CHOICE_ENABLE = $msg("TweakMismatchResolve.Action.EnableAutoAcceptCompatible"); - const CHOICE_DISABLE = $msg("TweakMismatchResolve.Action.DisableAutoAcceptCompatible"); - const CHOICES = [CHOICE_ENABLE, CHOICE_DISABLE] as const; - const message = $msg("TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined"); - const ret = await this.core.confirm.askSelectStringDialogue(message, CHOICES, { - title: $msg("TweakMismatchResolve.Title.AutoAcceptCompatible"), - timeout: 0, - defaultAction: CHOICE_ENABLE, - }); - if (ret !== CHOICE_ENABLE) { - return undefined; - } - await this.services.setting.applyPartial( - { - autoAcceptCompatibleTweak: true, - }, - true - ); - Logger("Auto-accept for compatible tweak mismatch has been enabled."); + // Keep the settings object stable: settings panes and an in-flight replication retry can + // retain this reference while the default is persisted. + this.settings.autoAcceptCompatibleTweak = true; + await this.services.setting.saveSettingData(); + autoAcceptCompatibleTweak = true; + Logger("Automatic alignment of compatible chunk settings has been enabled."); } - if (this.settings.autoAcceptCompatibleTweak !== true) return undefined; + if (autoAcceptCompatibleTweak !== true) return undefined; return this._selectNewerTweakSide(current, preferred); } @@ -215,7 +197,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { } else if (rebuildRecommended) { CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]); CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]); - CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [true, true]]); + CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [preferred, true]]); CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]); } else { CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]); @@ -255,9 +237,16 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { return "CHECKAGAIN"; } if (conf) { - this.settings = { ...this.settings, ...conf }; - await this.core.replicator.setPreferredRemoteTweakSettings(this.settings); + // ReplicationService retains the current settings object while it performs the immediate + // CHECKAGAIN retry. Update that object in place so the retry observes the accepted values. + Object.assign(this.settings, extractObject(TweakValuesTemplate, conf)); await this.services.setting.saveSettingData(); + if (!rebuildRequired) { + // The failed replication has settled before mismatch resolution runs. Reinitialise the + // chunk-generation managers now so hash and splitter changes take effect before retrying. + await this.localDatabase.managers.reinitialise(); + } + await this.core.replicator.setPreferredRemoteTweakSettings(this.settings); if (rebuildRequired) { await this.core.rebuilder.$fetchLocal(); } diff --git a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts index 68fc0661..21f773e0 100644 --- a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts +++ b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.unit.spec.ts @@ -1,9 +1,16 @@ import { describe, expect, it, vi } from "vitest"; -import { DEFAULT_SETTINGS, REMOTE_COUCHDB, type RemoteDBSettings, type TweakValues } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { + DEFAULT_SETTINGS, + REMOTE_COUCHDB, + type RemoteDBSettings, + type TweakValues, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { ModuleResolvingMismatchedTweaks } from "./ModuleResolveMismatchedTweaks"; function createModule(settingsOverride: Partial = {}) { - const askSelectStringDialogue = vi.fn(async () => undefined); + const askSelectStringDialogue = vi.fn(async (..._args: unknown[]): Promise => undefined); + const applyPartial = vi.fn(async (_partial: Record): Promise => undefined); + const reinitialise = vi.fn(async () => undefined); const core = { _services: { API: { @@ -15,6 +22,12 @@ function createModule(settingsOverride: Partial = {}) { }, setting: { saveSettingData: vi.fn(async () => undefined), + applyPartial, + }, + }, + localDatabase: { + managers: { + reinitialise, }, }, settings: { @@ -26,6 +39,9 @@ function createModule(settingsOverride: Partial = {}) { askSelectStringDialogue, }, } as any; + applyPartial.mockImplementation(async (partial: Record) => { + core.settings = { ...core.settings, ...partial }; + }); Object.defineProperty(core, "services", { get() { @@ -34,10 +50,35 @@ function createModule(settingsOverride: Partial = {}) { }); const module = new ModuleResolvingMismatchedTweaks(core); - return { module, core, askSelectStringDialogue }; + return { module, core, askSelectStringDialogue, applyPartial, reinitialise }; } describe("ModuleResolvingMismatchedTweaks", () => { + it("should enable and auto-accept compatible mismatches when the preference is undefined", async () => { + const { module, core, askSelectStringDialogue, applyPartial } = createModule({ + autoAcceptCompatibleTweak: undefined, + hashAlg: "xxhash64", + tweakModified: 100, + }); + const initialSettings = core.settings; + + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + tweakModified: 200, + } as Partial; + + const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred); + + expect(conf).toEqual(preferred); + expect(rebuild).toBe(false); + expect(core.settings).toBe(initialSettings); + expect(core.settings.autoAcceptCompatibleTweak).toBe(true); + expect(core._services.setting.saveSettingData).toHaveBeenCalledTimes(1); + expect(applyPartial).not.toHaveBeenCalled(); + expect(askSelectStringDialogue).not.toHaveBeenCalled(); + }); + it("should auto-accept compatible mismatches on connect check using newer remote tweakModified", async () => { const { module, askSelectStringDialogue } = createModule({ autoAcceptCompatibleTweak: true, @@ -58,6 +99,28 @@ describe("ModuleResolvingMismatchedTweaks", () => { expect(askSelectStringDialogue).not.toHaveBeenCalled(); }); + it.each([ + { label: "neither side has a recorded time", currentModified: 0, preferredModified: 0 }, + { label: "the recorded times are equal", currentModified: 200, preferredModified: 200 }, + ])("should use the remote compatible value when $label", async ({ currentModified, preferredModified }) => { + const { module, askSelectStringDialogue } = createModule({ + autoAcceptCompatibleTweak: true, + hashAlg: "xxhash64", + tweakModified: currentModified, + }); + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + tweakModified: preferredModified, + } as Partial; + + const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred); + + expect(conf).toEqual(preferred); + expect(rebuild).toBe(false); + expect(askSelectStringDialogue).not.toHaveBeenCalled(); + }); + it("should fallback to manual confirmation when mismatches are mixed on connect check", async () => { const { module, askSelectStringDialogue } = createModule({ autoAcceptCompatibleTweak: true, @@ -80,6 +143,24 @@ describe("ModuleResolvingMismatchedTweaks", () => { expect(askSelectStringDialogue).toHaveBeenCalledTimes(1); }); + it("should fetch after applying a compatible remote setting when the user selects the rebuild option", async () => { + const { module, askSelectStringDialogue } = createModule({ + autoAcceptCompatibleTweak: false, + hashAlg: "xxhash64", + }); + askSelectStringDialogue.mockResolvedValueOnce("Apply settings to this device, and fetch again"); + + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + } as TweakValues; + + const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred); + + expect(conf).toEqual(preferred); + expect(rebuild).toBe(true); + }); + it("should auto-accept compatible mismatches on remote-config check using newer local tweakModified", async () => { const { module, askSelectStringDialogue } = createModule({ autoAcceptCompatibleTweak: true, @@ -105,4 +186,42 @@ describe("ModuleResolvingMismatchedTweaks", () => { expect(result).toEqual({ result: false, requireFetch: false }); expect(askSelectStringDialogue).not.toHaveBeenCalled(); }); + + it("should apply remote compatible settings in place and reinitialise managers before retrying", async () => { + const { module, core, reinitialise } = createModule({ + autoAcceptCompatibleTweak: true, + hashAlg: "xxhash64", + tweakModified: 100, + }); + const initialSettings = core.settings; + const preferred = { + ...(DEFAULT_SETTINGS as unknown as TweakValues), + hashAlg: "xxhash32", + tweakModified: 200, + } as TweakValues; + const calls: string[] = []; + core._services.tweakValue = { + checkAndAskResolvingMismatched: vi.fn(async () => [preferred, false]), + }; + core._services.setting.saveSettingData = vi.fn(async () => { + calls.push("save"); + }); + core.replicator = { + tweakSettingsMismatched: true, + preferredTweakValue: preferred, + setPreferredRemoteTweakSettings: vi.fn(async () => { + calls.push("set-preferred"); + }), + }; + reinitialise.mockImplementation(async () => { + calls.push("reinitialise"); + }); + + const result = await module._askResolvingMismatchedTweaks(); + + expect(result).toBe("CHECKAGAIN"); + expect(core.settings).toBe(initialSettings); + expect(core.settings.hashAlg).toBe("xxhash32"); + expect(calls).toEqual(["save", "reinitialise", "set-preferred"]); + }); }); diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index 96ee66ef..b7876678 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -206,7 +206,8 @@ export class LiveSyncSetting extends Setting { const setValue = wrapMemo((value: boolean) => { toggle.setValue(opt?.invert ? !value : value); }); - this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] ?? false); + this.invalidateValue = () => + setValue(LiveSyncSetting.env.editingSettings[key] ?? opt?.defaultToggleValue ?? false); this.invalidateValue(); toggle.onChange(async (value) => { diff --git a/src/modules/features/SettingDialogue/PaneAdvanced.ts b/src/modules/features/SettingDialogue/PaneAdvanced.ts index 6c9c85a5..bfa0d02e 100644 --- a/src/modules/features/SettingDialogue/PaneAdvanced.ts +++ b/src/modules/features/SettingDialogue/PaneAdvanced.ts @@ -35,7 +35,9 @@ export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme clampMin: 10, onUpdate: this.onlyOnCouchDB, }); - new Setting(paneEl).setClass("wizardHidden").autoWireToggle("autoAcceptCompatibleTweak"); + new Setting(paneEl) + .setClass("wizardHidden") + .autoWireToggle("autoAcceptCompatibleTweak", { defaultToggleValue: true }); // new Setting(paneEl) // .setClass("wizardHidden") // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) diff --git a/src/modules/features/SettingDialogue/SettingPane.ts b/src/modules/features/SettingDialogue/SettingPane.ts index e34fdfbe..b3cd4e1b 100644 --- a/src/modules/features/SettingDialogue/SettingPane.ts +++ b/src/modules/features/SettingDialogue/SettingPane.ts @@ -75,6 +75,7 @@ export type AutoWireOption = { holdValue?: boolean; isPassword?: boolean; invert?: boolean; + defaultToggleValue?: boolean; onUpdate?: OnUpdateFunc; obsolete?: boolean; }; diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 6fc6d483..e0069e12 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -83,7 +83,7 @@ The underlying `test:e2e:obsidian:` scripts remain available for an im `test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then uses the permanent command to reopen the wizard on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. -`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, and the Setup URI controls. It captures the representative dialogues on desktop and mobile, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that the remote-selection promise settles without an error. These UI-only checks do not apply a remote configuration or contact a remote service. +`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database. `test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then selects the Synchronisation Settings pane and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour. diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts index 2065b7de..c12586e1 100644 --- a/test/e2e-obsidian/scripts/dialog-mounts.ts +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -6,6 +6,7 @@ import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobile import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { captureObsidianDialogue, + captureObsidianElement, captureObsidianPage, obsidianRemoteDebuggingPort, withObsidianPage, @@ -13,6 +14,7 @@ import { import { createTemporaryVault } from "../runner/vault.ts"; const dialogRunStateKey = "__livesyncE2EDialogMount"; +const repairRunStateKey = "__livesyncE2ETroubleshootingRepair"; const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000); type DialogueMode = "desktop" | "mobile"; @@ -20,6 +22,7 @@ type DialogueMode = "desktop" | "mobile"; type DialogueRunState = { done: boolean; error?: string; + expected?: unknown; kind: string; result?: unknown; }; @@ -27,18 +30,39 @@ type DialogueRunState = { type SetupManagerHandle = { constructor: { name: string }; onSelectServer?: (settings: unknown, remoteType: string) => Promise; + _askUseRemoteConfiguration?: (settings: unknown, preferred: unknown) => Promise; + _checkAndAskResolvingMismatchedTweaks?: (preferred: unknown) => Promise; + __addLog?: (message: string) => void; }; type LiveSyncTestPlugin = { core: { + fileHandler: { + createAllChunks(force: boolean): Promise; + }; modules: SetupManagerHandle[]; - settings: unknown; + settings: Record; }; }; +type ObsidianSettingsController = { + open(): void; + openTabById(tabId: string): void; +}; + +type ObsidianVaultFile = { + path: string; +}; + type ObsidianTestApp = { commands?: { executeCommandById(commandId: string): boolean }; plugins?: { plugins: Record }; + setting?: ObsidianSettingsController; + vault?: { + delete(file: ObsidianVaultFile, force: boolean): Promise; + getFiles(): ObsidianVaultFile[]; + read(file: ObsidianVaultFile): Promise; + }; }; type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; @@ -78,7 +102,62 @@ async function openSetupUriDialogue(): Promise { } } -async function assertDialogueRunCompleted(): Promise { +async function openConfigurationMismatchDialogue( + kind: "connected" | "connected-rebuild-recommended" | "remote-configuration" +): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate( + ({ stateKey, kind }) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const resolver = plugin.core.modules.find( + (module) => module.constructor.name === "ModuleResolvingMismatchedTweaks" + ); + if (resolver === undefined) throw new Error("Could not find ModuleResolvingMismatchedTweaks"); + if (kind === "connected-rebuild-recommended") { + plugin.core.settings.autoAcceptCompatibleTweak = false; + } + + const preferred = + kind === "connected-rebuild-recommended" + ? { + ...plugin.core.settings, + hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32", + } + : { + ...plugin.core.settings, + enableCompression: !Boolean(plugin.core.settings.enableCompression), + }; + const state: DialogueRunState = { + kind: `configuration-mismatch-${kind}`, + done: false, + expected: preferred, + }; + (globalThis as unknown as Record)[stateKey] = state; + const operation = + kind === "remote-configuration" + ? resolver._askUseRemoteConfiguration?.(plugin.core.settings, preferred) + : resolver._checkAndAskResolvingMismatchedTweaks?.(preferred); + if (operation === undefined) { + throw new Error(`The configuration mismatch resolver does not support ${kind}.`); + } + void operation.then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, + { stateKey: dialogRunStateKey, kind } + ); + }); +} + +async function assertDialogueRunCompleted(): Promise { const state = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { await page.waitForFunction( (stateKey) => @@ -92,11 +171,12 @@ async function assertDialogueRunCompleted(): Promise { ); }); if (!state) { - throw new Error("The remote selection dialogue did not record its completion state."); + throw new Error("The mounted dialogue did not record its completion state."); } if (state.error) { - throw new Error(`The remote selection dialogue failed: ${state.error}`); + throw new Error(`The mounted dialogue failed: ${state.error}`); } + return state; } async function verifyRemoteSizeNoticeAndDialogue(): Promise<{ @@ -347,6 +427,442 @@ async function verifySetupUriDialogue(mode: DialogueMode): Promise { return screenshotPath; } +async function verifyCompatibleMismatchAutoAdjustment(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const resolver = plugin.core.modules.find( + (module) => module.constructor.name === "ModuleResolvingMismatchedTweaks" + ); + if (typeof resolver?._checkAndAskResolvingMismatchedTweaks !== "function") { + throw new Error("Could not find the configuration mismatch resolver"); + } + plugin.core.settings.autoAcceptCompatibleTweak = undefined; + const currentModified = + typeof plugin.core.settings.tweakModified === "number" ? plugin.core.settings.tweakModified : 0; + const preferred = { + ...plugin.core.settings, + hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32", + tweakModified: currentModified + 1, + }; + const state: DialogueRunState = { + kind: "configuration-mismatch-compatible-auto-adjustment", + done: false, + expected: preferred, + }; + (globalThis as unknown as Record)[stateKey] = state; + void resolver._checkAndAskResolvingMismatchedTweaks(preferred).then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, dialogRunStateKey); + }); + + const state = await assertDialogueRunCompleted(); + if (!Array.isArray(state.result) || state.result.length !== 2) { + throw new Error("The compatible mismatch did not return its settings and rebuild decision."); + } + const [appliedSettings, shouldRebuild] = state.result; + const expectedSettings = state.expected; + if ( + typeof appliedSettings !== "object" || + appliedSettings === null || + typeof expectedSettings !== "object" || + expectedSettings === null || + !("hashAlg" in appliedSettings) || + !("hashAlg" in expectedSettings) || + appliedSettings.hashAlg !== expectedSettings.hashAlg || + shouldRebuild !== false + ) { + throw new Error("The compatible mismatch was not adjusted to the newer setting without a rebuild."); + } + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const autoAcceptEnabled = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.autoAcceptCompatibleTweak; + }); + if (autoAcceptEnabled !== true) { + throw new Error("Compatible mismatch auto-adjustment was not persisted as the default."); + } + for (const title of ["Auto-Accept Available", "Configuration Mismatch Detected"]) { + const dialogue = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: title }), + }); + if ((await dialogue.count()) !== 0) { + throw new Error(`Compatible mismatch auto-adjustment unexpectedly opened '${title}'.`); + } + } + }); +} + +async function verifyCompatibleAlignmentSettingDefault(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const persistedValue = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + return plugin.core.settings.autoAcceptCompatibleTweak; + }); + if (persistedValue !== undefined) { + throw new Error( + `The default-display fixture expected an undefined preference, received ${persistedValue}.` + ); + } + + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Advanced"]').click({ timeout: uiTimeoutMs }); + const settingItem = liveSyncSettings.locator(".setting-item").filter({ + has: page.getByText("Auto-accept compatible tweak mismatches", { exact: true }), + }); + await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const toggle = settingItem.locator(".checkbox-container"); + if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) { + throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined."); + } + }); +} + +async function verifyConfigurationMismatchDialogues(): Promise<{ general: string; fetch: string }> { + await verifyCompatibleMismatchAutoAdjustment(); + await openConfigurationMismatchDialogue("remote-configuration"); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Use Remote Configuration" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Use configured settings", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Dismiss", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + + await openConfigurationMismatchDialogue("connected"); + const generalScreenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-configuration-mismatch-dialogue.png", + async (page) => { + const container = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + const modal = container.locator(".modal").last(); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const action of ["Apply settings to this device", "Update remote database settings"]) { + await modal + .getByRole("button", { name: action, exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await modal.getByRole("button", { name: /Dismiss$/u }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + for (const retiredAction of ["Use configured", "Update with mine"]) { + if ((await modal.getByRole("button", { name: retiredAction, exact: true }).count()) !== 0) { + throw new Error(`The mismatch dialogue still exposes the retired action '${retiredAction}'.`); + } + } + const actions = modal.locator(".setting-item-control").last(); + const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection); + if (flexDirection !== "column") { + throw new Error(`Expected vertically stacked mismatch actions, received ${flexDirection}.`); + } + return modal; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + await modal.getByRole("button", { name: /Dismiss$/u }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + + await openConfigurationMismatchDialogue("connected-rebuild-recommended"); + const fetchScreenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-configuration-mismatch-fetch-dialogue.png", + async (page) => { + const container = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + const modal = container.locator(".modal").last(); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + return modal; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + await modal + .getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true }) + .click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + const fetchResult = await assertDialogueRunCompleted(); + if (!Array.isArray(fetchResult.result) || fetchResult.result.length !== 2) { + throw new Error("The configuration-mismatch Fetch action did not return its settings and Fetch decision."); + } + const [appliedSettings, shouldFetch] = fetchResult.result; + const expectedSettings = fetchResult.expected; + if ( + typeof appliedSettings !== "object" || + appliedSettings === null || + typeof expectedSettings !== "object" || + expectedSettings === null || + !("hashAlg" in appliedSettings) || + !("hashAlg" in expectedSettings) || + appliedSettings.hashAlg !== expectedSettings.hashAlg || + shouldFetch !== true + ) { + throw new Error("The configuration-mismatch Fetch action did not apply the remote setting before Fetch."); + } + + return { general: generalScreenshotPath, fetch: fetchScreenshotPath }; +} + +async function executeRegisteredCommand(commandId: string): Promise { + const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + return await page.evaluate( + (id) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(id) === true, + commandId + ); + }); + if (!opened) { + throw new Error(`The command was not registered or could not be executed: ${commandId}`); + } +} + +async function verifyLogAndReportSurfaces(): Promise<{ log: string; report: string }> { + await executeRegisteredCommand("obsidian-livesync:view-log"); + const logScreenshot = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-show-log.png", + async (page) => { + const logPane = page.locator(".logpane"); + await logPane.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const label of ["Wrap", "Auto scroll", "Pause"]) { + await logPane.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await logPane.getByRole("button", { name: "Close", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + return logPane; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const logPane = page.locator(".logpane"); + await logPane.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs }); + await logPane.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + await executeRegisteredCommand("obsidian-livesync:dump-debug-info"); + const reportScreenshot = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-full-report.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + hasText: "Your Debug info is ready to be copied", + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const report = await modal.locator("textarea").inputValue({ timeout: uiTimeoutMs }); + if (!report.includes("# ---- Debug Info Dump ----")) { + throw new Error("The full-report dialogue did not contain the generated debug report."); + } + await modal.getByRole("button", { name: "OK", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + return modal.locator(".modal").last(); + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + hasText: "Your Debug info is ready to be copied", + }); + await modal.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + return { log: logScreenshot, report: reportScreenshot }; +} + +async function verifyHatchSurfacesAndSafeActions(): Promise { + const screenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-hatch.png", + async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + for (const label of [ + "Write logs into the file", + "Recreate missing chunks for all files", + "Verify and repair all files", + ]) { + await liveSyncSettings.locator(".setting-item-name", { hasText: label }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings + .locator(".setting-item-name", { hasText: "Recreate missing chunks for all files" }) + .scrollIntoViewIfNeeded(); + return liveSyncSettings; + } + ); + + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const liveSyncSettings = page.locator(".sls-setting"); + const logSetting = liveSyncSettings.locator(".setting-item").filter({ + has: page.getByText("Write logs into the file", { exact: true }), + }); + await logSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.writeLogToTheFile === true; + }, + undefined, + { timeout: uiTimeoutMs } + ); + + const persistentLogMarker = "E2E persistent troubleshooting log"; + await page.evaluate((marker) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const module = plugin.core.modules.find((candidate) => candidate.constructor.name === "ModuleLog"); + if (typeof module?.__addLog !== "function") throw new Error("Could not find ModuleLog"); + module.__addLog(marker); + }, persistentLogMarker); + await page.waitForFunction( + async (marker) => { + const vault = (globalThis as ObsidianTestGlobal).app?.vault; + if (vault === undefined) return false; + const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_")); + if (logFile === undefined) return false; + return (await vault.read(logFile)).includes(marker); + }, + persistentLogMarker, + { timeout: uiTimeoutMs } + ); + + // Saving a toggle refreshes the settings pane. Resolve the visible control again so the + // second action does not target the detached pre-save element. + const refreshedLogToggle = page + .locator(".sls-setting:visible .setting-item:visible") + .filter({ + has: page.getByText("Write logs into the file", { exact: true }), + }) + .locator(".checkbox-container:visible") + .last(); + await refreshedLogToggle.click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.writeLogToTheFile === false; + }, + undefined, + { timeout: uiTimeoutMs } + ); + await page.evaluate(async () => { + const vault = (globalThis as ObsidianTestGlobal).app?.vault; + if (vault === undefined) throw new Error("Obsidian Vault is unavailable"); + const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_")); + if (logFile === undefined) throw new Error("The persistent troubleshooting log was not created"); + await vault.delete(logFile, true); + }); + await page.waitForFunction( + () => + !(globalThis as ObsidianTestGlobal).app?.vault + ?.getFiles() + .some((file) => file.path.startsWith("livesync_log_")), + undefined, + { timeout: uiTimeoutMs } + ); + + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const original = plugin.core.fileHandler.createAllChunks.bind(plugin.core.fileHandler); + const state: DialogueRunState = { kind: "recreate-missing-chunks", done: false }; + (globalThis as unknown as Record)[stateKey] = state; + plugin.core.fileHandler.createAllChunks = async (force) => { + try { + state.result = await original(force); + } catch (error) { + state.error = error instanceof Error ? error.message : String(error); + } finally { + state.done = true; + plugin.core.fileHandler.createAllChunks = original; + } + }; + }, repairRunStateKey); + await liveSyncSettings.getByRole("button", { name: "Recreate all", exact: true }).click({ + timeout: uiTimeoutMs, + }); + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[stateKey]?.done === true, + repairRunStateKey, + { timeout: uiTimeoutMs } + ); + const repairState = await page.evaluate( + (stateKey) => (globalThis as unknown as Record)[stateKey], + repairRunStateKey + ); + if (repairState?.error) { + throw new Error(`Recreate missing chunks failed: ${repairState.error}`); + } + + await liveSyncSettings.getByRole("button", { name: "Verify all", exact: true }).click({ + timeout: uiTimeoutMs, + }); + await page + .locator(".notice") + .filter({ hasText: /^done$/u }) + .waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + }); + + return screenshotPath; +} + async function verifyMobileStartupReviews(): Promise<{ compatibilityReview: string; remoteSizeReview: string }> { const compatibilityReviewScreenshot = await captureObsidianDialogue( obsidianRemoteDebuggingPort(), @@ -430,13 +946,14 @@ async function main(): Promise { dbName: "dialog-mounts-ui-only", }, { - notifyThresholdOfRemoteStorageSize: -1, - syncOnStart: false, - syncOnSave: false, - syncOnEditorSave: false, - syncOnFileOpen: false, - syncAfterMerge: false, - periodicReplication: false, + notifyThresholdOfRemoteStorageSize: -1, + syncOnStart: false, + syncOnSave: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncAfterMerge: false, + periodicReplication: false, + useAdvancedMode: true, } ), }); @@ -480,6 +997,20 @@ async function main(): Promise { ); const setupUriScreenshot = await verifySetupUriDialogue("desktop"); console.log(`Setup URI dialogue mounted and closed successfully. Screenshot: ${setupUriScreenshot}`); + await verifyCompatibleAlignmentSettingDefault(); + console.log("The undefined compatible-setting preference is displayed with its effective enabled default."); + const mismatchScreenshots = await verifyConfigurationMismatchDialogues(); + console.log( + `A mismatch limited to compatible chunk settings was adjusted without a dialogue, current manual mismatch actions mounted successfully, and the Fetch action applied the remote setting before scheduling Fetch. Screenshots: ${mismatchScreenshots.general}, ${mismatchScreenshots.fetch}` + ); + const troubleshootingScreenshots = await verifyLogAndReportSurfaces(); + console.log( + `Show log and the generated full-report dialogue were reached through their registered commands. Screenshots: ${troubleshootingScreenshots.log}, ${troubleshootingScreenshots.report}` + ); + const hatchScreenshot = await verifyHatchSurfacesAndSafeActions(); + console.log( + `Hatch repair controls were reachable, safe empty-fixture runs completed, and persistent logging was enabled, verified, disabled, and removed. Screenshot: ${hatchScreenshot}` + ); await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs); try { diff --git a/updates.md b/updates.md index af3b5ee7..4853a544 100644 --- a/updates.md +++ b/updates.md @@ -19,10 +19,13 @@ Earlier releases remain available in the [0.25 release history](https://github.c - P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. - First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. - Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. +- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. ### Fixed - Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. +- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. +- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. ### Testing