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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +1 -1
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +30 -18
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0nb0syg._.js → [root-of-the-server]__0cmdyn_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ath6v_._.js → [root-of-the-server]__0ls0gsr._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1psjybo._.js → [root-of-the-server]__0sdrz-j._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__170799-._.js → [root-of-the-server]__0spkm68._.js} +11 -11
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0114ewg._.js → [root-of-the-server]__1j4u3yd._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- 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
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +32 -20
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 06jakt_xhoolk.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/0oe2-hp0_2tzs.js +6 -0
- package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 0ue9ak_xexiqh.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/1fw3pxobk_otl.js +1 -0
- package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
- package/.next/standalone/.next/static/chunks/2c90h1g6x8z8y.js +1 -0
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 2zb3m1tzq6h98.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 31cqiv_cpgj9m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 3h5_3u46gu9jx.js} +1 -1
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 43a1ywqsg9bq9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-1lsv92x7klwrn.js → turbopack-3diub4ctvn1n0.js} +1 -1
- package/.next/standalone/Cargo.lock +1992 -198
- package/.next/standalone/Cargo.toml +1 -1
- package/.next/standalone/app/actions/get-active-pauses.ts +16 -0
- package/.next/standalone/app/components/pause-notices.tsx +111 -0
- package/.next/standalone/app/policies/hooks-client.tsx +81 -6
- package/.next/standalone/crates/CLOUD_POLICIES.md +136 -0
- package/.next/standalone/crates/failproofaid/Cargo.toml +7 -0
- package/.next/standalone/crates/failproofaid/src/cloud_client.rs +484 -0
- package/.next/standalone/crates/failproofaid/src/cloud_policies.rs +921 -0
- package/.next/standalone/crates/failproofaid/src/main.rs +452 -3
- package/.next/standalone/crates/failproofaid/src/paths.rs +29 -0
- package/.next/standalone/crates/fpai-collect/Cargo.toml +43 -0
- package/.next/standalone/crates/fpai-collect/src/config.rs +412 -0
- package/.next/standalone/crates/fpai-collect/src/cursor.rs +520 -0
- package/.next/standalone/crates/fpai-collect/src/delivery.rs +472 -0
- package/.next/standalone/crates/fpai-collect/src/filetail.rs +484 -0
- package/.next/standalone/crates/fpai-collect/src/health.rs +369 -0
- package/.next/standalone/crates/fpai-collect/src/lib.rs +34 -0
- package/.next/standalone/crates/fpai-collect/src/redact.rs +516 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/mod.rs +279 -0
- package/.next/standalone/crates/fpai-collect/src/sources/claude/transform.rs +619 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/mod.rs +156 -0
- package/.next/standalone/crates/fpai-collect/src/sources/codex/transform.rs +715 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/mod.rs +158 -0
- package/.next/standalone/crates/fpai-collect/src/sources/copilot/transform.rs +512 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/mod.rs +511 -0
- package/.next/standalone/crates/fpai-collect/src/sources/goose/transform.rs +586 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/mod.rs +318 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hermes/transform.rs +579 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +310 -0
- package/.next/standalone/crates/fpai-collect/src/sources/hooks/transform.rs +448 -0
- package/.next/standalone/crates/fpai-collect/src/sources/mod.rs +23 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/mod.rs +176 -0
- package/.next/standalone/crates/fpai-collect/src/sources/openclaw/transform.rs +500 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/mod.rs +315 -0
- package/.next/standalone/crates/fpai-collect/src/sources/opencode/transform.rs +493 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/mod.rs +201 -0
- package/.next/standalone/crates/fpai-collect/src/sources/pi/transform.rs +533 -0
- package/.next/standalone/crates/fpai-collect/src/spool.rs +458 -0
- package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +387 -0
- package/.next/standalone/crates/fpai-collect/src/supervisor.rs +423 -0
- package/.next/standalone/crates/fpai-collect/src/uploader.rs +600 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +738 -0
- package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +393 -0
- package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +975 -0
- package/.next/standalone/crates/fpai-collect/tests/config.rs +286 -0
- package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1019 -0
- package/.next/standalone/crates/fpai-collect/tests/delivery.rs +281 -0
- package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1008 -0
- package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1129 -0
- package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +621 -0
- package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +854 -0
- package/.next/standalone/crates/fpai-collect/tests/opencode_source.rs +1478 -0
- package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1191 -0
- package/.next/standalone/crates/fpai-collect/tests/supervisor.rs +290 -0
- package/.next/standalone/crates/fpai-collect/tests/uploader.rs +506 -0
- package/.next/standalone/package.json +15 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2255 -779
- package/dist/worker.mjs +365 -52
- package/package.json +15 -9
- package/scripts/build-daemon-packages.mjs +210 -0
- package/scripts/daemon-platforms.mjs +42 -0
- package/scripts/prune-standalone.mjs +6 -0
- package/scripts/publish-aliases.mjs +62 -41
- package/src/hooks/builtin-policies.ts +139 -0
- package/src/hooks/cloud-connection.ts +205 -0
- package/src/hooks/cloud-enrollment-cli.ts +227 -0
- package/src/hooks/cloud-enrollment.ts +173 -0
- package/src/hooks/cloud-managed-policies.ts +138 -0
- package/src/hooks/collector-config.ts +226 -0
- package/src/hooks/configure-wizard.ts +221 -5
- package/src/hooks/custom-hooks-loader.ts +69 -25
- package/src/hooks/daemon-client.ts +18 -0
- package/src/hooks/daemon-download.ts +137 -27
- package/src/hooks/daemon-service.ts +23 -8
- package/src/hooks/handler.ts +144 -6
- package/src/hooks/hook-activity-store.ts +50 -0
- package/src/hooks/loader-utils.ts +6 -4
- package/src/hooks/policy-types.ts +35 -0
- package/src/hooks/session-pause-cli.ts +157 -0
- package/src/hooks/session-pause.ts +236 -0
- package/src/hooks/tui.ts +115 -0
- package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
- package/.next/standalone/.next/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/31feojxhjyjpl.js +0 -1
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/3s7myk_co0gbh.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → StV6Lay7Q1cU4ysYCFJmH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
//! Uploader behaviour against a real HTTP server.
|
|
2
|
+
//!
|
|
3
|
+
//! The unit tests in the module cover the pure parts (filename retry state,
|
|
4
|
+
//! line splitting). These cover the parts that only show up over the wire, and
|
|
5
|
+
//! each one guards a way of losing data silently rather than loudly.
|
|
6
|
+
|
|
7
|
+
use std::fs;
|
|
8
|
+
use std::path::{Path, PathBuf};
|
|
9
|
+
use std::sync::atomic::Ordering;
|
|
10
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
11
|
+
|
|
12
|
+
use fpai_collect::Uploader;
|
|
13
|
+
use wiremock::matchers::{header, method, path};
|
|
14
|
+
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
15
|
+
|
|
16
|
+
fn tmpdir(name: &str) -> PathBuf {
|
|
17
|
+
let d = std::env::temp_dir().join(format!(
|
|
18
|
+
"fpai-up-{}-{}-{}",
|
|
19
|
+
name,
|
|
20
|
+
std::process::id(),
|
|
21
|
+
SystemTime::now()
|
|
22
|
+
.duration_since(UNIX_EPOCH)
|
|
23
|
+
.unwrap()
|
|
24
|
+
.as_nanos()
|
|
25
|
+
));
|
|
26
|
+
fs::create_dir_all(&d).unwrap();
|
|
27
|
+
d
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn write_batch(dir: &Path, name: &str, lines: usize) -> PathBuf {
|
|
31
|
+
let p = dir.join(name);
|
|
32
|
+
let body: String = (0..lines)
|
|
33
|
+
.map(|i| format!("{{\"type\":\"tool_use\",\"n\":{i}}}\n"))
|
|
34
|
+
.collect();
|
|
35
|
+
fs::write(&p, body).unwrap();
|
|
36
|
+
p
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Fast retries so a test does not wait out a real backoff.
|
|
40
|
+
fn uploader(server: &MockServer, failed: &Path) -> Uploader {
|
|
41
|
+
Uploader::new(
|
|
42
|
+
format!("{}/events", server.uri()),
|
|
43
|
+
"test-key".into(),
|
|
44
|
+
failed.to_path_buf(),
|
|
45
|
+
)
|
|
46
|
+
.unwrap()
|
|
47
|
+
.with_retry_base(Duration::from_millis(1))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn parked(dir: &Path) -> Vec<String> {
|
|
51
|
+
let mut v: Vec<String> = fs::read_dir(dir)
|
|
52
|
+
.map(|rd| {
|
|
53
|
+
rd.filter_map(|e| e.ok())
|
|
54
|
+
.map(|e| e.file_name().to_string_lossy().to_string())
|
|
55
|
+
.collect()
|
|
56
|
+
})
|
|
57
|
+
.unwrap_or_default();
|
|
58
|
+
v.sort();
|
|
59
|
+
v
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[tokio::test]
|
|
63
|
+
async fn a_2xx_with_an_accepting_ack_deletes_the_batch() {
|
|
64
|
+
let server = MockServer::start().await;
|
|
65
|
+
Mock::given(method("POST"))
|
|
66
|
+
.and(path("/events"))
|
|
67
|
+
.and(header("content-type", "application/x-ndjson"))
|
|
68
|
+
.and(header("authorization", "Bearer test-key"))
|
|
69
|
+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
70
|
+
"accepted": 3, "skipped": 0
|
|
71
|
+
})))
|
|
72
|
+
.mount(&server)
|
|
73
|
+
.await;
|
|
74
|
+
|
|
75
|
+
let spool = tmpdir("ok-spool");
|
|
76
|
+
let failed = tmpdir("ok-failed");
|
|
77
|
+
let batch = write_batch(&spool, "hooks-s-1-0.jsonl", 3);
|
|
78
|
+
|
|
79
|
+
let up = uploader(&server, &failed);
|
|
80
|
+
up.upload_file(&batch).await.unwrap();
|
|
81
|
+
|
|
82
|
+
assert!(!batch.exists(), "a delivered batch must be deleted");
|
|
83
|
+
assert!(parked(&failed).is_empty());
|
|
84
|
+
assert_eq!(up.metrics().accepted_total.load(Ordering::Relaxed), 3);
|
|
85
|
+
assert!(up.metrics().last_ok_ts.load(Ordering::Relaxed) > 0);
|
|
86
|
+
|
|
87
|
+
fs::remove_dir_all(&spool).ok();
|
|
88
|
+
fs::remove_dir_all(&failed).ok();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[tokio::test]
|
|
92
|
+
async fn a_200_that_stored_nothing_is_counted_as_fully_skipped() {
|
|
93
|
+
// The failure this exists for: a systematically malformed transform gets
|
|
94
|
+
// HTTP 200 for every batch while the server stores none of it. Without
|
|
95
|
+
// reading the ack body that is indistinguishable from working perfectly.
|
|
96
|
+
let server = MockServer::start().await;
|
|
97
|
+
Mock::given(method("POST"))
|
|
98
|
+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
99
|
+
"accepted": 0, "skipped": 5
|
|
100
|
+
})))
|
|
101
|
+
.mount(&server)
|
|
102
|
+
.await;
|
|
103
|
+
|
|
104
|
+
let spool = tmpdir("skip-spool");
|
|
105
|
+
let failed = tmpdir("skip-failed");
|
|
106
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", 5);
|
|
107
|
+
|
|
108
|
+
let up = uploader(&server, &failed);
|
|
109
|
+
up.upload_file(&batch).await.unwrap();
|
|
110
|
+
|
|
111
|
+
let m = up.metrics();
|
|
112
|
+
assert_eq!(m.batches_fully_skipped.load(Ordering::Relaxed), 1);
|
|
113
|
+
assert_eq!(m.skipped_total.load(Ordering::Relaxed), 5);
|
|
114
|
+
assert_eq!(m.accepted_total.load(Ordering::Relaxed), 0);
|
|
115
|
+
|
|
116
|
+
fs::remove_dir_all(&spool).ok();
|
|
117
|
+
fs::remove_dir_all(&failed).ok();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[tokio::test]
|
|
121
|
+
async fn a_4xx_parks_immediately_with_its_status_and_is_not_retried() {
|
|
122
|
+
let server = MockServer::start().await;
|
|
123
|
+
Mock::given(method("POST"))
|
|
124
|
+
.respond_with(ResponseTemplate::new(401))
|
|
125
|
+
.expect(1) // exactly one attempt: retrying a rotated key is pointless
|
|
126
|
+
.mount(&server)
|
|
127
|
+
.await;
|
|
128
|
+
|
|
129
|
+
let spool = tmpdir("4xx-spool");
|
|
130
|
+
let failed = tmpdir("4xx-failed");
|
|
131
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", 2);
|
|
132
|
+
|
|
133
|
+
let up = uploader(&server, &failed);
|
|
134
|
+
let err = up.upload_file(&batch).await.unwrap_err();
|
|
135
|
+
assert!(format!("{err}").contains("401"));
|
|
136
|
+
|
|
137
|
+
let files = parked(&failed);
|
|
138
|
+
assert_eq!(files.len(), 1, "got {files:?}");
|
|
139
|
+
assert!(
|
|
140
|
+
files[0].contains(".c401"),
|
|
141
|
+
"the status must be recorded so an automatic retry pass skips it: {files:?}"
|
|
142
|
+
);
|
|
143
|
+
assert!(!batch.exists(), "the batch moved rather than being copied");
|
|
144
|
+
|
|
145
|
+
fs::remove_dir_all(&spool).ok();
|
|
146
|
+
fs::remove_dir_all(&failed).ok();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[tokio::test]
|
|
150
|
+
async fn a_429_is_retried_rather_than_treated_as_definitive() {
|
|
151
|
+
// 408 and 429 are the two 4xx that mean "try again". Parking them would
|
|
152
|
+
// strand a batch the server explicitly asked us to resend.
|
|
153
|
+
let server = MockServer::start().await;
|
|
154
|
+
Mock::given(method("POST"))
|
|
155
|
+
.respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
|
|
156
|
+
.expect(5) // max_retries, not 1
|
|
157
|
+
.mount(&server)
|
|
158
|
+
.await;
|
|
159
|
+
|
|
160
|
+
let spool = tmpdir("429-spool");
|
|
161
|
+
let failed = tmpdir("429-failed");
|
|
162
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", 1);
|
|
163
|
+
|
|
164
|
+
let up = uploader(&server, &failed);
|
|
165
|
+
let err = up.upload_file(&batch).await.unwrap_err();
|
|
166
|
+
assert!(format!("{err}").contains("429"));
|
|
167
|
+
|
|
168
|
+
// Parked WITHOUT a client status, so it stays auto-retryable.
|
|
169
|
+
let files = parked(&failed);
|
|
170
|
+
assert_eq!(files.len(), 1, "got {files:?}");
|
|
171
|
+
assert!(
|
|
172
|
+
!files[0].contains(".c429"),
|
|
173
|
+
"a retryable status must not be recorded as definitive: {files:?}"
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
fs::remove_dir_all(&spool).ok();
|
|
177
|
+
fs::remove_dir_all(&failed).ok();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[tokio::test]
|
|
181
|
+
async fn a_5xx_retries_then_parks_without_a_client_status() {
|
|
182
|
+
let server = MockServer::start().await;
|
|
183
|
+
Mock::given(method("POST"))
|
|
184
|
+
.respond_with(ResponseTemplate::new(503))
|
|
185
|
+
.expect(5)
|
|
186
|
+
.mount(&server)
|
|
187
|
+
.await;
|
|
188
|
+
|
|
189
|
+
let spool = tmpdir("5xx-spool");
|
|
190
|
+
let failed = tmpdir("5xx-failed");
|
|
191
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", 1);
|
|
192
|
+
|
|
193
|
+
let up = uploader(&server, &failed);
|
|
194
|
+
let err = up.upload_file(&batch).await.unwrap_err();
|
|
195
|
+
assert!(format!("{err}").contains("503"));
|
|
196
|
+
|
|
197
|
+
let files = parked(&failed);
|
|
198
|
+
assert_eq!(files.len(), 1);
|
|
199
|
+
assert!(files[0].contains(".a1"), "attempt count encoded: {files:?}");
|
|
200
|
+
assert!(
|
|
201
|
+
!files[0].ends_with(".poison"),
|
|
202
|
+
"one failure is not poison yet"
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
fs::remove_dir_all(&spool).ok();
|
|
206
|
+
fs::remove_dir_all(&failed).ok();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[tokio::test]
|
|
210
|
+
async fn a_transient_failure_that_recovers_still_delivers() {
|
|
211
|
+
let server = MockServer::start().await;
|
|
212
|
+
// Fail twice, then succeed. Mounted newest-first by wiremock, so the
|
|
213
|
+
// limited mock is consulted before the fallback.
|
|
214
|
+
Mock::given(method("POST"))
|
|
215
|
+
.respond_with(ResponseTemplate::new(500))
|
|
216
|
+
.up_to_n_times(2)
|
|
217
|
+
.mount(&server)
|
|
218
|
+
.await;
|
|
219
|
+
Mock::given(method("POST"))
|
|
220
|
+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
221
|
+
"accepted": 1, "skipped": 0
|
|
222
|
+
})))
|
|
223
|
+
.mount(&server)
|
|
224
|
+
.await;
|
|
225
|
+
|
|
226
|
+
let spool = tmpdir("flap-spool");
|
|
227
|
+
let failed = tmpdir("flap-failed");
|
|
228
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", 1);
|
|
229
|
+
|
|
230
|
+
let up = uploader(&server, &failed);
|
|
231
|
+
up.upload_file(&batch).await.unwrap();
|
|
232
|
+
|
|
233
|
+
assert!(!batch.exists());
|
|
234
|
+
assert!(
|
|
235
|
+
parked(&failed).is_empty(),
|
|
236
|
+
"a recovered batch must not be parked"
|
|
237
|
+
);
|
|
238
|
+
assert_eq!(up.metrics().accepted_total.load(Ordering::Relaxed), 1);
|
|
239
|
+
|
|
240
|
+
fs::remove_dir_all(&spool).ok();
|
|
241
|
+
fs::remove_dir_all(&failed).ok();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[tokio::test]
|
|
245
|
+
async fn an_oversized_batch_is_split_and_every_chunk_delivered() {
|
|
246
|
+
let server = MockServer::start().await;
|
|
247
|
+
Mock::given(method("POST"))
|
|
248
|
+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
249
|
+
"accepted": 1, "skipped": 0
|
|
250
|
+
})))
|
|
251
|
+
.mount(&server)
|
|
252
|
+
.await;
|
|
253
|
+
|
|
254
|
+
let spool = tmpdir("split-spool");
|
|
255
|
+
let failed = tmpdir("split-failed");
|
|
256
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", 50);
|
|
257
|
+
|
|
258
|
+
// Cap well under the file size, forcing several requests.
|
|
259
|
+
let up = uploader(&server, &failed).with_max_upload_bytes(64);
|
|
260
|
+
up.upload_file(&batch).await.unwrap();
|
|
261
|
+
|
|
262
|
+
let requests = server.received_requests().await.unwrap();
|
|
263
|
+
assert!(
|
|
264
|
+
requests.len() > 1,
|
|
265
|
+
"expected a split, got {} request(s)",
|
|
266
|
+
requests.len()
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Splitting happens IN MEMORY. Chunks written to disk beside the original
|
|
270
|
+
// would be files the watcher had never seen, so it would post them
|
|
271
|
+
// concurrently — the same payload delivered twice.
|
|
272
|
+
let leftovers: Vec<String> = fs::read_dir(&spool)
|
|
273
|
+
.unwrap()
|
|
274
|
+
.filter_map(|e| e.ok())
|
|
275
|
+
.map(|e| e.file_name().to_string_lossy().to_string())
|
|
276
|
+
.collect();
|
|
277
|
+
assert!(
|
|
278
|
+
leftovers.is_empty(),
|
|
279
|
+
"split chunks must never hit the spool: {leftovers:?}"
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
fs::remove_dir_all(&spool).ok();
|
|
283
|
+
fs::remove_dir_all(&failed).ok();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[tokio::test]
|
|
287
|
+
async fn a_vanished_batch_is_not_an_error() {
|
|
288
|
+
// Two tasks can race for the same file; whoever loses must not report a
|
|
289
|
+
// failure for work the other already did.
|
|
290
|
+
let server = MockServer::start().await;
|
|
291
|
+
let spool = tmpdir("gone-spool");
|
|
292
|
+
let failed = tmpdir("gone-failed");
|
|
293
|
+
let up = uploader(&server, &failed);
|
|
294
|
+
|
|
295
|
+
up.upload_file(&spool.join("never-existed.jsonl"))
|
|
296
|
+
.await
|
|
297
|
+
.unwrap();
|
|
298
|
+
|
|
299
|
+
fs::remove_dir_all(&spool).ok();
|
|
300
|
+
fs::remove_dir_all(&failed).ok();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[tokio::test]
|
|
304
|
+
async fn parking_never_overwrites_an_existing_parked_batch() {
|
|
305
|
+
// A parked batch is the last copy of data the server does not have, so a
|
|
306
|
+
// name collision must not destroy one.
|
|
307
|
+
let server = MockServer::start().await;
|
|
308
|
+
Mock::given(method("POST"))
|
|
309
|
+
.respond_with(ResponseTemplate::new(401))
|
|
310
|
+
.mount(&server)
|
|
311
|
+
.await;
|
|
312
|
+
|
|
313
|
+
let spool = tmpdir("collide-spool");
|
|
314
|
+
let failed = tmpdir("collide-failed");
|
|
315
|
+
let up = uploader(&server, &failed);
|
|
316
|
+
|
|
317
|
+
for i in 0..2 {
|
|
318
|
+
let batch = write_batch(&spool, "claude-s-1-0.jsonl", i + 1);
|
|
319
|
+
let _ = up.upload_file(&batch).await;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let files = parked(&failed);
|
|
323
|
+
assert_eq!(
|
|
324
|
+
files.len(),
|
|
325
|
+
2,
|
|
326
|
+
"both parked batches must survive: {files:?}"
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
fs::remove_dir_all(&spool).ok();
|
|
330
|
+
fs::remove_dir_all(&failed).ok();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[tokio::test]
|
|
334
|
+
async fn a_batch_written_by_the_spool_writer_is_delivered_end_to_end() {
|
|
335
|
+
// The two halves have to agree on the filename shape and the NDJSON body.
|
|
336
|
+
// Testing them separately would let a change to either drift silently.
|
|
337
|
+
let server = MockServer::start().await;
|
|
338
|
+
Mock::given(method("POST"))
|
|
339
|
+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
340
|
+
"accepted": 2, "skipped": 0
|
|
341
|
+
})))
|
|
342
|
+
.mount(&server)
|
|
343
|
+
.await;
|
|
344
|
+
|
|
345
|
+
let spool = tmpdir("e2e-spool");
|
|
346
|
+
let failed = tmpdir("e2e-failed");
|
|
347
|
+
|
|
348
|
+
let mut w = fpai_collect::SpoolWriter::new(
|
|
349
|
+
spool.clone(),
|
|
350
|
+
fpai_collect::spool::DEFAULT_MAX_BATCH_BYTES,
|
|
351
|
+
"hooks",
|
|
352
|
+
"18efefd6",
|
|
353
|
+
);
|
|
354
|
+
w.push(serde_json::json!({
|
|
355
|
+
"timestamp": "2026-08-03T10:00:00.000000Z", "session_id": "s1",
|
|
356
|
+
"agent_id": "claude", "type": "hook_triggered", "hook_name": "PreToolUse"
|
|
357
|
+
}))
|
|
358
|
+
.await
|
|
359
|
+
.unwrap();
|
|
360
|
+
w.push(serde_json::json!({
|
|
361
|
+
"timestamp": "2026-08-03T10:00:00.000100Z", "session_id": "s1",
|
|
362
|
+
"agent_id": "claude", "type": "hook_completed", "hook_name": "PreToolUse"
|
|
363
|
+
}))
|
|
364
|
+
.await
|
|
365
|
+
.unwrap();
|
|
366
|
+
w.flush().await.unwrap();
|
|
367
|
+
|
|
368
|
+
let batch = fs::read_dir(&spool)
|
|
369
|
+
.unwrap()
|
|
370
|
+
.filter_map(|e| e.ok())
|
|
371
|
+
.map(|e| e.path())
|
|
372
|
+
.find(|p| fpai_collect::spool::is_batch_file(p))
|
|
373
|
+
.expect("the spool writer should have produced a batch");
|
|
374
|
+
|
|
375
|
+
let up = uploader(&server, &failed);
|
|
376
|
+
up.upload_file(&batch).await.unwrap();
|
|
377
|
+
|
|
378
|
+
assert!(!batch.exists());
|
|
379
|
+
assert!(parked(&failed).is_empty());
|
|
380
|
+
assert_eq!(up.metrics().accepted_total.load(Ordering::Relaxed), 2);
|
|
381
|
+
|
|
382
|
+
// The body the server received is exactly the NDJSON the spool wrote.
|
|
383
|
+
let reqs = server.received_requests().await.unwrap();
|
|
384
|
+
assert_eq!(reqs.len(), 1);
|
|
385
|
+
let body = String::from_utf8(reqs[0].body.clone()).unwrap();
|
|
386
|
+
assert_eq!(body.lines().count(), 2);
|
|
387
|
+
assert!(body.contains("hook_triggered") && body.contains("hook_completed"));
|
|
388
|
+
|
|
389
|
+
fs::remove_dir_all(&spool).ok();
|
|
390
|
+
fs::remove_dir_all(&failed).ok();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
// Redirect / non-ack hardening
|
|
395
|
+
//
|
|
396
|
+
// An adversarial red-team pointed a live daemon at a dashboard-lookalike
|
|
397
|
+
// (POST /events -> 307 -> /login -> 200 text/html). reqwest followed the
|
|
398
|
+
// redirect, re-POSTed the batch to the login page, parsed the HTML as a
|
|
399
|
+
// default ack, and DELETED the spool file. Cross-host, the same follow shipped
|
|
400
|
+
// full event payloads to an attacker. These pin the fix.
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
#[tokio::test]
|
|
404
|
+
async fn a_redirect_is_parked_not_followed() {
|
|
405
|
+
// The exfiltration vector: a 3xx must never be followed, or the batch (and
|
|
406
|
+
// its prompts/command text) is re-POSTed to wherever Location points.
|
|
407
|
+
let server = MockServer::start().await;
|
|
408
|
+
Mock::given(method("POST"))
|
|
409
|
+
.and(path("/events"))
|
|
410
|
+
.respond_with(ResponseTemplate::new(307).insert_header("location", "/login?next=%2Fevents"))
|
|
411
|
+
.expect(1) // exactly one POST — the redirect is NOT followed to /login
|
|
412
|
+
.mount(&server)
|
|
413
|
+
.await;
|
|
414
|
+
// If the client followed the redirect it would hit this; it must not.
|
|
415
|
+
Mock::given(method("POST"))
|
|
416
|
+
.and(path("/login"))
|
|
417
|
+
.respond_with(ResponseTemplate::new(200).set_body_string("<html>login</html>"))
|
|
418
|
+
.expect(0)
|
|
419
|
+
.mount(&server)
|
|
420
|
+
.await;
|
|
421
|
+
|
|
422
|
+
let spool = tmpdir("redir-spool");
|
|
423
|
+
let failed = tmpdir("redir-failed");
|
|
424
|
+
let batch = write_batch(&spool, "hooks-s-1-0.jsonl", 3);
|
|
425
|
+
|
|
426
|
+
let up = uploader(&server, &failed);
|
|
427
|
+
let err = up.upload_file(&batch).await.unwrap_err();
|
|
428
|
+
assert!(format!("{err}").contains("307"), "got {err}");
|
|
429
|
+
|
|
430
|
+
let files = parked(&failed);
|
|
431
|
+
assert_eq!(
|
|
432
|
+
files.len(),
|
|
433
|
+
1,
|
|
434
|
+
"the batch must be parked, not lost: {files:?}"
|
|
435
|
+
);
|
|
436
|
+
assert!(
|
|
437
|
+
files[0].contains(".c307"),
|
|
438
|
+
"the redirect status must be recorded: {files:?}"
|
|
439
|
+
);
|
|
440
|
+
assert!(
|
|
441
|
+
!batch.exists(),
|
|
442
|
+
"the batch moved to failed/, it was not deleted as delivered"
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
fs::remove_dir_all(&spool).ok();
|
|
446
|
+
fs::remove_dir_all(&failed).ok();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[tokio::test]
|
|
450
|
+
async fn a_200_that_is_not_an_ingest_ack_is_parked_not_deleted() {
|
|
451
|
+
// A login page, a proxy or a static host can answer 200. Without a numeric
|
|
452
|
+
// `accepted` it is not delivery, and treating it as such deletes the only
|
|
453
|
+
// copy of the data.
|
|
454
|
+
let server = MockServer::start().await;
|
|
455
|
+
Mock::given(method("POST"))
|
|
456
|
+
.respond_with(ResponseTemplate::new(200).set_body_string("<html>hello</html>"))
|
|
457
|
+
.expect(1)
|
|
458
|
+
.mount(&server)
|
|
459
|
+
.await;
|
|
460
|
+
|
|
461
|
+
let spool = tmpdir("noack-spool");
|
|
462
|
+
let failed = tmpdir("noack-failed");
|
|
463
|
+
let batch = write_batch(&spool, "hooks-s-1-0.jsonl", 3);
|
|
464
|
+
|
|
465
|
+
let up = uploader(&server, &failed);
|
|
466
|
+
let err = up.upload_file(&batch).await.unwrap_err();
|
|
467
|
+
assert!(format!("{err}").contains("200"), "got {err}");
|
|
468
|
+
|
|
469
|
+
let files = parked(&failed);
|
|
470
|
+
assert_eq!(files.len(), 1, "a non-ack 200 must be parked: {files:?}");
|
|
471
|
+
assert!(
|
|
472
|
+
!batch.exists(),
|
|
473
|
+
"the batch moved to failed/ rather than vanishing"
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
fs::remove_dir_all(&spool).ok();
|
|
477
|
+
fs::remove_dir_all(&failed).ok();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
#[tokio::test]
|
|
481
|
+
async fn a_200_whose_json_lacks_accepted_is_parked() {
|
|
482
|
+
// `{"status":"ok"}` parses as JSON but is not an ingest ack. The required
|
|
483
|
+
// `accepted` field is what rejects it.
|
|
484
|
+
let server = MockServer::start().await;
|
|
485
|
+
Mock::given(method("POST"))
|
|
486
|
+
.respond_with(
|
|
487
|
+
ResponseTemplate::new(200)
|
|
488
|
+
.insert_header("content-type", "application/json")
|
|
489
|
+
.set_body_string(r#"{"status":"ok"}"#),
|
|
490
|
+
)
|
|
491
|
+
.expect(1)
|
|
492
|
+
.mount(&server)
|
|
493
|
+
.await;
|
|
494
|
+
|
|
495
|
+
let spool = tmpdir("badack-spool");
|
|
496
|
+
let failed = tmpdir("badack-failed");
|
|
497
|
+
let batch = write_batch(&spool, "hooks-s-1-0.jsonl", 2);
|
|
498
|
+
|
|
499
|
+
let up = uploader(&server, &failed);
|
|
500
|
+
assert!(up.upload_file(&batch).await.is_err());
|
|
501
|
+
assert_eq!(parked(&failed).len(), 1, "must be parked");
|
|
502
|
+
assert!(!batch.exists());
|
|
503
|
+
|
|
504
|
+
fs::remove_dir_all(&spool).ok();
|
|
505
|
+
fs::remove_dir_all(&failed).ok();
|
|
506
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs",
|
|
@@ -76,19 +76,19 @@
|
|
|
76
76
|
"@anthropic-ai/sdk": "^0.115.0",
|
|
77
77
|
"@mdx-js/mdx": "^3.1.1",
|
|
78
78
|
"@tailwindcss/postcss": "^4.3.1",
|
|
79
|
-
"@tanstack/react-virtual": "^3.14.
|
|
79
|
+
"@tanstack/react-virtual": "^3.14.9",
|
|
80
80
|
"@testing-library/jest-dom": "^7.0.0",
|
|
81
81
|
"@testing-library/react": "^16.3.2",
|
|
82
82
|
"@testing-library/user-event": "^14.6.1",
|
|
83
|
-
"@types/node": "26.1.
|
|
83
|
+
"@types/node": "26.1.2",
|
|
84
84
|
"@types/react": "19.2.17",
|
|
85
85
|
"@types/react-dom": "^19.2.3",
|
|
86
|
-
"@vitejs/plugin-react": "^6.0.
|
|
86
|
+
"@vitejs/plugin-react": "^6.0.5",
|
|
87
87
|
"clsx": "^2.1.1",
|
|
88
88
|
"eslint": "^10.5.0",
|
|
89
89
|
"eslint-config-next": "^16.2.9",
|
|
90
|
-
"jsdom": "^30.0.
|
|
91
|
-
"lucide-react": "^1.
|
|
90
|
+
"jsdom": "^30.0.1",
|
|
91
|
+
"lucide-react": "^1.28.0",
|
|
92
92
|
"next": "^16.2.11",
|
|
93
93
|
"react": "^19.2.4",
|
|
94
94
|
"react-dom": "^19.2.4",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"dependencies": {
|
|
101
101
|
"html-to-image": "^1.11.13",
|
|
102
102
|
"html2canvas": "^1.4.1",
|
|
103
|
-
"posthog-node": "^5.
|
|
103
|
+
"posthog-node": "^5.47.7",
|
|
104
104
|
"sql.js": "^1.14.1",
|
|
105
105
|
"yaml": "^2.9.0"
|
|
106
106
|
},
|
|
@@ -108,8 +108,14 @@
|
|
|
108
108
|
"postcss": "8.5.23",
|
|
109
109
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
110
110
|
"vite": "8.0.16",
|
|
111
|
-
"undici": "7.
|
|
112
|
-
"brace-expansion": "5.0.
|
|
111
|
+
"undici": "7.29.0",
|
|
112
|
+
"brace-expansion": "5.0.9",
|
|
113
113
|
"sharp": "0.35.0"
|
|
114
|
+
},
|
|
115
|
+
"optionalDependencies": {
|
|
116
|
+
"@failproofai/failproofaid-linux-x64": "1.0.0-beta.4",
|
|
117
|
+
"@failproofai/failproofaid-linux-arm64": "1.0.0-beta.4",
|
|
118
|
+
"@failproofai/failproofaid-darwin-x64": "1.0.0-beta.4",
|
|
119
|
+
"@failproofai/failproofaid-darwin-arm64": "1.0.0-beta.4"
|
|
114
120
|
}
|
|
115
121
|
}
|
|
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
|
|
|
9
9
|
const hostname = process.env.HOSTNAME || '0.0.0.0'
|
|
10
10
|
|
|
11
11
|
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
|
|
12
|
-
const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.0-beta.
|
|
12
|
+
const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"1.0.0-beta.4"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"useTypeScriptCli":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
|
|
13
13
|
|
|
14
14
|
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
|
|
15
15
|
|