@creezio/observability 0.22.0

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 (209) hide show
  1. package/README.md +328 -0
  2. package/dist/.creezio-src-hash.json +4 -0
  3. package/dist/api-endpoints/http-routes.d.ts +11 -0
  4. package/dist/api-endpoints/http-routes.d.ts.map +1 -0
  5. package/dist/api-endpoints/http-routes.js +11 -0
  6. package/dist/api-endpoints/http-routes.js.map +1 -0
  7. package/dist/api-endpoints/index.d.ts +4 -0
  8. package/dist/api-endpoints/index.d.ts.map +1 -0
  9. package/dist/api-endpoints/index.js +3 -0
  10. package/dist/api-endpoints/index.js.map +1 -0
  11. package/dist/api-endpoints/registry.d.ts +66 -0
  12. package/dist/api-endpoints/registry.d.ts.map +1 -0
  13. package/dist/api-endpoints/registry.js +102 -0
  14. package/dist/api-endpoints/registry.js.map +1 -0
  15. package/dist/api-mount.d.ts +7 -0
  16. package/dist/api-mount.d.ts.map +1 -0
  17. package/dist/api-mount.js +114 -0
  18. package/dist/api-mount.js.map +1 -0
  19. package/dist/helpers.d.ts +23 -0
  20. package/dist/helpers.d.ts.map +1 -0
  21. package/dist/helpers.js +36 -0
  22. package/dist/helpers.js.map +1 -0
  23. package/dist/index.d.ts +35 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +24 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/memory-store.d.ts +3 -0
  28. package/dist/memory-store.d.ts.map +1 -0
  29. package/dist/memory-store.js +113 -0
  30. package/dist/memory-store.js.map +1 -0
  31. package/dist/ops/emit.d.ts +8 -0
  32. package/dist/ops/emit.d.ts.map +1 -0
  33. package/dist/ops/emit.js +15 -0
  34. package/dist/ops/emit.js.map +1 -0
  35. package/dist/ops/fleet-activity.d.ts +50 -0
  36. package/dist/ops/fleet-activity.d.ts.map +1 -0
  37. package/dist/ops/fleet-activity.js +123 -0
  38. package/dist/ops/fleet-activity.js.map +1 -0
  39. package/dist/ops/fleet-agent.d.ts +102 -0
  40. package/dist/ops/fleet-agent.d.ts.map +1 -0
  41. package/dist/ops/fleet-agent.js +352 -0
  42. package/dist/ops/fleet-agent.js.map +1 -0
  43. package/dist/ops/fleet-samples.d.ts +33 -0
  44. package/dist/ops/fleet-samples.d.ts.map +1 -0
  45. package/dist/ops/fleet-samples.js +223 -0
  46. package/dist/ops/fleet-samples.js.map +1 -0
  47. package/dist/ops/journal.d.ts +56 -0
  48. package/dist/ops/journal.d.ts.map +1 -0
  49. package/dist/ops/journal.js +307 -0
  50. package/dist/ops/journal.js.map +1 -0
  51. package/dist/ops/rules.d.ts +16 -0
  52. package/dist/ops/rules.d.ts.map +1 -0
  53. package/dist/ops/rules.js +98 -0
  54. package/dist/ops/rules.js.map +1 -0
  55. package/dist/ops/types.d.ts +69 -0
  56. package/dist/ops/types.d.ts.map +1 -0
  57. package/dist/ops/types.js +119 -0
  58. package/dist/ops/types.js.map +1 -0
  59. package/dist/request-logs/config.d.ts +19 -0
  60. package/dist/request-logs/config.d.ts.map +1 -0
  61. package/dist/request-logs/config.js +36 -0
  62. package/dist/request-logs/config.js.map +1 -0
  63. package/dist/request-logs/http-routes.d.ts +8 -0
  64. package/dist/request-logs/http-routes.d.ts.map +1 -0
  65. package/dist/request-logs/http-routes.js +29 -0
  66. package/dist/request-logs/http-routes.js.map +1 -0
  67. package/dist/request-logs/index.d.ts +6 -0
  68. package/dist/request-logs/index.d.ts.map +1 -0
  69. package/dist/request-logs/index.js +5 -0
  70. package/dist/request-logs/index.js.map +1 -0
  71. package/dist/request-logs/middleware.d.ts +11 -0
  72. package/dist/request-logs/middleware.d.ts.map +1 -0
  73. package/dist/request-logs/middleware.js +198 -0
  74. package/dist/request-logs/middleware.js.map +1 -0
  75. package/dist/request-logs/request-logs.d.ts +60 -0
  76. package/dist/request-logs/request-logs.d.ts.map +1 -0
  77. package/dist/request-logs/request-logs.js +260 -0
  78. package/dist/request-logs/request-logs.js.map +1 -0
  79. package/dist/schema.d.ts +3 -0
  80. package/dist/schema.d.ts.map +1 -0
  81. package/dist/schema.js +21 -0
  82. package/dist/schema.js.map +1 -0
  83. package/dist/sqlite-driver.d.ts +16 -0
  84. package/dist/sqlite-driver.d.ts.map +1 -0
  85. package/dist/sqlite-driver.js +15 -0
  86. package/dist/sqlite-driver.js.map +1 -0
  87. package/dist/sqlite-store.d.ts +12 -0
  88. package/dist/sqlite-store.d.ts.map +1 -0
  89. package/dist/sqlite-store.js +176 -0
  90. package/dist/sqlite-store.js.map +1 -0
  91. package/dist/types.d.ts +67 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +9 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/usage/adapters.d.ts +31 -0
  96. package/dist/usage/adapters.d.ts.map +1 -0
  97. package/dist/usage/adapters.js +37 -0
  98. package/dist/usage/adapters.js.map +1 -0
  99. package/dist/usage/http-routes.d.ts +23 -0
  100. package/dist/usage/http-routes.d.ts.map +1 -0
  101. package/dist/usage/http-routes.js +150 -0
  102. package/dist/usage/http-routes.js.map +1 -0
  103. package/dist/usage/index.d.ts +11 -0
  104. package/dist/usage/index.d.ts.map +1 -0
  105. package/dist/usage/index.js +7 -0
  106. package/dist/usage/index.js.map +1 -0
  107. package/dist/usage/ui-brand.d.ts +23 -0
  108. package/dist/usage/ui-brand.d.ts.map +1 -0
  109. package/dist/usage/ui-brand.js +18 -0
  110. package/dist/usage/ui-brand.js.map +1 -0
  111. package/dist/usage/usage-analytics-productivity.d.ts +73 -0
  112. package/dist/usage/usage-analytics-productivity.d.ts.map +1 -0
  113. package/dist/usage/usage-analytics-productivity.js +317 -0
  114. package/dist/usage/usage-analytics-productivity.js.map +1 -0
  115. package/dist/usage/usage-analytics-shared.d.ts +5 -0
  116. package/dist/usage/usage-analytics-shared.d.ts.map +1 -0
  117. package/dist/usage/usage-analytics-shared.js +20 -0
  118. package/dist/usage/usage-analytics-shared.js.map +1 -0
  119. package/dist/usage/usage-analytics.d.ts +127 -0
  120. package/dist/usage/usage-analytics.d.ts.map +1 -0
  121. package/dist/usage/usage-analytics.js +338 -0
  122. package/dist/usage/usage-analytics.js.map +1 -0
  123. package/dist-cjs/api-endpoints/http-routes.js +14 -0
  124. package/dist-cjs/api-endpoints/http-routes.js.map +1 -0
  125. package/dist-cjs/api-endpoints/index.js +10 -0
  126. package/dist-cjs/api-endpoints/index.js.map +1 -0
  127. package/dist-cjs/api-endpoints/registry.js +107 -0
  128. package/dist-cjs/api-endpoints/registry.js.map +1 -0
  129. package/dist-cjs/api-mount.js +117 -0
  130. package/dist-cjs/api-mount.js.map +1 -0
  131. package/dist-cjs/helpers.js +41 -0
  132. package/dist-cjs/helpers.js.map +1 -0
  133. package/dist-cjs/index.js +118 -0
  134. package/dist-cjs/index.js.map +1 -0
  135. package/dist-cjs/memory-store.js +119 -0
  136. package/dist-cjs/memory-store.js.map +1 -0
  137. package/dist-cjs/ops/emit.js +18 -0
  138. package/dist-cjs/ops/emit.js.map +1 -0
  139. package/dist-cjs/ops/fleet-activity.js +131 -0
  140. package/dist-cjs/ops/fleet-activity.js.map +1 -0
  141. package/dist-cjs/ops/fleet-agent.js +358 -0
  142. package/dist-cjs/ops/fleet-agent.js.map +1 -0
  143. package/dist-cjs/ops/fleet-samples.js +229 -0
  144. package/dist-cjs/ops/fleet-samples.js.map +1 -0
  145. package/dist-cjs/ops/journal.js +326 -0
  146. package/dist-cjs/ops/journal.js.map +1 -0
  147. package/dist-cjs/ops/rules.js +102 -0
  148. package/dist-cjs/ops/rules.js.map +1 -0
  149. package/dist-cjs/ops/types.js +126 -0
  150. package/dist-cjs/ops/types.js.map +1 -0
  151. package/dist-cjs/package.json +3 -0
  152. package/dist-cjs/request-logs/config.js +42 -0
  153. package/dist-cjs/request-logs/config.js.map +1 -0
  154. package/dist-cjs/request-logs/http-routes.js +32 -0
  155. package/dist-cjs/request-logs/http-routes.js.map +1 -0
  156. package/dist-cjs/request-logs/index.js +27 -0
  157. package/dist-cjs/request-logs/index.js.map +1 -0
  158. package/dist-cjs/request-logs/middleware.js +203 -0
  159. package/dist-cjs/request-logs/middleware.js.map +1 -0
  160. package/dist-cjs/request-logs/request-logs.js +277 -0
  161. package/dist-cjs/request-logs/request-logs.js.map +1 -0
  162. package/dist-cjs/schema.js +24 -0
  163. package/dist-cjs/schema.js.map +1 -0
  164. package/dist-cjs/sqlite-driver.js +18 -0
  165. package/dist-cjs/sqlite-driver.js.map +1 -0
  166. package/dist-cjs/sqlite-store.js +182 -0
  167. package/dist-cjs/sqlite-store.js.map +1 -0
  168. package/dist-cjs/types.js +12 -0
  169. package/dist-cjs/types.js.map +1 -0
  170. package/dist-cjs/usage/adapters.js +47 -0
  171. package/dist-cjs/usage/adapters.js.map +1 -0
  172. package/dist-cjs/usage/http-routes.js +154 -0
  173. package/dist-cjs/usage/http-routes.js.map +1 -0
  174. package/dist-cjs/usage/index.js +34 -0
  175. package/dist-cjs/usage/index.js.map +1 -0
  176. package/dist-cjs/usage/ui-brand.js +23 -0
  177. package/dist-cjs/usage/ui-brand.js.map +1 -0
  178. package/dist-cjs/usage/usage-analytics-productivity.js +321 -0
  179. package/dist-cjs/usage/usage-analytics-productivity.js.map +1 -0
  180. package/dist-cjs/usage/usage-analytics-shared.js +23 -0
  181. package/dist-cjs/usage/usage-analytics-shared.js.map +1 -0
  182. package/dist-cjs/usage/usage-analytics.js +353 -0
  183. package/dist-cjs/usage/usage-analytics.js.map +1 -0
  184. package/fleet-collector/AGENTS.md +7 -0
  185. package/fleet-collector/README.md +83 -0
  186. package/fleet-collector/configure-fleet-npm.sh +108 -0
  187. package/fleet-collector/env.mjs +174 -0
  188. package/fleet-collector/ops-api.mjs +455 -0
  189. package/fleet-collector/public/admin.html +696 -0
  190. package/fleet-collector/public/index.html +941 -0
  191. package/fleet-collector/server.mjs +564 -0
  192. package/fleet-collector/test-fleet-collector.mjs +413 -0
  193. package/fleet-collector/test-server-admin.mjs +475 -0
  194. package/package.json +109 -0
  195. package/scripts/test-api-endpoints-registry.mjs +60 -0
  196. package/ui/analytics-client.tsx +834 -0
  197. package/ui/analytics-productivity-panel.tsx +513 -0
  198. package/ui/api-endpoints-client.tsx +228 -0
  199. package/ui/index.ts +39 -0
  200. package/ui/primitives/badge.tsx +37 -0
  201. package/ui/primitives/button.tsx +44 -0
  202. package/ui/primitives/card.tsx +44 -0
  203. package/ui/primitives/cn.ts +6 -0
  204. package/ui/primitives/input.tsx +19 -0
  205. package/ui/primitives/tabs.tsx +51 -0
  206. package/ui/request-logs-client.tsx +327 -0
  207. package/ui/session-usage-analytics-provider.tsx +28 -0
  208. package/ui/usage-analytics-client.ts +479 -0
  209. package/ui/usage-analytics-provider.tsx +84 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Tests locaux du fleet-collector kit (spawn serveur éphémère).
3
+ * Env neutre CREEZIO_* — pas de domaine marque.
4
+ */
5
+ import http from "node:http";
6
+ import { spawn } from "node:child_process";
7
+ import path from "node:path";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import { fileURLToPath } from "node:url";
11
+ import assert from "node:assert/strict";
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const PORT = 18765;
15
+ const INGEST = "testingesttoken0123456789abcdef01234567";
16
+ const OPS = "testopstoken0123456789abcdef0123456789ab";
17
+ const OPS_USER = "ops";
18
+ const OPS_PASS = "ops";
19
+ const UI_TITLE = "Fleet Test";
20
+ const TUNNEL_SUFFIX = "example.test";
21
+ const BASIC = "Basic " + Buffer.from(`${OPS_USER}:${OPS_PASS}`).toString("base64");
22
+ const DIR = fs.mkdtempSync(path.join(os.tmpdir(), "creezio-fleet-"));
23
+
24
+ function req(method, urlPath, body, headers = {}) {
25
+ return new Promise((resolve, reject) => {
26
+ const r = http.request(
27
+ {
28
+ hostname: "127.0.0.1",
29
+ port: PORT,
30
+ path: urlPath,
31
+ method,
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ ...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
35
+ ...headers,
36
+ },
37
+ },
38
+ (res) => {
39
+ const chunks = [];
40
+ res.on("data", (c) => chunks.push(c));
41
+ res.on("end", () => {
42
+ const raw = Buffer.concat(chunks).toString("utf8");
43
+ let json = null;
44
+ try {
45
+ json = raw ? JSON.parse(raw) : null;
46
+ } catch {
47
+ json = raw;
48
+ }
49
+ resolve({ status: res.statusCode, json, raw });
50
+ });
51
+ },
52
+ );
53
+ r.on("error", reject);
54
+ if (body) r.write(body);
55
+ r.end();
56
+ });
57
+ }
58
+
59
+ const child = spawn(process.execPath, [path.join(__dirname, "server.mjs")], {
60
+ env: {
61
+ ...process.env,
62
+ CREEZIO_FLEET_PORT: String(PORT),
63
+ CREEZIO_FLEET_INGEST_TOKEN: INGEST,
64
+ CREEZIO_FLEET_OPS_TOKEN: OPS,
65
+ CREEZIO_FLEET_OPS_USER: OPS_USER,
66
+ CREEZIO_FLEET_OPS_PASS: OPS_PASS,
67
+ CREEZIO_FLEET_DIR: DIR,
68
+ CREEZIO_FLEET_UI_TITLE: UI_TITLE,
69
+ CREEZIO_FLEET_UI_MARK: "FT",
70
+ CREEZIO_FLEET_UI_HOME_TITLE: "Flotte Test",
71
+ CREEZIO_FLEET_TUNNEL_SUFFIX: TUNNEL_SUFFIX,
72
+ CREEZIO_FLEET_UI_EXTRAS_TITLE: "Dossiers",
73
+ },
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ });
76
+
77
+ // Readiness : poll (un sleep fixe flake quand la machine est chargée).
78
+ {
79
+ const deadline = Date.now() + 10_000;
80
+ for (;;) {
81
+ try {
82
+ await req("GET", "/ops/api/health", null, { Authorization: BASIC });
83
+ break;
84
+ } catch {
85
+ if (Date.now() > deadline) throw new Error("collector pas prêt en 10s");
86
+ await new Promise((r) => setTimeout(r, 150));
87
+ }
88
+ }
89
+ }
90
+
91
+ try {
92
+ const bad = await req("POST", "/i-wrong/heartbeat", "{}");
93
+ assert.equal(bad.status, 404);
94
+
95
+ const hb = await req(
96
+ "POST",
97
+ `/i-${INGEST}/heartbeat`,
98
+ JSON.stringify({
99
+ installId: "install-test-001",
100
+ appVersion: "0.5.0",
101
+ platform: "linux",
102
+ arch: "x64",
103
+ consent: { enabled: true, scopes: { heartbeat: true, crashes: true } },
104
+ tunnelSlug: "demo",
105
+ health: { next: "running" },
106
+ dossierStats: {
107
+ total: 3,
108
+ actifs7j: 1,
109
+ parEtat: { EN_COURS: 2, VALIDE: 1 },
110
+ },
111
+ }),
112
+ );
113
+ assert.equal(hb.status, 200);
114
+ assert.equal(hb.json.ok, true);
115
+
116
+ const crash = await req(
117
+ "POST",
118
+ `/i-${INGEST}/crash`,
119
+ JSON.stringify({
120
+ installId: "install-test-001",
121
+ kind: "boot-failure",
122
+ appVersion: "0.5.0",
123
+ detail: { message: "test" },
124
+ timestamp: new Date().toISOString(),
125
+ }),
126
+ );
127
+ assert.equal(crash.status, 204);
128
+
129
+ const unauthUi = await req("GET", "/");
130
+ assert.equal(unauthUi.status, 401);
131
+
132
+ const ui = await req("GET", "/", null, { Authorization: BASIC });
133
+ assert.equal(ui.status, 200);
134
+ assert.ok(String(ui.raw).includes(UI_TITLE));
135
+ assert.ok(String(ui.raw).includes("sideNav"));
136
+ assert.ok(String(ui.raw).includes("__FLEET_BRAND__"));
137
+
138
+ const unauth = await req("GET", "/ops/api/installs");
139
+ assert.equal(unauth.status, 401);
140
+
141
+ const fleet = await req("GET", "/ops/api/fleet", null, {
142
+ Authorization: BASIC,
143
+ });
144
+ assert.equal(fleet.status, 200);
145
+ assert.ok(fleet.json.servers.length >= 1);
146
+ assert.equal(fleet.json.servers[0].slug, "demo");
147
+ assert.equal(fleet.json.servers[0].hostname, `demo.${TUNNEL_SUFFIX}`);
148
+ assert.equal(fleet.json.stats.dossiers.total, 3);
149
+ assert.ok(fleet.json.stats.versions["0.5.0"] >= 1);
150
+
151
+ // Bundle actions riche (P0/P1)
152
+ const actBundle = await req(
153
+ "POST",
154
+ `/i-${INGEST}/bundle`,
155
+ JSON.stringify({
156
+ installId: "install-test-001",
157
+ kind: "actions",
158
+ timestamp: new Date().toISOString(),
159
+ items: [
160
+ {
161
+ schemaVersion: 1,
162
+ name: "ui.click",
163
+ type: "ui.click",
164
+ category: "ui",
165
+ label: "nav.panier",
166
+ path: "/dashboard",
167
+ userId: "u1",
168
+ username: "alice",
169
+ durationMs: undefined,
170
+ meta: { aid: "nav.panier" },
171
+ at: new Date().toISOString(),
172
+ },
173
+ {
174
+ schemaVersion: 1,
175
+ name: "page.hide",
176
+ type: "page.hide",
177
+ category: "navigation",
178
+ label: "Quitte /produits",
179
+ path: "/produits",
180
+ userId: "u1",
181
+ username: "alice",
182
+ durationMs: 15000,
183
+ at: new Date().toISOString(),
184
+ },
185
+ ],
186
+ }),
187
+ );
188
+ assert.equal(actBundle.status, 200);
189
+
190
+ const server = await req("GET", "/ops/api/server/demo", null, {
191
+ Authorization: BASIC,
192
+ });
193
+ assert.equal(server.status, 200);
194
+ assert.ok(server.json.users.length >= 1);
195
+ assert.ok(
196
+ (server.json.activity || []).some((a) => a.name === "ui.click" || a.type === "ui.click"),
197
+ );
198
+ assert.equal(server.json.server.dossierStats.total, 3);
199
+
200
+ const userDetail = await req(
201
+ "GET",
202
+ "/ops/api/server/demo/user/u1",
203
+ null,
204
+ { Authorization: BASIC },
205
+ );
206
+ // user may be host fallback if users list doesn't include u1 — still OK if activity filtered
207
+ assert.ok(userDetail.status === 200 || userDetail.status === 404);
208
+
209
+ // Orphans (sans userId) visibles pour le seul owner / seul humain
210
+ const usersHb = await req(
211
+ "POST",
212
+ `/i-${INGEST}/heartbeat`,
213
+ JSON.stringify({
214
+ installId: "install-test-001",
215
+ appVersion: "0.6.1",
216
+ platform: "linux",
217
+ arch: "x64",
218
+ consent: { enabled: true, scopes: { heartbeat: true, actions: true } },
219
+ tunnelSlug: "demo",
220
+ health: { next: "running" },
221
+ users: [
222
+ {
223
+ id: "owner-1",
224
+ username: "bob",
225
+ role: "owner",
226
+ kind: "human",
227
+ active: true,
228
+ },
229
+ ],
230
+ }),
231
+ );
232
+ assert.equal(usersHb.status, 200);
233
+ const orphanBundle = await req(
234
+ "POST",
235
+ `/i-${INGEST}/bundle`,
236
+ JSON.stringify({
237
+ installId: "install-test-001",
238
+ kind: "actions",
239
+ timestamp: new Date().toISOString(),
240
+ items: [
241
+ {
242
+ schemaVersion: 1,
243
+ name: "page.view",
244
+ type: "page.view",
245
+ category: "navigation",
246
+ label: "Page /orphan",
247
+ path: "/orphan",
248
+ at: new Date().toISOString(),
249
+ },
250
+ ],
251
+ }),
252
+ );
253
+ assert.equal(orphanBundle.status, 200);
254
+ const orphanUser = await req(
255
+ "GET",
256
+ "/ops/api/server/demo/user/owner-1",
257
+ null,
258
+ { Authorization: BASIC },
259
+ );
260
+ assert.equal(orphanUser.status, 200);
261
+ assert.ok(
262
+ (orphanUser.json.activity || []).some((a) => a.path === "/orphan"),
263
+ "activité orpheline rattachée au seul owner",
264
+ );
265
+
266
+ const list = await req("GET", "/ops/api/installs", null, {
267
+ Authorization: BASIC,
268
+ });
269
+ assert.equal(list.status, 200);
270
+ assert.ok(list.json.installs.length >= 1);
271
+ assert.equal(list.json.installs[0].tunnelSlug, "demo");
272
+
273
+ const listBearer = await req("GET", "/ops/api/installs", null, {
274
+ Authorization: `Bearer ${OPS}`,
275
+ });
276
+ assert.equal(listBearer.status, 200);
277
+
278
+ // Dual-read legacy prefix : TF2_* encore accepté si CREEZIO_* absents
279
+ // (couvert par resolveFleetCollectorEnv — smoke ici via Bearer OPS déjà set).
280
+
281
+ const enq = await req(
282
+ "POST",
283
+ "/ops/api/commands",
284
+ JSON.stringify({
285
+ installId: "install-test-001",
286
+ command: "sync-now",
287
+ }),
288
+ { Authorization: BASIC },
289
+ );
290
+ assert.equal(enq.status, 200);
291
+ assert.equal(enq.json.command.command, "sync-now");
292
+
293
+ const pending = await req(
294
+ "GET",
295
+ `/i-${INGEST}/commands?installId=install-test-001`,
296
+ );
297
+ assert.equal(pending.status, 200);
298
+ assert.equal(pending.json.commands.length, 1);
299
+
300
+ const ack = await req(
301
+ "POST",
302
+ `/i-${INGEST}/commands/ack`,
303
+ JSON.stringify({
304
+ installId: "install-test-001",
305
+ commandId: pending.json.commands[0].id,
306
+ ok: true,
307
+ detail: "ok",
308
+ }),
309
+ );
310
+ assert.equal(ack.status, 200);
311
+
312
+ const badCmd = await req(
313
+ "POST",
314
+ "/ops/api/commands",
315
+ JSON.stringify({ installId: "install-test-001", command: "rm -rf /" }),
316
+ { Authorization: `Bearer ${OPS}` },
317
+ );
318
+ assert.equal(badCmd.status, 400);
319
+
320
+ // Boîte noire : lastBootSummary (heartbeat) + bundle ops_events
321
+ const opsHb = await req(
322
+ "POST",
323
+ `/i-${INGEST}/heartbeat`,
324
+ JSON.stringify({
325
+ installId: "install-test-001",
326
+ appVersion: "0.9.0",
327
+ platform: "linux",
328
+ arch: "x64",
329
+ consent: { enabled: true, scopes: { heartbeat: true, ops: true } },
330
+ tunnelSlug: "demo",
331
+ health: { next: "running" },
332
+ lastBootSummary: {
333
+ bootId: "boot-abc",
334
+ startedAt: new Date().toISOString(),
335
+ durationMs: 42000,
336
+ counts: { decision: 3, event: 8 },
337
+ decisions: {
338
+ "meili.ready": { outcome: "full-reindex", reason: "fingerprint-absent" },
339
+ },
340
+ durations: { "boot.done": 42000 },
341
+ },
342
+ }),
343
+ );
344
+ assert.equal(opsHb.status, 200);
345
+
346
+ const opsBundle = await req(
347
+ "POST",
348
+ `/i-${INGEST}/bundle`,
349
+ JSON.stringify({
350
+ installId: "install-test-001",
351
+ kind: "ops_events",
352
+ timestamp: new Date().toISOString(),
353
+ items: [
354
+ {
355
+ ts: new Date().toISOString(),
356
+ bootId: "boot-abc",
357
+ seq: 1,
358
+ source: "main",
359
+ level: "decision",
360
+ kind: "meili.ready",
361
+ outcome: "full-reindex",
362
+ reason: "fingerprint-absent",
363
+ ctx: { sql: { produits: 10 } },
364
+ },
365
+ {
366
+ ts: new Date().toISOString(),
367
+ bootId: "boot-abc",
368
+ seq: 2,
369
+ source: "indexer",
370
+ level: "event",
371
+ kind: "index.done",
372
+ outcome: "ok",
373
+ durationMs: 61000,
374
+ },
375
+ ],
376
+ }),
377
+ );
378
+ assert.equal(opsBundle.status, 200);
379
+
380
+ const opsDetail = await req("GET", "/ops/api/server/demo", null, {
381
+ Authorization: BASIC,
382
+ });
383
+ assert.equal(opsDetail.status, 200);
384
+ assert.ok(opsDetail.json.ops, "section ops présente");
385
+ assert.equal(
386
+ opsDetail.json.ops.lastBootSummary.decisions["meili.ready"].outcome,
387
+ "full-reindex",
388
+ );
389
+ assert.ok(
390
+ (opsDetail.json.ops.events || []).some(
391
+ (e) => e.kind === "meili.ready" && e.reason === "fingerprint-absent",
392
+ ),
393
+ "événement ops meili.ready remonté",
394
+ );
395
+ assert.ok(
396
+ (opsDetail.json.ops.events || []).some((e) => e.kind === "index.done"),
397
+ "événement ops index.done remonté",
398
+ );
399
+
400
+ // 0 domaine marque hardcodé dans le binaire
401
+ const src = fs.readFileSync(path.join(__dirname, "server.mjs"), "utf8")
402
+ + fs.readFileSync(path.join(__dirname, "ops-api.mjs"), "utf8");
403
+ assert.ok(!/tempoflow\.fr|certivan\.creez\.io/i.test(src));
404
+
405
+ console.log("OK — fleet-collector (@creezio/observability)");
406
+ } finally {
407
+ child.kill("SIGTERM");
408
+ try {
409
+ fs.rmSync(DIR, { recursive: true, force: true });
410
+ } catch {
411
+ /* ignore */
412
+ }
413
+ }