fix: cap tran runtime and remove local lock
This commit is contained in:
@@ -5,7 +5,18 @@ import path from "node:path";
|
||||
import { sshHelp } from "./src/help";
|
||||
import { providerTriageRecommendedCrossChecks } from "./src/provider-triage";
|
||||
import { extractRemoteCliOptions, remoteSshFrontendPlanForTest } from "./src/remote";
|
||||
import { formatSshFailureHint, formatSshRuntimeTimingHint, parseSshArgs, parseSshInvocation, remoteApplyPatchSource, sshFailureHint, sshRuntimeTimingHint } from "./src/ssh";
|
||||
import {
|
||||
formatSshFailureHint,
|
||||
formatSshRuntimeTimeoutHint,
|
||||
formatSshRuntimeTimingHint,
|
||||
parseSshArgs,
|
||||
parseSshInvocation,
|
||||
remoteApplyPatchSource,
|
||||
sshFailureHint,
|
||||
sshRuntimeTimeoutHint,
|
||||
sshRuntimeTimeoutMs,
|
||||
sshRuntimeTimingHint,
|
||||
} from "./src/ssh";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
@@ -60,57 +71,6 @@ function applyPatchFixture(args: string[], patch: string, files: Record<string,
|
||||
}
|
||||
}
|
||||
|
||||
function tranConcurrentLockFixture(): { status: number | null; stdout: string; stderr: string } {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "unidesk-tran-lock-contract-"));
|
||||
try {
|
||||
const fakeRepo = path.join(root, "repo");
|
||||
const fakeScripts = path.join(fakeRepo, "scripts");
|
||||
const fakeBin = path.join(root, "bin");
|
||||
mkdirSync(fakeScripts, { recursive: true });
|
||||
mkdirSync(fakeBin, { recursive: true });
|
||||
writeFileSync(path.join(fakeScripts, "cli.ts"), "// fake cli entry for tran wrapper contract\n", "utf8");
|
||||
const fakeBun = path.join(fakeBin, "bun");
|
||||
writeFileSync(fakeBun, [
|
||||
"#!/bin/sh",
|
||||
"if mkdir \"$FAKE_BUN_RUN_LOCK\" 2>/dev/null; then",
|
||||
" sleep 1",
|
||||
" rmdir \"$FAKE_BUN_RUN_LOCK\"",
|
||||
" exit 0",
|
||||
"fi",
|
||||
"echo fake bun observed overlapping tran execution >&2",
|
||||
"exit 42",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
chmodSync(fakeBun, 0o755);
|
||||
const tranPath = path.resolve("scripts/tran");
|
||||
return spawnSync("sh", ["-c", [
|
||||
`"${tranPath}" D601:/tmp pwd >/tmp/unidesk-tran-lock-one.out 2>/tmp/unidesk-tran-lock-one.err &`,
|
||||
"p1=$!",
|
||||
`"${tranPath}" D601:/tmp pwd >/tmp/unidesk-tran-lock-two.out 2>/tmp/unidesk-tran-lock-two.err &`,
|
||||
"p2=$!",
|
||||
"wait $p1; s1=$?",
|
||||
"wait $p2; s2=$?",
|
||||
"cat /tmp/unidesk-tran-lock-one.err /tmp/unidesk-tran-lock-two.err >&2",
|
||||
"printf '%s %s\\n' \"$s1\" \"$s2\"",
|
||||
].join("\n")], {
|
||||
cwd: path.resolve("."),
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${fakeBin}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||
UNIDESK_TRAN_REPO_ROOT: fakeRepo,
|
||||
UNIDESK_TRAN_LOCK_DIR: path.join(root, "locks"),
|
||||
UNIDESK_TRAN_LOCK_NOTICE_SECONDS: "0",
|
||||
UNIDESK_TRAN_LOCK_TIMEOUT_SECONDS: "10",
|
||||
FAKE_BUN_RUN_LOCK: path.join(root, "fake-bun-running"),
|
||||
},
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
});
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
const argv = parseSshArgs(["argv", "true"]);
|
||||
assertCondition(argv.invocationKind === "argv", "argv subcommand must be classified as argv", argv);
|
||||
@@ -405,6 +365,17 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
|
||||
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);
|
||||
assertCondition(sshRuntimeTimeoutMs({ UNIDESK_SSH_RUNTIME_TIMEOUT_MS: "120000" } as NodeJS.ProcessEnv) === 60_000, "ssh runtime timeout must cap at 60s", {});
|
||||
assertCondition(sshRuntimeTimeoutMs({ UNIDESK_TRAN_RUNTIME_TIMEOUT_MS: "2500" } as NodeJS.ProcessEnv) === 2500, "ssh runtime timeout must accept smaller explicit limits", {});
|
||||
const runtimeTimeout = sshRuntimeTimeoutHint({
|
||||
invocation: parseSshInvocation("G14:k3s", ["script"]),
|
||||
transport: "backend-core-broker",
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
const formattedRuntimeTimeout = formatSshRuntimeTimeoutHint(runtimeTimeout);
|
||||
assertCondition(formattedRuntimeTimeout.startsWith("UNIDESK_SSH_RUNTIME_TIMEOUT "), "runtime timeout hint must have structured prefix", formattedRuntimeTimeout);
|
||||
assertCondition(formattedRuntimeTimeout.includes("short query plus poll semantics"), "runtime timeout hint must point to short polling", formattedRuntimeTimeout);
|
||||
assertCondition(!formattedRuntimeTimeout.includes("kubectl"), "runtime timeout hint must not echo remote command text", formattedRuntimeTimeout);
|
||||
|
||||
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);
|
||||
@@ -423,8 +394,9 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
assertCondition(helpText.includes("apply-patch [--allow-loose]") && helpText.includes("low-context update hunks"), "ssh help must document apply-patch loose-context guard", 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);
|
||||
assertCondition(helpText.includes("UNIDESK_SSH_RUNTIME_TIMEOUT") && helpText.includes("UNIDESK_TRAN_TIMEOUT_HINT") && helpText.includes("60s") && helpText.includes("submit-and-poll"), "ssh help must document top-level runtime timeout and short polling discipline", helpText);
|
||||
assertCondition(helpText.includes("UNIDESK_SSH_TIMING") && helpText.includes("10s") && helpText.includes("slow successful calls are a distributed performance monitoring signal") && helpText.includes("Routine short calls do not emit timing noise"), "ssh help must document slow-only runtime timing hints", helpText);
|
||||
assertCondition(helpText.includes("UNIDESK_TRAN_SESSION_LOCK=0") && helpText.includes("provider session allocator"), "ssh help must document tran provider session serialization", helpText);
|
||||
assertCondition(helpText.includes("must not add provider/plane directory locks") && helpText.includes("k8s/Tekton/Argo/Lease"), "ssh help must document tran's no-local-lock boundary", 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);
|
||||
@@ -455,12 +427,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
const tranScript = readFileSync(new URL("./tran", import.meta.url), "utf8");
|
||||
assertCondition(tranScript.includes("CODE_QUEUE_DEV_CONTAINER_MASTER_HOST") && tranScript.includes("--main-server-ip"), "tran wrapper must auto-select frontend transport inside Code Queue runner pods", tranScript);
|
||||
assertCondition(tranScript.includes("UNIDESK_TRAN_LOCAL"), "tran wrapper must keep an explicit local override for diagnostics", tranScript);
|
||||
assertCondition(tranScript.includes("tran_lock_scope") && tranScript.includes("UNIDESK_TRAN_LOCK_DIR"), "tran wrapper must serialize concurrent provider session opens with a local sh lock", tranScript);
|
||||
assertCondition(tranScript.includes("*:win|*:win/*) plane=win"), "tran wrapper must lock win route calls separately from host/k3s calls", tranScript);
|
||||
const tranLock = tranConcurrentLockFixture();
|
||||
assertCondition(tranLock.status === 0, "tran lock fixture shell should complete", tranLock);
|
||||
assertCondition(tranLock.stdout.trim() === "0 0", "parallel tran invocations for one provider must serialize instead of overlapping fake bun", tranLock);
|
||||
assertCondition(!tranLock.stderr.includes("overlapping tran execution"), "tran provider lock must prevent overlapping provider session allocation", tranLock);
|
||||
assertCondition(!tranScript.includes("tran_lock_scope") && !tranScript.includes("UNIDESK_TRAN_LOCK_DIR") && !tranScript.includes("mkdir \"$lock_path\""), "tran wrapper must not add local provider/plane directory locks", tranScript);
|
||||
|
||||
const remoteSource = readFileSync(new URL("./src/remote.ts", import.meta.url), "utf8");
|
||||
assertCondition(remoteSource.includes("UNIDESK_REMOTE_HTTP_CLIENT") && remoteSource.includes("isCodeQueueRunnerEnv(env) ? \"curl\" : \"fetch\""), "remote frontend transport must default to curl HTTP in Code Queue runner environments", remoteSource);
|
||||
@@ -506,7 +473,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
"host apply-patch bootstraps only the apply_patch helper and uses a Perl fast path for large files",
|
||||
"remote frontend ssh uses authenticated /ws/ssh streaming instead of host.ssh dispatch task polling",
|
||||
"Code Queue runner image installs the tran wrapper and runner tran auto-selects remote frontend transport",
|
||||
"tran serializes concurrent non-interactive calls per provider/plane before opening provider SSH sessions",
|
||||
"tran does not add local provider/plane directory locks and leaves coordination to k8s/Tekton/Argo/Lease",
|
||||
"Code Queue runner remote frontend HTTP uses curl by default for non-ssh API calls to avoid Bun response-body native crashes",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user