@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.
Files changed (717) hide show
  1. package/.claude/hooks/xgent-statusline.js +32 -18
  2. package/README.md +61 -35
  3. package/bin/phoenix-diagram.js +14 -0
  4. package/bin/xgent-skills.js +132 -240
  5. package/dist/phoenix-diagram/LICENSE +21 -0
  6. package/dist/phoenix-diagram/NOTICE.md +34 -0
  7. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/005fc765ddc5115da796cca915baa9557abae13ff35e0a47c47affc56f6c414d.txt +272 -0
  8. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/010492e490b92ec85db01daad2a290ea5959f29314a1c4ff25e7a172e223abf9.txt +18 -0
  9. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f.txt +3 -0
  10. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0218327e7a480793ffdd4eb792379a9709e5c135c7ba267f709d6f6d4d70af0a.txt +201 -0
  11. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/02bb8c3b4e70190e3986c0404ad2fd8d639b4f534252d82379cc1b502b6d1812.txt +191 -0
  12. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0444c6991eead6822f7b9102e654448d51624431119546492e8b231db42c48bb.txt +25 -0
  13. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0552c898248eadfc130df0c0e8f6f82776fcb3f1dd5a5af2fa991b5baff9367a.txt +21 -0
  14. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0ab4d106b6faac07fb6a051815fd1b4d862d730895e2d7d7358c2f13565e7a38.txt +20 -0
  15. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0b28172679e0009b655da42797c03fd163a3379d5cfa67ba1f1655e974a2a1a9.txt +25 -0
  16. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0d3f7c086c6b6cf3ea4aac714e9aa3e1fb02e355ce35bd21c56acf8d04390885.txt +21 -0
  17. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0dd882e53de11566d50f8e8e2d5a651bcf3fabee4987d70f306233cf39094ba7.txt +21 -0
  18. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/0f96a83840e146e43c0ec96a22ec1f392e0680e6c1226e6f3ba87e0740af850f.txt +21 -0
  19. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/117829c3ca21efb132d81a44b55363d395ab8eea18526873bc828da4c0e5f038.txt +21 -0
  20. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/123a331b5dbf04c30097fa43b8f858bc85df671fe776de498d01f3d6b7c1f69e.txt +25 -0
  21. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/1a2f5c12ddc934d58956aa5dbdd3255fe55fd957633ab7d0d39e4f0daa73f7df.txt +26 -0
  22. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5.txt +373 -0
  23. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/1f485f0d6125658861351e3d6084ea90fefe78f89450be9915044b7c56d5a75a.txt +15 -0
  24. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/209fbbe0ad52d9235e37badf9cadfe4dbdc87203179c0899e738b39ade42177b.txt +26 -0
  25. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/20c7855c364d57ea4c97889a5e8d98470a9952dade37bd9248b9a54431670e5e.txt +25 -0
  26. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/20fe7b00e904ed690e3b9fd6073784d3fc428141dbd10b81c01fd143d0797f58.txt +176 -0
  27. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/219920e865eee70b7dcfc948a86b099e7f4fe2de01bcca2ca9a20c0a033f2b59.txt +7 -0
  28. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/23860c2a7b5d96b21569afedf033469bab9fe14a1b24a35068b8641c578ce24d.txt +7 -0
  29. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3.txt +23 -0
  30. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/268872b9816f90fd8e85db5a28d33f8150ebb8dd016653fb39ef1f94f2686bc5.txt +220 -0
  31. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/27995d58ad5c1145c1a8cd86244ce844886958a35eb2b78c6b772748669999ac.txt +25 -0
  32. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5.txt +428 -0
  33. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/29e9fe5074bd27e0e5d5d110394fbbcd841baee2651a3c4b4560a632702cede4.txt +26 -0
  34. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/30fefc3a7d6a0041541858293bcbea2dde4caa4c0a5802f996a7f7e8c0085652.txt +23 -0
  35. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3521672491a3479422d5fe1aca6645dd2984090f85da6e5205abfb18fb7a6897.txt +25 -0
  36. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/35242e7a83f69875e6edeff02291e688c97caafe2f8902e4e19b49d3e78b4cab.txt +176 -0
  37. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/36516aefdc84c5d5a1e7485425913a22dbda69eb1930c5e84d6ae4972b5194b9.txt +23 -0
  38. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/375bdbf54d6584debb4bcc5bce1c9dcfdd581f977c9a8469113dbdf208f4c927.txt +131 -0
  39. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397.txt +25 -0
  40. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/391a5396cec6230bfabd4ef4eb2350eb895bc5efce377a2218f5702ed020d3e3.txt +20 -0
  41. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/39712155836e5c10ef1b07037b606e4ad2d633e72205d162fdd7e2290aa54f15.txt +21 -0
  42. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3bb1e548250b3acc43febde65a5cdba9f2e0a3288018aa41f009a3c59ffab51a.txt +390 -0
  43. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3bc70e239e91272782006c638fc0452a714d384224f11f0923036b7be07cf9b5.txt +21 -0
  44. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04.txt +373 -0
  45. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3f941b3b89cf7b8370ceb83cc76d2120d471b58735d8ca60238a751a48d7f72f.txt +359 -0
  46. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/3fa4ca83dcc9237839b1bdeb2e6d16bdfb5ec0c5ce42b24694d8bbf0dcbef72c.txt +25 -0
  47. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/42fa16951ce7f24b5a467a40e5b449a1d41e662f97ca779864f053f39e097737.txt +26 -0
  48. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/4455bf75a91154108304cb283e0fea9948c14f13e20d60887cf2552449dea3b1.txt +21 -0
  49. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/451167c55c0fa447cc2d5632714f5e3c567fe4f1e1badefab2c1333852198aca.txt +542 -0
  50. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/45f522cacecb1023856e46df79ca625dfc550c94910078bd8aec6e02880b3d42.txt +25 -0
  51. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/45fd05c4865e7c350b98ad7ac50e1b15462d49af4a91e9b0c9dd933dc9a69742.txt +201 -0
  52. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/4789210f4df8abc03fce1cddd9c2ba517f184840687f2815d741de282c1aa93a.txt +228 -0
  53. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/4cada0bd02ea3692eee6f16400d86c6508bbd3bafb2b65fed0419f36d4f83e8f.txt +25 -0
  54. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/516b24e051bf5630880ebbd55c40a25ce9552ebaf8970a53e8976eb70e522406.txt +201 -0
  55. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/553fffcd9b1cb158bc3e9edc35da85ca5c3b3d7d2e61c883ebcfa8a65814b583.txt +13 -0
  56. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/5757a1f28eff735a8b5e7425478f367812d71e974530f5bedd01219480965f4a.txt +51 -0
  57. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/5b698ca13897be3afdb7174256fa1574f8c6892b8bea1a66dd6469d3fe27885a.txt +19 -0
  58. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/5e05b024f653a5ce199e77cbbbd42fb5553562ec714b819421ed0c3e552a75d7.txt +25 -0
  59. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/60a7062291b01ba068f300612cdbdc20382ac1d4934f07bcdd7167c15299f309.txt +25 -0
  60. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/62065228e42caebca7e7d7db1204cbb867033de5982ca4009928915e4095f3a3.txt +25 -0
  61. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a.txt +176 -0
  62. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb.txt +25 -0
  63. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/65f94e99ddaf4f5d1782a6dae23f35d4293a9a01444a13135a6887017d353cee.txt +25 -0
  64. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/6df43f6f4b5d4587f3d8d71e45532c688fd168afa5fe89d571cb32fa09c4ef51.txt +187 -0
  65. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/709e3175b4212f7b13aa93971c9f62ff8c69ec45ad8c6532a7e0c41d7a7d6f8c.txt +25 -0
  66. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d.txt +21 -0
  67. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7365cc8878a1d7ce155a58c4ca09c3d7a6be413efa5334a80ea842912b669349.txt +25 -0
  68. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7abd9b6960dcf7d4d0a48606a5b71bfe37d472db68d70637f3a58a56785f1621.txt +13 -0
  69. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7b63ecd5f1902af1b63729947373683c32745c16a10e8e6292e2e2dcd7e90ae0.txt +25 -0
  70. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7ca6700600dfa9c9497bf5556365067daa802c871ea78239f129309c7a2048f7.txt +21 -0
  71. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7cfafc877eccc46c0e346ccbaa5c51bb6b894d2b818e617d970211e232785ad4.txt +15 -0
  72. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c.txt +24 -0
  73. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7e5886959f67f8c75063a9d55cd3cb08c5a8cbea9a944fa1df2d4ae038f53721.txt +21 -0
  74. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/7fea0ee51a4ca5d5cea7464135fd55e8b09caf3a61da3d451ac8a22af95c033f.txt +20 -0
  75. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90.txt +201 -0
  76. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/83c1763356e822adde0a2cae748d938a73fdc263849ccff6b27776dff213bd32.txt +24 -0
  77. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/86dd7f026f916daf7511e39951ad8ea8cf55a8db67ae64060dacf829761c18f3.txt +25 -0
  78. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8a35369f3ca263b3c62fbb5032947e53b6bfebc6c8a4d1bb982de1c069f6fba5.txt +21 -0
  79. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8b43ce8accd61e9d370b5ca9e9c4f953279b5c239926c62315b40e24df51b726.txt +25 -0
  80. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8bb1b50b0e5c9399ae33bd35fab2769010fa6c14e8860c729a52295d84896b7a.txt +201 -0
  81. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8bce3b45e49ecd1461f223b46de133d8f62cd39f745cfdaf81bee554b908bd42.txt +21 -0
  82. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/8c7516d4b27b1e495be5e38b612298b63de48d05f49cdac94f70f3cd70f8864b.txt +25 -0
  83. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9046848b63a5c92bff14e4accca80bd987e0623b74adf9226ce5198d312b79d5.txt +30 -0
  84. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/90eb64f0279b0d9432accfa6023ff803bc4965212383697eee27a0f426d5f8d5.txt +12 -0
  85. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9117d922e667125508dde62b02c1f57ed22f5ad21eb536aa2e2d99e1c796e639.txt +25 -0
  86. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9387c2d197e2370a740ca457dae1662c56de56b811ee4862024343961307f9bf.txt +19 -0
  87. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/946c9835d8034d24404f8cfec5f4654cee5dad17e944afc3d06d742cf2882831.txt +240 -0
  88. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/95bd3988beee069fa2848f648dab43cc6e0b2add2ad6bcb17360caf749802bcc.txt +176 -0
  89. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9ba9550ad48438d0836ddab3da480b3b69ffa0aac7b7878b5a0039e7ab429411.txt +396 -0
  90. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9d185ac6703c4b0453974c0d85e9eee43e6941009296bb1f5eb0b54e2329e9f3.txt +202 -0
  91. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9e0dfd2dd4173a530e238cb6adb37aa78c34c6bc7444e0e10c1ab5d8881f63ba.txt +25 -0
  92. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9f2ae19ef94ba82887729a4a1617ff25eede56611dd21a4b2c6f797df30fca33.txt +2013 -0
  93. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/9fa38f610a00115dd1f612bdd3559e3b381ea579538b6ff25a5899da250a5fce.txt +21 -0
  94. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a00511478c83ebb464ef19d840f061c7444adc4636bb561900e75c5808cfc6cc.txt +21 -0
  95. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499.txt +121 -0
  96. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a5dea80c1f383cb5f80a6bb0da5e55a2beb9f24adb123ce6300af2cbaaa3bf65.txt +19 -0
  97. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2.txt +201 -0
  98. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a825bd853ab71619a4923d7b4311221427848070ff44d990da39b0b274c1683f.txt +21 -0
  99. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/a9040321c3712d8fd0b09cf52b17445de04a23a10165049ae187cd39e5c86be5.txt +201 -0
  100. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/aaff376532ea30a0cd5330b9502ad4a4c8bf769c539c87ffe78819d188a18ebf.txt +201 -0
  101. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ae9baa7beea910273c2f384c2a6b721fb7bd02bda3436074a1072e4ee689f985.txt +25 -0
  102. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b1181a40b2a7b25cf66fd01481713bc1005df082c53ef73e851e55071b102744.txt +19 -0
  103. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b38f11f6096706e6de553dabe2a7ed142d59b6fa8c97e290c67496154745cdd5.txt +25 -0
  104. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b3d734001a94efff3579978d953391aa7115f877657d25eb54037a43875d078a.txt +9 -0
  105. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b40930bbcf80744c86c46a12bc9da056641d722716c378f5659b9e555ef833e1.txt +201 -0
  106. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b4eb00df6e2a4d22518fcaa6a2b4646f249b3a3c9814509b22bd2091f1392ff1.txt +27 -0
  107. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b68ad1a3367b825447089e1f8d6829b97f47a89eb78d2f4ebaef4672f5606186.txt +22 -0
  108. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/b7e650f3fce5c53249d1cdc608b54df156a97edd636cf9d23498d0cfe7aec63e.txt +19 -0
  109. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c09aae9d3c77b531f56351a9947bc7446511d6b025b3255312d3e3442a9a7583.txt +21 -0
  110. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c16f8dcf1a368b83be78d826ea23de4079fe1b4469a0ab9ee20563f37ff3d44b.txt +201 -0
  111. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383.txt +21 -0
  112. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c30152c94a6d75e021adbc52b3a52470366a46edb917e17deae3259251af244c.txt +42 -0
  113. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c394e3bda1f3b9378f30a7cab11242887eeeafc73c7bef36a0fe13e09691e18b.txt +17 -0
  114. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4.txt +201 -0
  115. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c962ee4d1d05ddc138b202b2540219ebc57893fcf97b364852094a9a94ce1365.txt +7 -0
  116. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/c9a75f18b9ab2927829a208fc6aa2cf4e63b8420887ba29cdb265d6619ae82d5.txt +25 -0
  117. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30.txt +202 -0
  118. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/d1fc1bc0d155df60b2e7705b6b2ae02a05c96f948e1cec6e2fb86360b09f346b.txt +29 -0
  119. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/d5c22aa3118d240e877ad41c5d9fa232f9c77d757d4aac0c2f943afc0a95e0ef.txt +25 -0
  120. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/d5fca2653c88ad02d197faead77e22b80c70636b5bec6e02f2805f389a21b86d.txt +10 -0
  121. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/db11fec9946737df39ca3898d9cd8c10ec6f6c3a884a6802b0ad0b81b4e8f23a.txt +1 -0
  122. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/dc91f8200e4b2a1f9261035d4c18c33c246911a6c0f7b543d75347e61b249cff.txt +25 -0
  123. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ddea4e94a1c35ed9826e54e13a5b50b8f81b21665a00ca07c8861f1859e56b6b.txt +14 -0
  124. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/e271993808fec50ab29350b39539cdec611a9103f827e0aa26d61da70e2d33f8.txt +61 -0
  125. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/e66c269d4819aaab34b49ef5220c4ddab6756f21bb5180761a4eb8561f2b7bbd.txt +438 -0
  126. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ecc269ef87fd38a1d98e30bfac9ba964a9dbd9315c3770fed98d4d7cb5882055.txt +25 -0
  127. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/efcfee7981ff72431fffb06925cad00a23dce079ed4354f61030ad5abdb78829.txt +22 -0
  128. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f025ccfb7dfb6bdfedc75ca0f67acc69e6fb4998143d834f7c2f38a29989680f.txt +13 -0
  129. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f367c1b8e1aa262435251e442901da4607b4650e0e63a026f5044473ecfb90f2.txt +46 -0
  130. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f51ac2c59a222f7476ce507ca879960e2b64ea64bb2786eefdbeb7b0b538d1b7.txt +25 -0
  131. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f7715d38a3fa1b4ac97c5729740752505a39cb92ee83ab5b102aeb5eaa7cdea4.txt +25 -0
  132. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f7db81051789b729fea528a63ec4c938fdcb93d9d61d97dc8cc2e9df6d47f2a1.txt +39 -0
  133. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/f9c4c77baa3828004ee54b8a4f2db2e88ed44a6237a493965bf551fac0fcb62d.txt +17 -0
  134. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85.txt +373 -0
  135. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/fb77f0a9c53e473abe5103c8632ef9f0f2874d4fb3f17cb2d8c661aab9cee9d7.txt +25 -0
  136. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ff8f68cb076caf8cefe7a6430d4ac086ce6af2ca8ce2c4e5a2004d4552ef52a2.txt +25 -0
  137. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/ff92bed461f50338dd703a9ba9aee496a425957873df1d91192776e4bdf5dda7.txt +74 -0
  138. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/COPYRIGHT-library.html +29357 -0
  139. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/Apache-2.0.txt +73 -0
  140. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/BSD-2-Clause.txt +9 -0
  141. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/CC-BY-SA-4.0.txt +427 -0
  142. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/GCC-exception-3.1.txt +30 -0
  143. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/GPL-2.0-only.txt +133 -0
  144. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/GPL-3.0-or-later.txt +202 -0
  145. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/ISC.txt +7 -0
  146. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/LLVM-exception.txt +15 -0
  147. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/MIT.txt +9 -0
  148. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/NCSA.txt +29 -0
  149. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/OFL-1.1.txt +43 -0
  150. package/dist/phoenix-diagram/THIRD-PARTY-LICENSES/rust-std-1.99.1/licenses/Unicode-3.0.txt +39 -0
  151. package/dist/phoenix-diagram/THIRD-PARTY-NOTICES.md +210 -0
  152. package/dist/phoenix-diagram/THIRD-PARTY.json +5459 -0
  153. package/dist/phoenix-diagram/VERSION.json +39 -0
  154. package/dist/phoenix-diagram/bundle/ENGINE.json +39 -0
  155. package/dist/phoenix-diagram/bundle/SKILL.md +109 -0
  156. package/dist/phoenix-diagram/bundle/examples/minimal.architecture.json +11 -0
  157. package/dist/phoenix-diagram/bundle/examples/minimal.class.json +23 -0
  158. package/dist/phoenix-diagram/bundle/examples/minimal.dataflow.json +11 -0
  159. package/dist/phoenix-diagram/bundle/examples/minimal.erd.json +25 -0
  160. package/dist/phoenix-diagram/bundle/examples/minimal.lifecycle.json +11 -0
  161. package/dist/phoenix-diagram/bundle/examples/minimal.sequence.json +13 -0
  162. package/dist/phoenix-diagram/bundle/examples/minimal.timeline.json +15 -0
  163. package/dist/phoenix-diagram/bundle/examples/minimal.tree.json +13 -0
  164. package/dist/phoenix-diagram/bundle/examples/minimal.waterfall.json +12 -0
  165. package/dist/phoenix-diagram/bundle/examples/minimal.workflow.json +12 -0
  166. package/dist/phoenix-diagram/bundle/manifest.json +253 -0
  167. package/dist/phoenix-diagram/bundle/references/architecture-compare.md +111 -0
  168. package/dist/phoenix-diagram/bundle/references/authoring-defaults.md +42 -0
  169. package/dist/phoenix-diagram/bundle/references/brand-marks.md +52 -0
  170. package/dist/phoenix-diagram/bundle/references/compatibility.md +22 -0
  171. package/dist/phoenix-diagram/bundle/references/delivery-contract.md +127 -0
  172. package/dist/phoenix-diagram/bundle/references/extending-types.md +14 -0
  173. package/dist/phoenix-diagram/bundle/references/migration.md +57 -0
  174. package/dist/phoenix-diagram/bundle/references/preview.md +45 -0
  175. package/dist/phoenix-diagram/bundle/references/repository-authoring.md +29 -0
  176. package/dist/phoenix-diagram/bundle/references/viewer-runtime.md +27 -0
  177. package/dist/phoenix-diagram/bundle/schemas/README.md +23 -0
  178. package/dist/phoenix-diagram/bundle/schemas/architecture.schema.json +329 -0
  179. package/dist/phoenix-diagram/bundle/schemas/class.schema.json +348 -0
  180. package/dist/phoenix-diagram/bundle/schemas/common.schema.json +358 -0
  181. package/{skills/archify → dist/phoenix-diagram/bundle}/schemas/dataflow.schema.json +36 -8
  182. package/dist/phoenix-diagram/bundle/schemas/erd.schema.json +384 -0
  183. package/dist/phoenix-diagram/bundle/schemas/lifecycle.schema.json +202 -0
  184. package/{skills/archify → dist/phoenix-diagram/bundle}/schemas/sequence.schema.json +41 -10
  185. package/dist/phoenix-diagram/bundle/schemas/timeline.schema.json +170 -0
  186. package/dist/phoenix-diagram/bundle/schemas/tree.schema.json +144 -0
  187. package/dist/phoenix-diagram/bundle/schemas/waterfall.schema.json +150 -0
  188. package/{skills/archify → dist/phoenix-diagram/bundle}/schemas/workflow.schema.json +42 -10
  189. package/dist/phoenix-diagram/bundle/scripts/phoenix-bootstrap.cjs +589 -0
  190. package/dist/phoenix-diagram/bundle/scripts/phoenix-diagram +6 -0
  191. package/dist/phoenix-diagram/bundle/scripts/phoenix-launcher.cjs +10 -0
  192. package/dist/phoenix-ui/THIRD-PARTY.json +164 -158
  193. package/dist/phoenix-ui/VERSION.json +17 -17
  194. package/dist/phoenix-ui/bundle/blobs/00727f7b333a7be8ac7740aa257a3a3f691db8ae545a6afab2b787acab5e88df +46 -0
  195. package/dist/phoenix-ui/bundle/blobs/{547e3c89d5b8feeddd67e43131fd84b95253eb42bf7d730b4e471dba011f6e51 → 00f4a6a630f31eb05fbdd5725e4a294233d24574bd70f4f32f4597d8256a7c1c} +0 -4
  196. package/dist/phoenix-ui/bundle/blobs/{41400e182e2180a567923d58bfa935d2799a54f2a747b2c5813c3884c84f768b → 04057592e18ea9dd3c4edcac14cde3fa5b75239c2552d4459db8e047068f61a8} +6 -7
  197. package/dist/phoenix-ui/bundle/blobs/{34571f7d46632205d518d15cb008457494dfad745c6b483bb11d63ca3a22ac48 → 0cb3bf26e1980b0e530373fb32edbe5864a171492a04e2abe1952ffe78bff231} +6 -7
  198. package/dist/phoenix-ui/bundle/blobs/{852d2943678a76c33b2f7a090d2ca6c152cd120e9ae62775fcc91519b9289011 → 0e684572eb98d24b39cd70fa23e66c163178f5a11756dcf8a98fe2335636b33f} +6 -3
  199. package/dist/phoenix-ui/bundle/blobs/{d17b3a454146e74ef4ecf69d7b72a8fa45c4fe1c695dffb0d36690b653cd2d8f → 10553c45c7bf38e527a57a281c4365152a768b4dd7ef597ff34ae841e9ff7d41} +6 -7
  200. package/dist/phoenix-ui/bundle/blobs/{e49257c0fd32b195cccb16fbfda769c007be6b38f25aceefecd23cfff9366cec → 125bc7030d7f2ba789d7b1d5f474af34f8902b764e7bc351b98f1f6f55779929} +6 -3
  201. package/dist/phoenix-ui/bundle/blobs/{74dbcbbad6c3831e95abb38775dd488c59ae9f1d44a95f5fd4bc0051577488fa → 13cf6c054ed424e2eb639ea12d20a4aa2b1c85eeb00afa8cbd8a30c6c22aadd8} +1 -1
  202. package/dist/phoenix-ui/bundle/blobs/1832ff9624cebd30370eb0e806665c656ac879215f93d33bcc0750baa47459dc +46 -0
  203. package/dist/phoenix-ui/bundle/blobs/{8e7de5ca347036a07f5f1e9cab942d8ff378d029bc101fa6d126406f3298c9db → 1864a10c29e7062cca5e49d1790dc7d9d8ea777bfa2ab6fd941635965cd0084d} +1 -1
  204. package/dist/phoenix-ui/bundle/blobs/{784722b72224a7005a80cdcd6408dd2b25b23fd9619d0f6c8788f7ac681fad4c → 1ecc81de39d28b85b95e797bcc338d4213a50231c3315a741d14571fd22c844f} +6 -7
  205. package/dist/phoenix-ui/bundle/blobs/{b8c614dc7c2eb30ee0f864fec116dbcafb39e5a199dce66d0c1a88b13f76265a → 2079e4146039a8dadf19a39237cbdc0b8b54ea1fa16d320716c2cbf58161c835} +1 -1
  206. package/dist/phoenix-ui/bundle/blobs/{dba6e4a09c5d752e5ddebe7c2c1aefc9c4b2a84e01a3178599579778ed2d09e9 → 2813aa68c0804a7fe061d88201d8a684cac9987b48511e1e2cbfe0a869a67a82} +6 -7
  207. package/dist/phoenix-ui/bundle/blobs/{2889c2817b5079abff254bb6d13f92dbde0fabc38be4acc7d20e613eef5e5829 → 2873e340be947c83c3d03520139b73524fd166a34063d2693d3b711021ea5b87} +6 -7
  208. package/dist/phoenix-ui/bundle/blobs/{64c4aa2d1c24ae5028e987498ac544020d48600a51d9654b2df562681ba480f2 → 298ef41899acefa056eccf2345378078832db6d868d2377f471f896d5c1599f9} +6 -14
  209. package/dist/phoenix-ui/bundle/blobs/{1b7174a6f3f49a8efc8f7fa74ff84cf8960e2646b9bc976e4129a7c8ad608faa → 2b5cef2a14a5b1256ec6de9172a4f57f227948ced1921ef86e899f48524d7fb8} +1 -1
  210. package/dist/phoenix-ui/bundle/blobs/{f148f630f43f3c91cf10feeda5a3c21448ca0683ce82d0fe763c1b43dee584d6 → 2d376cbbf7ec1e262614aac4f1b200e8f60e86e940c7137a2a26788bc857e509} +6 -7
  211. package/dist/phoenix-ui/bundle/blobs/2e5985a6eb339d70cfa25f6d2b420a0ef286caa8467f0226eb841702ec438d0b +46 -0
  212. package/dist/phoenix-ui/bundle/blobs/{7e199833376de5d2b6e088ee2965b448f94da842f28ccbd707e1cc3a295147c8 → 3322d691bf7b578b38723576df6b266993b2afb9e567b0cd1ded898ffe1dc17e} +1 -1
  213. package/dist/phoenix-ui/bundle/blobs/{f82bfc4c6bc9169d4978d5e16fb925f6e152fdd21dc6a2fae719dfdc1645581f → 3625c5c2750f117021232ce81c1e39ac5ef7c21445729327b88cf54dfc937df0} +6 -3
  214. package/dist/phoenix-ui/bundle/blobs/{2ceacad745665f79fd7ad5565646426a36a1b508c0469b931d0f202fad45a80b → 36310febe3e7bd4c9b21407603f2a04afefdc87b7821758bb47098b1b2876765} +6 -3
  215. package/dist/phoenix-ui/bundle/blobs/{24d92a176573768bfffaf8f9c6483e58bceb5bf2a095d1cdf3d3a2c0bcda0707 → 3a9c2daf4ef284ca83b0e7f6fef3a5300e273c32b20769e090463cb8a8b0b06f} +1 -1
  216. package/dist/phoenix-ui/bundle/blobs/{153e377c44b162cf0074698a61716db7f664db78f5635efa926bed484de4f54c → 3ce1a44efe39b93e7a39307c6a5e5f514e938fbae818e358986b4db1b05883ca} +1 -1
  217. package/dist/phoenix-ui/bundle/blobs/{ad8ad0b7acf6c88ef9ce0ee1a640c2200da77821f0d97d605d4f1d4a10e5897a → 403fcde498d75abec7844ee31434201152ac229f7675772164d69563f0572da4} +1 -1
  218. package/dist/phoenix-ui/bundle/blobs/{e8ee6c08e94b80324173d89d6f4cdc5b4829ea6858b3a11fc186588a490cada4 → 45d0e7eff0923c40296e971318756617944c4f1c97f03126cd74c0f8c629f8a4} +3 -1
  219. package/dist/phoenix-ui/bundle/blobs/{e23dabf908842be08751fc4e7f3b586e8c6b1ca0fc30303ec5317c77f83dcf26 → 4c0f396bfa85171f61972d3f3765733318769bb53fe06d2cdcb5d7f64578b5b8} +1 -1
  220. package/dist/phoenix-ui/bundle/blobs/{a5517d32c1539c388c47c00879a0d807ed2989647bfa8c0844ef22328f75f5be → 4f6563876e0e0cc51c38a06767b8800eb3238611e9fdd11e5ff9dd487c742e74} +1 -1
  221. package/dist/phoenix-ui/bundle/blobs/{a9d2203acd45ca33a13d5c68b02b23ed512425ac15f0e8438318ed124b43729d → 4fcada229d0c689c1a493d13184145a81ff40c498e882142ae390bda8e5b2492} +15 -0
  222. package/dist/phoenix-ui/bundle/blobs/5293adc55cf66068b6df20468ad9e91433fd1c48e75d66c86f874aca951a8fb9 +46 -0
  223. package/dist/phoenix-ui/bundle/blobs/{96e2e6bd4fcf9a2c6da65fb029f96d9176308fae2efd9d8653d3b8d838960e07 → 5436f0301ec258f3badcc6842019ee29a9b870d2727dc425fbc9cc0f4f88a284} +3 -1
  224. package/dist/phoenix-ui/bundle/blobs/{d0f4939e86a62b0ca437d51a997ceba7e77d64566b7fa8ca2747487e31ca819d → 54a09a4afdffca3cc135e5007716ea684dc1ed7a78349de859225c55a2fed810} +6 -7
  225. package/dist/phoenix-ui/bundle/blobs/{d5102701d41f7c6195d410a873f742ac72c428b04196e47df5555c5ccaf80504 → 55277fa15eb4bd2f09bf7bb24c4e79df417b06654b1e10f502c7928b192fd7cc} +6 -3
  226. package/dist/phoenix-ui/bundle/blobs/{878064e46af4e4109bab405f965e4149bee40bbe8912ffede10b74cec6cff027 → 567c3f42394f9aee9b048a001016a7ba9d8773d78027f8c995c379886634e719} +6 -3
  227. package/dist/phoenix-ui/bundle/blobs/57c8f33a7401f918566b8a4883871a69ef2223438c51f158c67f69e3250be465 +46 -0
  228. package/dist/phoenix-ui/bundle/blobs/{bc29e115fd337dda2cc7fc70b868dc18e426b08ec186fd270fcfe96bb2efaa5d → 61450dbef64859e3136cd17ae3e997134d5df5721d96985c4e07367f38522129} +6 -3
  229. package/dist/phoenix-ui/bundle/blobs/{e3c1e925be0b2deb0e3765b83ab94b86560292364f275e98bb23a8bdf2c2e049 → 656bbfda3e72b40d53b5832293dc8700ad8a784e804a6246271bf0ea23527822} +1 -1
  230. package/dist/phoenix-ui/bundle/blobs/{ffcaa5c58e779ec74d18bfc3013ad8003f6e7a23f629c1a3b878618961a8b6a3 → 68b454a99ba72c3b9b93ed96c4267847297169f6a5bf321b20e60d637c271068} +6 -3
  231. package/dist/phoenix-ui/bundle/blobs/68e44832c82b873b0aeb2da56def7b1de5f612c019445e5e28238c690124cbbe +46 -0
  232. package/dist/phoenix-ui/bundle/blobs/{45bbd4a8e02ca0012216c5c68e1dcd655ae0046bd50ba4295ef1f58ec4a0957c → 6b8d4c71355429a0b16bf367163fea46103a0a99b4e34f87608a39d85b1b980a} +6 -3
  233. package/dist/phoenix-ui/bundle/blobs/{264815d0080e4862715a4f82542e66241c6563f07259d32aed4a17bbc329b9cc → 6cef874722442de7c60ea96f4221dc54cc4250a270cd05a1e818b58c5b43e232} +6 -7
  234. package/dist/phoenix-ui/bundle/blobs/{2028a857c0476324a4256faf66693e2a869275e53a88d2b182a28b62c2a17893 → 7181f1f4c452c4345695f9100f6deb610f54f4c55f126043433a65b8491fc680} +6 -7
  235. package/dist/phoenix-ui/bundle/blobs/{ccb76aabba47938d518a5d46a09d7e70c4a9222bc391e163763f2a7f12d30437 → 75850a8c9d2a8259f71ef1f86dd5d1325415227c1887e81a3467ae1ee400979f} +1 -1
  236. package/dist/phoenix-ui/bundle/blobs/{4b03cdb5b17d534b5c7e84b079c066f59e8b60b9e454671a453867c4aeb9920d → 77bcc6c2d6ac00a8bfead3b6c6b5fc69f4b60bf086ebbc89271e1b23003168c9} +1 -1
  237. package/dist/phoenix-ui/bundle/blobs/{9520e25a381c82af6f46e175b7c347d1dc513d933dd6cf8c52570c72abb4ca7f → 792552563745d208594cf4ade19110f2c811a0e1a2661ae1ef43a185ccff5a69} +1 -1
  238. package/dist/phoenix-ui/bundle/blobs/{f7c8af35acf71a96056afb6afcff6a3ea42f65561f02652d04e11ad9ece9b8e9 → 7acd9b6f2f61b509c6a518508b8b52c2f0de67cdea4fd3f3d383b3ce8b38cf5e} +6 -7
  239. package/dist/phoenix-ui/bundle/blobs/7bacc1e3da7e8b7127ca3f8f69a2422098b9b2d23fbedefc56dfe76117c1c0e3 +46 -0
  240. package/dist/phoenix-ui/bundle/blobs/{78662176c1d31cec2d640f70df203f93cc1aec604cfc31619f4d2c62ab78861b → 8078b804bea630ed9c4ea07ec2f8ac92aba4128725860a66599eda90390d98f6} +6 -3
  241. package/dist/phoenix-ui/bundle/blobs/{2291169748fe0c4c80634dff3083e7464f82d4358b9b2186759b316e612d466f → 88b150cb804bc141b80a5687a091f61b99627155c465a27adb88546ef615af92} +6 -3
  242. package/dist/phoenix-ui/bundle/blobs/{bde88bf5ac59d744440d9b65f95ceaae01c803b101d48e6afd644a06eeb5de63 → 90f8b07ec12b78e633e306322cda58415aea70d8e0f9ff85bd439361c01dabe1} +6 -3
  243. package/dist/phoenix-ui/bundle/blobs/{486145071d3445e82774f1b39b2bb15986ce52277e798a31ef7e9c27afb42af3 → 95b2944686e8258a655c3b5e6db68593e24e5199354e46aac553918ea2c0f55a} +1 -1
  244. package/dist/phoenix-ui/bundle/blobs/{a1998e37084c11f496d4970f86a85d5a6375bbeaa4797b9f42bbac2d44048d23 → 979639f0e8859d600a8db57e733566d9ad37ff8258c3e05a6f9c0f3008e92373} +1 -1
  245. package/dist/phoenix-ui/bundle/blobs/{3173293607055ea6e1a153ec6f51fb8154782157c8a61abf411da1f41be61728 → 9eaaa00049844060f842fe7fb73cc8edc4be5225cf38a96f33bda3ffc381456c} +6 -7
  246. package/dist/phoenix-ui/bundle/blobs/{69086f7cb235b35ccc230e1f2da41cbb10cea8f0636b6d4514672113a3adc2cb → a420bbda622399399a6b5c7c3614dcaebc6c62faceaf7b3c83c97db53d26b165} +1 -1
  247. package/dist/phoenix-ui/bundle/blobs/a63054b8920905045753facece83f154bf806158c6f46ef380036e7a82d4fde5 +46 -0
  248. package/dist/phoenix-ui/bundle/blobs/{e802e4f7bdc89050a9c0f2ca506e1d493a2316e6e810c0336c57aa073717ef3e → a63788c764b0e4fdcbc029d46093016310edcbcf702d62688e5aaac7ddeee6fd} +3 -1
  249. package/dist/phoenix-ui/bundle/blobs/{827a211ea03d2cb44b4ea1bc81ba1a6dc51a87fa16d6692997c4e4751241c815 → acfc4c26b135d5b27fc0930738486c9d43d379704102fab45564320180942e4b} +4 -2
  250. package/dist/phoenix-ui/bundle/blobs/{8da14f3eabac53c990a66e721387367b46cb676cc4204e8c67d6f88c95a6217f → ad243ca615349127aa422bccc1a6604ad17149735659087ca5bab3c5fae97f4e} +6 -7
  251. package/dist/phoenix-ui/bundle/blobs/b63931066e2b2623d012e1a3c1a0769cc43ed6640460c3899669b45539d6a20d +46 -0
  252. package/dist/phoenix-ui/bundle/blobs/{bb65af58ecc3e0c0093dc9a705a7ab643035e4e592c3adb93583c82d1aef1f51 → bde793ab5ad776f7e6f6d25ce2b838c1b5022054a1dad5502a873423eb0ad917} +6 -7
  253. package/dist/phoenix-ui/bundle/blobs/{8de7c97949b0bc4194b3be7645e4b13e9419767927482d01cf19476f6cbeda14 → c0a70ab2889e5c16e1f09249aefad948ce352c42cfe2b5afb758a4524e1b21b6} +6 -3
  254. package/dist/phoenix-ui/bundle/blobs/{60b2914df394800688c4e20f4eb34e117cd430df9954cf4d48574734507723f4 → c9ee9006ae6c0194a5d39a9f0cb275c52d74a71ea3f6dc2b4c1c7bd89f48a2c0} +1 -1
  255. package/dist/phoenix-ui/bundle/blobs/{6d8315e307eab11d3236935daf7267746cce6da3b51296e6999651ac97bda3a3 → cadc2daefcf238aab981fa56388381d3d5ab9c0686bb9dd8187035c9933dfcee} +6 -3
  256. package/dist/phoenix-ui/bundle/blobs/{d7b744fcc95164d96d9f811a93e2e8a374887146eb762ef775f3d3c7f27abc8d → cbea5728b90f58ff047be6ec4f14eed9d63c7148f5db99b5f147d2f17b472317} +1 -1
  257. package/dist/phoenix-ui/bundle/blobs/d408a4fea71b656aa8a5f6b5c037815a6e4229df336629ebddf4b5c85ed82fac +46 -0
  258. package/dist/phoenix-ui/bundle/blobs/{6ff950a5729ad9ea480ddf8c1b489b1c84189bbf7d6034b3b5a98e7c11a28cec → d9ae0c782d8a77787561536c0d7bc3b61175d59303f3cb8c20796f85140c6984} +1 -1
  259. package/dist/phoenix-ui/bundle/blobs/{1d53e2226465b1047faec7cda2e743e61540ea69e073a3a4057e8d86b277405f → e2932d63eb7f52a82960a19ba79bc800ef4f212164e098f96dee7a4326302f38} +1 -1
  260. package/dist/phoenix-ui/bundle/blobs/{fa576f6655b235bdea535198647305b7a3855e810180cbdf5d4c246c03c7d190 → e381bcfec42ac541d574011460600cf0818aa0e3362e8c6d205b4157890bf821} +6 -7
  261. package/dist/phoenix-ui/bundle/blobs/{8fbce5b4859e6011afa83b1c07c4436589cd7181f9cb997538f4c27f196d0d34 → e415657d8725d9b60ece42cc18581da177c42fc47cb11e501db722a5f524ecf7} +6 -7
  262. package/dist/phoenix-ui/bundle/blobs/{546a8428a2d8628bb3496fadb8c36a6dd65a33464291f214ceb581828f37081b → e46d63e5f8a6ead1d44c8b6302a0c7247af8585b975ab5715518381777622ede} +6 -3
  263. package/dist/phoenix-ui/bundle/blobs/ee63b3f9b354d69443d05ea4a338af56546897335ace7533546e10be56ea2207 +46 -0
  264. package/dist/phoenix-ui/bundle/blobs/{c23617601e9c4d97b775c175a5ce1642c40459b157d0821609fe1d253b83ced8 → ef7d57633252f4604749447608850e299c277f7dd1a32776685df0724b1051c8} +6 -7
  265. package/dist/phoenix-ui/bundle/blobs/{0581c8428246f43c5374393878a29e38f2b1865df87e6f13f2309b8eae54e76f → f0652c6ce41e6949133360178f192ea4ee9f6129a018c7a8b6652870c2330392} +1 -1
  266. package/dist/phoenix-ui/bundle/blobs/{cfac9eaf96481a8874391e5cecc6149abea1f1f19622f77f1ebb9e1876720e91 → f06c9d797ca2ac8c6a49d3dcd2e99d1b4df8bc9637a49c7e80f3161f49d06b1b} +4 -2
  267. package/dist/phoenix-ui/bundle/blobs/{d50780cb0a345051ae3fb12c5f8249a5dac9324d6a1963f04d03aaf769ffc27c → f077545caeb3ecb9f51a3f703a09736ca924869f4b4345c1b1ffbd9bf927a287} +6 -3
  268. package/dist/phoenix-ui/bundle/blobs/f781c5874bcd79ebe1b3b012275b9af7eae6c4867e9a16e29bb3d3ed3dc1940b +46 -0
  269. package/dist/phoenix-ui/bundle/blobs/fa10c0c926997387c89b7c1070e25b1b2b53ee891741e3c450a89ec6c6dcaf7d +46 -0
  270. package/dist/phoenix-ui/bundle/blobs/fb1a36ab7116b0c6709b89720cdded29d80982114c2f87cad614e01e126270dd +46 -0
  271. package/dist/phoenix-ui/bundle/blobs/fbd3214fc12ba70d9c69a995742ba73dc5365823af63afdc93c0c8a8f0c484af +46 -0
  272. package/dist/phoenix-ui/bundle/blobs/{77682e9d63b484f94a699f2c0b393e4540797d311362c384a3d643aec11d6985 → fea19b8381ec10ff8ed1923a336332974bbb552227ed215e7ddaaf0ec0112c1d} +1 -1
  273. package/dist/phoenix-ui/bundle/manifest.json +126 -126
  274. package/docs/phoenix-diagram.md +173 -0
  275. package/docs/phoenix-ui.md +40 -12
  276. package/package.json +14 -10
  277. package/skills/agi-mode/SKILL.md +3 -1
  278. package/skills/agi-mode/assets/sdlc/event.md +4 -5
  279. package/skills/agi-mode/assets/sdlc/object-status.md +1 -1
  280. package/skills/agi-mode/assets/sdlc/protocol.md +4 -2
  281. package/skills/agi-mode/evals/scenarios.md +108 -3
  282. package/skills/agi-mode/playbooks/authoring-a-skill.md +13 -2
  283. package/skills/agi-mode/playbooks/eval.md +19 -3
  284. package/skills/agi-mode/playbooks/hillclimb.md +4 -2
  285. package/skills/agi-mode/playbooks/reflection.md +1 -1
  286. package/skills/agi-mode/playbooks/sdlc.md +10 -6
  287. package/skills/agi-mode/playbooks/what-next.md +6 -2
  288. package/skills/agi-mode/references/eval-protocol.md +13 -4
  289. package/skills/agi-mode/references/interface-design.md +1 -0
  290. package/skills/agi-mode/references/remote-evolution.md +178 -0
  291. package/skills/agi-mode/references/skill-evolution.md +2 -0
  292. package/skills/agi-mode/scripts/evolution/compare.py +569 -0
  293. package/skills/agi-mode/scripts/evolution/evolution.py +2886 -0
  294. package/skills/agi-mode/scripts/evolution/examples/public-smoke.json +128 -0
  295. package/skills/agi-mode/scripts/evolution/examples/quality-calibration.json +128 -0
  296. package/skills/agi-mode/scripts/evolution/examples/quality-rubric.json +133 -0
  297. package/skills/agi-mode/scripts/evolution/protocol.py +1586 -0
  298. package/skills/agi-mode/scripts/evolution/pyproject.toml +16 -0
  299. package/skills/agi-mode/scripts/evolution/quality.py +689 -0
  300. package/skills/agi-mode/scripts/evolution/remote.py +2303 -0
  301. package/skills/agi-mode/scripts/evolution/tests/support.py +201 -0
  302. package/skills/agi-mode/scripts/evolution/tests/test_apply.py +643 -0
  303. package/skills/agi-mode/scripts/evolution/tests/test_bounded_exploration.py +236 -0
  304. package/skills/agi-mode/scripts/evolution/tests/test_bundles.py +414 -0
  305. package/skills/agi-mode/scripts/evolution/tests/test_calibration.py +460 -0
  306. package/skills/agi-mode/scripts/evolution/tests/test_compare.py +377 -0
  307. package/skills/agi-mode/scripts/evolution/tests/test_protocol.py +357 -0
  308. package/skills/agi-mode/scripts/evolution/tests/test_quality.py +647 -0
  309. package/skills/agi-mode/scripts/evolution/tests/test_remote.py +857 -0
  310. package/skills/agi-mode/scripts/evolution/tests/test_search.py +1109 -0
  311. package/skills/agi-mode/scripts/evolution/tests/test_typesafe.py +624 -0
  312. package/skills/agi-mode/scripts/evolution/tests/test_verifier_context.py +132 -0
  313. package/skills/agi-mode/scripts/evolution/typesafe_worker.py +145 -0
  314. package/skills/agi-mode/scripts/evolution/uv.lock +2548 -0
  315. package/skills/apply-code-review/SKILL.md +1 -0
  316. package/skills/apply-code-review/agents/openai.yaml +2 -0
  317. package/skills/apply-doc-review/SKILL.md +1 -0
  318. package/skills/apply-doc-review/agents/openai.yaml +2 -0
  319. package/skills/apply-ui-review/SKILL.md +1 -0
  320. package/skills/apply-ui-review/agents/openai.yaml +2 -0
  321. package/skills/autoresearch/SKILL.md +1 -0
  322. package/skills/clean-artifacts/SKILL.md +52 -0
  323. package/skills/clean-artifacts/agents/openai.yaml +4 -0
  324. package/skills/clean-artifacts/evals/scenarios.md +13 -0
  325. package/skills/clean-artifacts/references/history-cleanup.md +41 -0
  326. package/skills/crd/SKILL.md +100 -65
  327. package/skills/crd/assets/cr-template.md +15 -0
  328. package/skills/crd-respond/SKILL.md +17 -5
  329. package/skills/crd-respond/agents/openai.yaml +2 -2
  330. package/skills/crd-respond/assets/response-template.md +7 -0
  331. package/skills/crd-respond/evals/scenarios.md +7 -0
  332. package/skills/create-verification-skill/SKILL.md +1 -0
  333. package/skills/debugging/SKILL.md +1 -0
  334. package/skills/design-md/SKILL.md +1 -0
  335. package/skills/design-reference/SKILL.md +6 -2
  336. package/skills/design-reference/evals/scenarios.md +4 -0
  337. package/skills/dev-plan/SKILL.md +11 -6
  338. package/skills/dev-plan/references/execution-workflow.md +1 -1
  339. package/skills/dev-plan/references/plan-skeleton.md +234 -69
  340. package/skills/dev-plan/scripts/check_plan.py +1 -1
  341. package/skills/documentation-writer/SKILL.md +19 -14
  342. package/skills/documentation-writer/references/architecture.md +6 -2
  343. package/skills/documentation-writer/references/user-manual.md +2 -2
  344. package/skills/elintp/SKILL.md +12 -6
  345. package/skills/experience-capture/SKILL.md +1 -0
  346. package/skills/gdpr-compliant/SKILL.md +1 -0
  347. package/skills/kpi-dashboard-design/SKILL.md +1 -0
  348. package/skills/landing-page-conversion-audit/SKILL.md +1 -0
  349. package/skills/mcp-release-qa/SKILL.md +2 -1
  350. package/skills/meeting-minutes/SKILL.md +1 -0
  351. package/skills/optimize-agents-md/SKILL.md +128 -0
  352. package/skills/optimize-agents-md/assets/coding-principles.md +118 -0
  353. package/skills/phoenix-diagram/ENGINE.json +39 -0
  354. package/skills/phoenix-diagram/SKILL.md +109 -0
  355. package/skills/phoenix-diagram/examples/legacy.agent-tool-call.workflow.json +94 -0
  356. package/skills/phoenix-diagram/examples/legacy.async-job-roundtrip.sequence.json +61 -0
  357. package/skills/phoenix-diagram/examples/legacy.deployment-release.lifecycle.json +49 -0
  358. package/skills/phoenix-diagram/examples/legacy.product-analytics.dataflow.json +76 -0
  359. package/skills/{archify/test/fixtures/v1-baseline/web-app.architecture.json → phoenix-diagram/examples/legacy.web-app.architecture.json} +16 -11
  360. package/skills/phoenix-diagram/examples/minimal.architecture.json +11 -0
  361. package/skills/phoenix-diagram/examples/minimal.class.json +23 -0
  362. package/skills/phoenix-diagram/examples/minimal.dataflow.json +11 -0
  363. package/skills/phoenix-diagram/examples/minimal.erd.json +25 -0
  364. package/skills/phoenix-diagram/examples/minimal.lifecycle.json +11 -0
  365. package/skills/phoenix-diagram/examples/minimal.sequence.json +13 -0
  366. package/skills/phoenix-diagram/examples/minimal.timeline.json +15 -0
  367. package/skills/phoenix-diagram/examples/minimal.tree.json +13 -0
  368. package/skills/phoenix-diagram/examples/minimal.waterfall.json +12 -0
  369. package/skills/phoenix-diagram/examples/minimal.workflow.json +12 -0
  370. package/skills/phoenix-diagram/references/architecture-compare.md +111 -0
  371. package/skills/phoenix-diagram/references/authoring-defaults.md +42 -0
  372. package/skills/phoenix-diagram/references/brand-marks.md +52 -0
  373. package/skills/phoenix-diagram/references/compatibility.md +22 -0
  374. package/skills/phoenix-diagram/references/delivery-contract.md +127 -0
  375. package/skills/phoenix-diagram/references/extending-types.md +14 -0
  376. package/skills/phoenix-diagram/references/migration.md +57 -0
  377. package/skills/phoenix-diagram/references/preview.md +45 -0
  378. package/skills/phoenix-diagram/references/repository-authoring.md +29 -0
  379. package/skills/phoenix-diagram/references/viewer-runtime.md +27 -0
  380. package/skills/phoenix-diagram/schemas/README.md +23 -0
  381. package/skills/phoenix-diagram/schemas/architecture.schema.json +329 -0
  382. package/skills/phoenix-diagram/schemas/class.schema.json +348 -0
  383. package/skills/phoenix-diagram/schemas/common.schema.json +358 -0
  384. package/skills/{archify/schemas/lifecycle.schema.json → phoenix-diagram/schemas/dataflow.schema.json} +63 -58
  385. package/skills/phoenix-diagram/schemas/erd.schema.json +384 -0
  386. package/skills/phoenix-diagram/schemas/lifecycle.schema.json +202 -0
  387. package/skills/phoenix-diagram/schemas/sequence.schema.json +254 -0
  388. package/skills/phoenix-diagram/schemas/timeline.schema.json +170 -0
  389. package/skills/phoenix-diagram/schemas/tree.schema.json +144 -0
  390. package/skills/phoenix-diagram/schemas/waterfall.schema.json +150 -0
  391. package/skills/phoenix-diagram/schemas/workflow.schema.json +460 -0
  392. package/skills/phoenix-diagram/scripts/phoenix-bootstrap.cjs +589 -0
  393. package/skills/phoenix-diagram/scripts/phoenix-diagram +6 -0
  394. package/skills/phoenix-diagram/scripts/phoenix-launcher.cjs +10 -0
  395. package/skills/phoenix-ui/GENERATED.json +255 -240
  396. package/skills/phoenix-ui/SKILL.md +7 -4
  397. package/skills/phoenix-ui/evals/scenarios.md +8 -1
  398. package/skills/phoenix-ui/reference/craft-floor.md +3 -1
  399. package/skills/phoenix-ui/reference/critique.md +6 -14
  400. package/skills/phoenix-ui/reference/init.md +1 -1
  401. package/skills/phoenix-ui/reference/live.md +1 -1
  402. package/skills/phoenix-ui/reference/operate.md +15 -0
  403. package/skills/phoenix-ui/scripts/ENGINE.json +17 -17
  404. package/skills/phoenix-ui/scripts/phoenix-bootstrap.cjs +0 -4
  405. package/skills/portal-app-exchange/SKILL.md +1 -0
  406. package/skills/portal-dev-setup/SKILL.md +71 -12
  407. package/skills/portal-dev-setup/scripts/onebox.sh +69 -6
  408. package/skills/portal-external-app/SKILL.md +55 -16
  409. package/skills/portal-external-app/references/config-placement.md +108 -2
  410. package/skills/portal-external-app/references/registration-and-onebox.md +56 -2
  411. package/skills/portal-logging/SKILL.md +1 -0
  412. package/skills/portal-micro-app/SKILL.md +2 -1
  413. package/skills/portal-micro-app/references/sdk-reference.md +3 -1
  414. package/skills/prd/SKILL.md +238 -159
  415. package/skills/prd/evals/scenarios.md +13 -0
  416. package/skills/prd/references/prd-skeleton.md +14 -4
  417. package/skills/react/SKILL.md +1 -0
  418. package/skills/resolve-blocked/SKILL.md +46 -19
  419. package/skills/resolve-blocked/agents/openai.yaml +2 -2
  420. package/skills/resolve-blocked/evals/scenarios.md +22 -0
  421. package/skills/resolve-blocked/references/decisions-contract.md +7 -4
  422. package/skills/review-code/SKILL.md +308 -46
  423. package/skills/review-code/agents/openai.yaml +2 -0
  424. package/skills/review-dev-plan/SKILL.md +88 -33
  425. package/skills/review-dev-plan/agents/openai.yaml +2 -0
  426. package/skills/review-dev-plan/references/report-contract.md +53 -19
  427. package/skills/review-dev-plan/references/review-lenses.md +2 -1
  428. package/skills/review-prd/SKILL.md +90 -34
  429. package/skills/review-prd/agents/openai.yaml +2 -0
  430. package/skills/review-prd/references/report-contract.md +54 -18
  431. package/skills/review-prd-dev-gaps/SKILL.md +1 -0
  432. package/skills/review-prd-dev-gaps/agents/openai.yaml +2 -0
  433. package/skills/review-ui/SKILL.md +369 -71
  434. package/skills/review-ui/agents/openai.yaml +2 -0
  435. package/skills/review-ui/evals/scenarios.md +2 -0
  436. package/skills/rust/SKILL.md +76 -84
  437. package/skills/rust/references/api-design.md +97 -0
  438. package/skills/rust/references/{chapter_10.md → async-tokio.md} +42 -18
  439. package/skills/rust/references/behavioral-patterns.md +49 -0
  440. package/skills/rust/references/bevy.md +95 -0
  441. package/skills/rust/references/{chapter_02.md → clippy-and-linting.md} +26 -15
  442. package/skills/rust/references/{chapter_01.md → coding-idioms.md} +58 -7
  443. package/skills/rust/references/crates-and-dependencies.md +66 -0
  444. package/skills/rust/references/{chapter_08.md → documentation.md} +30 -3
  445. package/skills/rust/references/{chapter_04.md → error-handling.md} +32 -6
  446. package/skills/rust/references/{chapter_06.md → generics-and-dispatch.md} +28 -11
  447. package/skills/rust/references/macros.md +45 -0
  448. package/skills/rust/references/{chapter_03.md → performance.md} +25 -2
  449. package/skills/rust/references/{chapter_09.md → pointers-and-ownership.md} +37 -17
  450. package/skills/rust/references/{chapter_05.md → testing.md} +18 -2
  451. package/skills/rust/references/{chapter_07.md → type-state.md} +5 -1
  452. package/skills/rust/references/unsafe-and-ffi.md +56 -0
  453. package/skills/rust-port/SKILL.md +144 -0
  454. package/skills/rust-review/SKILL.md +177 -0
  455. package/skills/security-audit/SKILL.md +1 -0
  456. package/skills/spec-human-decision/SKILL.md +1 -0
  457. package/skills/sql-optimization/SKILL.md +1 -0
  458. package/skills/story-points/SKILL.md +1 -0
  459. package/skills/swarm/SKILL.md +1 -0
  460. package/skills/test-plan/SKILL.md +1 -0
  461. package/skills/ui-loop/SKILL.md +39 -0
  462. package/skills/ui-loop/agents/openai.yaml +3 -0
  463. package/skills/ui-pattern-research/SKILL.md +1 -0
  464. package/skills/use-llm-gateway/SKILL.md +58 -0
  465. package/skills/use-llm-gateway/agents/openai.yaml +4 -0
  466. package/skills/use-llm-gateway/references/api.md +129 -0
  467. package/skills/use-llm-gateway/references/recovery.md +86 -0
  468. package/skills/use-prompt-dsl/SKILL.md +165 -0
  469. package/skills/use-prompt-dsl/references/examples.md +726 -0
  470. package/skills/use-prompt-dsl/references/language.md +938 -0
  471. package/skills/use-rustify-docs/SKILL.md +60 -0
  472. package/skills/use-rustify-docs/agents/openai.yaml +4 -0
  473. package/skills/use-rustify-docs/references/rustify-docs.md +138 -0
  474. package/skills/use-task-gateway/SKILL.md +60 -0
  475. package/skills/use-task-gateway/agents/openai.yaml +3 -0
  476. package/skills/use-task-gateway/references/authentication.md +75 -0
  477. package/skills/use-task-gateway/references/task-api.md +121 -0
  478. package/skills/use-task-gateway/references/workers.md +85 -0
  479. package/skills/use-task-gateway/references/workflow-actions.md +66 -0
  480. package/skills/use-workflow-service/SKILL.md +57 -0
  481. package/skills/use-workflow-service/agents/openai.yaml +3 -0
  482. package/skills/use-workflow-service/references/action-provider.md +84 -0
  483. package/skills/use-workflow-service/references/authentication.md +76 -0
  484. package/skills/use-workflow-service/references/authoring.md +62 -0
  485. package/skills/use-workflow-service/references/mcp.md +47 -0
  486. package/skills/use-workflow-service/references/open-api.md +88 -0
  487. package/skills/use-workflow-service/references/orchestration-choice.md +63 -0
  488. package/skills/working-with-mbx/SKILL.md +1 -0
  489. package/skills/working-with-mise/SKILL.md +1 -0
  490. package/skills/xgent-app-release/SKILL.md +114 -16
  491. package/skills/xgent-app-release/references/build-and-package.md +2 -1
  492. package/skills/xgent-app-release/references/publish-api.md +6 -1
  493. package/skills/xgent-app-release/references/troubleshooting.md +2 -1
  494. package/skills/xgent-app-release/references/verify-preflight.mjs +21 -1
  495. package/skills/xgent-app-release/scripts/preflight.mjs +42 -3
  496. package/skills/xgent-image-push/SKILL.md +1 -0
  497. package/skills/xgent-init/SKILL.md +1 -0
  498. package/docs/claude-context-goal-guard.md +0 -124
  499. package/docs/codex-context-goal-guard.md +0 -109
  500. package/hooks/claude/context-goal-guard.js +0 -139
  501. package/hooks/claude/settings.example.json +0 -26
  502. package/hooks/codex/context-goal-guard.py +0 -189
  503. package/hooks/codex/hooks.example.json +0 -26
  504. package/skills/archify/SKILL.md +0 -137
  505. package/skills/archify/assets/template.html +0 -14937
  506. package/skills/archify/bin/archify.mjs +0 -2139
  507. package/skills/archify/bin/open-artifact.mjs +0 -86
  508. package/skills/archify/bin/preview.mjs +0 -653
  509. package/skills/archify/bin/visual-check.mjs +0 -829
  510. package/skills/archify/brand-marks/README.md +0 -31
  511. package/skills/archify/brand-marks/catalog.json +0 -131
  512. package/skills/archify/delta/architecture-delta.mjs +0 -1232
  513. package/skills/archify/examples/agent-run.lifecycle.json +0 -60
  514. package/skills/archify/examples/brand-aware-delivery.architecture.json +0 -47
  515. package/skills/archify/examples/cache-miss-request.sequence.json +0 -82
  516. package/skills/archify/examples/checkout-platform.base.architecture.json +0 -31
  517. package/skills/archify/examples/checkout-platform.head.architecture.json +0 -31
  518. package/skills/archify/examples/dataflow-product-analytics.html +0 -15048
  519. package/skills/archify/examples/event-stream.dataflow.json +0 -57
  520. package/skills/archify/examples/incident-response.workflow.json +0 -64
  521. package/skills/archify/examples/lifecycle-agent-run.html +0 -14983
  522. package/skills/archify/examples/production-deployment.architecture.json +0 -71
  523. package/skills/archify/examples/release-delivery.workflow.json +0 -62
  524. package/skills/archify/examples/sequence-cache-miss-request.html +0 -15063
  525. package/skills/archify/examples/web-app-rendered.html +0 -15012
  526. package/skills/archify/examples/workflow-agent-tool-call-rendered.html +0 -15054
  527. package/skills/archify/migrations/workflow-v2.mjs +0 -279
  528. package/skills/archify/package-lock.json +0 -149
  529. package/skills/archify/package.json +0 -39
  530. package/skills/archify/recipes/scenarios.mjs +0 -391
  531. package/skills/archify/references/authoring-contract.md +0 -247
  532. package/skills/archify/references/brand-marks.md +0 -65
  533. package/skills/archify/references/delivery-contract.md +0 -138
  534. package/skills/archify/references/viewer-runtime.md +0 -45
  535. package/skills/archify/renderers/architecture/grid.mjs +0 -62
  536. package/skills/archify/renderers/architecture/render-architecture.mjs +0 -1078
  537. package/skills/archify/renderers/dataflow/README.md +0 -104
  538. package/skills/archify/renderers/dataflow/render-dataflow.mjs +0 -483
  539. package/skills/archify/renderers/lifecycle/README.md +0 -115
  540. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +0 -561
  541. package/skills/archify/renderers/sequence/README.md +0 -114
  542. package/skills/archify/renderers/sequence/render-sequence.mjs +0 -464
  543. package/skills/archify/renderers/shared/brand-marks.mjs +0 -563
  544. package/skills/archify/renderers/shared/cli.mjs +0 -218
  545. package/skills/archify/renderers/shared/desktop-readability.mjs +0 -26
  546. package/skills/archify/renderers/shared/diagnostics.mjs +0 -160
  547. package/skills/archify/renderers/shared/engineering-profiles.mjs +0 -157
  548. package/skills/archify/renderers/shared/generated-brand-marks.mjs +0 -2003
  549. package/skills/archify/renderers/shared/generated-validators.mjs +0 -13
  550. package/skills/archify/renderers/shared/geometry.mjs +0 -1423
  551. package/skills/archify/renderers/shared/i18n.mjs +0 -595
  552. package/skills/archify/renderers/shared/layout-report.mjs +0 -40
  553. package/skills/archify/renderers/shared/legend.mjs +0 -217
  554. package/skills/archify/renderers/shared/output-path.mjs +0 -340
  555. package/skills/archify/renderers/shared/repository-evidence.mjs +0 -239
  556. package/skills/archify/renderers/shared/repository-location.mjs +0 -58
  557. package/skills/archify/renderers/shared/text-fit.mjs +0 -49
  558. package/skills/archify/renderers/shared/utils.mjs +0 -232
  559. package/skills/archify/renderers/shared/validator.mjs +0 -86
  560. package/skills/archify/renderers/workflow/README.md +0 -223
  561. package/skills/archify/renderers/workflow/render-workflow.mjs +0 -35
  562. package/skills/archify/renderers/workflow/workflow-compiler.mjs +0 -4400
  563. package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +0 -144
  564. package/skills/archify/schemas/README.md +0 -211
  565. package/skills/archify/schemas/architecture.schema.json +0 -178
  566. package/skills/archify/schemas/common.schema.json +0 -115
  567. package/skills/archify/scripts/check-render-output.mjs +0 -916
  568. package/skills/archify/scripts/check-update.mjs +0 -1667
  569. package/skills/archify/scripts/generate-brand-marks.mjs +0 -141
  570. package/skills/archify/scripts/generate-validators.mjs +0 -66
  571. package/skills/archify/scripts/render-examples.mjs +0 -26
  572. package/skills/archify/scripts/update-contract.mjs +0 -182
  573. package/skills/archify/skill-release.json +0 -10
  574. package/skills/archify/test/adaptive-reader-layout.test.mjs +0 -86
  575. package/skills/archify/test/animation.test.mjs +0 -179
  576. package/skills/archify/test/architecture-compare-recovery.test.mjs +0 -157
  577. package/skills/archify/test/architecture-delta-markers.test.mjs +0 -137
  578. package/skills/archify/test/architecture-delta.test.mjs +0 -705
  579. package/skills/archify/test/artifact-receipt-flush.test.mjs +0 -126
  580. package/skills/archify/test/authored-reachability.test.mjs +0 -95
  581. package/skills/archify/test/authoring-safety-contract.test.mjs +0 -54
  582. package/skills/archify/test/automatic-port-spread.test.mjs +0 -376
  583. package/skills/archify/test/base-input-compatibility.test.mjs +0 -42
  584. package/skills/archify/test/brand-marks.test.mjs +0 -678
  585. package/skills/archify/test/chapter-delta-preview.test.mjs +0 -157
  586. package/skills/archify/test/chapter-handoff.test.mjs +0 -97
  587. package/skills/archify/test/chapter-rail.test.mjs +0 -86
  588. package/skills/archify/test/checkout-line-endings.test.mjs +0 -72
  589. package/skills/archify/test/ci-scope.test.mjs +0 -59
  590. package/skills/archify/test/clean-skill-staging.test.mjs +0 -440
  591. package/skills/archify/test/cli-output-types.test.mjs +0 -201
  592. package/skills/archify/test/cli.test.mjs +0 -986
  593. package/skills/archify/test/community-proof-intake.test.mjs +0 -125
  594. package/skills/archify/test/cursor-onboarding.test.mjs +0 -74
  595. package/skills/archify/test/degraded.test.mjs +0 -161
  596. package/skills/archify/test/delivery-contract.test.mjs +0 -52
  597. package/skills/archify/test/desktop-reader-browser.test.mjs +0 -80
  598. package/skills/archify/test/diagram-guide.test.mjs +0 -92
  599. package/skills/archify/test/engineering-profile.test.mjs +0 -209
  600. package/skills/archify/test/export-browser.test.mjs +0 -305
  601. package/skills/archify/test/export-cleanup-browser.test.mjs +0 -294
  602. package/skills/archify/test/finder-browser.test.mjs +0 -274
  603. package/skills/archify/test/finder.test.mjs +0 -104
  604. package/skills/archify/test/fixtures/automatic-routing-node-border-clearance.workflow.json +0 -40
  605. package/skills/archify/test/fixtures/fail-migration-cleanup.mjs +0 -17
  606. package/skills/archify/test/fixtures/issue-126/custom-widths.workflow.json +0 -18
  607. package/skills/archify/test/fixtures/issue-126/explicit-route.workflow.json +0 -28
  608. package/skills/archify/test/fixtures/issue-126/explicit-viewbox.workflow.json +0 -19
  609. package/skills/archify/test/fixtures/issue-126/labels.workflow.json +0 -18
  610. package/skills/archify/test/fixtures/v1-baseline/agent-run.lifecycle.json +0 -64
  611. package/skills/archify/test/fixtures/v1-baseline/agent-tool-call.workflow.json +0 -88
  612. package/skills/archify/test/fixtures/v1-baseline/cache-miss-request.sequence.json +0 -75
  613. package/skills/archify/test/fixtures/v1-baseline/event-stream.dataflow.json +0 -57
  614. package/skills/archify/test/fixtures/v1-baseline/product-analytics.dataflow.json +0 -70
  615. package/skills/archify/test/fixtures/v1-baseline/production-deployment.architecture.json +0 -71
  616. package/skills/archify/test/fixtures/v1-workflow-700x400.workflow.json +0 -22
  617. package/skills/archify/test/fixtures/v1-workflow-explicit-coordinates.workflow.json +0 -47
  618. package/skills/archify/test/focus-browser.test.mjs +0 -226
  619. package/skills/archify/test/gallery.test.mjs +0 -130
  620. package/skills/archify/test/generate-validators.test.mjs +0 -59
  621. package/skills/archify/test/generate-viewer.test.mjs +0 -165
  622. package/skills/archify/test/generated-artifact-xml.test.mjs +0 -88
  623. package/skills/archify/test/geometry.test.mjs +0 -739
  624. package/skills/archify/test/golden.mjs +0 -207
  625. package/skills/archify/test/grid.test.mjs +0 -47
  626. package/skills/archify/test/guide-page.test.mjs +0 -85
  627. package/skills/archify/test/guide.test.mjs +0 -81
  628. package/skills/archify/test/guided-views-browser.test.mjs +0 -460
  629. package/skills/archify/test/guided-views.test.mjs +0 -144
  630. package/skills/archify/test/helpers/desktop-browser.mjs +0 -48
  631. package/skills/archify/test/helpers/offline-fonts.mjs +0 -76
  632. package/skills/archify/test/helpers/viewer-click.mjs +0 -72
  633. package/skills/archify/test/helpers/xml.mjs +0 -37
  634. package/skills/archify/test/i18n.test.mjs +0 -422
  635. package/skills/archify/test/intent-trace-browser.test.mjs +0 -284
  636. package/skills/archify/test/intent-trace.test.mjs +0 -111
  637. package/skills/archify/test/landing.test.mjs +0 -97
  638. package/skills/archify/test/layout-rules.test.mjs +0 -1498
  639. package/skills/archify/test/legend-contract.test.mjs +0 -404
  640. package/skills/archify/test/motion-governor-browser.test.mjs +0 -342
  641. package/skills/archify/test/motion-governor.test.mjs +0 -111
  642. package/skills/archify/test/offline-font-browser.test.mjs +0 -210
  643. package/skills/archify/test/offline-self-containment.test.mjs +0 -80
  644. package/skills/archify/test/open-artifact.test.mjs +0 -118
  645. package/skills/archify/test/ordinary-model-floor.test.mjs +0 -1052
  646. package/skills/archify/test/output-path.test.mjs +0 -587
  647. package/skills/archify/test/presentation.test.mjs +0 -71
  648. package/skills/archify/test/preset-tryon.test.mjs +0 -113
  649. package/skills/archify/test/preview-contract.test.mjs +0 -40
  650. package/skills/archify/test/preview.test.mjs +0 -530
  651. package/skills/archify/test/proof-aperture.test.mjs +0 -78
  652. package/skills/archify/test/reach-share-card.test.mjs +0 -149
  653. package/skills/archify/test/reader-layout-browser.test.mjs +0 -320
  654. package/skills/archify/test/readme-showcase.test.mjs +0 -319
  655. package/skills/archify/test/real-repository-proof.test.mjs +0 -209
  656. package/skills/archify/test/relationship-direct-explorer.test.mjs +0 -115
  657. package/skills/archify/test/relationship-lens.test.mjs +0 -128
  658. package/skills/archify/test/relationship-permalink.test.mjs +0 -130
  659. package/skills/archify/test/relationship-pulse.test.mjs +0 -122
  660. package/skills/archify/test/release-identity.test.mjs +0 -476
  661. package/skills/archify/test/release-package-gates.test.mjs +0 -750
  662. package/skills/archify/test/render-output-checks.test.mjs +0 -588
  663. package/skills/archify/test/renderer-diagnostic-boundary.test.mjs +0 -107
  664. package/skills/archify/test/repair-receipt.test.mjs +0 -181
  665. package/skills/archify/test/repository-evidence-replacement.test.mjs +0 -136
  666. package/skills/archify/test/repository-evidence.test.mjs +0 -605
  667. package/skills/archify/test/repository-language-metadata.test.mjs +0 -67
  668. package/skills/archify/test/route-journey.test.mjs +0 -125
  669. package/skills/archify/test/route-probe-browser.test.mjs +0 -363
  670. package/skills/archify/test/route-probe.test.mjs +0 -114
  671. package/skills/archify/test/route-share-card.test.mjs +0 -163
  672. package/skills/archify/test/semantic-camera.test.mjs +0 -88
  673. package/skills/archify/test/semantic-flow.test.mjs +0 -90
  674. package/skills/archify/test/semantic-legend-gateway.test.mjs +0 -156
  675. package/skills/archify/test/semantic-lens-browser.test.mjs +0 -372
  676. package/skills/archify/test/semantic-lens.test.mjs +0 -103
  677. package/skills/archify/test/semantic-passport.test.mjs +0 -99
  678. package/skills/archify/test/semantic-radar.test.mjs +0 -735
  679. package/skills/archify/test/semantic-zoom.test.mjs +0 -86
  680. package/skills/archify/test/sequence-column-fit.test.mjs +0 -137
  681. package/skills/archify/test/settled-flow.test.mjs +0 -97
  682. package/skills/archify/test/share-card-export.test.mjs +0 -132
  683. package/skills/archify/test/site-language-continuity.test.mjs +0 -569
  684. package/skills/archify/test/site-language-integration.mjs +0 -9
  685. package/skills/archify/test/skill-metadata.test.mjs +0 -84
  686. package/skills/archify/test/stable-update-manifest.test.mjs +0 -167
  687. package/skills/archify/test/start-page.test.mjs +0 -312
  688. package/skills/archify/test/story-beat-navigator.test.mjs +0 -103
  689. package/skills/archify/test/story-carrier.test.mjs +0 -90
  690. package/skills/archify/test/story-director-strip.test.mjs +0 -91
  691. package/skills/archify/test/story-follow-camera.test.mjs +0 -103
  692. package/skills/archify/test/story-horizon.test.mjs +0 -91
  693. package/skills/archify/test/story-moment-link.test.mjs +0 -93
  694. package/skills/archify/test/story-shelf.test.mjs +0 -84
  695. package/skills/archify/test/story-trail.test.mjs +0 -81
  696. package/skills/archify/test/toolbar-polish.test.mjs +0 -57
  697. package/skills/archify/test/update-contract.test.mjs +0 -133
  698. package/skills/archify/test/update-notifier.test.mjs +0 -3317
  699. package/skills/archify/test/v1-compatibility.test.mjs +0 -329
  700. package/skills/archify/test/vertical-edge.test.mjs +0 -68
  701. package/skills/archify/test/viewer-camera-browser.test.mjs +0 -395
  702. package/skills/archify/test/viewer-chrome-layout.test.mjs +0 -1104
  703. package/skills/archify/test/viewer-identifiers-browser.test.mjs +0 -135
  704. package/skills/archify/test/visual-check.test.mjs +0 -400
  705. package/skills/archify/test/webm-artifact.smoke.mjs +0 -1855
  706. package/skills/archify/test/workflow-compiler-hard-contract.test.mjs +0 -2136
  707. package/skills/archify/test/workflow-compiler.test.mjs +0 -1163
  708. package/skills/archify/test/workflow-migration.test.mjs +0 -620
  709. package/skills/archify/test/workflow-semantic-contract.test.mjs +0 -131
  710. /package/{skills/archify/THIRD_PARTY_NOTICES.md → dist/phoenix-diagram/THIRD-PARTY-LICENSES/0b7fb59365dc21c93b0f62526d74ee913aeec9ae532c112bc76afa7edcd56e14.txt} +0 -0
  711. /package/{skills/archify/LICENSE → dist/phoenix-diagram/THIRD-PARTY-LICENSES/b799ab081703e7821ae5096d2c1abdf14bbdc75e8ea1c4045998c36ed6db9706.txt} +0 -0
  712. /package/{skills/archify/assets/JetBrainsMono-OFL.txt → dist/phoenix-diagram/THIRD-PARTY-LICENSES/c1ab7c666206842a02b35b30770dac0d7a10156ed401c9defc3f02a754d89e90.txt} +0 -0
  713. /package/{skills/archify/examples/agent-tool-call.workflow.json → dist/phoenix-diagram/bundle/examples/legacy.agent-tool-call.workflow.json} +0 -0
  714. /package/{skills/archify/examples/async-job-roundtrip.sequence.json → dist/phoenix-diagram/bundle/examples/legacy.async-job-roundtrip.sequence.json} +0 -0
  715. /package/{skills/archify/examples/deployment-release.lifecycle.json → dist/phoenix-diagram/bundle/examples/legacy.deployment-release.lifecycle.json} +0 -0
  716. /package/{skills/archify/examples/product-analytics.dataflow.json → dist/phoenix-diagram/bundle/examples/legacy.product-analytics.dataflow.json} +0 -0
  717. /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
- });