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
159 lines
5.8 KiB
TypeScript
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'));
|
|
});
|
|
});
|