@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.
- package/README.md +328 -0
- package/dist/.creezio-src-hash.json +4 -0
- package/dist/api-endpoints/http-routes.d.ts +11 -0
- package/dist/api-endpoints/http-routes.d.ts.map +1 -0
- package/dist/api-endpoints/http-routes.js +11 -0
- package/dist/api-endpoints/http-routes.js.map +1 -0
- package/dist/api-endpoints/index.d.ts +4 -0
- package/dist/api-endpoints/index.d.ts.map +1 -0
- package/dist/api-endpoints/index.js +3 -0
- package/dist/api-endpoints/index.js.map +1 -0
- package/dist/api-endpoints/registry.d.ts +66 -0
- package/dist/api-endpoints/registry.d.ts.map +1 -0
- package/dist/api-endpoints/registry.js +102 -0
- package/dist/api-endpoints/registry.js.map +1 -0
- package/dist/api-mount.d.ts +7 -0
- package/dist/api-mount.d.ts.map +1 -0
- package/dist/api-mount.js +114 -0
- package/dist/api-mount.js.map +1 -0
- package/dist/helpers.d.ts +23 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +36 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-store.d.ts +3 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +113 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/ops/emit.d.ts +8 -0
- package/dist/ops/emit.d.ts.map +1 -0
- package/dist/ops/emit.js +15 -0
- package/dist/ops/emit.js.map +1 -0
- package/dist/ops/fleet-activity.d.ts +50 -0
- package/dist/ops/fleet-activity.d.ts.map +1 -0
- package/dist/ops/fleet-activity.js +123 -0
- package/dist/ops/fleet-activity.js.map +1 -0
- package/dist/ops/fleet-agent.d.ts +102 -0
- package/dist/ops/fleet-agent.d.ts.map +1 -0
- package/dist/ops/fleet-agent.js +352 -0
- package/dist/ops/fleet-agent.js.map +1 -0
- package/dist/ops/fleet-samples.d.ts +33 -0
- package/dist/ops/fleet-samples.d.ts.map +1 -0
- package/dist/ops/fleet-samples.js +223 -0
- package/dist/ops/fleet-samples.js.map +1 -0
- package/dist/ops/journal.d.ts +56 -0
- package/dist/ops/journal.d.ts.map +1 -0
- package/dist/ops/journal.js +307 -0
- package/dist/ops/journal.js.map +1 -0
- package/dist/ops/rules.d.ts +16 -0
- package/dist/ops/rules.d.ts.map +1 -0
- package/dist/ops/rules.js +98 -0
- package/dist/ops/rules.js.map +1 -0
- package/dist/ops/types.d.ts +69 -0
- package/dist/ops/types.d.ts.map +1 -0
- package/dist/ops/types.js +119 -0
- package/dist/ops/types.js.map +1 -0
- package/dist/request-logs/config.d.ts +19 -0
- package/dist/request-logs/config.d.ts.map +1 -0
- package/dist/request-logs/config.js +36 -0
- package/dist/request-logs/config.js.map +1 -0
- package/dist/request-logs/http-routes.d.ts +8 -0
- package/dist/request-logs/http-routes.d.ts.map +1 -0
- package/dist/request-logs/http-routes.js +29 -0
- package/dist/request-logs/http-routes.js.map +1 -0
- package/dist/request-logs/index.d.ts +6 -0
- package/dist/request-logs/index.d.ts.map +1 -0
- package/dist/request-logs/index.js +5 -0
- package/dist/request-logs/index.js.map +1 -0
- package/dist/request-logs/middleware.d.ts +11 -0
- package/dist/request-logs/middleware.d.ts.map +1 -0
- package/dist/request-logs/middleware.js +198 -0
- package/dist/request-logs/middleware.js.map +1 -0
- package/dist/request-logs/request-logs.d.ts +60 -0
- package/dist/request-logs/request-logs.d.ts.map +1 -0
- package/dist/request-logs/request-logs.js +260 -0
- package/dist/request-logs/request-logs.js.map +1 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +21 -0
- package/dist/schema.js.map +1 -0
- package/dist/sqlite-driver.d.ts +16 -0
- package/dist/sqlite-driver.d.ts.map +1 -0
- package/dist/sqlite-driver.js +15 -0
- package/dist/sqlite-driver.js.map +1 -0
- package/dist/sqlite-store.d.ts +12 -0
- package/dist/sqlite-store.d.ts.map +1 -0
- package/dist/sqlite-store.js +176 -0
- package/dist/sqlite-store.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/usage/adapters.d.ts +31 -0
- package/dist/usage/adapters.d.ts.map +1 -0
- package/dist/usage/adapters.js +37 -0
- package/dist/usage/adapters.js.map +1 -0
- package/dist/usage/http-routes.d.ts +23 -0
- package/dist/usage/http-routes.d.ts.map +1 -0
- package/dist/usage/http-routes.js +150 -0
- package/dist/usage/http-routes.js.map +1 -0
- package/dist/usage/index.d.ts +11 -0
- package/dist/usage/index.d.ts.map +1 -0
- package/dist/usage/index.js +7 -0
- package/dist/usage/index.js.map +1 -0
- package/dist/usage/ui-brand.d.ts +23 -0
- package/dist/usage/ui-brand.d.ts.map +1 -0
- package/dist/usage/ui-brand.js +18 -0
- package/dist/usage/ui-brand.js.map +1 -0
- package/dist/usage/usage-analytics-productivity.d.ts +73 -0
- package/dist/usage/usage-analytics-productivity.d.ts.map +1 -0
- package/dist/usage/usage-analytics-productivity.js +317 -0
- package/dist/usage/usage-analytics-productivity.js.map +1 -0
- package/dist/usage/usage-analytics-shared.d.ts +5 -0
- package/dist/usage/usage-analytics-shared.d.ts.map +1 -0
- package/dist/usage/usage-analytics-shared.js +20 -0
- package/dist/usage/usage-analytics-shared.js.map +1 -0
- package/dist/usage/usage-analytics.d.ts +127 -0
- package/dist/usage/usage-analytics.d.ts.map +1 -0
- package/dist/usage/usage-analytics.js +338 -0
- package/dist/usage/usage-analytics.js.map +1 -0
- package/dist-cjs/api-endpoints/http-routes.js +14 -0
- package/dist-cjs/api-endpoints/http-routes.js.map +1 -0
- package/dist-cjs/api-endpoints/index.js +10 -0
- package/dist-cjs/api-endpoints/index.js.map +1 -0
- package/dist-cjs/api-endpoints/registry.js +107 -0
- package/dist-cjs/api-endpoints/registry.js.map +1 -0
- package/dist-cjs/api-mount.js +117 -0
- package/dist-cjs/api-mount.js.map +1 -0
- package/dist-cjs/helpers.js +41 -0
- package/dist-cjs/helpers.js.map +1 -0
- package/dist-cjs/index.js +118 -0
- package/dist-cjs/index.js.map +1 -0
- package/dist-cjs/memory-store.js +119 -0
- package/dist-cjs/memory-store.js.map +1 -0
- package/dist-cjs/ops/emit.js +18 -0
- package/dist-cjs/ops/emit.js.map +1 -0
- package/dist-cjs/ops/fleet-activity.js +131 -0
- package/dist-cjs/ops/fleet-activity.js.map +1 -0
- package/dist-cjs/ops/fleet-agent.js +358 -0
- package/dist-cjs/ops/fleet-agent.js.map +1 -0
- package/dist-cjs/ops/fleet-samples.js +229 -0
- package/dist-cjs/ops/fleet-samples.js.map +1 -0
- package/dist-cjs/ops/journal.js +326 -0
- package/dist-cjs/ops/journal.js.map +1 -0
- package/dist-cjs/ops/rules.js +102 -0
- package/dist-cjs/ops/rules.js.map +1 -0
- package/dist-cjs/ops/types.js +126 -0
- package/dist-cjs/ops/types.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/request-logs/config.js +42 -0
- package/dist-cjs/request-logs/config.js.map +1 -0
- package/dist-cjs/request-logs/http-routes.js +32 -0
- package/dist-cjs/request-logs/http-routes.js.map +1 -0
- package/dist-cjs/request-logs/index.js +27 -0
- package/dist-cjs/request-logs/index.js.map +1 -0
- package/dist-cjs/request-logs/middleware.js +203 -0
- package/dist-cjs/request-logs/middleware.js.map +1 -0
- package/dist-cjs/request-logs/request-logs.js +277 -0
- package/dist-cjs/request-logs/request-logs.js.map +1 -0
- package/dist-cjs/schema.js +24 -0
- package/dist-cjs/schema.js.map +1 -0
- package/dist-cjs/sqlite-driver.js +18 -0
- package/dist-cjs/sqlite-driver.js.map +1 -0
- package/dist-cjs/sqlite-store.js +182 -0
- package/dist-cjs/sqlite-store.js.map +1 -0
- package/dist-cjs/types.js +12 -0
- package/dist-cjs/types.js.map +1 -0
- package/dist-cjs/usage/adapters.js +47 -0
- package/dist-cjs/usage/adapters.js.map +1 -0
- package/dist-cjs/usage/http-routes.js +154 -0
- package/dist-cjs/usage/http-routes.js.map +1 -0
- package/dist-cjs/usage/index.js +34 -0
- package/dist-cjs/usage/index.js.map +1 -0
- package/dist-cjs/usage/ui-brand.js +23 -0
- package/dist-cjs/usage/ui-brand.js.map +1 -0
- package/dist-cjs/usage/usage-analytics-productivity.js +321 -0
- package/dist-cjs/usage/usage-analytics-productivity.js.map +1 -0
- package/dist-cjs/usage/usage-analytics-shared.js +23 -0
- package/dist-cjs/usage/usage-analytics-shared.js.map +1 -0
- package/dist-cjs/usage/usage-analytics.js +353 -0
- package/dist-cjs/usage/usage-analytics.js.map +1 -0
- package/fleet-collector/AGENTS.md +7 -0
- package/fleet-collector/README.md +83 -0
- package/fleet-collector/configure-fleet-npm.sh +108 -0
- package/fleet-collector/env.mjs +174 -0
- package/fleet-collector/ops-api.mjs +455 -0
- package/fleet-collector/public/admin.html +696 -0
- package/fleet-collector/public/index.html +941 -0
- package/fleet-collector/server.mjs +564 -0
- package/fleet-collector/test-fleet-collector.mjs +413 -0
- package/fleet-collector/test-server-admin.mjs +475 -0
- package/package.json +109 -0
- package/scripts/test-api-endpoints-registry.mjs +60 -0
- package/ui/analytics-client.tsx +834 -0
- package/ui/analytics-productivity-panel.tsx +513 -0
- package/ui/api-endpoints-client.tsx +228 -0
- package/ui/index.ts +39 -0
- package/ui/primitives/badge.tsx +37 -0
- package/ui/primitives/button.tsx +44 -0
- package/ui/primitives/card.tsx +44 -0
- package/ui/primitives/cn.ts +6 -0
- package/ui/primitives/input.tsx +19 -0
- package/ui/primitives/tabs.tsx +51 -0
- package/ui/request-logs-client.tsx +327 -0
- package/ui/session-usage-analytics-provider.tsx +28 -0
- package/ui/usage-analytics-client.ts +479 -0
- package/ui/usage-analytics-provider.tsx +84 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests locaux du Creezio Server Admin (spawn serveur éphémère).
|
|
3
|
+
*
|
|
4
|
+
* Ne requiert PAS docker : CREEZIO_DOCKER_SOCK pointe sur un chemin
|
|
5
|
+
* inexistant → état docker "unknown" attendu pour les instances du registre.
|
|
6
|
+
*/
|
|
7
|
+
import http from "node:http";
|
|
8
|
+
import net from "node:net";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ADMIN_USER = "admin";
|
|
18
|
+
const ADMIN_PASS = "test-admin-pass";
|
|
19
|
+
const BASIC =
|
|
20
|
+
"Basic " + Buffer.from(`${ADMIN_USER}:${ADMIN_PASS}`).toString("base64");
|
|
21
|
+
|
|
22
|
+
// Port éphémère : réservé par l'OS puis libéré pour le serveur admin.
|
|
23
|
+
function ephemeralPort() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const srv = net.createServer();
|
|
26
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
27
|
+
const p = srv.address().port;
|
|
28
|
+
srv.close(() => resolve(p));
|
|
29
|
+
});
|
|
30
|
+
srv.on("error", reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const PORT = await ephemeralPort();
|
|
34
|
+
// Ports des instances fixture : éphémères aussi (aucun vrai serveur ne doit
|
|
35
|
+
// écouter dessus — le test attend un 504 sur le proxy boot-status).
|
|
36
|
+
const INSTANCE_PORT_1 = await ephemeralPort();
|
|
37
|
+
const INSTANCE_PORT_2 = await ephemeralPort();
|
|
38
|
+
|
|
39
|
+
// Fixture : brandRoot avec registre servers.json (2 instances).
|
|
40
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "creezio-server-admin-"));
|
|
41
|
+
const BRAND_ROOT = path.join(TMP, "testbrand");
|
|
42
|
+
fs.mkdirSync(path.join(BRAND_ROOT, "docker-data", "servers", "demo"), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
});
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(BRAND_ROOT, "package.json"),
|
|
47
|
+
JSON.stringify({ name: "@creezio/app-testbrand" }, null, 2),
|
|
48
|
+
);
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(BRAND_ROOT, "docker-data", "servers.json"),
|
|
51
|
+
JSON.stringify(
|
|
52
|
+
{
|
|
53
|
+
version: 1,
|
|
54
|
+
brandId: "testbrand",
|
|
55
|
+
image: "creezio-server-testbrand:local",
|
|
56
|
+
instances: [
|
|
57
|
+
{
|
|
58
|
+
name: "demo",
|
|
59
|
+
containerName: "testbrand-server-demo",
|
|
60
|
+
port: INSTANCE_PORT_1,
|
|
61
|
+
bind: "127.0.0.1",
|
|
62
|
+
dataDir: "docker-data/servers/demo",
|
|
63
|
+
createdAt: "2026-08-04T00:00:00.000Z",
|
|
64
|
+
env: { CREEZIO_NATIVE_WARM: "1" },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "second",
|
|
68
|
+
containerName: "testbrand-server-second",
|
|
69
|
+
port: INSTANCE_PORT_2,
|
|
70
|
+
bind: "127.0.0.1",
|
|
71
|
+
dataDir: "docker-data/servers/second",
|
|
72
|
+
createdAt: "2026-08-04T00:00:00.000Z",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
// Événements ops JSONL pour l'instance demo.
|
|
81
|
+
const opsDir = path.join(BRAND_ROOT, "docker-data", "servers", "demo", "ops");
|
|
82
|
+
fs.mkdirSync(opsDir, { recursive: true });
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(opsDir, "boot-test.jsonl"),
|
|
85
|
+
[
|
|
86
|
+
JSON.stringify({ ts: "2026-08-04T00:00:01Z", kind: "boot.start", seq: 1 }),
|
|
87
|
+
JSON.stringify({ ts: "2026-08-04T00:00:02Z", kind: "boot.done", seq: 2 }),
|
|
88
|
+
"ligne invalide pas du json",
|
|
89
|
+
].join("\n") + "\n",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
function req(method, urlPath, body, headers = {}) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const r = http.request(
|
|
95
|
+
{
|
|
96
|
+
hostname: "127.0.0.1",
|
|
97
|
+
port: PORT,
|
|
98
|
+
path: urlPath,
|
|
99
|
+
method,
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
...(body ? { "Content-Length": Buffer.byteLength(body) } : {}),
|
|
103
|
+
...headers,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
(res) => {
|
|
107
|
+
const chunks = [];
|
|
108
|
+
res.on("data", (c) => chunks.push(c));
|
|
109
|
+
res.on("end", () => {
|
|
110
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
111
|
+
let json = null;
|
|
112
|
+
try {
|
|
113
|
+
json = raw ? JSON.parse(raw) : null;
|
|
114
|
+
} catch {
|
|
115
|
+
json = raw;
|
|
116
|
+
}
|
|
117
|
+
resolve({ status: res.statusCode, json, raw });
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
r.on("error", reject);
|
|
122
|
+
if (body) r.write(body);
|
|
123
|
+
r.end();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const child = spawn(
|
|
128
|
+
process.execPath,
|
|
129
|
+
// SoT @creezio/fleet (les wrappers .mjs de compat ont été retirés en 0.19) —
|
|
130
|
+
// exige le dist construit : npm run build -w @creezio/fleet.
|
|
131
|
+
[
|
|
132
|
+
path.join(
|
|
133
|
+
__dirname,
|
|
134
|
+
"../../fleet/dist/bin/server-admin-main.js",
|
|
135
|
+
),
|
|
136
|
+
],
|
|
137
|
+
{
|
|
138
|
+
env: {
|
|
139
|
+
...process.env,
|
|
140
|
+
CREEZIO_ADMIN_PORT: String(PORT),
|
|
141
|
+
CREEZIO_ADMIN_HOST: "127.0.0.1",
|
|
142
|
+
CREEZIO_ADMIN_USER: ADMIN_USER,
|
|
143
|
+
CREEZIO_ADMIN_PASS: ADMIN_PASS,
|
|
144
|
+
CREEZIO_ADMIN_BRAND_ROOTS: BRAND_ROOT,
|
|
145
|
+
// Socket inexistant → docker indisponible (le test n'exige PAS docker).
|
|
146
|
+
CREEZIO_DOCKER_SOCK: path.join(TMP, "nonexistent-docker.sock"),
|
|
147
|
+
},
|
|
148
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Readiness : poll (un sleep fixe flake quand la machine est chargée).
|
|
153
|
+
{
|
|
154
|
+
const deadline = Date.now() + 10_000;
|
|
155
|
+
for (;;) {
|
|
156
|
+
try {
|
|
157
|
+
await req("GET", "/admin/api/health", null, { Authorization: BASIC });
|
|
158
|
+
break;
|
|
159
|
+
} catch {
|
|
160
|
+
if (Date.now() > deadline) throw new Error("admin pas prêt en 10s");
|
|
161
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// 401 sans auth (UI + API).
|
|
168
|
+
const unauthUi = await req("GET", "/admin");
|
|
169
|
+
assert.equal(unauthUi.status, 401);
|
|
170
|
+
assert.ok(
|
|
171
|
+
String(unauthUi.raw).includes("unauthorized") ||
|
|
172
|
+
unauthUi.json?.error === "unauthorized",
|
|
173
|
+
);
|
|
174
|
+
const unauthApi = await req("GET", "/admin/api/servers");
|
|
175
|
+
assert.equal(unauthApi.status, 401);
|
|
176
|
+
const badCreds = await req("GET", "/admin/api/servers", null, {
|
|
177
|
+
Authorization:
|
|
178
|
+
"Basic " + Buffer.from(`${ADMIN_USER}:wrong`).toString("base64"),
|
|
179
|
+
});
|
|
180
|
+
assert.equal(badCreds.status, 401);
|
|
181
|
+
|
|
182
|
+
// 200 + HTML sur /admin avec Basic auth.
|
|
183
|
+
const ui = await req("GET", "/admin", null, { Authorization: BASIC });
|
|
184
|
+
assert.equal(ui.status, 200);
|
|
185
|
+
assert.ok(String(ui.raw).includes("<!DOCTYPE html>"));
|
|
186
|
+
assert.ok(String(ui.raw).includes("Creezio Server Admin"));
|
|
187
|
+
|
|
188
|
+
// Health.
|
|
189
|
+
const health = await req("GET", "/admin/api/health", null, {
|
|
190
|
+
Authorization: BASIC,
|
|
191
|
+
});
|
|
192
|
+
assert.equal(health.status, 200);
|
|
193
|
+
assert.equal(health.json.ok, true);
|
|
194
|
+
assert.equal(health.json.service, "creezio-server-admin");
|
|
195
|
+
assert.deepEqual(health.json.brandRoots, [BRAND_ROOT]);
|
|
196
|
+
assert.equal(health.json.docker, false);
|
|
197
|
+
|
|
198
|
+
// Servers : instances du registre, état docker "unknown" (docker absent).
|
|
199
|
+
const servers = await req("GET", "/admin/api/servers", null, {
|
|
200
|
+
Authorization: BASIC,
|
|
201
|
+
});
|
|
202
|
+
assert.equal(servers.status, 200);
|
|
203
|
+
assert.equal(servers.json.ok, true);
|
|
204
|
+
assert.equal(servers.json.docker, false);
|
|
205
|
+
assert.equal(servers.json.servers.length, 2);
|
|
206
|
+
const demo = servers.json.servers.find((s) => s.name === "demo");
|
|
207
|
+
assert.ok(demo, "instance demo présente");
|
|
208
|
+
assert.equal(demo.brandId, "testbrand");
|
|
209
|
+
assert.equal(demo.containerName, "testbrand-server-demo");
|
|
210
|
+
assert.equal(demo.port, INSTANCE_PORT_1);
|
|
211
|
+
assert.equal(demo.orphan, false);
|
|
212
|
+
assert.equal(demo.docker.state, "unknown");
|
|
213
|
+
assert.equal(demo.bootStatus, null);
|
|
214
|
+
assert.equal(demo.env.CREEZIO_NATIVE_WARM, "1");
|
|
215
|
+
const second = servers.json.servers.find((s) => s.name === "second");
|
|
216
|
+
assert.equal(second.docker.state, "unknown");
|
|
217
|
+
|
|
218
|
+
// Snapshot flotte (F1) : refreshedAt présent, snapshot servi tel quel
|
|
219
|
+
// (2e appel immédiat = même refreshedAt), ?fresh=1 = collecte immédiate.
|
|
220
|
+
assert.ok(servers.json.refreshedAt, "refreshedAt attendu sur /servers");
|
|
221
|
+
const serversAgain = await req("GET", "/admin/api/servers", null, {
|
|
222
|
+
Authorization: BASIC,
|
|
223
|
+
});
|
|
224
|
+
assert.equal(
|
|
225
|
+
serversAgain.json.refreshedAt,
|
|
226
|
+
servers.json.refreshedAt,
|
|
227
|
+
"sans fresh=1 le snapshot est servi tel quel (pas de recollecte)",
|
|
228
|
+
);
|
|
229
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
230
|
+
const serversFresh = await req("GET", "/admin/api/servers?fresh=1", null, {
|
|
231
|
+
Authorization: BASIC,
|
|
232
|
+
});
|
|
233
|
+
assert.equal(serversFresh.status, 200);
|
|
234
|
+
assert.equal(serversFresh.json.servers.length, 2);
|
|
235
|
+
assert.ok(
|
|
236
|
+
serversFresh.json.refreshedAt > servers.json.refreshedAt,
|
|
237
|
+
"?fresh=1 force une collecte immédiate (refreshedAt avance)",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Hosts : snapshot + refreshedAt (aucun hôte enrôlé dans la fixture).
|
|
241
|
+
const hosts = await req("GET", "/admin/api/hosts", null, {
|
|
242
|
+
Authorization: BASIC,
|
|
243
|
+
});
|
|
244
|
+
assert.equal(hosts.status, 200);
|
|
245
|
+
assert.equal(hosts.json.ok, true);
|
|
246
|
+
assert.deepEqual(hosts.json.hosts, []);
|
|
247
|
+
assert.ok(hosts.json.refreshedAt, "refreshedAt attendu sur /hosts");
|
|
248
|
+
|
|
249
|
+
// Ops JSONL : événements parsés, lignes invalides ignorées.
|
|
250
|
+
const ops = await req(
|
|
251
|
+
"GET",
|
|
252
|
+
"/admin/api/servers/testbrand/demo/ops?limit=100",
|
|
253
|
+
null,
|
|
254
|
+
{ Authorization: BASIC },
|
|
255
|
+
);
|
|
256
|
+
assert.equal(ops.status, 200);
|
|
257
|
+
assert.equal(ops.json.ok, true);
|
|
258
|
+
assert.equal(ops.json.events.length, 2);
|
|
259
|
+
assert.equal(ops.json.events[0].kind, "boot.start");
|
|
260
|
+
// Instance sans ops/ → vide, pas d'erreur.
|
|
261
|
+
const opsEmpty = await req(
|
|
262
|
+
"GET",
|
|
263
|
+
"/admin/api/servers/testbrand/second/ops",
|
|
264
|
+
null,
|
|
265
|
+
{ Authorization: BASIC },
|
|
266
|
+
);
|
|
267
|
+
assert.equal(opsEmpty.status, 200);
|
|
268
|
+
assert.deepEqual(opsEmpty.json.events, []);
|
|
269
|
+
|
|
270
|
+
// Disk : tailles par instance + filesystem.
|
|
271
|
+
const disk = await req("GET", "/admin/api/disk", null, {
|
|
272
|
+
Authorization: BASIC,
|
|
273
|
+
});
|
|
274
|
+
assert.equal(disk.status, 200);
|
|
275
|
+
assert.equal(disk.json.ok, true);
|
|
276
|
+
assert.equal(disk.json.instances.length, 2);
|
|
277
|
+
assert.ok(
|
|
278
|
+
disk.json.instances.find((d) => d.name === "demo").sizeBytes > 0,
|
|
279
|
+
"taille data demo > 0 (fixtures ops)",
|
|
280
|
+
);
|
|
281
|
+
assert.ok(disk.json.filesystem.freeBytes > 0);
|
|
282
|
+
assert.ok(disk.json.refreshedAt, "refreshedAt attendu sur /disk");
|
|
283
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
284
|
+
const diskFresh = await req("GET", "/admin/api/disk?fresh=1", null, {
|
|
285
|
+
Authorization: BASIC,
|
|
286
|
+
});
|
|
287
|
+
assert.equal(diskFresh.status, 200);
|
|
288
|
+
assert.ok(
|
|
289
|
+
diskFresh.json.refreshedAt > disk.json.refreshedAt,
|
|
290
|
+
"?fresh=1 force un scan disque immédiat",
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Instance inconnue → 404.
|
|
294
|
+
const notFound = await req(
|
|
295
|
+
"GET",
|
|
296
|
+
"/admin/api/servers/testbrand/nope/ops",
|
|
297
|
+
null,
|
|
298
|
+
{ Authorization: BASIC },
|
|
299
|
+
);
|
|
300
|
+
assert.equal(notFound.status, 404);
|
|
301
|
+
|
|
302
|
+
// Create : brandRoot hors CREEZIO_ADMIN_BRAND_ROOTS refusé.
|
|
303
|
+
const badRoot = await req(
|
|
304
|
+
"POST",
|
|
305
|
+
"/admin/api/servers",
|
|
306
|
+
JSON.stringify({ brandRoot: "/etc", name: "evil" }),
|
|
307
|
+
{ Authorization: BASIC },
|
|
308
|
+
);
|
|
309
|
+
assert.equal(badRoot.status, 400);
|
|
310
|
+
// Create : nom invalide refusé.
|
|
311
|
+
const badName = await req(
|
|
312
|
+
"POST",
|
|
313
|
+
"/admin/api/servers",
|
|
314
|
+
JSON.stringify({ brandRoot: BRAND_ROOT, name: "Bad_Name!" }),
|
|
315
|
+
{ Authorization: BASIC },
|
|
316
|
+
);
|
|
317
|
+
assert.equal(badName.status, 400);
|
|
318
|
+
// Create : nom déjà pris → 409 (avant tout appel docker).
|
|
319
|
+
const dup = await req(
|
|
320
|
+
"POST",
|
|
321
|
+
"/admin/api/servers",
|
|
322
|
+
JSON.stringify({ brandRoot: BRAND_ROOT, name: "demo" }),
|
|
323
|
+
{ Authorization: BASIC },
|
|
324
|
+
);
|
|
325
|
+
assert.equal(dup.status, 409);
|
|
326
|
+
|
|
327
|
+
// Boot-status proxy : serveur injoignable → 504.
|
|
328
|
+
const bs = await req(
|
|
329
|
+
"GET",
|
|
330
|
+
"/admin/api/servers/testbrand/demo/boot-status",
|
|
331
|
+
null,
|
|
332
|
+
{ Authorization: BASIC },
|
|
333
|
+
);
|
|
334
|
+
assert.equal(bs.status, 504);
|
|
335
|
+
|
|
336
|
+
// 0 domaine marque hardcodé dans l'UI admin servie (SoT fleet + collector).
|
|
337
|
+
const src =
|
|
338
|
+
fs.readFileSync(
|
|
339
|
+
path.join(__dirname, "../../fleet/public/admin.html"),
|
|
340
|
+
"utf8",
|
|
341
|
+
) + fs.readFileSync(path.join(__dirname, "public", "admin.html"), "utf8");
|
|
342
|
+
assert.ok(!/tempoflow\.fr|certivan\.creez\.io/i.test(src));
|
|
343
|
+
|
|
344
|
+
console.log("OK — server-admin (@creezio/observability)");
|
|
345
|
+
} finally {
|
|
346
|
+
child.kill("SIGTERM");
|
|
347
|
+
try {
|
|
348
|
+
fs.rmSync(TMP, { recursive: true, force: true });
|
|
349
|
+
} catch {
|
|
350
|
+
/* ignore */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── backupInstanceData : archive vérifiée (gzip -t), jamais de warning
|
|
355
|
+
// silencieux. Piège vécu : GNU tar exit 1 (« file changed as we read it »)
|
|
356
|
+
// sur volume vivant = archive VALIDE, l'ancien code la jetait en
|
|
357
|
+
// « backup indisponible (tar) » alors que le .tar.gz complet existait.
|
|
358
|
+
{
|
|
359
|
+
const { backupInstanceData, backupsDir } = await import("./server-lib.mjs");
|
|
360
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "creezio-backup-"));
|
|
361
|
+
try {
|
|
362
|
+
const brandRoot = path.join(tmp, "brand");
|
|
363
|
+
const dataDir = path.join("docker-data", "servers", "demo");
|
|
364
|
+
fs.mkdirSync(path.join(brandRoot, dataDir, "sub"), { recursive: true });
|
|
365
|
+
fs.writeFileSync(
|
|
366
|
+
path.join(brandRoot, dataDir, "sub", "core.db"),
|
|
367
|
+
"contenu-test",
|
|
368
|
+
);
|
|
369
|
+
const inst = { name: "demo", dataDir };
|
|
370
|
+
|
|
371
|
+
const ok = await backupInstanceData(brandRoot, inst);
|
|
372
|
+
assert.equal(ok.ok, true, `backup attendu OK: ${ok.detail}`);
|
|
373
|
+
assert.ok(fs.existsSync(ok.file), "archive .tar.gz absente");
|
|
374
|
+
assert.ok(fs.statSync(ok.file).size > 0, "archive vide");
|
|
375
|
+
assert.match(ok.detail, /gzip vérifié/);
|
|
376
|
+
assert.ok(ok.file.startsWith(backupsDir(brandRoot)));
|
|
377
|
+
|
|
378
|
+
// Volume introuvable → échec PROPRE avec détail (pas de null muet).
|
|
379
|
+
const ko = await backupInstanceData(brandRoot, {
|
|
380
|
+
name: "fantome",
|
|
381
|
+
dataDir: path.join("docker-data", "servers", "fantome"),
|
|
382
|
+
});
|
|
383
|
+
assert.equal(ko.ok, false);
|
|
384
|
+
assert.match(ko.detail, /introuvable/);
|
|
385
|
+
console.log("OK — backupInstanceData (archive gzip vérifiée)");
|
|
386
|
+
} finally {
|
|
387
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── updateServer : défaut backup=false, pas de prune des archives existantes
|
|
392
|
+
{
|
|
393
|
+
const { updateServer } = await import("./server-lib.mjs");
|
|
394
|
+
// P2.b : server-lib.mjs est un wrapper compat — lire la source RÉELLE
|
|
395
|
+
// (dist @creezio/fleet, résolu comme à l'exécution).
|
|
396
|
+
const src = fs.readFileSync(
|
|
397
|
+
new URL(import.meta.resolve("@creezio/fleet/server-lib")),
|
|
398
|
+
"utf8",
|
|
399
|
+
);
|
|
400
|
+
assert.match(src, /backup = false/);
|
|
401
|
+
assert.match(src, /pas de nouveau backup \(défaut\)/);
|
|
402
|
+
assert.match(src, /if \(backup\)/);
|
|
403
|
+
// Politique propriétaire : ne pas purger les backups de référence à l'update.
|
|
404
|
+
assert.doesNotMatch(src, /pruneBackups\(brandRoot, inst\.name\)/);
|
|
405
|
+
// 0.10.3 : update ne droppe plus un sidecar / hostname public.
|
|
406
|
+
assert.match(src, /resolveStackUpdatePolicy/);
|
|
407
|
+
assert.match(src, /preserve-sidecar/);
|
|
408
|
+
assert.equal(typeof updateServer, "function");
|
|
409
|
+
console.log("OK — updateServer défaut sans backup + archives conservées");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── dirSizeBytes / buildDiskReport asynchrones (F1) : fs.promises, plus de
|
|
413
|
+
// readdirSync/statSync récursifs qui gèlent l'event loop mono-thread.
|
|
414
|
+
{
|
|
415
|
+
const { dirSizeBytes, buildDiskReport } = await import("./server-lib.mjs");
|
|
416
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "creezio-disk-"));
|
|
417
|
+
try {
|
|
418
|
+
fs.mkdirSync(path.join(tmp, "sub"), { recursive: true });
|
|
419
|
+
fs.writeFileSync(path.join(tmp, "a.bin"), "12345");
|
|
420
|
+
fs.writeFileSync(path.join(tmp, "sub", "b.bin"), "1234567890");
|
|
421
|
+
const p = dirSizeBytes(tmp);
|
|
422
|
+
assert.ok(p instanceof Promise, "dirSizeBytes doit être asynchrone");
|
|
423
|
+
assert.equal(await p, 15);
|
|
424
|
+
const report = buildDiskReport([]);
|
|
425
|
+
assert.ok(report instanceof Promise, "buildDiskReport doit être asynchrone");
|
|
426
|
+
assert.equal((await report).ok, true);
|
|
427
|
+
console.log("OK — dirSizeBytes/buildDiskReport async");
|
|
428
|
+
} finally {
|
|
429
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── createFleetSnapshotPoller (F1) : jamais réentrant (cycle en cours
|
|
434
|
+
// réutilisé), refresh forcé après cycle, scan disque séparé.
|
|
435
|
+
{
|
|
436
|
+
const { createFleetSnapshotPoller } = await import("./server-lib.mjs");
|
|
437
|
+
let coreCalls = 0;
|
|
438
|
+
let diskCalls = 0;
|
|
439
|
+
let release;
|
|
440
|
+
const gate = new Promise((r) => (release = r));
|
|
441
|
+
const poller = createFleetSnapshotPoller({
|
|
442
|
+
collectServersView: async () => {
|
|
443
|
+
coreCalls += 1;
|
|
444
|
+
await gate;
|
|
445
|
+
return { docker: true, servers: [{ name: "x" }] };
|
|
446
|
+
},
|
|
447
|
+
collectHostsView: async () => [{ hostId: "h1" }],
|
|
448
|
+
collectDiskView: async () => {
|
|
449
|
+
diskCalls += 1;
|
|
450
|
+
return { ok: true, instances: [], filesystem: null };
|
|
451
|
+
},
|
|
452
|
+
intervalMs: 3_600_000,
|
|
453
|
+
});
|
|
454
|
+
// 3 refresh concurrents pendant qu'un cycle est en cours → 1 seule collecte.
|
|
455
|
+
const p1 = poller.refreshCore();
|
|
456
|
+
const p2 = poller.refreshCore();
|
|
457
|
+
const p3 = poller.refreshCore();
|
|
458
|
+
assert.equal(coreCalls, 1, "cycle en cours réutilisé (jamais réentrant)");
|
|
459
|
+
release();
|
|
460
|
+
await Promise.all([p1, p2, p3]);
|
|
461
|
+
assert.equal(coreCalls, 1);
|
|
462
|
+
assert.equal(poller.snapshot.servers.docker, true);
|
|
463
|
+
assert.deepEqual(poller.snapshot.hosts, [{ hostId: "h1" }]);
|
|
464
|
+
assert.ok(poller.snapshot.refreshedAt);
|
|
465
|
+
// Cycle terminé : un nouveau refresh relance bien une collecte.
|
|
466
|
+
await poller.refreshCore();
|
|
467
|
+
assert.equal(coreCalls, 2);
|
|
468
|
+
// Scan disque indépendant du cycle core (cadence 1/N côté poller).
|
|
469
|
+
assert.equal(diskCalls, 0);
|
|
470
|
+
await poller.refreshDisk();
|
|
471
|
+
assert.equal(diskCalls, 1);
|
|
472
|
+
assert.ok(poller.snapshot.diskRefreshedAt);
|
|
473
|
+
poller.stop();
|
|
474
|
+
console.log("OK — createFleetSnapshotPoller (non réentrant, snapshot)");
|
|
475
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@creezio/observability",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "Observabilité — activité/CP + ops/fleet + fleet-collector (P25) + usage analytics (N6) + request-logs admin (O5)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist-cjs/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"creezio-fleet-collector": "./fleet-collector/server.mjs"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist-cjs/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./ui": {
|
|
19
|
+
"types": "./ui/index.ts",
|
|
20
|
+
"import": "./ui/index.ts",
|
|
21
|
+
"default": "./ui/index.ts"
|
|
22
|
+
},
|
|
23
|
+
"./fleet-collector/*": "./fleet-collector/*"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"dist-cjs",
|
|
28
|
+
"ui",
|
|
29
|
+
"fleet-collector",
|
|
30
|
+
"scripts"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"fleet-collector": "node fleet-collector/server.mjs",
|
|
36
|
+
"test:fleet-collector": "node fleet-collector/test-fleet-collector.mjs",
|
|
37
|
+
"test:server-admin": "node fleet-collector/test-server-admin.mjs",
|
|
38
|
+
"test:api-endpoints": "node scripts/test-api-endpoints-registry.mjs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@creezio/api-kernel": "^0.22.0",
|
|
42
|
+
"@creezio/fleet": "^0.22.0",
|
|
43
|
+
"@creezio/platform-core": "^0.22.0",
|
|
44
|
+
"hono": "^4.12.32"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.15.3",
|
|
48
|
+
"@types/react": "^18.3.12",
|
|
49
|
+
"typescript": "^5.8.3"
|
|
50
|
+
},
|
|
51
|
+
"license": "UNLICENSED",
|
|
52
|
+
"module": "./dist/index.js",
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@creezio/auth": ">=0.4.0",
|
|
55
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
56
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
57
|
+
"class-variance-authority": "^0.7.0",
|
|
58
|
+
"clsx": "^2.0.0",
|
|
59
|
+
"lucide-react": ">=0.400.0",
|
|
60
|
+
"next": ">=14.0.0",
|
|
61
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
62
|
+
"recharts": ">=2.0.0",
|
|
63
|
+
"tailwind-merge": "^2.0.0 || ^3.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@creezio/auth": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@radix-ui/react-slot": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@radix-ui/react-tabs": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"class-variance-authority": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"clsx": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"lucide-react": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"react": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"recharts": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"tailwind-merge": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"next": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"publishConfig": {
|
|
98
|
+
"registry": "https://registry.npmjs.org",
|
|
99
|
+
"access": "public"
|
|
100
|
+
},
|
|
101
|
+
"repository": {
|
|
102
|
+
"type": "git",
|
|
103
|
+
"url": "https://github.com/creezio/creezio.git",
|
|
104
|
+
"directory": "packages/observability"
|
|
105
|
+
},
|
|
106
|
+
"engines": {
|
|
107
|
+
"node": ">=22.5"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Smoke O5 — buildApiEndpointsRegistry + createApiEndpointsRoutes.
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
import {
|
|
8
|
+
buildApiEndpointsRegistry,
|
|
9
|
+
collectHonoRoutes,
|
|
10
|
+
createApiEndpointsRoutes,
|
|
11
|
+
} from "../dist/api-endpoints/index.js";
|
|
12
|
+
|
|
13
|
+
const openapi = {
|
|
14
|
+
paths: {
|
|
15
|
+
"/api/v1/health": {
|
|
16
|
+
get: {
|
|
17
|
+
summary: "Health",
|
|
18
|
+
tags: ["core"],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const registry = buildApiEndpointsRegistry({
|
|
25
|
+
routes: [
|
|
26
|
+
{ method: "GET", path: "/api/v1/health" },
|
|
27
|
+
{ method: "POST", path: "/api/v1/notes" },
|
|
28
|
+
{ method: "GET", path: "/api/v1/health" }, // dedupe
|
|
29
|
+
],
|
|
30
|
+
openApiDocument: openapi,
|
|
31
|
+
source: "test",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(registry.endpoints.length, 2);
|
|
35
|
+
assert.equal(registry.endpoints[0].path, "/api/v1/health");
|
|
36
|
+
assert.equal(registry.endpoints[0].documented, true);
|
|
37
|
+
assert.equal(registry.endpoints[0].summary, "Health");
|
|
38
|
+
assert.equal(registry.endpoints[1].documented, false);
|
|
39
|
+
|
|
40
|
+
const app = new Hono();
|
|
41
|
+
app.get("/ping", (c) => c.json({ ok: true }));
|
|
42
|
+
app.route(
|
|
43
|
+
"/",
|
|
44
|
+
createApiEndpointsRoutes({
|
|
45
|
+
getRegistry: () =>
|
|
46
|
+
buildApiEndpointsRegistry({
|
|
47
|
+
routes: collectHonoRoutes(app, "/api/v1/admin"),
|
|
48
|
+
source: "hono-test",
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const res = await app.request("http://local/endpoints");
|
|
54
|
+
assert.equal(res.status, 200);
|
|
55
|
+
const body = await res.json();
|
|
56
|
+
assert.ok(Array.isArray(body.endpoints));
|
|
57
|
+
assert.ok(body.endpoints.some((e) => e.path.endsWith("/ping")));
|
|
58
|
+
assert.ok(body.endpoints.some((e) => e.path.endsWith("/endpoints")));
|
|
59
|
+
|
|
60
|
+
console.log("OK test-api-endpoints-registry");
|