@lifeaitools/clauth 1.30.15 → 1.30.16
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.
|
@@ -82,6 +82,23 @@ function appendSupervisorEvent(event) {
|
|
|
82
82
|
appendJsonl(file("events.jsonl"), { ts: now(), ...event });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
async function probeSurfaceHealth(url, fetchImpl, timeoutMs) {
|
|
86
|
+
try {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetchImpl(url, { signal: controller.signal });
|
|
91
|
+
return response?.ok
|
|
92
|
+
? { healthy: true, error: null }
|
|
93
|
+
: { healthy: false, error: `HTTP ${response?.status ?? "unknown"}` };
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { healthy: false, error: err?.name === "AbortError" ? "health timeout" : String(err?.message || err) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
/**
|
|
86
103
|
* Probe enabled local clauth-owned surfaces and repair an unavailable one via
|
|
87
104
|
* the same governed action path exposed to Dev Center. External/plugin-owned
|
|
@@ -95,21 +112,9 @@ export async function reconcileSurfaceHealth({ fetchImpl = globalThis.fetch, tim
|
|
|
95
112
|
const url = healthUrlForSurface(surface);
|
|
96
113
|
if (!url) continue;
|
|
97
114
|
const observedAt = now();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const controller = new AbortController();
|
|
102
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
|
-
try {
|
|
104
|
-
const response = await fetchImpl(url, { signal: controller.signal });
|
|
105
|
-
healthy = Boolean(response?.ok);
|
|
106
|
-
if (!healthy) error = `HTTP ${response?.status ?? "unknown"}`;
|
|
107
|
-
} finally {
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
}
|
|
110
|
-
} catch (err) {
|
|
111
|
-
error = err?.name === "AbortError" ? "health timeout" : String(err?.message || err);
|
|
112
|
-
}
|
|
115
|
+
const health = await probeSurfaceHealth(url, fetchImpl, timeoutMs);
|
|
116
|
+
const healthy = health.healthy;
|
|
117
|
+
const error = health.error;
|
|
113
118
|
|
|
114
119
|
if (healthy) {
|
|
115
120
|
updateSurfaceState(id, { state: "current", last_health_at: observedAt, last_health_ok: true, last_health_error: null });
|
|
@@ -133,9 +138,18 @@ export async function reconcileSurfaceHealth({ fetchImpl = globalThis.fetch, tim
|
|
|
133
138
|
});
|
|
134
139
|
appendSupervisorEvent({ kind: "surface_health_failed", surface_id: id, error, health: url });
|
|
135
140
|
const receipt = runSurfaceAction(id, "reconcile", "supervisor-health-loop");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
const commandCompleted = receipt?.resulting_state?.ok === true;
|
|
142
|
+
const postHealth = commandCompleted ? await probeSurfaceHealth(url, fetchImpl, timeoutMs) : { healthy: false, error: receipt?.resulting_state?.state || "reconcile_failed" };
|
|
143
|
+
const repaired = commandCompleted && postHealth.healthy;
|
|
144
|
+
updateSurfaceState(id, {
|
|
145
|
+
state: repaired ? "current" : "unavailable",
|
|
146
|
+
last_health_at: now(),
|
|
147
|
+
last_health_ok: repaired,
|
|
148
|
+
last_health_error: repaired ? null : postHealth.error,
|
|
149
|
+
last_reconcile_operation_id: receipt?.operationId || null,
|
|
150
|
+
});
|
|
151
|
+
appendSupervisorEvent({ kind: repaired ? "surface_reconciled" : "surface_reconcile_failed", surface_id: id, operation_id: receipt?.operationId || null, command_completed: commandCompleted, health_ok: postHealth.healthy, error: postHealth.error || null });
|
|
152
|
+
inspected.push({ surface_id: id, state: repaired ? "reconciled" : "reconcile_failed", error: postHealth.error || error, operation_id: receipt?.operationId || null, observed_at: observedAt });
|
|
139
153
|
}
|
|
140
154
|
return { inspected };
|
|
141
155
|
}
|
|
@@ -218,14 +218,46 @@ test("health reconciliation marks a failed clauth surface and repairs it through
|
|
|
218
218
|
}],
|
|
219
219
|
}));
|
|
220
220
|
discoverPlugins();
|
|
221
|
+
let healthCalls = 0;
|
|
221
222
|
const result = await reconcileSurfaceHealth({
|
|
222
|
-
fetchImpl: async () => ({ ok:
|
|
223
|
+
fetchImpl: async () => ({ ok: ++healthCalls > 1, status: 503 }),
|
|
223
224
|
});
|
|
224
225
|
assert.equal(result.inspected[0].surface_id, "health-demo:primary");
|
|
225
226
|
assert.equal(result.inspected[0].state, "reconciled");
|
|
226
227
|
const surface = listSurfaces().find((item) => item.plugin_id === "health-demo");
|
|
227
228
|
assert.equal(surface.state, "current");
|
|
228
229
|
assert.ok(surface.last_reconcile_operation_id);
|
|
230
|
+
assert.equal(healthCalls, 2);
|
|
231
|
+
} finally {
|
|
232
|
+
if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
|
|
233
|
+
else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
|
|
234
|
+
if (oldManaged === undefined) delete process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
|
|
235
|
+
else process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = oldManaged;
|
|
236
|
+
if (oldUser === undefined) delete process.env.CLAUTH_USER_PLUGIN_ROOTS;
|
|
237
|
+
else process.env.CLAUTH_USER_PLUGIN_ROOTS = oldUser;
|
|
238
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("health reconciliation does not claim current when a successful command leaves health down", async () => {
|
|
243
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-post-health-"));
|
|
244
|
+
const oldDir = process.env.CLAUTH_SUPERVISOR_DIR;
|
|
245
|
+
const oldManaged = process.env.CLAUTH_MANAGED_PLUGIN_ROOTS;
|
|
246
|
+
const oldUser = process.env.CLAUTH_USER_PLUGIN_ROOTS;
|
|
247
|
+
process.env.CLAUTH_SUPERVISOR_DIR = root;
|
|
248
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
|
|
249
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
250
|
+
try {
|
|
251
|
+
writePlugin(root, "managed", "post-health-demo", baseManifest("post-health-demo", {
|
|
252
|
+
core: true,
|
|
253
|
+
enable_default: true,
|
|
254
|
+
surfaces: [{ id: "primary", destination: "local/clauth/pm2", lifecycle_owner: "clauth", port: 39113, health: "/health", restart: [process.execPath, "--version"] }],
|
|
255
|
+
}));
|
|
256
|
+
discoverPlugins();
|
|
257
|
+
const result = await reconcileSurfaceHealth({ fetchImpl: async () => ({ ok: false, status: 503 }) });
|
|
258
|
+
assert.equal(result.inspected[0].state, "reconcile_failed");
|
|
259
|
+
assert.equal(listSurfaces()[0].state, "unavailable");
|
|
260
|
+
assert.equal(listSurfaces()[0].last_health_ok, false);
|
|
229
261
|
} finally {
|
|
230
262
|
if (oldDir === undefined) delete process.env.CLAUTH_SUPERVISOR_DIR;
|
|
231
263
|
else process.env.CLAUTH_SUPERVISOR_DIR = oldDir;
|