scripts/autostart.test.mjs
Rama main · réplica de lectura del repositorio original.
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import assert from "node:assert/strict";
import test from "node:test";
import {
buildSystemdUserUnit,
buildWindowsHostCompilePowerShellCommand,
buildWindowsInstallationManifest,
buildWindowsTaskStatusPowerShellCommand,
buildWindowsTaskXml,
defaultConfigPath,
defaultWindowsPowerShellPath,
encodePowerShellCommand,
parseAutostartArgs,
parseWindowsInstallationManifest,
parseWindowsTaskStatus,
quoteWindowsArg,
windowsAutostartPaths
} from "./autostart-core.mjs";
const windowsHostSourcePath = fileURLToPath(new URL("./windows-autostart-host.cs", import.meta.url));
test("parseAutostartArgs accepts install options and resolves paths", () => {
const result = parseAutostartArgs(["install", "--config", "./config.yaml", "--env-file", "./secrets.env"]);
assert.equal(result.command, "install");
assert.equal(result.configPath.endsWith("config.yaml"), true);
assert.equal(result.envFile.endsWith("secrets.env"), true);
});
test("parseAutostartArgs rejects duplicate and unknown options", () => {
assert.throws(() => parseAutostartArgs(["install", "--config", "a", "--config", "b"]), /only once/u);
assert.throws(() => parseAutostartArgs(["install", "--wat"]), /Unknown/u);
assert.throws(() => parseAutostartArgs(["status", "--config", "a"]), /does not accept/u);
assert.throws(() => parseAutostartArgs(["restart", "--env-file", "a"]), /does not accept/u);
});
test("default paths follow Windows and XDG conventions", () => {
assert.equal(
defaultConfigPath("win32", { APPDATA: "C:\\Users\\u\\AppData\\Roaming" }, "C:\\Users\\u"),
"C:\\Users\\u\\AppData\\Roaming\\PalmTTY\\config.yaml"
);
assert.equal(defaultConfigPath("linux", { XDG_CONFIG_HOME: "/tmp/config" }, "/home/u"), "/tmp/config/palmtty/config.yaml");
assert.deepEqual(
windowsAutostartPaths({ LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local" }, "C:\\Users\\u"),
{
directory: "C:\\Users\\u\\AppData\\Local\\PalmTTY\\autostart",
host: "C:\\Users\\u\\AppData\\Local\\PalmTTY\\autostart\\palmtty-autostart-host.exe",
installation: "C:\\Users\\u\\AppData\\Local\\PalmTTY\\autostart\\installation.json",
runtime: "C:\\Users\\u\\AppData\\Local\\PalmTTY\\autostart\\runtime.json",
lastError: "C:\\Users\\u\\AppData\\Local\\PalmTTY\\autostart\\last-error.txt"
}
);
});
test("defaultWindowsPowerShellPath resolves the system Windows PowerShell", () => {
assert.equal(
defaultWindowsPowerShellPath({ SystemRoot: "C:\\Windows" }),
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
);
assert.throws(() => defaultWindowsPowerShellPath({}), /SystemRoot/u);
});
test("quoteWindowsArg preserves spaces and trailing backslashes", () => {
assert.equal(quoteWindowsArg("plain"), "plain");
assert.equal(quoteWindowsArg("C:\\Program Files\\node.exe"), '"C:\\Program Files\\node.exe"');
assert.equal(quoteWindowsArg("C:\\path with space\\"), '"C:\\path with space\\\\"');
});
test("Windows installation manifest contains paths but never secret values", () => {
const manifest = buildWindowsInstallationManifest({
nodePath: "C:\\Program Files\\nodejs\\node.exe",
agentPath: "C:\\PalmTTY\\apps\\agent\\dist\\index.js",
repoRoot: "C:\\PalmTTY",
configPath: "C:\\Users\\me\\PalmTTY config.yaml",
envFile: "C:\\Users\\me\\PalmTTY\\autostart.env"
});
assert.equal(manifest.version, 1);
assert.equal(manifest.envFile, "C:\\Users\\me\\PalmTTY\\autostart.env");
assert.doesNotMatch(JSON.stringify(manifest), /PALMTTY_ACCESS_TOKEN=/u);
assert.deepEqual(
parseWindowsInstallationManifest(JSON.stringify(manifest)),
manifest
);
assert.throws(
() => parseWindowsInstallationManifest('{"version":2}'),
/unsupported version/u
);
});
test("Windows Task Scheduler launches only the native GUI host", () => {
const xml = buildWindowsTaskXml({
userSid: "S-1-5-21-123",
hostPath: "C:\\Users\\me\\AppData\\Local\\PalmTTY\\autostart\\palmtty-autostart-host.exe",
installationPath: "C:\\Users\\me\\AppData\\Local\\PalmTTY\\autostart\\installation.json",
workingDirectory: "C:\\Users\\me\\AppData\\Local\\PalmTTY\\autostart"
});
assert.match(xml, /<LogonType>InteractiveToken<\/LogonType>/u);
assert.match(xml, /<RunLevel>LeastPrivilege<\/RunLevel>/u);
assert.match(xml, /<MultipleInstancesPolicy>IgnoreNew<\/MultipleInstancesPolicy>/u);
assert.match(xml, /<Command>C:\\Users\\me\\AppData\\Local\\PalmTTY\\autostart\\palmtty-autostart-host\.exe<\/Command>/u);
assert.match(xml, /--installation/u);
assert.match(xml, /installation\.json/u);
assert.doesNotMatch(xml, /powershell/i);
assert.doesNotMatch(xml, /node\.exe/i);
assert.doesNotMatch(xml, /apps\\agent/i);
});
test("Windows host source owns Agent lifecycle without a console", () => {
const source = readFileSync(windowsHostSourcePath, "utf8");
assert.match(source, /JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE/u);
assert.match(source, /JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK/u);
assert.match(source, /CREATE_SUSPENDED \| CREATE_NO_WINDOW/u);
assert.match(source, /AssignProcessToJobObject/u);
assert.match(source, /WaitForSingleObject/u);
assert.match(source, /DataContractJsonSerializer/u);
assert.doesNotMatch(source, /System\.Windows\.Forms/u);
});
test("Windows host compilation targets the GUI subsystem", () => {
const command = buildWindowsHostCompilePowerShellCommand({
sourcePath: "C:\\PalmTTY\\scripts\\windows-autostart-host.cs",
outputPath: "C:\\Users\\me\\AppData\\Local\\PalmTTY\\autostart\\host.tmp.exe"
});
assert.match(command, /Add-Type/u);
assert.match(command, /-OutputType WindowsApplication/u);
assert.match(command, /System\.Runtime\.Serialization\.dll/u);
});
function peSubsystem(executablePath) {
const data = readFileSync(executablePath);
const peOffset = data.readUInt32LE(0x3c);
assert.equal(data.toString("ascii", peOffset, peOffset + 4), "PE\0\0");
const optionalHeader = peOffset + 4 + 20;
const magic = data.readUInt16LE(optionalHeader);
assert.ok(magic === 0x10b || magic === 0x20b, "PE optional-header magic must be PE32/PE32+");
return data.readUInt16LE(optionalHeader + 68);
}
test("Windows native host compiles as GUI, propagates Agent exit, and preserves detached Worker lifetime", { skip: process.platform !== "win32" }, async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "palmtty autostart-"));
const hostPath = path.join(directory, "palmtty-autostart-host.exe");
const installationPath = path.join(directory, "installation.json");
const markerPath = path.join(directory, "worker-survived.txt");
const workerPath = path.join(directory, "worker.cjs");
const agentPath = path.join(directory, "agent.cjs");
const configPath = path.join(directory, "config.yaml");
try {
writeFileSync(configPath, "server:\n port: 17688\n exposure:\n mode: local\n", "utf8");
writeFileSync(
workerPath,
`const { writeFileSync } = require("node:fs");\nsetTimeout(() => { writeFileSync(${JSON.stringify(markerPath)}, "ok"); process.exit(0); }, 300);\n`,
"utf8"
);
writeFileSync(
agentPath,
`const { spawn } = require("node:child_process");\nconst child = spawn(process.execPath, [${JSON.stringify(workerPath)}], { detached: true, windowsHide: true, stdio: "ignore" });\nchild.unref();\nprocess.exit(7);\n`,
"utf8"
);
const compileCommand = buildWindowsHostCompilePowerShellCommand({
sourcePath: windowsHostSourcePath,
outputPath: hostPath
});
const compile = spawnSync(
defaultWindowsPowerShellPath(),
[
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-EncodedCommand",
encodePowerShellCommand(compileCommand)
],
// Add-Type can spend well over 30s starting the Windows compiler on
// a cold/shared GitHub runner. Keep the smoke test bounded, but do not
// mistake runner startup latency for a product failure.
{ encoding: "utf8", windowsHide: true, timeout: 90_000 }
);
assert.equal(compile.status, 0, compile.stderr || compile.stdout);
assert.equal(peSubsystem(hostPath), 2, "PE subsystem must be Windows GUI");
writeFileSync(
installationPath,
JSON.stringify(buildWindowsInstallationManifest({
nodePath: process.execPath,
agentPath,
repoRoot: directory,
configPath
})),
"utf8"
);
const result = spawnSync(
hostPath,
["--installation", installationPath],
{ encoding: "utf8", windowsHide: false, timeout: 15_000 }
);
assert.equal(result.status, 7, result.stderr || result.stdout);
const deadline = Date.now() + 5_000;
while (Date.now() < deadline) {
try {
assert.equal(readFileSync(markerPath, "utf8"), "ok");
// Marker write precedes detached worker shutdown. Windows must not
// remove the executable/cwd while that survivor is still exiting.
await new Promise((resolve) => setTimeout(resolve, 500));
return;
} catch {
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
assert.fail("detached Worker did not survive native host Job close");
} finally {
try {
rmSync(directory, { recursive: true, force: true, maxRetries: 60, retryDelay: 250 });
} catch (error) {
// Windows AV/indexing can transiently retain handles after a detached
// survivor has finished; cleanup must not mask an otherwise passing
// lifecycle assertion. Unexpected filesystem errors still fail.
if (process.platform !== "win32" ||
!["EBUSY", "EPERM", "ENOTEMPTY"].includes(error?.code)) throw error;
console.warn(`Windows deferred temp cleanup: ${error.code}`);
}
}
});
test("Windows status query executes through the system PowerShell", { skip: process.platform !== "win32" }, () => {
const command = buildWindowsTaskStatusPowerShellCommand("PalmTTY status test 9f0dd16d");
const result = spawnSync(
defaultWindowsPowerShellPath(),
[
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-EncodedCommand",
encodePowerShellCommand(command)
],
{ encoding: "utf8", windowsHide: true }
);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.deepEqual(parseWindowsTaskStatus(result.stdout), { installed: false });
});
test("Windows status query is locale-independent UTF-8 JSON", () => {
const command = buildWindowsTaskStatusPowerShellCommand();
assert.match(command, /OutputEncoding/u);
assert.match(command, /Get-ScheduledTask/u);
assert.match(command, /ConvertTo-Json -Compress/u);
assert.doesNotMatch(command, /schtasks/u);
assert.deepEqual(parseWindowsTaskStatus('{"installed":false}'), { installed: false });
assert.deepEqual(
parseWindowsTaskStatus('{"installed":true,"state":"Running","lastRunTime":"2026-09-22T12:00:00.0000000Z","nextRunTime":null,"lastTaskResult":0}'),
{
installed: true,
state: "running",
lastRunTime: "2026-09-22T12:00:00.0000000Z",
nextRunTime: null,
lastTaskResult: 0
}
);
assert.throws(() => parseWindowsTaskStatus('{"installed":true,"state":"Running","lastRunTime":null,"nextRunTime":null}'), /lastTaskResult/u);
assert.throws(() => parseWindowsTaskStatus("garbage"), /invalid status payload/u);
});
test("systemd unit preserves independent Worker lifetime", () => {
const unit = buildSystemdUserUnit({
nodePath: "/usr/bin/node",
agentPath: "/opt/PalmTTY/apps/agent/dist/index.js",
repoRoot: "/opt/PalmTTY",
configPath: "/home/me/.config/palmtty/config.yaml",
envFile: "/home/me/.config/palmtty/autostart.env"
});
assert.match(unit, /^KillMode=process$/mu);
assert.match(unit, /^Restart=on-failure$/mu);
assert.doesNotMatch(unit, /network-online\.target/u);
assert.match(unit, /--env-file/u);
assert.doesNotMatch(unit, /PALMTTY_ACCESS_TOKEN=/u);
});