Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions nodejs/test/e2e/harness/CapiProxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn } from "child_process";
import { ChildProcess, spawn } from "child_process";
import { resolve } from "path";
import { createInterface } from "readline";
import { expect } from "vitest";
Expand All @@ -21,6 +21,7 @@ interface ProxyStartupInfo {
export class CapiProxy {
private proxyUrl: string | undefined;
private startupInfo: ProxyStartupInfo | undefined;
private serverProcess: ChildProcess | undefined;

/**
* Returns the URL of the running proxy. Throws if the proxy has not been started.
Expand All @@ -37,6 +38,7 @@ export class CapiProxy {
stdio: ["ignore", "pipe", "inherit"],
shell: true,
});
this.serverProcess = serverProcess;

this.startupInfo = await new Promise<ProxyStartupInfo>((resolve, reject) => {
const stdout = serverProcess.stdout!;
Expand Down Expand Up @@ -122,11 +124,34 @@ export class CapiProxy {
}

async stop(skipWritingCache?: boolean): Promise<void> {
const process = this.serverProcess;
if (!process) {
return;
}

const url = skipWritingCache
? `${this.proxyUrl}/stop?skipWritingCache=true`
: `${this.proxyUrl}/stop`;
const response = await fetch(url, { method: "POST" });
expect(response.ok).toBe(true);
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { method: "POST", signal: controller.signal });
expect(response.ok).toBe(true);
} finally {
clearTimeout(timeout);
}
} catch {
// Best-effort graceful stop; process termination below is the hard guarantee.
}

if (!(await waitForProcessExit(process, 5000))) {
process.kill();
await waitForProcessExit(process, 5000);
Comment thread
roji marked this conversation as resolved.
}
this.serverProcess = undefined;
this.proxyUrl = undefined;
this.startupInfo = undefined;
}

/**
Expand All @@ -142,6 +167,26 @@ export class CapiProxy {
});
expect(res.ok).toBe(true);
}

}

// Wait for proxy child exit so teardown doesn't leave hanging harness processes.
async function waitForProcessExit(process: ChildProcess, timeoutMs: number): Promise<boolean> {
if (process.exitCode !== null || process.signalCode !== null) {
return true;
}
return await new Promise<boolean>((resolve) => {
const onExit = () => {
clearTimeout(timer);
process.off("exit", onExit);
resolve(true);
};
const timer = setTimeout(() => {
process.off("exit", onExit);
resolve(false);
}, timeoutMs);
process.once("exit", onExit);
});
}

function tryParseStartupInfo(line: string): ProxyStartupInfo | undefined {
Expand Down
34 changes: 33 additions & 1 deletion nodejs/test/e2e/harness/sdkTestContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,19 @@
await rimraf([join(homeDir, "*"), join(workDir, "*")], { glob: true });
});

afterAll(async () => {

Check failure on line 293 in nodejs/test/e2e/harness/sdkTestContext.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (windows-latest, inprocess)

test/e2e/session.e2e.test.ts

Error: Hook timed out in 30000ms. If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout". ❯ createSdkTestContext test/e2e/harness/sdkTestContext.ts:293:5 ❯ test/e2e/session.e2e.test.ts:15:5
await copilotClient.stop();
const stopResult = await stopClientWithFallback(copilotClient, {
timeoutMs: isInProcess && process.platform === "win32" ? 10_000 : undefined,
});
if (stopResult.timedOut) {
console.warn("WARN: Copilot client stop timed out during e2e cleanup; force-stopped.");
} else if (stopResult.errors.length > 0) {
console.warn(
`WARN: Copilot client stop returned ${stopResult.errors.length} cleanup error(s): ${stopResult.errors
.map((error) => error.message)
.join("; ")}`
);
}
await openAiEndpoint.stop(anyTestFailed);
await rmDir("remove e2e test copilotHomeDir", copilotHomeDir);
await rmDir("remove e2e test homeDir", homeDir);
Expand Down Expand Up @@ -334,3 +345,24 @@
);
}
}

async function stopClientWithFallback(
client: CopilotClient,
options: { timeoutMs?: number } = {}
): Promise<{ timedOut: boolean; errors: Error[] }> {
const stopPromise = client.stop();
if (!options.timeoutMs) {
return { timedOut: false, errors: await stopPromise };
}

const timeoutPromise = new Promise<"timeout">((resolve) =>
setTimeout(() => resolve("timeout"), options.timeoutMs)
);
const result = await Promise.race([stopPromise, timeoutPromise]);
if (result === "timeout") {
stopPromise.catch(() => undefined);
await client.forceStop();
return { timedOut: true, errors: [] };
}
return { timedOut: false, errors: result };
Comment thread
roji marked this conversation as resolved.
Outdated
}
Loading