Files
paperclip/server/src/adapters/http/test.ts
Forgotten f80a802592 Add adapter environment testing infrastructure
Introduce testEnvironment() on ServerAdapterModule with structured
pass/warn/fail diagnostics for all four adapter types (claude_local,
codex_local, process, http). Adds POST test-environment endpoint,
shared types/validators, adapter test implementations, and UI API
client. Includes asset type foundations used by related features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:23 -06:00

117 lines
3.3 KiB
TypeScript

import type {
AdapterEnvironmentCheck,
AdapterEnvironmentTestContext,
AdapterEnvironmentTestResult,
} from "../types.js";
import { asString, parseObject } from "../utils.js";
function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] {
if (checks.some((check) => check.level === "error")) return "fail";
if (checks.some((check) => check.level === "warn")) return "warn";
return "pass";
}
function normalizeMethod(input: string): string {
const trimmed = input.trim();
return trimmed.length > 0 ? trimmed.toUpperCase() : "POST";
}
export async function testEnvironment(
ctx: AdapterEnvironmentTestContext,
): Promise<AdapterEnvironmentTestResult> {
const checks: AdapterEnvironmentCheck[] = [];
const config = parseObject(ctx.config);
const urlValue = asString(config.url, "");
const method = normalizeMethod(asString(config.method, "POST"));
if (!urlValue) {
checks.push({
code: "http_url_missing",
level: "error",
message: "HTTP adapter requires a URL.",
hint: "Set adapterConfig.url to an absolute http(s) endpoint.",
});
return {
adapterType: ctx.adapterType,
status: summarizeStatus(checks),
checks,
testedAt: new Date().toISOString(),
};
}
let url: URL | null = null;
try {
url = new URL(urlValue);
} catch {
checks.push({
code: "http_url_invalid",
level: "error",
message: `Invalid URL: ${urlValue}`,
});
}
if (url && url.protocol !== "http:" && url.protocol !== "https:") {
checks.push({
code: "http_url_protocol_invalid",
level: "error",
message: `Unsupported URL protocol: ${url.protocol}`,
hint: "Use an http:// or https:// endpoint.",
});
}
if (url) {
checks.push({
code: "http_url_valid",
level: "info",
message: `Configured endpoint: ${url.toString()}`,
});
}
checks.push({
code: "http_method_configured",
level: "info",
message: `Configured method: ${method}`,
});
if (url && (url.protocol === "http:" || url.protocol === "https:")) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(url, {
method: "HEAD",
signal: controller.signal,
});
if (!response.ok && response.status !== 405 && response.status !== 501) {
checks.push({
code: "http_endpoint_probe_unexpected_status",
level: "warn",
message: `Endpoint probe returned HTTP ${response.status}.`,
hint: "Verify the endpoint is reachable from the Paperclip server host.",
});
} else {
checks.push({
code: "http_endpoint_probe_ok",
level: "info",
message: "Endpoint responded to a HEAD probe.",
});
}
} catch (err) {
checks.push({
code: "http_endpoint_probe_failed",
level: "warn",
message: err instanceof Error ? err.message : "Endpoint probe failed",
hint: "This may be expected in restricted networks; verify connectivity when invoking runs.",
});
} finally {
clearTimeout(timeout);
}
}
return {
adapterType: ctx.adapterType,
status: summarizeStatus(checks),
checks,
testedAt: new Date().toISOString(),
};
}