132 lines
6.1 KiB
TypeScript
132 lines
6.1 KiB
TypeScript
import {
|
|
artifactRegistryReadonlyResultFromCommand,
|
|
buildArtifactRegistryReadonlyProbe,
|
|
parseArtifactRegistryOptions,
|
|
} from "./src/artifact-registry";
|
|
import type { CommandResult } from "./src/command";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function asRecord(value: unknown, label: string): JsonRecord {
|
|
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, { value });
|
|
return value as JsonRecord;
|
|
}
|
|
|
|
function asStringArray(value: unknown, label: string): string[] {
|
|
assertCondition(Array.isArray(value), `${label} must be an array`, { value });
|
|
return (value as unknown[]).map(String);
|
|
}
|
|
|
|
function command(overrides: Partial<CommandResult>): CommandResult {
|
|
return {
|
|
command: ["frontend", "/api/dispatch", "D601", "host.ssh", "health"],
|
|
cwd: ".",
|
|
exitCode: 0,
|
|
stdout: "",
|
|
stderr: "",
|
|
signal: null,
|
|
timedOut: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const options = parseArtifactRegistryOptions(["--provider-id", "D601"]);
|
|
const probe = buildArtifactRegistryReadonlyProbe("health", options);
|
|
assertCondition(Buffer.byteLength(probe.script, "utf8") < 4000, "readonly registry probe script must fit provider-gateway host.ssh command length limit", {
|
|
bytes: Buffer.byteLength(probe.script, "utf8"),
|
|
remoteCommandShape: probe.remoteCommandShape,
|
|
});
|
|
|
|
const missing = asRecord(artifactRegistryReadonlyResultFromCommand(probe, command({
|
|
exitCode: 1,
|
|
stderr: "provider does not declare host.ssh capability: D601\n",
|
|
})), "missing provider ssh result");
|
|
assertCondition(missing.ok === false, "missing provider ssh command should fail", missing);
|
|
assertCondition(missing.failureClassification === "provider-ssh-command-missing", "missing provider ssh command classification mismatch", missing);
|
|
assertCondition(asStringArray(missing.failedScopes, "missing.failedScopes").includes("provider-ssh-command"), "missing provider ssh command scope should be reported", missing);
|
|
assertCondition(typeof missing.recommendedAction === "string" && missing.recommendedAction.length > 0, "missing provider ssh command should include recommendedAction", missing);
|
|
assertCondition(typeof missing.remoteCommandShape === "string" && missing.remoteCommandShape.includes("bash -lc"), "missing provider ssh command should include remoteCommandShape", missing);
|
|
|
|
const commandShape = asRecord(artifactRegistryReadonlyResultFromCommand(probe, command({
|
|
exitCode: 1,
|
|
stderr: "Error: host SSH command is too long: 4039 bytes\n",
|
|
})), "command shape result");
|
|
assertCondition(commandShape.ok === false, "oversized host.ssh command should fail", commandShape);
|
|
assertCondition(commandShape.failureClassification === "ssh-helper-command-shape-incompatible", "oversized host.ssh command classification mismatch", commandShape);
|
|
assertCondition(asStringArray(commandShape.failedScopes, "commandShape.failedScopes").includes("ssh-helper-command-shape"), "oversized host.ssh command scope should be reported", commandShape);
|
|
|
|
const timeout = asRecord(artifactRegistryReadonlyResultFromCommand(probe, command({
|
|
exitCode: null,
|
|
stderr: "host.ssh task task_contract did not finish within 30000ms\n",
|
|
timedOut: true,
|
|
})), "timeout result");
|
|
assertCondition(timeout.ok === false, "remote timeout should fail", timeout);
|
|
assertCondition(timeout.failureClassification === "remote-command-timeout", "remote timeout classification mismatch", timeout);
|
|
assertCondition(asStringArray(timeout.failedScopes, "timeout.failedScopes").includes("remote-command-timeout"), "remote timeout scope should be reported", timeout);
|
|
assertCondition(timeout.retryable === true, "remote timeout should be retryable", timeout);
|
|
|
|
const successStdout = [
|
|
"readonly=true",
|
|
"unit_path=/etc/systemd/system/unidesk-artifact-registry.service",
|
|
"compose_path=/home/ubuntu/.unidesk/artifact-registry/compose.yml",
|
|
"config_path=/home/ubuntu/.unidesk/artifact-registry/config.yml",
|
|
"storage_path=/home/ubuntu/.unidesk/registry-storage",
|
|
"unit_exists=true",
|
|
"compose_exists=true",
|
|
"config_exists=true",
|
|
"storage_exists=true",
|
|
"systemctl_available=true",
|
|
"unit_active=active",
|
|
"unit_enabled=enabled",
|
|
"docker_available=true",
|
|
"container_running=true",
|
|
"container_status=running",
|
|
"container_image=registry:2.8.3",
|
|
"container_restart_policy=unless-stopped",
|
|
"listener_count=1",
|
|
"bad_listener_count=0",
|
|
"loopback_only=true",
|
|
"curl_available=true",
|
|
"v2_http_code=200",
|
|
"config_hash=contract-config",
|
|
"compose_hash=contract-compose",
|
|
"unit_hash=contract-unit",
|
|
"expected_config_hash=contract-config",
|
|
"expected_compose_hash=contract-compose",
|
|
"expected_unit_hash=contract-unit",
|
|
"config_hash_matches=true",
|
|
"compose_hash_matches=true",
|
|
"unit_hash_matches=true",
|
|
"image_matches=true",
|
|
"",
|
|
].join("\n");
|
|
|
|
const success = asRecord(artifactRegistryReadonlyResultFromCommand(probe, command({
|
|
stdout: successStdout,
|
|
})), "success result");
|
|
assertCondition(success.ok === true, "healthy registry command should pass", success);
|
|
assertCondition(success.failureClassification === null, "healthy registry should not have a failure classification", success);
|
|
assertCondition(asStringArray(success.failedScopes, "success.failedScopes").length === 0, "healthy registry should not have failed scopes", success);
|
|
assertCondition(success.recommendedAction === "none", "healthy registry recommendedAction should be none", success);
|
|
assertCondition(success.remoteCommandShape === probe.remoteCommandShape, "healthy registry should echo remote command shape", success);
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"provider-ssh-command missing is classified distinctly",
|
|
"oversized host.ssh command shape is classified distinctly",
|
|
"remote host.ssh timeout is classified distinctly",
|
|
"successful registry readonly probe has no failed scopes",
|
|
],
|
|
classifications: {
|
|
missing: missing.failureClassification,
|
|
commandShape: commandShape.failureClassification,
|
|
timeout: timeout.failureClassification,
|
|
success: success.failureClassification,
|
|
},
|
|
}, null, 2)}\n`);
|