Skip to content

Commit 4990592

Browse files
Copilotlpcox
andauthored
test: split Cloud Hypervisor runtime backend suites (#8024)
* Initial plan * test: split runtime backend env mapping tests * test: indent nested runtime backend suites Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent b7e426a commit 4990592

2 files changed

Lines changed: 936 additions & 862 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type { WrapperConfig } from './types';
2+
import { buildCloudHypervisorGuestEnvironment } from './cloud-hypervisor/guest-environment-builder';
3+
import { assertCloudHypervisorPreSecurityCompatibility } from './cloud-hypervisor-runtime-backend';
4+
import { assertCloudHypervisorSelection } from './cloud-hypervisor/runtime-validation';
5+
6+
function config(overrides: Partial<WrapperConfig> = {}): WrapperConfig {
7+
return {
8+
containerRuntime: 'cloud-hypervisor',
9+
cloudHypervisor: {
10+
previewEnabled: true,
11+
mountPolicy: 'workspace-only',
12+
cloudHypervisorBinary: '/opt/cloud-hypervisor',
13+
kernelPath: '/opt/kernel',
14+
rootfsPath: '/opt/rootfs',
15+
supervisorPath: '/opt/supervisor',
16+
artifactManifestPath: '/opt/manifest.json',
17+
artifactManifestBundlePath: '/opt/manifest.sigstore.jsonl',
18+
artifactReleaseTag: 'test',
19+
vcpuCount: 2,
20+
memoryMib: 512,
21+
apiTimeoutMs: 5000,
22+
},
23+
agentCommand: 'printf hello',
24+
allowedDomains: ['github.com'],
25+
workDir: '/tmp/awf',
26+
keepContainers: false,
27+
networkIsolation: true,
28+
legacySecurity: false,
29+
enableApiProxy: true,
30+
enableDind: false,
31+
enableHostAccess: false,
32+
tty: false,
33+
logLevel: 'info',
34+
buildLocal: false,
35+
skipPull: true,
36+
imageRegistry: 'registry',
37+
imageTag: 'tag',
38+
envAll: false,
39+
sslBump: false,
40+
enableDlp: false,
41+
...overrides,
42+
} as WrapperConfig;
43+
}
44+
45+
function infrastructure() {
46+
return {
47+
networkId: 'a'.repeat(64),
48+
bridgeName: 'br-aaaaaaaaaaaa',
49+
subnet: '172.30.0.0/24',
50+
gateway: '172.30.0.1',
51+
squidIp: '172.30.0.10',
52+
apiProxyIp: '172.30.0.30',
53+
topologyPeerIps: {},
54+
revalidate: jest.fn().mockResolvedValue(undefined),
55+
};
56+
}
57+
58+
describe('Cloud Hypervisor runtime backend environment and path mapping', () => {
59+
it('preserves sanitized env values without leaking real provider secrets', () => {
60+
const secret = 'sk-real-provider-' + 'secret';
61+
const environment = buildCloudHypervisorGuestEnvironment(
62+
config({
63+
openaiApiKey: secret,
64+
additionalEnv: {
65+
SAFE_SETTING: 'enabled',
66+
OPENAI_API_KEY: secret,
67+
},
68+
}),
69+
infrastructure(),
70+
);
71+
72+
expect(environment.SAFE_SETTING).toBe('enabled');
73+
expect(environment.OPENAI_API_KEY).not.toBe(secret);
74+
expect(Object.values(environment)).not.toContain(secret);
75+
expect(environment.HTTP_PROXY).toBe('http://172.30.0.10:3128');
76+
expect(environment.HOME).toBe('/workspace/.awf-home');
77+
78+
expect(() => buildCloudHypervisorGuestEnvironment(
79+
config({
80+
openaiApiKey: 'enabled',
81+
additionalEnv: { SAFE_SETTING: 'enabled' },
82+
}),
83+
infrastructure(),
84+
)).toThrow(/Refusing to pass a real provider credential/);
85+
});
86+
87+
it('sets lowercase http_proxy so BusyBox wget honors the proxy for https:// too', () => {
88+
// Regression coverage: a live-KVM connectivity investigation found
89+
// BusyBox wget reads only the lowercase "http_proxy" env var for
90+
// every protocol it supports, including https -- there is no
91+
// https_proxy check anywhere in its proxy-detection logic. Without
92+
// this (the shared container-runtime environment intentionally
93+
// omits it, for curl/Ubuntu-specific reasons that don't apply to
94+
// this guest's BusyBox wget), wget silently falls back to a direct,
95+
// unproxied connection attempt, which fails outright since guest DNS
96+
// is unconditionally blocked by network policy.
97+
const environment = buildCloudHypervisorGuestEnvironment(config(), infrastructure());
98+
99+
expect(environment.http_proxy).toBe('http://172.30.0.10:3128');
100+
});
101+
102+
it('maps only exported runner paths and keeps the guest home in the writable workspace', () => {
103+
const previousToolCache = process.env.RUNNER_TOOL_CACHE;
104+
const previousRunnerTemp = process.env.RUNNER_TEMP;
105+
try {
106+
process.env.RUNNER_TOOL_CACHE = '/opt/hostedtoolcache';
107+
process.env.RUNNER_TEMP = '/home/runner/work/_temp';
108+
const environment = buildCloudHypervisorGuestEnvironment(
109+
config(),
110+
infrastructure(),
111+
'100.64.0.2',
112+
[
113+
{ tag: 'workspace', source: '/host/workspace', target: '/workspace', mode: 'rw' },
114+
{
115+
tag: 'runner-tool-cache',
116+
source: '/opt/hostedtoolcache',
117+
target: '/opt/hostedtoolcache',
118+
mode: 'ro',
119+
},
120+
{
121+
tag: 'runner-temp-gh-aw',
122+
source: '/home/runner/work/_temp/gh-aw',
123+
target: '/home/runner/work/_temp/gh-aw',
124+
mode: 'ro',
125+
},
126+
],
127+
);
128+
expect(environment).toMatchObject({
129+
HOME: '/workspace/.awf-home',
130+
GITHUB_WORKSPACE: '/workspace',
131+
RUNNER_TOOL_CACHE: '/opt/hostedtoolcache',
132+
RUNNER_TEMP: '/home/runner/work/_temp',
133+
});
134+
} finally {
135+
if (previousToolCache === undefined) delete process.env.RUNNER_TOOL_CACHE;
136+
else process.env.RUNNER_TOOL_CACHE = previousToolCache;
137+
if (previousRunnerTemp === undefined) delete process.env.RUNNER_TEMP;
138+
else process.env.RUNNER_TEMP = previousRunnerTemp;
139+
}
140+
});
141+
142+
it('does not forward runner path variables without matching exports', () => {
143+
const previousToolCache = process.env.RUNNER_TOOL_CACHE;
144+
const previousAgentTools = process.env.AGENT_TOOLSDIRECTORY;
145+
const previousRunnerTemp = process.env.RUNNER_TEMP;
146+
try {
147+
process.env.RUNNER_TOOL_CACHE = '/opt/hostedtoolcache';
148+
process.env.AGENT_TOOLSDIRECTORY = '/opt/agent-tools';
149+
process.env.RUNNER_TEMP = '/home/runner/work/_temp';
150+
const environment = buildCloudHypervisorGuestEnvironment(
151+
config(),
152+
infrastructure(),
153+
'100.64.0.2',
154+
[{ tag: 'workspace', source: '/host/workspace', target: '/workspace', mode: 'rw' }],
155+
);
156+
157+
expect(environment.RUNNER_TOOL_CACHE).toBeUndefined();
158+
expect(environment.AGENT_TOOLSDIRECTORY).toBeUndefined();
159+
expect(environment.RUNNER_TEMP).toBeUndefined();
160+
} finally {
161+
if (previousToolCache === undefined) delete process.env.RUNNER_TOOL_CACHE;
162+
else process.env.RUNNER_TOOL_CACHE = previousToolCache;
163+
if (previousAgentTools === undefined) delete process.env.AGENT_TOOLSDIRECTORY;
164+
else process.env.AGENT_TOOLSDIRECTORY = previousAgentTools;
165+
if (previousRunnerTemp === undefined) delete process.env.RUNNER_TEMP;
166+
else process.env.RUNNER_TEMP = previousRunnerTemp;
167+
}
168+
});
169+
170+
it('rejects unsupported strict-security and topology combinations', () => {
171+
expect(() => assertCloudHypervisorPreSecurityCompatibility(
172+
config({ enableDind: true }),
173+
)).toThrow(/Docker-in-Docker/);
174+
expect(() => assertCloudHypervisorPreSecurityCompatibility(
175+
config({ enableHostAccess: true }),
176+
)).toThrow(/host access/);
177+
expect(() => assertCloudHypervisorPreSecurityCompatibility(
178+
config({ enclaves: { enabled: true } } as Partial<WrapperConfig>),
179+
)).toThrow(/DIFC proxies or enclaves/);
180+
expect(() => assertCloudHypervisorSelection(
181+
config({ containerRuntime: 'gvisor' }),
182+
)).toThrow(/require --container-runtime cloud-hypervisor/);
183+
});
184+
});

0 commit comments

Comments
 (0)