chore(lint): clear cli warnings to zero

Refactor polling loops into recursive helpers to satisfy
no-await-in-loop, fix WebSocket named import, and correct
an absolute-path import in tests/tmp.
This commit is contained in:
Mohamed Boudra
2026-04-24 02:08:08 +07:00
parent 9865eb0a34
commit f9a1ad21b5
17 changed files with 263 additions and 187 deletions

View File

@@ -287,13 +287,13 @@ function signalProcessGroupSafely(pid: number, signal: NodeJS.Signals): boolean
async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!isProcessRunning(pid)) {
return true;
}
async function poll(): Promise<boolean> {
if (!isProcessRunning(pid)) return true;
if (Date.now() >= deadline) return !isProcessRunning(pid);
await sleep(PID_POLL_INTERVAL_MS);
return poll();
}
return !isProcessRunning(pid);
return poll();
}
type LifecycleShutdownAttempt = { requested: true } | { requested: false; reason: string };

View File

@@ -76,25 +76,27 @@ async function resolveNodePathFromPidWindows(pid: number): Promise<NodePathFromP
];
const errors: string[] = [];
for (const probe of probes) {
async function tryProbe(index: number): Promise<NodePathFromPidResult> {
if (index >= probes.length) {
return {
nodePath: null,
error: errors.join("; ") || "could not resolve executable path from PID",
};
}
const probe = probes[index] as (typeof probes)[number];
const result = await runProcessProbe(probe.command, probe.args);
if (result.resolved) {
const resolved = probe.parseValue ? probe.parseValue(result.resolved) : result.resolved;
if (resolved) {
return { nodePath: resolved };
}
if (resolved) return { nodePath: resolved };
errors.push(`${probe.label} returned no executable path`);
continue;
}
if (result.error) {
} else if (result.error) {
errors.push(`${probe.label}: ${result.error}`);
}
return tryProbe(index + 1);
}
return {
nodePath: null,
error: errors.join("; ") || "could not resolve executable path from PID",
};
return tryProbe(0);
}
export async function resolveNodePathFromPid(pid: number): Promise<NodePathFromPidResult> {

View File

@@ -44,7 +44,7 @@ export async function runLoopLogsCommand(
): Promise<void> {
const host = getDaemonHost({ host: options.host as string | undefined });
const pollInterval = parsePollInterval(options.pollInterval ?? "1000");
let client;
let client: LoopDaemonClient;
try {
client = (await connectToDaemon({
host: options.host as string | undefined,
@@ -56,23 +56,24 @@ export async function runLoopLogsCommand(
process.exit(1);
}
let cursor = 0;
try {
for (;;) {
const payload = await client.loopLogs(id, cursor);
if (payload.error || !payload.loop) {
throw new Error(payload.error ?? `Loop not found: ${id}`);
}
cursor = payload.nextCursor;
for (const entry of payload.entries) {
console.log(renderLogEntry(entry));
}
if (payload.loop.status !== "running") {
await client.close();
return;
}
await new Promise((resolve) => setTimeout(resolve, pollInterval));
async function streamLogs(cursor: number): Promise<void> {
const payload = await client.loopLogs(id, cursor);
if (payload.error || !payload.loop) {
throw new Error(payload.error ?? `Loop not found: ${id}`);
}
for (const entry of payload.entries) {
console.log(renderLogEntry(entry));
}
if (payload.loop.status !== "running") {
await client.close();
return;
}
await new Promise((resolve) => setTimeout(resolve, pollInterval));
return streamLogs(payload.nextCursor);
}
try {
await streamLogs(0);
} catch (error) {
await client.close().catch(() => {});
const message = error instanceof Error ? error.message : String(error);

View File

@@ -199,60 +199,85 @@ function renderProgressLine(progress: DownloadProgress): string {
return `Downloading speech model${modelSuffix}: ${progress.pct}%`;
}
type ProbeResult = { kind: "ready"; listen: string; host: string | null } | { kind: "pending" };
async function probeDaemonReady(home: string): Promise<ProbeResult> {
const state = resolveLocalDaemonState({ home });
const host = resolveTcpHostFromListen(state.listen);
if (state.running && host) {
const client = await tryConnectToDaemon({ host, timeout: 1200 });
if (client) {
try {
await client.fetchAgents();
return { kind: "ready", listen: state.listen, host };
} catch {
// Daemon process is alive but not API-ready yet.
} finally {
await client.close().catch(() => {});
}
}
} else if (state.running && !host) {
return { kind: "ready", listen: state.listen, host: null };
}
return { kind: "pending" };
}
interface ProgressState {
lastStatus: string;
lastPrintedAt: number;
}
function announceProgress(
home: string,
state: ProgressState,
onStatus: ((message: string) => void) | undefined,
): ProgressState {
const progress = parseDownloadProgress(tailDaemonLog(home, 120) ?? "");
const progressLine = progress ? renderProgressLine(progress) : null;
const statusMessage = progressLine ?? "Waiting for daemon to become ready...";
if (statusMessage !== state.lastStatus) {
onStatus?.(statusMessage);
return { lastStatus: statusMessage, lastPrintedAt: Date.now() };
}
if (!onStatus && Date.now() - state.lastPrintedAt >= 3000) {
console.log(statusMessage);
return { lastStatus: state.lastStatus, lastPrintedAt: Date.now() };
}
return state;
}
async function waitForDaemonReady(args: {
home: string;
timeoutMs: number;
onStatus?: (message: string) => void;
}): Promise<{ listen: string; host: string | null }> {
const deadline = Date.now() + args.timeoutMs;
let lastStatus = "";
let lastPrintedAt = 0;
while (Date.now() < deadline) {
const state = resolveLocalDaemonState({ home: args.home });
const host = resolveTcpHostFromListen(state.listen);
if (state.running && host) {
const client = await tryConnectToDaemon({ host, timeout: 1200 });
if (client) {
try {
await client.fetchAgents();
return { listen: state.listen, host };
} catch {
// Daemon process is alive but not API-ready yet.
} finally {
await client.close().catch(() => {});
}
}
} else if (state.running && !host) {
return { listen: state.listen, host: null };
async function poll(state: ProgressState): Promise<{ listen: string; host: string | null }> {
const probe = await probeDaemonReady(args.home);
if (probe.kind === "ready") {
return { listen: probe.listen, host: probe.host };
}
const progress = parseDownloadProgress(tailDaemonLog(args.home, 120) ?? "");
const progressLine = progress ? renderProgressLine(progress) : null;
const statusMessage = progressLine ?? "Waiting for daemon to become ready...";
if (statusMessage !== lastStatus) {
args.onStatus?.(statusMessage);
lastStatus = statusMessage;
lastPrintedAt = Date.now();
} else if (!args.onStatus && Date.now() - lastPrintedAt >= 3000) {
console.log(statusMessage);
lastPrintedAt = Date.now();
const nextState = announceProgress(args.home, state, args.onStatus);
if (Date.now() >= deadline) {
const recentLogs = tailDaemonLog(args.home, 60);
throw new Error(
[
`Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`,
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
]
.filter(Boolean)
.join("\n\n"),
);
}
await sleep(200);
return poll(nextState);
}
const recentLogs = tailDaemonLog(args.home, 60);
throw new Error(
[
`Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`,
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
]
.filter(Boolean)
.join("\n\n"),
);
return poll({ lastStatus: "", lastPrintedAt: 0 });
}
function printNextSteps(pairingUrl: string | null, paseoHome: string, richUi: boolean): void {

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync } from "node:fs";
import { loadConfig, resolvePaseoHome, DaemonClient } from "@getpaseo/server";
import path from "node:path";
import WebSocket from "ws";
import { WebSocket } from "ws";
import { getOrCreateCliClientId } from "./client-id.js";
import { resolveCliVersion } from "../version.js";
@@ -195,45 +195,54 @@ function createNodeWebSocketFactory() {
* Create and connect a daemon client
* Returns the connected client or throws if connection fails
*/
async function tryConnectHost(
host: string,
clientId: string,
timeout: number,
nodeWebSocketFactory: ReturnType<typeof createNodeWebSocketFactory>,
): Promise<{ client: DaemonClient } | { error: unknown }> {
const target = resolveDaemonTarget(host);
const client = new DaemonClient({
url: target.url,
clientId,
clientType: "cli",
appVersion: resolveCliVersion(),
connectTimeoutMs: timeout,
webSocketFactory: (url: string, config?: { headers?: Record<string, string> }) =>
nodeWebSocketFactory(url, {
headers: config?.headers,
...(target.type === "ipc" ? { socketPath: target.socketPath } : {}),
}),
reconnect: { enabled: false },
} as unknown as ConstructorParameters<typeof DaemonClient>[0]);
try {
await client.connect();
return { client };
} catch (error) {
await client.close().catch(() => {});
return { error };
}
}
export async function connectToDaemon(options?: ConnectOptions): Promise<DaemonClient> {
const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
const clientId = await getOrCreateCliClientId();
const hosts = resolveDaemonHostCandidates(options);
const nodeWebSocketFactory = createNodeWebSocketFactory();
let lastError: unknown = null;
for (const host of hosts) {
const target = resolveDaemonTarget(host);
const client = new DaemonClient({
url: target.url,
clientId,
clientType: "cli",
appVersion: resolveCliVersion(),
connectTimeoutMs: timeout,
webSocketFactory: (url: string, config?: { headers?: Record<string, string> }) =>
nodeWebSocketFactory(url, {
headers: config?.headers,
...(target.type === "ipc" ? { socketPath: target.socketPath } : {}),
}),
reconnect: { enabled: false },
} as unknown as ConstructorParameters<typeof DaemonClient>[0]);
const connectPromise = client.connect();
try {
await connectPromise;
return client;
} catch (err) {
lastError = err;
await client.close().catch(() => {});
async function tryNext(index: number, lastError: unknown): Promise<DaemonClient> {
if (index >= hosts.length) {
if (lastError instanceof Error) throw lastError;
throw new Error(`Unable to connect to Paseo daemon via ${hosts.join(", ")}`);
}
const host = hosts[index] as string;
const result = await tryConnectHost(host, clientId, timeout, nodeWebSocketFactory);
if ("client" in result) return result.client;
return tryNext(index + 1, result.error);
}
if (lastError instanceof Error) {
throw lastError;
}
throw new Error(`Unable to connect to Paseo daemon via ${hosts.join(", ")}`);
return tryNext(0, null);
}
/**

View File

@@ -71,7 +71,7 @@ const ctx = await createE2ETestContext({ timeout: 120000 });
async function runProviderModelsJson(provider: string): Promise<ProviderModel[]> {
const transientNeedles = ["transport closed", "timed out", "timeout", "socket", "econn"];
for (let attempt = 1; attempt <= 3; attempt++) {
async function attemptRun(attempt: number): Promise<ProviderModel[]> {
const result = await ctx.paseo(["provider", "models", provider, "--json"]);
if (result.exitCode === 0) {
return JSON.parse(result.stdout.trim()) as ProviderModel[];
@@ -86,9 +86,10 @@ async function runProviderModelsJson(provider: string): Promise<ProviderModel[]>
}
await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
return attemptRun(attempt + 1);
}
assert.fail(`provider models ${provider} exhausted retries`);
return attemptRun(1);
}
function assertClaudeModels(data: ProviderModel[]): void {

View File

@@ -100,14 +100,14 @@ async function waitFor(
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return;
}
async function poll(): Promise<void> {
if (await check()) return;
if (Date.now() >= deadline) throw new Error(message);
await sleep(pollIntervalMs);
return poll();
}
throw new Error(message);
return poll();
}
console.log("=== Daemon Stop (supervisor regression) ===\n");

View File

@@ -93,14 +93,14 @@ async function waitFor(
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return;
}
async function poll(): Promise<void> {
if (await check()) return;
if (Date.now() >= deadline) throw new Error(message);
await sleep(pollIntervalMs);
return poll();
}
throw new Error(message);
return poll();
}
interface ExitResult {

View File

@@ -39,13 +39,15 @@ function isProcessRunning(pid: number): boolean {
async function waitForRunning(pid: number, timeoutMs: number): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (isProcessRunning(pid)) {
return;
async function poll(): Promise<void> {
if (isProcessRunning(pid)) return;
if (Date.now() >= deadline) {
throw new Error(`Process ${pid} did not become running in time`);
}
await sleep(50);
return poll();
}
throw new Error(`Process ${pid} did not become running in time`);
return poll();
}
console.log("=== Daemon Stop Ownership Regression ===\n");

View File

@@ -101,14 +101,14 @@ async function waitFor(
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return;
}
async function poll(): Promise<void> {
if (await check()) return;
if (Date.now() >= deadline) throw new Error(message);
await sleep(pollIntervalMs);
return poll();
}
throw new Error(message);
return poll();
}
console.log("=== Daemon Restart (supervisor regression) ===\n");

View File

@@ -44,14 +44,14 @@ async function waitFor(
): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await check()) {
return;
}
async function poll(): Promise<void> {
if (await check()) return;
if (Date.now() >= deadline) throw new Error(message);
await sleep(pollIntervalMs);
return poll();
}
throw new Error(message);
return poll();
}
interface ExitResult {
@@ -75,16 +75,18 @@ function waitForProcessExit(processRef: ChildProcess, timeoutMs: number): Promis
async function canConnectToDaemon(host: string, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
async function poll(): Promise<boolean> {
const client = await tryConnectToDaemon({ host, timeout: 500 }).catch(() => null);
if (client) {
await client.close().catch(() => undefined);
return true;
}
if (Date.now() >= deadline) return false;
await sleep(pollIntervalMs);
return poll();
}
return false;
return poll();
}
async function readPidLockPid(paseoHome: string): Promise<number | null> {

View File

@@ -141,18 +141,20 @@ try {
listed.stdout,
);
let status = "running";
for (let attempt = 0; attempt < 40; attempt += 1) {
async function pollStatus(attempt: number): Promise<string> {
if (attempt >= 40) return "running";
const inspect = await ctx.paseo(["loop", "inspect", runJson.id, "--json"]);
assert.strictEqual(inspect.exitCode, 0, inspect.stderr);
const inspectJson = JSON.parse(inspect.stdout);
status = inspectJson.status;
if (status !== "running") {
assert.strictEqual(status, "succeeded", inspect.stdout);
break;
const current = inspectJson.status;
if (current !== "running") {
assert.strictEqual(current, "succeeded", inspect.stdout);
return current;
}
await sleep(250);
return pollStatus(attempt + 1);
}
const status = await pollStatus(0);
assert.strictEqual(status, "succeeded");
const logs = await ctx.paseo(["loop", "logs", runJson.id], { timeout: 15000 });

View File

@@ -130,19 +130,23 @@ async function test_wait_for_permission_request(agentId: string): Promise<void>
// Poll for permission requests with timeout
const maxWait = 60000; // 60 seconds max
const pollInterval = 1000; // 1 second
const startTime = Date.now();
const deadline = Date.now() + maxWait;
while (Date.now() - startTime < maxWait) {
async function pollPermission(): Promise<boolean> {
const result = await ctx.paseo(["permit", "ls", "--json"]);
const ourPermission = findMatchingPermission(result, agentId);
if (ourPermission) {
console.log("Permission request detected:", ourPermission);
console.log("PASS: Agent requested permission");
return;
return true;
}
if (Date.now() >= deadline) return false;
await sleep(pollInterval);
return pollPermission();
}
if (await pollPermission()) return;
// If we get here, check agent status - it might have already completed
const statusResult = await ctx.paseo(["inspect", agentId]);
console.log("Agent status:", statusResult.stdout);

View File

@@ -167,33 +167,39 @@ export async function createTempDirs(): Promise<{ paseoHome: string; workDir: st
* Wait for daemon to be ready by running `paseo agent ls`
* This connects via WebSocket and ensures the daemon is responsive
*/
async function probeDaemonReady(port: number): Promise<boolean> {
try {
const { exitCode } = await runPaseoCli(
{
port,
wsUrl: `ws://${TEST_DAEMON_HOST}:${port}`,
paseoHome: "",
workDir: "",
process: null,
isReady: false,
stop: async () => {},
},
["agent", "ls"],
);
return exitCode === 0;
} catch {
return false;
}
}
async function waitForDaemonReady(port: number, timeout = 30000): Promise<void> {
const start = Date.now();
const deadline = Date.now() + timeout;
while (Date.now() - start < timeout) {
try {
const { exitCode } = await runPaseoCli(
{
port,
wsUrl: `ws://${TEST_DAEMON_HOST}:${port}`,
paseoHome: "",
workDir: "",
process: null,
isReady: false,
stop: async () => {},
},
["agent", "ls"],
);
if (exitCode === 0) {
return; // Daemon is ready
}
} catch {
// Connection failed, keep trying
async function poll(): Promise<void> {
if (await probeDaemonReady(port)) return;
if (Date.now() >= deadline) {
throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`);
}
await sleep(100);
return poll();
}
throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`);
return poll();
}
function sleep(ms: number): Promise<void> {

View File

@@ -118,7 +118,9 @@ await runCommand("Building relay", "npm run build --workspace=@getpaseo/relay");
await runCommand("Building server", "npm run build --workspace=@getpaseo/server");
await runCommand("Building CLI", "npm run build --workspace=@getpaseo/cli");
for (const testFile of testFiles) {
type TestOutcome = { status: "passed" } | { status: "failed"; failure: Failure };
async function runSingleTest(testFile: string): Promise<TestOutcome> {
const testPath = join(__dirname, testFile);
const testName = testFile.replace(/\.test\.ts$/, "");
@@ -131,26 +133,38 @@ for (const testFile of testFiles) {
await $`PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD=${testEnvDefaults.PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD} PASEO_DICTATION_ENABLED=${testEnvDefaults.PASEO_DICTATION_ENABLED} PASEO_VOICE_MODE_ENABLED=${testEnvDefaults.PASEO_VOICE_MODE_ENABLED} npx tsx ${testPath}`.nothrow();
if (result.exitCode === 0) {
console.log(`\n✅ ${testName} PASSED`);
passed++;
} else {
console.log(`\n❌ ${testName} FAILED (exit code: ${result.exitCode})`);
if (result.stderr) {
console.log("stderr:", result.stderr);
}
failed++;
failures.push({ test: testName, error: result.stderr || `Exit code: ${result.exitCode}` });
break;
return { status: "passed" };
}
console.log(`\n❌ ${testName} FAILED (exit code: ${result.exitCode})`);
if (result.stderr) {
console.log("stderr:", result.stderr);
}
return {
status: "failed",
failure: { test: testName, error: result.stderr || `Exit code: ${result.exitCode}` },
};
} catch (e) {
const error = e instanceof Error ? e.message : String(e);
console.log(`\n❌ ${testName} FAILED`);
console.log("Error:", error);
failed++;
failures.push({ test: testName, error });
break;
return { status: "failed", failure: { test: testName, error } };
}
}
async function runRemainingTests(index: number): Promise<void> {
if (index >= testFiles.length) return;
const testFile = testFiles[index] as string;
const outcome = await runSingleTest(testFile);
if (outcome.status === "passed") {
passed++;
return runRemainingTests(index + 1);
}
failed++;
failures.push(outcome.failure);
}
await runRemainingTests(0);
// Summary
console.log("\n" + "=".repeat(50));
console.log("📊 Test Results");

View File

@@ -83,18 +83,26 @@ export async function createTempDirs(): Promise<{ paseoHome: string; workDir: st
* Wait for daemon to be ready by testing WebSocket connection
* Uses `paseo agent ls` which connects via WebSocket
*/
async function probeDaemon(port: number): Promise<boolean> {
try {
const result = await $`PASEO_HOST=localhost:${port} paseo agent ls`.nothrow();
return result.exitCode === 0;
} catch {
return false;
}
}
export async function waitForDaemon(port: number, timeout = 30000): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const result = await $`PASEO_HOST=localhost:${port} paseo agent ls`.nothrow();
if (result.exitCode === 0) return;
} catch {
// Connection failed, keep trying
const deadline = Date.now() + timeout;
async function poll(): Promise<void> {
if (await probeDaemon(port)) return;
if (Date.now() >= deadline) {
throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`);
}
await sleep(100);
return poll();
}
throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`);
return poll();
}
/**

View File

@@ -1,4 +1,4 @@
import { createE2ETestContext } from "/Users/moboudra/.paseo/worktrees/1luy0po7/beefy-parrot/packages/cli/tests/helpers/test-daemon.ts";
import { createE2ETestContext } from "../helpers/test-daemon.js";
async function main() {
const ctx = await createE2ETestContext({ timeout: 180000 });