1846 lines
120 KiB
TypeScript
1846 lines
120 KiB
TypeScript
import { PassThrough, Writable } from "node:stream";
|
||
import { spawnSync } from "node:child_process";
|
||
import { createHash } from "node:crypto";
|
||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { sshHelp } from "./src/help";
|
||
import { runApplyPatchV2, type ApplyPatchV2TimingSummary, type ApplyPatchV2BulkReplacementWritePlan } from "./src/apply-patch-v2";
|
||
import { providerTriageRecommendedCrossChecks } from "./src/provider-triage";
|
||
import { extractRemoteCliOptions, remoteSshFrontendPlanForTest } from "./src/remote";
|
||
import { runSshFileTransferOperation, type SshFileTransferCommandBuilders, type SshRemoteCommandExecutor } from "./src/ssh-file-transfer";
|
||
import {
|
||
formatSshFailureHint,
|
||
formatSshRuntimeTimeoutHint,
|
||
formatSshRuntimeTimingHint,
|
||
normalizeSshOperationArgs,
|
||
parseSshArgs,
|
||
parseSshInvocation,
|
||
remoteApplyPatchSource,
|
||
shellArgv,
|
||
sshFailureHint,
|
||
sshRouteSeparatorCompatibilityHint,
|
||
sshShellCompatibilityPrelude,
|
||
sshUserToolPathPrelude,
|
||
sshRuntimeTimeoutHint,
|
||
sshRuntimeTimeoutMs,
|
||
sshRuntimeTimingHint,
|
||
} 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`);
|
||
}
|
||
|
||
function sha256Hex(value: string): string {
|
||
return createHash("sha256").update(Buffer.from(value, "utf8")).digest("hex");
|
||
}
|
||
|
||
function sha256BufferHex(value: Buffer): string {
|
||
return createHash("sha256").update(value).digest("hex");
|
||
}
|
||
|
||
function sshShellScriptPrelude(): string {
|
||
return `${sshUserToolPathPrelude}\n${sshShellCompatibilityPrelude}`;
|
||
}
|
||
|
||
async function captureStdout(fn: () => Promise<number>): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||
const originalWrite = process.stdout.write;
|
||
const originalStderrWrite = process.stderr.write;
|
||
let stdout = "";
|
||
let stderr = "";
|
||
process.stdout.write = ((chunk: unknown, ...args: unknown[]) => {
|
||
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
const callback = args.find((arg): arg is () => void => typeof arg === "function");
|
||
if (callback) callback();
|
||
return true;
|
||
}) as typeof process.stdout.write;
|
||
process.stderr.write = ((chunk: unknown, ...args: unknown[]) => {
|
||
stderr += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
const callback = args.find((arg): arg is () => void => typeof arg === "function");
|
||
if (callback) callback();
|
||
return true;
|
||
}) as typeof process.stderr.write;
|
||
try {
|
||
const exitCode = await fn();
|
||
return { exitCode, stdout, stderr };
|
||
} finally {
|
||
process.stdout.write = originalWrite;
|
||
process.stderr.write = originalStderrWrite;
|
||
}
|
||
}
|
||
|
||
function decodeWinEncodedCommand(remoteCommand: string | null | undefined): string {
|
||
const text = String(remoteCommand ?? "");
|
||
const match = /'-EncodedCommand' '([^']+)'/u.exec(text);
|
||
assertCondition(match !== null, "win command must use PowerShell -EncodedCommand", remoteCommand);
|
||
return Buffer.from(match[1] ?? "", "base64").toString("utf16le");
|
||
}
|
||
|
||
function applyPatchFixture(args: string[], patch: string, files: Record<string, string>): { status: number | null; stdout: string; stderr: string; files: Record<string, string> } {
|
||
const root = mkdtempSync(path.join(os.tmpdir(), "unidesk-apply-patch-contract-"));
|
||
try {
|
||
const helperPath = path.join(root, "apply_patch");
|
||
writeFileSync(helperPath, remoteApplyPatchSource, "utf8");
|
||
for (const [relativePath, content] of Object.entries(files)) {
|
||
const target = path.join(root, relativePath);
|
||
writeFileSync(target, content, "utf8");
|
||
}
|
||
const run = spawnSync("sh", [helperPath, ...args], {
|
||
cwd: root,
|
||
input: patch,
|
||
encoding: "utf8",
|
||
});
|
||
const outputFiles: Record<string, string> = {};
|
||
for (const relativePath of Object.keys(files)) {
|
||
outputFiles[relativePath] = readFileSync(path.join(root, relativePath), "utf8");
|
||
}
|
||
return {
|
||
status: run.status,
|
||
stdout: run.stdout,
|
||
stderr: run.stderr,
|
||
files: outputFiles,
|
||
};
|
||
} finally {
|
||
rmSync(root, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
async function applyPatchV2FixtureAttempt(patch: string, files: Record<string, string>, options: { stderrOutput?: boolean } = {}): Promise<{ stdout: string; stderr: string; exitCode: number | null; files: Record<string, string>; commands: string[]; error: unknown | null }> {
|
||
const state = new Map(Object.entries(files));
|
||
const pendingWrites = new Map<string, string>();
|
||
const commands: string[] = [];
|
||
const stdin = new PassThrough();
|
||
stdin.end(patch);
|
||
let stdout = "";
|
||
let stderr = "";
|
||
const stdoutSink = new Writable({
|
||
write(chunk, _encoding, callback) {
|
||
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
callback();
|
||
},
|
||
});
|
||
const stderrSink = new Writable({
|
||
write(chunk, _encoding, callback) {
|
||
stderr += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
callback();
|
||
},
|
||
});
|
||
let error: unknown | null = null;
|
||
let exitCode: number | null = null;
|
||
try {
|
||
exitCode = await runApplyPatchV2({
|
||
stdin,
|
||
stdout: stdoutSink,
|
||
...(options.stderrOutput === true ? { stderr: stderrSink } : {}),
|
||
executor: {
|
||
async run(command, input) {
|
||
const operation = command[4] ?? "";
|
||
const target = command[5] ?? "";
|
||
commands.push([operation, ...command.slice(5)].join(" "));
|
||
if (operation === "stat") {
|
||
if (!state.has(target)) return { exitCode: 1, stdout: "", stderr: `missing ${target}` };
|
||
const content = state.get(target) ?? "";
|
||
return { exitCode: 0, stdout: `${Buffer.byteLength(content, "utf8")} ${sha256Hex(content)}\n`, stderr: "" };
|
||
}
|
||
if (operation === "read-b64-block") {
|
||
if (!state.has(target)) return { exitCode: 1, stdout: "", stderr: `missing ${target}` };
|
||
const content = Buffer.from(state.get(target) ?? "", "utf8");
|
||
const blockIndex = Number(command[6] ?? "-1");
|
||
const blockSize = Number(command[7] ?? "-1");
|
||
if (!Number.isSafeInteger(blockIndex) || !Number.isSafeInteger(blockSize) || blockIndex < 0 || blockSize <= 0) {
|
||
return { exitCode: 2, stdout: "", stderr: "bad read block args" };
|
||
}
|
||
const start = blockIndex * blockSize;
|
||
return { exitCode: 0, stdout: content.subarray(start, start + blockSize).toString("base64"), stderr: "" };
|
||
}
|
||
if (operation === "read-bulk-b64") {
|
||
const targets = command.slice(5);
|
||
const records: string[] = [];
|
||
for (const item of targets) {
|
||
if (!state.has(item)) return { exitCode: 1, stdout: "", stderr: `missing ${item}` };
|
||
const content = Buffer.from(state.get(item) ?? "", "utf8");
|
||
records.push([
|
||
Buffer.from(item, "utf8").toString("base64"),
|
||
String(content.length),
|
||
sha256Hex(content),
|
||
content.toString("base64"),
|
||
].join(" "));
|
||
}
|
||
return { exitCode: 0, stdout: `UNIDESK_APPLY_PATCH_V2_BULK_READ ${targets.length}\n${records.join("\n")}\n`, stderr: "" };
|
||
}
|
||
if (operation === "write-b64-argv") {
|
||
const expectedBytes = Number(command[6] ?? "-1");
|
||
const expectedSha256 = command[7] ?? "";
|
||
const content = Buffer.from(command.slice(8).join(""), "base64").toString("utf8");
|
||
if (Buffer.byteLength(content, "utf8") !== expectedBytes || sha256Hex(content) !== expectedSha256) {
|
||
return { exitCode: 23, stdout: "", stderr: "mock integrity mismatch" };
|
||
}
|
||
state.set(target, content);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-stdin") {
|
||
const expectedBytes = Number(command[6] ?? "-1");
|
||
const expectedSha256 = command[7] ?? "";
|
||
const content = Buffer.from(input ?? "", "base64").toString("utf8");
|
||
if (Buffer.byteLength(content, "utf8") !== expectedBytes || sha256Hex(content) !== expectedSha256) {
|
||
return { exitCode: 23, stdout: "", stderr: "mock integrity mismatch" };
|
||
}
|
||
state.set(target, content);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "apply-replacements-bulk-stdin") {
|
||
const expectedCount = Number(command[5] ?? "-1");
|
||
const records = (input ?? "").split(/\r?\n/u).filter((line) => line.trim().length > 0);
|
||
if (records.length !== expectedCount) return { exitCode: 23, stdout: "", stderr: "mock bulk replacement record count mismatch" };
|
||
for (const record of records) {
|
||
const fields = record.split(/\s+/u);
|
||
if (fields.length !== 6) return { exitCode: 23, stdout: "", stderr: "mock bulk replacement malformed record" };
|
||
const [pathB64, originalBytesText, originalSha256, finalBytesText, finalSha256, replacementsText] = fields;
|
||
const targetPath = Buffer.from(pathB64 ?? "", "base64").toString("utf8");
|
||
const original = state.get(targetPath);
|
||
if (original === undefined) return { exitCode: 1, stdout: "", stderr: `missing ${targetPath}` };
|
||
const originalBuffer = Buffer.from(original, "utf8");
|
||
if (originalBuffer.length !== Number(originalBytesText) || sha256Hex(original) !== originalSha256) return { exitCode: 23, stdout: "", stderr: "mock bulk replacement original integrity mismatch" };
|
||
const lines = original.split("\n");
|
||
if (lines.at(-1) === "") lines.pop();
|
||
for (const item of (replacementsText ?? "").split(";").filter(Boolean).reverse()) {
|
||
const [startText, oldLengthText, newB64] = item.split(",", 3);
|
||
const newLines = Buffer.from(newB64 ?? "", "base64").toString("utf8").split("\n");
|
||
if (newLines.at(-1) === "") newLines.pop();
|
||
lines.splice(Number(startText), Number(oldLengthText), ...newLines);
|
||
}
|
||
const updated = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
||
if (Buffer.byteLength(updated, "utf8") !== Number(finalBytesText) || sha256Hex(updated) !== finalSha256) return { exitCode: 23, stdout: "", stderr: "mock bulk replacement final integrity mismatch" };
|
||
state.set(targetPath, updated);
|
||
}
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-begin") {
|
||
pendingWrites.set(`${target}\0${command[6] ?? ""}`, "");
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-append") {
|
||
const key = `${target}\0${command[6] ?? ""}`;
|
||
if (!pendingWrites.has(key)) return { exitCode: 2, stdout: "", stderr: "missing pending write" };
|
||
pendingWrites.set(key, `${pendingWrites.get(key) ?? ""}${command[7] ?? ""}`);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-commit") {
|
||
const key = `${target}\0${command[6] ?? ""}`;
|
||
const expectedBytes = Number(command[7] ?? "-1");
|
||
const expectedSha256 = command[8] ?? "";
|
||
const content = Buffer.from(pendingWrites.get(key) ?? "", "base64").toString("utf8");
|
||
if (Buffer.byteLength(content, "utf8") !== expectedBytes || sha256Hex(content) !== expectedSha256) {
|
||
return { exitCode: 23, stdout: "", stderr: "mock integrity mismatch" };
|
||
}
|
||
state.set(target, content);
|
||
pendingWrites.delete(key);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "delete") {
|
||
state.delete(target);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "move") {
|
||
state.set(command[6] ?? "", state.get(target) ?? "");
|
||
state.delete(target);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
return { exitCode: 2, stdout: "", stderr: "bad op" };
|
||
},
|
||
},
|
||
});
|
||
} catch (caught) {
|
||
error = caught;
|
||
}
|
||
return { stdout, stderr, exitCode, files: Object.fromEntries(state), commands, error };
|
||
}
|
||
|
||
function applyPatchTimingFromStderr(stderr: string): ApplyPatchV2TimingSummary {
|
||
const line = stderr.split(/\r?\n/u).find((item) => item.startsWith("UNIDESK_APPLY_PATCH_TIMING "));
|
||
assertCondition(line !== undefined, "apply-patch stderr must include UNIDESK_APPLY_PATCH_TIMING", stderr);
|
||
return JSON.parse(line!.slice("UNIDESK_APPLY_PATCH_TIMING ".length)) as ApplyPatchV2TimingSummary;
|
||
}
|
||
|
||
async function applyPatchV2ActualShellFixtureAttempt(
|
||
patch: string,
|
||
files: Record<string, string>,
|
||
mutateInput?: (operation: string, input: string | undefined) => string | undefined,
|
||
mutateResult?: (operation: string, result: { exitCode: number; stdout: string; stderr: string }) => { exitCode: number; stdout: string; stderr: string },
|
||
): Promise<{ stdout: string; files: Record<string, string>; commands: string[]; error: unknown | null }> {
|
||
const root = mkdtempSync(path.join(os.tmpdir(), "unidesk-apply-patch-v2-shell-"));
|
||
const commands: string[] = [];
|
||
const stdin = new PassThrough();
|
||
stdin.end(patch);
|
||
let stdout = "";
|
||
const stdoutSink = new Writable({
|
||
write(chunk, _encoding, callback) {
|
||
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
callback();
|
||
},
|
||
});
|
||
try {
|
||
for (const [relativePath, content] of Object.entries(files)) {
|
||
const target = path.join(root, relativePath);
|
||
mkdirSync(path.dirname(target), { recursive: true });
|
||
writeFileSync(target, content, "utf8");
|
||
}
|
||
let error: unknown | null = null;
|
||
try {
|
||
await runApplyPatchV2({
|
||
stdin,
|
||
stdout: stdoutSink,
|
||
executor: {
|
||
async run(command, input) {
|
||
const operation = command[4] ?? "";
|
||
commands.push([operation, ...command.slice(5)].join(" "));
|
||
const run = spawnSync(command[0] ?? "sh", command.slice(1), {
|
||
cwd: root,
|
||
input: mutateInput ? mutateInput(operation, input) : input,
|
||
encoding: "utf8",
|
||
});
|
||
const result = {
|
||
exitCode: run.status ?? 255,
|
||
stdout: run.stdout,
|
||
stderr: run.stderr,
|
||
};
|
||
return mutateResult ? mutateResult(operation, result) : result;
|
||
},
|
||
},
|
||
});
|
||
} catch (caught) {
|
||
error = caught;
|
||
}
|
||
const outputFiles: Record<string, string> = {};
|
||
for (const relativePath of Object.keys(files)) {
|
||
const target = path.join(root, relativePath);
|
||
outputFiles[relativePath] = existsSync(target) ? readFileSync(target, "utf8") : "";
|
||
}
|
||
return { stdout, files: outputFiles, commands, error };
|
||
} finally {
|
||
rmSync(root, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
async function applyPatchV2Fixture(patch: string, files: Record<string, string>): Promise<{ stdout: string; files: Record<string, string>; commands: string[] }> {
|
||
const result = await applyPatchV2FixtureAttempt(patch, files);
|
||
if (result.error !== null) throw result.error;
|
||
return { stdout: result.stdout, files: result.files, commands: result.commands };
|
||
}
|
||
|
||
async function applyPatchV2FsBulkFixtureAttempt(patch: string, files: Record<string, string>): Promise<{ stdout: string; stderr: string; exitCode: number | null; files: Record<string, string>; operations: string[]; error: unknown | null }> {
|
||
const state = new Map(Object.entries(files));
|
||
const operations: string[] = [];
|
||
const stdin = new PassThrough();
|
||
stdin.end(patch);
|
||
let stdout = "";
|
||
let stderr = "";
|
||
const stdoutSink = new Writable({
|
||
write(chunk, _encoding, callback) {
|
||
stdout += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
callback();
|
||
},
|
||
});
|
||
const stderrSink = new Writable({
|
||
write(chunk, _encoding, callback) {
|
||
stderr += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||
callback();
|
||
},
|
||
});
|
||
let error: unknown | null = null;
|
||
let exitCode: number | null = null;
|
||
try {
|
||
exitCode = await runApplyPatchV2({
|
||
stdin,
|
||
stdout: stdoutSink,
|
||
stderr: stderrSink,
|
||
executor: {
|
||
fs: {
|
||
async stat(filePath) {
|
||
operations.push(`stat ${filePath}`);
|
||
const content = state.get(filePath);
|
||
if (content === undefined) throw new Error(`missing ${filePath}`);
|
||
const buffer = Buffer.from(content, "utf8");
|
||
return { bytes: buffer.length, sha256: sha256BufferHex(buffer) };
|
||
},
|
||
async readBlock(filePath, blockIndex, blockBytes) {
|
||
operations.push(`readBlock ${filePath}`);
|
||
const content = state.get(filePath);
|
||
if (content === undefined) throw new Error(`missing ${filePath}`);
|
||
const buffer = Buffer.from(content, "utf8");
|
||
return buffer.subarray(blockIndex * blockBytes, (blockIndex + 1) * blockBytes);
|
||
},
|
||
async writeFile(filePath, content) {
|
||
operations.push(`writeFile ${filePath}`);
|
||
state.set(filePath, content.toString("utf8"));
|
||
},
|
||
async deleteFile(filePath) {
|
||
operations.push(`deleteFile ${filePath}`);
|
||
state.delete(filePath);
|
||
},
|
||
async readFiles(paths) {
|
||
operations.push(`readFiles ${paths.join(",")}`);
|
||
const result = new Map<string, string>();
|
||
for (const filePath of paths) {
|
||
const content = state.get(filePath);
|
||
if (content === undefined) throw new Error(`missing ${filePath}`);
|
||
result.set(filePath, content);
|
||
}
|
||
return result;
|
||
},
|
||
async applyReplacementsBulk(paths: Iterable<string>, plans: Map<string, ApplyPatchV2BulkReplacementWritePlan>) {
|
||
const targets = Array.from(paths);
|
||
operations.push(`applyReplacementsBulk ${targets.join(",")}`);
|
||
for (const filePath of targets) {
|
||
const plan = plans.get(filePath);
|
||
const original = state.get(filePath);
|
||
if (plan === undefined || original === undefined) throw new Error(`missing replacement plan ${filePath}`);
|
||
const originalBuffer = Buffer.from(original, "utf8");
|
||
assertCondition(originalBuffer.length === plan.originalBytes && sha256BufferHex(originalBuffer) === plan.originalSha256, "fs bulk fixture original integrity mismatch", { filePath, plan });
|
||
const lines = original.split("\n");
|
||
if (lines.at(-1) === "") lines.pop();
|
||
for (const [start, oldLength, newLines] of [...plan.replacements].reverse()) {
|
||
lines.splice(start, oldLength, ...newLines);
|
||
}
|
||
const updated = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
||
const updatedBuffer = Buffer.from(updated, "utf8");
|
||
assertCondition(updatedBuffer.length === plan.finalBytes && sha256BufferHex(updatedBuffer) === plan.finalSha256, "fs bulk fixture final integrity mismatch", { filePath, plan });
|
||
state.set(filePath, updated);
|
||
}
|
||
},
|
||
},
|
||
},
|
||
});
|
||
} catch (caught) {
|
||
error = caught;
|
||
}
|
||
return { stdout, stderr, exitCode, files: Object.fromEntries(state), operations, error };
|
||
}
|
||
|
||
function fileTransferFixture(initial: Record<string, Buffer> = {}, options: { emptyReadOnce?: Record<string, number[]>; shortReadOnce?: Record<string, Record<string, number>> } = {}): {
|
||
state: Map<string, Buffer>;
|
||
commands: Array<{ operation: string; stdin: boolean }>;
|
||
executor: SshRemoteCommandExecutor;
|
||
builders: SshFileTransferCommandBuilders;
|
||
} {
|
||
const state = new Map(Object.entries(initial));
|
||
const pending = new Map<string, string>();
|
||
const emptyReadOnce = new Map(Object.entries(options.emptyReadOnce ?? {}).map(([target, blocks]) => [target, new Set(blocks)]));
|
||
const shortReadOnce = new Map(Object.entries(options.shortReadOnce ?? {}).map(([target, blocks]) => [target, new Map(Object.entries(blocks).map(([block, bytes]) => [Number(block), bytes]))]));
|
||
const commands: Array<{ operation: string; stdin: boolean }> = [];
|
||
const builders: SshFileTransferCommandBuilders = {
|
||
buildRouteCommand(route, command, options) {
|
||
return JSON.stringify({ route: route.raw, command, stdin: options?.stdin === true });
|
||
},
|
||
buildWindowsPowerShellCommand(script) {
|
||
return JSON.stringify({ route: "win", command: ["powershell", script], stdin: false });
|
||
},
|
||
};
|
||
const executor: SshRemoteCommandExecutor = {
|
||
async runRemoteCommand(remoteCommand, input) {
|
||
const payload = JSON.parse(remoteCommand) as { command: string[]; stdin?: boolean };
|
||
const command = payload.command;
|
||
const operation = command[4] ?? "";
|
||
const target = command[5] ?? "";
|
||
commands.push({ operation, stdin: payload.stdin === true });
|
||
if (operation === "stat") {
|
||
const content = state.get(target);
|
||
if (content === undefined) return { exitCode: 1, stdout: "", stderr: "missing" };
|
||
return { exitCode: 0, stdout: `${content.length} ${sha256BufferHex(content)}\n`, stderr: "" };
|
||
}
|
||
if (operation === "read-b64-block") {
|
||
const content = state.get(target);
|
||
if (content === undefined) return { exitCode: 1, stdout: "", stderr: "missing" };
|
||
const blockIndex = Number(command[6] ?? "-1");
|
||
const blockSize = Number(command[7] ?? "-1");
|
||
const start = blockIndex * blockSize;
|
||
const emptyBlocks = emptyReadOnce.get(target);
|
||
if (emptyBlocks?.has(blockIndex)) {
|
||
emptyBlocks.delete(blockIndex);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
const shortBlocks = shortReadOnce.get(target);
|
||
const shortBytes = shortBlocks?.get(blockIndex);
|
||
if (shortBytes !== undefined) {
|
||
shortBlocks?.delete(blockIndex);
|
||
return { exitCode: 0, stdout: content.subarray(start, start + Math.max(0, shortBytes)).toString("base64"), stderr: "" };
|
||
}
|
||
return { exitCode: 0, stdout: content.subarray(start, start + blockSize).toString("base64"), stderr: "" };
|
||
}
|
||
if (operation === "write-b64-argv" || operation === "write-b64-stdin") {
|
||
const expectedBytes = Number(command[6] ?? "-1");
|
||
const expectedSha256 = command[7] ?? "";
|
||
const encoded = operation === "write-b64-argv" ? command.slice(8).join("") : String(input ?? "");
|
||
const content = Buffer.from(encoded, "base64");
|
||
if (content.length !== expectedBytes || sha256BufferHex(content) !== expectedSha256) return { exitCode: 23, stdout: "", stderr: "integrity mismatch" };
|
||
state.set(target, content);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-begin") {
|
||
pending.set(`${target}\0${command[6] ?? ""}`, "");
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-append-stdin") {
|
||
const key = `${target}\0${command[6] ?? ""}`;
|
||
if (!pending.has(key)) return { exitCode: 2, stdout: "", stderr: "missing pending write" };
|
||
pending.set(key, `${pending.get(key) ?? ""}${String(input ?? "")}`);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
if (operation === "write-b64-commit") {
|
||
const key = `${target}\0${command[6] ?? ""}`;
|
||
const expectedBytes = Number(command[7] ?? "-1");
|
||
const expectedSha256 = command[8] ?? "";
|
||
const content = Buffer.from(pending.get(key) ?? "", "base64");
|
||
if (content.length !== expectedBytes || sha256BufferHex(content) !== expectedSha256) return { exitCode: 23, stdout: "", stderr: "integrity mismatch" };
|
||
state.set(target, content);
|
||
pending.delete(key);
|
||
return { exitCode: 0, stdout: "", stderr: "" };
|
||
}
|
||
return { exitCode: 2, stdout: "", stderr: `unsupported op ${operation}` };
|
||
},
|
||
};
|
||
return { state, commands, executor, builders };
|
||
}
|
||
|
||
export async function runSshArgvGuidanceContract(): Promise<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);
|
||
assertThrows(
|
||
() => parseSshInvocation("D601:/tmp", ["argv", "pwd && ls -la"]),
|
||
/one shell-like command string.*script --/u,
|
||
"argv must reject a single shell command string before the remote host treats it as an executable path",
|
||
);
|
||
const argvExplicitShell = parseSshInvocation("D601:/tmp", ["argv", "sh", "-c", "pwd && ls -la"]);
|
||
assertCondition(argvExplicitShell.parsed.remoteCommand === "'sh' '-c' 'pwd && ls -la'", "argv must still allow explicit sh -c as multi-token direct argv", argvExplicitShell);
|
||
|
||
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 hostWorkspace = parseSshInvocation("D601:/home/ubuntu/workspace/hwlab-dev", ["pwd"]);
|
||
assertCondition(hostWorkspace.route.plane === "host" && hostWorkspace.route.workspace === "/home/ubuntu/workspace/hwlab-dev", "host workspace route must parse provider:/absolute/path", hostWorkspace);
|
||
assertCondition(hostWorkspace.parsed.remoteCommand === "'pwd'", "host workspace route must leave operation argv independent from location", hostWorkspace);
|
||
|
||
const hostWorkspaceLongForm = parseSshInvocation("D601:host:/home/ubuntu/workspace/hwlab-dev", ["argv", "git", "status", "--short"]);
|
||
assertCondition(hostWorkspaceLongForm.route.workspace === "/home/ubuntu/workspace/hwlab-dev", "host: workspace route must parse as the same location model", hostWorkspaceLongForm);
|
||
assertCondition(hostWorkspaceLongForm.parsed.remoteCommand === "'git' 'status' '--short'", "host workspace argv operation must stay argv-quoted", hostWorkspaceLongForm);
|
||
|
||
const routeSeparatorArgs = normalizeSshOperationArgs(["--", "apply-patch"]);
|
||
assertCondition(JSON.stringify(routeSeparatorArgs) === JSON.stringify(["apply-patch"]), "route-level -- before an operation should be ignored for MiniMax compatibility", routeSeparatorArgs);
|
||
const routeSeparatorHint = sshRouteSeparatorCompatibilityHint(["--", "apply-patch"], routeSeparatorArgs);
|
||
assertCondition(routeSeparatorHint.includes("route-level -- is ignored") && routeSeparatorHint.includes("canonical form"), "route-level -- compatibility should emit a canonical hint", routeSeparatorHint);
|
||
const hostRouteSeparatorApplyPatch = parseSshInvocation("D601:/tmp", ["--", "apply-patch"]);
|
||
assertCondition(hostRouteSeparatorApplyPatch.parsed.requiresStdin === true && hostRouteSeparatorApplyPatch.parsed.remoteCommand === null, "host route-level -- apply-patch should still use local v2 apply-patch", hostRouteSeparatorApplyPatch);
|
||
const hostRouteSeparatorRg = parseSshInvocation("D601:/tmp", ["--", "rg", "-ne", "DEVICE_JOB_READ_ONLY_SUB_ACTIONS", "-ne", "ACCESS_SCHEMA_STATEMENTS", "internal/cloud/access-control.ts"]);
|
||
assertCondition(
|
||
hostRouteSeparatorRg.parsed.invocationKind === "argv"
|
||
&& hostRouteSeparatorRg.parsed.remoteCommand === "'rg' '-ne' 'DEVICE_JOB_READ_ONLY_SUB_ACTIONS' '-ne' 'ACCESS_SCHEMA_STATEMENTS' 'internal/cloud/access-control.ts'",
|
||
"host route-level -- rg should preserve argv quoting instead of turning regex | into a remote shell pipe",
|
||
hostRouteSeparatorRg,
|
||
);
|
||
|
||
const winPs = parseSshInvocation("D601:win", ["ps"]);
|
||
assertCondition(winPs.route.plane === "win" && winPs.parsed.requiresStdin === true && winPs.parsed.invocationKind === "helper", "win ps without args must read PowerShell from stdin", winPs);
|
||
const winPsScript = decodeWinEncodedCommand(winPs.parsed.remoteCommand);
|
||
assertCondition(
|
||
winPsScript.includes("[Console]::In.ReadToEnd()")
|
||
&& winPsScript.includes("unidesk-win-ps-")
|
||
&& winPsScript.includes("$ErrorActionPreference = 'Stop'")
|
||
&& winPsScript.includes("powershell.exe")
|
||
&& winPsScript.includes("$global:LASTEXITCODE"),
|
||
"win ps stdin launcher must execute a UTF-8 temp .ps1 with fail-fast semantics",
|
||
{ winPs, winPsScript },
|
||
);
|
||
|
||
const winPsCwd = parseSshInvocation("D601:win/c/test", ["ps", "Write-Output", "'中文'"]);
|
||
assertCondition(winPsCwd.route.plane === "win" && winPsCwd.route.workspace === String.raw`C:\test` && winPsCwd.parsed.requiresStdin === false, "win ps inline route must map slash workspace and not require stdin", winPsCwd);
|
||
const winPsCwdScript = decodeWinEncodedCommand(winPsCwd.parsed.remoteCommand);
|
||
assertCondition(
|
||
winPsCwdScript.includes("Set-Location -LiteralPath ''C:\\test''")
|
||
&& winPsCwdScript.includes("Write-Output ''中文''")
|
||
&& !winPsCwdScript.includes("chcp 65001"),
|
||
"win ps inline launcher must use PowerShell cwd semantics rather than cmd.exe batch setup",
|
||
{ winPsCwd, winPsCwdScript },
|
||
);
|
||
|
||
assertThrows(
|
||
() => parseSshInvocation("D601:win", ["script", "Get-Location"]),
|
||
/unsupported ssh win operation: script.*win ps/u,
|
||
"win route must reject POSIX script operation and point callers to ps",
|
||
);
|
||
|
||
const winSkills = parseSshInvocation("D601:win", ["skills", "--scope", "all", "--limit", "20"]);
|
||
assertCondition(winSkills.route.plane === "win" && winSkills.parsed.invocationKind === "helper", "win skills route must be a Windows helper operation", winSkills);
|
||
const winSkillsScript = decodeWinEncodedCommand(winSkills.parsed.remoteCommand);
|
||
assertCondition(winSkillsScript.includes(".agents\\skills") && winSkillsScript.includes(".codex\\skills") && winSkillsScript.includes("$limit = 20") && winSkillsScript.includes("ConvertTo-Json"), "win skills must discover Windows user skill roots as JSON", { winSkills, winSkillsScript });
|
||
|
||
const hostUploadParse = parseSshInvocation("D601", ["upload", "/tmp/local.bin", "/tmp/remote.bin"]);
|
||
assertCondition(hostUploadParse.parsed.remoteCommand === null && hostUploadParse.parsed.invocationKind === "helper", "host upload must be a structured local operation, not an ssh-like command string", hostUploadParse);
|
||
const winDownloadParse = parseSshInvocation("D601:win", ["download", String.raw`F:\Work\hwlab\.tmp\tool.mjs`, "/tmp/tool.mjs"]);
|
||
assertCondition(winDownloadParse.route.plane === "win" && winDownloadParse.parsed.remoteCommand === null, "win download must be handled by the file-transfer operation module", winDownloadParse);
|
||
const podUploadParse = parseSshInvocation("D601:k3s:unidesk:code-queue/root/unidesk", ["upload", "/tmp/local.bin", "/root/unidesk/.tmp/remote.bin"]);
|
||
assertCondition(podUploadParse.route.plane === "k3s" && podUploadParse.parsed.remoteCommand === null, "pod upload must keep k3s route as location-only and defer transfer execution to the operation module", podUploadParse);
|
||
|
||
const transferRoot = mkdtempSync(path.join(os.tmpdir(), "unidesk-transfer-contract-"));
|
||
try {
|
||
const localSource = path.join(transferRoot, "local-source.bin");
|
||
const localDownload = path.join(transferRoot, "downloaded", "local-copy.bin");
|
||
const payload = Buffer.from("hello 中文\n\0binary tail", "utf8");
|
||
writeFileSync(localSource, payload);
|
||
const transfer = fileTransferFixture();
|
||
const uploadResult = await captureStdout(() => runSshFileTransferOperation(hostUploadParse, ["upload", localSource, "/tmp/remote.bin"], transfer.executor, transfer.builders));
|
||
const uploadJson = JSON.parse(uploadResult.stdout) as JsonRecord;
|
||
const uploadVerification = uploadJson.verification as JsonRecord;
|
||
const uploadMatch = uploadVerification.match as JsonRecord;
|
||
const uploadSource = uploadVerification.source as JsonRecord;
|
||
const uploadTarget = uploadVerification.target as JsonRecord;
|
||
assertCondition(uploadResult.exitCode === 0 && uploadJson.verified === true, "upload should report verified JSON success", uploadResult);
|
||
assertCondition(
|
||
uploadVerification.automatic === true
|
||
&& uploadVerification.verified === true
|
||
&& uploadMatch.bytes === true
|
||
&& uploadMatch.sha256 === true
|
||
&& uploadSource.side === "local"
|
||
&& uploadTarget.side === "remote",
|
||
"upload should expose automatic endpoint verification so callers do not need manual sha256sum checks",
|
||
uploadJson,
|
||
);
|
||
assertCondition(transfer.state.get("/tmp/remote.bin")?.equals(payload), "upload must preserve binary and UTF-8 bytes in the mock remote file", transfer.commands);
|
||
const downloadResult = await captureStdout(() => runSshFileTransferOperation(parseSshInvocation("D601", ["download", "/tmp/remote.bin", localDownload]), ["download", "/tmp/remote.bin", localDownload], transfer.executor, transfer.builders));
|
||
const downloadJson = JSON.parse(downloadResult.stdout) as JsonRecord;
|
||
const downloadVerification = downloadJson.verification as JsonRecord;
|
||
const downloadMatch = downloadVerification.match as JsonRecord;
|
||
const downloadSource = downloadVerification.source as JsonRecord;
|
||
const downloadTarget = downloadVerification.target as JsonRecord;
|
||
assertCondition(downloadResult.exitCode === 0 && downloadJson.sha256 === sha256BufferHex(payload), "download should report the verified sha256", downloadResult);
|
||
assertCondition(
|
||
downloadVerification.automatic === true
|
||
&& downloadVerification.verified === true
|
||
&& downloadMatch.bytes === true
|
||
&& downloadMatch.sha256 === true
|
||
&& downloadSource.side === "remote"
|
||
&& downloadTarget.side === "local",
|
||
"download should expose automatic endpoint verification so callers do not need manual sha256sum checks",
|
||
downloadJson,
|
||
);
|
||
assertCondition(readFileSync(localDownload).equals(payload), "download must preserve binary and UTF-8 bytes locally", { commands: transfer.commands });
|
||
assertCondition(transfer.commands.some((item) => item.operation === "stat") && transfer.commands.some((item) => item.operation === "read-b64-block"), "file transfer should use stat plus chunked verified reads", transfer.commands);
|
||
|
||
const retryDownload = path.join(transferRoot, "downloaded", "retry-copy.bin");
|
||
const retryPayload = Buffer.from("0123456789abcdef".repeat(4096), "utf8");
|
||
const retryTransfer = fileTransferFixture({ "/tmp/retry-remote.bin": retryPayload }, { emptyReadOnce: { "/tmp/retry-remote.bin": [1] } });
|
||
const retryResult = await captureStdout(() => runSshFileTransferOperation(parseSshInvocation("D601", ["download", "--chunk-bytes", "1024", "/tmp/retry-remote.bin", retryDownload]), ["download", "--chunk-bytes", "1024", "/tmp/retry-remote.bin", retryDownload], retryTransfer.executor, retryTransfer.builders));
|
||
const retryJson = JSON.parse(retryResult.stdout) as JsonRecord;
|
||
const retryReadBlocks = retryTransfer.commands.filter((item) => item.operation === "read-b64-block");
|
||
assertCondition(retryResult.exitCode === 0 && retryJson.sha256 === sha256BufferHex(retryPayload), "download should retry a transient empty block and keep sha256 verification", retryResult);
|
||
assertCondition(retryReadBlocks.length === Number(retryJson.transfer && typeof retryJson.transfer === "object" ? (retryJson.transfer as JsonRecord).chunks : 0) + 1, "transient empty block should add exactly one repeated read without counting as a chunk", retryTransfer.commands);
|
||
assertCondition(retryResult.stderr.includes("unidesk.ssh.download.progress") && retryResult.stderr.includes("unidesk.ssh.download.empty-read-retry"), "download should emit bounded progress and retry events to stderr", retryResult.stderr);
|
||
assertCondition(readFileSync(retryDownload).equals(retryPayload), "retry download must preserve complete content after transient empty block", { commands: retryTransfer.commands });
|
||
|
||
const shortReadDownload = path.join(transferRoot, "downloaded", "short-read-copy.bin");
|
||
const shortReadPayload = Buffer.from("fedcba9876543210".repeat(4096), "utf8");
|
||
const shortReadTransfer = fileTransferFixture({ "/tmp/short-read-remote.bin": shortReadPayload }, { shortReadOnce: { "/tmp/short-read-remote.bin": { 2: 512 } } });
|
||
const shortReadResult = await captureStdout(() => runSshFileTransferOperation(parseSshInvocation("D601", ["download", "--chunk-bytes", "1024", "/tmp/short-read-remote.bin", shortReadDownload]), ["download", "--chunk-bytes", "1024", "/tmp/short-read-remote.bin", shortReadDownload], shortReadTransfer.executor, shortReadTransfer.builders));
|
||
const shortReadJson = JSON.parse(shortReadResult.stdout) as JsonRecord;
|
||
const shortReadBlocks = shortReadTransfer.commands.filter((item) => item.operation === "read-b64-block");
|
||
assertCondition(shortReadResult.exitCode === 0 && shortReadJson.sha256 === sha256BufferHex(shortReadPayload), "download should retry a truncated block and keep sha256 verification", shortReadResult);
|
||
assertCondition(shortReadBlocks.length === Number(shortReadJson.transfer && typeof shortReadJson.transfer === "object" ? (shortReadJson.transfer as JsonRecord).chunks : 0) + 1, "short block should add exactly one repeated read without counting as a chunk", shortReadTransfer.commands);
|
||
assertCondition(shortReadResult.stderr.includes("unidesk.ssh.download.short-read-retry"), "download should emit short-read retry events to stderr", shortReadResult.stderr);
|
||
assertCondition(readFileSync(shortReadDownload).equals(shortReadPayload), "short-read retry download must preserve complete content", { commands: shortReadTransfer.commands });
|
||
} finally {
|
||
rmSync(transferRoot, { recursive: true, force: true });
|
||
}
|
||
|
||
assertThrows(
|
||
() => parseSshInvocation("D601:win32", ["cmd", "ver"]),
|
||
/use D601:win/u,
|
||
"win32 route spelling must be rejected in favor of win",
|
||
);
|
||
|
||
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 directScriptCommand = parseSshArgs(["script", "--", "sed", "-n", "1,2p", "file.txt"]);
|
||
assertCondition(directScriptCommand.invocationKind === "argv", "script -- command form must run as direct argv without stdin", directScriptCommand);
|
||
assertCondition(directScriptCommand.remoteCommand === "'sed' '-n' '1,2p' 'file.txt'", "script -- command form must preserve dash-prefixed command args", directScriptCommand);
|
||
assertCondition(directScriptCommand.requiresStdin === false, "script -- command form must not wait for stdin", directScriptCommand);
|
||
const routeSeparatorBeforeScript = parseSshInvocation("D601:/tmp", ["--", "script", "--", "sed", "-n", "1,2p", "file.txt"]);
|
||
assertCondition(routeSeparatorBeforeScript.parsed.remoteCommand === "'sed' '-n' '1,2p' 'file.txt'", "route-level -- compatibility must not remove the command-local script -- separator", routeSeparatorBeforeScript);
|
||
|
||
const directScriptOneLiner = parseSshArgs(["script", "--", "cd /root/hwlab && git status --short --branch"]);
|
||
assertCondition(directScriptOneLiner.invocationKind === "helper", "script -- single-string command should run through a remote shell", directScriptOneLiner);
|
||
assertCondition(directScriptOneLiner.remoteCommand === shellArgv(["sh", "-c", `${sshShellScriptPrelude()}\ncd /root/hwlab && git status --short --branch`]), "script -- single-string command should match the intuitive remote shell one-liner form with the compatibility prelude", directScriptOneLiner);
|
||
assertCondition(directScriptOneLiner.requiresStdin === false, "script -- single-string command should not wait for stdin", directScriptOneLiner);
|
||
|
||
const shellOneLiner = parseSshArgs(["shell", "sed -n '1,2p' a && sed -n '1,2p' b"]);
|
||
assertCondition(shellOneLiner.invocationKind === "helper", "shell one-liner must be a helper operation", shellOneLiner);
|
||
assertCondition(shellOneLiner.remoteCommand === shellArgv(["sh", "-c", `${sshShellScriptPrelude()}\nsed -n '1,2p' a && sed -n '1,2p' b`]), "shell one-liner must keep command operators inside the remote shell", shellOneLiner);
|
||
assertCondition(shellOneLiner.requiresStdin === false, "shell one-liner must not require stdin", shellOneLiner);
|
||
|
||
for (const shell of ["sh", "bash"]) {
|
||
const fixture = spawnSync(shell, ["-c", `${sshShellCompatibilityPrelude}\nprintf "--- AGENTS ---\\n"\nprintf -- "%s\\n" ok`], { encoding: "utf8" });
|
||
assertCondition(fixture.status === 0 && fixture.stdout === "--- AGENTS ---\nok\n", "script/shell compatibility prelude must make printf headings portable across sh and bash", { shell, status: fixture.status, stdout: fixture.stdout, stderr: fixture.stderr });
|
||
}
|
||
|
||
const k3sGuard = parseSshInvocation("D601:k3s", ["guard"]).parsed;
|
||
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 = parseSshInvocation("D601:k3s", ["exec", "--namespace", "hwlab-dev", "--deployment", "hwlab-cloud-api", "--", "node", "-e", "console.log(process.version)"]).parsed;
|
||
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 routeK3sShell = parseSshInvocation("D601:k3s", ["shell", "kubectl get nodes && kubectl get pods -A"]);
|
||
assertCondition(routeK3sShell.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "sh", "-c", `${sshShellScriptPrelude()}\nkubectl get nodes && kubectl get pods -A`]), "D601:k3s shell must run one-line shell logic on the k3s host with native kubeconfig", routeK3sShell);
|
||
|
||
const g14Guard = parseSshInvocation("G14:k3s", []);
|
||
assertCondition(g14Guard.providerId === "G14" && g14Guard.route.plane === "k3s", "G14:k3s must parse as a native k3s route", g14Guard);
|
||
assertCondition(g14Guard.parsed.remoteCommand?.includes("UNIDESK_K3S_PROVIDER_ID") && g14Guard.parsed.remoteCommand.includes("ubuntu-rog-zephyrus-g14-ga401iv-ga401iv"), "G14:k3s guard must use the G14 node profile", g14Guard);
|
||
|
||
assertThrows(
|
||
() => parseSshInvocation("G14", ["k3s", "kubectl", "get", "nodes"]),
|
||
/unsupported.*trans G14:k3s/u,
|
||
"k3s must be a route plane, not a post-provider shorthand",
|
||
);
|
||
|
||
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 routeTargetArgv = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["argv", "sh", "-c", "printf ok"]);
|
||
assertCondition(routeTargetArgv.parsed.invocationKind === "argv", "k3s target argv operation must stay explicit argv", routeTargetArgv);
|
||
assertCondition(routeTargetArgv.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'sh' '-c' 'printf ok'", "D601:k3s:<namespace>:<workload> argv must exec the argv payload instead of treating argv as a pod command", routeTargetArgv);
|
||
assertThrows(
|
||
() => parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["argv", "pwd && ls -la"]),
|
||
/one shell-like command string.*script --/u,
|
||
"k3s workload argv must reject a single shell command string before kubectl exec treats it as an executable path",
|
||
);
|
||
|
||
const routeTargetShell = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["shell", "pwd && ls"]);
|
||
assertCondition(routeTargetShell.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", "exec", "-n", "hwlab-dev", "deployment/hwlab-cloud-api", "--", "sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "unidesk-cwd", "/app", "sh", "-c", `${sshShellScriptPrelude()}\npwd && ls`]), "D601:k3s:<namespace>:<workload>/<workspace> shell must run shell logic after cd inside the pod", routeTargetShell);
|
||
|
||
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);
|
||
assertCondition(routeScript.parsed.stdinPrefix === `${sshShellScriptPrelude()}\n`, "k3s script stdin must inject the shell compatibility prelude before user script text", 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);
|
||
assertCondition(routeControlScript.parsed.stdinPrefix === `${sshShellScriptPrelude()}\n`, "k3s control-plane script stdin must inject the shell compatibility prelude before user script text", routeControlScript);
|
||
|
||
const routeControlScriptOneLiner = parseSshInvocation("D601:k3s", ["script", "--", "echo k3s-script-ok"]);
|
||
assertCondition(routeControlScriptOneLiner.parsed.requiresStdin === false, "k3s control-plane script -- one-liner must not wait for stdin", routeControlScriptOneLiner);
|
||
assertCondition(routeControlScriptOneLiner.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "sh", "-c", `${sshShellScriptPrelude()}\necho k3s-script-ok`]), "k3s control-plane script -- one-liner must run through the native kubeconfig shell path", routeControlScriptOneLiner);
|
||
|
||
const routePodScriptOneLiner = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["script", "--", "echo pod-script-ok"]);
|
||
assertCondition(routePodScriptOneLiner.parsed.requiresStdin === false, "k3s workload script -- one-liner must not wait for stdin", routePodScriptOneLiner);
|
||
assertCondition(routePodScriptOneLiner.parsed.remoteCommand === shellArgv(["env", "KUBECONFIG=/etc/rancher/k3s/k3s.yaml", "kubectl", "exec", "-n", "hwlab-dev", "deployment/hwlab-cloud-api", "--", "sh", "-c", `${sshShellScriptPrelude()}\necho pod-script-ok`]), "k3s workload script -- one-liner must run as sh -c inside the workload", routePodScriptOneLiner);
|
||
|
||
const topLevelScriptSeparator = extractRemoteCliOptions(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]);
|
||
assertCondition(JSON.stringify(topLevelScriptSeparator.args) === JSON.stringify(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]), "top-level remote option parser must preserve command-local -- after the command starts", topLevelScriptSeparator);
|
||
const topLevelScriptInvocation = parseSshInvocation(topLevelScriptSeparator.args[1] as string, topLevelScriptSeparator.args.slice(2));
|
||
assertCondition(topLevelScriptInvocation.parsed.remoteCommand === "'sed' '-n' '1,2p' 'file.txt'", "script -- must allow argv such as sed -n to execute without being parsed as script options", topLevelScriptInvocation);
|
||
assertCondition(topLevelScriptInvocation.parsed.requiresStdin === false, "script -- direct command must not require stdin after top-level parsing", topLevelScriptInvocation);
|
||
|
||
const remoteOptionSeparator = extractRemoteCliOptions(["--main-server-ip", "74.48.78.17", "--", "ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]);
|
||
assertCondition(remoteOptionSeparator.host === "74.48.78.17", "global remote options before -- must still be parsed", remoteOptionSeparator);
|
||
assertCondition(JSON.stringify(remoteOptionSeparator.args) === JSON.stringify(["ssh", "D601:/tmp", "script", "--", "sed", "-n", "1,2p", "file.txt"]), "global -- must not strip nested command separators", remoteOptionSeparator);
|
||
|
||
const routeApplyPatch = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["apply-patch"]);
|
||
assertCondition(routeApplyPatch.parsed.requiresStdin === true && routeApplyPatch.parsed.remoteCommand === null, "k3s apply-patch operation must use the default v2 local engine", routeApplyPatch);
|
||
|
||
const routeApplyPatchV1 = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["apply-patch-v1"]);
|
||
assertCondition(routeApplyPatchV1.parsed.requiresStdin === true, "k3s apply-patch-v1 operation must stream local patch stdin", routeApplyPatchV1);
|
||
assertCondition(routeApplyPatchV1.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-v1 must enter pod with stdin", routeApplyPatchV1);
|
||
assertCondition(routeApplyPatchV1.parsed.stdinPrefix?.includes("apply_patch") && routeApplyPatchV1.parsed.stdinPrefix.includes("__UNIDESK_APPLY_PATCH_PAYLOAD__"), "k3s apply-patch-v1 operation must inject pod helper before patch stdin", routeApplyPatchV1);
|
||
assertCondition(routeApplyPatchV1.parsed.stdinPrefix?.includes("#!/bin/sh"), "k3s apply-patch-v1 operation must inject the same sh helper used by host apply-patch-v1", routeApplyPatchV1);
|
||
assertCondition(!routeApplyPatchV1.parsed.stdinPrefix?.includes("python3") && !routeApplyPatchV1.parsed.stdinPrefix?.includes("node "), "k3s apply-patch-v1 operation must use the sh-only pod helper", routeApplyPatchV1);
|
||
assertCondition(routeApplyPatchV1.parsed.stdinSuffix === "\n__UNIDESK_APPLY_PATCH_PAYLOAD__\n", "k3s apply-patch-v1 operation must terminate patch heredoc", routeApplyPatchV1);
|
||
|
||
const hostApplyPatchLoose = parseSshArgs(["apply-patch-v1", "--allow-loose"]);
|
||
assertCondition(hostApplyPatchLoose.remoteCommand === "'apply_patch' '--allow-loose'", "host apply-patch-v1 must pass --allow-loose as an explicit helper argument", hostApplyPatchLoose);
|
||
assertCondition(hostApplyPatchLoose.requiredHelpers?.length === 1 && hostApplyPatchLoose.requiredHelpers.includes("apply_patch"), "host apply-patch-v1 must request only the apply_patch helper bootstrap", hostApplyPatchLoose);
|
||
assertCondition(remoteApplyPatchSource.includes("replace_once_with_perl") && remoteApplyPatchSource.includes("perl -0777"), "apply_patch helper must keep a fast path for large files", {});
|
||
|
||
const hostApplyPatchV2 = parseSshArgs(["apply-patch"]);
|
||
assertCondition(hostApplyPatchV2.requiresStdin === true && hostApplyPatchV2.requiredHelpers === undefined && hostApplyPatchV2.remoteCommand === null, "host apply-patch must be a local v2 engine operation, not a remote helper bootstrap", hostApplyPatchV2);
|
||
const hostApplyPatchV2Help = parseSshArgs(["apply-patch", "--help"]);
|
||
assertCondition(hostApplyPatchV2Help.requiresStdin === false && hostApplyPatchV2Help.remoteCommand === null, "host apply-patch --help must not wait for patch stdin", hostApplyPatchV2Help);
|
||
const podApplyPatchV2 = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["apply-patch"]);
|
||
assertCondition(podApplyPatchV2.parsed.requiresStdin === true && podApplyPatchV2.parsed.remoteCommand === null, "pod apply-patch must be handled by the local v2 engine instead of injecting the legacy helper", podApplyPatchV2);
|
||
assertThrows(() => parseSshArgs(["v2"]), /remote patch entrypoints/u, "v2 must not remain as an independent patch subcommand");
|
||
assertThrows(() => parseSshArgs(["patch"]), /remote patch entrypoints/u, "patch must not remain as a patch alias");
|
||
assertThrows(() => parseSshArgs(["patch-v1"]), /remote patch entrypoints/u, "patch-v1 must not remain as a legacy patch alias");
|
||
|
||
const longChinesePatch = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: story.md",
|
||
"@@",
|
||
"+这是一个很长很长的中文段落,用来证明远端 v2 不再依赖 shell hunk 拼接和手写长中文 search block。它只是通过本地行级 patch engine 计算新内容,然后把完整文件写回远端,所以中文、标点、长句都不应该影响 patch 解析和匹配。",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"story.md": "开头\n",
|
||
});
|
||
assertCondition(longChinesePatch.files["story.md"]?.includes("很长很长的中文段落"), "v2 should accept pure insertion with long Chinese text", longChinesePatch);
|
||
assertCondition(longChinesePatch.stdout.includes("Success. Updated the following files:"), "v2 must print visible success output", longChinesePatch);
|
||
|
||
const lowContextV1Baseline = applyPatchFixture([], [
|
||
"*** Begin Patch",
|
||
"*** Update File: story.md",
|
||
"@@",
|
||
" 开头",
|
||
"+低上下文纯插入在 v1 会失败,但 v2 应该按 Codex 行级语义允许。",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"story.md": "开头\n结尾\n",
|
||
});
|
||
assertCondition(lowContextV1Baseline.status !== 0 && lowContextV1Baseline.stderr.includes("insert-only without both leading and trailing context"), "v1 baseline should reject low-context pure insertion", lowContextV1Baseline);
|
||
const lowContextV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: story.md",
|
||
"@@",
|
||
" 开头",
|
||
"+低上下文纯插入在 v1 会失败,但 v2 应该按 Codex 行级语义允许。",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"story.md": "开头\n结尾\n",
|
||
});
|
||
assertCondition(lowContextV2.files["story.md"]?.includes("v2 应该按 Codex 行级语义允许"), "v2 should fix v1 low-context insertion friction", lowContextV2);
|
||
assertCondition(lowContextV2.commands.some((command) => command.includes("write-b64-argv")), "v2 should use argv write path for small remote files to work inside k3s pod exec capture", lowContextV2);
|
||
|
||
const unicodePunctuationV1Baseline = applyPatchFixture([], [
|
||
"*** Begin Patch",
|
||
"*** Update File: notes.txt",
|
||
"@@",
|
||
"-alpha - beta",
|
||
"+alpha - gamma",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"notes.txt": "alpha – beta\n",
|
||
});
|
||
assertCondition(unicodePunctuationV1Baseline.status !== 0, "v1 baseline should miss ASCII dash against typographic dash", unicodePunctuationV1Baseline);
|
||
const unicodePunctuationV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: notes.txt",
|
||
"@@",
|
||
"-alpha - beta",
|
||
"+alpha - gamma",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"notes.txt": "alpha – beta\n",
|
||
});
|
||
assertCondition(unicodePunctuationV2.files["notes.txt"] === "alpha - gamma\n", "v2 should normalize common Unicode punctuation while matching expected lines", unicodePunctuationV2);
|
||
|
||
const repeatedBlockWithContext = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: repeated.txt",
|
||
"@@ section two",
|
||
"-marker",
|
||
"+patched",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"repeated.txt": "section one\nmarker\nsection two\nmarker\n",
|
||
});
|
||
assertCondition(repeatedBlockWithContext.files["repeated.txt"] === "section one\nmarker\nsection two\npatched\n", "v2 should use @@ context to target repeated blocks", repeatedBlockWithContext);
|
||
|
||
const longChineseReplace = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: novel.md",
|
||
"@@",
|
||
"-林深在透明的舷窗前停下脚步,远处的群星像被压进黑色玻璃里的碎银,安静得让人怀疑整个宇宙都屏住了呼吸。",
|
||
"+林深在透明的舷窗前停下脚步,远处的群星像被压进黑色玻璃里的碎银,安静得让人怀疑整个宇宙正在等待他重新命名。",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"novel.md": "林深在透明的舷窗前停下脚步,远处的群星像被压进黑色玻璃里的碎银,安静得让人怀疑整个宇宙都屏住了呼吸。\n",
|
||
});
|
||
assertCondition(longChineseReplace.files["novel.md"]?.includes("等待他重新命名"), "v2 should replace long Chinese lines without remote shell search blocks", longChineseReplace);
|
||
|
||
const largeOriginal = `${"0123456789abcdef\n".repeat(4096)}`;
|
||
const largeV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: large.txt",
|
||
"@@",
|
||
"+large insert",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"large.txt": largeOriginal,
|
||
});
|
||
assertCondition(largeV2.commands.some((command) => command.includes("write-b64-stdin")), "v2 should use stdin write path for large remote files to avoid E2BIG", largeV2.commands);
|
||
assertCondition(!largeV2.commands.some((command) => command.includes("write-b64-append")), "v2 should keep the single stdin write as the normal large-file fast path", largeV2.commands);
|
||
assertCondition(largeV2.commands.filter((command) => command.startsWith("read-b64-block")).length <= 2, "v2 large-file verified read should use coarse chunks, not many tiny SSH calls", largeV2.commands);
|
||
|
||
const repeatedLargeLines = Array.from({ length: 1200 }, (_, index) => `repeat target ${String(index).padStart(4, "0")}`);
|
||
repeatedLargeLines[899] = "same marker";
|
||
repeatedLargeLines[1099] = "same marker";
|
||
const unifiedHeaderLineRangeV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: repeated-large.txt",
|
||
"@@ -1100,1 +1100,1 @@",
|
||
"-same marker",
|
||
"+same marker patched",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"repeated-large.txt": `${repeatedLargeLines.join("\n")}\n`,
|
||
}, { stderrOutput: true });
|
||
assertCondition(unifiedHeaderLineRangeV2.exitCode === 0 && unifiedHeaderLineRangeV2.error === null, "v2 should accept unified-diff hunk headers with a hint", unifiedHeaderLineRangeV2);
|
||
const unifiedLines = unifiedHeaderLineRangeV2.files["repeated-large.txt"]?.split("\n") ?? [];
|
||
assertCondition(unifiedLines[899] === "same marker" && unifiedLines[1099] === "same marker patched", "v2 should use unified header old line number to avoid patching the first repeated match", {
|
||
line900: unifiedLines[899],
|
||
line1100: unifiedLines[1099],
|
||
stderr: unifiedHeaderLineRangeV2.stderr,
|
||
});
|
||
assertCondition(unifiedHeaderLineRangeV2.stderr.includes("accepted unified-diff hunk header in repeated-large.txt"), "v2 unified header compatibility should emit a canonical syntax hint", unifiedHeaderLineRangeV2.stderr);
|
||
|
||
const unifiedTiming = applyPatchTimingFromStderr(unifiedHeaderLineRangeV2.stderr);
|
||
assertCondition(unifiedTiming.code === "apply-patch-v2-timing" && unifiedTiming.status === "succeeded", "v2 apply-patch timing summary should identify successful runs", unifiedTiming);
|
||
assertCondition(unifiedTiming.patchBytes > 0 && unifiedTiming.fileCount === 1 && unifiedTiming.hunkCount === 1 && unifiedTiming.changedCount === 1, "v2 timing summary should expose patch/file/hunk/change counts", unifiedTiming);
|
||
assertCondition(unifiedTiming.remoteOperationCount === unifiedHeaderLineRangeV2.commands.length, "v2 timing summary should count remote operations", { unifiedTiming, commands: unifiedHeaderLineRangeV2.commands });
|
||
assertCondition((unifiedTiming.remoteOperationCounts.stat ?? 0) >= 1 && (unifiedTiming.remoteOperationCounts["read-b64-block"] ?? 0) >= 1 && Object.keys(unifiedTiming.remoteOperationCounts).some((key) => key.startsWith("write-b64")), "v2 timing summary should classify stat/read/write operations", unifiedTiming);
|
||
assertCondition(unifiedHeaderLineRangeV2.stdout.startsWith("Success. Updated the following files:"), "v2 timing summary must not change Codex-compatible success stdout", unifiedHeaderLineRangeV2.stdout);
|
||
|
||
const bulkPatchLines = ["*** Begin Patch"];
|
||
const bulkFiles: Record<string, string> = {};
|
||
for (let fileIndex = 0; fileIndex < 4; fileIndex += 1) {
|
||
const fileName = `bulk-${fileIndex}.txt`;
|
||
bulkFiles[fileName] = Array.from({ length: 120 }, (_, lineIndex) => `file=${fileIndex} line=${lineIndex} value=alpha`).join("\n") + "\n";
|
||
bulkPatchLines.push(`*** Update File: ${fileName}`);
|
||
bulkPatchLines.push("@@");
|
||
bulkPatchLines.push(` file=${fileIndex} line=39 value=alpha`);
|
||
bulkPatchLines.push(`-file=${fileIndex} line=40 value=alpha`);
|
||
bulkPatchLines.push(`+file=${fileIndex} line=40 value=beta`);
|
||
bulkPatchLines.push(` file=${fileIndex} line=41 value=alpha`);
|
||
}
|
||
bulkPatchLines.push("*** End Patch", "");
|
||
const bulkV2 = await applyPatchV2FixtureAttempt(bulkPatchLines.join("\n"), bulkFiles, { stderrOutput: true });
|
||
assertCondition(bulkV2.exitCode === 0 && bulkV2.error === null, "v2 multi-file update patch should succeed through the bulk path", bulkV2);
|
||
assertCondition(bulkV2.commands.filter((command) => command.startsWith("read-bulk-b64")).length === 1 && bulkV2.commands.filter((command) => command.startsWith("apply-replacements-bulk-stdin")).length === 1, "v2 multi-file updates should collapse remote IO to one bulk read and one line-level bulk apply", bulkV2.commands);
|
||
assertCondition(!bulkV2.commands.some((command) => command.startsWith("stat") || command.startsWith("read-b64-block") || command.startsWith("write-b64-stdin")), "v2 bulk path should avoid per-file stat/read/write operations", bulkV2.commands);
|
||
for (let fileIndex = 0; fileIndex < 4; fileIndex += 1) {
|
||
assertCondition(bulkV2.files[`bulk-${fileIndex}.txt`]?.includes(`file=${fileIndex} line=40 value=beta`), "v2 bulk path should write all changed files", { fileIndex, files: bulkV2.files });
|
||
}
|
||
const bulkTiming = applyPatchTimingFromStderr(bulkV2.stderr);
|
||
assertCondition(bulkTiming.remoteOperationCount === 2 && bulkTiming.remoteOperationCounts["read-bulk-b64"] === 1 && bulkTiming.remoteOperationCounts["apply-replacements-bulk-stdin"] === 1, "v2 timing summary should classify bulk read and line-level apply operations", bulkTiming);
|
||
|
||
const fsBulkV2 = await applyPatchV2FsBulkFixtureAttempt(bulkPatchLines.join("\n"), bulkFiles);
|
||
assertCondition(fsBulkV2.exitCode === 0 && fsBulkV2.error === null, "v2 fs executor should support the same multi-file bulk update path", fsBulkV2);
|
||
assertCondition(
|
||
fsBulkV2.operations.length === 2
|
||
&& fsBulkV2.operations[0]?.startsWith("readFiles ")
|
||
&& fsBulkV2.operations[1]?.startsWith("applyReplacementsBulk "),
|
||
"v2 fs multi-file update path should use fs bulk read and line-level apply operations",
|
||
fsBulkV2.operations,
|
||
);
|
||
assertCondition(!fsBulkV2.operations.some((operation) => operation.startsWith("stat ") || operation.startsWith("readBlock ") || operation.startsWith("writeFile ")), "v2 fs bulk path should avoid per-file stat/read/write operations", fsBulkV2.operations);
|
||
for (let fileIndex = 0; fileIndex < 4; fileIndex += 1) {
|
||
assertCondition(fsBulkV2.files[`bulk-${fileIndex}.txt`]?.includes(`file=${fileIndex} line=40 value=beta`), "v2 fs bulk path should write all changed files", { fileIndex, files: fsBulkV2.files });
|
||
}
|
||
const fsBulkTiming = applyPatchTimingFromStderr(fsBulkV2.stderr);
|
||
assertCondition(fsBulkTiming.remoteOperationCount === 2 && fsBulkTiming.remoteOperationCounts["fs.readFiles"] === 1 && fsBulkTiming.remoteOperationCounts["fs.applyReplacementsBulk"] === 1, "v2 timing summary should classify fs bulk read and line-level apply operations", fsBulkTiming);
|
||
|
||
const unprefixedUpdateContextV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: internal/cloud/access-control.ts",
|
||
"@@",
|
||
" \"io.uart.jsonrpc\"",
|
||
"]);",
|
||
"+const DEVICE_JOB_READ_ONLY_SUB_ACTIONS = new Set([",
|
||
"+ \"status\",",
|
||
"+ \"output\"",
|
||
"+]);",
|
||
"const ACCESS_SCHEMA_STATEMENTS = Object.freeze([",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"internal/cloud/access-control.ts": [
|
||
" \"io.uart.write\",",
|
||
" \"io.uart.jsonrpc\"",
|
||
"]);",
|
||
"const ACCESS_SCHEMA_STATEMENTS = Object.freeze([",
|
||
"]);",
|
||
"",
|
||
].join("\n"),
|
||
}, { stderrOutput: true });
|
||
assertCondition(unprefixedUpdateContextV2.exitCode === 0 && unprefixedUpdateContextV2.error === null, "v2 should accept MiniMax-style unprefixed Update File context lines", unprefixedUpdateContextV2);
|
||
assertCondition(
|
||
unprefixedUpdateContextV2.files["internal/cloud/access-control.ts"]?.includes("const DEVICE_JOB_READ_ONLY_SUB_ACTIONS = new Set(["),
|
||
"v2 should apply the update when MiniMax omits context prefixes for column-0 lines",
|
||
unprefixedUpdateContextV2,
|
||
);
|
||
assertCondition(
|
||
unprefixedUpdateContextV2.stderr.includes("accepted unprefixed Update File context line in internal/cloud/access-control.ts")
|
||
&& unprefixedUpdateContextV2.stderr.includes("one extra space in addition to source indentation"),
|
||
"v2 MiniMax-style Update File compatibility should emit canonical syntax hints",
|
||
unprefixedUpdateContextV2.stderr,
|
||
);
|
||
|
||
const unprefixedFirstUpdateContextV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: zero-column.ts",
|
||
"const MUTATING_INTENTS = new Set([",
|
||
"+ \"workspace.apply-patch\",",
|
||
"]);",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"zero-column.ts": "const MUTATING_INTENTS = new Set([\n]);\n",
|
||
}, { stderrOutput: true });
|
||
assertCondition(unprefixedFirstUpdateContextV2.exitCode === 0 && unprefixedFirstUpdateContextV2.error === null, "v2 should accept an omitted first @@ plus unprefixed column-0 context", unprefixedFirstUpdateContextV2);
|
||
assertCondition(unprefixedFirstUpdateContextV2.files["zero-column.ts"] === "const MUTATING_INTENTS = new Set([\n \"workspace.apply-patch\",\n]);\n", "v2 omitted-@@ compatibility should preserve column-0 context correctly", unprefixedFirstUpdateContextV2);
|
||
|
||
const manyLines = Array.from({ length: 6200 }, (_, index) => {
|
||
if (index === 4) return "HEAD old";
|
||
if (index === 3099) return "MIDDLE old";
|
||
if (index === 6194) return "TAIL old";
|
||
return `ROW-${String(index + 1).padStart(5, "0")} keep`;
|
||
});
|
||
const largeMultiMixedV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: big-multi.txt",
|
||
"@@ -5,1 +5,1 @@",
|
||
"-HEAD old",
|
||
"+HEAD new",
|
||
"@@ -3100,1 +3100,2 @@",
|
||
"-MIDDLE old",
|
||
"+MIDDLE new",
|
||
"+MIDDLE inserted",
|
||
"@@ -6195,1 +6196,1 @@",
|
||
"-TAIL old",
|
||
"+TAIL new",
|
||
"*** Add File: nested/compat-created.txt",
|
||
"@@",
|
||
"first",
|
||
"+ ",
|
||
"+last",
|
||
"*** Delete File: stale.txt",
|
||
"@@",
|
||
"-stale",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"big-multi.txt": `${manyLines.join("\n")}\n`,
|
||
"stale.txt": "stale\n",
|
||
}, { stderrOutput: true });
|
||
assertCondition(largeMultiMixedV2.exitCode === 0 && largeMultiMixedV2.error === null, "v2 should apply large multi-hunk mixed compatibility patches", largeMultiMixedV2);
|
||
const bigMulti = largeMultiMixedV2.files["big-multi.txt"]?.split("\n") ?? [];
|
||
assertCondition(
|
||
bigMulti[4] === "HEAD new"
|
||
&& bigMulti[3099] === "MIDDLE new"
|
||
&& bigMulti[3100] === "MIDDLE inserted"
|
||
&& bigMulti[6195] === "TAIL new"
|
||
&& bigMulti[0] === "ROW-00001 keep"
|
||
&& bigMulti[6200] === "ROW-06200 keep"
|
||
&& bigMulti.length === 6202,
|
||
"v2 should preserve distant untouched lines while applying multiple large-file hunks",
|
||
{ head: bigMulti.slice(0, 6), middle: bigMulti.slice(3098, 3102), tail: bigMulti.slice(6194, 6201) },
|
||
);
|
||
assertCondition(largeMultiMixedV2.files["nested/compat-created.txt"] === "first\n\nlast\n", "v2 mixed compatibility Add File should preserve intended blank line", largeMultiMixedV2);
|
||
assertCondition(largeMultiMixedV2.files["stale.txt"] === undefined, "v2 mixed compatibility Delete File should delete stale files", largeMultiMixedV2);
|
||
assertCondition(
|
||
largeMultiMixedV2.stderr.includes("accepted unified-diff hunk header in big-multi.txt")
|
||
&& largeMultiMixedV2.stderr.includes("accepted MiniMax-style @@ inside Add File nested/compat-created.txt")
|
||
&& largeMultiMixedV2.stderr.includes("ignored extra MiniMax-style hunk/body lines after Delete File stale.txt"),
|
||
"v2 mixed compatibility patch should emit hints for every non-canonical form",
|
||
largeMultiMixedV2.stderr,
|
||
);
|
||
assertCondition(largeMultiMixedV2.commands.some((command) => command.startsWith("write-b64-stdin big-multi.txt")), "v2 large multi-hunk file should use stdin write path", largeMultiMixedV2.commands);
|
||
|
||
const multiChunkTailV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: two_chunks.txt",
|
||
"@@",
|
||
"-b",
|
||
"+B",
|
||
"@@",
|
||
"-d",
|
||
"+D",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"two_chunks.txt": "a\nb\nc\nd\ne\nf\n",
|
||
});
|
||
assertCondition(multiChunkTailV2.error === null, "v2 should apply explicit multi-chunk patches through the real shell writer", multiChunkTailV2);
|
||
assertCondition(multiChunkTailV2.files["two_chunks.txt"] === "a\nB\nc\nD\ne\nf\n", "v2 must preserve untouched tail lines when applying multiple chunks", multiChunkTailV2);
|
||
|
||
const largeTailV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: large-tail.txt",
|
||
"@@ LINE-2048 tail-preserve",
|
||
"-LINE-2049 keep middle",
|
||
"+LINE-2049 patched middle",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"large-tail.txt": Array.from({ length: 5000 }, (_, index) => `LINE-${String(index).padStart(4, "0")} ${index === 2049 ? "keep middle" : "tail-preserve"}`).join("\n") + "\n",
|
||
});
|
||
assertCondition(largeTailV2.error === null, "v2 should patch a large file through the real shell writer", largeTailV2);
|
||
assertCondition(largeTailV2.files["large-tail.txt"]?.includes("LINE-2049 patched middle"), "v2 large-file patch should update the target line", largeTailV2);
|
||
assertCondition(largeTailV2.files["large-tail.txt"]?.endsWith("LINE-4999 tail-preserve\n"), "v2 must preserve the untouched tail of large files", largeTailV2);
|
||
assertCondition(largeTailV2.commands.some((command) => command.startsWith("write-b64-stdin")), "v2 large-file real shell path should use stdin fast path before any fallback", largeTailV2.commands);
|
||
assertCondition(!largeTailV2.commands.some((command) => command.startsWith("write-b64-append")), "v2 large-file real shell path should not use slower chunk fallback unless stdin integrity fails", largeTailV2.commands);
|
||
|
||
const truncatedLargeReadV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: large-read.txt",
|
||
"@@",
|
||
"+this write must not happen after a truncated read",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"large-read.txt": largeOriginal,
|
||
}, undefined, (operation, result) => {
|
||
if (operation !== "read-b64-block" || result.exitCode !== 0) return result;
|
||
return { ...result, stdout: result.stdout.slice(0, Math.max(0, result.stdout.length - 32)) };
|
||
});
|
||
assertCondition(truncatedLargeReadV2.error !== null, "v2 should reject a truncated remote read before planning writes", truncatedLargeReadV2);
|
||
assertCondition(truncatedLargeReadV2.files["large-read.txt"] === largeOriginal, "v2 must keep the original file when bridge stdout truncates a read block", truncatedLargeReadV2);
|
||
assertCondition(!truncatedLargeReadV2.commands.some((command) => command.startsWith("write-b64")), "v2 must not write after read integrity fails", truncatedLargeReadV2.commands);
|
||
|
||
const missingUpdateShellV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: missing-dist.js",
|
||
"@@",
|
||
"-old",
|
||
"+new",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {});
|
||
const missingUpdateShellError = missingUpdateShellV2.error instanceof Error ? missingUpdateShellV2.error : null;
|
||
assertCondition(missingUpdateShellError !== null, "v2 real shell stat should reject missing update targets", missingUpdateShellV2);
|
||
assertCondition(
|
||
missingUpdateShellError.message.includes("remote apply-patch v2 operation failed")
|
||
&& JSON.stringify((missingUpdateShellError as { details?: unknown }).details ?? {}).includes("file not found: missing-dist.js"),
|
||
"v2 real shell stat must expose file-not-found instead of invalid metadata",
|
||
missingUpdateShellError,
|
||
);
|
||
assertCondition(!missingUpdateShellV2.commands.some((command) => command.startsWith("read-b64") || command.startsWith("write-b64")), "missing update target must fail before remote read/write", missingUpdateShellV2.commands);
|
||
|
||
const truncatedLargeWriteV2 = await applyPatchV2ActualShellFixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: large.txt",
|
||
"@@",
|
||
"+large insert that forces a rewritten full-file payload",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"large.txt": largeOriginal,
|
||
}, (operation, input) => {
|
||
if (operation !== "write-b64-stdin" || input === undefined) return input;
|
||
return input.slice(0, Math.max(0, input.length - 32));
|
||
});
|
||
assertCondition(truncatedLargeWriteV2.error === null, "v2 should fall back to bounded argv chunks when the stdin write path is truncated", truncatedLargeWriteV2);
|
||
assertCondition(truncatedLargeWriteV2.files["large.txt"]?.includes("large insert that forces a rewritten full-file payload"), "v2 fallback write should still apply the patch", truncatedLargeWriteV2);
|
||
assertCondition(truncatedLargeWriteV2.files["large.txt"]?.startsWith(largeOriginal), "v2 fallback write must preserve the original large-file content before appending the inserted line", {
|
||
commands: truncatedLargeWriteV2.commands.map((command) => command.split(" ").slice(0, 2).join(" ")),
|
||
outputBytes: Buffer.byteLength(truncatedLargeWriteV2.files["large.txt"] ?? "", "utf8"),
|
||
});
|
||
assertCondition(truncatedLargeWriteV2.commands.some((command) => command.startsWith("write-b64-stdin")), "v2 should attempt the stdin fast path first", truncatedLargeWriteV2.commands);
|
||
assertCondition(truncatedLargeWriteV2.commands.some((command) => command.startsWith("write-b64-commit")), "v2 should commit the chunked fallback after stdin integrity failure", truncatedLargeWriteV2.commands);
|
||
|
||
const failedCompoundV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: first.txt",
|
||
"@@",
|
||
"-old first",
|
||
"+new first",
|
||
"*** Update File: second.txt",
|
||
"@@",
|
||
"-missing second",
|
||
"+new second",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"first.txt": "old first\n",
|
||
"second.txt": "old second\n",
|
||
});
|
||
assertCondition(failedCompoundV2.error !== null, "v2 compound patch should fail when a later hunk does not match", failedCompoundV2);
|
||
assertCondition(failedCompoundV2.files["first.txt"] === "new first\n", "v2 should match Codex apply_patch by preserving earlier committed changes when a later hunk fails", failedCompoundV2);
|
||
assertCondition(failedCompoundV2.files["second.txt"] === "old second\n", "v2 must leave later failed files unchanged", failedCompoundV2);
|
||
assertCondition(failedCompoundV2.commands.some((command) => command.startsWith("write-b64") || command.startsWith("apply-replacements-bulk-stdin")), "v2 should commit preceding operations in patch order like Codex apply_patch", failedCompoundV2.commands);
|
||
assertCondition(
|
||
Array.isArray((failedCompoundV2.error as { details?: { partialChanges?: unknown } })?.details?.partialChanges)
|
||
&& ((failedCompoundV2.error as { details?: { partialChanges?: string[] } }).details?.partialChanges ?? []).includes("M first.txt"),
|
||
"v2 failure should expose partialChanges for already committed operations",
|
||
failedCompoundV2.error,
|
||
);
|
||
|
||
const failedCompoundVisibleV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: first.txt",
|
||
"@@",
|
||
"-old first",
|
||
"+new first",
|
||
"*** Update File: second.txt",
|
||
"@@",
|
||
"-missing second",
|
||
"+new second",
|
||
"*** Update File: third.txt",
|
||
"@@",
|
||
"-old third",
|
||
"+new third",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"first.txt": "old first\n",
|
||
"second.txt": "old second\n",
|
||
"third.txt": "old third\n",
|
||
}, { stderrOutput: true });
|
||
assertCondition(failedCompoundVisibleV2.exitCode === 1 && failedCompoundVisibleV2.error === null, "v2 CLI path should return non-zero instead of throwing when stderr is provided", failedCompoundVisibleV2);
|
||
assertCondition(failedCompoundVisibleV2.stdout === "", "v2 failed CLI path should keep Codex-style empty stdout", failedCompoundVisibleV2);
|
||
assertCondition(
|
||
failedCompoundVisibleV2.stderr.includes("failed to find expected lines")
|
||
&& failedCompoundVisibleV2.stderr.includes("Applied before failure:")
|
||
&& failedCompoundVisibleV2.stderr.includes("Expected lines for second.txt hunk 1:")
|
||
&& failedCompoundVisibleV2.stderr.includes("\"missing second\"")
|
||
&& failedCompoundVisibleV2.stderr.includes("M first.txt")
|
||
&& failedCompoundVisibleV2.stderr.includes("Failed:")
|
||
&& failedCompoundVisibleV2.stderr.includes("hunk 2 update second.txt")
|
||
&& !failedCompoundVisibleV2.stderr.includes("third.txt"),
|
||
"v2 failed CLI path should print Codex-style stderr plus expected lines, applied/failed summary, and stop before later hunks",
|
||
failedCompoundVisibleV2.stderr,
|
||
);
|
||
|
||
const missingPlusLargeInsertVisibleV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: access-control.test.ts",
|
||
"@@",
|
||
" });",
|
||
"",
|
||
" test(\"cloud api accepts read-only workspace.build / debug.download sub-actions without --reason\", async () => {",
|
||
" const receivedJobs = [];",
|
||
" const executor = createServer(async (request, response) => {",
|
||
" const body = await requestJson(request);",
|
||
" receivedJobs.push({ url: request.url, body });",
|
||
" response.writeHead(200, { \"content-type\": \"application/json; charset=utf-8\" });",
|
||
" response.end(JSON.stringify({ accepted: true, status: \"completed\" }));",
|
||
" });",
|
||
" });",
|
||
"",
|
||
" test(\"cloud api routes device-pod probe GET requests through executor jobs\", async () => {",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"access-control.test.ts": [
|
||
"test(\"cloud api bounds device-pod job output payloads\", async () => {",
|
||
"});",
|
||
"",
|
||
"test(\"cloud api routes device-pod probe GET requests through executor jobs\", async () => {",
|
||
"});",
|
||
"",
|
||
].join("\n"),
|
||
}, { stderrOutput: true });
|
||
assertCondition(missingPlusLargeInsertVisibleV2.exitCode === 1 && missingPlusLargeInsertVisibleV2.error === null, "v2 should still reject unsafe large insertion hunks whose added lines are missing + prefixes", missingPlusLargeInsertVisibleV2);
|
||
assertCondition(
|
||
missingPlusLargeInsertVisibleV2.stderr.includes("First expected line appears near target line(s): 2, 5")
|
||
&& missingPlusLargeInsertVisibleV2.stderr.includes("Best partial context match: 2 expected line(s) matched")
|
||
&& missingPlusLargeInsertVisibleV2.stderr.includes("large insertion whose new lines were written as context")
|
||
&& missingPlusLargeInsertVisibleV2.stderr.includes("regenerate the patch instead of editing it with sed")
|
||
&& missingPlusLargeInsertVisibleV2.stderr.includes("handled by apply-patch v2")
|
||
&& missingPlusLargeInsertVisibleV2.stderr.includes("do not switch the same patch to apply-patch-v1"),
|
||
"v2 missing-plus failure should diagnose the MiniMax sed-regression pattern explicitly",
|
||
missingPlusLargeInsertVisibleV2.stderr,
|
||
);
|
||
|
||
const staleBlockReplacementVisibleV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: runner-trace.ts",
|
||
"@@",
|
||
" export async function waitForAgentResult(",
|
||
" initial: AgentChatResponse,",
|
||
" activityRef?: ActivityRefSource",
|
||
" ): Promise<AgentChatResponse> {",
|
||
" if (isTerminalStatus(initial.status)) {",
|
||
" return isTerminalStatus(initial.status) ? initial : null;",
|
||
" }",
|
||
"- return initial;",
|
||
"+ return pollAgentResult(initial, activityRef);",
|
||
" }",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"runner-trace.ts": [
|
||
"export async function waitForAgentResult(",
|
||
" initial: AgentChatResponse,",
|
||
" activityRef?: ActivityRefSource",
|
||
"): Promise<AgentChatResponse> {",
|
||
" if (isTerminalStatus(initial.status)) return initial;",
|
||
" return pollAgentResult(initial);",
|
||
"}",
|
||
"",
|
||
].join("\n"),
|
||
}, { stderrOutput: true });
|
||
assertCondition(staleBlockReplacementVisibleV2.exitCode === 1 && staleBlockReplacementVisibleV2.error === null, "v2 should reject stale block-replacement hunks without falling back", staleBlockReplacementVisibleV2);
|
||
assertCondition(
|
||
staleBlockReplacementVisibleV2.stderr.includes("Best partial context match: 4 expected line(s) matched")
|
||
&& staleBlockReplacementVisibleV2.stderr.includes("stale or oversized context for a block/function replacement")
|
||
&& staleBlockReplacementVisibleV2.stderr.includes("Do not switch to download/upload")
|
||
&& staleBlockReplacementVisibleV2.stderr.includes("remote Python/Perl/sed")
|
||
&& staleBlockReplacementVisibleV2.stderr.includes("retry apply-patch with a smaller hunk")
|
||
&& staleBlockReplacementVisibleV2.stderr.includes("split the edit into hunks around unique anchors"),
|
||
"v2 stale block replacement failure should steer MiniMax back to smaller apply-patch hunks instead of file transfer or script rewrites",
|
||
staleBlockReplacementVisibleV2.stderr,
|
||
);
|
||
|
||
const fragmentedEnvelopeFailureV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: fragment.txt",
|
||
"@@",
|
||
"-alpha",
|
||
"+ALPHA",
|
||
"*** End Patch",
|
||
"printf '%s\\n' '*** Begin Patch'",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"fragment.txt": "alpha\n",
|
||
}, { stderrOutput: true });
|
||
assertCondition(fragmentedEnvelopeFailureV2.exitCode === 1 && fragmentedEnvelopeFailureV2.error === null, "v2 should reject non-patch shell fragments with a parser hint", fragmentedEnvelopeFailureV2);
|
||
assertCondition(
|
||
fragmentedEnvelopeFailureV2.stderr.includes("invalid hunk header")
|
||
&& fragmentedEnvelopeFailureV2.stderr.includes("concatenated MiniMax/MXCX fragments")
|
||
&& fragmentedEnvelopeFailureV2.stderr.includes("exactly one outer *** Begin Patch / *** End Patch envelope")
|
||
&& fragmentedEnvelopeFailureV2.stderr.includes("v2 engine only")
|
||
&& fragmentedEnvelopeFailureV2.stderr.includes("do not retry by switching to apply-patch-v1"),
|
||
"v2 parser failure should hint the MXCX printf/heredoc envelope fix without falling back to v1",
|
||
fragmentedEnvelopeFailureV2.stderr,
|
||
);
|
||
|
||
const nestedEnvelopeV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Update File: nested-envelope.txt",
|
||
"@@",
|
||
" alpha",
|
||
"*** End Patch",
|
||
"*** Begin Patch",
|
||
"*** Update File: nested-envelope.txt",
|
||
"@@",
|
||
"-beta",
|
||
"+BETA",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"nested-envelope.txt": "alpha\nbeta\n",
|
||
}, { stderrOutput: true });
|
||
assertCondition(nestedEnvelopeV2.exitCode === 0 && nestedEnvelopeV2.error === null, "v2 should accept MiniMax-style nested patch envelopes between hunks", nestedEnvelopeV2);
|
||
assertCondition(nestedEnvelopeV2.files["nested-envelope.txt"] === "alpha\nBETA\n", "v2 nested-envelope compatibility should still apply the later hunk", nestedEnvelopeV2);
|
||
assertCondition(
|
||
nestedEnvelopeV2.stderr.includes("ignored nested MiniMax-style End Patch marker")
|
||
&& nestedEnvelopeV2.stderr.includes("ignored nested MiniMax-style Begin Patch marker"),
|
||
"v2 nested-envelope compatibility should emit canonical envelope hints",
|
||
nestedEnvelopeV2.stderr,
|
||
);
|
||
|
||
const addBeforeFailedUpdateV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Add File: hwpod",
|
||
"+#!/bin/sh",
|
||
"+exec node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs \"$@\"",
|
||
"*** Update File: scripts/artifact-publish.mjs",
|
||
"@@",
|
||
"-missing artifact line",
|
||
"+patched artifact line",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"scripts/artifact-publish.mjs": "actual artifact line\n",
|
||
});
|
||
assertCondition(addBeforeFailedUpdateV2.error !== null, "v2 should still fail when a later file hunk misses", addBeforeFailedUpdateV2);
|
||
assertCondition(addBeforeFailedUpdateV2.files.hwpod?.includes("device-pod-cli.mjs"), "v2 should preserve an earlier Add File from a large patch when a later hunk misses", addBeforeFailedUpdateV2);
|
||
assertCondition(addBeforeFailedUpdateV2.files["scripts/artifact-publish.mjs"] === "actual artifact line\n", "v2 must leave the failed later file unchanged", addBeforeFailedUpdateV2);
|
||
|
||
const addFileWithHunkMarkerV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Add File: bad-add.txt",
|
||
"@@",
|
||
"+content",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {}, { stderrOutput: true });
|
||
assertCondition(addFileWithHunkMarkerV2.exitCode === 0 && addFileWithHunkMarkerV2.error === null, "v2 should accept MiniMax-style Add File @@ misuse with a hint", addFileWithHunkMarkerV2);
|
||
assertCondition(addFileWithHunkMarkerV2.stdout.includes("A bad-add.txt"), "v2 MiniMax-style Add File should still apply the new file", addFileWithHunkMarkerV2);
|
||
assertCondition(
|
||
addFileWithHunkMarkerV2.stderr.includes("accepted MiniMax-style @@ inside Add File bad-add.txt")
|
||
&& addFileWithHunkMarkerV2.stderr.includes("Add File does not need @@"),
|
||
"v2 MiniMax-style Add File compatibility should still hint the canonical syntax",
|
||
addFileWithHunkMarkerV2.stderr,
|
||
);
|
||
assertCondition(addFileWithHunkMarkerV2.files["bad-add.txt"] === "content\n", "v2 MiniMax-style Add File compatibility should preserve content", addFileWithHunkMarkerV2);
|
||
|
||
const addFileBareLinesV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Add File: loose-add.txt",
|
||
"first",
|
||
"",
|
||
"+ ",
|
||
"+third",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {}, { stderrOutput: true });
|
||
assertCondition(addFileBareLinesV2.exitCode === 0 && addFileBareLinesV2.error === null, "v2 should accept MiniMax-style Add File bare content and blank lines with hints", addFileBareLinesV2);
|
||
assertCondition(addFileBareLinesV2.files["loose-add.txt"] === "first\n\n\nthird\n", "v2 MiniMax-style Add File loose lines should preserve intended content", addFileBareLinesV2);
|
||
assertCondition(
|
||
addFileBareLinesV2.stderr.includes("accepted unprefixed Add File content in loose-add.txt")
|
||
&& addFileBareLinesV2.stderr.includes("accepted a bare blank line inside Add File loose-add.txt")
|
||
&& addFileBareLinesV2.stderr.includes("accepted MiniMax-style whitespace-only Add File line in loose-add.txt"),
|
||
"v2 MiniMax-style Add File loose lines should emit canonical syntax hints",
|
||
addFileBareLinesV2.stderr,
|
||
);
|
||
|
||
const deleteFileWithHunkMarkerV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Delete File: bad-delete.txt",
|
||
"@@",
|
||
"-content",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), { "bad-delete.txt": "content\n" }, { stderrOutput: true });
|
||
assertCondition(deleteFileWithHunkMarkerV2.exitCode === 0 && deleteFileWithHunkMarkerV2.error === null, "v2 should accept MiniMax-style Delete File @@ misuse with a hint", deleteFileWithHunkMarkerV2);
|
||
assertCondition(deleteFileWithHunkMarkerV2.stdout.includes("D bad-delete.txt"), "v2 MiniMax-style Delete File should still delete the file", deleteFileWithHunkMarkerV2);
|
||
assertCondition(
|
||
deleteFileWithHunkMarkerV2.stderr.includes("ignored extra MiniMax-style hunk/body lines after Delete File bad-delete.txt")
|
||
&& deleteFileWithHunkMarkerV2.stderr.includes("Delete File only needs the header"),
|
||
"v2 MiniMax-style Delete File compatibility should still hint the canonical syntax",
|
||
deleteFileWithHunkMarkerV2.stderr,
|
||
);
|
||
assertCondition(deleteFileWithHunkMarkerV2.files["bad-delete.txt"] === undefined, "v2 MiniMax-style Delete File compatibility should delete the file", deleteFileWithHunkMarkerV2);
|
||
|
||
const sequentialCompoundV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: sequence.txt",
|
||
"@@",
|
||
"-alpha",
|
||
"+beta",
|
||
"*** Update File: sequence.txt",
|
||
"@@",
|
||
"-beta",
|
||
"+gamma",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"sequence.txt": "alpha\n",
|
||
});
|
||
assertCondition(sequentialCompoundV2.files["sequence.txt"] === "gamma\n", "v2 should plan later hunks against earlier planned edits before remote writes", sequentialCompoundV2);
|
||
|
||
const emptyPatchV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"empty.txt": "kept\n",
|
||
});
|
||
assertCondition(emptyPatchV2.error !== null, "v2 should reject empty patches like Codex apply_patch", emptyPatchV2);
|
||
assertCondition(!emptyPatchV2.commands.some((command) => command.startsWith("write-b64") || command.startsWith("delete")), "empty v2 patches must not touch remote files", emptyPatchV2.commands);
|
||
|
||
const environmentPreambleV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Environment ID: remote",
|
||
"*** Update File: env.txt",
|
||
"@@",
|
||
"-before",
|
||
"+after",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"env.txt": "before\n",
|
||
});
|
||
assertCondition(environmentPreambleV2.files["env.txt"] === "after\n", "v2 should accept Codex apply_patch environment_id preamble", environmentPreambleV2);
|
||
|
||
const absolutePathV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: /tmp/absolute.txt",
|
||
"@@",
|
||
"-old",
|
||
"+new",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"/tmp/absolute.txt": "old\n",
|
||
});
|
||
assertCondition(absolutePathV2.files["/tmp/absolute.txt"] === "new\n", "v2 should accept absolute paths like Codex apply_patch when the executor route supports them", absolutePathV2);
|
||
|
||
const parentSegmentPathV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: ../outside.txt",
|
||
"@@",
|
||
"-old",
|
||
"+new",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"../outside.txt": "old\n",
|
||
});
|
||
assertCondition(parentSegmentPathV2.files["../outside.txt"] === "new\n", "v2 should leave path containment policy to the executor like Codex apply_patch", parentSegmentPathV2);
|
||
|
||
const missingDeleteV2 = await applyPatchV2FixtureAttempt([
|
||
"*** Begin Patch",
|
||
"*** Delete File: missing.txt",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"keep.txt": "kept\n",
|
||
});
|
||
assertCondition(missingDeleteV2.error !== null, "v2 should reject deleting a missing file like Codex apply_patch", missingDeleteV2);
|
||
assertCondition(missingDeleteV2.files["keep.txt"] === "kept\n", "missing delete must leave unrelated files unchanged", missingDeleteV2);
|
||
|
||
const moveOverwriteV2 = await applyPatchV2Fixture([
|
||
"*** Begin Patch",
|
||
"*** Update File: old-name.txt",
|
||
"*** Move to: new-name.txt",
|
||
"@@",
|
||
"-old content",
|
||
"+new content",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"old-name.txt": "old content\n",
|
||
"new-name.txt": "existing content\n",
|
||
});
|
||
assertCondition(!Object.prototype.hasOwnProperty.call(moveOverwriteV2.files, "old-name.txt"), "v2 rename should remove the source file", moveOverwriteV2);
|
||
assertCondition(moveOverwriteV2.files["new-name.txt"] === "new content\n", "v2 rename should overwrite the destination like Codex apply_patch", moveOverwriteV2);
|
||
|
||
const safePatch = applyPatchFixture([], [
|
||
"*** Begin Patch",
|
||
"*** Update File: sample.txt",
|
||
"@@",
|
||
" function alpha() {",
|
||
" return 1;",
|
||
" }",
|
||
"+function inserted() {",
|
||
"+ return 3;",
|
||
"+}",
|
||
" function beta() {",
|
||
" return 2;",
|
||
" }",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"sample.txt": [
|
||
"function alpha() {",
|
||
" return 1;",
|
||
"}",
|
||
"function beta() {",
|
||
" return 2;",
|
||
"}",
|
||
"",
|
||
].join("\n"),
|
||
});
|
||
assertCondition(safePatch.status === 0, "apply_patch sh helper should accept anchored insertions", safePatch);
|
||
assertCondition(safePatch.stderr.includes("apply_patch: hunk 1 matched sample.txt:1"), "apply_patch sh helper must report matched file:line", safePatch);
|
||
assertCondition(safePatch.files["sample.txt"]?.includes("function inserted()"), "apply_patch sh helper should apply anchored insertion", safePatch);
|
||
|
||
const lowContextPatch = applyPatchFixture([], [
|
||
"*** Begin Patch",
|
||
"*** Update File: sample.txt",
|
||
"@@",
|
||
" function alpha() {",
|
||
" return 1;",
|
||
" }",
|
||
"+function misplaced() {",
|
||
"+ return 9;",
|
||
"+}",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"sample.txt": [
|
||
"function alpha() {",
|
||
" return 1;",
|
||
"}",
|
||
"function beta() {",
|
||
" return 2;",
|
||
"}",
|
||
"",
|
||
].join("\n"),
|
||
});
|
||
assertCondition(lowContextPatch.status !== 0, "apply_patch sh helper should reject insert-only hunks without trailing context", lowContextPatch);
|
||
assertCondition(lowContextPatch.stderr.includes("insert-only without both leading and trailing context"), "low-context rejection must explain the risk", lowContextPatch);
|
||
|
||
const duplicateContextPatch = applyPatchFixture([], [
|
||
"*** Begin Patch",
|
||
"*** Update File: sample.txt",
|
||
"@@",
|
||
" marker",
|
||
"+inserted",
|
||
" x",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"sample.txt": "marker\nx\nmarker\nx\n",
|
||
});
|
||
assertCondition(duplicateContextPatch.status !== 0, "apply_patch sh helper should reject hunks whose context matches multiple locations", duplicateContextPatch);
|
||
assertCondition(duplicateContextPatch.stderr.includes("matched multiple locations"), "duplicate context rejection must explain the risk", duplicateContextPatch);
|
||
|
||
const reviewedLoosePatch = applyPatchFixture(["--allow-loose"], [
|
||
"*** Begin Patch",
|
||
"*** Update File: sample.txt",
|
||
"@@",
|
||
" marker",
|
||
"+reviewed",
|
||
" x",
|
||
"*** End Patch",
|
||
"",
|
||
].join("\n"), {
|
||
"sample.txt": "marker\nx\nmarker\nx\n",
|
||
});
|
||
assertCondition(reviewedLoosePatch.status === 0, "--allow-loose should allow an explicitly reviewed ambiguous hunk", reviewedLoosePatch);
|
||
assertCondition(reviewedLoosePatch.files["sample.txt"] === "marker\nreviewed\nx\nmarker\nx\n", "--allow-loose should still apply only one hunk", reviewedLoosePatch);
|
||
|
||
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:hwlab-ci:kubectl", ["get", "pods"]),
|
||
/route must locate a target only.*operation "kubectl" after the route/u,
|
||
"operation names must not be accepted as nested k3s route segments",
|
||
);
|
||
assertThrows(
|
||
() => parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api:logs", []),
|
||
/route must locate a target only.*operation "logs" after the route/u,
|
||
"operation names must not be accepted as k3s container 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 routePodWorkspace = parseSshInvocation("D601:k3s:hwlab-dev:pod:hwlab-cloud-api-abc/workspace/app:api", ["pwd"]);
|
||
assertCondition(routePodWorkspace.route.resource === "pod/hwlab-cloud-api-abc" && routePodWorkspace.route.container === "api" && routePodWorkspace.route.workspace === "/workspace/app", "pod route must support a workspace suffix after the pod id", routePodWorkspace);
|
||
assertCondition(routePodWorkspace.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'pod/hwlab-cloud-api-abc' '-c' 'api' '--' 'sh' '-c' 'cd \"$1\" || exit; shift; exec \"$@\"' 'unidesk-cwd' '/workspace/app' 'pwd'", "pod workspace route must run commands through a fixed cwd wrapper", routePodWorkspace);
|
||
|
||
const legacyRoutePodTarget = parseSshInvocation("D601:k3s:hwlab-dev:pod/hwlab-cloud-api-abc:api", ["printenv", "HOSTNAME"]);
|
||
assertCondition(legacyRoutePodTarget.parsed.remoteCommand === routePodTarget.parsed.remoteCommand, "legacy pod/name route remains accepted for compatibility", legacyRoutePodTarget);
|
||
|
||
const routeDeploymentWorkspaceScript = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["script"]);
|
||
assertCondition(routeDeploymentWorkspaceScript.route.resource === "hwlab-cloud-api" && routeDeploymentWorkspaceScript.route.workspace === "/app", "deployment shorthand route must support workspace suffix", routeDeploymentWorkspaceScript);
|
||
assertCondition(routeDeploymentWorkspaceScript.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-i' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'sh' '-c' 'cd \"$1\" || exit; shift; exec \"$@\"' 'unidesk-cwd' '/app' 'sh' '-s' '--'", "pod workspace script must set cwd before shell -s consumes stdin", routeDeploymentWorkspaceScript);
|
||
|
||
const routeApplyPatchWorkspace = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["apply-patch"]);
|
||
assertCondition(routeApplyPatchWorkspace.parsed.requiresStdin === true, "pod workspace apply-patch must still stream patch stdin", routeApplyPatchWorkspace);
|
||
assertCondition(routeApplyPatchWorkspace.parsed.remoteCommand === null, "pod workspace apply-patch must use the default v2 local engine", routeApplyPatchWorkspace);
|
||
|
||
const routeApplyPatchV1Workspace = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api/app", ["apply-patch-v1"]);
|
||
assertCondition(routeApplyPatchV1Workspace.parsed.requiresStdin === true, "pod workspace apply-patch-v1 must still stream patch stdin", routeApplyPatchV1Workspace);
|
||
assertCondition(routeApplyPatchV1Workspace.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-i' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'sh' '-c' 'cd \"$1\" || exit; shift; exec \"$@\"' 'unidesk-cwd' '/app' 'sh' '-s' '--'", "pod workspace apply-patch-v1 must set cwd before injecting the sh helper", routeApplyPatchV1Workspace);
|
||
|
||
const routeExecStdin = parseSshInvocation("D601:k3s:unidesk:code-queue/root/unidesk", ["exec", "--stdin", "--", "tar", "-xf", "-", "-C", "/root/unidesk"]);
|
||
assertCondition(routeExecStdin.parsed.requiresStdin === true, "pod route exec --stdin must stream local stdin into kubectl exec", routeExecStdin);
|
||
assertCondition(routeExecStdin.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'unidesk' 'deployment/code-queue' '-i' '--' 'sh' '-c' 'cd \"$1\" || exit; shift; exec \"$@\"' 'unidesk-cwd' '/root/unidesk' 'tar' '-xf' '-' '-C' '/root/unidesk'", "pod route exec --stdin must keep exec flags before -- and command argv after --", routeExecStdin);
|
||
|
||
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 === "trans 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 timingInfo = sshRuntimeTimingHint({
|
||
invocation: parseSshInvocation("D601:/home/ubuntu/workspace/hwlab-dev", ["argv", "true"]),
|
||
transport: "backend-core-broker",
|
||
exitCode: 0,
|
||
startedAtMs: 1000,
|
||
finishedAtMs: 5200,
|
||
thresholdMs: 10_000,
|
||
});
|
||
assertCondition(timingInfo.level === "info" && timingInfo.slow === false, "short ssh operation should stay below the timing warning threshold", timingInfo);
|
||
assertCondition(timingInfo.elapsedMs === 4200 && timingInfo.elapsedSeconds === 4.2, "timing hint must include elapsed ms and seconds", timingInfo);
|
||
assertCondition(formatSshRuntimeTimingHint(timingInfo) === "", "short ssh operation must not write routine timing noise to stderr", timingInfo);
|
||
const slowTiming = sshRuntimeTimingHint({
|
||
invocation: parseSshInvocation("D601", ["apply-patch"]),
|
||
transport: "frontend-websocket",
|
||
exitCode: 0,
|
||
startedAtMs: 0,
|
||
finishedAtMs: 12_345,
|
||
thresholdMs: 10_000,
|
||
});
|
||
assertCondition(slowTiming.level === "warning" && slowTiming.slow === true, "slow ssh operation should emit a warning timing hint", slowTiming);
|
||
assertCondition(slowTiming.message.includes("above the 10s warning threshold"), "slow timing warning must explain the threshold", slowTiming);
|
||
const formattedTiming = formatSshRuntimeTimingHint(slowTiming);
|
||
assertCondition(formattedTiming.startsWith("UNIDESK_SSH_TIMING "), "formatted timing hint must have structured prefix", formattedTiming);
|
||
assertCondition(formattedTiming.includes("\"exitCode\":0"), "slow successful ssh operation must still emit timing warning as a performance signal", formattedTiming);
|
||
assertCondition(!formattedTiming.includes("apply_patch"), "timing hint must not echo the original remote command", formattedTiming);
|
||
|
||
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 help = sshHelp() as { notes?: unknown };
|
||
const helpText = JSON.stringify(help);
|
||
const helpNotes = Array.isArray(help.notes) ? help.notes.map(String).join("\n") : "";
|
||
assertCondition(helpText.includes("trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'"), "ssh help must recommend stdin script passthrough for shell scripts", helpText);
|
||
assertCondition(helpText.includes("trans <providerId> shell [--shell sh|bash]") && helpText.includes("outer shell operators written outside trans"), "ssh help must document one-line shell passthrough and the local operator boundary", helpText);
|
||
assertCondition(helpText.includes("inherits provider proxy variables"), "ssh help must state default script inherits provider proxy env", helpText);
|
||
assertCondition(helpText.includes("not as a proxy workaround"), "ssh help must reserve --shell bash for bash syntax instead of proxy workarounds", helpText);
|
||
assertCondition(helpNotes.includes("portable printf headings") && helpNotes.includes('printf "--- section ---\\n"'), "ssh help must document script/shell printf heading compatibility", helpNotes);
|
||
assertCondition(helpText.includes("trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch"), "ssh help must document host workspace routes", helpText);
|
||
assertCondition(helpText.includes("trans D601:win ps <<'PS'") && helpText.includes("trans D601:win/c/test ps <<'PS'"), "ssh help must document Windows PowerShell ps routes", helpText);
|
||
assertCondition(helpText.includes("Use `win`, not `win32`") && helpText.includes("win route is a Windows operation plane"), "ssh help must document win route naming and operation boundary", helpText);
|
||
assertCondition(helpText.includes("Do not use `script` for Windows PowerShell") && helpText.includes("trans <provider>:win ps <<'PS'"), "ssh help must direct Windows PowerShell users to ps rather than script", helpText);
|
||
assertCondition(helpText.includes("trans D601:k3s kubectl get pods -n hwlab-dev"), "ssh help must document k3s kubectl operation", helpText);
|
||
assertCondition(helpText.includes("trans G14:k3s kubectl get pipelineruns -n hwlab-ci"), "ssh help must document G14 k3s route operation", helpText);
|
||
assertCondition(helpText.includes("trans D601:k3s:hwlab-dev:hwlab-cloud-api/app pwd"), "ssh help must document k3s pod workspace route", helpText);
|
||
assertCondition(helpText.includes("trans D601:k3s script <<'SCRIPT'"), "ssh help must document k3s control-plane script operation", helpText);
|
||
assertCondition(helpText.includes("trans D601:k3s:hwlab-dev:hwlab-cloud-api/app apply-patch <<'PATCH'"), "ssh help must document k3s pod apply-patch operation", helpText);
|
||
assertCondition(helpText.includes("trans <route> upload <local-file> <remote-file>") && helpText.includes("trans <route> download <remote-file> <local-file>"), "ssh help must document verified file transfer operations", helpText);
|
||
assertCondition(helpText.includes("trans D601:k3s:unidesk:code-queue/root/unidesk exec --stdin -- tar -xf - -C /root/unidesk"), "ssh help must document one-step stdin file streaming into pod exec", helpText);
|
||
assertCondition(helpText.includes("apply-patch-v1 [--allow-loose]") && helpText.includes("low-context update hunks"), "ssh help must document apply-patch-v1 loose-context guard", helpText);
|
||
assertCondition(helpText.includes("trans 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_APPLY_PATCH_TIMING") && helpText.includes("remoteOperationCounts") && helpText.includes("durationMs"), "ssh help must document apply-patch aggregate timing summary", 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("trans D601 argv true"), "provider triage cross-checks must keep argv true", crossChecks);
|
||
|
||
const frontendRemoteK3sPlan = remoteSshFrontendPlanForTest("D601:k3s", ["kubectl", "get", "nodes", "-o", "name"]);
|
||
assertCondition(frontendRemoteK3sPlan.transport === "frontend-websocket", "remote frontend ssh must use the streaming websocket bridge", frontendRemoteK3sPlan);
|
||
assertCondition(frontendRemoteK3sPlan.providerId === "D601", "remote frontend ssh must dispatch route target to the provider id", frontendRemoteK3sPlan);
|
||
assertCondition(frontendRemoteK3sPlan.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'get' 'nodes' '-o' 'name'", "remote frontend ssh must preserve k3s route command construction", frontendRemoteK3sPlan);
|
||
assertCondition(!String(frontendRemoteK3sPlan.wrappedRemoteCommand ?? "").includes("UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools"), "remote frontend ssh must not bootstrap helper tools for plain kubectl argv", frontendRemoteK3sPlan);
|
||
|
||
const frontendRemoteHostPatchPlan = remoteSshFrontendPlanForTest("D601", ["apply-patch"]);
|
||
assertCondition(frontendRemoteHostPatchPlan.requiresStdin === true && frontendRemoteHostPatchPlan.remoteCommand === null && !String(frontendRemoteHostPatchPlan.wrappedRemoteCommand ?? "").includes("UNIDESK_SSH_TOOL_DIR"), "frontend apply-patch plan must stay a local v2 engine operation and not bootstrap legacy helpers", frontendRemoteHostPatchPlan);
|
||
const frontendRemoteHostPatchSeparatorPlan = remoteSshFrontendPlanForTest("D601", ["--", "apply-patch"]);
|
||
assertCondition(frontendRemoteHostPatchSeparatorPlan.requiresStdin === true && frontendRemoteHostPatchSeparatorPlan.remoteCommand === null, "frontend apply-patch plan should also accept route-level -- before apply-patch", frontendRemoteHostPatchSeparatorPlan);
|
||
|
||
const frontendRemoteV1Plan = remoteSshFrontendPlanForTest("D601:/tmp", ["apply-patch-v1"]);
|
||
assertCondition(String(frontendRemoteV1Plan.wrappedRemoteCommand ?? "").includes("UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools"), "frontend apply-patch-v1 must bootstrap the remote apply_patch helper", frontendRemoteV1Plan);
|
||
assertCondition(String(frontendRemoteV1Plan.wrappedRemoteCommand ?? "").includes("/apply_patch") && !String(frontendRemoteV1Plan.wrappedRemoteCommand ?? "").includes("/glob") && !String(frontendRemoteV1Plan.wrappedRemoteCommand ?? "").includes("/skill-discover"), "frontend apply-patch-v1 must not bootstrap unrelated helper tools", frontendRemoteV1Plan);
|
||
|
||
const frontendRemotePodArgvPlan = remoteSshFrontendPlanForTest("G14:k3s:unidesk:code-queue", ["argv", "sh", "-c", "command -v tran"]);
|
||
assertCondition(frontendRemotePodArgvPlan.providerId === "G14", "remote frontend pod route must dispatch through G14 provider", frontendRemotePodArgvPlan);
|
||
assertCondition(frontendRemotePodArgvPlan.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'unidesk' 'deployment/code-queue' '--' 'sh' '-c' 'command -v tran'", "remote frontend pod argv route must be fully assembled before dispatch", frontendRemotePodArgvPlan);
|
||
|
||
const frontendRemoteWorkspacePlan = remoteSshFrontendPlanForTest("D601:/home/ubuntu/workspace/hwlab-dev", ["git", "status", "--short"]);
|
||
assertCondition(frontendRemoteWorkspacePlan.payloadCwd === "/home/ubuntu/workspace/hwlab-dev", "remote frontend host workspace route must pass cwd to host.ssh payload", frontendRemoteWorkspacePlan);
|
||
assertCondition(frontendRemoteWorkspacePlan.remoteCommand === "'git' 'status' '--short'", "remote frontend host workspace route must keep command argv-quoted", frontendRemoteWorkspacePlan);
|
||
|
||
const frontendRemoteWinPlan = remoteSshFrontendPlanForTest("D601:win/c/test", ["ps", "Get-Location"]);
|
||
assertCondition(frontendRemoteWinPlan.providerId === "D601" && frontendRemoteWinPlan.payloadCwd === "/mnt/c/Windows", "remote frontend win route must dispatch through provider host.ssh from a Windows-mounted cwd", frontendRemoteWinPlan);
|
||
const frontendRemoteWinScript = decodeWinEncodedCommand(String(frontendRemoteWinPlan.remoteCommand));
|
||
assertCondition(frontendRemoteWinScript.includes("powershell.exe") && frontendRemoteWinScript.includes("Set-Location -LiteralPath ''C:\\test''") && frontendRemoteWinScript.includes("Get-Location"), "remote frontend win route must assemble Windows PowerShell cwd internally", { frontendRemoteWinPlan, frontendRemoteWinScript });
|
||
|
||
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") && !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);
|
||
assertCondition(remoteSource.includes("frontendSshWebSocketUrl") && remoteSource.includes("runRemoteSshWebSocket"), "remote frontend ssh must go through the streaming websocket implementation", remoteSource);
|
||
assertCondition(remoteSource.includes("UNIDESK_SSH_CLIENT_TOKEN") && remoteSource.includes("authorization: `Bearer ${session.sshClientToken}`"), "remote frontend ssh must support scoped bearer-token clients without frontend admin login", remoteSource);
|
||
assertCondition(!remoteSource.includes("remote frontend transport does not stream stdin"), "remote frontend ssh must not reject stdin-backed helpers", remoteSource);
|
||
assertCondition(!remoteSource.includes("source: \"cli-remote-ssh\""), "remote frontend ssh must not use host.ssh dispatch task polling", remoteSource);
|
||
|
||
const sshSource = readFileSync(new URL("./src/ssh.ts", import.meta.url), "utf8");
|
||
const sshFileTransferSource = readFileSync(new URL("./src/ssh-file-transfer.ts", import.meta.url), "utf8");
|
||
assertCondition(sshFileTransferSource.includes("runSshFileTransferOperation") && sshFileTransferSource.includes("write-b64-commit"), "file transfer operation implementation must live in the dedicated ssh-file-transfer module", {});
|
||
assertCondition(sshFileTransferSource.includes("buildTransferVerification") && sshFileTransferSource.includes("automatic: true") && sshFileTransferSource.includes("match"), "file transfer JSON must expose automatic endpoint verification instead of relying on manual sha256sum checks", sshFileTransferSource);
|
||
assertCondition(!sshSource.includes("type SshFileTransferOperation") && !sshSource.includes("posixFileTransferScript"), "ssh.ts must not accumulate the full upload/download implementation", {});
|
||
|
||
const frontendSource = readFileSync(new URL("../src/components/frontend/src/index.ts", import.meta.url), "utf8");
|
||
assertCondition(frontendSource.includes('url.pathname === "/ws/ssh"') && frontendSource.includes("proxySshWebSocket"), "frontend must expose an authenticated /ws/ssh proxy", frontendSource);
|
||
assertCondition(frontendSource.includes("coreSshWebSocketUrl") && frontendSource.includes('url.searchParams.set("token"'), "frontend /ws/ssh proxy must connect to backend-core ssh bridge with the provider token", frontendSource);
|
||
assertCondition(frontendSource.includes("PROVIDER_TOKEN_FILE") && frontendSource.includes("/run/secrets/unidesk_provider_token"), "frontend ssh proxy must support file-based provider token injection for runtime hotfix and secret mounts", frontendSource);
|
||
assertCondition(frontendSource.includes("UNIDESK_SSH_CLIENT_TOKEN") && frontendSource.includes("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST"), "frontend ssh proxy must support scoped client-token configuration", frontendSource);
|
||
assertCondition(frontendSource.includes("route-not-allowed") && frontendSource.includes("sshRouteAllowed") && frontendSource.includes("ssh.open"), "frontend ssh proxy must reject disallowed scoped-client routes before opening a provider session", frontendSource);
|
||
|
||
const composeSource = readFileSync(new URL("../docker-compose.yml", import.meta.url), "utf8");
|
||
assertCondition(composeSource.includes('PROVIDER_TOKEN: "${UNIDESK_PROVIDER_TOKEN}"'), "frontend compose service must receive provider token for the ssh proxy", composeSource);
|
||
assertCondition(composeSource.includes('UNIDESK_SSH_CLIENT_TOKEN: "${UNIDESK_SSH_CLIENT_TOKEN:-}"') && composeSource.includes("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST"), "frontend compose service must receive scoped ssh client token and route allowlist", composeSource);
|
||
const dockerCliSource = readFileSync(new URL("./src/docker.ts", import.meta.url), "utf8");
|
||
assertCondition(dockerCliSource.includes('UNIDESK_SSH_CLIENT_TOKEN: runtimeSecret("UNIDESK_SSH_CLIENT_TOKEN")') && dockerCliSource.includes('UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST: runtimeSecret("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST")'), "server rebuild must preserve scoped ssh client runtime env instead of dropping it from docker-compose.env", dockerCliSource);
|
||
|
||
const devCoreManifest = readFileSync(new URL("../src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml", import.meta.url), "utf8");
|
||
assertCondition(devCoreManifest.includes("name: frontend-dev") && devCoreManifest.includes("name: PROVIDER_TOKEN"), "dev frontend manifest must receive provider token for the ssh proxy", devCoreManifest);
|
||
|
||
const codeQueueDockerfile = readFileSync(new URL("../src/components/microservices/code-queue/Dockerfile", import.meta.url), "utf8");
|
||
assertCondition(codeQueueDockerfile.includes("COPY scripts/tran /usr/local/bin/tran") && codeQueueDockerfile.includes("chmod 755 /usr/local/bin/tran"), "Code Queue runner image must install tran on PATH", codeQueueDockerfile);
|
||
|
||
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",
|
||
"script -- single-string runs as a remote shell one-liner while multi-token form keeps dash-prefixed argv",
|
||
"script/shell helpers inject a portable printf prelude for common section headings",
|
||
"pod apply-patch operation uses the v2 local engine and apply-patch-v1 injects the legacy helper",
|
||
"pod exec --stdin streams arbitrary local stdin through workload routes without shell wrapping",
|
||
"upload/download file transfer operations use a dedicated module with automatic endpoint byte-count and sha256 verification JSON",
|
||
"apply-patch-v1 uses one sh helper for host and pod paths and rejects low-context hunks unless --allow-loose is explicit",
|
||
"legacy operation-in-route forms are rejected in any k3s route segment with canonical route-plus-operation guidance",
|
||
"post-provider k3s shorthand is rejected so location and operation stay separated",
|
||
"k3s route stays location-only while operations fix native kubeconfig and assemble kubectl exec as argv",
|
||
"win route supports Windows PowerShell ps heredoc with slash cwd syntax such as D601:win/c/test",
|
||
"win skills discovers the current Windows user's skill roots without hand-written cmd dir or PowerShell",
|
||
"top-level remote option parsing preserves command-local -- separators for script -- sed -n style commands",
|
||
"ssh-like timeout/kex failures emit one structured argv retry hint",
|
||
"ssh runtime emits structured timing for slow operations over 10 seconds, including successful slow calls",
|
||
"help text documents stdin script passthrough and UNIDESK_SSH_HINT",
|
||
"provider triage recommendedCrossChecks keeps trans D601 argv true",
|
||
"remote frontend ssh uses the same structured route parser for host, k3s and pod argv routes",
|
||
"ssh helper bootstrap is lazy so plain argv/script commands do not transfer helper sources",
|
||
"host apply-patch-v1 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 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",
|
||
],
|
||
};
|
||
}
|
||
|
||
if (import.meta.main) {
|
||
const result = await runSshArgvGuidanceContract();
|
||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||
}
|