125 lines
10 KiB
TypeScript
125 lines
10 KiB
TypeScript
import { sshHelp } from "./src/help";
|
|
import { providerTriageRecommendedCrossChecks } from "./src/provider-triage";
|
|
import { formatSshFailureHint, parseSshArgs, parseSshInvocation, sshFailureHint } from "./src/ssh";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function assertThrows(fn: () => unknown, pattern: RegExp, message: string): void {
|
|
try {
|
|
fn();
|
|
} catch (error) {
|
|
const text = error instanceof Error ? error.message : String(error);
|
|
assertCondition(pattern.test(text), message, { error: text });
|
|
return;
|
|
}
|
|
throw new Error(`${message}: expected throw`);
|
|
}
|
|
|
|
export function runSshArgvGuidanceContract(): JsonRecord {
|
|
const argv = parseSshArgs(["argv", "true"]);
|
|
assertCondition(argv.invocationKind === "argv", "argv subcommand must be classified as argv", argv);
|
|
assertCondition(argv.remoteCommand === "'true'", "argv command must shell-quote each token", argv);
|
|
assertCondition(argv.requiresStdin === false, "argv command must not require stdin", argv);
|
|
assertCondition(sshFailureHint("D601", argv, 255, "kex_exchange_identification: Connection closed by remote host") === null, "argv failures must not produce ssh-like friction hint", argv);
|
|
|
|
const shortcut = parseSshArgs(["pwd"]);
|
|
assertCondition(shortcut.invocationKind === "argv", "safe command shortcuts must use argv quoting", shortcut);
|
|
assertCondition(shortcut.remoteCommand === "'pwd'", "safe command shortcut should be shell-quoted", shortcut);
|
|
|
|
const script = parseSshArgs(["script", "--shell", "bash", "--", "alpha beta"]);
|
|
assertCondition(script.invocationKind === "helper", "script stdin helper must be classified as helper", script);
|
|
assertCondition(script.remoteCommand === "'bash' '-s' '--' 'alpha beta'", "script helper must pass stdin to shell directly", script);
|
|
assertCondition(script.requiresStdin === true, "script helper must require stdin", script);
|
|
|
|
const k3sGuard = parseSshArgs(["k3s", "guard"]);
|
|
assertCondition(k3sGuard.invocationKind === "helper", "k3s guard must be classified as helper", k3sGuard);
|
|
assertCondition(k3sGuard.remoteCommand?.includes("KUBECONFIG") && k3sGuard.remoteCommand.includes("/etc/rancher/k3s/k3s.yaml"), "k3s guard must force native k3s kubeconfig", k3sGuard);
|
|
|
|
const k3sExec = parseSshArgs(["k3s", "exec", "--namespace", "hwlab-dev", "--deployment", "hwlab-cloud-api", "--", "node", "-e", "console.log(process.version)"]);
|
|
assertCondition(k3sExec.invocationKind === "helper", "k3s exec must be classified as helper", k3sExec);
|
|
assertCondition(k3sExec.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'node' '-e' 'console.log(process.version)'", "k3s exec must assemble kubectl argv without nested shell quoting", k3sExec);
|
|
|
|
const routeKubectl = parseSshInvocation("D601:k3s", ["kubectl", "get", "pods", "-n", "hwlab-dev"]);
|
|
assertCondition(routeKubectl.providerId === "D601", "route must preserve provider id", routeKubectl);
|
|
assertCondition(routeKubectl.route.plane === "k3s" && routeKubectl.route.entry === null, "route must keep kubectl as an operation, not as a route entry", routeKubectl);
|
|
assertCondition(routeKubectl.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'get' 'pods' '-n' 'hwlab-dev'", "D601:k3s kubectl must map to kubectl argv", routeKubectl);
|
|
|
|
const routeTarget = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["node", "-e", "console.log(process.version)"]);
|
|
assertCondition(routeTarget.route.namespace === "hwlab-dev" && routeTarget.route.resource === "hwlab-cloud-api", "route target must parse namespace and workload", routeTarget);
|
|
assertCondition(routeTarget.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'node' '-e' 'console.log(process.version)'", "D601:k3s:<namespace>:<workload> must default to deployment exec", routeTarget);
|
|
|
|
const routeScript = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["script", "--shell", "bash", "--", "arg"]);
|
|
assertCondition(routeScript.parsed.requiresStdin === true, "k3s script operation must stream local stdin", routeScript);
|
|
assertCondition(routeScript.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-i' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'bash' '-s' '--' 'arg'", "D601:k3s:<namespace>:<workload> script must map stdin to shell -s", routeScript);
|
|
|
|
const routeControlScript = parseSshInvocation("D601:k3s", ["script", "--shell", "bash", "--", "arg"]);
|
|
assertCondition(routeControlScript.parsed.requiresStdin === true, "k3s control-plane script operation must stream local stdin", routeControlScript);
|
|
assertCondition(routeControlScript.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'bash' '-s' '--' 'arg'", "D601:k3s script must inject native kubeconfig without manual export", routeControlScript);
|
|
|
|
const routeApplyPatch = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["apply-patch"]);
|
|
assertCondition(routeApplyPatch.parsed.requiresStdin === true, "k3s apply-patch operation must stream local patch stdin", routeApplyPatch);
|
|
assertCondition(routeApplyPatch.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-i' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'sh' '-s' '--'", "D601:k3s:<namespace>:<workload> apply-patch must enter pod with stdin", routeApplyPatch);
|
|
assertCondition(routeApplyPatch.parsed.stdinPrefix?.includes("apply_patch") && routeApplyPatch.parsed.stdinPrefix.includes("__UNIDESK_APPLY_PATCH_PAYLOAD__"), "k3s apply-patch operation must inject pod helper before patch stdin", routeApplyPatch);
|
|
assertCondition(!routeApplyPatch.parsed.stdinPrefix?.includes("python3") && !routeApplyPatch.parsed.stdinPrefix?.includes("node "), "k3s apply-patch operation must use the sh-only pod helper", routeApplyPatch);
|
|
assertCondition(routeApplyPatch.parsed.stdinSuffix === "\n__UNIDESK_APPLY_PATCH_PAYLOAD__\n", "k3s apply-patch operation must terminate patch heredoc", routeApplyPatch);
|
|
|
|
assertThrows(
|
|
() => parseSshInvocation("D601:k3s:kubectl", ["get", "pods"]),
|
|
/route must locate a target only.*ssh D601:k3s kubectl/u,
|
|
"operation names must not be accepted as k3s route segments",
|
|
);
|
|
assertThrows(
|
|
() => parseSshInvocation("D601:k3s:apply-patch:hwlab-dev:hwlab-cloud-api", []),
|
|
/route must locate a target only.*apply-patch/u,
|
|
"pod apply-patch must be an operation after the route",
|
|
);
|
|
|
|
const routePodTarget = parseSshInvocation("D601:k3s:hwlab-dev:pod/hwlab-cloud-api-abc:api", ["printenv", "HOSTNAME"]);
|
|
assertCondition(routePodTarget.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'pod/hwlab-cloud-api-abc' '-c' 'api' '--' 'printenv' 'HOSTNAME'", "pod route with container must preserve explicit pod kind", routePodTarget);
|
|
|
|
const sshLike = parseSshArgs(["echo hello"]);
|
|
const hint = sshFailureHint("D601", sshLike, 255, "kex_exchange_identification: Connection closed by remote host");
|
|
assertCondition(hint !== null, "ssh-like kex failure must produce a hint", sshLike);
|
|
assertCondition(hint?.try === "bun scripts/cli.ts ssh D601 script <<'SCRIPT'", "hint must provide canonical stdin script retry", hint);
|
|
assertCondition(hint?.triage.includes("provider triage D601"), "hint must provide provider triage command", hint);
|
|
const formatted = formatSshFailureHint(hint!);
|
|
assertCondition(formatted.startsWith("UNIDESK_SSH_HINT "), "formatted hint must have structured prefix", formatted);
|
|
assertCondition(!formatted.includes("echo hello"), "formatted hint must not echo the original remote command", formatted);
|
|
|
|
const timeoutHint = sshFailureHint("D601", sshLike, 255, "unidesk ssh bridge timed out waiting for provider session");
|
|
assertCondition(timeoutHint?.trigger === "timeout-or-kex", "provider session timeout must map to timeout-or-kex", timeoutHint);
|
|
|
|
const helpText = JSON.stringify(sshHelp());
|
|
assertCondition(helpText.includes("ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'"), "ssh help must recommend stdin script passthrough for shell scripts", helpText);
|
|
assertCondition(helpText.includes("ssh D601:k3s kubectl get pods -n hwlab-dev"), "ssh help must document k3s kubectl operation", helpText);
|
|
assertCondition(helpText.includes("ssh D601:k3s script <<'SCRIPT'"), "ssh help must document k3s control-plane script operation", helpText);
|
|
assertCondition(helpText.includes("ssh D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch <<'PATCH'"), "ssh help must document k3s pod apply-patch operation", helpText);
|
|
assertCondition(helpText.includes("ssh D601:k3s:hwlab-dev:hwlab-cloud-api script <<'SCRIPT'"), "ssh help must document k3s script operation", helpText);
|
|
assertCondition(helpText.includes("UNIDESK_SSH_HINT"), "ssh help must document structured failure hint", helpText);
|
|
|
|
const crossChecks = providerTriageRecommendedCrossChecks("D601");
|
|
assertCondition(crossChecks.includes("bun scripts/cli.ts ssh D601 argv true"), "provider triage cross-checks must keep argv true", crossChecks);
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"argv form is classified and quoted as the success path for non-interactive commands",
|
|
"stdin script form removes shell-command strings for host and k3s workload scripts",
|
|
"pod apply-patch operation injects helper and forwards patch stdin",
|
|
"legacy operation-in-route forms are rejected with canonical route-plus-operation guidance",
|
|
"k3s route stays location-only while operations fix native kubeconfig and assemble kubectl exec as argv",
|
|
"ssh-like timeout/kex failures emit one structured argv retry hint",
|
|
"help text documents stdin script passthrough and UNIDESK_SSH_HINT",
|
|
"provider triage recommendedCrossChecks keeps ssh D601 argv true",
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runSshArgvGuidanceContract(), null, 2)}\n`);
|
|
}
|