@xgent-ai/skills 0.7.5 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/xgent-statusline.js +32 -18
- package/README.md +61 -35
- package/bin/phoenix-diagram.js +14 -0
- package/bin/xgent-skills.js +132 -240
- package/dist/phoenix-diagram/LICENSE +21 -0
- package/dist/phoenix-diagram/NOTICE.md +34 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/005fc765ddc5115da796cca915baa9557abae13ff35e0a47c47affc56f6c414d.txt +272 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/010492e490b92ec85db01daad2a290ea5959f29314a1c4ff25e7a172e223abf9.txt +18 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f.txt +3 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0218327e7a480793ffdd4eb792379a9709e5c135c7ba267f709d6f6d4d70af0a.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/02bb8c3b4e70190e3986c0404ad2fd8d639b4f534252d82379cc1b502b6d1812.txt +191 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0444c6991eead6822f7b9102e654448d51624431119546492e8b231db42c48bb.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0552c898248eadfc130df0c0e8f6f82776fcb3f1dd5a5af2fa991b5baff9367a.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38.txt +20 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0b28172679e0009b655da42797c03fd163a3379d5cfa67ba1f1655e974a2a1a9.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0d3f7c086c6b6cf3ea4aac714e9aa3e1fb02e355ce35bd21c56acf8d04390885.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0dd882e53de11566d50f8e8e2d5a651bcf3fabee4987d70f306233cf39094ba7.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/117829c3ca21efb132d81a44b55363d395ab8eea18526873bc828da4c0e5f038.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/123a331b5dbf04c30097fa43b8f858bc85df671fe776de498d01f3d6b7c1f69e.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/1a2f5c12ddc934d58956aa5dbdd3255fe55fd957633ab7d0d39e4f0daa73f7df.txt +26 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5.txt +373 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/1f485f0d6125658861351e3d6084ea90fefe78f89450be9915044b7c56d5a75a.txt +15 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/209fbbe0ad52d9235e37badf9cadfe4dbdc87203179c0899e738b39ade42177b.txt +26 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/20c7855c364d57ea4c97889a5e8d98470a9952dade37bd9248b9a54431670e5e.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/20fe7b00e904ed690e3b9fd6073784d3fc428141dbd10b81c01fd143d0797f58.txt +176 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/219920e865eee70b7dcfc948a86b099e7f4fe2de01bcca2ca9a20c0a033f2b59.txt +7 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/23860c2a7b5d96b21569afedf033469bab9fe14a1b24a35068b8641c578ce24d.txt +7 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3.txt +23 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/268872b9816f90fd8e85db5a28d33f8150ebb8dd016653fb39ef1f94f2686bc5.txt +220 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/27995d58ad5c1145c1a8cd86244ce844886958a35eb2b78c6b772748669999ac.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5.txt +428 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/29e9fe5074bd27e0e5d5d110394fbbcd841baee2651a3c4b4560a632702cede4.txt +26 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/30fefc3a7d6a0041541858293bcbea2dde4caa4c0a5802f996a7f7e8c0085652.txt +23 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3521672491a3479422d5fe1aca6645dd2984090f85da6e5205abfb18fb7a6897.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/35242e7a83f69875e6edeff02291e688c97caafe2f8902e4e19b49d3e78b4cab.txt +176 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/36516aefdc84c5d5a1e7485425913a22dbda69eb1930c5e84d6ae4972b5194b9.txt +23 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/375bdbf54d6584debb4bcc5bce1c9dcfdd581f977c9a8469113dbdf208f4c927.txt +131 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/391a5396cec6230bfabd4ef4eb2350eb895bc5efce377a2218f5702ed020d3e3.txt +20 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/39712155836e5c10ef1b07037b606e4ad2d633e72205d162fdd7e2290aa54f15.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3bb1e548250b3acc43febde65a5cdba9f2e0a3288018aa41f009a3c59ffab51a.txt +390 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3bc70e239e91272782006c638fc0452a714d384224f11f0923036b7be07cf9b5.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04.txt +373 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3f941b3b89cf7b8370ceb83cc76d2120d471b58735d8ca60238a751a48d7f72f.txt +359 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/42fa16951ce7f24b5a467a40e5b449a1d41e662f97ca779864f053f39e097737.txt +26 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/4455bf75a91154108304cb283e0fea9948c14f13e20d60887cf2552449dea3b1.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/451167c55c0fa447cc2d5632714f5e3c567fe4f1e1badefab2c1333852198aca.txt +542 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/45f522cacecb1023856e46df79ca625dfc550c94910078bd8aec6e02880b3d42.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/45fd05c4865e7c350b98ad7ac50e1b15462d49af4a91e9b0c9dd933dc9a69742.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/4789210f4df8abc03fce1cddd9c2ba517f184840687f2815d741de282c1aa93a.txt +228 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/4cada0bd02ea3692eee6f16400d86c6508bbd3bafb2b65fed0419f36d4f83e8f.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/516b24e051bf5630880ebbd55c40a25ce9552ebaf8970a53e8976eb70e522406.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/553fffcd9b1cb158bc3e9edc35da85ca5c3b3d7d2e61c883ebcfa8a65814b583.txt +13 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/5757a1f28eff735a8b5e7425478f367812d71e974530f5bedd01219480965f4a.txt +51 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/5b698ca13897be3afdb7174256fa1574f8c6892b8bea1a66dd6469d3fe27885a.txt +19 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/5e05b024f653a5ce199e77cbbbd42fb5553562ec714b819421ed0c3e552a75d7.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/60a7062291b01ba068f300612cdbdc20382ac1d4934f07bcdd7167c15299f309.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/62065228e42caebca7e7d7db1204cbb867033de5982ca4009928915e4095f3a3.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a.txt +176 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/65f94e99ddaf4f5d1782a6dae23f35d4293a9a01444a13135a6887017d353cee.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/6df43f6f4b5d4587f3d8d71e45532c688fd168afa5fe89d571cb32fa09c4ef51.txt +187 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/709e3175b4212f7b13aa93971c9f62ff8c69ec45ad8c6532a7e0c41d7a7d6f8c.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7365cc8878a1d7ce155a58c4ca09c3d7a6be413efa5334a80ea842912b669349.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7abd9b6960dcf7d4d0a48606a5b71bfe37d472db68d70637f3a58a56785f1621.txt +13 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7ca6700600dfa9c9497bf5556365067daa802c871ea78239f129309c7a2048f7.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7cfafc877eccc46c0e346ccbaa5c51bb6b894d2b818e617d970211e232785ad4.txt +15 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c.txt +24 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7e5886959f67f8c75063a9d55cd3cb08c5a8cbea9a944fa1df2d4ae038f53721.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7fea0ee51a4ca5d5cea7464135fd55e8b09caf3a61da3d451ac8a22af95c033f.txt +20 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/83c1763356e822adde0a2cae748d938a73fdc263849ccff6b27776dff213bd32.txt +24 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/86dd7f026f916daf7511e39951ad8ea8cf55a8db67ae64060dacf829761c18f3.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8a35369f3ca263b3c62fbb5032947e53b6bfebc6c8a4d1bb982de1c069f6fba5.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8b43ce8accd61e9d370b5ca9e9c4f953279b5c239926c62315b40e24df51b726.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8bb1b50b0e5c9399ae33bd35fab2769010fa6c14e8860c729a52295d84896b7a.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8bce3b45e49ecd1461f223b46de133d8f62cd39f745cfdaf81bee554b908bd42.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8c7516d4b27b1e495be5e38b612298b63de48d05f49cdac94f70f3cd70f8864b.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9046848b63a5c92bff14e4accca80bd987e0623b74adf9226ce5198d312b79d5.txt +30 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/90eb64f0279b0d9432accfa6023ff803bc4965212383697eee27a0f426d5f8d5.txt +12 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9117d922e667125508dde62b02c1f57ed22f5ad21eb536aa2e2d99e1c796e639.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9387c2d197e2370a740ca457dae1662c56de56b811ee4862024343961307f9bf.txt +19 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/946c9835d8034d24404f8cfec5f4654cee5dad17e944afc3d06d742cf2882831.txt +240 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/95bd3988beee069fa2848f648dab43cc6e0b2add2ad6bcb17360caf749802bcc.txt +176 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9ba9550ad48438d0836ddab3da480b3b69ffa0aac7b7878b5a0039e7ab429411.txt +396 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9d185ac6703c4b0453974c0d85e9eee43e6941009296bb1f5eb0b54e2329e9f3.txt +202 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9e0dfd2dd4173a530e238cb6adb37aa78c34c6bc7444e0e10c1ab5d8881f63ba.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9f2ae19ef94ba82887729a4a1617ff25eede56611dd21a4b2c6f797df30fca33.txt +2013 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9fa38f610a00115dd1f612bdd3559e3b381ea579538b6ff25a5899da250a5fce.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a00511478c83ebb464ef19d840f061c7444adc4636bb561900e75c5808cfc6cc.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499.txt +121 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a5dea80c1f383cb5f80a6bb0da5e55a2beb9f24adb123ce6300af2cbaaa3bf65.txt +19 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a825bd853ab71619a4923d7b4311221427848070ff44d990da39b0b274c1683f.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/aaff376532ea30a0cd5330b9502ad4a4c8bf769c539c87ffe78819d188a18ebf.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ae9baa7beea910273c2f384c2a6b721fb7bd02bda3436074a1072e4ee689f985.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b1181a40b2a7b25cf66fd01481713bc1005df082c53ef73e851e55071b102744.txt +19 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b38f11f6096706e6de553dabe2a7ed142d59b6fa8c97e290c67496154745cdd5.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b3d734001a94efff3579978d953391aa7115f877657d25eb54037a43875d078a.txt +9 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b4eb00df6e2a4d22518fcaa6a2b4646f249b3a3c9814509b22bd2091f1392ff1.txt +27 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b68ad1a3367b825447089e1f8d6829b97f47a89eb78d2f4ebaef4672f5606186.txt +22 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b7e650f3fce5c53249d1cdc608b54df156a97edd636cf9d23498d0cfe7aec63e.txt +19 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c09aae9d3c77b531f56351a9947bc7446511d6b025b3255312d3e3442a9a7583.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383.txt +21 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c30152c94a6d75e021adbc52b3a52470366a46edb917e17deae3259251af244c.txt +42 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c394e3bda1f3b9378f30a7cab11242887eeeafc73c7bef36a0fe13e09691e18b.txt +17 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4.txt +201 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c962ee4d1d05ddc138b202b2540219ebc57893fcf97b364852094a9a94ce1365.txt +7 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c9a75f18b9ab2927829a208fc6aa2cf4e63b8420887ba29cdb265d6619ae82d5.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30.txt +202 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/d1fc1bc0d155df60b2e7705b6b2ae02a05c96f948e1cec6e2fb86360b09f346b.txt +29 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/d5c22aa3118d240e877ad41c5d9fa232f9c77d757d4aac0c2f943afc0a95e0ef.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/d5fca2653c88ad02d197faead77e22b80c70636b5bec6e02f2805f389a21b86d.txt +10 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/db11fec9946737df39ca3898d9cd8c10ec6f6c3a884a6802b0ad0b81b4e8f23a.txt +1 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/dc91f8200e4b2a1f9261035d4c18c33c246911a6c0f7b543d75347e61b249cff.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ddea4e94a1c35ed9826e54e13a5b50b8f81b21665a00ca07c8861f1859e56b6b.txt +14 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/e271993808fec50ab29350b39539cdec611a9103f827e0aa26d61da70e2d33f8.txt +61 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/e66c269d4819aaab34b49ef5220c4ddab6756f21bb5180761a4eb8561f2b7bbd.txt +438 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ecc269ef87fd38a1d98e30bfac9ba964a9dbd9315c3770fed98d4d7cb5882055.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/efcfee7981ff72431fffb06925cad00a23dce079ed4354f61030ad5abdb78829.txt +22 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f025ccfb7dfb6bdfedc75ca0f67acc69e6fb4998143d834f7c2f38a29989680f.txt +13 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2.txt +46 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f51ac2c59a222f7476ce507ca879960e2b64ea64bb2786eefdbeb7b0b538d1b7.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f7715d38a3fa1b4ac97c5729740752505a39cb92ee83ab5b102aeb5eaa7cdea4.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f7db81051789b729fea528a63ec4c938fdcb93d9d61d97dc8cc2e9df6d47f2a1.txt +39 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f9c4c77baa3828004ee54b8a4f2db2e88ed44a6237a493965bf551fac0fcb62d.txt +17 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85.txt +373 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/fb77f0a9c53e473abe5103c8632ef9f0f2874d4fb3f17cb2d8c661aab9cee9d7.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ff8f68cb076caf8cefe7a6430d4ac086ce6af2ca8ce2c4e5a2004d4552ef52a2.txt +25 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ff92bed461f50338dd703a9ba9aee496a425957873df1d91192776e4bdf5dda7.txt +74 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/COPYRIGHT-library.html +29357 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/Apache-2.0.txt +73 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/BSD-2-Clause.txt +9 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/CC-BY-SA-4.0.txt +427 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/GCC-exception-3.1.txt +30 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/GPL-2.0-only.txt +133 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/GPL-3.0-or-later.txt +202 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/ISC.txt +7 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/LLVM-exception.txt +15 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/MIT.txt +9 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/NCSA.txt +29 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/OFL-1.1.txt +43 -0
- package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/Unicode-3.0.txt +39 -0
- package/dist/phoenix-diagram/THIRD-PARTY-NOTICES.md +210 -0
- package/dist/phoenix-diagram/THIRD-PARTY.json +5459 -0
- package/dist/phoenix-diagram/VERSION.json +39 -0
- package/dist/phoenix-diagram/bundle/ENGINE.json +39 -0
- package/dist/phoenix-diagram/bundle/SKILL.md +109 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.architecture.json +11 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.class.json +23 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.dataflow.json +11 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.erd.json +25 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.lifecycle.json +11 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.sequence.json +13 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.timeline.json +15 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.tree.json +13 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.waterfall.json +12 -0
- package/dist/phoenix-diagram/bundle/examples/minimal.workflow.json +12 -0
- package/dist/phoenix-diagram/bundle/manifest.json +253 -0
- package/dist/phoenix-diagram/bundle/references/architecture-compare.md +111 -0
- package/dist/phoenix-diagram/bundle/references/authoring-defaults.md +42 -0
- package/dist/phoenix-diagram/bundle/references/brand-marks.md +52 -0
- package/dist/phoenix-diagram/bundle/references/compatibility.md +22 -0
- package/dist/phoenix-diagram/bundle/references/delivery-contract.md +127 -0
- package/dist/phoenix-diagram/bundle/references/extending-types.md +14 -0
- package/dist/phoenix-diagram/bundle/references/migration.md +57 -0
- package/dist/phoenix-diagram/bundle/references/preview.md +45 -0
- package/dist/phoenix-diagram/bundle/references/repository-authoring.md +29 -0
- package/dist/phoenix-diagram/bundle/references/viewer-runtime.md +27 -0
- package/dist/phoenix-diagram/bundle/schemas/README.md +23 -0
- package/dist/phoenix-diagram/bundle/schemas/architecture.schema.json +329 -0
- package/dist/phoenix-diagram/bundle/schemas/class.schema.json +348 -0
- package/dist/phoenix-diagram/bundle/schemas/common.schema.json +358 -0
- package/{skills/archify → dist/phoenix-diagram/bundle}/schemas/dataflow.schema.json +36 -8
- package/dist/phoenix-diagram/bundle/schemas/erd.schema.json +384 -0
- package/dist/phoenix-diagram/bundle/schemas/lifecycle.schema.json +202 -0
- package/{skills/archify → dist/phoenix-diagram/bundle}/schemas/sequence.schema.json +41 -10
- package/dist/phoenix-diagram/bundle/schemas/timeline.schema.json +170 -0
- package/dist/phoenix-diagram/bundle/schemas/tree.schema.json +144 -0
- package/dist/phoenix-diagram/bundle/schemas/waterfall.schema.json +150 -0
- package/{skills/archify → dist/phoenix-diagram/bundle}/schemas/workflow.schema.json +42 -10
- package/dist/phoenix-diagram/bundle/scripts/phoenix-bootstrap.cjs +589 -0
- package/dist/phoenix-diagram/bundle/scripts/phoenix-diagram +6 -0
- package/dist/phoenix-diagram/bundle/scripts/phoenix-launcher.cjs +10 -0
- package/dist/phoenix-ui/THIRD-PARTY.json +164 -158
- package/dist/phoenix-ui/VERSION.json +17 -17
- package/dist/phoenix-ui/bundle/blobs/00727f7b333a7be8ac7740aa257a3a3f691db8ae545a6afab2b787acab5e88df +46 -0
- package/dist/phoenix-ui/bundle/blobs/{547e3c89d5b8feeddd67e43131fd84b95253eb42bf7d730b4e471dba011f6e51 → 00f4a6a630f31eb05fbdd5725e4a294233d24574bd70f4f32f4597d8256a7c1c} +0 -4
- package/dist/phoenix-ui/bundle/blobs/{41400e182e2180a567923d58bfa935d2799a54f2a747b2c5813c3884c84f768b → 04057592e18ea9dd3c4edcac14cde3fa5b75239c2552d4459db8e047068f61a8} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{34571f7d46632205d518d15cb008457494dfad745c6b483bb11d63ca3a22ac48 → 0cb3bf26e1980b0e530373fb32edbe5864a171492a04e2abe1952ffe78bff231} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{852d2943678a76c33b2f7a090d2ca6c152cd120e9ae62775fcc91519b9289011 → 0e684572eb98d24b39cd70fa23e66c163178f5a11756dcf8a98fe2335636b33f} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{d17b3a454146e74ef4ecf69d7b72a8fa45c4fe1c695dffb0d36690b653cd2d8f → 10553c45c7bf38e527a57a281c4365152a768b4dd7ef597ff34ae841e9ff7d41} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{e49257c0fd32b195cccb16fbfda769c007be6b38f25aceefecd23cfff9366cec → 125bc7030d7f2ba789d7b1d5f474af34f8902b764e7bc351b98f1f6f55779929} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{74dbcbbad6c3831e95abb38775dd488c59ae9f1d44a95f5fd4bc0051577488fa → 13cf6c054ed424e2eb639ea12d20a4aa2b1c85eeb00afa8cbd8a30c6c22aadd8} +1 -1
- package/dist/phoenix-ui/bundle/blobs/1832ff9624cebd30370eb0e806665c656ac879215f93d33bcc0750baa47459dc +46 -0
- package/dist/phoenix-ui/bundle/blobs/{8e7de5ca347036a07f5f1e9cab942d8ff378d029bc101fa6d126406f3298c9db → 1864a10c29e7062cca5e49d1790dc7d9d8ea777bfa2ab6fd941635965cd0084d} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{784722b72224a7005a80cdcd6408dd2b25b23fd9619d0f6c8788f7ac681fad4c → 1ecc81de39d28b85b95e797bcc338d4213a50231c3315a741d14571fd22c844f} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{b8c614dc7c2eb30ee0f864fec116dbcafb39e5a199dce66d0c1a88b13f76265a → 2079e4146039a8dadf19a39237cbdc0b8b54ea1fa16d320716c2cbf58161c835} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{dba6e4a09c5d752e5ddebe7c2c1aefc9c4b2a84e01a3178599579778ed2d09e9 → 2813aa68c0804a7fe061d88201d8a684cac9987b48511e1e2cbfe0a869a67a82} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{2889c2817b5079abff254bb6d13f92dbde0fabc38be4acc7d20e613eef5e5829 → 2873e340be947c83c3d03520139b73524fd166a34063d2693d3b711021ea5b87} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{64c4aa2d1c24ae5028e987498ac544020d48600a51d9654b2df562681ba480f2 → 298ef41899acefa056eccf2345378078832db6d868d2377f471f896d5c1599f9} +6 -14
- package/dist/phoenix-ui/bundle/blobs/{1b7174a6f3f49a8efc8f7fa74ff84cf8960e2646b9bc976e4129a7c8ad608faa → 2b5cef2a14a5b1256ec6de9172a4f57f227948ced1921ef86e899f48524d7fb8} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{f148f630f43f3c91cf10feeda5a3c21448ca0683ce82d0fe763c1b43dee584d6 → 2d376cbbf7ec1e262614aac4f1b200e8f60e86e940c7137a2a26788bc857e509} +6 -7
- package/dist/phoenix-ui/bundle/blobs/2e5985a6eb339d70cfa25f6d2b420a0ef286caa8467f0226eb841702ec438d0b +46 -0
- package/dist/phoenix-ui/bundle/blobs/{7e199833376de5d2b6e088ee2965b448f94da842f28ccbd707e1cc3a295147c8 → 3322d691bf7b578b38723576df6b266993b2afb9e567b0cd1ded898ffe1dc17e} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{f82bfc4c6bc9169d4978d5e16fb925f6e152fdd21dc6a2fae719dfdc1645581f → 3625c5c2750f117021232ce81c1e39ac5ef7c21445729327b88cf54dfc937df0} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{2ceacad745665f79fd7ad5565646426a36a1b508c0469b931d0f202fad45a80b → 36310febe3e7bd4c9b21407603f2a04afefdc87b7821758bb47098b1b2876765} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{24d92a176573768bfffaf8f9c6483e58bceb5bf2a095d1cdf3d3a2c0bcda0707 → 3a9c2daf4ef284ca83b0e7f6fef3a5300e273c32b20769e090463cb8a8b0b06f} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{153e377c44b162cf0074698a61716db7f664db78f5635efa926bed484de4f54c → 3ce1a44efe39b93e7a39307c6a5e5f514e938fbae818e358986b4db1b05883ca} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{ad8ad0b7acf6c88ef9ce0ee1a640c2200da77821f0d97d605d4f1d4a10e5897a → 403fcde498d75abec7844ee31434201152ac229f7675772164d69563f0572da4} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{e8ee6c08e94b80324173d89d6f4cdc5b4829ea6858b3a11fc186588a490cada4 → 45d0e7eff0923c40296e971318756617944c4f1c97f03126cd74c0f8c629f8a4} +3 -1
- package/dist/phoenix-ui/bundle/blobs/{e23dabf908842be08751fc4e7f3b586e8c6b1ca0fc30303ec5317c77f83dcf26 → 4c0f396bfa85171f61972d3f3765733318769bb53fe06d2cdcb5d7f64578b5b8} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{a5517d32c1539c388c47c00879a0d807ed2989647bfa8c0844ef22328f75f5be → 4f6563876e0e0cc51c38a06767b8800eb3238611e9fdd11e5ff9dd487c742e74} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{a9d2203acd45ca33a13d5c68b02b23ed512425ac15f0e8438318ed124b43729d → 4fcada229d0c689c1a493d13184145a81ff40c498e882142ae390bda8e5b2492} +15 -0
- package/dist/phoenix-ui/bundle/blobs/5293adc55cf66068b6df20468ad9e91433fd1c48e75d66c86f874aca951a8fb9 +46 -0
- package/dist/phoenix-ui/bundle/blobs/{96e2e6bd4fcf9a2c6da65fb029f96d9176308fae2efd9d8653d3b8d838960e07 → 5436f0301ec258f3badcc6842019ee29a9b870d2727dc425fbc9cc0f4f88a284} +3 -1
- package/dist/phoenix-ui/bundle/blobs/{d0f4939e86a62b0ca437d51a997ceba7e77d64566b7fa8ca2747487e31ca819d → 54a09a4afdffca3cc135e5007716ea684dc1ed7a78349de859225c55a2fed810} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{d5102701d41f7c6195d410a873f742ac72c428b04196e47df5555c5ccaf80504 → 55277fa15eb4bd2f09bf7bb24c4e79df417b06654b1e10f502c7928b192fd7cc} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{878064e46af4e4109bab405f965e4149bee40bbe8912ffede10b74cec6cff027 → 567c3f42394f9aee9b048a001016a7ba9d8773d78027f8c995c379886634e719} +6 -3
- package/dist/phoenix-ui/bundle/blobs/57c8f33a7401f918566b8a4883871a69ef2223438c51f158c67f69e3250be465 +46 -0
- package/dist/phoenix-ui/bundle/blobs/{bc29e115fd337dda2cc7fc70b868dc18e426b08ec186fd270fcfe96bb2efaa5d → 61450dbef64859e3136cd17ae3e997134d5df5721d96985c4e07367f38522129} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{e3c1e925be0b2deb0e3765b83ab94b86560292364f275e98bb23a8bdf2c2e049 → 656bbfda3e72b40d53b5832293dc8700ad8a784e804a6246271bf0ea23527822} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{ffcaa5c58e779ec74d18bfc3013ad8003f6e7a23f629c1a3b878618961a8b6a3 → 68b454a99ba72c3b9b93ed96c4267847297169f6a5bf321b20e60d637c271068} +6 -3
- package/dist/phoenix-ui/bundle/blobs/68e44832c82b873b0aeb2da56def7b1de5f612c019445e5e28238c690124cbbe +46 -0
- package/dist/phoenix-ui/bundle/blobs/{45bbd4a8e02ca0012216c5c68e1dcd655ae0046bd50ba4295ef1f58ec4a0957c → 6b8d4c71355429a0b16bf367163fea46103a0a99b4e34f87608a39d85b1b980a} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{264815d0080e4862715a4f82542e66241c6563f07259d32aed4a17bbc329b9cc → 6cef874722442de7c60ea96f4221dc54cc4250a270cd05a1e818b58c5b43e232} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{2028a857c0476324a4256faf66693e2a869275e53a88d2b182a28b62c2a17893 → 7181f1f4c452c4345695f9100f6deb610f54f4c55f126043433a65b8491fc680} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{ccb76aabba47938d518a5d46a09d7e70c4a9222bc391e163763f2a7f12d30437 → 75850a8c9d2a8259f71ef1f86dd5d1325415227c1887e81a3467ae1ee400979f} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{4b03cdb5b17d534b5c7e84b079c066f59e8b60b9e454671a453867c4aeb9920d → 77bcc6c2d6ac00a8bfead3b6c6b5fc69f4b60bf086ebbc89271e1b23003168c9} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{9520e25a381c82af6f46e175b7c347d1dc513d933dd6cf8c52570c72abb4ca7f → 792552563745d208594cf4ade19110f2c811a0e1a2661ae1ef43a185ccff5a69} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{f7c8af35acf71a96056afb6afcff6a3ea42f65561f02652d04e11ad9ece9b8e9 → 7acd9b6f2f61b509c6a518508b8b52c2f0de67cdea4fd3f3d383b3ce8b38cf5e} +6 -7
- package/dist/phoenix-ui/bundle/blobs/7bacc1e3da7e8b7127ca3f8f69a2422098b9b2d23fbedefc56dfe76117c1c0e3 +46 -0
- package/dist/phoenix-ui/bundle/blobs/{78662176c1d31cec2d640f70df203f93cc1aec604cfc31619f4d2c62ab78861b → 8078b804bea630ed9c4ea07ec2f8ac92aba4128725860a66599eda90390d98f6} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{2291169748fe0c4c80634dff3083e7464f82d4358b9b2186759b316e612d466f → 88b150cb804bc141b80a5687a091f61b99627155c465a27adb88546ef615af92} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{bde88bf5ac59d744440d9b65f95ceaae01c803b101d48e6afd644a06eeb5de63 → 90f8b07ec12b78e633e306322cda58415aea70d8e0f9ff85bd439361c01dabe1} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{486145071d3445e82774f1b39b2bb15986ce52277e798a31ef7e9c27afb42af3 → 95b2944686e8258a655c3b5e6db68593e24e5199354e46aac553918ea2c0f55a} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{a1998e37084c11f496d4970f86a85d5a6375bbeaa4797b9f42bbac2d44048d23 → 979639f0e8859d600a8db57e733566d9ad37ff8258c3e05a6f9c0f3008e92373} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{3173293607055ea6e1a153ec6f51fb8154782157c8a61abf411da1f41be61728 → 9eaaa00049844060f842fe7fb73cc8edc4be5225cf38a96f33bda3ffc381456c} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{69086f7cb235b35ccc230e1f2da41cbb10cea8f0636b6d4514672113a3adc2cb → a420bbda622399399a6b5c7c3614dcaebc6c62faceaf7b3c83c97db53d26b165} +1 -1
- package/dist/phoenix-ui/bundle/blobs/a63054b8920905045753facece83f154bf806158c6f46ef380036e7a82d4fde5 +46 -0
- package/dist/phoenix-ui/bundle/blobs/{e802e4f7bdc89050a9c0f2ca506e1d493a2316e6e810c0336c57aa073717ef3e → a63788c764b0e4fdcbc029d46093016310edcbcf702d62688e5aaac7ddeee6fd} +3 -1
- package/dist/phoenix-ui/bundle/blobs/{827a211ea03d2cb44b4ea1bc81ba1a6dc51a87fa16d6692997c4e4751241c815 → acfc4c26b135d5b27fc0930738486c9d43d379704102fab45564320180942e4b} +4 -2
- package/dist/phoenix-ui/bundle/blobs/{8da14f3eabac53c990a66e721387367b46cb676cc4204e8c67d6f88c95a6217f → ad243ca615349127aa422bccc1a6604ad17149735659087ca5bab3c5fae97f4e} +6 -7
- package/dist/phoenix-ui/bundle/blobs/b63931066e2b2623d012e1a3c1a0769cc43ed6640460c3899669b45539d6a20d +46 -0
- package/dist/phoenix-ui/bundle/blobs/{bb65af58ecc3e0c0093dc9a705a7ab643035e4e592c3adb93583c82d1aef1f51 → bde793ab5ad776f7e6f6d25ce2b838c1b5022054a1dad5502a873423eb0ad917} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{8de7c97949b0bc4194b3be7645e4b13e9419767927482d01cf19476f6cbeda14 → c0a70ab2889e5c16e1f09249aefad948ce352c42cfe2b5afb758a4524e1b21b6} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{60b2914df394800688c4e20f4eb34e117cd430df9954cf4d48574734507723f4 → c9ee9006ae6c0194a5d39a9f0cb275c52d74a71ea3f6dc2b4c1c7bd89f48a2c0} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{6d8315e307eab11d3236935daf7267746cce6da3b51296e6999651ac97bda3a3 → cadc2daefcf238aab981fa56388381d3d5ab9c0686bb9dd8187035c9933dfcee} +6 -3
- package/dist/phoenix-ui/bundle/blobs/{d7b744fcc95164d96d9f811a93e2e8a374887146eb762ef775f3d3c7f27abc8d → cbea5728b90f58ff047be6ec4f14eed9d63c7148f5db99b5f147d2f17b472317} +1 -1
- package/dist/phoenix-ui/bundle/blobs/d408a4fea71b656aa8a5f6b5c037815a6e4229df336629ebddf4b5c85ed82fac +46 -0
- package/dist/phoenix-ui/bundle/blobs/{6ff950a5729ad9ea480ddf8c1b489b1c84189bbf7d6034b3b5a98e7c11a28cec → d9ae0c782d8a77787561536c0d7bc3b61175d59303f3cb8c20796f85140c6984} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{1d53e2226465b1047faec7cda2e743e61540ea69e073a3a4057e8d86b277405f → e2932d63eb7f52a82960a19ba79bc800ef4f212164e098f96dee7a4326302f38} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{fa576f6655b235bdea535198647305b7a3855e810180cbdf5d4c246c03c7d190 → e381bcfec42ac541d574011460600cf0818aa0e3362e8c6d205b4157890bf821} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{8fbce5b4859e6011afa83b1c07c4436589cd7181f9cb997538f4c27f196d0d34 → e415657d8725d9b60ece42cc18581da177c42fc47cb11e501db722a5f524ecf7} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{546a8428a2d8628bb3496fadb8c36a6dd65a33464291f214ceb581828f37081b → e46d63e5f8a6ead1d44c8b6302a0c7247af8585b975ab5715518381777622ede} +6 -3
- package/dist/phoenix-ui/bundle/blobs/ee63b3f9b354d69443d05ea4a338af56546897335ace7533546e10be56ea2207 +46 -0
- package/dist/phoenix-ui/bundle/blobs/{c23617601e9c4d97b775c175a5ce1642c40459b157d0821609fe1d253b83ced8 → ef7d57633252f4604749447608850e299c277f7dd1a32776685df0724b1051c8} +6 -7
- package/dist/phoenix-ui/bundle/blobs/{0581c8428246f43c5374393878a29e38f2b1865df87e6f13f2309b8eae54e76f → f0652c6ce41e6949133360178f192ea4ee9f6129a018c7a8b6652870c2330392} +1 -1
- package/dist/phoenix-ui/bundle/blobs/{cfac9eaf96481a8874391e5cecc6149abea1f1f19622f77f1ebb9e1876720e91 → f06c9d797ca2ac8c6a49d3dcd2e99d1b4df8bc9637a49c7e80f3161f49d06b1b} +4 -2
- package/dist/phoenix-ui/bundle/blobs/{d50780cb0a345051ae3fb12c5f8249a5dac9324d6a1963f04d03aaf769ffc27c → f077545caeb3ecb9f51a3f703a09736ca924869f4b4345c1b1ffbd9bf927a287} +6 -3
- package/dist/phoenix-ui/bundle/blobs/f781c5874bcd79ebe1b3b012275b9af7eae6c4867e9a16e29bb3d3ed3dc1940b +46 -0
- package/dist/phoenix-ui/bundle/blobs/fa10c0c926997387c89b7c1070e25b1b2b53ee891741e3c450a89ec6c6dcaf7d +46 -0
- package/dist/phoenix-ui/bundle/blobs/fb1a36ab7116b0c6709b89720cdded29d80982114c2f87cad614e01e126270dd +46 -0
- package/dist/phoenix-ui/bundle/blobs/fbd3214fc12ba70d9c69a995742ba73dc5365823af63afdc93c0c8a8f0c484af +46 -0
- package/dist/phoenix-ui/bundle/blobs/{77682e9d63b484f94a699f2c0b393e4540797d311362c384a3d643aec11d6985 → fea19b8381ec10ff8ed1923a336332974bbb552227ed215e7ddaaf0ec0112c1d} +1 -1
- package/dist/phoenix-ui/bundle/manifest.json +126 -126
- package/docs/phoenix-diagram.md +173 -0
- package/docs/phoenix-ui.md +40 -12
- package/package.json +14 -10
- package/skills/agi-mode/SKILL.md +3 -1
- package/skills/agi-mode/assets/sdlc/event.md +4 -5
- package/skills/agi-mode/assets/sdlc/object-status.md +1 -1
- package/skills/agi-mode/assets/sdlc/protocol.md +4 -2
- package/skills/agi-mode/evals/scenarios.md +108 -3
- package/skills/agi-mode/playbooks/authoring-a-skill.md +13 -2
- package/skills/agi-mode/playbooks/eval.md +19 -3
- package/skills/agi-mode/playbooks/hillclimb.md +4 -2
- package/skills/agi-mode/playbooks/reflection.md +1 -1
- package/skills/agi-mode/playbooks/sdlc.md +10 -6
- package/skills/agi-mode/playbooks/what-next.md +6 -2
- package/skills/agi-mode/references/eval-protocol.md +13 -4
- package/skills/agi-mode/references/interface-design.md +1 -0
- package/skills/agi-mode/references/remote-evolution.md +178 -0
- package/skills/agi-mode/references/skill-evolution.md +2 -0
- package/skills/agi-mode/scripts/evolution/compare.py +569 -0
- package/skills/agi-mode/scripts/evolution/evolution.py +2886 -0
- package/skills/agi-mode/scripts/evolution/examples/public-smoke.json +128 -0
- package/skills/agi-mode/scripts/evolution/examples/quality-calibration.json +128 -0
- package/skills/agi-mode/scripts/evolution/examples/quality-rubric.json +133 -0
- package/skills/agi-mode/scripts/evolution/protocol.py +1586 -0
- package/skills/agi-mode/scripts/evolution/pyproject.toml +16 -0
- package/skills/agi-mode/scripts/evolution/quality.py +689 -0
- package/skills/agi-mode/scripts/evolution/remote.py +2303 -0
- package/skills/agi-mode/scripts/evolution/tests/support.py +201 -0
- package/skills/agi-mode/scripts/evolution/tests/test_apply.py +643 -0
- package/skills/agi-mode/scripts/evolution/tests/test_bounded_exploration.py +236 -0
- package/skills/agi-mode/scripts/evolution/tests/test_bundles.py +414 -0
- package/skills/agi-mode/scripts/evolution/tests/test_calibration.py +460 -0
- package/skills/agi-mode/scripts/evolution/tests/test_compare.py +377 -0
- package/skills/agi-mode/scripts/evolution/tests/test_protocol.py +357 -0
- package/skills/agi-mode/scripts/evolution/tests/test_quality.py +647 -0
- package/skills/agi-mode/scripts/evolution/tests/test_remote.py +857 -0
- package/skills/agi-mode/scripts/evolution/tests/test_search.py +1109 -0
- package/skills/agi-mode/scripts/evolution/tests/test_typesafe.py +624 -0
- package/skills/agi-mode/scripts/evolution/tests/test_verifier_context.py +132 -0
- package/skills/agi-mode/scripts/evolution/typesafe_worker.py +145 -0
- package/skills/agi-mode/scripts/evolution/uv.lock +2548 -0
- package/skills/apply-code-review/SKILL.md +1 -0
- package/skills/apply-code-review/agents/openai.yaml +2 -0
- package/skills/apply-doc-review/SKILL.md +1 -0
- package/skills/apply-doc-review/agents/openai.yaml +2 -0
- package/skills/apply-ui-review/SKILL.md +1 -0
- package/skills/apply-ui-review/agents/openai.yaml +2 -0
- package/skills/autoresearch/SKILL.md +1 -0
- package/skills/clean-artifacts/SKILL.md +52 -0
- package/skills/clean-artifacts/agents/openai.yaml +4 -0
- package/skills/clean-artifacts/evals/scenarios.md +13 -0
- package/skills/clean-artifacts/references/history-cleanup.md +41 -0
- package/skills/crd/SKILL.md +100 -65
- package/skills/crd/assets/cr-template.md +15 -0
- package/skills/crd-respond/SKILL.md +17 -5
- package/skills/crd-respond/agents/openai.yaml +2 -2
- package/skills/crd-respond/assets/response-template.md +7 -0
- package/skills/crd-respond/evals/scenarios.md +7 -0
- package/skills/create-verification-skill/SKILL.md +1 -0
- package/skills/debugging/SKILL.md +1 -0
- package/skills/design-md/SKILL.md +1 -0
- package/skills/design-reference/SKILL.md +6 -2
- package/skills/design-reference/evals/scenarios.md +4 -0
- package/skills/dev-plan/SKILL.md +11 -6
- package/skills/dev-plan/references/execution-workflow.md +1 -1
- package/skills/dev-plan/references/plan-skeleton.md +234 -69
- package/skills/dev-plan/scripts/check_plan.py +1 -1
- package/skills/documentation-writer/SKILL.md +19 -14
- package/skills/documentation-writer/references/architecture.md +6 -2
- package/skills/documentation-writer/references/user-manual.md +2 -2
- package/skills/elintp/SKILL.md +12 -6
- package/skills/experience-capture/SKILL.md +1 -0
- package/skills/gdpr-compliant/SKILL.md +1 -0
- package/skills/kpi-dashboard-design/SKILL.md +1 -0
- package/skills/landing-page-conversion-audit/SKILL.md +1 -0
- package/skills/mcp-release-qa/SKILL.md +2 -1
- package/skills/meeting-minutes/SKILL.md +1 -0
- package/skills/optimize-agents-md/SKILL.md +128 -0
- package/skills/optimize-agents-md/assets/coding-principles.md +118 -0
- package/skills/phoenix-diagram/ENGINE.json +39 -0
- package/skills/phoenix-diagram/SKILL.md +109 -0
- package/skills/phoenix-diagram/examples/legacy.agent-tool-call.workflow.json +94 -0
- package/skills/phoenix-diagram/examples/legacy.async-job-roundtrip.sequence.json +61 -0
- package/skills/phoenix-diagram/examples/legacy.deployment-release.lifecycle.json +49 -0
- package/skills/phoenix-diagram/examples/legacy.product-analytics.dataflow.json +76 -0
- package/skills/{archify/test/fixtures/v1-baseline/web-app.architecture.json → phoenix-diagram/examples/legacy.web-app.architecture.json} +16 -11
- package/skills/phoenix-diagram/examples/minimal.architecture.json +11 -0
- package/skills/phoenix-diagram/examples/minimal.class.json +23 -0
- package/skills/phoenix-diagram/examples/minimal.dataflow.json +11 -0
- package/skills/phoenix-diagram/examples/minimal.erd.json +25 -0
- package/skills/phoenix-diagram/examples/minimal.lifecycle.json +11 -0
- package/skills/phoenix-diagram/examples/minimal.sequence.json +13 -0
- package/skills/phoenix-diagram/examples/minimal.timeline.json +15 -0
- package/skills/phoenix-diagram/examples/minimal.tree.json +13 -0
- package/skills/phoenix-diagram/examples/minimal.waterfall.json +12 -0
- package/skills/phoenix-diagram/examples/minimal.workflow.json +12 -0
- package/skills/phoenix-diagram/references/architecture-compare.md +111 -0
- package/skills/phoenix-diagram/references/authoring-defaults.md +42 -0
- package/skills/phoenix-diagram/references/brand-marks.md +52 -0
- package/skills/phoenix-diagram/references/compatibility.md +22 -0
- package/skills/phoenix-diagram/references/delivery-contract.md +127 -0
- package/skills/phoenix-diagram/references/extending-types.md +14 -0
- package/skills/phoenix-diagram/references/migration.md +57 -0
- package/skills/phoenix-diagram/references/preview.md +45 -0
- package/skills/phoenix-diagram/references/repository-authoring.md +29 -0
- package/skills/phoenix-diagram/references/viewer-runtime.md +27 -0
- package/skills/phoenix-diagram/schemas/README.md +23 -0
- package/skills/phoenix-diagram/schemas/architecture.schema.json +329 -0
- package/skills/phoenix-diagram/schemas/class.schema.json +348 -0
- package/skills/phoenix-diagram/schemas/common.schema.json +358 -0
- package/skills/{archify/schemas/lifecycle.schema.json → phoenix-diagram/schemas/dataflow.schema.json} +63 -58
- package/skills/phoenix-diagram/schemas/erd.schema.json +384 -0
- package/skills/phoenix-diagram/schemas/lifecycle.schema.json +202 -0
- package/skills/phoenix-diagram/schemas/sequence.schema.json +254 -0
- package/skills/phoenix-diagram/schemas/timeline.schema.json +170 -0
- package/skills/phoenix-diagram/schemas/tree.schema.json +144 -0
- package/skills/phoenix-diagram/schemas/waterfall.schema.json +150 -0
- package/skills/phoenix-diagram/schemas/workflow.schema.json +460 -0
- package/skills/phoenix-diagram/scripts/phoenix-bootstrap.cjs +589 -0
- package/skills/phoenix-diagram/scripts/phoenix-diagram +6 -0
- package/skills/phoenix-diagram/scripts/phoenix-launcher.cjs +10 -0
- package/skills/phoenix-ui/GENERATED.json +255 -240
- package/skills/phoenix-ui/SKILL.md +7 -4
- package/skills/phoenix-ui/evals/scenarios.md +8 -1
- package/skills/phoenix-ui/reference/craft-floor.md +3 -1
- package/skills/phoenix-ui/reference/critique.md +6 -14
- package/skills/phoenix-ui/reference/init.md +1 -1
- package/skills/phoenix-ui/reference/live.md +1 -1
- package/skills/phoenix-ui/reference/operate.md +15 -0
- package/skills/phoenix-ui/scripts/ENGINE.json +17 -17
- package/skills/phoenix-ui/scripts/phoenix-bootstrap.cjs +0 -4
- package/skills/portal-app-exchange/SKILL.md +1 -0
- package/skills/portal-dev-setup/SKILL.md +71 -12
- package/skills/portal-dev-setup/scripts/onebox.sh +69 -6
- package/skills/portal-external-app/SKILL.md +55 -16
- package/skills/portal-external-app/references/config-placement.md +108 -2
- package/skills/portal-external-app/references/registration-and-onebox.md +56 -2
- package/skills/portal-logging/SKILL.md +1 -0
- package/skills/portal-micro-app/SKILL.md +2 -1
- package/skills/portal-micro-app/references/sdk-reference.md +3 -1
- package/skills/prd/SKILL.md +238 -159
- package/skills/prd/evals/scenarios.md +13 -0
- package/skills/prd/references/prd-skeleton.md +14 -4
- package/skills/react/SKILL.md +1 -0
- package/skills/resolve-blocked/SKILL.md +46 -19
- package/skills/resolve-blocked/agents/openai.yaml +2 -2
- package/skills/resolve-blocked/evals/scenarios.md +22 -0
- package/skills/resolve-blocked/references/decisions-contract.md +7 -4
- package/skills/review-code/SKILL.md +308 -46
- package/skills/review-code/agents/openai.yaml +2 -0
- package/skills/review-dev-plan/SKILL.md +88 -33
- package/skills/review-dev-plan/agents/openai.yaml +2 -0
- package/skills/review-dev-plan/references/report-contract.md +53 -19
- package/skills/review-dev-plan/references/review-lenses.md +2 -1
- package/skills/review-prd/SKILL.md +90 -34
- package/skills/review-prd/agents/openai.yaml +2 -0
- package/skills/review-prd/references/report-contract.md +54 -18
- package/skills/review-prd-dev-gaps/SKILL.md +1 -0
- package/skills/review-prd-dev-gaps/agents/openai.yaml +2 -0
- package/skills/review-ui/SKILL.md +369 -71
- package/skills/review-ui/agents/openai.yaml +2 -0
- package/skills/review-ui/evals/scenarios.md +2 -0
- package/skills/rust/SKILL.md +76 -84
- package/skills/rust/references/api-design.md +97 -0
- package/skills/rust/references/{chapter_10.md → async-tokio.md} +42 -18
- package/skills/rust/references/behavioral-patterns.md +49 -0
- package/skills/rust/references/bevy.md +95 -0
- package/skills/rust/references/{chapter_02.md → clippy-and-linting.md} +26 -15
- package/skills/rust/references/{chapter_01.md → coding-idioms.md} +58 -7
- package/skills/rust/references/crates-and-dependencies.md +66 -0
- package/skills/rust/references/{chapter_08.md → documentation.md} +30 -3
- package/skills/rust/references/{chapter_04.md → error-handling.md} +32 -6
- package/skills/rust/references/{chapter_06.md → generics-and-dispatch.md} +28 -11
- package/skills/rust/references/macros.md +45 -0
- package/skills/rust/references/{chapter_03.md → performance.md} +25 -2
- package/skills/rust/references/{chapter_09.md → pointers-and-ownership.md} +37 -17
- package/skills/rust/references/{chapter_05.md → testing.md} +18 -2
- package/skills/rust/references/{chapter_07.md → type-state.md} +5 -1
- package/skills/rust/references/unsafe-and-ffi.md +56 -0
- package/skills/rust-port/SKILL.md +144 -0
- package/skills/rust-review/SKILL.md +177 -0
- package/skills/security-audit/SKILL.md +1 -0
- package/skills/spec-human-decision/SKILL.md +1 -0
- package/skills/sql-optimization/SKILL.md +1 -0
- package/skills/story-points/SKILL.md +1 -0
- package/skills/swarm/SKILL.md +1 -0
- package/skills/test-plan/SKILL.md +1 -0
- package/skills/ui-loop/SKILL.md +39 -0
- package/skills/ui-loop/agents/openai.yaml +3 -0
- package/skills/ui-pattern-research/SKILL.md +1 -0
- package/skills/use-llm-gateway/SKILL.md +58 -0
- package/skills/use-llm-gateway/agents/openai.yaml +4 -0
- package/skills/use-llm-gateway/references/api.md +129 -0
- package/skills/use-llm-gateway/references/recovery.md +86 -0
- package/skills/use-prompt-dsl/SKILL.md +165 -0
- package/skills/use-prompt-dsl/references/examples.md +726 -0
- package/skills/use-prompt-dsl/references/language.md +938 -0
- package/skills/use-rustify-docs/SKILL.md +60 -0
- package/skills/use-rustify-docs/agents/openai.yaml +4 -0
- package/skills/use-rustify-docs/references/rustify-docs.md +138 -0
- package/skills/use-task-gateway/SKILL.md +60 -0
- package/skills/use-task-gateway/agents/openai.yaml +3 -0
- package/skills/use-task-gateway/references/authentication.md +75 -0
- package/skills/use-task-gateway/references/task-api.md +121 -0
- package/skills/use-task-gateway/references/workers.md +85 -0
- package/skills/use-task-gateway/references/workflow-actions.md +66 -0
- package/skills/use-workflow-service/SKILL.md +57 -0
- package/skills/use-workflow-service/agents/openai.yaml +3 -0
- package/skills/use-workflow-service/references/action-provider.md +84 -0
- package/skills/use-workflow-service/references/authentication.md +76 -0
- package/skills/use-workflow-service/references/authoring.md +62 -0
- package/skills/use-workflow-service/references/mcp.md +47 -0
- package/skills/use-workflow-service/references/open-api.md +88 -0
- package/skills/use-workflow-service/references/orchestration-choice.md +63 -0
- package/skills/working-with-mbx/SKILL.md +1 -0
- package/skills/working-with-mise/SKILL.md +1 -0
- package/skills/xgent-app-release/SKILL.md +114 -16
- package/skills/xgent-app-release/references/build-and-package.md +2 -1
- package/skills/xgent-app-release/references/publish-api.md +6 -1
- package/skills/xgent-app-release/references/troubleshooting.md +2 -1
- package/skills/xgent-app-release/references/verify-preflight.mjs +21 -1
- package/skills/xgent-app-release/scripts/preflight.mjs +42 -3
- package/skills/xgent-image-push/SKILL.md +1 -0
- package/skills/xgent-init/SKILL.md +1 -0
- package/docs/claude-context-goal-guard.md +0 -124
- package/docs/codex-context-goal-guard.md +0 -109
- package/hooks/claude/context-goal-guard.js +0 -139
- package/hooks/claude/settings.example.json +0 -26
- package/hooks/codex/context-goal-guard.py +0 -189
- package/hooks/codex/hooks.example.json +0 -26
- package/skills/archify/SKILL.md +0 -137
- package/skills/archify/assets/template.html +0 -14937
- package/skills/archify/bin/archify.mjs +0 -2139
- package/skills/archify/bin/open-artifact.mjs +0 -86
- package/skills/archify/bin/preview.mjs +0 -653
- package/skills/archify/bin/visual-check.mjs +0 -829
- package/skills/archify/brand-marks/README.md +0 -31
- package/skills/archify/brand-marks/catalog.json +0 -131
- package/skills/archify/delta/architecture-delta.mjs +0 -1232
- package/skills/archify/examples/agent-run.lifecycle.json +0 -60
- package/skills/archify/examples/brand-aware-delivery.architecture.json +0 -47
- package/skills/archify/examples/cache-miss-request.sequence.json +0 -82
- package/skills/archify/examples/checkout-platform.base.architecture.json +0 -31
- package/skills/archify/examples/checkout-platform.head.architecture.json +0 -31
- package/skills/archify/examples/dataflow-product-analytics.html +0 -15048
- package/skills/archify/examples/event-stream.dataflow.json +0 -57
- package/skills/archify/examples/incident-response.workflow.json +0 -64
- package/skills/archify/examples/lifecycle-agent-run.html +0 -14983
- package/skills/archify/examples/production-deployment.architecture.json +0 -71
- package/skills/archify/examples/release-delivery.workflow.json +0 -62
- package/skills/archify/examples/sequence-cache-miss-request.html +0 -15063
- package/skills/archify/examples/web-app-rendered.html +0 -15012
- package/skills/archify/examples/workflow-agent-tool-call-rendered.html +0 -15054
- package/skills/archify/migrations/workflow-v2.mjs +0 -279
- package/skills/archify/package-lock.json +0 -149
- package/skills/archify/package.json +0 -39
- package/skills/archify/recipes/scenarios.mjs +0 -391
- package/skills/archify/references/authoring-contract.md +0 -247
- package/skills/archify/references/brand-marks.md +0 -65
- package/skills/archify/references/delivery-contract.md +0 -138
- package/skills/archify/references/viewer-runtime.md +0 -45
- package/skills/archify/renderers/architecture/grid.mjs +0 -62
- package/skills/archify/renderers/architecture/render-architecture.mjs +0 -1078
- package/skills/archify/renderers/dataflow/README.md +0 -104
- package/skills/archify/renderers/dataflow/render-dataflow.mjs +0 -483
- package/skills/archify/renderers/lifecycle/README.md +0 -115
- package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +0 -561
- package/skills/archify/renderers/sequence/README.md +0 -114
- package/skills/archify/renderers/sequence/render-sequence.mjs +0 -464
- package/skills/archify/renderers/shared/brand-marks.mjs +0 -563
- package/skills/archify/renderers/shared/cli.mjs +0 -218
- package/skills/archify/renderers/shared/desktop-readability.mjs +0 -26
- package/skills/archify/renderers/shared/diagnostics.mjs +0 -160
- package/skills/archify/renderers/shared/engineering-profiles.mjs +0 -157
- package/skills/archify/renderers/shared/generated-brand-marks.mjs +0 -2003
- package/skills/archify/renderers/shared/generated-validators.mjs +0 -13
- package/skills/archify/renderers/shared/geometry.mjs +0 -1423
- package/skills/archify/renderers/shared/i18n.mjs +0 -595
- package/skills/archify/renderers/shared/layout-report.mjs +0 -40
- package/skills/archify/renderers/shared/legend.mjs +0 -217
- package/skills/archify/renderers/shared/output-path.mjs +0 -340
- package/skills/archify/renderers/shared/repository-evidence.mjs +0 -239
- package/skills/archify/renderers/shared/repository-location.mjs +0 -58
- package/skills/archify/renderers/shared/text-fit.mjs +0 -49
- package/skills/archify/renderers/shared/utils.mjs +0 -232
- package/skills/archify/renderers/shared/validator.mjs +0 -86
- package/skills/archify/renderers/workflow/README.md +0 -223
- package/skills/archify/renderers/workflow/render-workflow.mjs +0 -35
- package/skills/archify/renderers/workflow/workflow-compiler.mjs +0 -4400
- package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +0 -144
- package/skills/archify/schemas/README.md +0 -211
- package/skills/archify/schemas/architecture.schema.json +0 -178
- package/skills/archify/schemas/common.schema.json +0 -115
- package/skills/archify/scripts/check-render-output.mjs +0 -916
- package/skills/archify/scripts/check-update.mjs +0 -1667
- package/skills/archify/scripts/generate-brand-marks.mjs +0 -141
- package/skills/archify/scripts/generate-validators.mjs +0 -66
- package/skills/archify/scripts/render-examples.mjs +0 -26
- package/skills/archify/scripts/update-contract.mjs +0 -182
- package/skills/archify/skill-release.json +0 -10
- package/skills/archify/test/adaptive-reader-layout.test.mjs +0 -86
- package/skills/archify/test/animation.test.mjs +0 -179
- package/skills/archify/test/architecture-compare-recovery.test.mjs +0 -157
- package/skills/archify/test/architecture-delta-markers.test.mjs +0 -137
- package/skills/archify/test/architecture-delta.test.mjs +0 -705
- package/skills/archify/test/artifact-receipt-flush.test.mjs +0 -126
- package/skills/archify/test/authored-reachability.test.mjs +0 -95
- package/skills/archify/test/authoring-safety-contract.test.mjs +0 -54
- package/skills/archify/test/automatic-port-spread.test.mjs +0 -376
- package/skills/archify/test/base-input-compatibility.test.mjs +0 -42
- package/skills/archify/test/brand-marks.test.mjs +0 -678
- package/skills/archify/test/chapter-delta-preview.test.mjs +0 -157
- package/skills/archify/test/chapter-handoff.test.mjs +0 -97
- package/skills/archify/test/chapter-rail.test.mjs +0 -86
- package/skills/archify/test/checkout-line-endings.test.mjs +0 -72
- package/skills/archify/test/ci-scope.test.mjs +0 -59
- package/skills/archify/test/clean-skill-staging.test.mjs +0 -440
- package/skills/archify/test/cli-output-types.test.mjs +0 -201
- package/skills/archify/test/cli.test.mjs +0 -986
- package/skills/archify/test/community-proof-intake.test.mjs +0 -125
- package/skills/archify/test/cursor-onboarding.test.mjs +0 -74
- package/skills/archify/test/degraded.test.mjs +0 -161
- package/skills/archify/test/delivery-contract.test.mjs +0 -52
- package/skills/archify/test/desktop-reader-browser.test.mjs +0 -80
- package/skills/archify/test/diagram-guide.test.mjs +0 -92
- package/skills/archify/test/engineering-profile.test.mjs +0 -209
- package/skills/archify/test/export-browser.test.mjs +0 -305
- package/skills/archify/test/export-cleanup-browser.test.mjs +0 -294
- package/skills/archify/test/finder-browser.test.mjs +0 -274
- package/skills/archify/test/finder.test.mjs +0 -104
- package/skills/archify/test/fixtures/automatic-routing-node-border-clearance.workflow.json +0 -40
- package/skills/archify/test/fixtures/fail-migration-cleanup.mjs +0 -17
- package/skills/archify/test/fixtures/issue-126/custom-widths.workflow.json +0 -18
- package/skills/archify/test/fixtures/issue-126/explicit-route.workflow.json +0 -28
- package/skills/archify/test/fixtures/issue-126/explicit-viewbox.workflow.json +0 -19
- package/skills/archify/test/fixtures/issue-126/labels.workflow.json +0 -18
- package/skills/archify/test/fixtures/v1-baseline/agent-run.lifecycle.json +0 -64
- package/skills/archify/test/fixtures/v1-baseline/agent-tool-call.workflow.json +0 -88
- package/skills/archify/test/fixtures/v1-baseline/cache-miss-request.sequence.json +0 -75
- package/skills/archify/test/fixtures/v1-baseline/event-stream.dataflow.json +0 -57
- package/skills/archify/test/fixtures/v1-baseline/product-analytics.dataflow.json +0 -70
- package/skills/archify/test/fixtures/v1-baseline/production-deployment.architecture.json +0 -71
- package/skills/archify/test/fixtures/v1-workflow-700x400.workflow.json +0 -22
- package/skills/archify/test/fixtures/v1-workflow-explicit-coordinates.workflow.json +0 -47
- package/skills/archify/test/focus-browser.test.mjs +0 -226
- package/skills/archify/test/gallery.test.mjs +0 -130
- package/skills/archify/test/generate-validators.test.mjs +0 -59
- package/skills/archify/test/generate-viewer.test.mjs +0 -165
- package/skills/archify/test/generated-artifact-xml.test.mjs +0 -88
- package/skills/archify/test/geometry.test.mjs +0 -739
- package/skills/archify/test/golden.mjs +0 -207
- package/skills/archify/test/grid.test.mjs +0 -47
- package/skills/archify/test/guide-page.test.mjs +0 -85
- package/skills/archify/test/guide.test.mjs +0 -81
- package/skills/archify/test/guided-views-browser.test.mjs +0 -460
- package/skills/archify/test/guided-views.test.mjs +0 -144
- package/skills/archify/test/helpers/desktop-browser.mjs +0 -48
- package/skills/archify/test/helpers/offline-fonts.mjs +0 -76
- package/skills/archify/test/helpers/viewer-click.mjs +0 -72
- package/skills/archify/test/helpers/xml.mjs +0 -37
- package/skills/archify/test/i18n.test.mjs +0 -422
- package/skills/archify/test/intent-trace-browser.test.mjs +0 -284
- package/skills/archify/test/intent-trace.test.mjs +0 -111
- package/skills/archify/test/landing.test.mjs +0 -97
- package/skills/archify/test/layout-rules.test.mjs +0 -1498
- package/skills/archify/test/legend-contract.test.mjs +0 -404
- package/skills/archify/test/motion-governor-browser.test.mjs +0 -342
- package/skills/archify/test/motion-governor.test.mjs +0 -111
- package/skills/archify/test/offline-font-browser.test.mjs +0 -210
- package/skills/archify/test/offline-self-containment.test.mjs +0 -80
- package/skills/archify/test/open-artifact.test.mjs +0 -118
- package/skills/archify/test/ordinary-model-floor.test.mjs +0 -1052
- package/skills/archify/test/output-path.test.mjs +0 -587
- package/skills/archify/test/presentation.test.mjs +0 -71
- package/skills/archify/test/preset-tryon.test.mjs +0 -113
- package/skills/archify/test/preview-contract.test.mjs +0 -40
- package/skills/archify/test/preview.test.mjs +0 -530
- package/skills/archify/test/proof-aperture.test.mjs +0 -78
- package/skills/archify/test/reach-share-card.test.mjs +0 -149
- package/skills/archify/test/reader-layout-browser.test.mjs +0 -320
- package/skills/archify/test/readme-showcase.test.mjs +0 -319
- package/skills/archify/test/real-repository-proof.test.mjs +0 -209
- package/skills/archify/test/relationship-direct-explorer.test.mjs +0 -115
- package/skills/archify/test/relationship-lens.test.mjs +0 -128
- package/skills/archify/test/relationship-permalink.test.mjs +0 -130
- package/skills/archify/test/relationship-pulse.test.mjs +0 -122
- package/skills/archify/test/release-identity.test.mjs +0 -476
- package/skills/archify/test/release-package-gates.test.mjs +0 -750
- package/skills/archify/test/render-output-checks.test.mjs +0 -588
- package/skills/archify/test/renderer-diagnostic-boundary.test.mjs +0 -107
- package/skills/archify/test/repair-receipt.test.mjs +0 -181
- package/skills/archify/test/repository-evidence-replacement.test.mjs +0 -136
- package/skills/archify/test/repository-evidence.test.mjs +0 -605
- package/skills/archify/test/repository-language-metadata.test.mjs +0 -67
- package/skills/archify/test/route-journey.test.mjs +0 -125
- package/skills/archify/test/route-probe-browser.test.mjs +0 -363
- package/skills/archify/test/route-probe.test.mjs +0 -114
- package/skills/archify/test/route-share-card.test.mjs +0 -163
- package/skills/archify/test/semantic-camera.test.mjs +0 -88
- package/skills/archify/test/semantic-flow.test.mjs +0 -90
- package/skills/archify/test/semantic-legend-gateway.test.mjs +0 -156
- package/skills/archify/test/semantic-lens-browser.test.mjs +0 -372
- package/skills/archify/test/semantic-lens.test.mjs +0 -103
- package/skills/archify/test/semantic-passport.test.mjs +0 -99
- package/skills/archify/test/semantic-radar.test.mjs +0 -735
- package/skills/archify/test/semantic-zoom.test.mjs +0 -86
- package/skills/archify/test/sequence-column-fit.test.mjs +0 -137
- package/skills/archify/test/settled-flow.test.mjs +0 -97
- package/skills/archify/test/share-card-export.test.mjs +0 -132
- package/skills/archify/test/site-language-continuity.test.mjs +0 -569
- package/skills/archify/test/site-language-integration.mjs +0 -9
- package/skills/archify/test/skill-metadata.test.mjs +0 -84
- package/skills/archify/test/stable-update-manifest.test.mjs +0 -167
- package/skills/archify/test/start-page.test.mjs +0 -312
- package/skills/archify/test/story-beat-navigator.test.mjs +0 -103
- package/skills/archify/test/story-carrier.test.mjs +0 -90
- package/skills/archify/test/story-director-strip.test.mjs +0 -91
- package/skills/archify/test/story-follow-camera.test.mjs +0 -103
- package/skills/archify/test/story-horizon.test.mjs +0 -91
- package/skills/archify/test/story-moment-link.test.mjs +0 -93
- package/skills/archify/test/story-shelf.test.mjs +0 -84
- package/skills/archify/test/story-trail.test.mjs +0 -81
- package/skills/archify/test/toolbar-polish.test.mjs +0 -57
- package/skills/archify/test/update-contract.test.mjs +0 -133
- package/skills/archify/test/update-notifier.test.mjs +0 -3317
- package/skills/archify/test/v1-compatibility.test.mjs +0 -329
- package/skills/archify/test/vertical-edge.test.mjs +0 -68
- package/skills/archify/test/viewer-camera-browser.test.mjs +0 -395
- package/skills/archify/test/viewer-chrome-layout.test.mjs +0 -1104
- package/skills/archify/test/viewer-identifiers-browser.test.mjs +0 -135
- package/skills/archify/test/visual-check.test.mjs +0 -400
- package/skills/archify/test/webm-artifact.smoke.mjs +0 -1855
- package/skills/archify/test/workflow-compiler-hard-contract.test.mjs +0 -2136
- package/skills/archify/test/workflow-compiler.test.mjs +0 -1163
- package/skills/archify/test/workflow-migration.test.mjs +0 -620
- package/skills/archify/test/workflow-semantic-contract.test.mjs +0 -131
- /package/{skills/archify/THIRD_PARTY_NOTICES.md → dist/phoenix-diagram/THIRD-PARTY-LICENSES/0b7fb59365dc21c93b0f62526d74ee913aeec9ae532c112bc76afa7edcd56e14.txt} +0 -0
- /package/{skills/archify/LICENSE → dist/phoenix-diagram/THIRD-PARTY-LICENSES/b799ab081703e7821ae5096d2c1abdf14bbdc75e8ea1c4045998c36ed6db9706.txt} +0 -0
- /package/{skills/archify/assets/JetBrainsMono-OFL.txt → dist/phoenix-diagram/THIRD-PARTY-LICENSES/c1ab7c666206842a02b35b30770dac0d7a10156ed401c9defc3f02a754d89e90.txt} +0 -0
- /package/{skills/archify/examples/agent-tool-call.workflow.json → dist/phoenix-diagram/bundle/examples/legacy.agent-tool-call.workflow.json} +0 -0
- /package/{skills/archify/examples/async-job-roundtrip.sequence.json → dist/phoenix-diagram/bundle/examples/legacy.async-job-roundtrip.sequence.json} +0 -0
- /package/{skills/archify/examples/deployment-release.lifecycle.json → dist/phoenix-diagram/bundle/examples/legacy.deployment-release.lifecycle.json} +0 -0
- /package/{skills/archify/examples/product-analytics.dataflow.json → dist/phoenix-diagram/bundle/examples/legacy.product-analytics.dataflow.json} +0 -0
- /package/{skills/archify/examples/web-app.architecture.json → dist/phoenix-diagram/bundle/examples/legacy.web-app.architecture.json} +0 -0
|
@@ -1,3317 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import crypto from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import fsPromises from 'node:fs/promises';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import test from 'node:test';
|
|
9
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
acknowledgeUpdate,
|
|
13
|
-
checkForUpdate,
|
|
14
|
-
} from '../scripts/check-update.mjs';
|
|
15
|
-
import { DEFAULT_MANIFEST_URL, compareSemver, parseSemver } from '../scripts/update-contract.mjs';
|
|
16
|
-
|
|
17
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const skillRoot = path.resolve(here, '..');
|
|
19
|
-
const checkerPath = path.join(skillRoot, 'scripts', 'check-update.mjs');
|
|
20
|
-
const contractPath = path.join(skillRoot, 'scripts', 'update-contract.mjs');
|
|
21
|
-
const expectedRepository = 'https://github.com/tt-a1i/archify';
|
|
22
|
-
const expectedManifestUrl = 'https://tt-a1i.github.io/archify/skill-updates/archify/stable.json';
|
|
23
|
-
const baseTime = Date.parse('2026-08-28T08:00:00Z');
|
|
24
|
-
const childCheckTimeoutMs = 2_000;
|
|
25
|
-
const parentCheckTimeoutMs = 5_000;
|
|
26
|
-
const maxCacheStateBytes = 64 * 1_024;
|
|
27
|
-
|
|
28
|
-
function writeJson(target, value) {
|
|
29
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
30
|
-
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function compactStateSource(state) {
|
|
34
|
-
return `${JSON.stringify(state)}\n`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function metadataWithOverrides(metadata, overrides) {
|
|
38
|
-
return new Proxy(metadata, {
|
|
39
|
-
get(target, property) {
|
|
40
|
-
if (Object.hasOwn(overrides, property)) return overrides[property];
|
|
41
|
-
const value = Reflect.get(target, property, target);
|
|
42
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function historyDigest(index) {
|
|
48
|
-
return `sha256:${crypto.createHash('sha256').update(`history-${index}`).digest('hex')}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function cachedStateWithHistory({ offeredDigests = [], acknowledgedDigests = [] } = {}) {
|
|
52
|
-
return {
|
|
53
|
-
schemaVersion: 1,
|
|
54
|
-
skillId: 'archify',
|
|
55
|
-
installedVersion: '2.15.0',
|
|
56
|
-
check: {
|
|
57
|
-
nextCheckAt: '2020-01-01T00:00:00.000Z',
|
|
58
|
-
consecutiveFailures: 0,
|
|
59
|
-
},
|
|
60
|
-
notification: {
|
|
61
|
-
offeredDigests: [...offeredDigests],
|
|
62
|
-
acknowledgedDigests: [...acknowledgedDigests],
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function candidateStateForDigest(state, digest) {
|
|
68
|
-
return {
|
|
69
|
-
...state,
|
|
70
|
-
check: {
|
|
71
|
-
nextCheckAt: new Date(baseTime + (72 * 60 * 60 * 1_000)).toISOString(),
|
|
72
|
-
consecutiveFailures: 0,
|
|
73
|
-
},
|
|
74
|
-
notification: {
|
|
75
|
-
offeredDigests: [...state.notification.offeredDigests, digest],
|
|
76
|
-
acknowledgedDigests: [...state.notification.acknowledgedDigests],
|
|
77
|
-
},
|
|
78
|
-
candidate: {
|
|
79
|
-
version: '2.16.0',
|
|
80
|
-
targetDigest: digest,
|
|
81
|
-
severity: 'normal',
|
|
82
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function writeCompactCommittedState(testFixture, state, generation = 1n) {
|
|
88
|
-
const target = path.join(operationPath(testFixture, 'committed', generation), 'state.json');
|
|
89
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
90
|
-
fs.writeFileSync(target, compactStateSource(state));
|
|
91
|
-
return target;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function committedStateFiles(testFixture) {
|
|
95
|
-
return fs.readdirSync(stateDirectory(testFixture), { withFileTypes: true })
|
|
96
|
-
.filter((entry) => entry.isDirectory() && /^committed-\d+$/.test(entry.name))
|
|
97
|
-
.map((entry) => path.join(stateDirectory(testFixture), entry.name, 'state.json'));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function localRelease(version = '2.15.0') {
|
|
101
|
-
return {
|
|
102
|
-
schemaVersion: 1,
|
|
103
|
-
skillId: 'archify',
|
|
104
|
-
channel: version.includes('-') ? 'development' : 'stable',
|
|
105
|
-
version,
|
|
106
|
-
source: { repository: expectedRepository },
|
|
107
|
-
updateManifestUrl: expectedManifestUrl,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function remoteRelease(overrides = {}) {
|
|
112
|
-
return {
|
|
113
|
-
schemaVersion: 1,
|
|
114
|
-
skillId: 'archify',
|
|
115
|
-
channel: 'stable',
|
|
116
|
-
version: '2.16.0',
|
|
117
|
-
publishedAt: '2026-08-28T07:00:00Z',
|
|
118
|
-
source: {
|
|
119
|
-
repository: expectedRepository,
|
|
120
|
-
ref: 'v2.16.0',
|
|
121
|
-
treeSha: 'a'.repeat(40),
|
|
122
|
-
},
|
|
123
|
-
artifact: {
|
|
124
|
-
sha256: 'b'.repeat(64),
|
|
125
|
-
},
|
|
126
|
-
summary: 'Improve large-repository scanning and diagram layout.',
|
|
127
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
|
|
128
|
-
severity: 'normal',
|
|
129
|
-
...overrides,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function remoteReleaseForVersion(version, digest = 'b'.repeat(64)) {
|
|
134
|
-
const release = remoteRelease();
|
|
135
|
-
return {
|
|
136
|
-
...release,
|
|
137
|
-
version,
|
|
138
|
-
source: { ...release.source, ref: `v${version}` },
|
|
139
|
-
artifact: { sha256: digest },
|
|
140
|
-
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${version}`,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function response(body, { status = 200, etag = '"archify-2.16.0"' } = {}) {
|
|
145
|
-
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
|
|
146
|
-
status,
|
|
147
|
-
headers: {
|
|
148
|
-
'content-type': 'application/json',
|
|
149
|
-
etag,
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function fixture(version = '2.15.0') {
|
|
155
|
-
const root = fs.realpathSync(fs.mkdtempSync(
|
|
156
|
-
path.join(os.tmpdir(), 'archify-update-notifier-'),
|
|
157
|
-
));
|
|
158
|
-
const releasePath = path.join(root, 'skill-release.json');
|
|
159
|
-
const cacheDirectory = path.join(root, 'cache');
|
|
160
|
-
writeJson(releasePath, localRelease(version));
|
|
161
|
-
return { root, releasePath, cacheDirectory };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function stateDirectory(testFixture, version = '2.15.0') {
|
|
165
|
-
const partition = crypto.createHash('sha256').update(version).digest('hex').slice(0, 24);
|
|
166
|
-
return path.join(testFixture.cacheDirectory, `version-${partition}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function statePath(testFixture, version = '2.15.0') {
|
|
170
|
-
const directory = stateDirectory(testFixture, version);
|
|
171
|
-
let committed = [];
|
|
172
|
-
try {
|
|
173
|
-
committed = fs.readdirSync(directory)
|
|
174
|
-
.filter((name) => /^committed-\d+$/.test(name))
|
|
175
|
-
.sort((left, right) => {
|
|
176
|
-
const leftGeneration = BigInt(left.slice('committed-'.length));
|
|
177
|
-
const rightGeneration = BigInt(right.slice('committed-'.length));
|
|
178
|
-
return leftGeneration > rightGeneration ? -1 : 1;
|
|
179
|
-
});
|
|
180
|
-
} catch {
|
|
181
|
-
// A seed may be written before the cache partition exists.
|
|
182
|
-
}
|
|
183
|
-
return committed[0]
|
|
184
|
-
? path.join(directory, committed[0], 'state.json')
|
|
185
|
-
: path.join(operationPath(testFixture, 'committed', 1n, version), 'state.json');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function operationPath(testFixture, kind, generation, version = '2.15.0') {
|
|
189
|
-
return path.join(
|
|
190
|
-
stateDirectory(testFixture, version),
|
|
191
|
-
`${kind}-${BigInt(generation).toString().padStart(20, '0')}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function writePendingOperation(testFixture, {
|
|
196
|
-
generation = 1n,
|
|
197
|
-
owner = { pid: process.pid, token: 'e'.repeat(32) },
|
|
198
|
-
modifiedAt = new Date(),
|
|
199
|
-
} = {}) {
|
|
200
|
-
const directory = operationPath(testFixture, 'pending', generation);
|
|
201
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
202
|
-
const ownerPath = path.join(directory, 'owner.json');
|
|
203
|
-
fs.writeFileSync(ownerPath, typeof owner === 'string' ? owner : `${JSON.stringify(owner)}\n`);
|
|
204
|
-
fs.utimesSync(ownerPath, modifiedAt, modifiedAt);
|
|
205
|
-
return directory;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function pauseMkdirOnce(targetPath) {
|
|
209
|
-
const originalMkdir = fsPromises.mkdir;
|
|
210
|
-
let release;
|
|
211
|
-
let markReached;
|
|
212
|
-
let intercepted = false;
|
|
213
|
-
const reached = new Promise((resolve) => { markReached = resolve; });
|
|
214
|
-
const gate = new Promise((resolve) => { release = resolve; });
|
|
215
|
-
fsPromises.mkdir = async (target, ...args) => {
|
|
216
|
-
if (!intercepted && path.resolve(target) === path.resolve(targetPath)) {
|
|
217
|
-
intercepted = true;
|
|
218
|
-
markReached();
|
|
219
|
-
await gate;
|
|
220
|
-
}
|
|
221
|
-
return originalMkdir(target, ...args);
|
|
222
|
-
};
|
|
223
|
-
return {
|
|
224
|
-
reached,
|
|
225
|
-
release,
|
|
226
|
-
restore() {
|
|
227
|
-
release();
|
|
228
|
-
fsPromises.mkdir = originalMkdir;
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function pauseReaddirSnapshot(targetDirectory, callNumber) {
|
|
234
|
-
const originalReaddir = fsPromises.readdir;
|
|
235
|
-
let release;
|
|
236
|
-
let markReached;
|
|
237
|
-
let calls = 0;
|
|
238
|
-
const reached = new Promise((resolve) => { markReached = resolve; });
|
|
239
|
-
const gate = new Promise((resolve) => { release = resolve; });
|
|
240
|
-
fsPromises.readdir = async (target, ...args) => {
|
|
241
|
-
const result = await originalReaddir(target, ...args);
|
|
242
|
-
if (path.resolve(target) === path.resolve(targetDirectory) && ++calls === callNumber) {
|
|
243
|
-
markReached();
|
|
244
|
-
await gate;
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
};
|
|
248
|
-
return {
|
|
249
|
-
reached,
|
|
250
|
-
release,
|
|
251
|
-
restore() {
|
|
252
|
-
release();
|
|
253
|
-
fsPromises.readdir = originalReaddir;
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function pauseReaddirSnapshotOnce(targetDirectory, predicate) {
|
|
259
|
-
const originalReaddir = fsPromises.readdir;
|
|
260
|
-
let release;
|
|
261
|
-
let markReached;
|
|
262
|
-
let intercepted = false;
|
|
263
|
-
const reached = new Promise((resolve) => { markReached = resolve; });
|
|
264
|
-
const gate = new Promise((resolve) => { release = resolve; });
|
|
265
|
-
fsPromises.readdir = async (target, ...args) => {
|
|
266
|
-
const result = await originalReaddir(target, ...args);
|
|
267
|
-
if (!intercepted
|
|
268
|
-
&& path.resolve(target) === path.resolve(targetDirectory)
|
|
269
|
-
&& predicate(result)) {
|
|
270
|
-
intercepted = true;
|
|
271
|
-
markReached();
|
|
272
|
-
await gate;
|
|
273
|
-
}
|
|
274
|
-
return result;
|
|
275
|
-
};
|
|
276
|
-
return {
|
|
277
|
-
reached,
|
|
278
|
-
release,
|
|
279
|
-
restore() {
|
|
280
|
-
release();
|
|
281
|
-
fsPromises.readdir = originalReaddir;
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function pauseOpenOnce(predicate) {
|
|
287
|
-
const originalOpen = fsPromises.open;
|
|
288
|
-
let release;
|
|
289
|
-
let markReached;
|
|
290
|
-
let intercepted = false;
|
|
291
|
-
const reached = new Promise((resolve) => { markReached = resolve; });
|
|
292
|
-
const gate = new Promise((resolve) => { release = resolve; });
|
|
293
|
-
fsPromises.open = async (target, ...args) => {
|
|
294
|
-
if (!intercepted && predicate(target)) {
|
|
295
|
-
intercepted = true;
|
|
296
|
-
markReached();
|
|
297
|
-
await gate;
|
|
298
|
-
}
|
|
299
|
-
return originalOpen(target, ...args);
|
|
300
|
-
};
|
|
301
|
-
return {
|
|
302
|
-
reached,
|
|
303
|
-
release,
|
|
304
|
-
restore() {
|
|
305
|
-
release();
|
|
306
|
-
fsPromises.open = originalOpen;
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function pauseWriteOnce(basename) {
|
|
312
|
-
return pauseOpenOnce((target) => path.basename(target) === basename);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function pauseRenameMatchingOnce(predicate) {
|
|
316
|
-
const originalRename = fsPromises.rename;
|
|
317
|
-
let release;
|
|
318
|
-
let markReached;
|
|
319
|
-
let intercepted = false;
|
|
320
|
-
const reached = new Promise((resolve) => { markReached = resolve; });
|
|
321
|
-
const gate = new Promise((resolve) => { release = resolve; });
|
|
322
|
-
fsPromises.rename = async (source, destination, ...args) => {
|
|
323
|
-
if (!intercepted && predicate(source, destination)) {
|
|
324
|
-
intercepted = true;
|
|
325
|
-
markReached();
|
|
326
|
-
await gate;
|
|
327
|
-
}
|
|
328
|
-
return originalRename(source, destination, ...args);
|
|
329
|
-
};
|
|
330
|
-
return {
|
|
331
|
-
reached,
|
|
332
|
-
release,
|
|
333
|
-
restore() {
|
|
334
|
-
release();
|
|
335
|
-
fsPromises.rename = originalRename;
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function pauseRenameOnce(sourcePath, destinationPath) {
|
|
341
|
-
return pauseRenameMatchingOnce((source, destination) => (
|
|
342
|
-
path.resolve(source) === path.resolve(sourcePath)
|
|
343
|
-
&& path.resolve(destination) === path.resolve(destinationPath)
|
|
344
|
-
));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function options(testFixture, fetchImpl, overrides = {}) {
|
|
348
|
-
return {
|
|
349
|
-
releasePath: testFixture.releasePath,
|
|
350
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
351
|
-
fetchImpl,
|
|
352
|
-
now: () => baseTime,
|
|
353
|
-
random: () => 0.5,
|
|
354
|
-
timeoutMs: 50,
|
|
355
|
-
...overrides,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function cachedUpdateState() {
|
|
360
|
-
return {
|
|
361
|
-
schemaVersion: 1,
|
|
362
|
-
skillId: 'archify',
|
|
363
|
-
installedVersion: '2.15.0',
|
|
364
|
-
check: {
|
|
365
|
-
nextCheckAt: new Date(baseTime + (24 * 60 * 60 * 1_000)).toISOString(),
|
|
366
|
-
consecutiveFailures: 0,
|
|
367
|
-
},
|
|
368
|
-
notification: {
|
|
369
|
-
offeredDigests: [`sha256:${'b'.repeat(64)}`],
|
|
370
|
-
acknowledgedDigests: [],
|
|
371
|
-
},
|
|
372
|
-
candidate: {
|
|
373
|
-
version: '2.16.0',
|
|
374
|
-
targetDigest: `sha256:${'b'.repeat(64)}`,
|
|
375
|
-
severity: 'normal',
|
|
376
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function createFifo(target) {
|
|
382
|
-
const created = spawnSync('mkfifo', [target], { encoding: 'utf8' });
|
|
383
|
-
if (created.error?.code === 'ENOENT') return false;
|
|
384
|
-
assert.equal(created.status, 0, created.stderr || created.error?.message);
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function runCheckInChild(testFixture) {
|
|
389
|
-
const manifest = remoteReleaseForVersion('2.15.0', 'd'.repeat(64));
|
|
390
|
-
const childSource = `
|
|
391
|
-
import { checkForUpdate } from ${JSON.stringify(pathToFileURL(checkerPath).href)};
|
|
392
|
-
const watchdog = setTimeout(() => {
|
|
393
|
-
process.stderr.write('child update check timed out\\n');
|
|
394
|
-
process.exit(124);
|
|
395
|
-
}, ${childCheckTimeoutMs});
|
|
396
|
-
let requests = 0;
|
|
397
|
-
try {
|
|
398
|
-
const result = await checkForUpdate({
|
|
399
|
-
releasePath: ${JSON.stringify(testFixture.releasePath)},
|
|
400
|
-
cacheDirectory: ${JSON.stringify(testFixture.cacheDirectory)},
|
|
401
|
-
fetchImpl: async () => {
|
|
402
|
-
requests += 1;
|
|
403
|
-
return new Response(${JSON.stringify(JSON.stringify(manifest))}, {
|
|
404
|
-
status: 200,
|
|
405
|
-
headers: { 'content-type': 'application/json' },
|
|
406
|
-
});
|
|
407
|
-
},
|
|
408
|
-
now: () => ${baseTime},
|
|
409
|
-
random: () => 0.5,
|
|
410
|
-
timeoutMs: 50,
|
|
411
|
-
});
|
|
412
|
-
process.stdout.write(JSON.stringify({ result, requests }));
|
|
413
|
-
} finally {
|
|
414
|
-
clearTimeout(watchdog);
|
|
415
|
-
}
|
|
416
|
-
`;
|
|
417
|
-
const child = spawnSync(process.execPath, ['--input-type=module', '--eval', childSource], {
|
|
418
|
-
cwd: skillRoot,
|
|
419
|
-
encoding: 'utf8',
|
|
420
|
-
timeout: parentCheckTimeoutMs,
|
|
421
|
-
});
|
|
422
|
-
const diagnostics = [child.error?.stack, child.stderr, child.stdout].filter(Boolean).join('\n');
|
|
423
|
-
assert.equal(child.error, undefined, diagnostics);
|
|
424
|
-
assert.equal(child.status, 0, diagnostics);
|
|
425
|
-
return JSON.parse(child.stdout);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function assertUnsafeCacheStateIsIgnored(testFixture) {
|
|
429
|
-
assert.deepEqual(runCheckInChild(testFixture), {
|
|
430
|
-
result: { status: 'silent', reason: 'current' },
|
|
431
|
-
requests: 1,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
test('production manifest URL is a fixed trusted GitHub Pages resource', () => {
|
|
436
|
-
assert.equal(DEFAULT_MANIFEST_URL, expectedManifestUrl);
|
|
437
|
-
const local = JSON.parse(fs.readFileSync(path.join(skillRoot, 'skill-release.json'), 'utf8'));
|
|
438
|
-
assert.equal(local.updateManifestUrl, expectedManifestUrl);
|
|
439
|
-
assert.equal(local.source.repository, expectedRepository);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test('SemVer comparison handles stable, prerelease, and downgrade ordering', () => {
|
|
443
|
-
assert.equal(compareSemver('2.16.0', '2.15.0'), 1);
|
|
444
|
-
assert.equal(compareSemver('2.16.0-dev.0', '2.15.0'), 1);
|
|
445
|
-
assert.equal(compareSemver('2.16.0', '2.16.0-dev.9'), 1);
|
|
446
|
-
assert.equal(compareSemver('2.16.0-dev.2', '2.16.0-dev.10'), -1);
|
|
447
|
-
assert.equal(compareSemver('2.16.0', '2.16.0'), 0);
|
|
448
|
-
assert.equal(compareSemver('2.16.0+build.9', '2.16.0+build.1'), 0);
|
|
449
|
-
assert.equal(compareSemver('9007199254740993.0.0', '9007199254740992.0.0'), 1);
|
|
450
|
-
assert.equal(compareSemver('2.16.0-dev.9007199254740993', '2.16.0-dev.9007199254740992'), 1);
|
|
451
|
-
assert.throws(() => compareSemver('2.16.0-dev.01', '2.16.0'));
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
test('development installs never treat the older stable release as an update', async (t) => {
|
|
455
|
-
const testFixture = fixture('2.16.0-dev.0');
|
|
456
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
457
|
-
|
|
458
|
-
const result = await checkForUpdate(options(
|
|
459
|
-
testFixture,
|
|
460
|
-
async () => response(remoteRelease({
|
|
461
|
-
version: '2.15.0',
|
|
462
|
-
source: {
|
|
463
|
-
repository: expectedRepository,
|
|
464
|
-
ref: 'v2.15.0',
|
|
465
|
-
treeSha: 'c'.repeat(40),
|
|
466
|
-
},
|
|
467
|
-
artifact: { sha256: 'd'.repeat(64) },
|
|
468
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.15.0',
|
|
469
|
-
})),
|
|
470
|
-
));
|
|
471
|
-
|
|
472
|
-
assert.deepEqual(result, { status: 'silent', reason: 'current' });
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test('a changed digest never bypasses same-version or downgrade protection', async () => {
|
|
476
|
-
for (const [version, digest] of [
|
|
477
|
-
['2.15.0', 'c'.repeat(64)],
|
|
478
|
-
['2.14.9', 'd'.repeat(64)],
|
|
479
|
-
]) {
|
|
480
|
-
const testFixture = fixture();
|
|
481
|
-
try {
|
|
482
|
-
const result = await checkForUpdate(options(
|
|
483
|
-
testFixture,
|
|
484
|
-
async () => response(remoteReleaseForVersion(version, digest)),
|
|
485
|
-
));
|
|
486
|
-
assert.deepEqual(result, { status: 'silent', reason: 'current' });
|
|
487
|
-
} finally {
|
|
488
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
test('a successful refresh withdraws a previously offered higher candidate', async (t) => {
|
|
494
|
-
const testFixture = fixture();
|
|
495
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
496
|
-
let requests = 0;
|
|
497
|
-
const fetchImpl = async () => {
|
|
498
|
-
requests += 1;
|
|
499
|
-
return response(requests === 1
|
|
500
|
-
? remoteReleaseForVersion('3.0.0', 'c'.repeat(64))
|
|
501
|
-
: remoteReleaseForVersion('2.15.0', 'd'.repeat(64)));
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const offered = await checkForUpdate(options(testFixture, fetchImpl));
|
|
505
|
-
assert.equal(offered.status, 'update_available');
|
|
506
|
-
assert.equal(offered.latestVersion, '3.0.0');
|
|
507
|
-
|
|
508
|
-
const withdrawn = await checkForUpdate(options(testFixture, fetchImpl, {
|
|
509
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
510
|
-
}));
|
|
511
|
-
assert.deepEqual(withdrawn, { status: 'silent', reason: 'current' });
|
|
512
|
-
assert.equal(JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).candidate.version, '2.15.0');
|
|
513
|
-
assert.equal(requests, 2);
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
test('a newer immutable candidate is re-offered until the visible notice is acknowledged', async (t) => {
|
|
517
|
-
const testFixture = fixture();
|
|
518
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
519
|
-
let requests = 0;
|
|
520
|
-
const fetchImpl = async () => {
|
|
521
|
-
requests += 1;
|
|
522
|
-
return response(remoteRelease());
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const first = await checkForUpdate(options(testFixture, fetchImpl));
|
|
526
|
-
assert.equal(first.status, 'update_available');
|
|
527
|
-
assert.equal(first.installedVersion, '2.15.0');
|
|
528
|
-
assert.equal(first.latestVersion, '2.16.0');
|
|
529
|
-
assert.equal(first.eventKey, `archify@sha256:${'b'.repeat(64)}`);
|
|
530
|
-
assert.equal(first.targetDigest, `sha256:${'b'.repeat(64)}`);
|
|
531
|
-
assert.equal(Object.hasOwn(first, 'updateCommand'), false);
|
|
532
|
-
assert.equal(first.summary, 'Archify 2.16.0 is available; see the official release notes for details.');
|
|
533
|
-
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
|
|
534
|
-
assert.deepEqual(Object.keys(persisted.check).sort(), [
|
|
535
|
-
'consecutiveFailures', 'nextCheckAt',
|
|
536
|
-
]);
|
|
537
|
-
assert.deepEqual(Object.keys(persisted.notification).sort(), [
|
|
538
|
-
'acknowledgedDigests', 'offeredDigests',
|
|
539
|
-
]);
|
|
540
|
-
assert.deepEqual(Object.keys(persisted.candidate).sort(), [
|
|
541
|
-
'releaseNotes', 'severity', 'targetDigest', 'version',
|
|
542
|
-
]);
|
|
543
|
-
|
|
544
|
-
const second = await checkForUpdate(options(testFixture, fetchImpl));
|
|
545
|
-
assert.equal(second.status, 'update_available');
|
|
546
|
-
assert.equal(second.eventKey, first.eventKey);
|
|
547
|
-
assert.equal(requests, 1, 'fresh cached candidates must not make another request');
|
|
548
|
-
|
|
549
|
-
const acknowledgement = await acknowledgeUpdate({
|
|
550
|
-
releasePath: testFixture.releasePath,
|
|
551
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
552
|
-
eventKey: first.eventKey,
|
|
553
|
-
now: () => baseTime + 1_000,
|
|
554
|
-
});
|
|
555
|
-
assert.deepEqual(acknowledgement, { status: 'acknowledged', eventKey: first.eventKey });
|
|
556
|
-
assert.deepEqual(
|
|
557
|
-
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification.acknowledgedDigests,
|
|
558
|
-
[first.targetDigest],
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
const third = await checkForUpdate(options(testFixture, fetchImpl));
|
|
562
|
-
assert.deepEqual(third, { status: 'silent', reason: 'already-notified' });
|
|
563
|
-
assert.equal(requests, 1);
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
test('an acknowledged candidate stays suppressed after a later candidate and manifest rollback', async (t) => {
|
|
567
|
-
const testFixture = fixture();
|
|
568
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
569
|
-
const releases = [
|
|
570
|
-
remoteReleaseForVersion('2.16.0', 'b'.repeat(64)),
|
|
571
|
-
remoteReleaseForVersion('2.17.0', 'c'.repeat(64)),
|
|
572
|
-
remoteReleaseForVersion('2.16.0', 'b'.repeat(64)),
|
|
573
|
-
];
|
|
574
|
-
let requests = 0;
|
|
575
|
-
const fetchImpl = async () => response(releases[requests++]);
|
|
576
|
-
|
|
577
|
-
const first = await checkForUpdate(options(testFixture, fetchImpl));
|
|
578
|
-
assert.equal(first.status, 'update_available');
|
|
579
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
580
|
-
releasePath: testFixture.releasePath,
|
|
581
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
582
|
-
eventKey: first.eventKey,
|
|
583
|
-
}), { status: 'acknowledged', eventKey: first.eventKey });
|
|
584
|
-
|
|
585
|
-
const second = await checkForUpdate(options(testFixture, fetchImpl, {
|
|
586
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
587
|
-
}));
|
|
588
|
-
assert.equal(second.status, 'update_available');
|
|
589
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
590
|
-
releasePath: testFixture.releasePath,
|
|
591
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
592
|
-
eventKey: second.eventKey,
|
|
593
|
-
}), { status: 'acknowledged', eventKey: second.eventKey });
|
|
594
|
-
|
|
595
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl, {
|
|
596
|
-
now: () => baseTime + (146 * 60 * 60 * 1_000),
|
|
597
|
-
})), { status: 'silent', reason: 'already-notified' });
|
|
598
|
-
assert.equal(requests, 3);
|
|
599
|
-
assert.deepEqual(
|
|
600
|
-
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification.acknowledgedDigests,
|
|
601
|
-
[first.targetDigest, second.targetDigest],
|
|
602
|
-
);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test('a 64 KiB multi-offer cache only returns an event whose acknowledgement closure can commit', async (t) => {
|
|
606
|
-
const testFixture = fixture();
|
|
607
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
608
|
-
const earlierDigest = `sha256:${'a'.repeat(64)}`;
|
|
609
|
-
const currentDigest = `sha256:${'b'.repeat(64)}`;
|
|
610
|
-
const replacementDigest = `sha256:${'c'.repeat(64)}`;
|
|
611
|
-
const cachedVersion = `3.16.${'9'.repeat(35)}`;
|
|
612
|
-
const state = cachedStateWithHistory({
|
|
613
|
-
offeredDigests: [earlierDigest, currentDigest],
|
|
614
|
-
acknowledgedDigests: Array.from({ length: 877 }, (_, index) => historyDigest(index)),
|
|
615
|
-
});
|
|
616
|
-
state.check.nextCheckAt = 'Mon, 31 Aug 2026 08:00:00 GMT';
|
|
617
|
-
state.candidate = {
|
|
618
|
-
version: cachedVersion,
|
|
619
|
-
targetDigest: currentDigest,
|
|
620
|
-
severity: 'normal',
|
|
621
|
-
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${cachedVersion}`,
|
|
622
|
-
};
|
|
623
|
-
assert.equal(Buffer.byteLength(compactStateSource(state)), maxCacheStateBytes);
|
|
624
|
-
writeCompactCommittedState(testFixture, state);
|
|
625
|
-
let requests = 0;
|
|
626
|
-
|
|
627
|
-
const offered = await checkForUpdate(options(testFixture, async () => {
|
|
628
|
-
requests += 1;
|
|
629
|
-
return response(remoteReleaseForVersion('2.16.0', 'c'.repeat(64)));
|
|
630
|
-
}));
|
|
631
|
-
assert.equal(offered.status, 'update_available');
|
|
632
|
-
const acknowledgement = await acknowledgeUpdate({
|
|
633
|
-
releasePath: testFixture.releasePath,
|
|
634
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
635
|
-
eventKey: offered.eventKey,
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
assert.deepEqual(acknowledgement, {
|
|
639
|
-
status: 'acknowledged',
|
|
640
|
-
eventKey: offered.eventKey,
|
|
641
|
-
});
|
|
642
|
-
assert.equal(requests, 1, 'an unrecoverable cached offer must be rebuilt before exposure');
|
|
643
|
-
assert.equal(offered.targetDigest, replacementDigest);
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
test('a recoverable multi-offer boundary acknowledges every event without pruning history', async (t) => {
|
|
647
|
-
const testFixture = fixture();
|
|
648
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
649
|
-
const earlierDigest = `sha256:${'a'.repeat(64)}`;
|
|
650
|
-
const currentDigest = `sha256:${'b'.repeat(64)}`;
|
|
651
|
-
const cachedVersion = `3.16.${'9'.repeat(35)}`;
|
|
652
|
-
const exactHistory = Array.from({ length: 877 }, (_, index) => historyDigest(index));
|
|
653
|
-
const state = cachedStateWithHistory({
|
|
654
|
-
offeredDigests: [earlierDigest, currentDigest],
|
|
655
|
-
acknowledgedDigests: exactHistory,
|
|
656
|
-
});
|
|
657
|
-
state.check.nextCheckAt = 'Mon, 31 Aug 2026 8:00:00 GMT';
|
|
658
|
-
state.candidate = {
|
|
659
|
-
version: cachedVersion,
|
|
660
|
-
targetDigest: currentDigest,
|
|
661
|
-
severity: 'normal',
|
|
662
|
-
releaseNotes: `https://github.com/tt-a1i/archify/releases/tag/v${cachedVersion}`,
|
|
663
|
-
};
|
|
664
|
-
assert.equal(Buffer.byteLength(compactStateSource(state)), maxCacheStateBytes - 1);
|
|
665
|
-
writeCompactCommittedState(testFixture, state);
|
|
666
|
-
|
|
667
|
-
const current = await checkForUpdate(options(testFixture, async () => {
|
|
668
|
-
throw new Error('a fresh recoverable cache must not use the network');
|
|
669
|
-
}));
|
|
670
|
-
assert.equal(current.targetDigest, currentDigest);
|
|
671
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
672
|
-
releasePath: testFixture.releasePath,
|
|
673
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
674
|
-
eventKey: current.eventKey,
|
|
675
|
-
}), { status: 'acknowledged', eventKey: current.eventKey });
|
|
676
|
-
const earlierEventKey = `archify@${earlierDigest}`;
|
|
677
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
678
|
-
releasePath: testFixture.releasePath,
|
|
679
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
680
|
-
eventKey: earlierEventKey,
|
|
681
|
-
}), { status: 'acknowledged', eventKey: earlierEventKey });
|
|
682
|
-
|
|
683
|
-
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
|
|
684
|
-
assert.deepEqual(persisted.notification, {
|
|
685
|
-
offeredDigests: [],
|
|
686
|
-
acknowledgedDigests: [...exactHistory, currentDigest, earlierDigest],
|
|
687
|
-
});
|
|
688
|
-
assert.equal(fs.statSync(statePath(testFixture)).size, maxCacheStateBytes);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
test('a near-capacity null schedule does not retry the network on every activation', async (t) => {
|
|
692
|
-
const testFixture = fixture();
|
|
693
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
694
|
-
const state = cachedStateWithHistory({
|
|
695
|
-
acknowledgedDigests: Array.from({ length: 883 }, (_, index) => historyDigest(index)),
|
|
696
|
-
});
|
|
697
|
-
state.check.nextCheckAt = null;
|
|
698
|
-
assert.equal(Buffer.byteLength(compactStateSource(state)), 65_524);
|
|
699
|
-
writeCompactCommittedState(testFixture, state);
|
|
700
|
-
let requests = 0;
|
|
701
|
-
const fetchImpl = async () => {
|
|
702
|
-
requests += 1;
|
|
703
|
-
return response(remoteRelease());
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
const first = await checkForUpdate(options(testFixture, fetchImpl));
|
|
707
|
-
const second = await checkForUpdate(options(testFixture, fetchImpl));
|
|
708
|
-
|
|
709
|
-
assert.equal(first.status, 'update_available');
|
|
710
|
-
assert.deepEqual(second, first);
|
|
711
|
-
assert.equal(requests, 1, 'the first check must commit a bounded retry or reusable candidate');
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
test('a saturated exact acknowledgement history never returns an unacknowledgeable offer', async (t) => {
|
|
715
|
-
const testFixture = fixture();
|
|
716
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
717
|
-
const targetDigest = `sha256:${'f'.repeat(64)}`;
|
|
718
|
-
const state = cachedStateWithHistory();
|
|
719
|
-
let projected = candidateStateForDigest(state, targetDigest);
|
|
720
|
-
let index = 0;
|
|
721
|
-
while (Buffer.byteLength(compactStateSource(projected)) <= maxCacheStateBytes) {
|
|
722
|
-
state.notification.acknowledgedDigests.push(historyDigest(index));
|
|
723
|
-
index += 1;
|
|
724
|
-
projected = candidateStateForDigest(state, targetDigest);
|
|
725
|
-
}
|
|
726
|
-
assert.ok(Buffer.byteLength(compactStateSource(state)) <= maxCacheStateBytes);
|
|
727
|
-
assert.ok(Buffer.byteLength(compactStateSource(projected)) > maxCacheStateBytes);
|
|
728
|
-
const exactHistory = [...state.notification.acknowledgedDigests];
|
|
729
|
-
writeCompactCommittedState(testFixture, state);
|
|
730
|
-
let requests = 0;
|
|
731
|
-
|
|
732
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
733
|
-
requests += 1;
|
|
734
|
-
return response(remoteReleaseForVersion('2.16.0', 'f'.repeat(64)));
|
|
735
|
-
}));
|
|
736
|
-
const acknowledgement = result.status === 'update_available'
|
|
737
|
-
? await acknowledgeUpdate({
|
|
738
|
-
releasePath: testFixture.releasePath,
|
|
739
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
740
|
-
eventKey: result.eventKey,
|
|
741
|
-
})
|
|
742
|
-
: null;
|
|
743
|
-
|
|
744
|
-
assert.deepEqual({ result, acknowledgement }, {
|
|
745
|
-
result: { status: 'silent', reason: 'cache-unavailable' },
|
|
746
|
-
acknowledgement: null,
|
|
747
|
-
});
|
|
748
|
-
assert.equal(requests, 1);
|
|
749
|
-
assert.ok(committedStateFiles(testFixture).every(
|
|
750
|
-
(target) => fs.statSync(target).size <= maxCacheStateBytes,
|
|
751
|
-
));
|
|
752
|
-
assert.deepEqual(
|
|
753
|
-
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification.acknowledgedDigests,
|
|
754
|
-
exactHistory,
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, async () => {
|
|
758
|
-
requests += 1;
|
|
759
|
-
return response(remoteReleaseForVersion('2.16.0', 'f'.repeat(64)));
|
|
760
|
-
})), { status: 'silent', reason: 'cache-valid' });
|
|
761
|
-
assert.equal(requests, 1, 'capacity rejection should commit a bounded retry delay');
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
test('capacity backoff withdraws a stale candidate while preserving its late acknowledgement', async (t) => {
|
|
765
|
-
const testFixture = fixture();
|
|
766
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
767
|
-
const staleDigest = `sha256:${'b'.repeat(64)}`;
|
|
768
|
-
const replacementDigest = `sha256:${'c'.repeat(64)}`;
|
|
769
|
-
const state = cachedStateWithHistory({ offeredDigests: [staleDigest] });
|
|
770
|
-
state.candidate = {
|
|
771
|
-
version: '2.16.0',
|
|
772
|
-
targetDigest: staleDigest,
|
|
773
|
-
severity: 'normal',
|
|
774
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
|
|
775
|
-
};
|
|
776
|
-
let replacement = candidateStateForDigest(state, replacementDigest);
|
|
777
|
-
let index = 0;
|
|
778
|
-
while (Buffer.byteLength(compactStateSource(replacement)) <= maxCacheStateBytes) {
|
|
779
|
-
state.notification.acknowledgedDigests.push(historyDigest(index));
|
|
780
|
-
index += 1;
|
|
781
|
-
replacement = candidateStateForDigest(state, replacementDigest);
|
|
782
|
-
}
|
|
783
|
-
assert.ok(Buffer.byteLength(compactStateSource(state)) <= maxCacheStateBytes);
|
|
784
|
-
assert.ok(Buffer.byteLength(compactStateSource(replacement)) > maxCacheStateBytes);
|
|
785
|
-
const exactHistory = [...state.notification.acknowledgedDigests];
|
|
786
|
-
writeCompactCommittedState(testFixture, state);
|
|
787
|
-
let requests = 0;
|
|
788
|
-
const fetchImpl = async () => {
|
|
789
|
-
requests += 1;
|
|
790
|
-
return response(remoteReleaseForVersion('2.16.0', 'c'.repeat(64)));
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
const refresh = await checkForUpdate(options(testFixture, fetchImpl));
|
|
794
|
-
const cached = await checkForUpdate(options(testFixture, fetchImpl));
|
|
795
|
-
const staleEventKey = `archify@${staleDigest}`;
|
|
796
|
-
const lateAcknowledgement = await acknowledgeUpdate({
|
|
797
|
-
releasePath: testFixture.releasePath,
|
|
798
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
799
|
-
eventKey: staleEventKey,
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
assert.deepEqual({ refresh, cached, lateAcknowledgement }, {
|
|
803
|
-
refresh: { status: 'silent', reason: 'cache-unavailable' },
|
|
804
|
-
cached: { status: 'silent', reason: 'cache-valid' },
|
|
805
|
-
lateAcknowledgement: { status: 'acknowledged', eventKey: staleEventKey },
|
|
806
|
-
});
|
|
807
|
-
assert.equal(requests, 1);
|
|
808
|
-
assert.ok(committedStateFiles(testFixture).every(
|
|
809
|
-
(target) => fs.statSync(target).size <= maxCacheStateBytes,
|
|
810
|
-
));
|
|
811
|
-
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
|
|
812
|
-
assert.equal(Object.hasOwn(persisted, 'candidate'), false);
|
|
813
|
-
assert.deepEqual(persisted.notification, {
|
|
814
|
-
offeredDigests: [],
|
|
815
|
-
acknowledgedDigests: [...exactHistory, staleDigest],
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
test('a 64 KiB state can be acknowledged but a 64 KiB plus one state is ignored', async () => {
|
|
820
|
-
const targetDigest = `sha256:${'e'.repeat(64)}`;
|
|
821
|
-
const exactState = cachedStateWithHistory({ offeredDigests: [targetDigest] });
|
|
822
|
-
exactState.candidate = {
|
|
823
|
-
version: '2.16.0',
|
|
824
|
-
targetDigest,
|
|
825
|
-
severity: 'normal',
|
|
826
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
|
|
827
|
-
};
|
|
828
|
-
const initialBytes = Buffer.byteLength(compactStateSource(exactState));
|
|
829
|
-
exactState.check.nextCheckAt += 'x'.repeat(maxCacheStateBytes - initialBytes);
|
|
830
|
-
assert.equal(Buffer.byteLength(compactStateSource(exactState)), maxCacheStateBytes);
|
|
831
|
-
|
|
832
|
-
const exactFixture = fixture();
|
|
833
|
-
try {
|
|
834
|
-
writeCompactCommittedState(exactFixture, exactState);
|
|
835
|
-
const eventKey = `archify@${targetDigest}`;
|
|
836
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
837
|
-
releasePath: exactFixture.releasePath,
|
|
838
|
-
cacheDirectory: exactFixture.cacheDirectory,
|
|
839
|
-
eventKey,
|
|
840
|
-
}), { status: 'acknowledged', eventKey });
|
|
841
|
-
assert.equal(fs.statSync(statePath(exactFixture)).size, maxCacheStateBytes);
|
|
842
|
-
assert.deepEqual(
|
|
843
|
-
JSON.parse(fs.readFileSync(statePath(exactFixture), 'utf8')).notification,
|
|
844
|
-
{ offeredDigests: [], acknowledgedDigests: [targetDigest] },
|
|
845
|
-
);
|
|
846
|
-
} finally {
|
|
847
|
-
fs.rmSync(exactFixture.root, { recursive: true, force: true });
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const oversizedFixture = fixture();
|
|
851
|
-
try {
|
|
852
|
-
const target = writeCompactCommittedState(oversizedFixture, exactState);
|
|
853
|
-
fs.appendFileSync(target, ' ');
|
|
854
|
-
assert.equal(fs.statSync(target).size, maxCacheStateBytes + 1);
|
|
855
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
856
|
-
releasePath: oversizedFixture.releasePath,
|
|
857
|
-
cacheDirectory: oversizedFixture.cacheDirectory,
|
|
858
|
-
eventKey: `archify@${targetDigest}`,
|
|
859
|
-
}), { status: 'silent', reason: 'invalid-acknowledgement' });
|
|
860
|
-
} finally {
|
|
861
|
-
fs.rmSync(oversizedFixture.root, { recursive: true, force: true });
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
test('a boundary offer remains acknowledgeable without pruning exact history', async (t) => {
|
|
866
|
-
const testFixture = fixture();
|
|
867
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
868
|
-
const targetDigest = `sha256:${'d'.repeat(64)}`;
|
|
869
|
-
const state = cachedStateWithHistory();
|
|
870
|
-
let index = 0;
|
|
871
|
-
while (true) {
|
|
872
|
-
state.notification.acknowledgedDigests.push(historyDigest(index));
|
|
873
|
-
if (Buffer.byteLength(compactStateSource(
|
|
874
|
-
candidateStateForDigest(state, targetDigest),
|
|
875
|
-
)) > maxCacheStateBytes) {
|
|
876
|
-
state.notification.acknowledgedDigests.pop();
|
|
877
|
-
break;
|
|
878
|
-
}
|
|
879
|
-
index += 1;
|
|
880
|
-
}
|
|
881
|
-
const projected = candidateStateForDigest(state, targetDigest);
|
|
882
|
-
assert.ok(Buffer.byteLength(compactStateSource(projected)) <= maxCacheStateBytes);
|
|
883
|
-
const oneMoreAcknowledgement = cachedStateWithHistory({
|
|
884
|
-
acknowledgedDigests: [...state.notification.acknowledgedDigests, historyDigest(index)],
|
|
885
|
-
});
|
|
886
|
-
assert.ok(Buffer.byteLength(compactStateSource(
|
|
887
|
-
candidateStateForDigest(oneMoreAcknowledgement, targetDigest),
|
|
888
|
-
)) > maxCacheStateBytes);
|
|
889
|
-
const exactHistory = [...state.notification.acknowledgedDigests];
|
|
890
|
-
writeCompactCommittedState(testFixture, state);
|
|
891
|
-
const fetchImpl = async () => response(remoteReleaseForVersion('2.16.0', 'd'.repeat(64)));
|
|
892
|
-
|
|
893
|
-
const offered = await checkForUpdate(options(testFixture, fetchImpl));
|
|
894
|
-
assert.equal(offered.status, 'update_available');
|
|
895
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
896
|
-
releasePath: testFixture.releasePath,
|
|
897
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
898
|
-
eventKey: offered.eventKey,
|
|
899
|
-
}), { status: 'acknowledged', eventKey: offered.eventKey });
|
|
900
|
-
|
|
901
|
-
assert.ok(committedStateFiles(testFixture).every(
|
|
902
|
-
(target) => fs.statSync(target).size <= maxCacheStateBytes,
|
|
903
|
-
));
|
|
904
|
-
assert.deepEqual(
|
|
905
|
-
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).notification,
|
|
906
|
-
{
|
|
907
|
-
offeredDigests: [],
|
|
908
|
-
acknowledgedDigests: [...exactHistory, targetDigest],
|
|
909
|
-
},
|
|
910
|
-
);
|
|
911
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
|
|
912
|
-
status: 'silent',
|
|
913
|
-
reason: 'already-notified',
|
|
914
|
-
});
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
test('opaque response validators are neither persisted nor replayed after the check TTL', async (t) => {
|
|
918
|
-
const testFixture = fixture();
|
|
919
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
920
|
-
let requests = 0;
|
|
921
|
-
const fetchImpl = async (_url, init) => {
|
|
922
|
-
requests += 1;
|
|
923
|
-
assert.equal(init.headers['if-none-match'], undefined);
|
|
924
|
-
return response(remoteRelease(), { etag: `"per-client-${requests}"` });
|
|
925
|
-
};
|
|
926
|
-
|
|
927
|
-
const first = await checkForUpdate(options(testFixture, fetchImpl));
|
|
928
|
-
assert.equal(first.status, 'update_available');
|
|
929
|
-
|
|
930
|
-
const second = await checkForUpdate(options(testFixture, fetchImpl, {
|
|
931
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
932
|
-
}));
|
|
933
|
-
assert.equal(second.status, 'update_available');
|
|
934
|
-
assert.equal(second.eventKey, first.eventKey);
|
|
935
|
-
assert.equal(requests, 2);
|
|
936
|
-
assert.equal(Object.hasOwn(JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).check, 'etag'), false);
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
test('an HTTP 304 is always a failed unconditional refresh', async (t) => {
|
|
940
|
-
const testFixture = fixture();
|
|
941
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
942
|
-
let requests = 0;
|
|
943
|
-
const fetchImpl = async () => {
|
|
944
|
-
requests += 1;
|
|
945
|
-
if (requests === 1) {
|
|
946
|
-
return new Response(JSON.stringify(remoteRelease()), {
|
|
947
|
-
status: 200,
|
|
948
|
-
headers: { 'content-type': 'application/json' },
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
return new Response(null, { status: 304 });
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
955
|
-
const refresh = await checkForUpdate(options(testFixture, fetchImpl, {
|
|
956
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
957
|
-
}));
|
|
958
|
-
|
|
959
|
-
assert.deepEqual(refresh, { status: 'silent', reason: 'check-failed' });
|
|
960
|
-
assert.equal(requests, 2);
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
test('a failed refresh preserves the last-good unacknowledged candidate', async (t) => {
|
|
964
|
-
const testFixture = fixture();
|
|
965
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
966
|
-
let requests = 0;
|
|
967
|
-
const fetchImpl = async () => {
|
|
968
|
-
requests += 1;
|
|
969
|
-
if (requests === 1) return response(remoteRelease());
|
|
970
|
-
throw new Error('offline');
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
const first = await checkForUpdate(options(testFixture, fetchImpl));
|
|
974
|
-
assert.equal(first.status, 'update_available');
|
|
975
|
-
const failed = await checkForUpdate(options(testFixture, fetchImpl, {
|
|
976
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
977
|
-
}));
|
|
978
|
-
assert.deepEqual(failed, { status: 'silent', reason: 'check-failed' });
|
|
979
|
-
|
|
980
|
-
const cached = await checkForUpdate(options(testFixture, fetchImpl, {
|
|
981
|
-
now: () => baseTime + (74 * 60 * 60 * 1_000),
|
|
982
|
-
}));
|
|
983
|
-
assert.equal(cached.status, 'update_available');
|
|
984
|
-
assert.equal(cached.eventKey, first.eventKey);
|
|
985
|
-
assert.equal(requests, 2);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
test('failure backoff saturates safely instead of overflowing the cache counter', async (t) => {
|
|
989
|
-
const testFixture = fixture();
|
|
990
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
991
|
-
writeJson(statePath(testFixture), {
|
|
992
|
-
schemaVersion: 1,
|
|
993
|
-
skillId: 'archify',
|
|
994
|
-
installedVersion: '2.15.0',
|
|
995
|
-
check: {
|
|
996
|
-
nextCheckAt: new Date(baseTime - 1_000).toISOString(),
|
|
997
|
-
consecutiveFailures: Number.MAX_SAFE_INTEGER,
|
|
998
|
-
},
|
|
999
|
-
notification: {
|
|
1000
|
-
offeredDigests: [],
|
|
1001
|
-
acknowledgedDigests: [],
|
|
1002
|
-
},
|
|
1003
|
-
});
|
|
1004
|
-
let requests = 0;
|
|
1005
|
-
const fetchImpl = async () => {
|
|
1006
|
-
requests += 1;
|
|
1007
|
-
throw new Error('offline');
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
|
|
1011
|
-
status: 'silent',
|
|
1012
|
-
reason: 'check-failed',
|
|
1013
|
-
});
|
|
1014
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl, {
|
|
1015
|
-
now: () => baseTime + (60 * 60 * 1_000),
|
|
1016
|
-
})), { status: 'silent', reason: 'cache-valid' });
|
|
1017
|
-
assert.equal(requests, 1);
|
|
1018
|
-
assert.equal(JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).check.consecutiveFailures, 2);
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
test('a committed cache FIFO is ignored without blocking the update check', (t) => {
|
|
1022
|
-
if (process.platform === 'win32') {
|
|
1023
|
-
t.skip('FIFO files are unavailable on Windows');
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
const testFixture = fixture();
|
|
1027
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1028
|
-
const cacheStatePath = statePath(testFixture);
|
|
1029
|
-
fs.mkdirSync(path.dirname(cacheStatePath), { recursive: true });
|
|
1030
|
-
if (!createFifo(cacheStatePath)) {
|
|
1031
|
-
t.skip('mkfifo is unavailable');
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
assertUnsafeCacheStateIsIgnored(testFixture);
|
|
1036
|
-
});
|
|
1037
|
-
|
|
1038
|
-
test('a committed cache state symlink is ignored without following its target', (t) => {
|
|
1039
|
-
const testFixture = fixture();
|
|
1040
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1041
|
-
const cacheStatePath = statePath(testFixture);
|
|
1042
|
-
const linkedTarget = path.join(testFixture.root, 'linked-state-target');
|
|
1043
|
-
fs.mkdirSync(path.dirname(cacheStatePath), { recursive: true });
|
|
1044
|
-
const targetIsFifo = process.platform !== 'win32' && createFifo(linkedTarget);
|
|
1045
|
-
if (!targetIsFifo) writeJson(linkedTarget, cachedUpdateState());
|
|
1046
|
-
fs.symlinkSync(linkedTarget, cacheStatePath, process.platform === 'win32' ? 'file' : undefined);
|
|
1047
|
-
|
|
1048
|
-
assertUnsafeCacheStateIsIgnored(testFixture);
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
test('an oversized committed cache state is ignored without an unbounded read', (t) => {
|
|
1052
|
-
const testFixture = fixture();
|
|
1053
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1054
|
-
const oversizedState = `${JSON.stringify(cachedUpdateState())}${' '.repeat(256 * 1_024)}`;
|
|
1055
|
-
const cacheStatePath = statePath(testFixture);
|
|
1056
|
-
fs.mkdirSync(path.dirname(cacheStatePath), { recursive: true });
|
|
1057
|
-
fs.writeFileSync(cacheStatePath, oversizedState);
|
|
1058
|
-
|
|
1059
|
-
assertUnsafeCacheStateIsIgnored(testFixture);
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
test('a cache ancestor replaced before a cached read cannot inject a reminder', async (t) => {
|
|
1063
|
-
const testFixture = fixture();
|
|
1064
|
-
const originalReaddir = fsPromises.readdir;
|
|
1065
|
-
t.after(() => {
|
|
1066
|
-
fsPromises.readdir = originalReaddir;
|
|
1067
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1068
|
-
});
|
|
1069
|
-
writeCompactCommittedState(testFixture, cachedStateWithHistory());
|
|
1070
|
-
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
|
|
1071
|
-
writeJson(
|
|
1072
|
-
path.join(replacementRoot, path.basename(stateDirectory(testFixture)),
|
|
1073
|
-
path.basename(operationPath(testFixture, 'committed', 1n)), 'state.json'),
|
|
1074
|
-
cachedUpdateState(),
|
|
1075
|
-
);
|
|
1076
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1077
|
-
const probe = path.join(testFixture.root, 'symlink-probe');
|
|
1078
|
-
try {
|
|
1079
|
-
fs.symlinkSync(
|
|
1080
|
-
replacementRoot,
|
|
1081
|
-
probe,
|
|
1082
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1083
|
-
);
|
|
1084
|
-
fs.unlinkSync(probe);
|
|
1085
|
-
} catch (error) {
|
|
1086
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1087
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
throw error;
|
|
1091
|
-
}
|
|
1092
|
-
let replaced = false;
|
|
1093
|
-
fsPromises.readdir = async (target, ...args) => {
|
|
1094
|
-
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
|
|
1095
|
-
replaced = true;
|
|
1096
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1097
|
-
fs.symlinkSync(
|
|
1098
|
-
replacementRoot,
|
|
1099
|
-
testFixture.cacheDirectory,
|
|
1100
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
|
-
return originalReaddir(target, ...args);
|
|
1104
|
-
};
|
|
1105
|
-
let requests = 0;
|
|
1106
|
-
|
|
1107
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1108
|
-
requests += 1;
|
|
1109
|
-
return response(remoteRelease());
|
|
1110
|
-
}));
|
|
1111
|
-
|
|
1112
|
-
assert.equal(replaced, true);
|
|
1113
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1114
|
-
assert.equal(requests, 0);
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
test('a committed directory replaced by a symlink cannot inject a reminder', async (t) => {
|
|
1118
|
-
const testFixture = fixture();
|
|
1119
|
-
const originalReaddir = fsPromises.readdir;
|
|
1120
|
-
t.after(() => {
|
|
1121
|
-
fsPromises.readdir = originalReaddir;
|
|
1122
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1123
|
-
});
|
|
1124
|
-
writeCompactCommittedState(testFixture, cachedStateWithHistory());
|
|
1125
|
-
const committed = operationPath(testFixture, 'committed', 1n);
|
|
1126
|
-
const replacement = path.join(testFixture.root, 'replacement-committed');
|
|
1127
|
-
writeJson(path.join(replacement, 'state.json'), cachedUpdateState());
|
|
1128
|
-
const probe = path.join(testFixture.root, 'nested-symlink-probe');
|
|
1129
|
-
try {
|
|
1130
|
-
fs.symlinkSync(
|
|
1131
|
-
replacement,
|
|
1132
|
-
probe,
|
|
1133
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1134
|
-
);
|
|
1135
|
-
fs.unlinkSync(probe);
|
|
1136
|
-
} catch (error) {
|
|
1137
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1138
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
throw error;
|
|
1142
|
-
}
|
|
1143
|
-
const detached = path.join(stateDirectory(testFixture), 'detached-committed');
|
|
1144
|
-
let replaced = false;
|
|
1145
|
-
fsPromises.readdir = async (target, ...args) => {
|
|
1146
|
-
const entries = await originalReaddir(target, ...args);
|
|
1147
|
-
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
|
|
1148
|
-
replaced = true;
|
|
1149
|
-
fs.renameSync(committed, detached);
|
|
1150
|
-
fs.symlinkSync(
|
|
1151
|
-
replacement,
|
|
1152
|
-
committed,
|
|
1153
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
return entries;
|
|
1157
|
-
};
|
|
1158
|
-
let requests = 0;
|
|
1159
|
-
|
|
1160
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1161
|
-
requests += 1;
|
|
1162
|
-
return response(remoteRelease());
|
|
1163
|
-
}));
|
|
1164
|
-
|
|
1165
|
-
assert.equal(replaced, true);
|
|
1166
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1167
|
-
assert.equal(requests, 0);
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
test('a pending directory replaced by a symlink cannot be ignored as inactive', async (t) => {
|
|
1171
|
-
const testFixture = fixture();
|
|
1172
|
-
const originalReaddir = fsPromises.readdir;
|
|
1173
|
-
t.after(() => {
|
|
1174
|
-
fsPromises.readdir = originalReaddir;
|
|
1175
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1176
|
-
});
|
|
1177
|
-
const pending = writePendingOperation(testFixture);
|
|
1178
|
-
const replacement = path.join(testFixture.root, 'replacement-pending');
|
|
1179
|
-
writePendingOperation({
|
|
1180
|
-
...testFixture,
|
|
1181
|
-
cacheDirectory: path.join(testFixture.root, 'replacement-cache'),
|
|
1182
|
-
}, { modifiedAt: new Date(Date.now() - 60_000) });
|
|
1183
|
-
const replacementPending = operationPath({
|
|
1184
|
-
...testFixture,
|
|
1185
|
-
cacheDirectory: path.join(testFixture.root, 'replacement-cache'),
|
|
1186
|
-
}, 'pending', 1n);
|
|
1187
|
-
fs.renameSync(replacementPending, replacement);
|
|
1188
|
-
const probe = path.join(testFixture.root, 'pending-symlink-probe');
|
|
1189
|
-
try {
|
|
1190
|
-
fs.symlinkSync(
|
|
1191
|
-
replacement,
|
|
1192
|
-
probe,
|
|
1193
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1194
|
-
);
|
|
1195
|
-
fs.unlinkSync(probe);
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1198
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
throw error;
|
|
1202
|
-
}
|
|
1203
|
-
const detached = path.join(stateDirectory(testFixture), 'detached-pending');
|
|
1204
|
-
let replaced = false;
|
|
1205
|
-
fsPromises.readdir = async (target, ...args) => {
|
|
1206
|
-
const entries = await originalReaddir(target, ...args);
|
|
1207
|
-
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
|
|
1208
|
-
replaced = true;
|
|
1209
|
-
fs.renameSync(pending, detached);
|
|
1210
|
-
fs.symlinkSync(
|
|
1211
|
-
replacement,
|
|
1212
|
-
pending,
|
|
1213
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1214
|
-
);
|
|
1215
|
-
}
|
|
1216
|
-
return entries;
|
|
1217
|
-
};
|
|
1218
|
-
let requests = 0;
|
|
1219
|
-
|
|
1220
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1221
|
-
requests += 1;
|
|
1222
|
-
return response(remoteRelease());
|
|
1223
|
-
}));
|
|
1224
|
-
|
|
1225
|
-
assert.equal(replaced, true);
|
|
1226
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1227
|
-
assert.equal(requests, 0);
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
test('zero-inode cache metadata uses timestamp fallback to reject a replaced ancestor', async (t) => {
|
|
1231
|
-
const testFixture = fixture();
|
|
1232
|
-
const originalLstat = fsPromises.lstat;
|
|
1233
|
-
const originalReaddir = fsPromises.readdir;
|
|
1234
|
-
t.after(() => {
|
|
1235
|
-
fsPromises.lstat = originalLstat;
|
|
1236
|
-
fsPromises.readdir = originalReaddir;
|
|
1237
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1238
|
-
});
|
|
1239
|
-
writeCompactCommittedState(testFixture, cachedStateWithHistory());
|
|
1240
|
-
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
|
|
1241
|
-
writeJson(
|
|
1242
|
-
path.join(replacementRoot, path.basename(stateDirectory(testFixture)),
|
|
1243
|
-
path.basename(operationPath(testFixture, 'committed', 1n)), 'state.json'),
|
|
1244
|
-
cachedUpdateState(),
|
|
1245
|
-
);
|
|
1246
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1247
|
-
fsPromises.lstat = async (target, ...args) => {
|
|
1248
|
-
const metadata = await originalLstat(target, ...args);
|
|
1249
|
-
const resolved = path.resolve(target);
|
|
1250
|
-
const isCachePath = resolved === path.resolve(testFixture.cacheDirectory)
|
|
1251
|
-
|| resolved.startsWith(`${path.resolve(testFixture.cacheDirectory)}${path.sep}`);
|
|
1252
|
-
if (args[0]?.bigint && isCachePath && metadata.isDirectory()) {
|
|
1253
|
-
return metadataWithOverrides(metadata, {
|
|
1254
|
-
ino: 0n,
|
|
1255
|
-
birthtimeNs: replaced ? 2_000n : 1_000n,
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
return metadata;
|
|
1259
|
-
};
|
|
1260
|
-
let replaced = false;
|
|
1261
|
-
fsPromises.readdir = async (target, ...args) => {
|
|
1262
|
-
if (!replaced && path.resolve(target) === path.resolve(stateDirectory(testFixture))) {
|
|
1263
|
-
replaced = true;
|
|
1264
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1265
|
-
fs.renameSync(replacementRoot, testFixture.cacheDirectory);
|
|
1266
|
-
}
|
|
1267
|
-
return originalReaddir(target, ...args);
|
|
1268
|
-
};
|
|
1269
|
-
let requests = 0;
|
|
1270
|
-
|
|
1271
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1272
|
-
requests += 1;
|
|
1273
|
-
return response(remoteRelease());
|
|
1274
|
-
}));
|
|
1275
|
-
|
|
1276
|
-
assert.equal(replaced, true);
|
|
1277
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1278
|
-
assert.equal(requests, 0);
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
test('zero-inode cache metadata still supports a normal check and acknowledgement', async (t) => {
|
|
1282
|
-
const testFixture = fixture();
|
|
1283
|
-
const originalLstat = fsPromises.lstat;
|
|
1284
|
-
const originalOpen = fsPromises.open;
|
|
1285
|
-
t.after(() => {
|
|
1286
|
-
fsPromises.lstat = originalLstat;
|
|
1287
|
-
fsPromises.open = originalOpen;
|
|
1288
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1289
|
-
});
|
|
1290
|
-
const cacheRoot = path.resolve(testFixture.cacheDirectory);
|
|
1291
|
-
const isCachePath = (target) => {
|
|
1292
|
-
const resolved = path.resolve(target);
|
|
1293
|
-
return resolved === cacheRoot || resolved.startsWith(`${cacheRoot}${path.sep}`);
|
|
1294
|
-
};
|
|
1295
|
-
fsPromises.lstat = async (target, ...args) => {
|
|
1296
|
-
const metadata = await originalLstat(target, ...args);
|
|
1297
|
-
if (args[0]?.bigint && isCachePath(target)) {
|
|
1298
|
-
return metadataWithOverrides(metadata, { ino: 0n, birthtimeNs: 1_000n });
|
|
1299
|
-
}
|
|
1300
|
-
return metadata;
|
|
1301
|
-
};
|
|
1302
|
-
fsPromises.open = async (target, ...args) => {
|
|
1303
|
-
const handle = await originalOpen(target, ...args);
|
|
1304
|
-
if (!isCachePath(target)) return handle;
|
|
1305
|
-
return new Proxy(handle, {
|
|
1306
|
-
get(fileHandle, property) {
|
|
1307
|
-
if (property === 'stat') {
|
|
1308
|
-
return async (...statArguments) => {
|
|
1309
|
-
const metadata = await fileHandle.stat(...statArguments);
|
|
1310
|
-
return statArguments[0]?.bigint
|
|
1311
|
-
? metadataWithOverrides(metadata, { ino: 0n, birthtimeNs: 1_000n })
|
|
1312
|
-
: metadataWithOverrides(metadata, { ino: 0 });
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
const value = Reflect.get(fileHandle, property, fileHandle);
|
|
1316
|
-
return typeof value === 'function' ? value.bind(fileHandle) : value;
|
|
1317
|
-
},
|
|
1318
|
-
});
|
|
1319
|
-
};
|
|
1320
|
-
let requests = 0;
|
|
1321
|
-
|
|
1322
|
-
const offered = await checkForUpdate(options(testFixture, async () => {
|
|
1323
|
-
requests += 1;
|
|
1324
|
-
return response(remoteRelease());
|
|
1325
|
-
}));
|
|
1326
|
-
const acknowledged = await acknowledgeUpdate({
|
|
1327
|
-
releasePath: testFixture.releasePath,
|
|
1328
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
1329
|
-
eventKey: offered.eventKey,
|
|
1330
|
-
});
|
|
1331
|
-
|
|
1332
|
-
assert.equal(offered.status, 'update_available');
|
|
1333
|
-
assert.deepEqual(acknowledged, { status: 'acknowledged', eventKey: offered.eventKey });
|
|
1334
|
-
assert.equal(requests, 1);
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
test('zero-inode cache metadata without birthtime fails closed', async (t) => {
|
|
1338
|
-
const testFixture = fixture();
|
|
1339
|
-
const originalLstat = fsPromises.lstat;
|
|
1340
|
-
t.after(() => {
|
|
1341
|
-
fsPromises.lstat = originalLstat;
|
|
1342
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1343
|
-
});
|
|
1344
|
-
const cacheRoot = path.resolve(testFixture.cacheDirectory);
|
|
1345
|
-
fsPromises.lstat = async (target, ...args) => {
|
|
1346
|
-
const metadata = await originalLstat(target, ...args);
|
|
1347
|
-
const resolved = path.resolve(target);
|
|
1348
|
-
if (args[0]?.bigint
|
|
1349
|
-
&& (resolved === cacheRoot || resolved.startsWith(`${cacheRoot}${path.sep}`))) {
|
|
1350
|
-
return metadataWithOverrides(metadata, { ino: 0n, birthtimeNs: 0n });
|
|
1351
|
-
}
|
|
1352
|
-
return metadata;
|
|
1353
|
-
};
|
|
1354
|
-
let requests = 0;
|
|
1355
|
-
|
|
1356
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1357
|
-
requests += 1;
|
|
1358
|
-
return response(remoteRelease());
|
|
1359
|
-
}));
|
|
1360
|
-
|
|
1361
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1362
|
-
assert.equal(requests, 0);
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
test('a state write replaced before post-verification is never reported as committed', async (t) => {
|
|
1366
|
-
const testFixture = fixture();
|
|
1367
|
-
const originalOpen = fsPromises.open;
|
|
1368
|
-
const originalWriteFile = fsPromises.writeFile;
|
|
1369
|
-
t.after(() => {
|
|
1370
|
-
fsPromises.open = originalOpen;
|
|
1371
|
-
fsPromises.writeFile = originalWriteFile;
|
|
1372
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1373
|
-
});
|
|
1374
|
-
let replaced = false;
|
|
1375
|
-
const replaceStateFile = (target) => {
|
|
1376
|
-
if (replaced) return;
|
|
1377
|
-
replaced = true;
|
|
1378
|
-
fs.renameSync(target, `${target}.written-by-checker`);
|
|
1379
|
-
fs.writeFileSync(target, '{}\n', { flag: 'wx', mode: 0o600 });
|
|
1380
|
-
};
|
|
1381
|
-
fsPromises.writeFile = async (target, ...args) => {
|
|
1382
|
-
const result = await originalWriteFile(target, ...args);
|
|
1383
|
-
if (path.basename(target).startsWith('.state.')) replaceStateFile(target);
|
|
1384
|
-
return result;
|
|
1385
|
-
};
|
|
1386
|
-
fsPromises.open = async (target, ...args) => {
|
|
1387
|
-
const handle = await originalOpen(target, ...args);
|
|
1388
|
-
if (!path.basename(target).startsWith('.state.')) return handle;
|
|
1389
|
-
return {
|
|
1390
|
-
stat: handle.stat.bind(handle),
|
|
1391
|
-
writeFile: handle.writeFile.bind(handle),
|
|
1392
|
-
close: async () => {
|
|
1393
|
-
const result = await handle.close();
|
|
1394
|
-
replaceStateFile(target);
|
|
1395
|
-
return result;
|
|
1396
|
-
},
|
|
1397
|
-
};
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
const result = await checkForUpdate(options(testFixture, async () => response(remoteRelease())));
|
|
1401
|
-
|
|
1402
|
-
assert.equal(replaced, true);
|
|
1403
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1404
|
-
for (const committed of committedStateFiles(testFixture)) {
|
|
1405
|
-
assert.notDeepEqual(JSON.parse(fs.readFileSync(committed, 'utf8')), {});
|
|
1406
|
-
}
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
test('a pending directory replaced by a symlink cannot be reported as committed', async (t) => {
|
|
1410
|
-
const testFixture = fixture();
|
|
1411
|
-
const originalRename = fsPromises.rename;
|
|
1412
|
-
t.after(() => {
|
|
1413
|
-
fsPromises.rename = originalRename;
|
|
1414
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1415
|
-
});
|
|
1416
|
-
const probe = path.join(testFixture.root, 'symlink-probe');
|
|
1417
|
-
try {
|
|
1418
|
-
fs.mkdirSync(probe);
|
|
1419
|
-
const probeLink = `${probe}-link`;
|
|
1420
|
-
fs.symlinkSync(probe, probeLink, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1421
|
-
fs.unlinkSync(probeLink);
|
|
1422
|
-
fs.rmdirSync(probe);
|
|
1423
|
-
} catch (error) {
|
|
1424
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1425
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1426
|
-
return;
|
|
1427
|
-
}
|
|
1428
|
-
throw error;
|
|
1429
|
-
}
|
|
1430
|
-
const detachedPending = path.join(testFixture.root, 'detached-pending');
|
|
1431
|
-
let pending = null;
|
|
1432
|
-
let replaced = false;
|
|
1433
|
-
fsPromises.rename = async (source, destination, ...args) => {
|
|
1434
|
-
if (!pending
|
|
1435
|
-
&& path.basename(source).startsWith('.state.')
|
|
1436
|
-
&& path.basename(destination) === 'state.json') {
|
|
1437
|
-
const result = await originalRename(source, destination, ...args);
|
|
1438
|
-
pending = path.dirname(destination);
|
|
1439
|
-
return result;
|
|
1440
|
-
}
|
|
1441
|
-
if (!replaced
|
|
1442
|
-
&& pending
|
|
1443
|
-
&& path.resolve(source) === path.resolve(pending)
|
|
1444
|
-
&& path.basename(destination).startsWith('committed-')) {
|
|
1445
|
-
replaced = true;
|
|
1446
|
-
fs.renameSync(pending, detachedPending);
|
|
1447
|
-
fs.symlinkSync(
|
|
1448
|
-
detachedPending,
|
|
1449
|
-
pending,
|
|
1450
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
return originalRename(source, destination, ...args);
|
|
1454
|
-
};
|
|
1455
|
-
let requests = 0;
|
|
1456
|
-
|
|
1457
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1458
|
-
requests += 1;
|
|
1459
|
-
return response(remoteRelease());
|
|
1460
|
-
}));
|
|
1461
|
-
|
|
1462
|
-
assert.equal(replaced, true);
|
|
1463
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1464
|
-
assert.equal(requests, 1);
|
|
1465
|
-
assert.equal(fs.existsSync(path.join(detachedPending, 'state.json')), true);
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
test('a cache trust failure while cancelling cannot return a cached reminder', async (t) => {
|
|
1469
|
-
const testFixture = fixture();
|
|
1470
|
-
const originalRename = fsPromises.rename;
|
|
1471
|
-
t.after(() => {
|
|
1472
|
-
fsPromises.rename = originalRename;
|
|
1473
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1474
|
-
});
|
|
1475
|
-
const stale = cachedUpdateState();
|
|
1476
|
-
stale.check.nextCheckAt = new Date(baseTime - 1_000).toISOString();
|
|
1477
|
-
const committed = writeCompactCommittedState(testFixture, stale);
|
|
1478
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1479
|
-
let refreshed = false;
|
|
1480
|
-
let replaced = false;
|
|
1481
|
-
fsPromises.rename = async (source, destination, ...args) => {
|
|
1482
|
-
const sourceName = path.basename(source);
|
|
1483
|
-
const destinationName = path.basename(destination);
|
|
1484
|
-
if (!refreshed && sourceName.startsWith('claim-') && destinationName === 'active-claim') {
|
|
1485
|
-
const result = await originalRename(source, destination, ...args);
|
|
1486
|
-
const fresh = cachedUpdateState();
|
|
1487
|
-
writeJson(committed, fresh);
|
|
1488
|
-
refreshed = true;
|
|
1489
|
-
return result;
|
|
1490
|
-
}
|
|
1491
|
-
if (!replaced && sourceName.startsWith('pending-') && destinationName.startsWith('cancelled-')) {
|
|
1492
|
-
replaced = true;
|
|
1493
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1494
|
-
fs.symlinkSync(
|
|
1495
|
-
detachedCache,
|
|
1496
|
-
testFixture.cacheDirectory,
|
|
1497
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
|
-
return originalRename(source, destination, ...args);
|
|
1501
|
-
};
|
|
1502
|
-
let requests = 0;
|
|
1503
|
-
|
|
1504
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1505
|
-
requests += 1;
|
|
1506
|
-
return response(remoteRelease());
|
|
1507
|
-
}));
|
|
1508
|
-
|
|
1509
|
-
assert.equal(refreshed, true);
|
|
1510
|
-
assert.equal(replaced, true);
|
|
1511
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1512
|
-
assert.equal(requests, 0);
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
test('prepared claim cleanup never recursively deletes through a replaced cache ancestor', async (t) => {
|
|
1516
|
-
const testFixture = fixture();
|
|
1517
|
-
const originalOpen = fsPromises.open;
|
|
1518
|
-
const originalRm = fsPromises.rm;
|
|
1519
|
-
const originalWriteFile = fsPromises.writeFile;
|
|
1520
|
-
t.after(() => {
|
|
1521
|
-
fsPromises.open = originalOpen;
|
|
1522
|
-
fsPromises.rm = originalRm;
|
|
1523
|
-
fsPromises.writeFile = originalWriteFile;
|
|
1524
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1525
|
-
});
|
|
1526
|
-
const activeClaim = path.join(stateDirectory(testFixture), 'active-claim');
|
|
1527
|
-
let injectedActiveClaim = false;
|
|
1528
|
-
const injectActiveClaim = () => {
|
|
1529
|
-
if (injectedActiveClaim) return;
|
|
1530
|
-
injectedActiveClaim = true;
|
|
1531
|
-
fs.mkdirSync(activeClaim);
|
|
1532
|
-
};
|
|
1533
|
-
fsPromises.writeFile = async (target, ...args) => {
|
|
1534
|
-
const result = await originalWriteFile(target, ...args);
|
|
1535
|
-
if (path.basename(path.dirname(target)).startsWith('claim-')
|
|
1536
|
-
&& path.basename(target) === 'owner.json') injectActiveClaim();
|
|
1537
|
-
return result;
|
|
1538
|
-
};
|
|
1539
|
-
fsPromises.open = async (target, ...args) => {
|
|
1540
|
-
const handle = await originalOpen(target, ...args);
|
|
1541
|
-
if (path.basename(path.dirname(target)).startsWith('claim-')
|
|
1542
|
-
&& path.basename(target) === 'owner.json') {
|
|
1543
|
-
return {
|
|
1544
|
-
stat: handle.stat.bind(handle),
|
|
1545
|
-
writeFile: handle.writeFile.bind(handle),
|
|
1546
|
-
close: async () => {
|
|
1547
|
-
const result = await handle.close();
|
|
1548
|
-
injectActiveClaim();
|
|
1549
|
-
return result;
|
|
1550
|
-
},
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
return handle;
|
|
1554
|
-
};
|
|
1555
|
-
const victimRoot = path.join(testFixture.root, 'victim-cache');
|
|
1556
|
-
const victimClaim = path.join(
|
|
1557
|
-
victimRoot,
|
|
1558
|
-
path.basename(stateDirectory(testFixture)),
|
|
1559
|
-
path.basename(operationPath(testFixture, 'claim', 1n)),
|
|
1560
|
-
);
|
|
1561
|
-
const victimFile = path.join(victimClaim, 'important.txt');
|
|
1562
|
-
fs.mkdirSync(victimClaim, { recursive: true });
|
|
1563
|
-
fs.writeFileSync(victimFile, 'must survive\n');
|
|
1564
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1565
|
-
let interceptedRemoval = false;
|
|
1566
|
-
fsPromises.rm = async (target, ...args) => {
|
|
1567
|
-
if (!interceptedRemoval && path.basename(target).startsWith('claim-')) {
|
|
1568
|
-
interceptedRemoval = true;
|
|
1569
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1570
|
-
fs.symlinkSync(
|
|
1571
|
-
victimRoot,
|
|
1572
|
-
testFixture.cacheDirectory,
|
|
1573
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1574
|
-
);
|
|
1575
|
-
}
|
|
1576
|
-
return originalRm(target, ...args);
|
|
1577
|
-
};
|
|
1578
|
-
let requests = 0;
|
|
1579
|
-
|
|
1580
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1581
|
-
requests += 1;
|
|
1582
|
-
return response(remoteRelease());
|
|
1583
|
-
}));
|
|
1584
|
-
|
|
1585
|
-
assert.equal(injectedActiveClaim, true);
|
|
1586
|
-
assert.equal(requests, 0);
|
|
1587
|
-
assert.equal(result.status, 'silent');
|
|
1588
|
-
assert.equal(interceptedRemoval, false);
|
|
1589
|
-
assert.equal(fs.readFileSync(victimFile, 'utf8'), 'must survive\n');
|
|
1590
|
-
const coordinationEntries = fs.readdirSync(stateDirectory(testFixture));
|
|
1591
|
-
assert.deepEqual(
|
|
1592
|
-
coordinationEntries.filter((name) => name.startsWith('claim-')),
|
|
1593
|
-
[],
|
|
1594
|
-
);
|
|
1595
|
-
const discardedClaims = coordinationEntries
|
|
1596
|
-
.filter((name) => name.startsWith('discarded-claim-'));
|
|
1597
|
-
assert.equal(discardedClaims.length, 1);
|
|
1598
|
-
const discardedOwnerPath = path.join(
|
|
1599
|
-
stateDirectory(testFixture),
|
|
1600
|
-
discardedClaims[0],
|
|
1601
|
-
'owner.json',
|
|
1602
|
-
);
|
|
1603
|
-
const discardedOwner = JSON.parse(fs.readFileSync(discardedOwnerPath, 'utf8'));
|
|
1604
|
-
assert.deepEqual(Object.keys(discardedOwner).sort(), ['generation', 'pid', 'token']);
|
|
1605
|
-
assert.equal(discardedOwner.generation, '1');
|
|
1606
|
-
assert.match(discardedOwner.token, /^[a-f0-9]{32}$/);
|
|
1607
|
-
assert.equal(discardedClaims[0].endsWith(`-${discardedOwner.token}`), true);
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
test('a symlink cache root cannot write into its target', async (t) => {
|
|
1611
|
-
const testFixture = fixture();
|
|
1612
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1613
|
-
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
|
|
1614
|
-
fs.mkdirSync(protectedTarget);
|
|
1615
|
-
fs.writeFileSync(path.join(protectedTarget, 'settings.json'), '{"protected":true}\n');
|
|
1616
|
-
try {
|
|
1617
|
-
fs.symlinkSync(
|
|
1618
|
-
protectedTarget,
|
|
1619
|
-
testFixture.cacheDirectory,
|
|
1620
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1621
|
-
);
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1624
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
throw error;
|
|
1628
|
-
}
|
|
1629
|
-
let requests = 0;
|
|
1630
|
-
|
|
1631
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1632
|
-
requests += 1;
|
|
1633
|
-
return response(remoteRelease());
|
|
1634
|
-
}));
|
|
1635
|
-
|
|
1636
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1637
|
-
assert.equal(requests, 0);
|
|
1638
|
-
assert.deepEqual(fs.readdirSync(protectedTarget), ['settings.json']);
|
|
1639
|
-
});
|
|
1640
|
-
|
|
1641
|
-
test('a symlink cache ancestor cannot redirect cache writes', async (t) => {
|
|
1642
|
-
const testFixture = fixture();
|
|
1643
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1644
|
-
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
|
|
1645
|
-
const linkedAncestor = path.join(testFixture.root, 'cache-parent');
|
|
1646
|
-
const cacheDirectory = path.join(linkedAncestor, 'nested', 'cache');
|
|
1647
|
-
fs.mkdirSync(protectedTarget);
|
|
1648
|
-
fs.writeFileSync(path.join(protectedTarget, 'settings.json'), '{"protected":true}\n');
|
|
1649
|
-
try {
|
|
1650
|
-
fs.symlinkSync(
|
|
1651
|
-
protectedTarget,
|
|
1652
|
-
linkedAncestor,
|
|
1653
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1654
|
-
);
|
|
1655
|
-
} catch (error) {
|
|
1656
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1657
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
throw error;
|
|
1661
|
-
}
|
|
1662
|
-
let requests = 0;
|
|
1663
|
-
|
|
1664
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1665
|
-
requests += 1;
|
|
1666
|
-
return response(remoteRelease());
|
|
1667
|
-
}, { cacheDirectory }));
|
|
1668
|
-
|
|
1669
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1670
|
-
assert.equal(requests, 0);
|
|
1671
|
-
assert.deepEqual(fs.readdirSync(protectedTarget), ['settings.json']);
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
|
-
test('a cache ancestor replaced during preparation cannot redirect later writes', async (t) => {
|
|
1675
|
-
const testFixture = fixture();
|
|
1676
|
-
const originalLstat = fsPromises.lstat;
|
|
1677
|
-
t.after(() => {
|
|
1678
|
-
fsPromises.lstat = originalLstat;
|
|
1679
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1680
|
-
});
|
|
1681
|
-
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
|
|
1682
|
-
const protectedVersion = path.join(protectedTarget, path.basename(stateDirectory(testFixture)));
|
|
1683
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1684
|
-
const probe = path.join(testFixture.root, 'symlink-probe');
|
|
1685
|
-
fs.mkdirSync(testFixture.cacheDirectory);
|
|
1686
|
-
fs.mkdirSync(protectedVersion, { recursive: true });
|
|
1687
|
-
fs.writeFileSync(path.join(protectedVersion, 'settings.json'), '{"protected":true}\n');
|
|
1688
|
-
try {
|
|
1689
|
-
fs.symlinkSync(
|
|
1690
|
-
protectedTarget,
|
|
1691
|
-
probe,
|
|
1692
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1693
|
-
);
|
|
1694
|
-
fs.unlinkSync(probe);
|
|
1695
|
-
} catch (error) {
|
|
1696
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1697
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
throw error;
|
|
1701
|
-
}
|
|
1702
|
-
let replaced = false;
|
|
1703
|
-
const versionDirectoryName = path.basename(stateDirectory(testFixture));
|
|
1704
|
-
fsPromises.lstat = async (target, ...args) => {
|
|
1705
|
-
if (!replaced && path.basename(target) === versionDirectoryName) {
|
|
1706
|
-
replaced = true;
|
|
1707
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1708
|
-
fs.symlinkSync(
|
|
1709
|
-
protectedTarget,
|
|
1710
|
-
testFixture.cacheDirectory,
|
|
1711
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1712
|
-
);
|
|
1713
|
-
}
|
|
1714
|
-
return originalLstat(target, ...args);
|
|
1715
|
-
};
|
|
1716
|
-
let requests = 0;
|
|
1717
|
-
|
|
1718
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1719
|
-
requests += 1;
|
|
1720
|
-
return response(remoteRelease());
|
|
1721
|
-
}));
|
|
1722
|
-
|
|
1723
|
-
assert.equal(replaced, true);
|
|
1724
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1725
|
-
assert.equal(requests, 0);
|
|
1726
|
-
assert.deepEqual(fs.readdirSync(protectedVersion), ['settings.json']);
|
|
1727
|
-
});
|
|
1728
|
-
|
|
1729
|
-
test('a cache ancestor replaced before reservation creation fails closed', async (t) => {
|
|
1730
|
-
const testFixture = fixture();
|
|
1731
|
-
const originalMkdir = fsPromises.mkdir;
|
|
1732
|
-
t.after(() => {
|
|
1733
|
-
fsPromises.mkdir = originalMkdir;
|
|
1734
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1735
|
-
});
|
|
1736
|
-
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
|
|
1737
|
-
const replacementVersion = path.join(
|
|
1738
|
-
replacementRoot,
|
|
1739
|
-
path.basename(stateDirectory(testFixture)),
|
|
1740
|
-
);
|
|
1741
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1742
|
-
const probe = path.join(testFixture.root, 'symlink-probe');
|
|
1743
|
-
fs.mkdirSync(replacementVersion, { recursive: true });
|
|
1744
|
-
fs.writeFileSync(path.join(replacementVersion, 'settings.json'), '{"protected":true}\n');
|
|
1745
|
-
try {
|
|
1746
|
-
fs.symlinkSync(
|
|
1747
|
-
replacementRoot,
|
|
1748
|
-
probe,
|
|
1749
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1750
|
-
);
|
|
1751
|
-
fs.unlinkSync(probe);
|
|
1752
|
-
} catch (error) {
|
|
1753
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1754
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
throw error;
|
|
1758
|
-
}
|
|
1759
|
-
let replaced = false;
|
|
1760
|
-
fsPromises.mkdir = async (target, ...args) => {
|
|
1761
|
-
if (!replaced && path.basename(target).startsWith('reserved-')) {
|
|
1762
|
-
replaced = true;
|
|
1763
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1764
|
-
fs.symlinkSync(
|
|
1765
|
-
replacementRoot,
|
|
1766
|
-
testFixture.cacheDirectory,
|
|
1767
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
return originalMkdir(target, ...args);
|
|
1771
|
-
};
|
|
1772
|
-
let requests = 0;
|
|
1773
|
-
|
|
1774
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1775
|
-
requests += 1;
|
|
1776
|
-
return response(remoteRelease());
|
|
1777
|
-
}));
|
|
1778
|
-
|
|
1779
|
-
assert.equal(replaced, true);
|
|
1780
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1781
|
-
assert.equal(requests, 0);
|
|
1782
|
-
const replacementEntries = fs.readdirSync(replacementVersion).sort();
|
|
1783
|
-
const reservations = replacementEntries.filter((entry) => entry.startsWith('reserved-'));
|
|
1784
|
-
assert.equal(reservations.length, 1);
|
|
1785
|
-
assert.deepEqual(
|
|
1786
|
-
replacementEntries.filter((entry) => !entry.startsWith('reserved-')),
|
|
1787
|
-
['settings.json'],
|
|
1788
|
-
);
|
|
1789
|
-
assert.deepEqual(fs.readdirSync(path.join(replacementVersion, reservations[0])), []);
|
|
1790
|
-
});
|
|
1791
|
-
|
|
1792
|
-
test('a cache ancestor replacement restored after reservation creation fails closed', async (t) => {
|
|
1793
|
-
const testFixture = fixture();
|
|
1794
|
-
const originalMkdir = fsPromises.mkdir;
|
|
1795
|
-
t.after(() => {
|
|
1796
|
-
fsPromises.mkdir = originalMkdir;
|
|
1797
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1798
|
-
});
|
|
1799
|
-
const replacementRoot = path.join(testFixture.root, 'replacement-cache');
|
|
1800
|
-
const replacementVersion = path.join(
|
|
1801
|
-
replacementRoot,
|
|
1802
|
-
path.basename(stateDirectory(testFixture)),
|
|
1803
|
-
);
|
|
1804
|
-
const detachedCache = path.join(testFixture.root, 'detached-cache');
|
|
1805
|
-
const probe = path.join(testFixture.root, 'symlink-probe');
|
|
1806
|
-
fs.mkdirSync(replacementVersion, { recursive: true });
|
|
1807
|
-
fs.writeFileSync(path.join(replacementVersion, 'settings.json'), '{"protected":true}\n');
|
|
1808
|
-
try {
|
|
1809
|
-
fs.symlinkSync(
|
|
1810
|
-
replacementRoot,
|
|
1811
|
-
probe,
|
|
1812
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1813
|
-
);
|
|
1814
|
-
fs.unlinkSync(probe);
|
|
1815
|
-
} catch (error) {
|
|
1816
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1817
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1818
|
-
return;
|
|
1819
|
-
}
|
|
1820
|
-
throw error;
|
|
1821
|
-
}
|
|
1822
|
-
let restored = false;
|
|
1823
|
-
fsPromises.mkdir = async (target, ...args) => {
|
|
1824
|
-
if (!restored && path.basename(target).startsWith('reserved-')) {
|
|
1825
|
-
fs.renameSync(testFixture.cacheDirectory, detachedCache);
|
|
1826
|
-
fs.symlinkSync(
|
|
1827
|
-
replacementRoot,
|
|
1828
|
-
testFixture.cacheDirectory,
|
|
1829
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1830
|
-
);
|
|
1831
|
-
try {
|
|
1832
|
-
return await originalMkdir(target, ...args);
|
|
1833
|
-
} finally {
|
|
1834
|
-
fs.unlinkSync(testFixture.cacheDirectory);
|
|
1835
|
-
fs.renameSync(detachedCache, testFixture.cacheDirectory);
|
|
1836
|
-
restored = true;
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
return originalMkdir(target, ...args);
|
|
1840
|
-
};
|
|
1841
|
-
let requests = 0;
|
|
1842
|
-
|
|
1843
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
1844
|
-
requests += 1;
|
|
1845
|
-
return response(remoteRelease());
|
|
1846
|
-
}));
|
|
1847
|
-
|
|
1848
|
-
assert.equal(restored, true);
|
|
1849
|
-
assert.deepEqual(result, { status: 'silent', reason: 'cache-unavailable' });
|
|
1850
|
-
assert.equal(requests, 0);
|
|
1851
|
-
const replacementEntries = fs.readdirSync(replacementVersion).sort();
|
|
1852
|
-
const reservations = replacementEntries.filter((entry) => entry.startsWith('reserved-'));
|
|
1853
|
-
assert.equal(reservations.length, 1);
|
|
1854
|
-
assert.deepEqual(
|
|
1855
|
-
replacementEntries.filter((entry) => !entry.startsWith('reserved-')),
|
|
1856
|
-
['settings.json'],
|
|
1857
|
-
);
|
|
1858
|
-
assert.deepEqual(fs.readdirSync(path.join(replacementVersion, reservations[0])), []);
|
|
1859
|
-
});
|
|
1860
|
-
|
|
1861
|
-
test('a trusted cache prefix switched after validation cannot redirect later writes', async (t) => {
|
|
1862
|
-
const testFixture = fixture();
|
|
1863
|
-
const originalHomedir = os.homedir;
|
|
1864
|
-
const originalLstat = fsPromises.lstat;
|
|
1865
|
-
t.after(() => {
|
|
1866
|
-
os.homedir = originalHomedir;
|
|
1867
|
-
fsPromises.lstat = originalLstat;
|
|
1868
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
1869
|
-
});
|
|
1870
|
-
const trustedAlias = path.join(testFixture.root, 'trusted-home');
|
|
1871
|
-
const canonicalHome = path.join(testFixture.root, 'canonical-home');
|
|
1872
|
-
const protectedTarget = path.join(testFixture.root, 'simulated-project-config');
|
|
1873
|
-
const cacheDirectory = path.join(trustedAlias, 'cache');
|
|
1874
|
-
fs.mkdirSync(canonicalHome);
|
|
1875
|
-
fs.mkdirSync(protectedTarget);
|
|
1876
|
-
fs.writeFileSync(path.join(protectedTarget, 'settings.json'), '{"protected":true}\n');
|
|
1877
|
-
try {
|
|
1878
|
-
fs.symlinkSync(
|
|
1879
|
-
canonicalHome,
|
|
1880
|
-
trustedAlias,
|
|
1881
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1882
|
-
);
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
1885
|
-
t.skip(`directory symlinks are unavailable: ${error.code}`);
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
|
-
throw error;
|
|
1889
|
-
}
|
|
1890
|
-
os.homedir = () => trustedAlias;
|
|
1891
|
-
const canonicalVersionDirectory = path.join(
|
|
1892
|
-
fs.realpathSync(canonicalHome),
|
|
1893
|
-
'cache',
|
|
1894
|
-
path.basename(stateDirectory(testFixture)),
|
|
1895
|
-
);
|
|
1896
|
-
let targetVisits = 0;
|
|
1897
|
-
let switched = false;
|
|
1898
|
-
fsPromises.lstat = async (target, ...args) => {
|
|
1899
|
-
const metadata = await originalLstat(target, ...args);
|
|
1900
|
-
if (!switched && path.resolve(target) === canonicalVersionDirectory) {
|
|
1901
|
-
targetVisits += 1;
|
|
1902
|
-
if (targetVisits === 2) {
|
|
1903
|
-
switched = true;
|
|
1904
|
-
fs.unlinkSync(trustedAlias);
|
|
1905
|
-
fs.symlinkSync(
|
|
1906
|
-
protectedTarget,
|
|
1907
|
-
trustedAlias,
|
|
1908
|
-
process.platform === 'win32' ? 'junction' : 'dir',
|
|
1909
|
-
);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
return metadata;
|
|
1913
|
-
};
|
|
1914
|
-
|
|
1915
|
-
const result = await checkForUpdate(options(testFixture, async () => response(remoteRelease()), {
|
|
1916
|
-
cacheDirectory,
|
|
1917
|
-
}));
|
|
1918
|
-
|
|
1919
|
-
assert.equal(switched, true);
|
|
1920
|
-
assert.equal(result.status, 'update_available');
|
|
1921
|
-
assert.deepEqual(fs.readdirSync(protectedTarget), ['settings.json']);
|
|
1922
|
-
assert.ok(fs.readdirSync(canonicalVersionDirectory).some(
|
|
1923
|
-
(entry) => entry.startsWith('committed-'),
|
|
1924
|
-
));
|
|
1925
|
-
});
|
|
1926
|
-
|
|
1927
|
-
test('concurrent checks use one writer and leave a valid cache', async (t) => {
|
|
1928
|
-
const testFixture = fixture();
|
|
1929
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1930
|
-
let releaseFetch;
|
|
1931
|
-
let markStarted;
|
|
1932
|
-
const started = new Promise((resolve) => { markStarted = resolve; });
|
|
1933
|
-
const fetchImpl = async () => {
|
|
1934
|
-
markStarted();
|
|
1935
|
-
return new Promise((resolve) => { releaseFetch = resolve; });
|
|
1936
|
-
};
|
|
1937
|
-
|
|
1938
|
-
const firstCheck = checkForUpdate(options(testFixture, fetchImpl));
|
|
1939
|
-
await started;
|
|
1940
|
-
const overlapping = await checkForUpdate(options(testFixture, fetchImpl));
|
|
1941
|
-
assert.deepEqual(overlapping, { status: 'silent', reason: 'check-in-progress' });
|
|
1942
|
-
|
|
1943
|
-
releaseFetch(response(remoteRelease()));
|
|
1944
|
-
const first = await firstCheck;
|
|
1945
|
-
assert.equal(first.status, 'update_available');
|
|
1946
|
-
assert.doesNotThrow(() => JSON.parse(
|
|
1947
|
-
fs.readFileSync(statePath(testFixture), 'utf8'),
|
|
1948
|
-
));
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
test('an empty precheck snapshot cannot start a second concurrent network request', async (t) => {
|
|
1952
|
-
const testFixture = fixture();
|
|
1953
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1954
|
-
fs.mkdirSync(stateDirectory(testFixture), { recursive: true });
|
|
1955
|
-
const pause = pauseReaddirSnapshot(stateDirectory(testFixture), 2);
|
|
1956
|
-
t.after(() => pause.restore());
|
|
1957
|
-
let releaseFetch;
|
|
1958
|
-
let markFetchStarted;
|
|
1959
|
-
let requests = 0;
|
|
1960
|
-
const fetchStarted = new Promise((resolve) => { markFetchStarted = resolve; });
|
|
1961
|
-
const fetchImpl = async () => {
|
|
1962
|
-
requests += 1;
|
|
1963
|
-
markFetchStarted();
|
|
1964
|
-
return new Promise((resolve) => { releaseFetch = resolve; });
|
|
1965
|
-
};
|
|
1966
|
-
|
|
1967
|
-
const delayedPrecheck = checkForUpdate(options(testFixture, fetchImpl));
|
|
1968
|
-
await pause.reached;
|
|
1969
|
-
const claimedCheck = checkForUpdate(options(testFixture, fetchImpl));
|
|
1970
|
-
await fetchStarted;
|
|
1971
|
-
pause.release();
|
|
1972
|
-
assert.deepEqual(await delayedPrecheck, { status: 'silent', reason: 'check-in-progress' });
|
|
1973
|
-
assert.equal(requests, 1);
|
|
1974
|
-
|
|
1975
|
-
releaseFetch(response(remoteRelease()));
|
|
1976
|
-
assert.equal((await claimedCheck).status, 'update_available');
|
|
1977
|
-
pause.restore();
|
|
1978
|
-
assert.equal(requests, 1);
|
|
1979
|
-
});
|
|
1980
|
-
|
|
1981
|
-
test('two promoters cannot replace and then steal a stale empty active claim', async (t) => {
|
|
1982
|
-
const testFixture = fixture();
|
|
1983
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
1984
|
-
const activeClaim = path.join(stateDirectory(testFixture), 'active-claim');
|
|
1985
|
-
fs.mkdirSync(activeClaim, { recursive: true });
|
|
1986
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
1987
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
1988
|
-
const pause = pauseWriteOnce('.retirement-guard');
|
|
1989
|
-
t.after(() => pause.restore());
|
|
1990
|
-
let releaseFetch;
|
|
1991
|
-
let markFetchStarted;
|
|
1992
|
-
let requests = 0;
|
|
1993
|
-
const fetchStarted = new Promise((resolve) => { markFetchStarted = resolve; });
|
|
1994
|
-
const fetchImpl = async () => {
|
|
1995
|
-
requests += 1;
|
|
1996
|
-
markFetchStarted();
|
|
1997
|
-
return new Promise((resolve) => { releaseFetch = resolve; });
|
|
1998
|
-
};
|
|
1999
|
-
|
|
2000
|
-
const delayedRetirement = checkForUpdate(options(testFixture, fetchImpl));
|
|
2001
|
-
await pause.reached;
|
|
2002
|
-
fs.utimesSync(
|
|
2003
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2004
|
-
staleTime,
|
|
2005
|
-
staleTime,
|
|
2006
|
-
);
|
|
2007
|
-
const claimedCheck = checkForUpdate(options(testFixture, fetchImpl));
|
|
2008
|
-
await fetchStarted;
|
|
2009
|
-
pause.release();
|
|
2010
|
-
assert.deepEqual(await delayedRetirement, { status: 'silent', reason: 'check-in-progress' });
|
|
2011
|
-
assert.equal(requests, 1);
|
|
2012
|
-
|
|
2013
|
-
releaseFetch(response(remoteRelease()));
|
|
2014
|
-
assert.equal((await claimedCheck).status, 'update_available');
|
|
2015
|
-
pause.restore();
|
|
2016
|
-
assert.equal(requests, 1);
|
|
2017
|
-
});
|
|
2018
|
-
|
|
2019
|
-
test('a delayed lower-generation promoter cannot retire a completed higher claim', async (t) => {
|
|
2020
|
-
const testFixture = fixture();
|
|
2021
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2022
|
-
const pause = pauseRenameOnce(
|
|
2023
|
-
operationPath(testFixture, 'claim', 1n),
|
|
2024
|
-
path.join(stateDirectory(testFixture), 'active-claim'),
|
|
2025
|
-
);
|
|
2026
|
-
t.after(() => pause.restore());
|
|
2027
|
-
let requests = 0;
|
|
2028
|
-
const fetchImpl = async () => {
|
|
2029
|
-
requests += 1;
|
|
2030
|
-
return response(remoteRelease());
|
|
2031
|
-
};
|
|
2032
|
-
|
|
2033
|
-
const delayedLower = checkForUpdate(options(testFixture, fetchImpl));
|
|
2034
|
-
await pause.reached;
|
|
2035
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2036
|
-
fs.utimesSync(
|
|
2037
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2038
|
-
staleTime,
|
|
2039
|
-
staleTime,
|
|
2040
|
-
);
|
|
2041
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2042
|
-
assert.equal(requests, 1);
|
|
2043
|
-
|
|
2044
|
-
pause.release();
|
|
2045
|
-
assert.equal((await delayedLower).status, 'update_available');
|
|
2046
|
-
pause.restore();
|
|
2047
|
-
assert.equal(requests, 1);
|
|
2048
|
-
assert.equal(
|
|
2049
|
-
JSON.parse(fs.readFileSync(
|
|
2050
|
-
path.join(stateDirectory(testFixture), 'active-claim', 'owner.json'),
|
|
2051
|
-
'utf8',
|
|
2052
|
-
)).generation,
|
|
2053
|
-
'2',
|
|
2054
|
-
);
|
|
2055
|
-
});
|
|
2056
|
-
|
|
2057
|
-
test('an expired contender cannot fetch after its delayed claim promotion succeeds', async (t) => {
|
|
2058
|
-
const testFixture = fixture();
|
|
2059
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2060
|
-
const pause = pauseRenameOnce(
|
|
2061
|
-
operationPath(testFixture, 'claim', 1n),
|
|
2062
|
-
path.join(stateDirectory(testFixture), 'active-claim'),
|
|
2063
|
-
);
|
|
2064
|
-
t.after(() => pause.restore());
|
|
2065
|
-
let requests = 0;
|
|
2066
|
-
const fetchImpl = async () => {
|
|
2067
|
-
requests += 1;
|
|
2068
|
-
return response(remoteRelease());
|
|
2069
|
-
};
|
|
2070
|
-
|
|
2071
|
-
const delayedExpired = checkForUpdate(options(testFixture, fetchImpl));
|
|
2072
|
-
await pause.reached;
|
|
2073
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2074
|
-
fs.utimesSync(
|
|
2075
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2076
|
-
staleTime,
|
|
2077
|
-
staleTime,
|
|
2078
|
-
);
|
|
2079
|
-
pause.release();
|
|
2080
|
-
assert.deepEqual(await delayedExpired, { status: 'silent', reason: 'check-in-progress' });
|
|
2081
|
-
assert.equal(requests, 0);
|
|
2082
|
-
|
|
2083
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2084
|
-
pause.restore();
|
|
2085
|
-
assert.equal(requests, 1);
|
|
2086
|
-
assert.equal(
|
|
2087
|
-
JSON.parse(fs.readFileSync(
|
|
2088
|
-
path.join(stateDirectory(testFixture), 'active-claim', 'owner.json'),
|
|
2089
|
-
'utf8',
|
|
2090
|
-
)).generation,
|
|
2091
|
-
'2',
|
|
2092
|
-
);
|
|
2093
|
-
});
|
|
2094
|
-
|
|
2095
|
-
test('a stale supersession snapshot cannot let a lower promoter retire a higher claim', async (t) => {
|
|
2096
|
-
const testFixture = fixture();
|
|
2097
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2098
|
-
const generationOneClaim = path.basename(operationPath(testFixture, 'claim', 1n));
|
|
2099
|
-
const pause = pauseReaddirSnapshotOnce(
|
|
2100
|
-
stateDirectory(testFixture),
|
|
2101
|
-
(entries) => entries.some((entry) => entry.name === generationOneClaim)
|
|
2102
|
-
&& !entries.some((entry) => /-(?:0{19}2)$/.test(entry.name)),
|
|
2103
|
-
);
|
|
2104
|
-
t.after(() => pause.restore());
|
|
2105
|
-
let requests = 0;
|
|
2106
|
-
const fetchImpl = async () => {
|
|
2107
|
-
requests += 1;
|
|
2108
|
-
return response(remoteRelease());
|
|
2109
|
-
};
|
|
2110
|
-
|
|
2111
|
-
const delayedLower = checkForUpdate(options(testFixture, fetchImpl));
|
|
2112
|
-
await pause.reached;
|
|
2113
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2114
|
-
fs.utimesSync(
|
|
2115
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2116
|
-
staleTime,
|
|
2117
|
-
staleTime,
|
|
2118
|
-
);
|
|
2119
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2120
|
-
assert.equal(requests, 1);
|
|
2121
|
-
|
|
2122
|
-
pause.release();
|
|
2123
|
-
assert.equal((await delayedLower).status, 'update_available');
|
|
2124
|
-
pause.restore();
|
|
2125
|
-
assert.equal(requests, 1);
|
|
2126
|
-
assert.equal(
|
|
2127
|
-
JSON.parse(fs.readFileSync(
|
|
2128
|
-
path.join(stateDirectory(testFixture), 'active-claim', 'owner.json'),
|
|
2129
|
-
'utf8',
|
|
2130
|
-
)).generation,
|
|
2131
|
-
'2',
|
|
2132
|
-
);
|
|
2133
|
-
});
|
|
2134
|
-
|
|
2135
|
-
test('an owner fenced while active ownership is read cannot issue a second GET', {
|
|
2136
|
-
timeout: parentCheckTimeoutMs,
|
|
2137
|
-
}, async (t) => {
|
|
2138
|
-
const testFixture = fixture();
|
|
2139
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2140
|
-
const directory = stateDirectory(testFixture);
|
|
2141
|
-
const originalOpen = fsPromises.open;
|
|
2142
|
-
let releaseVerification;
|
|
2143
|
-
let markVerification;
|
|
2144
|
-
let intercepted = false;
|
|
2145
|
-
const verificationReached = new Promise((resolve) => { markVerification = resolve; });
|
|
2146
|
-
const verificationGate = new Promise((resolve) => { releaseVerification = resolve; });
|
|
2147
|
-
fsPromises.open = async (target, ...args) => {
|
|
2148
|
-
const handle = await originalOpen(target, ...args);
|
|
2149
|
-
if (intercepted
|
|
2150
|
-
|| path.resolve(target) !== path.join(directory, 'active-claim', 'owner.json')) {
|
|
2151
|
-
return handle;
|
|
2152
|
-
}
|
|
2153
|
-
intercepted = true;
|
|
2154
|
-
return new Proxy(handle, {
|
|
2155
|
-
get(fileHandle, property) {
|
|
2156
|
-
if (property === 'close') {
|
|
2157
|
-
return async () => {
|
|
2158
|
-
const result = await fileHandle.close();
|
|
2159
|
-
markVerification();
|
|
2160
|
-
await verificationGate;
|
|
2161
|
-
return result;
|
|
2162
|
-
};
|
|
2163
|
-
}
|
|
2164
|
-
const value = Reflect.get(fileHandle, property, fileHandle);
|
|
2165
|
-
return typeof value === 'function' ? value.bind(fileHandle) : value;
|
|
2166
|
-
},
|
|
2167
|
-
});
|
|
2168
|
-
};
|
|
2169
|
-
t.after(() => {
|
|
2170
|
-
releaseVerification();
|
|
2171
|
-
fsPromises.open = originalOpen;
|
|
2172
|
-
});
|
|
2173
|
-
let requests = 0;
|
|
2174
|
-
const checkOptions = options(testFixture, async () => {
|
|
2175
|
-
requests += 1;
|
|
2176
|
-
return response(remoteRelease());
|
|
2177
|
-
});
|
|
2178
|
-
|
|
2179
|
-
const suspendedOwner = checkForUpdate(checkOptions);
|
|
2180
|
-
await verificationReached;
|
|
2181
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2182
|
-
fs.utimesSync(
|
|
2183
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2184
|
-
staleTime,
|
|
2185
|
-
staleTime,
|
|
2186
|
-
);
|
|
2187
|
-
assert.equal((await checkForUpdate(checkOptions)).status, 'update_available');
|
|
2188
|
-
assert.equal(requests, 1);
|
|
2189
|
-
|
|
2190
|
-
releaseVerification();
|
|
2191
|
-
assert.deepEqual(await suspendedOwner, { status: 'silent', reason: 'cache-unavailable' });
|
|
2192
|
-
assert.equal(requests, 1);
|
|
2193
|
-
});
|
|
2194
|
-
|
|
2195
|
-
test('an expired active owner cannot start a network request', async (t) => {
|
|
2196
|
-
const testFixture = fixture();
|
|
2197
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2198
|
-
const pendingOne = path.basename(operationPath(testFixture, 'pending', 1n));
|
|
2199
|
-
const claimOne = path.basename(operationPath(testFixture, 'claim', 1n));
|
|
2200
|
-
const pause = pauseReaddirSnapshotOnce(
|
|
2201
|
-
stateDirectory(testFixture),
|
|
2202
|
-
(entries) => entries.some((entry) => entry.name === 'active-claim')
|
|
2203
|
-
&& entries.some((entry) => entry.name === pendingOne)
|
|
2204
|
-
&& !entries.some((entry) => entry.name === claimOne),
|
|
2205
|
-
);
|
|
2206
|
-
t.after(() => pause.restore());
|
|
2207
|
-
let requests = 0;
|
|
2208
|
-
|
|
2209
|
-
const check = checkForUpdate(options(testFixture, async () => {
|
|
2210
|
-
requests += 1;
|
|
2211
|
-
return response(remoteRelease());
|
|
2212
|
-
}));
|
|
2213
|
-
await pause.reached;
|
|
2214
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2215
|
-
fs.utimesSync(
|
|
2216
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2217
|
-
staleTime,
|
|
2218
|
-
staleTime,
|
|
2219
|
-
);
|
|
2220
|
-
pause.release();
|
|
2221
|
-
|
|
2222
|
-
assert.deepEqual(await check, { status: 'silent', reason: 'check-in-progress' });
|
|
2223
|
-
pause.restore();
|
|
2224
|
-
assert.equal(requests, 0);
|
|
2225
|
-
});
|
|
2226
|
-
|
|
2227
|
-
test('a fenced owner rechecks its active claim immediately before network access', async (t) => {
|
|
2228
|
-
const testFixture = fixture();
|
|
2229
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2230
|
-
const pendingOne = path.basename(operationPath(testFixture, 'pending', 1n));
|
|
2231
|
-
const claimOne = path.basename(operationPath(testFixture, 'claim', 1n));
|
|
2232
|
-
const pause = pauseReaddirSnapshotOnce(
|
|
2233
|
-
stateDirectory(testFixture),
|
|
2234
|
-
(entries) => entries.some((entry) => entry.name === 'active-claim')
|
|
2235
|
-
&& entries.some((entry) => entry.name === pendingOne)
|
|
2236
|
-
&& !entries.some((entry) => entry.name === claimOne),
|
|
2237
|
-
);
|
|
2238
|
-
t.after(() => pause.restore());
|
|
2239
|
-
let requests = 0;
|
|
2240
|
-
let releaseFetch;
|
|
2241
|
-
let markFetchStarted;
|
|
2242
|
-
const fetchStarted = new Promise((resolve) => { markFetchStarted = resolve; });
|
|
2243
|
-
const fetchImpl = async () => {
|
|
2244
|
-
requests += 1;
|
|
2245
|
-
markFetchStarted();
|
|
2246
|
-
return new Promise((resolve) => { releaseFetch = resolve; });
|
|
2247
|
-
};
|
|
2248
|
-
|
|
2249
|
-
const delayedOwner = checkForUpdate(options(testFixture, fetchImpl));
|
|
2250
|
-
await pause.reached;
|
|
2251
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2252
|
-
fs.utimesSync(
|
|
2253
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2254
|
-
staleTime,
|
|
2255
|
-
staleTime,
|
|
2256
|
-
);
|
|
2257
|
-
const successor = checkForUpdate(options(testFixture, fetchImpl));
|
|
2258
|
-
await fetchStarted;
|
|
2259
|
-
assert.equal(requests, 1);
|
|
2260
|
-
|
|
2261
|
-
pause.release();
|
|
2262
|
-
assert.deepEqual(await delayedOwner, { status: 'silent', reason: 'check-in-progress' });
|
|
2263
|
-
pause.restore();
|
|
2264
|
-
assert.equal(requests, 1);
|
|
2265
|
-
|
|
2266
|
-
releaseFetch(response(remoteRelease()));
|
|
2267
|
-
assert.equal((await successor).status, 'update_available');
|
|
2268
|
-
assert.equal(requests, 1);
|
|
2269
|
-
});
|
|
2270
|
-
|
|
2271
|
-
test('an overlapping check reads the last-good candidate while another process refreshes it', async (t) => {
|
|
2272
|
-
const testFixture = fixture();
|
|
2273
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2274
|
-
let requests = 0;
|
|
2275
|
-
let releaseRefresh;
|
|
2276
|
-
let markRefreshStarted;
|
|
2277
|
-
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
|
|
2278
|
-
const fetchImpl = async () => {
|
|
2279
|
-
requests += 1;
|
|
2280
|
-
if (requests === 1) return response(remoteRelease());
|
|
2281
|
-
markRefreshStarted();
|
|
2282
|
-
return new Promise((resolve) => { releaseRefresh = resolve; });
|
|
2283
|
-
};
|
|
2284
|
-
|
|
2285
|
-
const first = await checkForUpdate(options(testFixture, fetchImpl));
|
|
2286
|
-
const refreshOptions = options(testFixture, fetchImpl, {
|
|
2287
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2288
|
-
});
|
|
2289
|
-
const refresh = checkForUpdate(refreshOptions);
|
|
2290
|
-
await refreshStarted;
|
|
2291
|
-
|
|
2292
|
-
const overlapping = await checkForUpdate(refreshOptions);
|
|
2293
|
-
assert.equal(overlapping.status, 'update_available');
|
|
2294
|
-
assert.equal(overlapping.eventKey, first.eventKey);
|
|
2295
|
-
assert.equal(requests, 2);
|
|
2296
|
-
|
|
2297
|
-
releaseRefresh(response(remoteRelease()));
|
|
2298
|
-
assert.equal((await refresh).status, 'update_available');
|
|
2299
|
-
});
|
|
2300
|
-
|
|
2301
|
-
test('acknowledgement waits briefly for an in-flight refresh instead of losing the notice', async (t) => {
|
|
2302
|
-
const testFixture = fixture();
|
|
2303
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2304
|
-
const offered = await checkForUpdate(options(
|
|
2305
|
-
testFixture,
|
|
2306
|
-
async () => response(remoteRelease()),
|
|
2307
|
-
));
|
|
2308
|
-
let releaseRefresh;
|
|
2309
|
-
let markRefreshStarted;
|
|
2310
|
-
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
|
|
2311
|
-
const refresh = checkForUpdate(options(testFixture, async () => {
|
|
2312
|
-
markRefreshStarted();
|
|
2313
|
-
return new Promise((resolve) => { releaseRefresh = resolve; });
|
|
2314
|
-
}, {
|
|
2315
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2316
|
-
}));
|
|
2317
|
-
await refreshStarted;
|
|
2318
|
-
|
|
2319
|
-
const acknowledgement = acknowledgeUpdate({
|
|
2320
|
-
releasePath: testFixture.releasePath,
|
|
2321
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2322
|
-
eventKey: offered.eventKey,
|
|
2323
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000) + 1,
|
|
2324
|
-
});
|
|
2325
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2326
|
-
releaseRefresh(response(remoteRelease()));
|
|
2327
|
-
await refresh;
|
|
2328
|
-
|
|
2329
|
-
assert.deepEqual(await acknowledgement, {
|
|
2330
|
-
status: 'acknowledged',
|
|
2331
|
-
eventKey: offered.eventKey,
|
|
2332
|
-
});
|
|
2333
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(remoteRelease()), {
|
|
2334
|
-
now: () => baseTime + (74 * 60 * 60 * 1_000),
|
|
2335
|
-
})), { status: 'silent', reason: 'already-notified' });
|
|
2336
|
-
});
|
|
2337
|
-
|
|
2338
|
-
test('a last-good notice remains acknowledgeable after the refresh commits a new candidate', async (t) => {
|
|
2339
|
-
const testFixture = fixture();
|
|
2340
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2341
|
-
const firstRelease = remoteReleaseForVersion('2.16.0', 'b'.repeat(64));
|
|
2342
|
-
const secondRelease = remoteReleaseForVersion('2.17.0', 'c'.repeat(64));
|
|
2343
|
-
const offered = await checkForUpdate(options(
|
|
2344
|
-
testFixture,
|
|
2345
|
-
async () => response(firstRelease),
|
|
2346
|
-
));
|
|
2347
|
-
let releaseRefresh;
|
|
2348
|
-
let markRefreshStarted;
|
|
2349
|
-
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
|
|
2350
|
-
const refresh = checkForUpdate(options(testFixture, async () => {
|
|
2351
|
-
markRefreshStarted();
|
|
2352
|
-
return new Promise((resolve) => { releaseRefresh = resolve; });
|
|
2353
|
-
}, {
|
|
2354
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2355
|
-
}));
|
|
2356
|
-
await refreshStarted;
|
|
2357
|
-
|
|
2358
|
-
const visibleDuringRefresh = await checkForUpdate(options(testFixture, async () => {
|
|
2359
|
-
assert.fail('an overlapping reader must not start another network request');
|
|
2360
|
-
}, {
|
|
2361
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2362
|
-
}));
|
|
2363
|
-
assert.equal(visibleDuringRefresh.eventKey, offered.eventKey);
|
|
2364
|
-
const acknowledgement = acknowledgeUpdate({
|
|
2365
|
-
releasePath: testFixture.releasePath,
|
|
2366
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2367
|
-
eventKey: visibleDuringRefresh.eventKey,
|
|
2368
|
-
});
|
|
2369
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2370
|
-
releaseRefresh(response(secondRelease));
|
|
2371
|
-
assert.equal((await refresh).latestVersion, '2.17.0');
|
|
2372
|
-
assert.deepEqual(await acknowledgement, {
|
|
2373
|
-
status: 'acknowledged',
|
|
2374
|
-
eventKey: offered.eventKey,
|
|
2375
|
-
});
|
|
2376
|
-
|
|
2377
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(firstRelease), {
|
|
2378
|
-
now: () => baseTime + (146 * 60 * 60 * 1_000),
|
|
2379
|
-
})), { status: 'silent', reason: 'already-notified' });
|
|
2380
|
-
});
|
|
2381
|
-
|
|
2382
|
-
test('acknowledgement retry budget uses a monotonic clock', async (t) => {
|
|
2383
|
-
const testFixture = fixture();
|
|
2384
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2385
|
-
const offered = await checkForUpdate(options(
|
|
2386
|
-
testFixture,
|
|
2387
|
-
async () => response(remoteRelease()),
|
|
2388
|
-
));
|
|
2389
|
-
writePendingOperation(testFixture, { generation: 2n });
|
|
2390
|
-
const ticks = [100, 1_301];
|
|
2391
|
-
const started = Date.now();
|
|
2392
|
-
|
|
2393
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
2394
|
-
releasePath: testFixture.releasePath,
|
|
2395
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2396
|
-
eventKey: offered.eventKey,
|
|
2397
|
-
monotonicNow: () => ticks.shift() ?? 1_301,
|
|
2398
|
-
}), { status: 'silent', reason: 'check-in-progress' });
|
|
2399
|
-
assert.ok(Date.now() - started < 200, 'an exhausted monotonic budget should not sleep on wall time');
|
|
2400
|
-
});
|
|
2401
|
-
|
|
2402
|
-
test('a delayed allocator cannot reuse a generation after its reservation name was taken', async (t) => {
|
|
2403
|
-
const testFixture = fixture();
|
|
2404
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2405
|
-
const firstRelease = remoteReleaseForVersion('2.16.0', 'b'.repeat(64));
|
|
2406
|
-
const secondRelease = remoteReleaseForVersion('2.17.0', 'c'.repeat(64));
|
|
2407
|
-
const offered = await checkForUpdate(options(
|
|
2408
|
-
testFixture,
|
|
2409
|
-
async () => response(firstRelease),
|
|
2410
|
-
));
|
|
2411
|
-
const pause = pauseMkdirOnce(operationPath(testFixture, 'reserved', 2n));
|
|
2412
|
-
t.after(() => pause.restore());
|
|
2413
|
-
|
|
2414
|
-
const acknowledgement = acknowledgeUpdate({
|
|
2415
|
-
releasePath: testFixture.releasePath,
|
|
2416
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2417
|
-
eventKey: offered.eventKey,
|
|
2418
|
-
});
|
|
2419
|
-
await pause.reached;
|
|
2420
|
-
assert.equal((await checkForUpdate(options(testFixture, async () => response(secondRelease), {
|
|
2421
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2422
|
-
}))).latestVersion, '2.17.0');
|
|
2423
|
-
|
|
2424
|
-
pause.release();
|
|
2425
|
-
assert.deepEqual(await acknowledgement, { status: 'acknowledged', eventKey: offered.eventKey });
|
|
2426
|
-
pause.restore();
|
|
2427
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 2n)), true);
|
|
2428
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 3n)), true);
|
|
2429
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(firstRelease), {
|
|
2430
|
-
now: () => baseTime + (146 * 60 * 60 * 1_000),
|
|
2431
|
-
})), { status: 'silent', reason: 'already-notified' });
|
|
2432
|
-
});
|
|
2433
|
-
|
|
2434
|
-
test('an oversized generation name cannot poison future allocation', async (t) => {
|
|
2435
|
-
const testFixture = fixture();
|
|
2436
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2437
|
-
fs.mkdirSync(stateDirectory(testFixture), { recursive: true });
|
|
2438
|
-
fs.mkdirSync(path.join(stateDirectory(testFixture), `reserved-${'9'.repeat(246)}`));
|
|
2439
|
-
let requests = 0;
|
|
2440
|
-
const fetchImpl = async () => {
|
|
2441
|
-
requests += 1;
|
|
2442
|
-
return response(remoteRelease());
|
|
2443
|
-
};
|
|
2444
|
-
|
|
2445
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2446
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2447
|
-
assert.equal(requests, 1);
|
|
2448
|
-
assert.ok(fs.existsSync(operationPath(testFixture, 'reserved', 1n)));
|
|
2449
|
-
});
|
|
2450
|
-
|
|
2451
|
-
test('a lower generation stalled after reservation cannot commit behind a newer generation', async (t) => {
|
|
2452
|
-
const testFixture = fixture();
|
|
2453
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2454
|
-
const firstRelease = remoteReleaseForVersion('2.16.0', 'b'.repeat(64));
|
|
2455
|
-
const secondRelease = remoteReleaseForVersion('2.17.0', 'c'.repeat(64));
|
|
2456
|
-
const offered = await checkForUpdate(options(
|
|
2457
|
-
testFixture,
|
|
2458
|
-
async () => response(firstRelease),
|
|
2459
|
-
));
|
|
2460
|
-
const pause = pauseMkdirOnce(operationPath(testFixture, 'pending', 2n));
|
|
2461
|
-
t.after(() => pause.restore());
|
|
2462
|
-
|
|
2463
|
-
const acknowledgement = acknowledgeUpdate({
|
|
2464
|
-
releasePath: testFixture.releasePath,
|
|
2465
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2466
|
-
eventKey: offered.eventKey,
|
|
2467
|
-
});
|
|
2468
|
-
await pause.reached;
|
|
2469
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 2n)), true);
|
|
2470
|
-
assert.equal((await checkForUpdate(options(testFixture, async () => response(secondRelease), {
|
|
2471
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2472
|
-
}))).latestVersion, '2.17.0');
|
|
2473
|
-
|
|
2474
|
-
pause.release();
|
|
2475
|
-
assert.deepEqual(await acknowledgement, { status: 'acknowledged', eventKey: offered.eventKey });
|
|
2476
|
-
pause.restore();
|
|
2477
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'committed', 2n)), false);
|
|
2478
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'cancelled', 2n)), true);
|
|
2479
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 3n)), true);
|
|
2480
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'reserved', 4n)), true);
|
|
2481
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(firstRelease), {
|
|
2482
|
-
now: () => baseTime + (146 * 60 * 60 * 1_000),
|
|
2483
|
-
})), { status: 'silent', reason: 'already-notified' });
|
|
2484
|
-
});
|
|
2485
|
-
|
|
2486
|
-
test('acknowledgement fences an expired refresh and the resumed owner cannot erase it', async (t) => {
|
|
2487
|
-
const testFixture = fixture();
|
|
2488
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2489
|
-
const offered = await checkForUpdate(options(
|
|
2490
|
-
testFixture,
|
|
2491
|
-
async () => response(remoteRelease()),
|
|
2492
|
-
));
|
|
2493
|
-
|
|
2494
|
-
let resumeRefresh;
|
|
2495
|
-
let markRefreshStarted;
|
|
2496
|
-
const refreshStarted = new Promise((resolve) => { markRefreshStarted = resolve; });
|
|
2497
|
-
const refresh = checkForUpdate(options(testFixture, async () => {
|
|
2498
|
-
markRefreshStarted();
|
|
2499
|
-
return new Promise((resolve) => { resumeRefresh = resolve; });
|
|
2500
|
-
}, {
|
|
2501
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2502
|
-
timeoutMs: 2_000,
|
|
2503
|
-
}));
|
|
2504
|
-
await refreshStarted;
|
|
2505
|
-
|
|
2506
|
-
const refreshPending = operationPath(testFixture, 'pending', 2n);
|
|
2507
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2508
|
-
fs.utimesSync(path.join(refreshPending, 'owner.json'), staleTime, staleTime);
|
|
2509
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
2510
|
-
releasePath: testFixture.releasePath,
|
|
2511
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2512
|
-
eventKey: offered.eventKey,
|
|
2513
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000) + 1,
|
|
2514
|
-
}), { status: 'acknowledged', eventKey: offered.eventKey });
|
|
2515
|
-
|
|
2516
|
-
resumeRefresh(response(remoteRelease()));
|
|
2517
|
-
assert.deepEqual(await refresh, { status: 'silent', reason: 'check-in-progress' });
|
|
2518
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, async () => response(remoteRelease()), {
|
|
2519
|
-
now: () => baseTime + (71 * 60 * 60 * 1_000),
|
|
2520
|
-
})), { status: 'silent', reason: 'already-notified' });
|
|
2521
|
-
});
|
|
2522
|
-
|
|
2523
|
-
for (const pausePoint of ['pending owner creation', 'state temp creation']) {
|
|
2524
|
-
test(`acknowledgement retries when fenced during ${pausePoint}`, async (t) => {
|
|
2525
|
-
const testFixture = fixture();
|
|
2526
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2527
|
-
const offered = await checkForUpdate(options(
|
|
2528
|
-
testFixture,
|
|
2529
|
-
async () => response(remoteRelease()),
|
|
2530
|
-
));
|
|
2531
|
-
const pending = operationPath(testFixture, 'pending', 2n);
|
|
2532
|
-
const pause = pauseOpenOnce((target) => {
|
|
2533
|
-
if (pausePoint === 'pending owner creation') {
|
|
2534
|
-
return path.resolve(target) === path.join(pending, 'owner.json');
|
|
2535
|
-
}
|
|
2536
|
-
return path.dirname(path.resolve(target)) === pending
|
|
2537
|
-
&& /^\.state\..+\.tmp$/.test(path.basename(target));
|
|
2538
|
-
});
|
|
2539
|
-
t.after(() => pause.restore());
|
|
2540
|
-
|
|
2541
|
-
const acknowledgement = acknowledgeUpdate({
|
|
2542
|
-
releasePath: testFixture.releasePath,
|
|
2543
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2544
|
-
eventKey: offered.eventKey,
|
|
2545
|
-
});
|
|
2546
|
-
await pause.reached;
|
|
2547
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2548
|
-
fs.utimesSync(
|
|
2549
|
-
pausePoint === 'pending owner creation' ? pending : path.join(pending, 'owner.json'),
|
|
2550
|
-
staleTime,
|
|
2551
|
-
staleTime,
|
|
2552
|
-
);
|
|
2553
|
-
|
|
2554
|
-
assert.equal((await checkForUpdate(options(
|
|
2555
|
-
testFixture,
|
|
2556
|
-
async () => response(remoteRelease()),
|
|
2557
|
-
{ now: () => baseTime + (73 * 60 * 60 * 1_000) },
|
|
2558
|
-
))).status, 'update_available');
|
|
2559
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 2n)), true);
|
|
2560
|
-
|
|
2561
|
-
pause.release();
|
|
2562
|
-
assert.deepEqual(await acknowledgement, {
|
|
2563
|
-
status: 'acknowledged',
|
|
2564
|
-
eventKey: offered.eventKey,
|
|
2565
|
-
});
|
|
2566
|
-
pause.restore();
|
|
2567
|
-
});
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
test('a stale corrupt operation is fenced without exposing a failure', async (t) => {
|
|
2571
|
-
const testFixture = fixture();
|
|
2572
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2573
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2574
|
-
const pending = writePendingOperation(testFixture, {
|
|
2575
|
-
owner: 'corrupt-owner',
|
|
2576
|
-
modifiedAt: staleTime,
|
|
2577
|
-
});
|
|
2578
|
-
|
|
2579
|
-
const result = await checkForUpdate(options(
|
|
2580
|
-
testFixture,
|
|
2581
|
-
async () => response(remoteRelease()),
|
|
2582
|
-
));
|
|
2583
|
-
|
|
2584
|
-
assert.equal(result.status, 'update_available');
|
|
2585
|
-
assert.equal(fs.existsSync(pending), false);
|
|
2586
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
|
|
2587
|
-
});
|
|
2588
|
-
|
|
2589
|
-
test('malformed active-claim shapes recover after the hard lease', async () => {
|
|
2590
|
-
for (const shape of ['file', 'symlink', 'owner-directory']) {
|
|
2591
|
-
const testFixture = fixture();
|
|
2592
|
-
try {
|
|
2593
|
-
const directory = stateDirectory(testFixture);
|
|
2594
|
-
const activeClaim = path.join(directory, 'active-claim');
|
|
2595
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
2596
|
-
if (shape === 'file') {
|
|
2597
|
-
fs.writeFileSync(activeClaim, 'corrupt claim');
|
|
2598
|
-
} else if (shape === 'symlink') {
|
|
2599
|
-
fs.symlinkSync(path.join(testFixture.root, 'missing-claim-target'), activeClaim);
|
|
2600
|
-
} else {
|
|
2601
|
-
fs.mkdirSync(path.join(activeClaim, 'owner.json'), { recursive: true });
|
|
2602
|
-
}
|
|
2603
|
-
let requests = 0;
|
|
2604
|
-
const fetchImpl = async () => {
|
|
2605
|
-
requests += 1;
|
|
2606
|
-
return response(remoteRelease());
|
|
2607
|
-
};
|
|
2608
|
-
|
|
2609
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
|
|
2610
|
-
status: 'silent',
|
|
2611
|
-
reason: 'check-in-progress',
|
|
2612
|
-
}, `${shape} should retain a fresh hard lease`);
|
|
2613
|
-
assert.equal(requests, 0);
|
|
2614
|
-
|
|
2615
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2616
|
-
if (shape === 'symlink') fs.lutimesSync(activeClaim, staleTime, staleTime);
|
|
2617
|
-
else if (shape === 'owner-directory') {
|
|
2618
|
-
fs.utimesSync(path.join(activeClaim, 'owner.json'), staleTime, staleTime);
|
|
2619
|
-
} else fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2620
|
-
|
|
2621
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2622
|
-
assert.equal(requests, 1, `${shape} should be retired after its hard lease`);
|
|
2623
|
-
} finally {
|
|
2624
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
});
|
|
2628
|
-
|
|
2629
|
-
test('a delayed corrupt-claim retirement cannot move a successor active claim', async (t) => {
|
|
2630
|
-
const testFixture = fixture();
|
|
2631
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2632
|
-
const directory = stateDirectory(testFixture);
|
|
2633
|
-
const activeClaim = path.join(directory, 'active-claim');
|
|
2634
|
-
fs.mkdirSync(activeClaim, { recursive: true });
|
|
2635
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2636
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2637
|
-
let firstRetirement = null;
|
|
2638
|
-
const pause = pauseRenameMatchingOnce((source, destination) => {
|
|
2639
|
-
if (path.resolve(source) !== activeClaim
|
|
2640
|
-
|| !path.basename(path.dirname(destination)).startsWith('retired-claim-corrupt-')) {
|
|
2641
|
-
return false;
|
|
2642
|
-
}
|
|
2643
|
-
firstRetirement = destination;
|
|
2644
|
-
return true;
|
|
2645
|
-
});
|
|
2646
|
-
t.after(() => pause.restore());
|
|
2647
|
-
|
|
2648
|
-
const delayed = checkForUpdate(options(
|
|
2649
|
-
testFixture,
|
|
2650
|
-
async () => response(remoteRelease()),
|
|
2651
|
-
));
|
|
2652
|
-
await pause.reached;
|
|
2653
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2654
|
-
fs.utimesSync(
|
|
2655
|
-
path.join(operationPath(testFixture, 'pending', 1n), 'owner.json'),
|
|
2656
|
-
staleTime,
|
|
2657
|
-
staleTime,
|
|
2658
|
-
);
|
|
2659
|
-
|
|
2660
|
-
assert.equal((await checkForUpdate(options(
|
|
2661
|
-
testFixture,
|
|
2662
|
-
async () => response(remoteRelease()),
|
|
2663
|
-
))).status, 'update_available');
|
|
2664
|
-
assert.equal(
|
|
2665
|
-
JSON.parse(fs.readFileSync(path.join(activeClaim, 'owner.json'), 'utf8')).generation,
|
|
2666
|
-
'2',
|
|
2667
|
-
);
|
|
2668
|
-
|
|
2669
|
-
pause.release();
|
|
2670
|
-
assert.equal((await delayed).status, 'update_available');
|
|
2671
|
-
pause.restore();
|
|
2672
|
-
assert.equal(firstRetirement !== null, true);
|
|
2673
|
-
assert.equal(fs.existsSync(activeClaim), true);
|
|
2674
|
-
assert.equal(
|
|
2675
|
-
JSON.parse(fs.readFileSync(path.join(activeClaim, 'owner.json'), 'utf8')).generation,
|
|
2676
|
-
'2',
|
|
2677
|
-
);
|
|
2678
|
-
});
|
|
2679
|
-
|
|
2680
|
-
test('repeated corrupt claim contents receive distinct retirement identities', async (t) => {
|
|
2681
|
-
const testFixture = fixture();
|
|
2682
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2683
|
-
const directory = stateDirectory(testFixture);
|
|
2684
|
-
const activeClaim = path.join(directory, 'active-claim');
|
|
2685
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
2686
|
-
fs.writeFileSync(activeClaim, 'same corrupt claim');
|
|
2687
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2688
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2689
|
-
let requests = 0;
|
|
2690
|
-
const fetchImpl = async () => {
|
|
2691
|
-
requests += 1;
|
|
2692
|
-
return response(remoteRelease());
|
|
2693
|
-
};
|
|
2694
|
-
|
|
2695
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2696
|
-
fs.renameSync(activeClaim, path.join(directory, 'test-completed-active-claim'));
|
|
2697
|
-
fs.writeFileSync(activeClaim, 'same corrupt claim');
|
|
2698
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2699
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl, {
|
|
2700
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2701
|
-
}))).status, 'update_available');
|
|
2702
|
-
|
|
2703
|
-
assert.equal(requests, 2);
|
|
2704
|
-
assert.equal(
|
|
2705
|
-
fs.readdirSync(directory).filter((name) => name.startsWith('retired-claim-corrupt-')).length,
|
|
2706
|
-
2,
|
|
2707
|
-
);
|
|
2708
|
-
});
|
|
2709
|
-
|
|
2710
|
-
test('corrupt claim retirement preserves distinct 64-bit inode identities', async (t) => {
|
|
2711
|
-
const testFixture = fixture();
|
|
2712
|
-
const originalLstat = fsPromises.lstat;
|
|
2713
|
-
t.after(() => {
|
|
2714
|
-
fsPromises.lstat = originalLstat;
|
|
2715
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
2716
|
-
});
|
|
2717
|
-
const directory = stateDirectory(testFixture);
|
|
2718
|
-
const activeClaim = path.join(directory, 'active-claim');
|
|
2719
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
2720
|
-
fs.writeFileSync(activeClaim, 'corrupt claim');
|
|
2721
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2722
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2723
|
-
let instance = 1n;
|
|
2724
|
-
fsPromises.lstat = async (target, ...args) => {
|
|
2725
|
-
const metadata = await originalLstat(target, ...args);
|
|
2726
|
-
const resolved = path.resolve(target);
|
|
2727
|
-
const isCorruptClaimInstance = resolved === path.resolve(activeClaim)
|
|
2728
|
-
|| (path.basename(resolved) === 'active'
|
|
2729
|
-
&& path.basename(path.dirname(resolved)).startsWith('retired-claim-corrupt-'));
|
|
2730
|
-
if (!isCorruptClaimInstance || !metadata.isFile()) return metadata;
|
|
2731
|
-
const inode = 9_007_199_254_740_992n + (instance - 1n);
|
|
2732
|
-
return args[0]?.bigint
|
|
2733
|
-
? metadataWithOverrides(metadata, {
|
|
2734
|
-
ino: inode,
|
|
2735
|
-
birthtimeNs: 1_000n,
|
|
2736
|
-
ctimeNs: 2_000n,
|
|
2737
|
-
})
|
|
2738
|
-
: metadataWithOverrides(metadata, {
|
|
2739
|
-
ino: Number(inode),
|
|
2740
|
-
birthtimeMs: 1,
|
|
2741
|
-
ctimeMs: 2,
|
|
2742
|
-
});
|
|
2743
|
-
};
|
|
2744
|
-
let requests = 0;
|
|
2745
|
-
const fetchImpl = async () => {
|
|
2746
|
-
requests += 1;
|
|
2747
|
-
return response(remoteRelease());
|
|
2748
|
-
};
|
|
2749
|
-
|
|
2750
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2751
|
-
fs.renameSync(activeClaim, path.join(directory, 'test-completed-active-claim'));
|
|
2752
|
-
fs.writeFileSync(activeClaim, 'corrupt claim');
|
|
2753
|
-
fs.utimesSync(activeClaim, staleTime, staleTime);
|
|
2754
|
-
instance = 2n;
|
|
2755
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl, {
|
|
2756
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2757
|
-
}))).status, 'update_available');
|
|
2758
|
-
|
|
2759
|
-
assert.equal(requests, 2);
|
|
2760
|
-
assert.equal(
|
|
2761
|
-
fs.readdirSync(directory).filter((name) => name.startsWith('retired-claim-corrupt-')).length,
|
|
2762
|
-
2,
|
|
2763
|
-
);
|
|
2764
|
-
});
|
|
2765
|
-
|
|
2766
|
-
test('a forged retirement symlink cannot move an active claim outside its cache partition', async (t) => {
|
|
2767
|
-
const testFixture = fixture();
|
|
2768
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2769
|
-
let requests = 0;
|
|
2770
|
-
const fetchImpl = async () => {
|
|
2771
|
-
requests += 1;
|
|
2772
|
-
return response(remoteRelease());
|
|
2773
|
-
};
|
|
2774
|
-
assert.equal((await checkForUpdate(options(testFixture, fetchImpl))).status, 'update_available');
|
|
2775
|
-
|
|
2776
|
-
const outside = path.join(testFixture.root, 'outside');
|
|
2777
|
-
fs.mkdirSync(outside);
|
|
2778
|
-
fs.symlinkSync(outside, path.join(stateDirectory(testFixture), 'retired-claim-1'));
|
|
2779
|
-
const refreshOptions = options(testFixture, fetchImpl, {
|
|
2780
|
-
now: () => baseTime + (73 * 60 * 60 * 1_000),
|
|
2781
|
-
});
|
|
2782
|
-
assert.equal((await checkForUpdate(refreshOptions)).status, 'update_available');
|
|
2783
|
-
assert.deepEqual(fs.readdirSync(outside), []);
|
|
2784
|
-
assert.equal(requests, 1);
|
|
2785
|
-
|
|
2786
|
-
fs.unlinkSync(path.join(stateDirectory(testFixture), 'retired-claim-1'));
|
|
2787
|
-
assert.equal((await checkForUpdate(refreshOptions)).status, 'update_available');
|
|
2788
|
-
assert.deepEqual(fs.readdirSync(outside), []);
|
|
2789
|
-
assert.equal(requests, 2);
|
|
2790
|
-
});
|
|
2791
|
-
|
|
2792
|
-
test('an expired operation is fenced even when its PID was reused by a live process', async (t) => {
|
|
2793
|
-
const testFixture = fixture();
|
|
2794
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2795
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2796
|
-
const pending = writePendingOperation(testFixture, { modifiedAt: staleTime });
|
|
2797
|
-
let requests = 0;
|
|
2798
|
-
|
|
2799
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
2800
|
-
requests += 1;
|
|
2801
|
-
return response(remoteRelease());
|
|
2802
|
-
}));
|
|
2803
|
-
|
|
2804
|
-
assert.equal(result.status, 'update_available');
|
|
2805
|
-
assert.equal(requests, 1);
|
|
2806
|
-
assert.equal(fs.existsSync(pending), false);
|
|
2807
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
|
|
2808
|
-
});
|
|
2809
|
-
|
|
2810
|
-
test('a corrupt future-dated operation cannot suppress checks indefinitely', async (t) => {
|
|
2811
|
-
const testFixture = fixture();
|
|
2812
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2813
|
-
const futureTime = new Date(Date.now() + (365 * 24 * 60 * 60 * 1_000));
|
|
2814
|
-
const pending = writePendingOperation(testFixture, {
|
|
2815
|
-
owner: 'corrupt-owner',
|
|
2816
|
-
modifiedAt: futureTime,
|
|
2817
|
-
});
|
|
2818
|
-
|
|
2819
|
-
const result = await checkForUpdate(options(
|
|
2820
|
-
testFixture,
|
|
2821
|
-
async () => response(remoteRelease()),
|
|
2822
|
-
));
|
|
2823
|
-
|
|
2824
|
-
assert.equal(result.status, 'update_available');
|
|
2825
|
-
assert.equal(fs.existsSync(pending), false);
|
|
2826
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
test('a newly-created operation with slight clock skew is never fenced as stale', async (t) => {
|
|
2830
|
-
const testFixture = fixture();
|
|
2831
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2832
|
-
const nearFuture = new Date(Date.now() + 1_000);
|
|
2833
|
-
const pending = writePendingOperation(testFixture, {
|
|
2834
|
-
owner: 'writer-has-not-finished-the-record',
|
|
2835
|
-
modifiedAt: nearFuture,
|
|
2836
|
-
});
|
|
2837
|
-
let requests = 0;
|
|
2838
|
-
|
|
2839
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
2840
|
-
requests += 1;
|
|
2841
|
-
return response(remoteRelease());
|
|
2842
|
-
}));
|
|
2843
|
-
|
|
2844
|
-
assert.deepEqual(result, { status: 'silent', reason: 'check-in-progress' });
|
|
2845
|
-
assert.equal(requests, 0);
|
|
2846
|
-
assert.equal(fs.existsSync(pending), true);
|
|
2847
|
-
});
|
|
2848
|
-
|
|
2849
|
-
test('a fenced stale owner cannot overwrite a newer committed candidate after resuming', async (t) => {
|
|
2850
|
-
const testFixture = fixture();
|
|
2851
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2852
|
-
let resumeOldOwner;
|
|
2853
|
-
let markOldStarted;
|
|
2854
|
-
const oldStarted = new Promise((resolve) => { markOldStarted = resolve; });
|
|
2855
|
-
const oldCheck = checkForUpdate(options(testFixture, async () => {
|
|
2856
|
-
markOldStarted();
|
|
2857
|
-
return new Promise((resolve) => { resumeOldOwner = resolve; });
|
|
2858
|
-
}, { timeoutMs: 2_000 }));
|
|
2859
|
-
await oldStarted;
|
|
2860
|
-
|
|
2861
|
-
const oldPending = operationPath(testFixture, 'pending', 1n);
|
|
2862
|
-
const staleTime = new Date(Date.now() - 60_000);
|
|
2863
|
-
fs.utimesSync(path.join(oldPending, 'owner.json'), staleTime, staleTime);
|
|
2864
|
-
|
|
2865
|
-
const newer = await checkForUpdate(options(
|
|
2866
|
-
testFixture,
|
|
2867
|
-
async () => response(remoteReleaseForVersion('3.0.0', 'c'.repeat(64))),
|
|
2868
|
-
));
|
|
2869
|
-
assert.equal(newer.status, 'update_available');
|
|
2870
|
-
assert.equal(newer.latestVersion, '3.0.0');
|
|
2871
|
-
|
|
2872
|
-
resumeOldOwner(response(remoteReleaseForVersion('2.16.0', 'b'.repeat(64))));
|
|
2873
|
-
assert.deepEqual(await oldCheck, { status: 'silent', reason: 'check-in-progress' });
|
|
2874
|
-
const persisted = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
|
|
2875
|
-
assert.equal(persisted.candidate.version, '3.0.0');
|
|
2876
|
-
assert.equal(fs.existsSync(operationPath(testFixture, 'fenced', 1n)), true);
|
|
2877
|
-
});
|
|
2878
|
-
|
|
2879
|
-
test('different installed versions share no acknowledgement or cache-reset state', async (t) => {
|
|
2880
|
-
const testFixture = fixture('2.15.0');
|
|
2881
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2882
|
-
const olderReleasePath = path.join(testFixture.root, 'older-skill-release.json');
|
|
2883
|
-
writeJson(olderReleasePath, localRelease('2.14.0'));
|
|
2884
|
-
let requests = 0;
|
|
2885
|
-
const fetchImpl = async () => {
|
|
2886
|
-
requests += 1;
|
|
2887
|
-
return response(remoteRelease());
|
|
2888
|
-
};
|
|
2889
|
-
|
|
2890
|
-
const newerInstall = await checkForUpdate(options(testFixture, fetchImpl));
|
|
2891
|
-
assert.equal(newerInstall.status, 'update_available');
|
|
2892
|
-
assert.deepEqual(await acknowledgeUpdate({
|
|
2893
|
-
releasePath: testFixture.releasePath,
|
|
2894
|
-
cacheDirectory: testFixture.cacheDirectory,
|
|
2895
|
-
eventKey: newerInstall.eventKey,
|
|
2896
|
-
now: () => baseTime + 1_000,
|
|
2897
|
-
}), { status: 'acknowledged', eventKey: newerInstall.eventKey });
|
|
2898
|
-
|
|
2899
|
-
const olderInstallOptions = options(testFixture, fetchImpl, { releasePath: olderReleasePath });
|
|
2900
|
-
const olderInstall = await checkForUpdate(olderInstallOptions);
|
|
2901
|
-
assert.equal(olderInstall.status, 'update_available');
|
|
2902
|
-
assert.equal(olderInstall.installedVersion, '2.14.0');
|
|
2903
|
-
|
|
2904
|
-
assert.deepEqual(await checkForUpdate(options(testFixture, fetchImpl)), {
|
|
2905
|
-
status: 'silent',
|
|
2906
|
-
reason: 'already-notified',
|
|
2907
|
-
});
|
|
2908
|
-
const olderAgain = await checkForUpdate(olderInstallOptions);
|
|
2909
|
-
assert.equal(olderAgain.status, 'update_available');
|
|
2910
|
-
assert.equal(requests, 2, 'each installed version should retain its own fresh cache');
|
|
2911
|
-
assert.equal(fs.existsSync(statePath(testFixture, '2.15.0')), true);
|
|
2912
|
-
assert.equal(fs.existsSync(statePath(testFixture, '2.14.0')), true);
|
|
2913
|
-
});
|
|
2914
|
-
|
|
2915
|
-
test('an identity mismatch is rejected silently and never cached as a candidate', async (t) => {
|
|
2916
|
-
const testFixture = fixture();
|
|
2917
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2918
|
-
|
|
2919
|
-
const result = await checkForUpdate(options(
|
|
2920
|
-
testFixture,
|
|
2921
|
-
async () => response(remoteRelease({ skillId: 'different-skill' })),
|
|
2922
|
-
));
|
|
2923
|
-
|
|
2924
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
|
|
2925
|
-
const state = JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8'));
|
|
2926
|
-
assert.equal(Object.hasOwn(state, 'candidate'), false);
|
|
2927
|
-
});
|
|
2928
|
-
|
|
2929
|
-
test('release notes must byte-match the exact trusted GitHub URL', async () => {
|
|
2930
|
-
const base = 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0';
|
|
2931
|
-
for (const releaseNotes of [
|
|
2932
|
-
'https://github.com:443/tt-a1i/archify/releases/tag/v2.16.0',
|
|
2933
|
-
'https://github.com:444/tt-a1i/archify/releases/tag/v2.16.0',
|
|
2934
|
-
`${base}?`,
|
|
2935
|
-
`${base}#`,
|
|
2936
|
-
`${base}?source=manifest`,
|
|
2937
|
-
'https://GITHUB.COM/tt-a1i/archify/releases/tag/v2.16.0',
|
|
2938
|
-
]) {
|
|
2939
|
-
const testFixture = fixture();
|
|
2940
|
-
try {
|
|
2941
|
-
const result = await checkForUpdate(options(
|
|
2942
|
-
testFixture,
|
|
2943
|
-
async () => response(remoteRelease({ releaseNotes })),
|
|
2944
|
-
));
|
|
2945
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' }, releaseNotes);
|
|
2946
|
-
} finally {
|
|
2947
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
});
|
|
2951
|
-
|
|
2952
|
-
test('malformed release notes are classified as an invalid manifest', async (t) => {
|
|
2953
|
-
const testFixture = fixture();
|
|
2954
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
2955
|
-
|
|
2956
|
-
const result = await checkForUpdate(options(
|
|
2957
|
-
testFixture,
|
|
2958
|
-
async () => response(remoteRelease({ releaseNotes: 'not-a-url' })),
|
|
2959
|
-
));
|
|
2960
|
-
|
|
2961
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
|
|
2962
|
-
});
|
|
2963
|
-
|
|
2964
|
-
test('publishedAt must use canonical UTC seconds and a real calendar date', async () => {
|
|
2965
|
-
for (const publishedAt of [
|
|
2966
|
-
'2026-08-28T15:00:00+08:00',
|
|
2967
|
-
'2026-08-28',
|
|
2968
|
-
'2026-02-30T00:00:00Z',
|
|
2969
|
-
]) {
|
|
2970
|
-
const testFixture = fixture();
|
|
2971
|
-
try {
|
|
2972
|
-
const result = await checkForUpdate(options(
|
|
2973
|
-
testFixture,
|
|
2974
|
-
async () => response(remoteRelease({ publishedAt })),
|
|
2975
|
-
));
|
|
2976
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' }, publishedAt);
|
|
2977
|
-
} finally {
|
|
2978
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
});
|
|
2982
|
-
|
|
2983
|
-
test('stable candidates cannot use prerelease or build metadata', async () => {
|
|
2984
|
-
for (const version of ['2.16.0-dev.1', '2.16.0+build.1']) {
|
|
2985
|
-
const testFixture = fixture();
|
|
2986
|
-
try {
|
|
2987
|
-
const result = await checkForUpdate(options(
|
|
2988
|
-
testFixture,
|
|
2989
|
-
async () => response(remoteReleaseForVersion(version)),
|
|
2990
|
-
));
|
|
2991
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' }, version);
|
|
2992
|
-
} finally {
|
|
2993
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
});
|
|
2997
|
-
|
|
2998
|
-
test('an invalid local identity fails before any network disclosure', async (t) => {
|
|
2999
|
-
const testFixture = fixture();
|
|
3000
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3001
|
-
writeJson(testFixture.releasePath, {
|
|
3002
|
-
...localRelease(),
|
|
3003
|
-
source: { repository: 'https://example.com/untrusted/archify' },
|
|
3004
|
-
});
|
|
3005
|
-
let requests = 0;
|
|
3006
|
-
|
|
3007
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
3008
|
-
requests += 1;
|
|
3009
|
-
return response(remoteRelease());
|
|
3010
|
-
}));
|
|
3011
|
-
|
|
3012
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-local-release' });
|
|
3013
|
-
assert.equal(requests, 0);
|
|
3014
|
-
});
|
|
3015
|
-
|
|
3016
|
-
test('a bounded timeout fails silently without retrying', async (t) => {
|
|
3017
|
-
const testFixture = fixture();
|
|
3018
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3019
|
-
let requests = 0;
|
|
3020
|
-
const fetchImpl = (_url, init) => new Promise((_resolve, reject) => {
|
|
3021
|
-
requests += 1;
|
|
3022
|
-
init.signal.addEventListener('abort', () => {
|
|
3023
|
-
const error = new Error('aborted');
|
|
3024
|
-
error.name = 'AbortError';
|
|
3025
|
-
reject(error);
|
|
3026
|
-
}, { once: true });
|
|
3027
|
-
});
|
|
3028
|
-
|
|
3029
|
-
const result = await checkForUpdate(options(testFixture, fetchImpl, { timeoutMs: 10 }));
|
|
3030
|
-
assert.deepEqual(result, { status: 'silent', reason: 'check-failed' });
|
|
3031
|
-
assert.equal(requests, 1);
|
|
3032
|
-
});
|
|
3033
|
-
|
|
3034
|
-
test('non-success responses cancel their unread body before failing silently', async (t) => {
|
|
3035
|
-
const testFixture = fixture();
|
|
3036
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3037
|
-
let cancelled = false;
|
|
3038
|
-
|
|
3039
|
-
const result = await checkForUpdate(options(testFixture, async () => ({
|
|
3040
|
-
status: 503,
|
|
3041
|
-
headers: new Headers(),
|
|
3042
|
-
body: {
|
|
3043
|
-
async cancel() {
|
|
3044
|
-
cancelled = true;
|
|
3045
|
-
},
|
|
3046
|
-
},
|
|
3047
|
-
})));
|
|
3048
|
-
|
|
3049
|
-
assert.deepEqual(result, { status: 'silent', reason: 'check-failed' });
|
|
3050
|
-
assert.equal(cancelled, true);
|
|
3051
|
-
});
|
|
3052
|
-
|
|
3053
|
-
test('media-type and declared-size rejection cancel unread response bodies', async () => {
|
|
3054
|
-
for (const headers of [
|
|
3055
|
-
{ 'content-type': 'text/html' },
|
|
3056
|
-
{ 'content-type': 'application/json', 'content-length': String((32 * 1024) + 1) },
|
|
3057
|
-
]) {
|
|
3058
|
-
const testFixture = fixture();
|
|
3059
|
-
let cancelled = false;
|
|
3060
|
-
try {
|
|
3061
|
-
const result = await checkForUpdate(options(testFixture, async () => ({
|
|
3062
|
-
status: 200,
|
|
3063
|
-
headers: new Headers(headers),
|
|
3064
|
-
body: {
|
|
3065
|
-
async cancel() {
|
|
3066
|
-
cancelled = true;
|
|
3067
|
-
},
|
|
3068
|
-
},
|
|
3069
|
-
})));
|
|
3070
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
|
|
3071
|
-
assert.equal(cancelled, true);
|
|
3072
|
-
} finally {
|
|
3073
|
-
fs.rmSync(testFixture.root, { recursive: true, force: true });
|
|
3074
|
-
}
|
|
3075
|
-
}
|
|
3076
|
-
});
|
|
3077
|
-
|
|
3078
|
-
test('response stream failures are classified as network failures', async (t) => {
|
|
3079
|
-
const testFixture = fixture();
|
|
3080
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3081
|
-
|
|
3082
|
-
const result = await checkForUpdate(options(testFixture, async () => ({
|
|
3083
|
-
status: 200,
|
|
3084
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
3085
|
-
body: {
|
|
3086
|
-
getReader() {
|
|
3087
|
-
return {
|
|
3088
|
-
async read() {
|
|
3089
|
-
throw new TypeError('socket closed while reading');
|
|
3090
|
-
},
|
|
3091
|
-
releaseLock() {},
|
|
3092
|
-
};
|
|
3093
|
-
},
|
|
3094
|
-
},
|
|
3095
|
-
})));
|
|
3096
|
-
|
|
3097
|
-
assert.deepEqual(result, { status: 'silent', reason: 'check-failed' });
|
|
3098
|
-
});
|
|
3099
|
-
|
|
3100
|
-
test('invalid UTF-8 is classified as an invalid manifest rather than a network failure', async (t) => {
|
|
3101
|
-
const testFixture = fixture();
|
|
3102
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3103
|
-
const result = await checkForUpdate(options(
|
|
3104
|
-
testFixture,
|
|
3105
|
-
async () => new Response(new Uint8Array([0xff]), {
|
|
3106
|
-
status: 200,
|
|
3107
|
-
headers: { 'content-type': 'application/json' },
|
|
3108
|
-
}),
|
|
3109
|
-
));
|
|
3110
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
|
|
3111
|
-
});
|
|
3112
|
-
|
|
3113
|
-
test('chunked responses above 32 KiB are rejected instead of fully trusted', async (t) => {
|
|
3114
|
-
const testFixture = fixture();
|
|
3115
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3116
|
-
const oversized = `${JSON.stringify(remoteRelease())}${' '.repeat(33 * 1024)}`;
|
|
3117
|
-
|
|
3118
|
-
const result = await checkForUpdate(options(
|
|
3119
|
-
testFixture,
|
|
3120
|
-
async () => response(oversized),
|
|
3121
|
-
));
|
|
3122
|
-
|
|
3123
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
|
|
3124
|
-
});
|
|
3125
|
-
|
|
3126
|
-
test('a poisoned far-future cache timestamp cannot suppress checks indefinitely', async (t) => {
|
|
3127
|
-
const testFixture = fixture();
|
|
3128
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3129
|
-
await checkForUpdate(options(testFixture, async () => response(remoteRelease())));
|
|
3130
|
-
const cacheStatePath = statePath(testFixture);
|
|
3131
|
-
const state = JSON.parse(fs.readFileSync(cacheStatePath, 'utf8'));
|
|
3132
|
-
state.check.nextCheckAt = '9999-12-31T23:59:59.000Z';
|
|
3133
|
-
writeJson(cacheStatePath, state);
|
|
3134
|
-
let requests = 0;
|
|
3135
|
-
|
|
3136
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
3137
|
-
requests += 1;
|
|
3138
|
-
return response(remoteRelease());
|
|
3139
|
-
}));
|
|
3140
|
-
|
|
3141
|
-
assert.equal(result.status, 'update_available');
|
|
3142
|
-
assert.equal(requests, 1);
|
|
3143
|
-
});
|
|
3144
|
-
|
|
3145
|
-
test('a semantically corrupt cached candidate discards legacy validators and rebuilds unconditionally', async (t) => {
|
|
3146
|
-
const testFixture = fixture();
|
|
3147
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3148
|
-
await checkForUpdate(options(testFixture, async () => response(remoteRelease())));
|
|
3149
|
-
const cacheStatePath = statePath(testFixture);
|
|
3150
|
-
const state = JSON.parse(fs.readFileSync(cacheStatePath, 'utf8'));
|
|
3151
|
-
state.candidate.targetDigest = 'sha256:corrupt';
|
|
3152
|
-
state.check.etag = '"legacy-tracker"';
|
|
3153
|
-
state.check.nextCheckAt = '9999-12-31T23:59:59.000Z';
|
|
3154
|
-
writeJson(cacheStatePath, state);
|
|
3155
|
-
let conditionalHeader = 'not-observed';
|
|
3156
|
-
|
|
3157
|
-
const result = await checkForUpdate(options(testFixture, async (_url, init) => {
|
|
3158
|
-
conditionalHeader = init.headers['if-none-match'];
|
|
3159
|
-
return response(remoteRelease());
|
|
3160
|
-
}));
|
|
3161
|
-
|
|
3162
|
-
assert.equal(result.status, 'update_available');
|
|
3163
|
-
assert.equal(conditionalHeader, undefined);
|
|
3164
|
-
assert.equal(
|
|
3165
|
-
JSON.parse(fs.readFileSync(statePath(testFixture), 'utf8')).candidate.targetDigest,
|
|
3166
|
-
result.targetDigest,
|
|
3167
|
-
);
|
|
3168
|
-
});
|
|
3169
|
-
|
|
3170
|
-
test('a newer cached candidate without offered or acknowledged provenance is rebuilt', async (t) => {
|
|
3171
|
-
const testFixture = fixture();
|
|
3172
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3173
|
-
writeJson(statePath(testFixture), {
|
|
3174
|
-
schemaVersion: 1,
|
|
3175
|
-
skillId: 'archify',
|
|
3176
|
-
installedVersion: '2.15.0',
|
|
3177
|
-
check: {
|
|
3178
|
-
nextCheckAt: new Date(baseTime + (24 * 60 * 60 * 1_000)).toISOString(),
|
|
3179
|
-
consecutiveFailures: 0,
|
|
3180
|
-
},
|
|
3181
|
-
notification: {
|
|
3182
|
-
offeredDigests: [],
|
|
3183
|
-
acknowledgedDigests: [],
|
|
3184
|
-
},
|
|
3185
|
-
candidate: {
|
|
3186
|
-
version: '2.16.0',
|
|
3187
|
-
targetDigest: `sha256:${'b'.repeat(64)}`,
|
|
3188
|
-
severity: 'normal',
|
|
3189
|
-
releaseNotes: 'https://github.com/tt-a1i/archify/releases/tag/v2.16.0',
|
|
3190
|
-
},
|
|
3191
|
-
});
|
|
3192
|
-
let requests = 0;
|
|
3193
|
-
|
|
3194
|
-
const result = await checkForUpdate(options(testFixture, async () => {
|
|
3195
|
-
requests += 1;
|
|
3196
|
-
return response(remoteReleaseForVersion('2.15.0', 'd'.repeat(64)));
|
|
3197
|
-
}));
|
|
3198
|
-
|
|
3199
|
-
assert.deepEqual(result, { status: 'silent', reason: 'current' });
|
|
3200
|
-
assert.equal(requests, 1, 'semantic corruption must not produce an unacknowledgeable cached notice');
|
|
3201
|
-
});
|
|
3202
|
-
|
|
3203
|
-
test('corrupt cache is rebuilt without exposing an error to the user', async (t) => {
|
|
3204
|
-
const testFixture = fixture();
|
|
3205
|
-
t.after(() => fs.rmSync(testFixture.root, { recursive: true, force: true }));
|
|
3206
|
-
fs.mkdirSync(stateDirectory(testFixture), { recursive: true });
|
|
3207
|
-
fs.mkdirSync(path.dirname(statePath(testFixture)), { recursive: true });
|
|
3208
|
-
fs.writeFileSync(statePath(testFixture), '{not-json');
|
|
3209
|
-
|
|
3210
|
-
const result = await checkForUpdate(options(
|
|
3211
|
-
testFixture,
|
|
3212
|
-
async () => response(remoteRelease({ version: '2.15.0' })),
|
|
3213
|
-
));
|
|
3214
|
-
|
|
3215
|
-
assert.deepEqual(result, { status: 'silent', reason: 'invalid-manifest' });
|
|
3216
|
-
assert.doesNotThrow(() => JSON.parse(
|
|
3217
|
-
fs.readFileSync(statePath(testFixture), 'utf8'),
|
|
3218
|
-
));
|
|
3219
|
-
});
|
|
3220
|
-
|
|
3221
|
-
test('disabled CLI returns one silent JSON line and never needs the network', () => {
|
|
3222
|
-
const result = spawnSync(process.execPath, [checkerPath], {
|
|
3223
|
-
cwd: skillRoot,
|
|
3224
|
-
encoding: 'utf8',
|
|
3225
|
-
env: { ...process.env, ARCHIFY_UPDATE_CHECK_DISABLED: '1' },
|
|
3226
|
-
});
|
|
3227
|
-
|
|
3228
|
-
assert.equal(result.status, 0, result.stderr);
|
|
3229
|
-
assert.deepEqual(JSON.parse(result.stdout), { status: 'silent', reason: 'disabled' });
|
|
3230
|
-
assert.equal(result.stdout.trim().split('\n').length, 1);
|
|
3231
|
-
});
|
|
3232
|
-
|
|
3233
|
-
test('CLI acknowledgement emits the documented one-line success schema', async (t) => {
|
|
3234
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-update-cli-ack-'));
|
|
3235
|
-
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
3236
|
-
const home = path.join(root, 'home');
|
|
3237
|
-
const xdg = path.join(root, 'xdg-cache');
|
|
3238
|
-
const localData = path.join(root, 'local-data');
|
|
3239
|
-
for (const directory of [home, xdg, localData]) fs.mkdirSync(directory, { recursive: true });
|
|
3240
|
-
const cacheDirectory = process.platform === 'win32'
|
|
3241
|
-
? path.join(localData, 'archify-skill')
|
|
3242
|
-
: process.platform === 'darwin'
|
|
3243
|
-
? path.join(home, 'Library', 'Caches', 'archify-skill')
|
|
3244
|
-
: path.join(xdg, 'archify-skill');
|
|
3245
|
-
const releasePath = path.join(skillRoot, 'skill-release.json');
|
|
3246
|
-
const installedRelease = JSON.parse(fs.readFileSync(releasePath, 'utf8'));
|
|
3247
|
-
const [major, minor, patch] = parseSemver(installedRelease.version).core;
|
|
3248
|
-
const candidateVersion = `${major}.${minor}.${BigInt(patch) + 1n}`;
|
|
3249
|
-
const offered = await checkForUpdate({
|
|
3250
|
-
releasePath,
|
|
3251
|
-
cacheDirectory,
|
|
3252
|
-
fetchImpl: async () => response(remoteReleaseForVersion(candidateVersion)),
|
|
3253
|
-
now: () => baseTime,
|
|
3254
|
-
random: () => 0.5,
|
|
3255
|
-
timeoutMs: 50,
|
|
3256
|
-
});
|
|
3257
|
-
assert.equal(offered.status, 'update_available');
|
|
3258
|
-
|
|
3259
|
-
const acknowledgement = spawnSync(process.execPath, [checkerPath, '--ack', offered.eventKey], {
|
|
3260
|
-
cwd: skillRoot,
|
|
3261
|
-
encoding: 'utf8',
|
|
3262
|
-
env: {
|
|
3263
|
-
...process.env,
|
|
3264
|
-
HOME: home,
|
|
3265
|
-
USERPROFILE: home,
|
|
3266
|
-
XDG_CACHE_HOME: xdg,
|
|
3267
|
-
LOCALAPPDATA: localData,
|
|
3268
|
-
},
|
|
3269
|
-
});
|
|
3270
|
-
assert.equal(acknowledgement.status, 0, acknowledgement.stderr);
|
|
3271
|
-
assert.deepEqual(JSON.parse(acknowledgement.stdout), {
|
|
3272
|
-
status: 'acknowledged',
|
|
3273
|
-
eventKey: offered.eventKey,
|
|
3274
|
-
});
|
|
3275
|
-
assert.equal(acknowledgement.stdout.trim().split('\n').length, 1);
|
|
3276
|
-
});
|
|
3277
|
-
|
|
3278
|
-
test('CLI entry detection survives a realpath or symlink alias', (t) => {
|
|
3279
|
-
const aliasRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-update-cli-alias-'));
|
|
3280
|
-
t.after(() => fs.rmSync(aliasRoot, { recursive: true, force: true }));
|
|
3281
|
-
const aliasPath = path.join(aliasRoot, 'check-update-alias.mjs');
|
|
3282
|
-
try {
|
|
3283
|
-
fs.symlinkSync(checkerPath, aliasPath);
|
|
3284
|
-
} catch (error) {
|
|
3285
|
-
if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error?.code)) {
|
|
3286
|
-
t.skip(`symlinks unavailable: ${error.code}`);
|
|
3287
|
-
return;
|
|
3288
|
-
}
|
|
3289
|
-
throw error;
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
const result = spawnSync(process.execPath, [aliasPath], {
|
|
3293
|
-
cwd: aliasRoot,
|
|
3294
|
-
encoding: 'utf8',
|
|
3295
|
-
env: { ...process.env, ARCHIFY_UPDATE_CHECK_DISABLED: '1' },
|
|
3296
|
-
});
|
|
3297
|
-
assert.equal(result.status, 0, result.stderr);
|
|
3298
|
-
assert.deepEqual(JSON.parse(result.stdout), { status: 'silent', reason: 'disabled' });
|
|
3299
|
-
});
|
|
3300
|
-
|
|
3301
|
-
test('notifier source has no process execution or remote-origin override surface', () => {
|
|
3302
|
-
const checkerSource = fs.readFileSync(checkerPath, 'utf8');
|
|
3303
|
-
const contractSource = fs.readFileSync(contractPath, 'utf8');
|
|
3304
|
-
const combinedSource = `${checkerSource}\n${contractSource}`;
|
|
3305
|
-
assert.doesNotMatch(combinedSource, /(?:node:)?child_process/);
|
|
3306
|
-
assert.doesNotMatch(combinedSource, /(?:^|[^\w.])(?:spawn|exec|execFile|fork)(?:Sync)?\s*\(/m);
|
|
3307
|
-
assert.doesNotMatch(contractSource, /\bfetch\s*\(/);
|
|
3308
|
-
const environmentReads = [...combinedSource.matchAll(/process\.env\.([A-Z0-9_]+)/g)]
|
|
3309
|
-
.map((match) => match[1])
|
|
3310
|
-
.sort();
|
|
3311
|
-
assert.deepEqual(environmentReads, [
|
|
3312
|
-
'ARCHIFY_UPDATE_CHECK_DISABLED',
|
|
3313
|
-
'LOCALAPPDATA',
|
|
3314
|
-
'XDG_CACHE_HOME',
|
|
3315
|
-
]);
|
|
3316
|
-
assert.doesNotMatch(combinedSource, /updateCommand/);
|
|
3317
|
-
});
|