mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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 };
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user