failproofai 1.0.0-beta.2 → 1.0.0-beta.3
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.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.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.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.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.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.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/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- 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]__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]__0qucxyj._.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]__1psjybo._.js → [root-of-the-server]__10s_z4l._.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]__0nb0syg._.js → [root-of-the-server]__1ky3im9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- 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_0jfts82.js} +1 -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/{3s7myk_co0gbh.js → 01h6lfjz1_oie.js} +1 -1
- package/.next/standalone/.next/static/chunks/{03fmihek9n986.js → 06zgzyh3wv1ig.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fi645si-uiiq.js → 0a7k_x4rp_97z.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2sbelanzvxvv0.js → 0cvd6bcdwchc_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18rvdw5v72364.js → 13-obgukzg_ke.js} +1 -1
- package/.next/standalone/.next/static/chunks/16p68cybetr31.js +1 -0
- package/.next/standalone/.next/static/chunks/{2j94uwkb4s3qr.js → 1h0_yaoq5f3yx.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3vs4ay36tjds_.js → 29g8ykjk6sx_8.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3hp8ulqknybil.js → 2rqm-klet_gbl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{31feojxhjyjpl.js → 2uvyh8y0naofj.js} +1 -1
- package/.next/standalone/.next/static/chunks/31coz11ia5vc4.js +1 -0
- package/.next/standalone/.next/static/chunks/3p-n_j80zlzi3.js +6 -0
- package/.next/standalone/.next/static/chunks/3zdwnnthucs_1.css +2 -0
- 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 +2 -2
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +129 -0
- package/dist/cli.mjs +2195 -770
- package/dist/worker.mjs +365 -52
- package/package.json +2 -2
- 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/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/static/chunks/0zbxssxh53n-3.js +0 -1
- package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +0 -2
- package/.next/standalone/.next/static/chunks/33u59vf_8xpd-.js +0 -1
- package/.next/standalone/.next/static/chunks/40k1_v4os4cw6.js +0 -6
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NWAjs0tHFyzk97gRGG09r → pM1veX5Llx4YKEaiFc8Sf}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
//! Watcher and sweeper against a real filesystem and a real HTTP server.
|
|
2
|
+
//!
|
|
3
|
+
//! The first test here is the one that would have caught a whole class of
|
|
4
|
+
//! silent failure: the spool publishes a batch by RENAMING it into place,
|
|
5
|
+
//! which Linux reports as `IN_MOVED_TO`, not as a create. A watcher subscribed
|
|
6
|
+
//! only to create events registers successfully, logs nothing, and delivers
|
|
7
|
+
//! nothing — the sweeper would quietly cover for it a minute later and the bug
|
|
8
|
+
//! would look like latency.
|
|
9
|
+
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::path::{Path, PathBuf};
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
14
|
+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
15
|
+
|
|
16
|
+
use fpai_collect::supervisor::{Shutdown, TaskSpec, spawn_supervised};
|
|
17
|
+
use fpai_collect::{Delivery, SpoolWriter, Uploader};
|
|
18
|
+
use wiremock::matchers::method;
|
|
19
|
+
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
20
|
+
|
|
21
|
+
fn tmpdir(name: &str) -> PathBuf {
|
|
22
|
+
let d = std::env::temp_dir().join(format!(
|
|
23
|
+
"fpai-dlv-{}-{}-{}",
|
|
24
|
+
name,
|
|
25
|
+
std::process::id(),
|
|
26
|
+
SystemTime::now()
|
|
27
|
+
.duration_since(UNIX_EPOCH)
|
|
28
|
+
.unwrap()
|
|
29
|
+
.as_nanos()
|
|
30
|
+
));
|
|
31
|
+
fs::create_dir_all(&d).unwrap();
|
|
32
|
+
d
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async fn ok_server() -> MockServer {
|
|
36
|
+
let s = MockServer::start().await;
|
|
37
|
+
Mock::given(method("POST"))
|
|
38
|
+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
39
|
+
"accepted": 1, "skipped": 0
|
|
40
|
+
})))
|
|
41
|
+
.mount(&s)
|
|
42
|
+
.await;
|
|
43
|
+
s
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn delivery(server: &MockServer, failed: &Path) -> Arc<Delivery> {
|
|
47
|
+
let up = Uploader::new(
|
|
48
|
+
format!("{}/events", server.uri()),
|
|
49
|
+
"k".into(),
|
|
50
|
+
failed.to_path_buf(),
|
|
51
|
+
)
|
|
52
|
+
.unwrap()
|
|
53
|
+
.with_retry_base(Duration::from_millis(1));
|
|
54
|
+
Arc::new(Delivery::new(Arc::new(up)))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Write a batch the way the spool writer does — tmp, then rename.
|
|
58
|
+
async fn publish_batch(dir: &Path, tag: &str) {
|
|
59
|
+
let mut w = SpoolWriter::new(
|
|
60
|
+
dir.to_path_buf(),
|
|
61
|
+
fpai_collect::spool::DEFAULT_MAX_BATCH_BYTES,
|
|
62
|
+
"hooks",
|
|
63
|
+
tag,
|
|
64
|
+
);
|
|
65
|
+
w.push(serde_json::json!({"type": "hook_completed", "session_id": "s"}))
|
|
66
|
+
.await
|
|
67
|
+
.unwrap();
|
|
68
|
+
w.flush().await.unwrap();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn wait_until(budget: Duration, mut cond: impl FnMut() -> bool) -> bool {
|
|
72
|
+
let deadline = Instant::now() + budget;
|
|
73
|
+
while Instant::now() < deadline {
|
|
74
|
+
if cond() {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
78
|
+
}
|
|
79
|
+
cond()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
83
|
+
async fn the_watcher_fires_on_the_rename_the_spool_writer_actually_performs() {
|
|
84
|
+
let server = ok_server().await;
|
|
85
|
+
let spool = tmpdir("watch-spool");
|
|
86
|
+
let failed = tmpdir("watch-failed");
|
|
87
|
+
let d = delivery(&server, &failed);
|
|
88
|
+
|
|
89
|
+
let shutdown = Arc::new(AtomicBool::new(false));
|
|
90
|
+
let sd = Shutdown::for_test(shutdown.clone());
|
|
91
|
+
|
|
92
|
+
let watcher = tokio::spawn(fpai_collect::delivery::watch(
|
|
93
|
+
d.clone(),
|
|
94
|
+
vec![spool.clone()],
|
|
95
|
+
sd,
|
|
96
|
+
));
|
|
97
|
+
// Let the watch register before publishing; otherwise this would pass or
|
|
98
|
+
// fail on timing rather than on behaviour.
|
|
99
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
100
|
+
|
|
101
|
+
publish_batch(&spool, "s1").await;
|
|
102
|
+
|
|
103
|
+
let spool_check = spool.clone();
|
|
104
|
+
let delivered = wait_until(Duration::from_secs(5), || {
|
|
105
|
+
fs::read_dir(&spool_check)
|
|
106
|
+
.map(|rd| rd.filter_map(|e| e.ok()).count() == 0)
|
|
107
|
+
.unwrap_or(false)
|
|
108
|
+
});
|
|
109
|
+
assert!(
|
|
110
|
+
delivered,
|
|
111
|
+
"the watcher did not deliver a renamed-in batch; \
|
|
112
|
+
on Linux the publish is IN_MOVED_TO, not a create"
|
|
113
|
+
);
|
|
114
|
+
assert_eq!(server.received_requests().await.unwrap().len(), 1);
|
|
115
|
+
|
|
116
|
+
shutdown.store(true, Ordering::Relaxed);
|
|
117
|
+
let _ = tokio::time::timeout(Duration::from_secs(3), watcher).await;
|
|
118
|
+
fs::remove_dir_all(&spool).ok();
|
|
119
|
+
fs::remove_dir_all(&failed).ok();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
123
|
+
async fn the_watcher_survives_a_spool_directory_it_cannot_watch() {
|
|
124
|
+
// A watch that fails to register must degrade to sweeper-only, not kill
|
|
125
|
+
// the task — on a filesystem without event support it would otherwise
|
|
126
|
+
// restart forever.
|
|
127
|
+
let server = ok_server().await;
|
|
128
|
+
let spool = tmpdir("degrade-spool");
|
|
129
|
+
let failed = tmpdir("degrade-failed");
|
|
130
|
+
let d = delivery(&server, &failed);
|
|
131
|
+
|
|
132
|
+
let shutdown = Arc::new(AtomicBool::new(false));
|
|
133
|
+
let sd = Shutdown::for_test(shutdown.clone());
|
|
134
|
+
|
|
135
|
+
let watcher = tokio::spawn(fpai_collect::delivery::watch(
|
|
136
|
+
d,
|
|
137
|
+
vec![
|
|
138
|
+
spool.clone(),
|
|
139
|
+
PathBuf::from("/proc/nonexistent/cannot-create"),
|
|
140
|
+
],
|
|
141
|
+
sd,
|
|
142
|
+
));
|
|
143
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
144
|
+
assert!(
|
|
145
|
+
!watcher.is_finished(),
|
|
146
|
+
"an unwatchable directory must not stop the task"
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
shutdown.store(true, Ordering::Relaxed);
|
|
150
|
+
let res = tokio::time::timeout(Duration::from_secs(3), watcher).await;
|
|
151
|
+
assert!(res.is_ok(), "the watcher must stop on shutdown");
|
|
152
|
+
fs::remove_dir_all(&spool).ok();
|
|
153
|
+
fs::remove_dir_all(&failed).ok();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
157
|
+
async fn the_sweeper_delivers_what_accumulated_while_the_daemon_was_stopped() {
|
|
158
|
+
// No filesystem event is coming for these — they were published while
|
|
159
|
+
// nothing was listening. This is why the sweeper, not the watcher, is the
|
|
160
|
+
// delivery guarantee.
|
|
161
|
+
let server = ok_server().await;
|
|
162
|
+
let spool = tmpdir("sweep-spool");
|
|
163
|
+
let failed = tmpdir("sweep-failed");
|
|
164
|
+
|
|
165
|
+
publish_batch(&spool, "old1").await;
|
|
166
|
+
publish_batch(&spool, "old2").await;
|
|
167
|
+
assert_eq!(fs::read_dir(&spool).unwrap().count(), 2);
|
|
168
|
+
|
|
169
|
+
let d = delivery(&server, &failed);
|
|
170
|
+
let shutdown = Arc::new(AtomicBool::new(false));
|
|
171
|
+
let sd = Shutdown::for_test(shutdown.clone());
|
|
172
|
+
|
|
173
|
+
// Deliver directly rather than waiting out SWEEP_MIN_AGE: the selection
|
|
174
|
+
// rules are unit-tested in the module; what matters here is that a batch
|
|
175
|
+
// with no event behind it still reaches the server.
|
|
176
|
+
for entry in fs::read_dir(&spool).unwrap() {
|
|
177
|
+
d.deliver(entry.unwrap().path()).await;
|
|
178
|
+
}
|
|
179
|
+
let _ = sd;
|
|
180
|
+
|
|
181
|
+
assert_eq!(
|
|
182
|
+
fs::read_dir(&spool).unwrap().count(),
|
|
183
|
+
0,
|
|
184
|
+
"both batches must be delivered"
|
|
185
|
+
);
|
|
186
|
+
assert_eq!(server.received_requests().await.unwrap().len(), 2);
|
|
187
|
+
|
|
188
|
+
shutdown.store(true, Ordering::Relaxed);
|
|
189
|
+
fs::remove_dir_all(&spool).ok();
|
|
190
|
+
fs::remove_dir_all(&failed).ok();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
194
|
+
async fn the_same_batch_is_never_uploaded_twice_concurrently() {
|
|
195
|
+
// The watcher and sweeper share one in-flight set precisely so this cannot
|
|
196
|
+
// happen. With per-task sets, a file the watcher is mid-upload on is
|
|
197
|
+
// invisible to a concurrent sweep and both would POST it.
|
|
198
|
+
let server = ok_server().await;
|
|
199
|
+
let spool = tmpdir("dedup-spool");
|
|
200
|
+
let failed = tmpdir("dedup-failed");
|
|
201
|
+
let d = delivery(&server, &failed);
|
|
202
|
+
|
|
203
|
+
publish_batch(&spool, "s1").await;
|
|
204
|
+
let batch = fs::read_dir(&spool)
|
|
205
|
+
.unwrap()
|
|
206
|
+
.filter_map(|e| e.ok())
|
|
207
|
+
.map(|e| e.path())
|
|
208
|
+
.next()
|
|
209
|
+
.unwrap();
|
|
210
|
+
|
|
211
|
+
// Ten concurrent attempts on the same path.
|
|
212
|
+
let mut set = tokio::task::JoinSet::new();
|
|
213
|
+
for _ in 0..10 {
|
|
214
|
+
let d = d.clone();
|
|
215
|
+
let p = batch.clone();
|
|
216
|
+
set.spawn(async move { d.deliver(p).await });
|
|
217
|
+
}
|
|
218
|
+
while set.join_next().await.is_some() {}
|
|
219
|
+
|
|
220
|
+
let n = server.received_requests().await.unwrap().len();
|
|
221
|
+
assert_eq!(
|
|
222
|
+
n, 1,
|
|
223
|
+
"the batch was POSTed {n} times; the in-flight set failed"
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
fs::remove_dir_all(&spool).ok();
|
|
227
|
+
fs::remove_dir_all(&failed).ok();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
231
|
+
async fn watcher_and_sweeper_run_under_the_supervisor_and_stop_on_shutdown() {
|
|
232
|
+
// How they actually run in the daemon: as supervised tasks observing the
|
|
233
|
+
// same shutdown flag as the socket server.
|
|
234
|
+
let server = ok_server().await;
|
|
235
|
+
let spool = tmpdir("sup-spool");
|
|
236
|
+
let failed = tmpdir("sup-failed");
|
|
237
|
+
let d = delivery(&server, &failed);
|
|
238
|
+
|
|
239
|
+
let shutdown = Arc::new(AtomicBool::new(false));
|
|
240
|
+
|
|
241
|
+
let dw = d.clone();
|
|
242
|
+
let sw = spool.clone();
|
|
243
|
+
let ds = d.clone();
|
|
244
|
+
let ss = spool.clone();
|
|
245
|
+
let fs_dir = failed.clone();
|
|
246
|
+
|
|
247
|
+
let handle = spawn_supervised(
|
|
248
|
+
vec![
|
|
249
|
+
TaskSpec::new("spool-watcher", move |sd| {
|
|
250
|
+
fpai_collect::delivery::watch(dw.clone(), vec![sw.clone()], sd)
|
|
251
|
+
}),
|
|
252
|
+
TaskSpec::new("spool-sweeper", move |sd| {
|
|
253
|
+
fpai_collect::delivery::sweep(ds.clone(), vec![ss.clone()], fs_dir.clone(), sd)
|
|
254
|
+
}),
|
|
255
|
+
],
|
|
256
|
+
shutdown.clone(),
|
|
257
|
+
)
|
|
258
|
+
.expect("supervisor should start");
|
|
259
|
+
|
|
260
|
+
tokio::time::sleep(Duration::from_millis(400)).await;
|
|
261
|
+
publish_batch(&spool, "s1").await;
|
|
262
|
+
|
|
263
|
+
let spool_check = spool.clone();
|
|
264
|
+
assert!(
|
|
265
|
+
wait_until(Duration::from_secs(5), || {
|
|
266
|
+
fs::read_dir(&spool_check)
|
|
267
|
+
.map(|rd| rd.filter_map(|e| e.ok()).count() == 0)
|
|
268
|
+
.unwrap_or(false)
|
|
269
|
+
}),
|
|
270
|
+
"a batch published while supervised was not delivered"
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
shutdown.store(true, Ordering::Relaxed);
|
|
274
|
+
assert!(
|
|
275
|
+
handle.join_with_flush(Duration::from_secs(5)),
|
|
276
|
+
"both tasks must drain on shutdown"
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
fs::remove_dir_all(&spool).ok();
|
|
280
|
+
fs::remove_dir_all(&failed).ok();
|
|
281
|
+
}
|