From e318f8a36d4e289f77796254f31e45575a0adb2b Mon Sep 17 00:00:00 2001 From: David Meister Date: Tue, 25 Aug 2026 19:02:21 +0000 Subject: [PATCH] Stop matchstick from running a cached wasm Closes #230. `subgraph/tests/.bin/metaboard.wasm` was a tracked matchstick build, last written in 2024, still exporting a `MetaBoard#hash` entry point deleted from the sources in 2026. Matchstick 0.6.0 reuses that file unless `--recompile` is passed, the wasm is absent, or an mtime says a source is newer, so a checkout where no import lands newer than the committed binary runs the 2024 build instead of the current mappings. The binary is untracked and `tests/.bin` ignored, so there is nothing to reuse and a local run cannot stage one back. `docker-compose.yml` passes `-r` through the image's `ARGS`, so the decision never consults an mtime. `test/subgraph/SubgraphMatchstick.t.sol` holds both, in the sol lane rather than inside the suite a stale binary would answer for. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 10 +- foundry.toml | 10 +- subgraph/.gitignore | 1 + subgraph/docker-compose.yml | 5 + subgraph/tests/.bin/metaboard.wasm | Bin 45919 -> 0 bytes test/subgraph/SubgraphMatchstick.t.sol | 137 +++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 9 deletions(-) delete mode 100644 subgraph/tests/.bin/metaboard.wasm create mode 100644 test/subgraph/SubgraphMatchstick.t.sol diff --git a/CLAUDE.md b/CLAUDE.md index c134ce5a..dd3ce094 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,11 +28,11 @@ means adding it to `LibCopyArtifacts.contracts()` AND `crates/bindings/src/lib.rs` together, or that assertion fails. `foundry.toml`'s `ffi = true` and its filesystem permissions exist for -`CopyArtifacts` and for `test/subgraph/SubgraphManifest.t.sol`, which reads the -built interface artifact with `jq` and the subgraph manifest with `yq` — both -over `vm.ffi`, so the manifest needs no `fs_permissions` entry, and both -binaries have to be on `PATH` in the sol shell. Nothing else here shells out or -touches the filesystem. +`CopyArtifacts` and for `test/subgraph/`, which reads the built interface +artifact with `jq`, the subgraph manifest and compose file with `yq`, and the +git index with `git` — all over `vm.ffi`, so none of them needs an +`fs_permissions` entry, and all three binaries have to be on `PATH` in the sol +shell. Nothing else here shells out or touches the filesystem. ## The subgraph is SOURCE only (#149) diff --git a/foundry.toml b/foundry.toml index ec266cb5..88f6313a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -12,10 +12,12 @@ bytecode_hash = "none" cbor_metadata = false # `script/CopyArtifacts.sol` and its freshness test shell out to `jq` via -# `vm.ffi` to extract the deterministic subset of the forge artifact, and +# `vm.ffi` to extract the deterministic subset of the forge artifact, # `test/subgraph/SubgraphManifest.t.sol` shells out to `jq` for that artifact's # event signatures and to `yq` to read `subgraph/subgraph.yaml` as parsed YAML -# rather than as text. Both are on `PATH` in the rainix sol shell. +# rather than as text, and `test/subgraph/SubgraphMatchstick.t.sol` shells out +# to `git` for what the index tracks and to `yq` for the compose file. All +# three are on `PATH` in the rainix sol shell. ffi = true libs = ["dependencies"] @@ -23,8 +25,8 @@ libs = ["dependencies"] # The filesystem access in this repo. `script/CopyArtifacts.sol` reads forge # build output and writes the stable subset for the rust bindings to consume; # `test/script/CopyArtifacts.t.sol` reads both back to assert the committed copy -# is fresh. `test/subgraph/SubgraphManifest.t.sol` needs no entry: it reads the -# manifest through `yq` over `vm.ffi`, which this list does not gate. +# is fresh. The `test/subgraph/` tests need no entry: they read through `yq` and +# `git` over `vm.ffi`, which this list does not gate. fs_permissions = [ { access = "read", path = "out/" }, { access = "read-write", path = "crates/bindings/abi/" }, diff --git a/subgraph/.gitignore b/subgraph/.gitignore index bb4f6494..1ba821ec 100644 --- a/subgraph/.gitignore +++ b/subgraph/.gitignore @@ -2,4 +2,5 @@ build cache generated node_modules +tests/.bin typechain \ No newline at end of file diff --git a/subgraph/docker-compose.yml b/subgraph/docker-compose.yml index 463438da..19aad1b9 100644 --- a/subgraph/docker-compose.yml +++ b/subgraph/docker-compose.yml @@ -2,5 +2,10 @@ version: '3' services: matchstick: image: rainprotocol/matchstick:main + # Matchstick reuses `tests/.bin/*.wasm` unless an mtime says a source is + # newer, and an mtime is not a fact about content, so without `-r` the run + # can execute a cached binary instead of the sources in the tree. + environment: + ARGS: -r volumes: - ..:/matchstick diff --git a/subgraph/tests/.bin/metaboard.wasm b/subgraph/tests/.bin/metaboard.wasm deleted file mode 100644 index 1d083ef5d5f2389a50f610a49b10339c7857cb3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45919 zcmeHwdze&LmG3!qsvq>}g2N*OXb9EayaRO8K=TZ+8@fU#5J(`2#we-oE}Ehr)l^jz zaAH7BB1U4+L`6kK9SzPUBi=iAZk)-zgGn@~sQA1YWfULdpvDsI2P9_3g~$3)x&>O>uXBX0`uuZlRdZ z_H}K}t)180Xc%RMVlJPl>gdU&^P5;SOkFins@nQmdU74zUQ4#GGqWqPC7jX&{u5jD`tz?Ou=Jpq%XfUnLw1Cf(ZmQ1QQ7+5!^s9nIK6}OHfBpPcVgGD#0{@=>#(fW)jRIxRGEs!5o6Q1Pug@ z1oH@*2<8(kAXrGyOt6SxF~Jgor3A|emJ{4Wu!3ME!Eq7b7X&^h@OgnR2>hbJFA026 z;0b|W7Wfr`Uln*#;MW9xUEntao)Y*?f!`ANZGqnr_+5eD6L?ni^b3K%6!$2|0M9Nz+VXbrNCbad|BYH1^%mb@Q8aogJ!cUZCr$y}CQIKD~rDFY7#l!^4#x#S!63kEAo& zWhd2>OBcPaBTH(%zN5V9TYTSbjrLY39Jb_gJ=U19VM&Boy>5KCq18Qz?n?IzWUR3z zgXEOc4cA{69&}3~*Qe{pd8?6$T_ub!q0+iD(^uS>&Zm2=2{Xb}Sngb(?#FqM->ruR zgllHLM%NH?6UVuIH}$6TMI7zLe7d8!8V9O1$rDzHiyJ1m-MzJM&Zqkd7=feNnmlGu zqn!6v(o=;uMyS_LbvxgkPxoUn{KORvUQC@QCAXg_sQ&uW!NJ<64CX=0daAdU(9)-Q ziVsFlA3R!?Zq1nDPUkTh5_^c?%=(LW3+9+Lc~CFC;t-u1Zy2&iKw|dnAv>^gC=t%_ zOy`bFVW6jYk!~W`xt@M?SAR-rY#8oND+YV#VykgPNyWToYhG#1{3ffZw5EB1HNS*R z+hS`$NtZT|Sy);FGR>tmAhW2XCWStWOKZ?)Ny%;sZTh7XUCwVqtSZY{lP;$3$aG|~ z2z9Mxo*4Y*)%o51#oXF-VVkwQX7E@g_OC7NCU3WM==w~)yC<_TUEF4^@RaJ4ME`7O z8gcTbV!AtHtxOIkyQv4$Wjg(S)+*2a_%t?UyZX|_fqceN7jU(D+dyCU+Dy6=3u^I9 zQ$}`lWD12_GrKot^1ayt1AEQ;YEN9c7dN$TSmW&rHmu3!GkU|fhO0LYwD)8?K-Efx zs{;;it_>~Fi|6SJ0yeC*-sa#$sezszCA@}uw1tW5R?c~g=MjX%ur6;63x0JzlP+d3 zjKQ>4o3$=H=bB7^Pj0v82i+d)CtZYdxBa%TUc@8f=ycmQgk>t4b{jT^IaZyWScThi zd)WHcWO_2iOwf2ocsaVUbUB+mj~*yd?7F{bZ4NhW8W7QfrQ$%rx--0lz--%!nO^Iz zuxYjK%5)6SGBykpJ951l>+W#t1t$Bt@bcw6a4qun;R$^c*IVJ9@Sw06zrl0aT$?^! z?hWh4UFhA};x>=HH+u7gPAX3xZ>nMkF2GXrtO9{%+q+7Z? z*P+MhmLf9J_5m0>qd9AvXH#C6Y-gsg!)?r#Hm=WgX4?9;=Jd$zo~zU&xt}xnpt-wr z?B-qi-M zr#p%**`k)g{e#E#6*7GVY{*@M$GHtIXS?fM)74z88R&=eh1BlK6*H|nwQ{XDmkMvq zKt3&j+MksQPX{>m6q4BE6ks?K4Zah1aki@mJP*=b%-3t($O})|0Uw9W)q=FK)^d z-G#l&ER}e0?(R3elT1q%JF!0pvuVK2m&_L6eIs>0DpvqO@iBdj}~ca5mNxgOE=3Onh&fV6*K`Qnfob|y$4EgTM`p}@s z{TA1XKRifCemj(G9WqOudALzK#)9>ci%pS4QM= zc#sK&br4us{6#k_UItjcz7$!*!QC3X1_hd|?d<)DC_u-p|jWKzXIlON6;?|qnZn?E?{cRgI-d^?N ztt}6mX4#&kq0Ybh>Z{hR@m1<@r8-loYSkpiFzkAx&Q$U3busi9yI7-vaSpRlkudLEbCSvvt8}o`@jz6?5rz8ZP_g~t8kaP7G28Rf??Zs(wbr< z&7{#BGm$U+~YN{uiRKaFFT?plf_iILSN z6lT9!bkqtPiK{YaOfB`IKUKJ&WTnbc!<;d&8+X}2agYP5)Tmk|m4O~Hm2|G7utYpW z?3WDsQIsN4TmYId4^CFCszXy(t3=7CSeOwlEnn7VVB=>J>evW1S}hUd0&nCdX@VQr zqSm2sBlANt_6$gA2KiqP7S4@O#~k%JQ_a!C*%aTNjKR)q1x6Kw?p#j+fP+`f9S)^Q zQC1H4A7QV`2|il*IBtL)?Z2%utv#Y6YX>OAYN#k!z9|~Su0z4ZR*TOu#IkABwJ~Z4 zG^`zv5eCPUBMMHMGh(6!tOQJo<*`&rAxm8~jm*Th0a5OKW(jIf$8%$%F35p6f$vJ779B;bqdR_Ji*3nF3?w>3ViydwmX3VmM zrP?y`=1M|J=2F8HqQ1y5!BA4Outt&8NMBMW##9!QWoq2cytV>}stkisY3WK>6DJ;~ zd&YI8E6PM&S*Yj5rCJ>uig{4fn-uejWJJZX96BbK*-<{TF${3XtP^6k9D`Ab7FN2< zR_UH-VWrFLa4;M6yacmmz%1xo+6qOUqkR@F3>ZfiD?=<+fJIb7(jgXY-4l`yu{Z)O z20bs%A}k*!TVg%hDgs;3=2mwROZk7g)pD&{%mb#KM1{XFy>q-hI`lrc2CKUa9=zRu zNgghPj^`yjIxvijY>Od$hW+&z&ZrptU#-}F3THe-)}7=boP_HvcEf?wIFfN^0(>Tm zZKyF-#SVCliXAC&pDdv^-kOMWR5z(HdZ2ftlMojiqbBHr>3d=MFkaX4XIgTuqC!T} zQDU6rW5JA!exE)Hs3AeqLYjRIB#<%F1faL8Md&}9|4Fy{44dxKknndyLTDa(B+06+F z9W@ntZpu~}wK$@zRy)gO#}ovjj9Y3XkoKfmXCdarniv^FO4}u3z~@_PYUytep7>d0`dVXcxNd#{;Shs_iOIILU6*XL2# zJEtvSOi(6^tG25u1fUam?n)DPk)e(pH+11MUg5J|;YVI!?-{qxtADc48G=xhhB9m} zkq5D61j6uw7G-jCsT;}8oKzkPXPeKv+9OQON!6g6PXVXCtr}D48oOl&hJb2JCG+YP zBUPp+Lko^fj9?Ig-HD~dpjghDG{_7!h+jN(tEx5{8A>3kzJOqU{CcTaBo+S~&V#G} zu{6}!HU>#(Wx2xTVGi3+@#1!vgAQs?$NVN4@3B8`>cqy=xkNeY@Ytl`OjKva0vx_x zeHFhbanl^XUu=VLVK|f2;c?ClvMlmxqT;w$LH)##alQI#g)^CFfww1OJ8O|zDBBqW z4ay3@2!0r&4o`CGnaas% zb~!##X9Q<}No^(cF@q&H7C$gt{RY2m(E>z}%LNs;Bkrw}gyFzsg&x1huV8c!`&-O- z;jU`q>gY&xgI}w)u+fpdQZDfB-7M|#Ta#t52(_mSfs9#H$41ga+3Jt3t?gko_pV0_ zieqPPPQUY?e8UvhW9!i8%p^3!0FK|F8j&&K=TR_GjG`l<2Q{6k&O}%}$=kJMNTty~ zS>{Y)tu58HW%M<0uJw=!95-Db{w#GILUg7%7z8?yCM?5VHmJ{mq~C{9ll(@w1AzFU z>~h*0vl@F5HCU;}P=hr5N_8DffnI7*mUm{fwgatRyFqjXDk&`31UHm-EM=3%-YM!p zy;`X%v|eUfl~37UG-I(n=7Y+FAs9}5T@0K}NSaSWSB@Gf_Ty%Yai*+nmK2eG4Ww&f zG^nbzU~Oa38^Vw{o`_xs2;nbLwMIyH-4v4s%r4s645h8jU^<9SPK7q6V@Y+cmO7gV zaI6jhOZa4&w+47RC~cPg6Eha)GR};1YHN-9GG|IH6kD<;BUqEAWKHlF{+is~6mzBz z9;l9ucWN(h>bS zPf2W^iR#oer@polw97!-0_|bYU9H$-IqeZORj8RPH3`(z<7mO8_3>I;L{W{_BzPIF zot;K%Htw><)H%s2^5-NIbVfFyMf*i5hVW72dh_V$*z39F`ixaPPo$Zo^L?-qWt+X~;pl3varlDK}j9Nz1Gj zS5aM8POm*osMJ-e*iK$y4wJLgKvA7Z@Yoow8cEW1FLh;b&6SMCEPa=R!$R&&+iDQ1 z4O5kTZmq%bQ^(8D_^mZ{@w$q^mzqEIE;V`2QWsMrb|~C|;hG1=hnv?q%Hovu+&;W- zth0EO+1pdK)yWE6(9|XTo7mc_q@7GKf2qck2=2EI%N_0lRV(s{C9AgA;n=LJ4sL7{ zTy8ao54y2Upf(ZQ*d|s%ga}aO#@5D-ttIwb2HQYv2SKo+t%i5A;MYc^Z`H<-vydUq z>yMBmJ>E?ta+19G#7bh@adssuyP@wG;%E#JJEGd*e{jZWD+8vPm-ATn%mPMRZ9i?s zEVx-`PMzh9C;|+d5iZqqT zbZ!XMR5OEMJYeaJ0+%Fcg`B|`?o?DI)@JGbB#1UAyGD2MC^y>nD$E?3H|Eb9yI@|a ziOc01BN2Dz8=w;~2pc^&TOFIDPWch59NWiDJ%OnXeBEFYEQNjvh*8s53j^0z_c~#* z)X$7Qs8j84+`f>@$LIdwb4&7(hj1}1v5ko)Qe;36P3rNi^!kG>Q27IzP5aIbvjoWbY%|@5A7BKY|a@MkP-&1u~O`Xxg_5ft zGOBe$Moo`V(`OV>w#O(ceMV9C7-gCtFp3D@S%`41W$)e(efpQ5{i(T`*-Fda!%u$d zu}NWrs_ z+}|VS2tP_WCu11KiO{eYq9cCrEl*)-&|*LC8%ynypFKN+ow{}j0fxxQ&^}No9m>xa z4i%F|AoLU!4UWaIJx!N`GiSN|Sd0n>x7|3S=zI0mN;zy(HX+W?gELkpJ)|Jwv_R%?6 z5u%R6%`EbECt0zbIf;q6y?!w+fsL>gX$r&-%(jPbI zewKOpvUs05w?_qgoY6hLTk4?xj+=-V8l`_)p5&V19$sl$@v`!Y%GfY$OlJY_m^gD8 zlNV%X^1`HI(`!gH;3fWV%%ZFHuQiv`>#|o}i>ot#pjFQb*%uP6Cw=J*Rc(G{cv}>EQe5FNnBfJ7Mh368k1Y9jcZ|0eBJi zosto!FyiEM;Sm%Z;!^Z>rrLQNPq3}yGo1N4e?2}^9h;@bx?&RpKu@tT?9An`In%ib zQREA=83lm$Y&lug@!9Ix+3e}^MJnTT*s0Ep#vm*UiyLm+jaZO6FcT9Rbd4NShRZ3_ zhAo|Y4pux%eG}Kr2+&|y&N5q4Wp#!eUUa~6zyDI(o}(p(fCDN-^boJ(B#_L37cg3oJU8v);~-i}8h0{~v@`1xM5YUeVt6wV zIu6AJww6mzM_bM;=f+D=eBl!78AKhiAadnnZ*&F$r|OC}#LG-!Z@s zY8yh=<-91x9x&8jy{Myd^(+dq8;-qbsG~2kYj_|0T${74j>E>B1<>$t(Jn}s&UnOv zGU*wNJ#)fPpEo_9g^nY_e2b~Le8;ebgm0S48)3qXph->-;EfP|tHFihxZy%$lB+pR zxCbu8LZa|*Grnx_I^%Ef1p%`pdomX1G++xi=-ZBCUy*kBY~B8A{M*COoeu8CoO!h- zu2`nPb9#3>^ocx1so3CTEk;pFQjX;8Vy#+5p48Kc^J z(!dp6OyATYL@v!7EjM$7Phu_@d%6}GxMYsLUMs+in>m)bnIpEhn>k*pce$H6mb+Og zGe-+~BK-_*dEG*}(fkYm13u9&B7W<&G%YPYOoHaBa*X0Zp2C1 zn_~D!ve1CLUhG!VG+Shs>HEUgu=J%FcCB@zrGMrdBK=Q$TMRaIe6%|L4a2dWwRQ@} zR1JN|u^K0?&eY)g;~cYKXP7!L4Id7Cl^zD4j$scXDEPYj@$lFA@%SnI(SC3EuGOqB z$HtiF-nkCiUKhVd=C;+Tv)-482E&)NgYHiK4Kl=iExl}uT(#pzHu`qz$?w6l;FFGR z4&XO!`@cutecwrUINLj|?Iy%3IHs|qc}9!RJoRlz<2#1>g?n>^X6$>zS>>$b#D_ypv`Fvx0r)+wS%LdC$v@IixQA_*6FRc20_!g>6aRII6SPr+~N0zIPotvDcA-y{rBuayZ?K!`|rCS4&S`G^)$3Aj)dPvO@ewA%#CUzMj4{5q^6m8bx!OeOe5(y&wlH^{0gl|a<5>{J4o zjjB#1ka(y>DvRX(DkVVf($h-l|Xu?#-|dvWLFbXiAg{=q!Kp( zO->~y10_?5Bv5TCQ43U;O4I?>rxNu*Q&Nd3KvPqRsX)_GiD^L7Q;F$7Gg1k7GBq=m zK=`F*r4qA%ZcHU^1e%>nAS=R`^@%w^b5n`AKnTs}w*&tg zxEuJNfqQ`e1-KXZ9B?1-zXIoge*@eP{9E81z`p~|1OFRv0r>a8xS3M_4m<$-3h++g z{{X%p_z%Flfd2@*8~8l%n}PoX{Aa-b3H$)?tH8MSRC}?z9t3_XFw$!EHsHM`3U5c@ ztt>o*!rNGQ7=^d9@CXVIvG5KQ9%f-53XibxP88n3!lNkcW8qyWypx6fC_Ku-V<^0f zg?FQ{pM}R!c#MSuD7>46_n`1N3-3kY01Ho`@E#VPMB%+GybpyZSa?4QPqOd<6yC?e zK@{H4!Us|K01F>N;UEhiM&W}jyZ|@xAr`)f!iQP-5(B=|*wFA=;*aDreto|7zmjo|A9xE@oB@hrvj4OSc{_zuAr{L*(>dWqos z1V11+P4Eu{KP31O!5MnPY8ZW@H2v+6MT!{EWs}b{+Zw$!EXqDNAPb1|3vU> zf`29WJ;A>dNb5hc^a{a$5d4AQj|Arl{zUL9!Cp%Dtpsl)_$BM!&eFfI^bkugv-Den zhxzdlf>ZqXO#({!+bq0;_4^291&^}y7{Pl84iJ2t;L`+02#ygPCit%eM+yEL!TSg( zq-R)oC&3|tcM*Jw;KKy}gWw5*zae-x!Q%w~li*2$_Y(XS!RHB{CioD+{~|a@@BxBP z5d1m8CkgORmTdJgg8xqNS%SYNAX6Xo3Qw`HpWrVEjuZR^!G9t648cbU{+8e)1YaQd z9Ko{$&k_6`!Se)PB6xw|iv%wcoFMozfq@@3A)Fm;XN>yK;N}li=9JHvYX50t3IZ%= z^aLbHab87DP{)Tm<`lEuoPtck89&7m7+0r8p}I`%z0RqfQm#%-P-k>9GE(iuo!)^T zNWeByXxa8>*FBEtcYmdAF2_c%wDIF1!mLm`oLHbI0xK=+Pu+a zWN;Sgu`gBHQ#tkqJP)EhYqT2;=u-qN01n{uT;Kv`8)lmmG21YEG)Gk9*@Tun#`J-v zI;z{B8D=NgUXG^?Ek(?Qmf41SX_#HZmN9r*FsjQ~15P?n>IGjt@Y9;YzAAeJ`&Hs; z7tYaks7hu(Qe}^1TNNIKx$_vg+fdKy))S$Tec+w4P^Wd<+0e)qj45Jntm?dO-Dd}@ z(!9C6AF}nlcEFQvS&UiH2Kk?~?P*iM&*8XX9LIA8k5gtCjd(WV>Bn;b&#^KdMIs9P zuW_!kFvP)g3Qc@SVi?3&ZJXfwnQHqDTg*18xtWz3gy26(Ztz*rv3ltdphS!;^ACd(UYcJG`0ZdFvk{@H5GMeq-`F; z?`TKbkC`0w2%gsqYf13|ea3WQvKMuoQ0`U?OoCw2SdW?xqgz->VS+w<3OokQfcu|| z>Odck*^18;PZD&v@NK9o0Jo!-W{||EG|YpGn~U8sN)&uzRH=fSpv)9hNfj-EB0G#F zs9A`=M&Jz8JRkKmf@YMLK=EtvnNJfqXfj$cM-%EBQJRMub#%1s&}T*mJ9s_lkoUXL zL!CtXY+6CTW|Mj-{0FICA?QW`}6Hl-! zR>7*dgZg1q{V3(2KAL9^7T*wRn}hlSYS_|?KOS%T6J0$I6VHST-Y(uhPOod6J&g0l zMBeJCu6-s{y9=vA?K-S#9DbWB{j_c<#YS+q7}BPvp@M1hi%`RIBP84isnUSeq49c0 zjvu7pW(d0zgy`6)l*JHwJ02>DmPGZYfwuxLL=6eH3voICDv)aJz{pOtZtC*a(P& z=P^`&s74dj=yfkdS!3T$QQm@QHQMMUs5W|@VAfzCCb5>mzKF`E ztJLc4fd^AZC+a;vF*udAH{#4_qiIgWzYf8yv+#IrGawP~Ldqc@1TJ#&M- z!~~a3c!DjVM^Tm>mlcW40VSHhJ_g)l>O81C04+_n@sG0&<4t&4WixmjG5*uX1tmTp z^t9Jbf@2*e(G_q5(YNzB3l1mNa828=CiZ~z&rcQ^9ZK5fvp9Q__GiSEeF8I6$J~lM z5Va*SmgI>r8+CeX^U&sOJc)RA(C*2VK4oYj_o1dBH5`2(YG?thC)FM3os|+>=%l#9 zZ8Dy=LM_ml&>2t%3~I=^=0TmV75g|_8$``|Q~~{X;BqGNL&{w!-zW5Tck$7 z)Y&(iSnVo2a`z#nmW$O^PtI-r{3-$&3o|#R&eExk+x3S z4Ooem3S~z7bnWHkda=4!>g|=BZ!wxE{CkA1;OBIu^jp%au|3&S}S)!8j?ev@a#09cUq&f(}GiyhO47OEa5!b@5Kbu z1Bant5}hDKSIa4aE$A7mRv0JWh3ROvdUNnZ2@71Eo-VHU?DQaJFF?HQ#&(S5IJJL< zuz7Gs;21sEu8!)qXJ#M=fuiDgXz=PJ>KP!-2&kq@cvk0ik9{+P1dchvF3h+dn4wl1 zDIJ}uWBQpv5T%{IXFOhj$naB-3%h>pKp9-pHQW#GXuw{atq+ejhE<1Vi5id03e?ER zn>s(-g%DaXkRFx~l|*kt9~gMep_mQS$Q{Qs zlU|d)h%8=6Bgi?aQk^<@UP~ua(_ub0iYoTa4pOM;pr!rDV6N2B+0y%&+4h8btb=)V z|8)TP&l#)nDjNwfy20qr&bH^9sGW`HF0k4ICNA9GX-pSBsIbWo%@M60nPYF^_}AeH zw&j&OsBq+_b4l{yI;Z_}h1Y{~1Lu4xUMcP@f}JLeS0^#{0od_e+mA5mm^VR@-myCa zN4(C}m<%zC8+?3+_a_f0eMA*ujb4(|B%`?FlMe z@sH6r6GVMmz*UiBa>JQOg*_I_-xBoHQlx~qdajIIlOxVXO6;W$&9`F{;|Of#+wZ6} zVeNF4m#Zb_3A~qRgzssDjWj~vjc_N_@8xK39>(kZtpk{M8q5Hh_+Fk?n=qbK6VvJvm z`4?mU%eB5G7{3JLnQ8KhVF|`B!FXB~ErZ&nhA3r9imN5ZoRuS)%`?1a9Klmu-xF*_ z4Xsk&kWstT5YtaeimN5ZoK*+s?Z8YQJMm=jFu|ln^AdxWMeCr3nY2>UlonS^t~oPD z^N%+dj7*r~ayQVTX_d4rS_czdYKfAjw76n&&6zoxe+=B2Y;h-XH_)PKm9#8c2Q^GB zQPPwaSFFFC?A0uSv(dny1A3$jNz7 zn26YSMG$-P5Ct2@ZT8>+>h5RFhMv7>|db{o_Gc%%YK;P1h7viSsM^K%?>@V zRp7f#)NjO7njkPu;s+3#NH7H^L5v-vB@gmtt(C{;QQYMlKn@bz3HzvA&`Ukm;Va0+Ta-cvzI90b0T zy>7ce5*MaQyv=5k=w&OfktChx9EY$zybUAxa^Uil9X;KZra9qg`x1%HA-tS}Vmbos zCoc}I5C4Uemy_)H0IU|cKuPuBNkNX@osM_=!bG?i@(J%6=5qQd2yU2oJHu}>g8LdK zJo@Ix%e1CL#`<0iIoV0PL4>+`3AlqCM7^O2CqUt(z`wG77H=z|Wu5@Ol{2ovQ+me{ zwu04?BGW2f^M>UR-he{NM}Ylftdw%8J-1%T{mpq&p9qV?8&>=qV+7KN@R?rp2=EfF zhc}&i-}9!e-}PEdw!-%>-mdX{ntGXZ4&nDcz@^`wFbdGI2_p&S7CK_$`OR2{5x)Lz zkWq>kV>3?RDXQ}pW*>|pc%CyBV0^>~iSY(=3Z2m~qhQX!c!_h+yznK{e!NJAf;|X) z5tVrdLje~KS8yrzGATo-uK@c(#W%_O@zNR!;~;P#j61-N=Raxvo*nZ#g;yawBN(Fb zj#l45mU@)AsONWLoj5<3DtouV92d4Zo-{es6hKjb!^rC(-c#Tb>L}i%LzbTb_D>pq z6UTZ8=_TMc6E@ItfnA4f@iuo4zNS?l#XEasK44!&<(SW{e<@sFffJAQ;NulwUs;=F zHQYl(-S*Je^Q7_Gv~wKnOw~!eH;A0P1nhHCi5XcBclQdgeO99_-hFsSl0Pqd(Hi)Dg}0M~S$W-~+ue(wlNc-U5%6@* zo5T~oGt;+U^xobN4j7u?eN1wW0{cIg@V@ybhu)nveT%+sGeI-^@a`u+xVevCF5$n!v*6^=AgTlQRaXVovHm1f2mHLrl|75++U?Uetg=OR zE$^_ee%ErZbC-K}k{!^x{?<*OEd0hse{ZMuzvN0C*q6Hg68jo|w||~9QR;sm5-P4V z58*Q{?g+3SmtUEsbG^8-ewmao!zR)){q2S8ga6_`zBsY$h9Ual6^adb_$tNnsYA4DI^mZqRxFWj7nQ}fbhfX- zd+VZKn9J8MR?Zk=2K^$&hSC=@4!evs@fD3#YcI{rYbhppi=#dJl9aR@L`{6dWA(~Q z_I3@zeF1(j2cwim%0DC>!q$&{!i@< z8qM9B?dciYa><$fP2JO@-(*y1z#F}?#ohJL3O4jW2lnjstyfBeuXvIF|9qItxTKJw z*XT_1RV)p``{e4kY=O?ktE--9?$&&+w{3pY1T6@>m#4=S15ro+?wX+_)RF7oJ#oBD zs=Zf(egzrX#{c|&(vVHt@Sd#V4VqlZsJUCVWOhs*YD{6E9X*p1WhUXx9SBs+^(@cM zZ(31%bF{_dC9Rvp1J^al|DJy--m(L?-J2<vZ0np$?+X%+@sCyi#a@mZ?Q~ea+lA zirg+x+La!ELQLMRu_7$5hLn)MQfeYE+1Gd{SRSwDXuRv%v)MmT*fuYzw+c5V;+#BR zjCLEu11q>|o~)^9uX*Vuct>#HM)@WezJhQF^S|^t=DOsaw3Y^u!HRnL#`*u3S-^f> zu=sL>5Mmt0u3gf?$ud$rpFVOM28y}#{H86PnT{bkUWsTQ>Q!s}LAo&4@^3`LZjWGPJsz zu6FRP@UAN+2ac)Gu`Sb^4mrUBUUIW?<|R77N|yHF-8kGkZJn#8y++rx-q>ns(2wWU zkZ$;{d0U#cY|(G-spmX+Js4i-*fOz@?+8x0hHdG-&Yq0kyIY!8>%CTbhPlDG_i}WJ zcUCjBTQgUreyQFr$F1^4_0}u*%1x=)=1AgoJ;TjcKEwRAZLhrwc|ZJZc&p^{K=L7N z-!<=nJ~XaqyYlH5+%oO*!jD@okM_LqUR`(7Vl^X|B*53eQL(wplX=*bisE_M)i_2k;q$oMr1-UhX4*;Nep-XFEO z{6VpYvbbrOjTO&!{DtY>K6q(OZtGT@Z{|?xlpn3Q1y`f7q2j!Y^cBThGl$p`g9m8O z8^7@~y23*X-Li?{$YJ${+cw{!R&U;-)~vasb(x_`gH6BLeAw!%Cj&v~ccE9UiA2PG*|{B!iu=m*>S$ElSEMH*Pze&-;SqEc zdJFuBh-6&JI1$OXq8XXGdS!>-O+P9s9haZc5mbd;zb4a>?M?TLiLej!cFN^?u4}t` z(Fl>ibjGfWNXmQ5yd)9VM`bK{E&I5&5gGGeranG`J&Lq+ls+Nq+JeNj<_!_o=H-33 zM$7Nk?D@-`c<;3*9;ju~tyfQdQ!ifVh`an^9z#THq%W>$ zS_In)%zb(UckQof#x+=znETATuU^oe34~lmu*_L^T~osT%5S_QI$dqE0k5-fzg9GT zHs@@)RwO+s587yRBRG8hg7=FM=Jsod;QusnUl1hMsIFcZI=EOouO%vpdCg6cY0Ph$ zADKq;f(22<(YAPDR5se?H8)45F~4b1WE!{;jY=amziCNi8qEuqMvcaTkbK#OtGCwM zGWed3Uo-N;_l`_QhOc#B9$`cJ1Xt&G_ZM?(@wLoNw_OANR7WwJ>ywjlMTE$j?5)q_ zyL&Pl)5UEoBe-IJq2MXonZ`H3n~Le~%&JY-V6mHeAht}WPgzAsa(q5FWxM*)_(Ct! zaxL_+dfPx>_u5Pvm&>ao9KOO`M+d&Qx;3+VVS=(O(2bl}_PT;GO)BEJ03ye=ZW!AI=tBO0*W znCSEHo`{0;kA2w`Z-~fP=%Btg!esz$P1g9vh@Qwb)w}U)Bv+MhiXgAhy_ZXP%hi01 z$Lti}7_G~7-4{Vd5=gIYTG*V9njnCY_6Uru&J}ud1=Y8^BSNU`6Om>SuGt<|he|ow-GP(8m(!8y2YfhHj9U&Ff+%c}r10+h!DAl$h39lBio1Y)7F{lNq4p4 zCoF{s5l@IS{e!7uge!MVjDBe#!q+Aq6}qII*T74KFVqcx>?N4_UTe3y5`(4+7 zw?6zdtuRp7eGPbXNog&<`Od3Pm9+fV4D=&O$8NkUSIo5T)b8@nB5%qy1NaR!BG?Ba zZ%R2A-V)iDl^!n-M%Jm&<3-}0YeS0S-)lyeyQjgonh^p`Z5bY^b=gAkZRRylfVM4d zxNkQjhkU6_<{>k(|Ium8!)D~2sR?c9>)9=C`w=s8bhR$qwXG<`-eJBz>OL1@t9!B; z+}sq4Y5vY%>tkPZqw~U|cbbtS7?0F?{F8+2qtUI-qqiyB*VU7GS9AtpzQs+MqSp3) zGxFy5FG(LWBV48F9gE$AZ^c-e%4VSSo@+xc)2X%m-snPziF<82{ z|9#ON^X6>-`=k5Q&}|2g*$fJIh{uqK6(w8tx0|?`ayA5CeLs8v=|SYk#o28_}(zx zmEm6ac=V$pw7dA_3;u!5C!*{PmA^lOe+eYc{F6~mZI{r-d@i?j!&dz**q@sba#fe$ z=5*JmqHMH)jO2u$j.wasm` and then +/// reuses that file: `Compiler::execute` compiles only when `--recompile` is +/// passed, when the wasm is absent, or when `is_source_modified` finds the test +/// file or a transitively imported file carrying an mtime strictly NEWER than +/// the wasm. An mtime is not a fact about content. Archive extraction that +/// preserves timestamps, a coarse timestamp granularity, or any ordering that +/// lands the wasm last all leave a cached binary that matchstick executes in +/// place of the mappings actually in the tree — a suite reporting passes for +/// code nothing ran (rainlanguage/rain.metadata#230). +/// +/// That is not hypothetical here. `subgraph/tests/.bin/metaboard.wasm` was +/// tracked in git, last written in 2024, and still exports +/// `generated/.../MetaBoard#hash` — an entry point deleted from the sources, +/// along with its mocks, in 2026. The binary a fresh checkout landed was a +/// build of tests that no longer exist. +/// +/// Two independent things have to hold for the cached path to be unreachable, +/// and neither implies the other, so both are checked. There is nothing to +/// reuse: no compiled binary is in git, and the directory they land in is +/// ignored so a local run cannot put one back. And nothing is reused anyway: +/// the docker run passes `--recompile`, so the decision never consults an +/// mtime at all. +/// +/// Solidity in the `rainix-sol` lane rather than a check inside the matchstick +/// suite, deliberately: the question is whether the matchstick lane executes +/// the current sources at all, and a check that ships inside the cached binary +/// is answered by the stale build along with everything else. It needs no +/// docker, so it runs on every push. +contract SubgraphMatchstickTest is Test { + /// The compose file `rainix`'s `subgraph-test` task brings up. That task is + /// `npm ci && docker compose up --abort-on-container-exit` from + /// `subgraph/`, so this file is the whole of how matchstick is invoked in + /// CI. + string constant DOCKER_COMPOSE_YML = "subgraph/docker-compose.yml"; + + /// JSON path of the environment variable the matchstick image expands into + /// its own command line. The image's `CMD` is `/binary-linux-22 ${ARGS}` + /// with `ENV ARGS=` empty, so `ARGS` is the only place a flag can be put + /// without overriding the command outright. + string constant MATCHSTICK_ARGS = ".services.matchstick.environment.ARGS"; + + /// The flag that makes compilation unconditional. `-r` is matchstick's + /// short spelling of `--recompile`; pinned as an exact value rather than + /// searched for, so respelling it or appending to it is a change that has + /// to be made here too. + string constant RECOMPILE_FLAG = "-r"; + + /// The directory matchstick writes compiled suites into, from the repo + /// root. `.bin` under the tests folder is matchstick's own layout and + /// `subgraph/` carries no `matchstick.yaml`, so the default tests folder + /// `tests` applies and this is where the wasm lands. + string constant MATCHSTICK_BIN = "subgraph/tests/.bin"; + + /// A file under `subgraph/tests/` that IS tracked. + /// + /// Named so that the tracked-file check has a non-empty expected result. An + /// empty `git ls-files` is what "no binary is tracked" looks like, and it + /// is also what a `git` that failed to run looks like, so asserting against + /// emptiness would pass in the one case this test exists to catch itself + /// being broken by. Asking for this path in the same query makes the + /// expected output a value the command has to produce. + string constant TRACKED_TEST_SOURCE = "subgraph/tests/metaBoard.test.ts"; + + /// No compiled matchstick binary is in git, and none can be added back. + /// + /// Tracked and ignored are separate claims. Deleting the wasm without the + /// ignore rule leaves every local `subgraph-test` run rewriting an + /// untracked file that the next `git add -A` commits again. The ignore rule + /// without the deletion leaves the stale binary exactly where it was, since + /// ignore rules do not apply to tracked paths. + function testMatchstickBinariesAreNotInGit() external { + string[] memory cmd = new string[](5); + cmd[0] = "git"; + cmd[1] = "ls-files"; + cmd[2] = "--"; + cmd[3] = MATCHSTICK_BIN; + cmd[4] = TRACKED_TEST_SOURCE; + assertEq( + string(vm.ffi(cmd)), + TRACKED_TEST_SOURCE, + string.concat( + "git tracks a compiled matchstick binary under ", + MATCHSTICK_BIN, + ", or no longer tracks ", + TRACKED_TEST_SOURCE + ) + ); + + cmd = new string[](4); + cmd[0] = "git"; + cmd[1] = "check-ignore"; + cmd[2] = "--no-index"; + cmd[3] = MATCHSTICK_BIN; + assertEq( + string(vm.ffi(cmd)), + MATCHSTICK_BIN, + string.concat(MATCHSTICK_BIN, " is not gitignored; a local matchstick run leaves its binary stageable") + ); + } + + /// The docker matchstick run recompiles unconditionally. + /// + /// Read as parsed YAML rather than as text for the reason + /// `SubgraphManifest.t.sol` gives: a commented out `# ARGS: -r` satisfies a + /// text search while the compose parser never sees it, which is exactly the + /// way this protection would be lost. + function testMatchstickRunForcesRecompilation() external { + string[] memory cmd = new string[](4); + cmd[0] = "yq"; + cmd[1] = "-o=json"; + cmd[2] = "."; + cmd[3] = DOCKER_COMPOSE_YML; + string memory json = string(vm.ffi(cmd)); + + assertTrue( + vm.keyExistsJson(json, MATCHSTICK_ARGS), + string.concat(DOCKER_COMPOSE_YML, " sets no ", MATCHSTICK_ARGS, "; matchstick then falls back to mtimes") + ); + assertEq( + vm.parseJsonString(json, MATCHSTICK_ARGS), + RECOMPILE_FLAG, + string.concat( + DOCKER_COMPOSE_YML, " does not pass ", RECOMPILE_FLAG, " to matchstick; it then falls back to mtimes" + ) + ); + } +}