71 lines
6.9 KiB
TypeScript
71 lines
6.9 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function compareSemver(left: unknown, right: string): number {
|
|
if (typeof left !== "string") return -1;
|
|
const a = left.split(".").map((part) => Number.parseInt(part, 10));
|
|
const b = right.split(".").map((part) => Number.parseInt(part, 10));
|
|
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
const delta = (Number.isFinite(a[i]) ? a[i] : 0) - (Number.isFinite(b[i]) ? b[i] : 0);
|
|
if (delta !== 0) return delta;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const rustBridge = readFileSync("src/components/backend-core/src/ssh_bridge.rs", "utf8");
|
|
const rustData = readFileSync("src/components/backend-core/src/ssh_data_channel.rs", "utf8");
|
|
const rustMain = readFileSync("src/components/backend-core/src/main.rs", "utf8");
|
|
const rustState = readFileSync("src/components/backend-core/src/state.rs", "utf8");
|
|
const rustProviderRegistry = readFileSync("src/components/backend-core/src/provider_registry.rs", "utf8");
|
|
const provider = readFileSync("src/components/provider-gateway/src/index.ts", "utf8");
|
|
const providerRegistryTs = readFileSync("src/components/backend-core/src/provider-registry.ts", "utf8");
|
|
const providerPackage = JSON.parse(readFileSync("src/components/provider-gateway/package.json", "utf8")) as { version?: unknown };
|
|
const shared = readFileSync("src/components/shared/src/index.ts", "utf8");
|
|
const compose = readFileSync("docker-compose.yml", "utf8");
|
|
const config = readFileSync("config.json", "utf8");
|
|
const backendCoreDockerfile = readFileSync("src/components/backend-core/Dockerfile", "utf8");
|
|
|
|
assertCondition(rustBridge.includes('"host.ssh.tcp-pool"'), "backend-core ssh bridge must require host.ssh.tcp-pool");
|
|
assertCondition(rustBridge.includes("provider-gateway-upgrade-required"), "old provider must fail with upgrade-required classification");
|
|
assertCondition(rustBridge.includes("provider-data-pool-exhausted"), "tcp pool exhaustion must be visible");
|
|
assertCondition(rustBridge.includes("remove_ssh_data_channel") && rustBridge.includes("ssh_data_channel_removed_after_control_error"), "control-fallback host_ssh_error must remove the claimed data channel");
|
|
assertCondition(rustBridge.includes("provider-data-channel-missing"), "missing data channel must be classified for clients");
|
|
assertCondition(!rustBridge.includes('"host_ssh_input"'), "ssh input must not fall back to provider control websocket");
|
|
assertCondition(!rustBridge.includes('"host_ssh_eof"'), "ssh eof must not fall back to provider control websocket");
|
|
assertCondition(!rustBridge.includes('"host_ssh_close"'), "ssh close must not fall back to provider control websocket");
|
|
assertCondition(rustBridge.includes('json!({ "type": "input"') && rustBridge.includes('json!({ "type": "eof"'), "ssh bridge must send stdin/eof over data frames");
|
|
assertCondition(!rustProviderRegistry.includes('"host_ssh_opened"') && !rustProviderRegistry.includes('"host_ssh_data"') && !rustProviderRegistry.includes('"host_ssh_exit"'), "rust provider registry must not accept old websocket ssh data messages");
|
|
|
|
assertCondition(rustData.includes("TcpListener") && rustData.includes("read_data_frame") && rustData.includes("write_data_frame"), "backend-core must expose raw TCP data frame listener");
|
|
assertCondition(rustData.includes("pub async fn remove_ssh_data_channel") && rustData.includes("channels.remove(&key)") && rustData.includes(".max_by_key(|channel| channel.connected_at_millis)"), "backend-core must purge stale/dead data channels and prefer the freshest idle channel");
|
|
assertCondition(rustData.includes("SSH_DATA_PROTOCOL") && rustData.includes("unidesk-host-ssh-tcp-pool-v1"), "tcp data protocol must be explicit");
|
|
assertCondition(rustData.includes("base64::engine::general_purpose::STANDARD.encode(payload)"), "only client-facing websocket payload should remain base64 encoded");
|
|
assertCondition(rustMain.includes("providerDataTcpUrl") && rustMain.includes("spawn_ssh_data_listener"), "backend-core startup must expose provider data listener visibility");
|
|
assertCondition(rustState.includes("active_ssh_data_channels") && rustState.includes("data_channel_id"), "backend state must track data channels separately from provider control socket");
|
|
|
|
assertCondition(provider.includes("createConnection") && provider.includes("sshDataChannels"), "provider-gateway must use direct TCP sockets for ssh data pool");
|
|
assertCondition(provider.includes("PROVIDER_DATA_POOL_SIZE") && provider.includes("providerGatewaySshDataPoolReady"), "provider-gateway must expose pool config and heartbeat labels");
|
|
assertCondition(provider.includes('capabilities.push("host.ssh", "host.ssh.tcp-pool")'), "provider-gateway must declare tcp-pool capability only with host ssh");
|
|
assertCondition(provider.includes("acquireSshDataChannel") && provider.includes("releaseSshDataChannel"), "provider-gateway must claim/release one data channel per ssh session");
|
|
assertCondition(provider.includes("writeSshDataFrame(dataChannel") && provider.includes("sendSshDataSessionFrame"), "provider-gateway ssh output must use tcp data frames");
|
|
assertCondition(!provider.includes('parsed.type === "host_ssh_input"') && !provider.includes('parsed.type === "host_ssh_close"'), "provider-gateway must not retain old websocket ssh input handlers");
|
|
assertCondition(compareSemver(providerPackage.version, "0.2.29") >= 0, "provider-gateway tcp-pool contract must not regress below the deployed tcp-pool version", providerPackage);
|
|
|
|
assertCondition(shared.includes('transport: "tcp-pool"') && shared.includes("dataChannelId: string"), "shared host_ssh_open contract must require tcp-pool fields");
|
|
assertCondition(!shared.includes("CoreHostSshInputMessage") && !shared.includes("ProviderHostSshDataMessage"), "shared protocol must remove old websocket ssh data contracts");
|
|
assertCondition(!providerRegistryTs.includes("host_ssh_data") && !providerRegistryTs.includes("host_ssh_exit"), "typescript backend registry must not forward old websocket ssh data messages");
|
|
assertCondition(compose.includes("UNIDESK_PROVIDER_DATA_PORT") && compose.includes("PROVIDER_DATA_POOL_SIZE"), "compose must wire provider data port and pool size");
|
|
assertCondition(config.includes('"providerData"') && config.includes('"port": 18084') && config.includes('"containerPort": 8082'), "config.json must declare providerData port pair");
|
|
assertCondition(
|
|
backendCoreDockerfile.includes("ARG CARGO_BUILD_JOBS=1")
|
|
&& backendCoreDockerfile.includes('ENV CARGO_BUILD_JOBS=${CARGO_BUILD_JOBS}')
|
|
&& backendCoreDockerfile.includes('cargo build --release --locked --jobs "${CARGO_BUILD_JOBS}"'),
|
|
"backend-core main-server online build must keep cargo concurrency constrained",
|
|
);
|
|
assertCondition(compose.includes("UNIDESK_BACKEND_CORE_CARGO_BUILD_JOBS") && compose.includes("CARGO_BUILD_JOBS"), "compose must pass backend-core cargo build concurrency explicitly");
|
|
|
|
console.log(JSON.stringify({ ok: true, test: "ssh-data-tcp-pool-contract" }));
|