agent-ecosystem/src/main/services/runtime/agentChildProcessPreflight.test.ts
Jonas Korani 0e2be0348e
fix(team): make Windows child env setup fail open
Prepare writable Windows temp/cache env for agent child processes without blocking team launch.\n\n- fail open if cache roots cannot be created or written\n- keep hard child-process preflight out of launch path\n- cover Windows env setup and fail-open behavior
2026-05-15 22:04:01 +03:00

159 lines
5.8 KiB
TypeScript

// @vitest-environment node
import {
applyAgentChildProcessWritableEnv,
prepareAgentChildProcessWritableEnv,
} from '@main/services/runtime/agentChildProcessPreflight';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const originalPlatform = process.platform;
function setPlatform(value: string): void {
Object.defineProperty(process, 'platform', {
value,
configurable: true,
writable: true,
});
}
describe('agent child-process writable env', () => {
let tmpRoot: string;
beforeEach(() => {
setPlatform('win32');
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-child-env-'));
});
afterEach(() => {
vi.restoreAllMocks();
setPlatform(originalPlatform);
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
it('does not mutate env on non-Windows platforms', async () => {
setPlatform('darwin');
const env: NodeJS.ProcessEnv = {
TEMP: path.join(tmpRoot, 'existing-temp'),
};
const result = await prepareAgentChildProcessWritableEnv(env, { home: tmpRoot });
expect(result).toEqual({ applied: false });
expect(env).toEqual({
TEMP: path.join(tmpRoot, 'existing-temp'),
});
});
it('prepares stable writable cache and temp env for Windows agents', async () => {
const home = path.join(tmpRoot, 'home');
const env: NodeJS.ProcessEnv = {
COMSPEC: 'cmd.exe',
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
};
const result = await prepareAgentChildProcessWritableEnv(env, { home });
const expectedBase = path.join(home, 'AppData', 'Local', 'AgentStudio', 'runner-cache');
expect(result).toEqual({ applied: true, cacheBase: expectedBase });
expect(env.TEMP).toBe(path.join(expectedBase, 'tmp'));
expect(env.TMP).toBe(path.join(expectedBase, 'tmp'));
expect(env.TMPDIR).toBe(path.join(expectedBase, 'tmp'));
expect(env.npm_config_cache).toBe(path.join(expectedBase, 'npm-cache'));
expect(env.NPM_CONFIG_CACHE).toBe(path.join(expectedBase, 'npm-cache'));
expect(env.GRADLE_USER_HOME).toBe(path.join(expectedBase, 'gradle-home'));
expect(env.ANDROID_USER_HOME).toBe(path.join(expectedBase, 'android-home'));
expect(env.ANDROID_SDK_HOME).toBe(path.join(expectedBase, 'android-home'));
expect(env.npm_config_script_shell).toBe('cmd.exe');
expect(env.AGENT_STUDIO_NPM_CMD).toBe('npm.cmd');
expect(env.AGENT_STUDIO_NPX_CMD).toBe('npx.cmd');
expect(env.GRADLE_OPTS).toContain('-Djava.io.tmpdir=');
expect(env.JAVA_TOOL_OPTIONS).toContain('-Djava.io.tmpdir=');
await expect(fs.promises.access(path.join(expectedBase, 'tmp'))).resolves.toBeUndefined();
await expect(fs.promises.access(path.join(expectedBase, 'npm-cache'))).resolves.toBeUndefined();
await expect(
fs.promises.access(path.join(expectedBase, 'gradle-home'))
).resolves.toBeUndefined();
await expect(
fs.promises.access(path.join(expectedBase, 'android-home'))
).resolves.toBeUndefined();
for (const dirName of ['tmp', 'npm-cache', 'gradle-home', 'android-home']) {
const entries = await fs.promises.readdir(path.join(expectedBase, dirName));
const probeEntries = entries.filter((entry) =>
entry.startsWith('.agent-studio-write-probe-')
);
expect(probeEntries).toEqual([]);
}
});
it('fails open and leaves existing env untouched when cache dirs cannot be created', async () => {
const home = path.join(tmpRoot, 'home');
const originalTemp = path.join(tmpRoot, 'existing-temp');
const env: NodeJS.ProcessEnv = {
COMSPEC: 'cmd.exe',
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
TEMP: originalTemp,
TMP: originalTemp,
};
vi.spyOn(fs.promises, 'mkdir').mockRejectedValueOnce(new Error('EACCES'));
const result = await prepareAgentChildProcessWritableEnv(env, { home });
expect(result.applied).toBe(false);
expect(result.cacheBase).toBe(
path.join(home, 'AppData', 'Local', 'AgentStudio', 'runner-cache')
);
expect(result.warning).toContain('keeping existing temp/cache env');
expect(env).toEqual({
COMSPEC: 'cmd.exe',
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
TEMP: originalTemp,
TMP: originalTemp,
});
});
it('fails open and leaves existing env untouched when cache dirs are not writable', async () => {
const home = path.join(tmpRoot, 'home');
const originalTemp = path.join(tmpRoot, 'existing-temp');
const env: NodeJS.ProcessEnv = {
COMSPEC: 'cmd.exe',
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
TEMP: originalTemp,
TMP: originalTemp,
};
vi.spyOn(fs.promises, 'writeFile').mockRejectedValueOnce(new Error('EPERM'));
const result = await prepareAgentChildProcessWritableEnv(env, { home });
expect(result.applied).toBe(false);
expect(result.cacheBase).toBe(
path.join(home, 'AppData', 'Local', 'AgentStudio', 'runner-cache')
);
expect(result.warning).toContain('failed writable check');
expect(result.warning).toContain('EPERM');
expect(env).toEqual({
COMSPEC: 'cmd.exe',
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
TEMP: originalTemp,
TMP: originalTemp,
});
});
it('keeps the synchronous env applicator available for prepared directories', () => {
const home = path.join(tmpRoot, 'home');
const env: NodeJS.ProcessEnv = {
COMSPEC: 'cmd.exe',
LOCALAPPDATA: path.join(home, 'AppData', 'Local'),
};
applyAgentChildProcessWritableEnv(env, { home });
const expectedBase = path.join(home, 'AppData', 'Local', 'AgentStudio', 'runner-cache');
expect(env.TEMP).toBe(path.join(expectedBase, 'tmp'));
expect(env.TMP).toBe(path.join(expectedBase, 'tmp'));
expect(env.TMPDIR).toBe(path.join(expectedBase, 'tmp'));
});
});