fix(ci): stabilize windows mcp preflight cleanup

This commit is contained in:
777genius 2026-04-18 22:49:00 +03:00
parent cd4e9ccba8
commit d766d174e3
2 changed files with 77 additions and 11 deletions

View file

@ -205,6 +205,9 @@ const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
const RUN_TIMEOUT_MS = 300_000;
const VERIFY_TIMEOUT_MS = 15_000;
const VERIFY_POLL_MS = 500;
const MCP_PREFLIGHT_SHUTDOWN_GRACE_MS = 250;
const MCP_PREFLIGHT_SHUTDOWN_TIMEOUT_MS = 2_000;
const MCP_PREFLIGHT_SHUTDOWN_POLL_MS = 50;
const STDERR_RING_LIMIT = 64 * 1024;
const STDOUT_RING_LIMIT = 64 * 1024;
// Progress emissions fan out the latest CLI tail + assistant output to the
@ -1004,6 +1007,39 @@ async function waitForPidsToExit(
}
}
async function waitForChildProcessToExit(
child: ChildProcess | null | undefined,
timeoutMs: number
): Promise<void> {
if (!child?.pid || !isProcessAlive(child.pid)) {
return;
}
await new Promise<void>((resolve) => {
let settled = false;
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
const finish = (): void => {
if (settled) {
return;
}
settled = true;
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
child.off('close', finish);
child.off('exit', finish);
child.off('error', finish);
resolve();
};
timeoutHandle = setTimeout(finish, timeoutMs);
child.once('close', finish);
child.once('exit', finish);
child.once('error', finish);
});
}
async function tryReadRegularFileUtf8(
filePath: string,
opts: { timeoutMs: number; maxBytes: number }
@ -13588,11 +13624,26 @@ export class TeamProvisioningService {
throw new Error(this.buildAgentTeamsMcpValidationError(errorText));
} finally {
rejectAll(new Error('agent-teams MCP preflight session closed'));
if (child?.stdin && !child.stdin.destroyed) {
child.stdin.end();
if (child?.stdin && !child.stdin.destroyed && !child.stdin.writableEnded) {
const stdin = child.stdin;
await new Promise<void>((resolve) => {
try {
stdin.end(() => resolve());
} catch {
resolve();
}
});
}
if (child) {
killProcessTree(child);
if (child?.pid) {
await waitForChildProcessToExit(child, MCP_PREFLIGHT_SHUTDOWN_GRACE_MS);
if (isProcessAlive(child.pid)) {
killProcessTree(child);
await waitForPidsToExit([child.pid], {
timeoutMs: MCP_PREFLIGHT_SHUTDOWN_TIMEOUT_MS,
pollMs: MCP_PREFLIGHT_SHUTDOWN_POLL_MS,
});
await waitForChildProcessToExit(child, MCP_PREFLIGHT_SHUTDOWN_GRACE_MS);
}
}
await fs.promises.rm(fixture.claudeDir, { recursive: true, force: true }).catch(() => {});
}

View file

@ -144,6 +144,26 @@ process.stdin.on('data', (chunk) => {
return scriptPath;
}
async function removeTempRoot(dirPath: string): Promise<void> {
if (!dirPath) {
return;
}
const maxAttempts = process.platform === 'win32' ? 20 : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
await fs.promises.rm(dirPath, { recursive: true, force: true });
return;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if ((code !== 'EBUSY' && code !== 'EPERM') || attempt === maxAttempts) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
}
describe('TeamProvisioningService prepare/auth behavior', () => {
let tempRoot = '';
@ -166,13 +186,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
delete process.env.ANTHROPIC_AUTH_TOKEN;
});
afterEach(() => {
fs.rmSync(tempRoot, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 200,
});
afterEach(async () => {
await removeTempRoot(tempRoot);
});
it('does not create missing directories during prepareForProvisioning', async () => {