[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,747 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const {
addCodexExecutableEnvForSdk,
buildWindowsShellCommandLine,
extractTrailingIdlePrompt,
formatSyntheticEcho,
getFreshIdlePrompt,
isDefaultPowerShellPromptLine,
isDefaultCmdPromptLine,
isDefaultPosixPromptLine,
isPlausibleCliVersionOutput,
looksLikeIdleAutoLogout,
prepareCommandForSpawn,
resolveWindowsShimToNativeExe,
resolveClaudeCodeExecutableForSdk,
resolveCodexExecutableForSdk,
resolveCodebuddyExecutableForSdk,
parseRegQueryPath,
expandWindowsEnvRefs,
mergeWindowsPath,
readWindowsRegistryPath,
trackSessionIdlePrompt,
} = require("./shellUtils.cjs");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
test("formatSyntheticEcho normalizes multi-line commands to CRLF so xterm doesn't staircase", () => {
assert.equal(
formatSyntheticEcho("set -e\ncd /tmp\necho done"),
"set -e\r\ncd /tmp\r\necho done\r\n",
);
// Already-CRLF input is not doubled.
assert.equal(formatSyntheticEcho("a\r\nb"), "a\r\nb\r\n");
// Single-line commands keep the original shape.
assert.equal(formatSyntheticEcho("npm test"), "npm test\r\n");
});
test("extracts a trailing PowerShell idle prompt", () => {
assert.equal(
extractTrailingIdlePrompt("Microsoft Windows...\r\nPS C:\\Users\\alice>"),
"PS C:\\Users\\alice>",
);
});
test("preserves trailing whitespace on a captured PowerShell prompt", () => {
// The wrapper-selection logic trims this, but the suffix-match logic in
// hasExpectedPromptSuffix() compares against raw PTY bytes, so the trailing
// space PowerShell emits after `>` must round-trip unchanged.
assert.equal(
extractTrailingIdlePrompt("Microsoft Windows...\r\nPS C:\\Users\\alice> "),
"PS C:\\Users\\alice> ",
);
});
test("extracts a bare PowerShell prompt with no working directory", () => {
assert.equal(extractTrailingIdlePrompt("welcome\r\nPS>"), "PS>");
});
test("does not extract content that merely looks PowerShell-ish", () => {
// Any non-prompt output ending in `PSO>` or `ZIPS>` would have produced a
// trailing newline before the next prompt; this guards against the regex
// accidentally matching command output that just happens to contain "PS".
assert.equal(extractTrailingIdlePrompt("nope\r\nPSO>"), "");
assert.equal(extractTrailingIdlePrompt("nope\r\nZIPS>"), "");
});
test("rejects `PS >` (literal `PS` + space + `>`) so spoofed scripts can't masquerade as a default prompt", () => {
// Default PowerShell never emits this shape; rejecting it makes the
// override harder to coerce via printed output.
assert.equal(extractTrailingIdlePrompt("welcome\r\nPS >"), "");
});
test("treats CR repaints as line breaks so only the redrawn line is captured", () => {
// PSReadLine / ConPTY emit bare `\r` to repaint the current line. The
// captured prompt must equal the visible last line, not the
// concatenation of every overwritten frame, so hasExpectedPromptSuffix
// can still match the live PTY tail later.
assert.equal(
extractTrailingIdlePrompt("PS C:\\old>\rPS C:\\new>"),
"PS C:\\new>",
);
});
test("isDefaultPowerShellPromptLine matches default shapes and rejects look-alikes", () => {
assert.equal(isDefaultPowerShellPromptLine("PS C:\\Users\\alice>"), true);
assert.equal(isDefaultPowerShellPromptLine("PS /home/alice>"), true);
assert.equal(isDefaultPowerShellPromptLine("PS>"), true);
assert.equal(isDefaultPowerShellPromptLine("PS >"), false);
assert.equal(isDefaultPowerShellPromptLine("PSO>"), false);
assert.equal(isDefaultPowerShellPromptLine("ZIPS>"), false);
assert.equal(isDefaultPowerShellPromptLine(""), false);
assert.equal(isDefaultPowerShellPromptLine(null), false);
});
test("extracts a trailing cmd.exe idle prompt", () => {
// Windows OpenSSH default shell is cmd.exe; without capturing `C:\...>`
// AI exec cannot select the cmd wrapper when shellKind is still unset.
assert.equal(
extractTrailingIdlePrompt("Microsoft Windows...\r\nC:\\Users\\alice>"),
"C:\\Users\\alice>",
);
assert.equal(extractTrailingIdlePrompt("welcome\r\nC:\\>"), "C:\\>");
assert.equal(extractTrailingIdlePrompt("welcome\r\nD:\\data\\proj>"), "D:\\data\\proj>");
});
test("isDefaultCmdPromptLine matches drive-letter cmd prompts only", () => {
assert.equal(isDefaultCmdPromptLine("C:\\Users\\alice>"), true);
assert.equal(isDefaultCmdPromptLine("C:\\>"), true);
assert.equal(isDefaultCmdPromptLine("C:>"), true);
assert.equal(isDefaultCmdPromptLine("PS C:\\Users\\alice>"), false);
assert.equal(isDefaultCmdPromptLine("alice@host:~$"), false);
assert.equal(isDefaultCmdPromptLine("C: >"), false);
assert.equal(isDefaultCmdPromptLine(""), false);
});
test("isDefaultPosixPromptLine matches classic user@host prompts", () => {
assert.equal(isDefaultPosixPromptLine("alice@host:~$"), true);
assert.equal(isDefaultPosixPromptLine("alice@wsl:/mnt/c$"), true);
assert.equal(isDefaultPosixPromptLine("root@box:/#"), true);
assert.equal(isDefaultPosixPromptLine("root@host ~#"), false);
assert.equal(isDefaultPosixPromptLine("PS C:\\Users\\alice>"), false);
assert.equal(isDefaultPosixPromptLine("C:\\Users\\alice>"), false);
assert.equal(isDefaultPosixPromptLine(""), false);
});
test("isPlausibleCliVersionOutput rejects stack traces and file URLs", () => {
assert.equal(isPlausibleCliVersionOutput("2.1.123 (Claude Code)"), true);
assert.equal(isPlausibleCliVersionOutput("codex-cli 0.125.0"), true);
assert.equal(isPlausibleCliVersionOutput("file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:95"), false);
assert.equal(isPlausibleCliVersionOutput("TypeError: Cannot read properties of undefined"), false);
assert.equal(isPlausibleCliVersionOutput(" at runCli (cli.js:10:1)"), false);
assert.equal(isPlausibleCliVersionOutput("permission denied"), false);
assert.equal(isPlausibleCliVersionOutput("Usage: claude [options]"), false);
});
test("buildWindowsShellCommandLine quotes command paths and args with spaces", () => {
assert.equal(
buildWindowsShellCommandLine("C:\\Program Files\\Codex\\codex.cmd", ["login", "status"]),
"\"C:\\Program Files\\Codex\\codex.cmd\" \"login\" \"status\"",
);
});
test("prepareCommandForSpawn wraps Windows cmd shims as a single shell command", () => {
const result = prepareCommandForSpawn("C:\\Program Files\\Codex\\codex.cmd", ["--version"]);
if (process.platform === "win32") {
assert.deepEqual(result, {
command: "\"C:\\Program Files\\Codex\\codex.cmd\" \"--version\"",
args: [],
shell: true,
});
} else {
assert.deepEqual(result, {
command: "C:\\Program Files\\Codex\\codex.cmd",
args: ["--version"],
shell: false,
});
}
});
test("resolveClaudeCodeExecutableForSdk maps Windows npm cmd shim to Claude Code cli.js", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-claude-shim-"));
try {
const shimPath = path.join(tmp, "claude.cmd");
const scriptPath = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "cli.js");
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
fs.writeFileSync(scriptPath, "", "utf8");
fs.writeFileSync(
shimPath,
'@ECHO off\r\nnode "%basedir%\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\r\n',
"utf8",
);
assert.equal(resolveClaudeCodeExecutableForSdk(shimPath, "win32"), scriptPath);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveClaudeCodeExecutableForSdk leaves non-Windows Claude paths unchanged", () => {
assert.equal(
resolveClaudeCodeExecutableForSdk("/usr/local/bin/claude", "darwin"),
"/usr/local/bin/claude",
);
});
test("resolveClaudeCodeExecutableForSdk keeps Windows cmd shim when Claude Code cli.js is missing", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-claude-missing-cli-"));
try {
const shimPath = path.join(tmp, "claude.cmd");
fs.writeFileSync(
shimPath,
'@ECHO off\r\nnode "%basedir%\\node_modules\\@anthropic-ai\\claude-code\\cli.js" %*\r\n',
"utf8",
);
assert.equal(resolveClaudeCodeExecutableForSdk(shimPath, "win32"), shimPath);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveClaudeCodeExecutableForSdk maps Windows npm cmd shim to native claude.exe when cli.js is absent", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-claude-native-"));
try {
const shimPath = path.join(tmp, "claude.cmd");
const nativeExe = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe");
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
fs.writeFileSync(nativeExe, "", "utf8");
fs.writeFileSync(
shimPath,
'@ECHO off\r\n"%~dp0\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*\r\n',
"utf8",
);
assert.equal(resolveClaudeCodeExecutableForSdk(shimPath, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveWindowsShimToNativeExe resolves npm .cmd shim to native exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-shim-native-"));
try {
const shimPath = path.join(tmp, "claude.cmd");
const nativeExe = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe");
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
fs.writeFileSync(nativeExe, "", "utf8");
// Single backslashes in the .cmd content (%~dp0 expands to the shim dir)
fs.writeFileSync(
shimPath,
'@ECHO off\r\n"%~dp0\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*\r\n',
"utf8",
);
const resolved = resolveWindowsShimToNativeExe(shimPath, "win32");
assert.equal(resolved, nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("prepareCommandForSpawn can skip native exe unwrap for node+script shims", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-spawn-no-unwrap-"));
try {
const shimPath = path.join(tmp, "cursor-agent.cmd");
const nodeExe = path.join(tmp, "versions", "2026.06.01-abc", "node.exe");
fs.mkdirSync(path.dirname(nodeExe), { recursive: true });
fs.writeFileSync(nodeExe, "", "utf8");
fs.writeFileSync(
shimPath,
'@ECHO off\r\n"%~dp0\\versions\\2026.06.01-abc\\node.exe" "%~dp0\\versions\\2026.06.01-abc\\index.js" %*\r\n',
"utf8",
);
assert.equal(resolveWindowsShimToNativeExe(shimPath, "win32"), nodeExe);
const unwrapped = prepareCommandForSpawn(shimPath, ["status", "--format", "json"]);
const wrapped = prepareCommandForSpawn(shimPath, ["status", "--format", "json"], {
unwrapNativeExe: false,
});
if (process.platform === "win32") {
assert.deepEqual(unwrapped, {
command: nodeExe,
args: ["status", "--format", "json"],
shell: false,
});
assert.deepEqual(wrapped, {
command: buildWindowsShellCommandLine(shimPath, ["status", "--format", "json"]),
args: [],
shell: true,
});
} else {
assert.equal(unwrapped.shell, false);
assert.equal(wrapped.shell, false);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("prepareCommandForSpawn resolves Windows cmd shim to native exe with shell:false", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-spawn-native-"));
try {
const shimPath = path.join(tmp, "claude.cmd");
const nativeExe = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe");
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
fs.writeFileSync(nativeExe, "", "utf8");
fs.writeFileSync(
shimPath,
'@ECHO off\r\n"%~dp0\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*\r\n',
"utf8",
);
const result = prepareCommandForSpawn(shimPath, ["--version"]);
if (process.platform === "win32") {
assert.deepEqual(result, {
command: nativeExe,
args: ["--version"],
shell: false,
});
} else {
// On non-Windows, resolveWindowsShimToNativeExe is skipped; verify win32 behavior explicitly.
assert.equal(resolveWindowsShimToNativeExe(shimPath, "win32"), nativeExe);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
function writeCodexWin32NativeLayout(globalPrefix, arch = process.arch === "arm64" ? "arm64" : "x64") {
const triple = arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc";
const platformPackage = arch === "arm64" ? "@openai/codex-win32-arm64" : "@openai/codex-win32-x64";
const nativeExe = path.join(
globalPrefix,
"node_modules",
platformPackage,
"vendor",
triple,
"bin",
"codex.exe",
);
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
fs.writeFileSync(nativeExe, "", "utf8");
return nativeExe;
}
test("resolveCodexExecutableForSdk maps Windows npm cmd shim to native codex.exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-shim-"));
try {
const shimPath = path.join(tmp, "codex.cmd");
const nativeExe = writeCodexWin32NativeLayout(tmp);
fs.writeFileSync(
shimPath,
'@ECHO off\r\nnode "%~dp0\\node_modules\\@openai\\codex\\bin\\codex.js" %*\r\n',
"utf8",
);
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodexExecutableForSdk maps Windows local npm bin shim to native codex.exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-local-shim-"));
try {
const shimPath = path.join(tmp, "node_modules", ".bin", "codex.cmd");
const nativeExe = writeCodexWin32NativeLayout(tmp);
fs.mkdirSync(path.dirname(shimPath), { recursive: true });
fs.writeFileSync(
shimPath,
'@ECHO off\r\nnode "%~dp0\\..\\@openai\\codex\\bin\\codex.js" %*\r\n',
"utf8",
);
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodexExecutableForSdk leaves non-Windows Codex paths unchanged", () => {
assert.equal(
resolveCodexExecutableForSdk("/usr/local/bin/codex", "darwin"),
"/usr/local/bin/codex",
);
});
test("resolveCodexExecutableForSdk returns null for Windows cmd shim when native codex.exe is missing", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-missing-native-"));
try {
const shimPath = path.join(tmp, "codex.cmd");
fs.writeFileSync(
shimPath,
'@ECHO off\r\nnode "%~dp0\\node_modules\\@openai\\codex\\bin\\codex.js" %*\r\n',
"utf8",
);
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), null);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodexExecutableForSdk maps Windows nvmd bin shim to native codex.exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-nvmd-shim-"));
try {
const nvmdHome = path.join(tmp, ".nvmd");
const binDir = path.join(nvmdHome, "bin");
const versionRoot = path.join(nvmdHome, "versions", "22.14.0");
fs.mkdirSync(binDir, { recursive: true });
fs.writeFileSync(path.join(nvmdHome, "default"), "22.14.0\n", "utf8");
fs.writeFileSync(
path.join(nvmdHome, "packages.json"),
JSON.stringify({ codex: ["22.14.0"] }),
"utf8",
);
// nvmd Windows package shims are copies of npm.cmd / nvmd.exe, not npm's
// @openai/codex launcher. The real install lives under versions/<ver>/.
const shimPath = path.join(binDir, "codex.cmd");
fs.writeFileSync(shimPath, '@echo off\r\n"%~dpn0.exe" %*\r\n', "utf8");
fs.writeFileSync(path.join(binDir, "codex.exe"), "", "utf8");
fs.writeFileSync(path.join(binDir, "nvmd.exe"), "", "utf8");
const nativeExe = writeCodexWin32NativeLayout(versionRoot);
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodexExecutableForSdk maps Windows nvmd.exe package shim to native codex.exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-nvmd-exe-"));
try {
const nvmdHome = path.join(tmp, ".nvmd");
const binDir = path.join(nvmdHome, "bin");
const versionRoot = path.join(nvmdHome, "versions", "20.18.0");
fs.mkdirSync(binDir, { recursive: true });
fs.writeFileSync(path.join(nvmdHome, "default"), "20.18.0\n", "utf8");
const shimPath = path.join(binDir, "codex.exe");
fs.writeFileSync(shimPath, "", "utf8");
fs.writeFileSync(path.join(binDir, "nvmd.exe"), "", "utf8");
const nativeExe = writeCodexWin32NativeLayout(versionRoot);
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodexExecutableForSdk maps Windows PowerShell shim to native codex.exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-ps1-shim-"));
try {
const shimPath = path.join(tmp, "codex.ps1");
const nativeExe = writeCodexWin32NativeLayout(tmp);
fs.writeFileSync(
shimPath,
'& "$basedir/node_modules/@openai/codex/bin/codex.js" $args\r\n',
"utf8",
);
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodexExecutableForSdk maps codex.js entry to native codex.exe", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-js-entry-"));
try {
const codexJs = path.join(tmp, "node_modules", "@openai", "codex", "bin", "codex.js");
const nativeExe = writeCodexWin32NativeLayout(tmp);
fs.mkdirSync(path.dirname(codexJs), { recursive: true });
fs.writeFileSync(codexJs, "", "utf8");
assert.equal(resolveCodexExecutableForSdk(codexJs, "win32"), nativeExe);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("addCodexExecutableEnvForSdk prepends bundled Codex path dir on Windows", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-env-path-"));
try {
const nativeExe = writeCodexWin32NativeLayout(tmp);
const pathDir = path.join(path.dirname(path.dirname(nativeExe)), "codex-path");
fs.mkdirSync(pathDir, { recursive: true });
const env = addCodexExecutableEnvForSdk({ Path: "C:\\Windows\\System32" }, nativeExe, "win32");
assert.equal(env.Path, `${pathDir};C:\\Windows\\System32`);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
function writeCodebuddyWin32BinLayout(dir) {
const binJs = path.join(dir, "node_modules", "@tencent-ai", "codebuddy-code", "bin", "codebuddy");
fs.mkdirSync(path.dirname(binJs), { recursive: true });
fs.writeFileSync(binJs, "#!/usr/bin/env node\n", "utf8");
return binJs;
}
test("resolveCodebuddyExecutableForSdk leaves non-Windows CodeBuddy paths unchanged", () => {
assert.equal(
resolveCodebuddyExecutableForSdk("/usr/local/bin/codebuddy", "darwin"),
"/usr/local/bin/codebuddy",
);
});
test("resolveCodebuddyExecutableForSdk maps Windows npm cmd shim to package bin/codebuddy", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codebuddy-shim-"));
try {
const shimPath = path.join(tmp, "codebuddy.cmd");
const binJs = writeCodebuddyWin32BinLayout(tmp);
fs.writeFileSync(
shimPath,
'@ECHO off\r\nnode "%~dp0\\node_modules\\@tencent-ai\\codebuddy-code\\bin\\codebuddy" %*\r\n',
"utf8",
);
assert.equal(resolveCodebuddyExecutableForSdk(shimPath, "win32"), binJs);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodebuddyExecutableForSdk maps extensionless Windows shim to package bin/codebuddy", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codebuddy-noext-"));
try {
const shimPath = path.join(tmp, "codebuddy");
const binJs = writeCodebuddyWin32BinLayout(tmp);
fs.writeFileSync(shimPath, "#!/bin/sh\n", "utf8");
assert.equal(resolveCodebuddyExecutableForSdk(shimPath, "win32"), binJs);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodebuddyExecutableForSdk returns null for Windows cmd shim when package JS is missing", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codebuddy-missing-"));
try {
const shimPath = path.join(tmp, "codebuddy.cmd");
fs.writeFileSync(shimPath, "@ECHO off\r\nnode foo %*\r\n", "utf8");
assert.equal(resolveCodebuddyExecutableForSdk(shimPath, "win32"), null);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test("resolveCodebuddyExecutableForSdk passes through a native exe path", () => {
assert.equal(
resolveCodebuddyExecutableForSdk("C:\\tools\\codebuddy.exe", "win32"),
"C:\\tools\\codebuddy.exe",
);
});
test("parseRegQueryPath extracts the Path value from reg query output", () => {
const out = parseRegQueryPath(
"\r\nHKEY_CURRENT_USER\\Environment\r\n Path REG_EXPAND_SZ C:\\Users\\me\\AppData\\Roaming\\npm;C:\\tools\r\n",
);
assert.equal(out, "C:\\Users\\me\\AppData\\Roaming\\npm;C:\\tools");
});
test("parseRegQueryPath handles REG_SZ and missing value", () => {
assert.equal(parseRegQueryPath(" Path REG_SZ C:\\bin"), "C:\\bin");
assert.equal(parseRegQueryPath("HKEY_CURRENT_USER\\Environment\r\n Temp REG_SZ C:\\Temp"), "");
});
test("expandWindowsEnvRefs expands %VAR% case-insensitively", () => {
assert.equal(
expandWindowsEnvRefs("%AppData%\\npm;%Other%", { APPDATA: "C:\\Users\\me\\AppData\\Roaming" }),
"C:\\Users\\me\\AppData\\Roaming\\npm;%Other%",
);
});
test("mergeWindowsPath dedupes case-insensitively and trims trailing slashes", () => {
const out = mergeWindowsPath(
"C:\\Windows\\System32;C:\\tools\\",
"c:\\windows\\system32;C:\\tools;C:\\new",
);
assert.equal(out, "C:\\Windows\\System32;C:\\tools\\;C:\\new");
});
test("mergeWindowsPath keeps refreshed Windows PATH entries ahead of stale process entries", () => {
const out = mergeWindowsPath(
"C:\\new-codebuddy;C:\\Windows\\System32",
"C:\\Users\\me\\AppData\\Roaming\\npm",
"C:\\old-codebuddy;C:\\Windows\\System32",
);
assert.equal(out, "C:\\new-codebuddy;C:\\Windows\\System32;C:\\Users\\me\\AppData\\Roaming\\npm;C:\\old-codebuddy");
});
test("readWindowsRegistryPath merges HKCU and HKLM and expands refs", async () => {
const exec = async (cmd, args) => {
assert.equal(cmd, "reg");
const hive = args[1];
if (hive === "HKCU\\Environment") {
return { stdout: " Path REG_EXPAND_SZ %APPDATA%\\npm\r\n" };
}
return { stdout: " Path REG_EXPAND_SZ C:\\Windows\\System32\r\n" };
};
const out = await readWindowsRegistryPath({ exec, env: { APPDATA: "C:\\Roaming" } });
assert.equal(out, "C:\\Roaming\\npm;C:\\Windows\\System32");
});
test("readWindowsRegistryPath tolerates a failing hive query", async () => {
const exec = async (cmd, args) => {
if (args[1] === "HKCU\\Environment") throw new Error("ERROR: cannot read");
return { stdout: " Path REG_SZ C:\\tools\r\n" };
};
const out = await readWindowsRegistryPath({ exec, env: {} });
assert.equal(out, "C:\\tools");
});
test("tracks PowerShell idle prompt after SSH output", () => {
const session = {};
const prompt = trackSessionIdlePrompt(session, "Last login...\r\nPS C:\\Windows\\System32>");
assert.equal(prompt, "PS C:\\Windows\\System32>");
assert.equal(session.lastIdlePrompt, "PS C:\\Windows\\System32>");
assert.equal(typeof session.lastIdlePromptAt, "number");
});
test("getFreshIdlePrompt returns the cached prompt when the live tail still ends with it", () => {
const session = {
lastIdlePrompt: "PS C:\\Users\\alice>",
_promptTrackTail: "Microsoft Windows...\r\nPS C:\\Users\\alice>",
};
assert.equal(getFreshIdlePrompt(session), "PS C:\\Users\\alice>");
});
test("getFreshIdlePrompt drops a stale prompt when the live tail has moved on (e.g. exited PowerShell)", () => {
// Simulates: SSH session entered PowerShell, captured `PS C:\>`, then
// user `exit`-ed back into a shell with a custom prompt the regex
// doesn't recognize. lastIdlePrompt is still the old PS line, but the
// visible tail now shows the new prompt — we must NOT keep handing
// the stale value to resolveEffectiveShellKind.
const session = {
lastIdlePrompt: "PS C:\\Users\\alice>",
_promptTrackTail: "PS C:\\Users\\alice>\r\nexit\r\nlogout\r\n ",
};
assert.equal(getFreshIdlePrompt(session), "");
});
test("getFreshIdlePrompt drops a stale prompt when the live tail switched to cmd.exe", () => {
const session = {
lastIdlePrompt: "PS C:\\Users\\alice>",
_promptTrackTail: "PS C:\\Users\\alice>\r\ncmd\r\nMicrosoft Windows...\r\nC:\\Users\\alice>",
};
assert.equal(getFreshIdlePrompt(session), "");
});
test("getFreshIdlePrompt tolerates ANSI colour codes that wrap the prompt in either side", () => {
const session = {
lastIdlePrompt: "PS C:\\Users\\alice>",
_promptTrackTail: "stuff\r\nPS C:\\Users\\alice>",
};
assert.equal(getFreshIdlePrompt(session), "PS C:\\Users\\alice>");
});
test("getFreshIdlePrompt returns empty string when the session has no cached prompt or tail", () => {
assert.equal(getFreshIdlePrompt(null), "");
assert.equal(getFreshIdlePrompt(undefined), "");
assert.equal(getFreshIdlePrompt({}), "");
assert.equal(getFreshIdlePrompt({ lastIdlePrompt: "PS C:\\>" }), "");
assert.equal(
getFreshIdlePrompt({ lastIdlePrompt: "", _promptTrackTail: "anything" }),
"",
);
});
test("getFreshIdlePrompt and trackSessionIdlePrompt round-trip through a real PTY-like flow", () => {
// (1) Remote PowerShell prompt arrives — lastIdlePrompt is captured.
const session = {};
trackSessionIdlePrompt(session, "Microsoft Windows...\r\nPS C:\\Users\\alice>");
assert.equal(getFreshIdlePrompt(session), "PS C:\\Users\\alice>");
// (2) User runs `exit` and the shell now shows an unrecognized prompt.
// trackSessionIdlePrompt does not update lastIdlePrompt (the new shape
// doesn't match POSIX or PowerShell regexes), so the cache is stale.
trackSessionIdlePrompt(session, "\r\nexit\r\nlogout\r\n ");
assert.equal(session.lastIdlePrompt, "PS C:\\Users\\alice>"); // unchanged
// The freshness check rescues us: the visible tail no longer ends
// with the cached PS line, so downstream wrapper selection sees "".
assert.equal(getFreshIdlePrompt(session), "");
});
test("looksLikeIdleAutoLogout detects the bash TMOUT banner at the tail", () => {
// bash prints this immediately before a TMOUT auto-logout exit. The exit
// itself is a clean shell exit (code 0, no signal), so the banner is the
// only reliable discriminator from a user-typed `exit` (#1062 / #977).
assert.equal(
looksLikeIdleAutoLogout("user@host:~$ \x07timed out waiting for input: auto-logout\r\n"),
true,
);
});
test("looksLikeIdleAutoLogout detects the csh/tcsh auto-logout banner", () => {
assert.equal(looksLikeIdleAutoLogout("\r\nauto-logout\r\n"), true);
});
test("looksLikeIdleAutoLogout sees through ANSI escapes around the banner", () => {
assert.equal(
looksLikeIdleAutoLogout("\x1b[0m\x1b[33mtimed out waiting for input: auto-logout\x1b[0m\r\n"),
true,
);
});
test("looksLikeIdleAutoLogout ignores a plain (non-timeout) logout", () => {
// A normal login-shell exit prints "logout" — without the "auto-" prefix —
// and must still auto-close the tab.
assert.equal(looksLikeIdleAutoLogout("user@host:~$ logout\r\n"), false);
});
test("looksLikeIdleAutoLogout ignores the banner when it is not at the tail", () => {
// "auto-logout" scrolled past long ago; the user then ran more commands and
// exited normally. Only the tail end is inspected, so this is not a timeout.
const tail = "auto-logout\n" + "x".repeat(400) + "\nuser@host:~$ logout\r\n";
assert.equal(looksLikeIdleAutoLogout(tail), false);
});
test("looksLikeIdleAutoLogout ignores auto-logout in command output before an intentional exit", () => {
// Investigating TMOUT: the user greps the profile (output mentions
// "auto-logout"), reads it, then exits on purpose. The banner is not the
// final line, so the tab must still auto-close. Guards against matching an
// unanchored substring anywhere in the recent output.
const tail =
"root@h:~# grep -i auto-logout /etc/profile\r\n" +
"# bash TMOUT auto-logout setting\r\nTMOUT=300\r\n" +
"root@h:~# exit\r\nlogout\r\n";
assert.equal(looksLikeIdleAutoLogout(tail), false);
});
test("looksLikeIdleAutoLogout matches the real-server banner shape (prompt + banner on one line)", () => {
// The banner can share a line with the trailing prompt after ANSI/control
// bytes are stripped (observed over real SSH); anchoring on the line end
// must still match.
const tail =
"\x1b]0;root@VM:~\x07root@VM:~# \x1b[?2004l\x07timed out waiting for input: auto-logout\n";
assert.equal(looksLikeIdleAutoLogout(tail), true);
});
test("looksLikeIdleAutoLogout returns false for empty / non-string input", () => {
assert.equal(looksLikeIdleAutoLogout(""), false);
assert.equal(looksLikeIdleAutoLogout(undefined), false);
assert.equal(looksLikeIdleAutoLogout(null), false);
});
function withExecPath(fakePath, fn) {
const original = process.execPath;
Object.defineProperty(process, "execPath", { value: fakePath, configurable: true, writable: true });
try {
return fn();
} finally {
Object.defineProperty(process, "execPath", { value: original, configurable: true, writable: true });
}
}