failproofai 1.0.0-beta.2 → 1.0.0-beta.4

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 (234) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  17. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  39. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  40. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  41. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/index.html +1 -1
  44. package/.next/standalone/.next/server/app/index.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  47. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  48. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  49. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  50. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  51. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  55. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
  56. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  57. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  60. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  61. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  65. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  70. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  71. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  72. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
  76. package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  89. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
  90. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
  91. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
  98. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0oy_-36.js → node_modules_html-to-image_es_index_1q9xxca.js} +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  117. package/.next/standalone/.next/server/pages/404.html +1 -1
  118. package/.next/standalone/.next/server/pages/500.html +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
  121. package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
  124. package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
  127. package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
  128. package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
  129. package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
  130. package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
  132. package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
  135. package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
  136. package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
  137. package/.next/standalone/Cargo.lock +1992 -198
  138. package/.next/standalone/Cargo.toml +1 -1
  139. package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
  140. package/.next/standalone/app/components/pause-notices.tsx +111 -0
  141. package/.next/standalone/app/policies/hooks-client.tsx +81 -6
  142. package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
  143. package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
  144. package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
  145. package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
  146. package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
  147. package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
  148. package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
  149. package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
  150. package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
  151. package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
  152. package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
  153. package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
  154. package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
  155. package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
  156. package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
  157. package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
  158. package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
  159. package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
  160. package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
  161. package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
  162. package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
  163. package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
  164. package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
  165. package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
  166. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
  167. package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
  168. package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
  169. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
  170. package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
  171. package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
  172. package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
  173. package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
  174. package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
  175. package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
  176. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
  177. package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
  178. package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
  179. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
  180. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
  181. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
  182. package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
  183. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
  184. package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
  185. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
  186. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
  187. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
  188. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
  189. package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
  190. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
  191. package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
  192. package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
  193. package/.next/standalone/package.json +15 -9
  194. package/.next/standalone/server.js +1 -1
  195. package/bin/failproofai.mjs +129 -0
  196. package/dist/cli.mjs +2255 -779
  197. package/dist/worker.mjs +365 -52
  198. package/package.json +15 -9
  199. package/scripts/build-daemon-packages.mjs +210 -0
  200. package/scripts/daemon-platforms.mjs +42 -0
  201. package/scripts/prune-standalone.mjs +6 -0
  202. package/scripts/publish-aliases.mjs +62 -41
  203. package/src/hooks/builtin-policies.ts +139 -0
  204. package/src/hooks/cloud-connection.ts +205 -0
  205. package/src/hooks/cloud-enrollment-cli.ts +227 -0
  206. package/src/hooks/cloud-enrollment.ts +173 -0
  207. package/src/hooks/cloud-managed-policies.ts +138 -0
  208. package/src/hooks/collector-config.ts +226 -0
  209. package/src/hooks/configure-wizard.ts +221 -5
  210. package/src/hooks/custom-hooks-loader.ts +69 -25
  211. package/src/hooks/daemon-client.ts +18 -0
  212. package/src/hooks/daemon-download.ts +137 -27
  213. package/src/hooks/daemon-service.ts +23 -8
  214. package/src/hooks/handler.ts +144 -6
  215. package/src/hooks/hook-activity-store.ts +50 -0
  216. package/src/hooks/loader-utils.ts +6 -4
  217. package/src/hooks/policy-types.ts +35 -0
  218. package/src/hooks/session-pause-cli.ts +157 -0
  219. package/src/hooks/session-pause.ts +236 -0
  220. package/src/hooks/tui.ts +115 -0
  221. package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
  222. package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
  223. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
  225. package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
  226. package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
  227. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
  228. package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
  229. package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
  230. package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
  231. package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
  232. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
  233. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
  234. /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
@@ -0,0 +1,290 @@
1
+ //! Contract tests for the collector supervisor.
2
+ //!
3
+ //! These are the tests that justify running collection inside the daemon at
4
+ //! all. `failproofai` fails closed — an unreachable daemon denies every tool
5
+ //! call on the machine — so "a collector bug cannot take the daemon down" is
6
+ //! not a nice property, it is the precondition for the whole design. Each test
7
+ //! below pins one half of that claim.
8
+
9
+ use std::sync::Arc;
10
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
11
+ use std::time::{Duration, Instant};
12
+
13
+ use fpai_collect::{TaskError, TaskSpec, spawn_supervised};
14
+
15
+ /// Poll `cond` until true or `budget` elapses. Returns whether it became true.
16
+ /// Used instead of a fixed sleep so the tests are neither flaky nor slower
17
+ /// than they need to be.
18
+ fn wait_until(budget: Duration, mut cond: impl FnMut() -> bool) -> bool {
19
+ let deadline = Instant::now() + budget;
20
+ while Instant::now() < deadline {
21
+ if cond() {
22
+ return true;
23
+ }
24
+ std::thread::sleep(Duration::from_millis(10));
25
+ }
26
+ cond()
27
+ }
28
+
29
+ #[test]
30
+ fn no_tasks_means_no_thread_and_no_runtime() {
31
+ // The normal state on a machine that has not opted in to collection. It
32
+ // must cost nothing at all — not an idle thread, not a Tokio runtime.
33
+ let shutdown = Arc::new(AtomicBool::new(false));
34
+ let handle = spawn_supervised(vec![], shutdown);
35
+ assert!(
36
+ handle.is_none(),
37
+ "an empty task list must not start a runtime"
38
+ );
39
+ }
40
+
41
+ #[test]
42
+ fn a_panicking_task_is_contained_and_restarted() {
43
+ // The core claim. A transform that panics on some agent's malformed log
44
+ // line must not unwind into the runtime, must not abort the process, and
45
+ // must be tried again rather than silently dropped.
46
+ let shutdown = Arc::new(AtomicBool::new(false));
47
+ let starts = Arc::new(AtomicUsize::new(0));
48
+
49
+ let s = starts.clone();
50
+ let handle = spawn_supervised(
51
+ vec![TaskSpec::new("panicky", move |_sd| {
52
+ let s = s.clone();
53
+ async move {
54
+ s.fetch_add(1, Ordering::SeqCst);
55
+ panic!("simulated transform bug");
56
+ }
57
+ })],
58
+ shutdown.clone(),
59
+ )
60
+ .expect("supervisor should start");
61
+
62
+ // Backoff starts at 1s, so a second start proves a real restart happened
63
+ // rather than the first attempt being counted twice.
64
+ assert!(
65
+ wait_until(Duration::from_secs(5), || starts.load(Ordering::SeqCst)
66
+ >= 2),
67
+ "a panicking task must be restarted, got {} start(s)",
68
+ starts.load(Ordering::SeqCst)
69
+ );
70
+
71
+ let m = handle.metrics();
72
+ assert!(
73
+ m.panics.load(Ordering::Relaxed) >= 1,
74
+ "the panic must be counted"
75
+ );
76
+ assert_eq!(
77
+ m.failures.load(Ordering::Relaxed),
78
+ 0,
79
+ "a panic is not an Err — they are counted separately so a bug is distinguishable from a flaky environment"
80
+ );
81
+
82
+ assert!(handle.join_with_flush(Duration::from_secs(5)));
83
+ }
84
+
85
+ #[test]
86
+ fn a_panic_in_one_task_does_not_disturb_another() {
87
+ // Isolation between tasks, not just between the collector and the daemon.
88
+ // One badly-behaved source must not stop the others from shipping.
89
+ let shutdown = Arc::new(AtomicBool::new(false));
90
+ let healthy_ticks = Arc::new(AtomicUsize::new(0));
91
+
92
+ let ticks = healthy_ticks.clone();
93
+ let handle = spawn_supervised(
94
+ vec![
95
+ TaskSpec::new("panicky", |_sd| async {
96
+ panic!("bad source");
97
+ }),
98
+ TaskSpec::new("healthy", move |sd| {
99
+ let ticks = ticks.clone();
100
+ // Shaped like a real source: loop until asked to stop, and
101
+ // use the interruptible sleep so shutdown is prompt.
102
+ async move {
103
+ while !sd.is_set() {
104
+ ticks.fetch_add(1, Ordering::SeqCst);
105
+ sd.sleep(Duration::from_millis(10)).await;
106
+ }
107
+ Ok(())
108
+ }
109
+ }),
110
+ ],
111
+ shutdown.clone(),
112
+ )
113
+ .expect("supervisor should start");
114
+
115
+ assert!(
116
+ wait_until(Duration::from_secs(5), || healthy_ticks
117
+ .load(Ordering::SeqCst)
118
+ >= 5),
119
+ "the healthy task must keep running while its sibling panics"
120
+ );
121
+
122
+ assert!(handle.join_with_flush(Duration::from_secs(5)));
123
+ }
124
+
125
+ #[test]
126
+ fn a_failing_task_is_restarted_and_counted_as_a_failure_not_a_panic() {
127
+ let shutdown = Arc::new(AtomicBool::new(false));
128
+ let starts = Arc::new(AtomicUsize::new(0));
129
+
130
+ let s = starts.clone();
131
+ let handle = spawn_supervised(
132
+ vec![TaskSpec::new("flaky", move |_sd| {
133
+ let s = s.clone();
134
+ async move {
135
+ s.fetch_add(1, Ordering::SeqCst);
136
+ Err(TaskError::from("upstream refused the connection"))
137
+ }
138
+ })],
139
+ shutdown.clone(),
140
+ )
141
+ .expect("supervisor should start");
142
+
143
+ assert!(
144
+ wait_until(Duration::from_secs(5), || starts.load(Ordering::SeqCst)
145
+ >= 2),
146
+ "a failing task must be restarted"
147
+ );
148
+
149
+ let m = handle.metrics();
150
+ assert!(m.failures.load(Ordering::Relaxed) >= 1);
151
+ assert_eq!(
152
+ m.panics.load(Ordering::Relaxed),
153
+ 0,
154
+ "an Err is the environment, not a bug — it must not inflate the panic count"
155
+ );
156
+
157
+ assert!(handle.join_with_flush(Duration::from_secs(5)));
158
+ }
159
+
160
+ #[test]
161
+ fn a_task_that_finishes_cleanly_is_not_restarted() {
162
+ // A one-shot task (a backfill pass, say) returning Ok is done. Restarting
163
+ // it would turn "finished" into an infinite loop.
164
+ let shutdown = Arc::new(AtomicBool::new(false));
165
+ let starts = Arc::new(AtomicUsize::new(0));
166
+
167
+ let s = starts.clone();
168
+ let handle = spawn_supervised(
169
+ vec![TaskSpec::new("oneshot", move |_sd| {
170
+ let s = s.clone();
171
+ async move {
172
+ s.fetch_add(1, Ordering::SeqCst);
173
+ Ok(())
174
+ }
175
+ })],
176
+ shutdown.clone(),
177
+ )
178
+ .expect("supervisor should start");
179
+
180
+ // Well past the 1s backoff a restart would have used.
181
+ std::thread::sleep(Duration::from_millis(2500));
182
+ assert_eq!(
183
+ starts.load(Ordering::SeqCst),
184
+ 1,
185
+ "a completed task must run exactly once"
186
+ );
187
+
188
+ assert!(handle.join_with_flush(Duration::from_secs(5)));
189
+ }
190
+
191
+ #[test]
192
+ fn the_daemons_shutdown_flag_stops_the_collector() {
193
+ // The collector observes the same flag the socket server does, so one
194
+ // SIGTERM stops both. If this regressed, `systemctl stop` would hang until
195
+ // its own timeout and then SIGKILL, losing whatever was buffered.
196
+ let shutdown = Arc::new(AtomicBool::new(false));
197
+
198
+ let handle = spawn_supervised(
199
+ vec![TaskSpec::new("long-runner", |sd| async move {
200
+ sd.sleep(Duration::from_secs(3600)).await;
201
+ Ok(())
202
+ })],
203
+ shutdown.clone(),
204
+ )
205
+ .expect("supervisor should start");
206
+
207
+ shutdown.store(true, Ordering::Relaxed);
208
+
209
+ let drained = handle.join_with_flush(Duration::from_secs(5));
210
+ assert!(
211
+ drained,
212
+ "the collector must stop when the daemon's shutdown flag is set"
213
+ );
214
+ }
215
+
216
+ #[test]
217
+ fn shutdown_interrupts_a_backoff_rather_than_serving_it_out() {
218
+ // A task failing repeatedly climbs to a 60s backoff. Process exit must not
219
+ // wait for that: without an interruptible sleep, stopping the daemon while
220
+ // a source was backing off would block for up to a minute.
221
+ let shutdown = Arc::new(AtomicBool::new(false));
222
+ let starts = Arc::new(AtomicUsize::new(0));
223
+
224
+ let s = starts.clone();
225
+ let handle = spawn_supervised(
226
+ vec![TaskSpec::new("always-fails", move |_sd| {
227
+ let s = s.clone();
228
+ async move {
229
+ s.fetch_add(1, Ordering::SeqCst);
230
+ Err(TaskError::from("nope"))
231
+ }
232
+ })],
233
+ shutdown.clone(),
234
+ )
235
+ .expect("supervisor should start");
236
+
237
+ // Let it fail once and enter its backoff.
238
+ assert!(wait_until(Duration::from_secs(2), || starts
239
+ .load(Ordering::SeqCst)
240
+ >= 1));
241
+
242
+ let t0 = Instant::now();
243
+ shutdown.store(true, Ordering::Relaxed);
244
+ let drained = handle.join_with_flush(Duration::from_secs(5));
245
+ let elapsed = t0.elapsed();
246
+
247
+ assert!(drained, "must drain");
248
+ assert!(
249
+ elapsed < Duration::from_millis(1500),
250
+ "shutdown waited out the backoff instead of interrupting it: took {elapsed:?}"
251
+ );
252
+ }
253
+
254
+ #[test]
255
+ fn join_with_flush_gives_up_on_a_wedged_task_instead_of_hanging() {
256
+ // The bound we actually promise. A task that ignores shutdown — stuck in a
257
+ // blocking syscall, say — delays process exit by the flush budget and no
258
+ // more. Hanging here is precisely the failure this module exists to
259
+ // prevent, because the daemon would never exit and the service manager
260
+ // would SIGKILL it mid-write.
261
+ let shutdown = Arc::new(AtomicBool::new(false));
262
+
263
+ let handle = spawn_supervised(
264
+ vec![TaskSpec::new("wedged", |_sd| async {
265
+ // `spawn_blocking` is not cancellable, which is exactly the shape
266
+ // of a real wedge (a synchronous read on a stalled filesystem).
267
+ tokio::task::spawn_blocking(|| std::thread::sleep(Duration::from_secs(30)))
268
+ .await
269
+ .ok();
270
+ Ok(())
271
+ })],
272
+ shutdown.clone(),
273
+ )
274
+ .expect("supervisor should start");
275
+
276
+ std::thread::sleep(Duration::from_millis(200));
277
+
278
+ let t0 = Instant::now();
279
+ let drained = handle.join_with_flush(Duration::from_millis(600));
280
+ let elapsed = t0.elapsed();
281
+
282
+ assert!(
283
+ !drained,
284
+ "a wedged task should report that it did not drain"
285
+ );
286
+ assert!(
287
+ elapsed < Duration::from_secs(3),
288
+ "join must respect its budget rather than waiting for the wedged task: took {elapsed:?}"
289
+ );
290
+ }