mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-21 08:42:39 +00:00
* fix(daemon): report the local daemon's server id when a password is set Since #4575, `paseo daemon status` only learned the server id from an authenticated connection. The desktop app runs that command without the daemon password, so a password-protected desktop daemon reported no server id, the app treated it as remote, and hid Open in editor. The worker now announces its server id with readiness and the supervisor publishes it in the instance record next to the listen address, so status reports the running daemon's identity from local state. Closes #5300 * fix(cli): omit serverId from status until a daemon publishes one * chore: rerun CI after macOS file observation flake --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { expect, test } from "vitest";
|
|
|
|
async function runWorker(worker: string, publicationDelay = 0) {
|
|
const home = await mkdtemp(path.join(tmpdir(), "paseo-readiness-"));
|
|
const workerPath = path.join(home, "worker.mjs");
|
|
const runnerPath = path.join(home, "runner.mjs");
|
|
const eventsPath = path.join(home, "events.jsonl");
|
|
const supervisor = new URL("./supervisor.ts", import.meta.url).href;
|
|
await writeFile(workerPath, worker);
|
|
await writeFile(
|
|
runnerPath,
|
|
`
|
|
import { appendFile } from "node:fs/promises";
|
|
import { runSupervisor } from ${JSON.stringify(supervisor)};
|
|
const record = (event) => appendFile(${JSON.stringify(eventsPath)}, JSON.stringify(event) + "\\n");
|
|
runSupervisor({
|
|
name: "ReadinessTest", startupMessage: "starting", restartOnCrash: true,
|
|
resolveWorkerEntry: () => ${JSON.stringify(workerPath)}, workerArgs: [], workerExecArgv: [],
|
|
onWorkerReady: async ({ listen }) => {
|
|
await new Promise(resolve => setTimeout(resolve, ${publicationDelay}));
|
|
await record(listen);
|
|
},
|
|
onWorkerExit: () => record(null),
|
|
});
|
|
`,
|
|
);
|
|
const env = Object.fromEntries(
|
|
Object.entries(process.env).filter(([key]) => !key.startsWith("PASEO_")),
|
|
);
|
|
const child = spawn(process.execPath, ["--import", "tsx", runnerPath], {
|
|
cwd: fileURLToPath(new URL("../../../", import.meta.url)),
|
|
env: { ...env, HOME: home, PASEO_HOME: home },
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let output = "";
|
|
child.stdout.on("data", (chunk) => {
|
|
output += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
output += chunk;
|
|
});
|
|
try {
|
|
const code = await new Promise<number | null>((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
child.kill("SIGKILL");
|
|
reject(new Error(output));
|
|
}, 5_000);
|
|
child.once("error", reject);
|
|
child.once("close", (exitCode) => {
|
|
clearTimeout(timer);
|
|
resolve(exitCode);
|
|
});
|
|
});
|
|
const events = await readFile(eventsPath, "utf8").catch(() => "");
|
|
return {
|
|
code,
|
|
output,
|
|
events: events
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => JSON.parse(line)),
|
|
};
|
|
} finally {
|
|
await rm(home, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test("a worker that exits before ready fails the supervisor without respawning", async () => {
|
|
const result = await runWorker(`
|
|
import { existsSync, writeFileSync } from "node:fs";
|
|
const marker = process.argv[1] + ".started";
|
|
if (existsSync(marker)) process.exit(0);
|
|
writeFileSync(marker, "started");
|
|
process.exit(1);
|
|
`);
|
|
expect(result.code).toBe(1);
|
|
expect(result.output).not.toContain("Restarting worker");
|
|
});
|
|
|
|
test("a replacement worker must independently reach ready", async () => {
|
|
const result = await runWorker(`
|
|
import { existsSync, writeFileSync } from "node:fs";
|
|
const marker = process.argv[1] + ".started";
|
|
if (existsSync(marker)) process.exit(0);
|
|
writeFileSync(marker, "started");
|
|
process.on("message", message => {
|
|
if (message.type === "paseo:graceful-shutdown") process.exit(0);
|
|
});
|
|
process.send({ type: "paseo:ready", listen: "test-endpoint", serverId: "srv_test" });
|
|
process.send({ type: "paseo:restart" });
|
|
`);
|
|
expect(result.code).toBe(1);
|
|
expect(result.events, result.output).toEqual(["test-endpoint", null, null]);
|
|
});
|
|
|
|
test("exit clears publication after an in-flight ready write", async () => {
|
|
const result = await runWorker(
|
|
`
|
|
process.send({ type: "paseo:ready", listen: "test-endpoint", serverId: "srv_test" }, () => process.exit(0));
|
|
`,
|
|
100,
|
|
);
|
|
expect(result.code).toBe(0);
|
|
expect(result.events.at(-1)).toBeNull();
|
|
});
|
|
|
|
test("requested shutdown before first readiness exits successfully", async () => {
|
|
const result = await runWorker(`
|
|
process.on("message", message => {
|
|
if (message.type === "paseo:graceful-shutdown") process.exit(0);
|
|
});
|
|
process.send({ type: "paseo:shutdown", reason: "cancelled_start" });
|
|
`);
|
|
expect(result.code).toBe(0);
|
|
expect(result.events).toEqual([null]);
|
|
});
|