@agent-vm/agent-vm 0.0.92 → 0.0.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/managed-image-dockerfile.d.ts +2 -1
- package/dist/build/managed-image-dockerfile.d.ts.map +1 -1
- package/dist/build/managed-image-dockerfile.js +51 -27
- package/dist/build/managed-image-dockerfile.js.map +1 -1
- package/dist/cli/agent-vm-cli-support.d.ts +4 -1
- package/dist/cli/agent-vm-cli-support.d.ts.map +1 -1
- package/dist/cli/agent-vm-cli-support.js.map +1 -1
- package/dist/cli/commands/controller-definition.d.ts +42 -42
- package/dist/cli/commands/create-app.d.ts +42 -42
- package/dist/cli/commands/doctor-definition.d.ts.map +1 -1
- package/dist/cli/commands/doctor-definition.js +6 -0
- package/dist/cli/commands/doctor-definition.js.map +1 -1
- package/dist/cli/controller-operation-commands.d.ts +19 -0
- package/dist/cli/controller-operation-commands.d.ts.map +1 -1
- package/dist/cli/controller-operation-commands.js +63 -44
- package/dist/cli/controller-operation-commands.js.map +1 -1
- package/dist/cli/manual-templates.d.ts.map +1 -1
- package/dist/cli/manual-templates.js +11 -2
- package/dist/cli/manual-templates.js.map +1 -1
- package/dist/config/system-config.d.ts +7 -0
- package/dist/config/system-config.d.ts.map +1 -1
- package/dist/config/system-config.js +35 -0
- package/dist/config/system-config.js.map +1 -1
- package/dist/controller/controller-runtime-operations.d.ts +1 -0
- package/dist/controller/controller-runtime-operations.d.ts.map +1 -1
- package/dist/controller/controller-runtime-operations.js +2 -0
- package/dist/controller/controller-runtime-operations.js.map +1 -1
- package/dist/controller/controller-runtime-types.d.ts +3 -0
- package/dist/controller/controller-runtime-types.d.ts.map +1 -1
- package/dist/controller/controller-runtime.d.ts +1 -1
- package/dist/controller/controller-runtime.d.ts.map +1 -1
- package/dist/controller/controller-runtime.js +208 -117
- package/dist/controller/controller-runtime.js.map +1 -1
- package/dist/controller/health/channel-provider-recovery-observation.d.ts +23 -0
- package/dist/controller/health/channel-provider-recovery-observation.d.ts.map +1 -0
- package/dist/controller/health/channel-provider-recovery-observation.js +69 -0
- package/dist/controller/health/channel-provider-recovery-observation.js.map +1 -0
- package/dist/controller/health/durable-health-event-log.d.ts +24 -0
- package/dist/controller/health/durable-health-event-log.d.ts.map +1 -0
- package/dist/controller/health/durable-health-event-log.js +89 -0
- package/dist/controller/health/durable-health-event-log.js.map +1 -0
- package/dist/controller/health/gateway-recovery-actions.d.ts +27 -0
- package/dist/controller/health/gateway-recovery-actions.d.ts.map +1 -0
- package/dist/controller/health/gateway-recovery-actions.js +71 -0
- package/dist/controller/health/gateway-recovery-actions.js.map +1 -0
- package/dist/controller/health/gateway-service-health-monitor.d.ts +41 -3
- package/dist/controller/health/gateway-service-health-monitor.d.ts.map +1 -1
- package/dist/controller/health/gateway-service-health-monitor.js +231 -57
- package/dist/controller/health/gateway-service-health-monitor.js.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-policy.d.ts +20 -0
- package/dist/controller/health/gateway-vm-recovery-policy.d.ts.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-policy.js +85 -21
- package/dist/controller/health/gateway-vm-recovery-policy.js.map +1 -1
- package/dist/controller/health/gateway-vm-recovery-runner.d.ts +39 -0
- package/dist/controller/health/gateway-vm-recovery-runner.d.ts.map +1 -0
- package/dist/controller/health/gateway-vm-recovery-runner.js +251 -0
- package/dist/controller/health/gateway-vm-recovery-runner.js.map +1 -0
- package/dist/controller/health/health-event-store.d.ts +4 -0
- package/dist/controller/health/health-event-store.d.ts.map +1 -1
- package/dist/controller/health/health-event-store.js +19 -0
- package/dist/controller/health/health-event-store.js.map +1 -1
- package/dist/controller/http/controller-health-event-routes.d.ts +6 -0
- package/dist/controller/http/controller-health-event-routes.d.ts.map +1 -1
- package/dist/controller/http/controller-health-event-routes.js +49 -0
- package/dist/controller/http/controller-health-event-routes.js.map +1 -1
- package/dist/controller/http/controller-http-routes.d.ts.map +1 -1
- package/dist/controller/http/controller-http-routes.js +6 -0
- package/dist/controller/http/controller-http-routes.js.map +1 -1
- package/dist/controller/leases/lease-manager.d.ts.map +1 -1
- package/dist/controller/leases/lease-manager.js +37 -16
- package/dist/controller/leases/lease-manager.js.map +1 -1
- package/dist/controller/leases/tool-vm-lease-lifecycle.d.ts +44 -0
- package/dist/controller/leases/tool-vm-lease-lifecycle.d.ts.map +1 -0
- package/dist/controller/leases/tool-vm-lease-lifecycle.js +28 -0
- package/dist/controller/leases/tool-vm-lease-lifecycle.js.map +1 -0
- package/dist/controller/worker-task-runner.d.ts +6 -0
- package/dist/controller/worker-task-runner.d.ts.map +1 -1
- package/dist/controller/worker-task-runner.js +13 -4
- package/dist/controller/worker-task-runner.js.map +1 -1
- package/dist/controller/zone-runtimes/gateway-lifecycle-operation-record.d.ts +37 -0
- package/dist/controller/zone-runtimes/gateway-lifecycle-operation-record.d.ts.map +1 -0
- package/dist/controller/zone-runtimes/gateway-lifecycle-operation-record.js +133 -0
- package/dist/controller/zone-runtimes/gateway-lifecycle-operation-record.js.map +1 -0
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.d.ts +101 -0
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.d.ts.map +1 -0
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.js +143 -0
- package/dist/controller/zone-runtimes/gateway-zone-state-machine.js.map +1 -0
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.d.ts +8 -1
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.d.ts.map +1 -1
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.js +621 -65
- package/dist/controller/zone-runtimes/openclaw-zone-runtime.js.map +1 -1
- package/dist/controller/zone-runtimes/zone-runtime-errors.d.ts +7 -1
- package/dist/controller/zone-runtimes/zone-runtime-errors.d.ts.map +1 -1
- package/dist/controller/zone-runtimes/zone-runtime-errors.js +5 -1
- package/dist/controller/zone-runtimes/zone-runtime-errors.js.map +1 -1
- package/dist/controller/zone-runtimes/zone-runtime-registry.d.ts +2 -0
- package/dist/controller/zone-runtimes/zone-runtime-registry.d.ts.map +1 -1
- package/dist/controller/zone-runtimes/zone-runtime-registry.js +23 -0
- package/dist/controller/zone-runtimes/zone-runtime-registry.js.map +1 -1
- package/dist/controller/zone-runtimes/zone-runtime-types.d.ts +7 -0
- package/dist/controller/zone-runtimes/zone-runtime-types.d.ts.map +1 -1
- package/dist/gateway/gateway-ownership-evidence.d.ts +35 -0
- package/dist/gateway/gateway-ownership-evidence.d.ts.map +1 -0
- package/dist/gateway/gateway-ownership-evidence.js +10 -0
- package/dist/gateway/gateway-ownership-evidence.js.map +1 -0
- package/dist/gateway/gateway-recovery.d.ts +16 -0
- package/dist/gateway/gateway-recovery.d.ts.map +1 -1
- package/dist/gateway/gateway-recovery.js +105 -9
- package/dist/gateway/gateway-recovery.js.map +1 -1
- package/dist/gateway/gateway-zone-orchestrator.d.ts.map +1 -1
- package/dist/gateway/gateway-zone-orchestrator.js +50 -39
- package/dist/gateway/gateway-zone-orchestrator.js.map +1 -1
- package/dist/gateway/mcp-portal-effective-config.d.ts +11 -0
- package/dist/gateway/mcp-portal-effective-config.d.ts.map +1 -1
- package/dist/gateway/mcp-portal-effective-config.js +27 -8
- package/dist/gateway/mcp-portal-effective-config.js.map +1 -1
- package/dist/integration-tests/{smoke-harness.d.ts → e2e-harness.d.ts} +52 -37
- package/dist/integration-tests/e2e-harness.d.ts.map +1 -0
- package/dist/integration-tests/{smoke-harness.js → e2e-harness.js} +483 -131
- package/dist/integration-tests/e2e-harness.js.map +1 -0
- package/dist/integration-tests/e2e-protocol-wait.d.ts +2 -0
- package/dist/integration-tests/e2e-protocol-wait.d.ts.map +1 -0
- package/dist/integration-tests/e2e-protocol-wait.js +5 -0
- package/dist/integration-tests/e2e-protocol-wait.js.map +1 -0
- package/dist/integration-tests/e2e-workspace-build-global-setup.d.ts +27 -0
- package/dist/integration-tests/e2e-workspace-build-global-setup.d.ts.map +1 -0
- package/dist/integration-tests/e2e-workspace-build-global-setup.js +49 -0
- package/dist/integration-tests/e2e-workspace-build-global-setup.js.map +1 -0
- package/dist/integration-tests/live-agent-model-roundtrip-deployment.d.ts +11 -0
- package/dist/integration-tests/live-agent-model-roundtrip-deployment.d.ts.map +1 -0
- package/dist/integration-tests/live-agent-model-roundtrip-deployment.js +48 -0
- package/dist/integration-tests/live-agent-model-roundtrip-deployment.js.map +1 -0
- package/dist/integration-tests/live-agent-model-roundtrip-gates.d.ts +11 -0
- package/dist/integration-tests/live-agent-model-roundtrip-gates.d.ts.map +1 -0
- package/dist/integration-tests/live-agent-model-roundtrip-gates.js +21 -0
- package/dist/integration-tests/live-agent-model-roundtrip-gates.js.map +1 -0
- package/dist/integration-tests/live-vm-e2e-gates.d.ts +2 -0
- package/dist/integration-tests/live-vm-e2e-gates.d.ts.map +1 -0
- package/dist/integration-tests/live-vm-e2e-gates.js +4 -0
- package/dist/integration-tests/live-vm-e2e-gates.js.map +1 -0
- package/dist/operations/controller-status.d.ts +5 -0
- package/dist/operations/controller-status.d.ts.map +1 -1
- package/dist/operations/controller-status.js +42 -0
- package/dist/operations/controller-status.js.map +1 -1
- package/package.json +11 -11
- package/dist/integration-tests/live-integration-gates.d.ts +0 -2
- package/dist/integration-tests/live-integration-gates.d.ts.map +0 -1
- package/dist/integration-tests/live-integration-gates.js +0 -4
- package/dist/integration-tests/live-integration-gates.js.map +0 -1
- package/dist/integration-tests/smoke-harness.d.ts.map +0 -1
- package/dist/integration-tests/smoke-harness.js.map +0 -1
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
import { execFile, execFileSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs/promises';
|
|
3
4
|
import net from 'node:net';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
7
|
+
import { setTimeout as waitForRetryInterval } from 'node:timers/promises';
|
|
6
8
|
import { promisify } from 'node:util';
|
|
7
|
-
import { resolveGondolinMinimumZigVersion } from '@agent-vm/gondolin-adapter';
|
|
9
|
+
import { hasBuiltImageAssets, resolveGondolinMinimumZigVersion } from '@agent-vm/gondolin-adapter';
|
|
8
10
|
import { computeFingerprintFromConfigPath } from '../build/gondolin-image-builder.js';
|
|
9
11
|
import { generateManagedDockerfile, resolveManagedImageRelease, } from '../build/managed-image-dockerfile.js';
|
|
12
|
+
import { readPreparedGondolinImage, writePreparedGondolinImage, } from '../build/prepared-gondolin-image-cache.js';
|
|
10
13
|
import { isZigVersionAtLeast, resolveHostZigVersion } from '../build/zig-compatibility.js';
|
|
14
|
+
import { runBuildCommand } from '../cli/build-command.js';
|
|
11
15
|
import { scaffoldAgentVmProject } from '../cli/init-command.js';
|
|
12
16
|
import { loadJsonConfigFile } from '../config/json-config-file.js';
|
|
13
17
|
import { loadSystemConfig } from '../config/system-config.js';
|
|
14
18
|
import { startControllerRuntime } from '../controller/controller-runtime.js';
|
|
15
19
|
import { startGatewayZone } from '../gateway/gateway-zone-orchestrator.js';
|
|
16
20
|
const defaultOpenClawMcpPortalExtensionsPath = '/home/openclaw/.openclaw/extensions/mcp-portal';
|
|
21
|
+
const dockerContextLocalPackageTimestamp = new Date('2000-01-01T00:00:00.000Z');
|
|
22
|
+
const e2ePreparedImageManifestFileName = 'prepared-e2e-images.json';
|
|
23
|
+
const e2ePreparedImageManifestSchemaVersion = 1;
|
|
17
24
|
const execFileAsync = promisify(execFile);
|
|
18
25
|
const openClawMcpPortalPluginName = 'mcp-portal';
|
|
19
|
-
const
|
|
20
|
-
'agent-vm-gateway-
|
|
21
|
-
'agent-vm-
|
|
22
|
-
'openclaw-control-link-
|
|
23
|
-
'openclaw-mcp-portal-
|
|
24
|
-
'openclaw-subagent-lease-
|
|
25
|
-
'openclaw-zone-git-
|
|
26
|
-
'worker-loop-
|
|
26
|
+
const e2eTempRootPrefixes = [
|
|
27
|
+
'agent-vm-gateway-e2e-project-',
|
|
28
|
+
'agent-vm-e2e-harness-',
|
|
29
|
+
'openclaw-control-link-e2e-',
|
|
30
|
+
'openclaw-mcp-portal-e2e-',
|
|
31
|
+
'openclaw-subagent-lease-e2e-',
|
|
32
|
+
'openclaw-zone-git-e2e-',
|
|
33
|
+
'worker-loop-e2e-',
|
|
27
34
|
];
|
|
35
|
+
function resolveE2eCacheRoot() {
|
|
36
|
+
const configuredCacheRoot = process.env.AGENT_VM_E2E_CACHE_DIR;
|
|
37
|
+
if (configuredCacheRoot !== undefined && configuredCacheRoot.length > 0) {
|
|
38
|
+
return path.resolve(configuredCacheRoot);
|
|
39
|
+
}
|
|
40
|
+
return path.join(os.tmpdir(), 'agent-vm-e2e-cache');
|
|
41
|
+
}
|
|
42
|
+
function isObjectRecord(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
export function shouldCleanupE2eDockerImages(options = {}) {
|
|
46
|
+
const env = options.env ?? process.env;
|
|
47
|
+
return options.cleanupImages === true || env.AGENT_VM_E2E_CLEAN_IMAGES === '1';
|
|
48
|
+
}
|
|
28
49
|
export function hasCommand(command) {
|
|
29
50
|
try {
|
|
30
51
|
execFileSync('sh', ['-lc', `command -v ${command} >/dev/null`], { stdio: 'ignore' });
|
|
@@ -34,13 +55,13 @@ export function hasCommand(command) {
|
|
|
34
55
|
return false;
|
|
35
56
|
}
|
|
36
57
|
}
|
|
37
|
-
export function
|
|
58
|
+
export function currentE2eArchitecture() {
|
|
38
59
|
return process.arch === 'arm64' ? 'aarch64' : 'x86_64';
|
|
39
60
|
}
|
|
40
61
|
export function qemuCommandForArchitecture(architecture) {
|
|
41
62
|
return architecture === 'aarch64' ? 'qemu-system-aarch64' : 'qemu-system-x86_64';
|
|
42
63
|
}
|
|
43
|
-
function
|
|
64
|
+
function isOwnedE2eTempRoot(tempRoot) {
|
|
44
65
|
const resolvedTempRoot = path.resolve(tempRoot);
|
|
45
66
|
const resolvedSystemTempRoot = path.resolve(os.tmpdir());
|
|
46
67
|
if (resolvedTempRoot === resolvedSystemTempRoot ||
|
|
@@ -48,15 +69,15 @@ function isOwnedSmokeTempRoot(tempRoot) {
|
|
|
48
69
|
return false;
|
|
49
70
|
}
|
|
50
71
|
const basename = path.basename(resolvedTempRoot);
|
|
51
|
-
return
|
|
72
|
+
return e2eTempRootPrefixes.some((prefix) => basename.startsWith(prefix));
|
|
52
73
|
}
|
|
53
|
-
export async function
|
|
54
|
-
if (!
|
|
74
|
+
export async function removeE2eTempRoot(tempRoot) {
|
|
75
|
+
if (!isOwnedE2eTempRoot(tempRoot)) {
|
|
55
76
|
return;
|
|
56
77
|
}
|
|
57
78
|
await fs.rm(tempRoot, { force: true, recursive: true });
|
|
58
79
|
}
|
|
59
|
-
export async function
|
|
80
|
+
export async function canRunGondolinE2e(options) {
|
|
60
81
|
const commandExists = options.commandExists ?? hasCommand;
|
|
61
82
|
if (!commandExists(qemuCommandForArchitecture(options.architecture)) ||
|
|
62
83
|
!commandExists('docker')) {
|
|
@@ -67,14 +88,14 @@ export async function canRunGondolinSmoke(options) {
|
|
|
67
88
|
return (installedZigVersion !== undefined &&
|
|
68
89
|
isZigVersionAtLeast(installedZigVersion, requiredZigVersion));
|
|
69
90
|
}
|
|
70
|
-
export async function
|
|
91
|
+
export async function shouldRunWorkerGatewayE2e(options) {
|
|
71
92
|
const env = options.env ?? process.env;
|
|
72
|
-
if (env.
|
|
73
|
-
typeof env.
|
|
74
|
-
env.
|
|
93
|
+
if (env.AGENT_VM_WORKER_E2E !== '1' ||
|
|
94
|
+
typeof env.AGENT_VM_TEST_OPENAI_API_KEY !== 'string' ||
|
|
95
|
+
env.AGENT_VM_TEST_OPENAI_API_KEY.length === 0) {
|
|
75
96
|
return false;
|
|
76
97
|
}
|
|
77
|
-
return await
|
|
98
|
+
return await canRunGondolinE2e({
|
|
78
99
|
architecture: options.architecture,
|
|
79
100
|
...(options.commandExists ? { commandExists: options.commandExists } : {}),
|
|
80
101
|
...(options.resolveRequiredZigVersion
|
|
@@ -83,12 +104,6 @@ export async function shouldRunWorkerGatewaySmoke(options) {
|
|
|
83
104
|
...(options.resolveZigVersion ? { resolveZigVersion: options.resolveZigVersion } : {}),
|
|
84
105
|
});
|
|
85
106
|
}
|
|
86
|
-
export function rebuildWorkspacePackages(repoRoot) {
|
|
87
|
-
execFileSync('pnpm', ['build'], {
|
|
88
|
-
cwd: repoRoot,
|
|
89
|
-
stdio: 'inherit',
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
107
|
export async function findAvailablePort() {
|
|
93
108
|
return await new Promise((resolve, reject) => {
|
|
94
109
|
const server = net.createServer();
|
|
@@ -109,57 +124,292 @@ export async function findAvailablePort() {
|
|
|
109
124
|
});
|
|
110
125
|
});
|
|
111
126
|
}
|
|
127
|
+
function readNodeNetworkErrorCode(error) {
|
|
128
|
+
if (!(error instanceof TypeError) || error.message !== 'fetch failed') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const cause = error.cause;
|
|
132
|
+
if (typeof cause !== 'object' || cause === null || !('code' in cause)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return typeof cause.code === 'string' ? cause.code : null;
|
|
136
|
+
}
|
|
137
|
+
function isRecoverableControllerReadyError(error) {
|
|
138
|
+
return ['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH'].includes(readNodeNetworkErrorCode(error) ?? '');
|
|
139
|
+
}
|
|
112
140
|
export async function waitForControllerReady(controllerPort) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
141
|
+
const timeoutMs = 5_000;
|
|
142
|
+
const retryIntervalMs = 50;
|
|
143
|
+
const startedAtMs = performance.now();
|
|
144
|
+
let lastError = 'not attempted';
|
|
145
|
+
while (performance.now() - startedAtMs <= timeoutMs) {
|
|
146
|
+
try {
|
|
147
|
+
// oxlint-disable-next-line no-await-in-loop -- controller startup readiness must observe sequential protocol state.
|
|
148
|
+
const response = await fetch(`http://127.0.0.1:${String(controllerPort)}/controller-status`, {
|
|
149
|
+
signal: AbortSignal.timeout(1_000),
|
|
150
|
+
});
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
lastError = `HTTP ${String(response.status)}`;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!isRecoverableControllerReadyError(error)) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
161
|
+
}
|
|
162
|
+
// oxlint-disable-next-line no-await-in-loop -- controller readiness has no event source from the subprocess boundary.
|
|
163
|
+
await waitForRetryInterval(retryIntervalMs);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Controller did not report ready within ${String(timeoutMs)}ms. Last error: ${lastError}`);
|
|
166
|
+
}
|
|
167
|
+
async function pathExists(filePath) {
|
|
168
|
+
try {
|
|
169
|
+
await fs.access(filePath);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function listDirectoryFiles(directoryPath) {
|
|
180
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
181
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
182
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
183
|
+
if (entry.isDirectory()) {
|
|
184
|
+
return await listDirectoryFiles(entryPath);
|
|
185
|
+
}
|
|
186
|
+
return entry.isFile() ? [entryPath] : [];
|
|
187
|
+
}));
|
|
188
|
+
return files.flat().toSorted((leftPath, rightPath) => leftPath.localeCompare(rightPath));
|
|
189
|
+
}
|
|
190
|
+
async function hashFileInto(hasher, filePath, baseDirectory) {
|
|
191
|
+
const relativePath = path.relative(baseDirectory, filePath).split(path.sep).join('/');
|
|
192
|
+
hasher.update(`file:${relativePath}\0`);
|
|
193
|
+
hasher.update(await fs.readFile(filePath));
|
|
194
|
+
hasher.update('\0');
|
|
195
|
+
}
|
|
196
|
+
async function computeDockerContextFingerprint(dockerfilePath) {
|
|
197
|
+
const dockerContextDirectory = path.dirname(dockerfilePath);
|
|
198
|
+
const hasher = crypto.createHash('sha256');
|
|
199
|
+
for (const filePath of await listDirectoryFiles(dockerContextDirectory)) {
|
|
200
|
+
// oxlint-disable-next-line no-await-in-loop -- hash order is deterministic and low-volume for e2e Docker contexts.
|
|
201
|
+
await hashFileInto(hasher, filePath, dockerContextDirectory);
|
|
202
|
+
}
|
|
203
|
+
return hasher.digest('hex');
|
|
204
|
+
}
|
|
205
|
+
async function readTextIfPresent(filePath) {
|
|
206
|
+
if (filePath === undefined || !(await pathExists(filePath))) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return await fs.readFile(filePath, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
async function normalizeE2eImageSourceForFingerprint(source) {
|
|
212
|
+
if (!isObjectRecord(source)) {
|
|
213
|
+
return source;
|
|
214
|
+
}
|
|
215
|
+
const overlay = source.overlay;
|
|
216
|
+
return {
|
|
217
|
+
...source,
|
|
218
|
+
...(typeof overlay === 'string'
|
|
219
|
+
? { overlay: { content: await readTextIfPresent(overlay) } }
|
|
220
|
+
: {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async function computeE2eImageRecipeFingerprint(options) {
|
|
224
|
+
const hasher = crypto.createHash('sha256');
|
|
225
|
+
hasher.update(JSON.stringify({
|
|
226
|
+
buildConfig: await readTextIfPresent(options.buildConfigPath),
|
|
227
|
+
dockerContext: options.dockerfile === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: await computeDockerContextFingerprint(options.dockerfile),
|
|
230
|
+
source: await normalizeE2eImageSourceForFingerprint(options.source),
|
|
231
|
+
}));
|
|
232
|
+
return hasher.digest('hex');
|
|
233
|
+
}
|
|
234
|
+
async function collectE2eImageTargets(project) {
|
|
235
|
+
const createImageTarget = async (family, profileName, profile) => {
|
|
236
|
+
const target = {
|
|
237
|
+
buildConfigPath: profile.buildConfig,
|
|
238
|
+
cacheDirectory: path.join(project.systemConfig.cacheDir, family === 'gateway' ? 'gateway-images' : 'tool-vm-images', profileName),
|
|
239
|
+
e2eManifestEligible: profile.source === undefined,
|
|
240
|
+
family,
|
|
241
|
+
name: profileName,
|
|
242
|
+
recipeFingerprint: await computeE2eImageRecipeFingerprint({
|
|
243
|
+
buildConfigPath: profile.buildConfig,
|
|
244
|
+
...(profile.dockerfile === undefined ? {} : { dockerfile: profile.dockerfile }),
|
|
245
|
+
...(profile.source === undefined ? {} : { source: profile.source }),
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
248
|
+
if (profile.dockerfile !== undefined) {
|
|
249
|
+
return { ...target, dockerfile: profile.dockerfile };
|
|
250
|
+
}
|
|
251
|
+
if (profile.source !== undefined) {
|
|
252
|
+
return { ...target, source: profile.source };
|
|
118
253
|
}
|
|
119
|
-
|
|
120
|
-
|
|
254
|
+
return target;
|
|
255
|
+
};
|
|
256
|
+
const gatewayTargets = await Promise.all(Object.entries(project.systemConfig.imageProfiles.gateways).map(async ([profileName, profile]) => await createImageTarget('gateway', profileName, profile)));
|
|
257
|
+
const toolVmTargets = await Promise.all(Object.entries(project.systemConfig.imageProfiles.toolVms).map(async ([profileName, profile]) => await createImageTarget('toolVm', profileName, profile)));
|
|
258
|
+
return [...gatewayTargets, ...toolVmTargets];
|
|
259
|
+
}
|
|
260
|
+
function e2ePreparedImageManifestPath(cacheDir) {
|
|
261
|
+
return path.join(cacheDir, e2ePreparedImageManifestFileName);
|
|
262
|
+
}
|
|
263
|
+
function parseE2ePreparedImageManifest(value) {
|
|
264
|
+
if (!isObjectRecord(value) || value.schemaVersion !== e2ePreparedImageManifestSchemaVersion) {
|
|
265
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
266
|
+
}
|
|
267
|
+
if (!Array.isArray(value.entries)) {
|
|
268
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
121
269
|
}
|
|
122
|
-
|
|
270
|
+
const entries = value.entries.filter((entry) => {
|
|
271
|
+
if (!isObjectRecord(entry)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return ((entry.family === 'gateway' || entry.family === 'toolVm') &&
|
|
275
|
+
typeof entry.name === 'string' &&
|
|
276
|
+
typeof entry.recipeFingerprint === 'string' &&
|
|
277
|
+
typeof entry.buildConfigPath === 'string' &&
|
|
278
|
+
typeof entry.cacheDirectory === 'string' &&
|
|
279
|
+
typeof entry.fingerprint === 'string' &&
|
|
280
|
+
typeof entry.imagePath === 'string');
|
|
281
|
+
});
|
|
282
|
+
return { entries, schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
123
283
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
284
|
+
async function readE2ePreparedImageManifest(cacheDir) {
|
|
285
|
+
try {
|
|
286
|
+
const parsed = JSON.parse(await fs.readFile(e2ePreparedImageManifestPath(cacheDir), 'utf8'));
|
|
287
|
+
return parseE2ePreparedImageManifest(parsed);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error instanceof SyntaxError) {
|
|
291
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
292
|
+
}
|
|
293
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
294
|
+
return { entries: [], schemaVersion: e2ePreparedImageManifestSchemaVersion };
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function writeE2ePreparedImageManifest(cacheDir, manifest) {
|
|
300
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
301
|
+
await fs.writeFile(e2ePreparedImageManifestPath(cacheDir), `${JSON.stringify(manifest, null, '\t')}\n`, 'utf8');
|
|
302
|
+
}
|
|
303
|
+
function e2eImageTargetKey(target) {
|
|
304
|
+
return `${target.family}:${target.name}:${target.recipeFingerprint}`;
|
|
305
|
+
}
|
|
306
|
+
function e2eManifestEntryKey(entry) {
|
|
307
|
+
return `${entry.family}:${entry.name}:${entry.recipeFingerprint}`;
|
|
308
|
+
}
|
|
309
|
+
async function materializePreparedE2eImagesFromManifest(project, targets) {
|
|
310
|
+
if (targets.some((target) => !target.e2eManifestEligible)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
const manifest = await readE2ePreparedImageManifest(project.systemConfig.cacheDir);
|
|
314
|
+
const entriesByKey = new Map(manifest.entries.map((entry) => [e2eManifestEntryKey(entry), entry]));
|
|
315
|
+
const targetReadiness = await Promise.all(targets.map(async (target) => {
|
|
316
|
+
const entry = entriesByKey.get(e2eImageTargetKey(target));
|
|
317
|
+
return entry !== undefined && (await hasBuiltImageAssets(entry.imagePath));
|
|
318
|
+
}));
|
|
319
|
+
if (targetReadiness.some((isReady) => !isReady)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
await Promise.all(targets.map(async (target) => {
|
|
323
|
+
const entry = entriesByKey.get(e2eImageTargetKey(target));
|
|
324
|
+
if (entry === undefined) {
|
|
325
|
+
throw new Error(`Missing prepared e2e image manifest entry for ${target.family}/${target.name}.`);
|
|
326
|
+
}
|
|
327
|
+
await writePreparedGondolinImage({
|
|
328
|
+
buildConfigPath: target.buildConfigPath,
|
|
329
|
+
cacheDir: target.cacheDirectory,
|
|
330
|
+
fingerprint: entry.fingerprint,
|
|
331
|
+
...(entry.fingerprintInput === undefined
|
|
332
|
+
? {}
|
|
333
|
+
: { fingerprintInput: entry.fingerprintInput }),
|
|
334
|
+
imagePath: entry.imagePath,
|
|
335
|
+
});
|
|
336
|
+
}));
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
async function recordPreparedE2eImages(project, targets) {
|
|
340
|
+
const manifest = await readE2ePreparedImageManifest(project.systemConfig.cacheDir);
|
|
341
|
+
const entriesByKey = new Map(manifest.entries.map((entry) => [e2eManifestEntryKey(entry), entry]));
|
|
342
|
+
const preparedImages = await Promise.all(targets.map(async (target) => {
|
|
343
|
+
if (!target.e2eManifestEligible) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const preparedImage = await readPreparedGondolinImage({
|
|
347
|
+
buildConfigPath: target.buildConfigPath,
|
|
348
|
+
cacheDir: target.cacheDirectory,
|
|
349
|
+
});
|
|
350
|
+
return { preparedImage, target };
|
|
351
|
+
}));
|
|
352
|
+
for (const item of preparedImages) {
|
|
353
|
+
if (item === null || item.preparedImage === undefined) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const { preparedImage, target } = item;
|
|
357
|
+
entriesByKey.set(e2eImageTargetKey(target), {
|
|
358
|
+
buildConfigPath: path.resolve(target.buildConfigPath),
|
|
359
|
+
cacheDirectory: path.resolve(target.cacheDirectory),
|
|
360
|
+
family: target.family,
|
|
361
|
+
fingerprint: preparedImage.fingerprint,
|
|
362
|
+
...(preparedImage.fingerprintInput === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { fingerprintInput: preparedImage.fingerprintInput }),
|
|
365
|
+
imagePath: preparedImage.imagePath,
|
|
366
|
+
name: target.name,
|
|
367
|
+
recipeFingerprint: target.recipeFingerprint,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
await writeE2ePreparedImageManifest(project.systemConfig.cacheDir, {
|
|
371
|
+
entries: [...entriesByKey.values()].toSorted((leftEntry, rightEntry) => e2eManifestEntryKey(leftEntry).localeCompare(e2eManifestEntryKey(rightEntry))),
|
|
372
|
+
schemaVersion: e2ePreparedImageManifestSchemaVersion,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
export async function findReusableGatewayImageDirectory(currentProjectRoot, gatewayBuildConfigPath, imageProfileName = 'worker') {
|
|
376
|
+
const explicitE2eCacheRoot = process.env.AGENT_VM_E2E_CACHE_DIR;
|
|
377
|
+
if (!explicitE2eCacheRoot) {
|
|
127
378
|
return null;
|
|
128
379
|
}
|
|
129
380
|
const requiredFingerprint = await computeFingerprintFromConfigPath(gatewayBuildConfigPath);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
381
|
+
if (!(await pathExists(explicitE2eCacheRoot))) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const tempRootEntries = await fs.readdir(explicitE2eCacheRoot, { withFileTypes: true });
|
|
385
|
+
const e2eRunDirectories = tempRootEntries
|
|
386
|
+
.filter((entry) => entry.isDirectory())
|
|
387
|
+
.map((entry) => path.join(explicitE2eCacheRoot, entry.name));
|
|
388
|
+
for (const e2eRunDirectory of e2eRunDirectories) {
|
|
389
|
+
if (e2eRunDirectory === currentProjectRoot) {
|
|
136
390
|
continue;
|
|
137
391
|
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await fs.access(path.join(candidateImageDir, 'rootfs.ext4'));
|
|
144
|
-
// oxlint-disable-next-line eslint/no-await-in-loop -- intentionally searches cache candidates
|
|
145
|
-
await fs.access(path.join(candidateImageDir, 'initramfs.cpio.lz4'));
|
|
392
|
+
const candidateImageDirectories = [
|
|
393
|
+
path.join(e2eRunDirectory, 'gateway-images', imageProfileName, requiredFingerprint),
|
|
394
|
+
path.join(e2eRunDirectory, 'cache', 'gateway-images', imageProfileName, requiredFingerprint),
|
|
395
|
+
];
|
|
396
|
+
for (const candidateImageDir of candidateImageDirectories) {
|
|
146
397
|
// oxlint-disable-next-line eslint/no-await-in-loop -- intentionally searches cache candidates
|
|
147
|
-
await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
catch {
|
|
151
|
-
continue;
|
|
398
|
+
if (await hasBuiltImageAssets(candidateImageDir)) {
|
|
399
|
+
return candidateImageDir;
|
|
400
|
+
}
|
|
152
401
|
}
|
|
153
402
|
}
|
|
154
403
|
return null;
|
|
155
404
|
}
|
|
156
405
|
export async function seedGatewayImageCacheIfAvailable(options) {
|
|
157
|
-
const
|
|
406
|
+
const imageProfileName = options.imageProfileName ?? 'worker';
|
|
407
|
+
const reusableImageDir = await findReusableGatewayImageDirectory(options.currentProjectRoot, options.gatewayBuildConfigPath, imageProfileName);
|
|
158
408
|
if (!reusableImageDir) {
|
|
159
409
|
return;
|
|
160
410
|
}
|
|
161
411
|
const requiredFingerprint = await computeFingerprintFromConfigPath(options.gatewayBuildConfigPath);
|
|
162
|
-
const activeImageDir = path.join(options.activeCacheDir, 'images',
|
|
412
|
+
const activeImageDir = path.join(options.activeCacheDir, 'gateway-images', imageProfileName, requiredFingerprint);
|
|
163
413
|
if (activeImageDir === reusableImageDir) {
|
|
164
414
|
return;
|
|
165
415
|
}
|
|
@@ -167,6 +417,23 @@ export async function seedGatewayImageCacheIfAvailable(options) {
|
|
|
167
417
|
await fs.mkdir(path.dirname(activeImageDir), { recursive: true });
|
|
168
418
|
await fs.symlink(reusableImageDir, activeImageDir, 'dir');
|
|
169
419
|
}
|
|
420
|
+
export async function prepareGatewayE2eProjectImages(options) {
|
|
421
|
+
const imageTargets = await collectE2eImageTargets(options.project);
|
|
422
|
+
if (await materializePreparedE2eImagesFromManifest(options.project, imageTargets)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
await Promise.all(Object.entries(options.project.systemConfig.imageProfiles.gateways).map(async ([profileName, gatewayProfile]) => {
|
|
426
|
+
await seedGatewayImageCacheIfAvailable({
|
|
427
|
+
activeCacheDir: options.project.systemConfig.cacheDir,
|
|
428
|
+
currentProjectRoot: options.project.tempRoot,
|
|
429
|
+
gatewayBuildConfigPath: gatewayProfile.buildConfig,
|
|
430
|
+
imageProfileName: profileName,
|
|
431
|
+
});
|
|
432
|
+
}));
|
|
433
|
+
const runBuild = options.runBuild ?? runBuildCommand;
|
|
434
|
+
await runBuild({ systemConfig: options.project.systemConfig });
|
|
435
|
+
await recordPreparedE2eImages(options.project, imageTargets);
|
|
436
|
+
}
|
|
170
437
|
export function createSmokeSecretResolver(secrets) {
|
|
171
438
|
const resolve = async (ref) => {
|
|
172
439
|
if (ref.source === 'config') {
|
|
@@ -208,14 +475,14 @@ function applySmokeEnvironment(secrets) {
|
|
|
208
475
|
}
|
|
209
476
|
};
|
|
210
477
|
}
|
|
211
|
-
export function
|
|
478
|
+
export function getOpenClawE2eZone(systemConfig) {
|
|
212
479
|
const zone = systemConfig.zones[0];
|
|
213
480
|
if (!zone || zone.gateway.type !== 'openclaw') {
|
|
214
481
|
throw new Error('Expected smoke system config to contain an OpenClaw zone.');
|
|
215
482
|
}
|
|
216
483
|
return { ...zone, gateway: zone.gateway };
|
|
217
484
|
}
|
|
218
|
-
function
|
|
485
|
+
function getWorkerE2eZone(systemConfig) {
|
|
219
486
|
const zone = systemConfig.zones[0];
|
|
220
487
|
if (!zone || zone.gateway.type !== 'worker') {
|
|
221
488
|
throw new Error('Expected smoke system config to contain a Worker Gateway zone.');
|
|
@@ -247,41 +514,123 @@ async function assertLocalPackageFilesExist(props) {
|
|
|
247
514
|
}
|
|
248
515
|
}));
|
|
249
516
|
}
|
|
517
|
+
export function resolveLocalPackagePackArgs(packDirectory) {
|
|
518
|
+
return ['pack', '--pack-destination', packDirectory, '--config.ignore-scripts=true'];
|
|
519
|
+
}
|
|
520
|
+
function parseLocalPackagePackPlan(value, packageName) {
|
|
521
|
+
if (!isJsonRecord(value)) {
|
|
522
|
+
throw new Error(`Expected pnpm pack dry-run for ${packageName} to return an object.`);
|
|
523
|
+
}
|
|
524
|
+
const files = Array.isArray(value.files)
|
|
525
|
+
? value.files.filter((file) => {
|
|
526
|
+
return isJsonRecord(file) && typeof file.path === 'string' && file.path.length > 0;
|
|
527
|
+
})
|
|
528
|
+
: [];
|
|
529
|
+
if (typeof value.name !== 'string' ||
|
|
530
|
+
typeof value.version !== 'string' ||
|
|
531
|
+
typeof value.filename !== 'string' ||
|
|
532
|
+
files.length === 0) {
|
|
533
|
+
throw new Error(`Unexpected pnpm pack dry-run result for ${packageName}.`);
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
filename: value.filename,
|
|
537
|
+
files,
|
|
538
|
+
name: value.name,
|
|
539
|
+
version: value.version,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function resolveLocalPackagePackPlan(packageDirectory, packageName) {
|
|
543
|
+
const dryRunOutput = execFileSync('pnpm', ['pack', '--dry-run', '--json', '--config.ignore-scripts=true'], {
|
|
544
|
+
cwd: packageDirectory,
|
|
545
|
+
encoding: 'utf8',
|
|
546
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
547
|
+
});
|
|
548
|
+
return parseLocalPackagePackPlan(JSON.parse(dryRunOutput), packageName);
|
|
549
|
+
}
|
|
550
|
+
function cacheSafeLocalPackageName(packageName) {
|
|
551
|
+
return packageName.replace(/^@/u, '').replaceAll('/', '__');
|
|
552
|
+
}
|
|
553
|
+
async function computeLocalPackagePackFingerprint(packageDirectory, packPlan) {
|
|
554
|
+
const hash = crypto.createHash('sha256');
|
|
555
|
+
hash.update(`${packPlan.name}\0${packPlan.version}\0${packPlan.filename}\0`);
|
|
556
|
+
for (const file of packPlan.files.toSorted((left, right) => left.path.localeCompare(right.path))) {
|
|
557
|
+
const filePath = path.join(packageDirectory, file.path);
|
|
558
|
+
// oxlint-disable-next-line no-await-in-loop -- ordered hashing keeps cache keys deterministic
|
|
559
|
+
const fileContents = await fs.readFile(filePath);
|
|
560
|
+
hash.update(`${file.path}\0`);
|
|
561
|
+
hash.update(fileContents);
|
|
562
|
+
hash.update('\0');
|
|
563
|
+
}
|
|
564
|
+
return hash.digest('hex').slice(0, 24);
|
|
565
|
+
}
|
|
250
566
|
async function packLocalPackageTarball(props) {
|
|
251
567
|
const packageJsonPath = path.join(props.packageDirectory, 'package.json');
|
|
252
568
|
await fs.access(packageJsonPath);
|
|
253
569
|
await assertLocalPackageFilesExist(props);
|
|
570
|
+
const packPlan = resolveLocalPackagePackPlan(props.packageDirectory, props.packageName);
|
|
571
|
+
const packFingerprint = await computeLocalPackagePackFingerprint(props.packageDirectory, packPlan);
|
|
572
|
+
const cachedPackageDirectory = path.join(resolveE2eCacheRoot(), 'local-package-tarballs', cacheSafeLocalPackageName(props.packageName), packFingerprint);
|
|
573
|
+
const cachedTarballPath = path.join(cachedPackageDirectory, packPlan.filename);
|
|
574
|
+
try {
|
|
575
|
+
await fs.access(cachedTarballPath);
|
|
576
|
+
return cachedTarballPath;
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
if (!isJsonRecord(error) || error.code !== 'ENOENT') {
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
254
583
|
const packDirectory = await fs.mkdtemp(path.join(os.tmpdir(), `${props.packageName}-pack-`));
|
|
255
584
|
try {
|
|
256
|
-
execFileSync('pnpm', [
|
|
585
|
+
execFileSync('pnpm', [...resolveLocalPackagePackArgs(packDirectory)], {
|
|
257
586
|
cwd: props.packageDirectory,
|
|
258
587
|
stdio: 'pipe',
|
|
259
588
|
});
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
589
|
+
const packedTarballPath = path.join(packDirectory, packPlan.filename);
|
|
590
|
+
await fs.access(packedTarballPath);
|
|
591
|
+
await fs.mkdir(cachedPackageDirectory, { recursive: true });
|
|
592
|
+
const temporaryCachedTarballPath = path.join(cachedPackageDirectory, `${packPlan.filename}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
593
|
+
await fs.copyFile(packedTarballPath, temporaryCachedTarballPath);
|
|
594
|
+
await fs.rename(temporaryCachedTarballPath, cachedTarballPath).catch(async (error) => {
|
|
595
|
+
await fs.rm(temporaryCachedTarballPath, { force: true });
|
|
596
|
+
if (isJsonRecord(error) && error.code === 'EEXIST') {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
throw error;
|
|
600
|
+
});
|
|
601
|
+
return cachedTarballPath;
|
|
269
602
|
}
|
|
270
603
|
catch (error) {
|
|
271
604
|
await fs.rm(packDirectory, { force: true, recursive: true });
|
|
272
605
|
throw error;
|
|
273
606
|
}
|
|
607
|
+
finally {
|
|
608
|
+
await fs.rm(packDirectory, { force: true, recursive: true });
|
|
609
|
+
}
|
|
274
610
|
}
|
|
275
|
-
|
|
611
|
+
function isOwnedLocalPackagePackDirectory(packDirectory) {
|
|
612
|
+
const resolvedPackDirectory = path.resolve(packDirectory);
|
|
613
|
+
const resolvedSystemTempRoot = path.resolve(os.tmpdir());
|
|
614
|
+
if (resolvedPackDirectory === resolvedSystemTempRoot ||
|
|
615
|
+
!resolvedPackDirectory.startsWith(`${resolvedSystemTempRoot}${path.sep}`)) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
return path.basename(resolvedPackDirectory).includes('-pack-');
|
|
619
|
+
}
|
|
620
|
+
export async function removeE2eLocalPackageTarballs(tarballPaths) {
|
|
276
621
|
await Promise.all(Array.from(new Set(tarballPaths
|
|
277
622
|
.filter((tarballPath) => tarballPath !== undefined)
|
|
278
|
-
.map((tarballPath) => path.dirname(tarballPath))))
|
|
623
|
+
.map((tarballPath) => path.dirname(tarballPath))))
|
|
624
|
+
.filter(isOwnedLocalPackagePackDirectory)
|
|
625
|
+
.map(async (packDirectory) => {
|
|
279
626
|
await fs.rm(packDirectory, { force: true, recursive: true });
|
|
280
627
|
}));
|
|
281
628
|
}
|
|
282
629
|
async function copyLocalPackageTarballsToDockerContext(options) {
|
|
283
630
|
await Promise.all(options.tarballs.map(async (tarball) => {
|
|
284
|
-
|
|
631
|
+
const targetPath = path.join(options.dockerContextDirectory, tarball.archiveName);
|
|
632
|
+
await fs.copyFile(tarball.sourcePath, targetPath);
|
|
633
|
+
await fs.utimes(targetPath, dockerContextLocalPackageTimestamp, dockerContextLocalPackageTimestamp);
|
|
285
634
|
}));
|
|
286
635
|
}
|
|
287
636
|
async function useLocalToolVmMcpPortalPackageTarballs(options) {
|
|
@@ -349,7 +698,7 @@ export async function useLocalToolVmMcpPortalPackage(options) {
|
|
|
349
698
|
});
|
|
350
699
|
}
|
|
351
700
|
finally {
|
|
352
|
-
await
|
|
701
|
+
await removeE2eLocalPackageTarballs([
|
|
353
702
|
localConfigContractsTarballPath,
|
|
354
703
|
localSecretManagementTarballPath,
|
|
355
704
|
localMcpPortalTarballPath,
|
|
@@ -359,13 +708,14 @@ export async function useLocalToolVmMcpPortalPackage(options) {
|
|
|
359
708
|
function localPackageTarballArchiveName(packageName) {
|
|
360
709
|
return `${packageName}-local.tgz`;
|
|
361
710
|
}
|
|
362
|
-
async function
|
|
711
|
+
async function writeManagedOpenClawE2eDockerfileBase(options) {
|
|
363
712
|
const managedImageRelease = await resolveManagedImageRelease();
|
|
364
713
|
const result = await generateManagedDockerfile({
|
|
365
714
|
base: 'openclaw-gateway',
|
|
366
715
|
imageTargetFamily: 'gateway',
|
|
367
716
|
imageTargetName: options.profileName,
|
|
368
717
|
managedImageRelease,
|
|
718
|
+
openClawAgentVmPackageInstallMode: options.openClawAgentVmPackageInstallMode,
|
|
369
719
|
outputDirectory: options.dockerContextDirectory,
|
|
370
720
|
requiredOpenClawPackageNames: ['@openclaw/discord'],
|
|
371
721
|
});
|
|
@@ -383,7 +733,7 @@ function mutableJsonRecord(value) {
|
|
|
383
733
|
async function runDockerCommand(args) {
|
|
384
734
|
await execFileAsync('docker', [...args]);
|
|
385
735
|
}
|
|
386
|
-
async function
|
|
736
|
+
async function readE2eDockerImageTag(buildConfigPath) {
|
|
387
737
|
let buildConfig;
|
|
388
738
|
try {
|
|
389
739
|
buildConfig = mutableJsonRecord(await loadJsonConfigFile(buildConfigPath));
|
|
@@ -398,7 +748,7 @@ async function readSmokeDockerImageTag(buildConfigPath) {
|
|
|
398
748
|
const imageTag = ociConfig?.image;
|
|
399
749
|
return typeof imageTag === 'string' && imageTag.length > 0 ? imageTag : null;
|
|
400
750
|
}
|
|
401
|
-
export async function
|
|
751
|
+
export async function collectE2eDockerImageTags(systemConfig) {
|
|
402
752
|
const imageProfiles = [
|
|
403
753
|
...Object.values(systemConfig.imageProfiles.gateways),
|
|
404
754
|
...Object.values(systemConfig.imageProfiles.toolVms),
|
|
@@ -406,14 +756,14 @@ export async function collectSmokeDockerImageTags(systemConfig) {
|
|
|
406
756
|
const imageTags = [];
|
|
407
757
|
for (const imageProfile of imageProfiles) {
|
|
408
758
|
// oxlint-disable-next-line eslint/no-await-in-loop -- config files are intentionally read deterministically
|
|
409
|
-
const imageTag = await
|
|
759
|
+
const imageTag = await readE2eDockerImageTag(imageProfile.buildConfig);
|
|
410
760
|
if (imageTag !== null) {
|
|
411
761
|
imageTags.push(imageTag);
|
|
412
762
|
}
|
|
413
763
|
}
|
|
414
764
|
return Array.from(new Set(imageTags));
|
|
415
765
|
}
|
|
416
|
-
export async function
|
|
766
|
+
export async function removeE2eDockerImages(imageTags, options = {}) {
|
|
417
767
|
const dockerCommand = options.runDockerCommand ?? runDockerCommand;
|
|
418
768
|
for (const imageTag of Array.from(new Set(imageTags))) {
|
|
419
769
|
try {
|
|
@@ -427,10 +777,10 @@ export async function removeSmokeDockerImages(imageTags, options = {}) {
|
|
|
427
777
|
await dockerCommand(['image', 'rm', '--force', imageTag]);
|
|
428
778
|
}
|
|
429
779
|
}
|
|
430
|
-
export async function
|
|
431
|
-
await
|
|
780
|
+
export async function removeE2eDockerImagesForSystemConfig(systemConfig, options = {}) {
|
|
781
|
+
await removeE2eDockerImages(await collectE2eDockerImageTags(systemConfig), options);
|
|
432
782
|
}
|
|
433
|
-
function
|
|
783
|
+
function throwIfE2eHarnessCleanupFailed(errors) {
|
|
434
784
|
if (errors.length === 0) {
|
|
435
785
|
return;
|
|
436
786
|
}
|
|
@@ -530,9 +880,10 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
530
880
|
sourcePath: localOpenClawMcpPortalPluginTarballPath,
|
|
531
881
|
},
|
|
532
882
|
];
|
|
533
|
-
const dockerfilePath = await
|
|
883
|
+
const dockerfilePath = await writeManagedOpenClawE2eDockerfileBase({
|
|
534
884
|
dockerContextDirectory,
|
|
535
885
|
profileName: options.profileName,
|
|
886
|
+
openClawAgentVmPackageInstallMode: 'local-overlay',
|
|
536
887
|
});
|
|
537
888
|
await copyLocalPackageTarballsToDockerContext({
|
|
538
889
|
dockerContextDirectory,
|
|
@@ -556,7 +907,9 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
556
907
|
' rm -f ' +
|
|
557
908
|
localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
|
|
558
909
|
'RUN package_root="/opt/agent-vm/local-packages/node_modules" && \\',
|
|
559
|
-
'
|
|
910
|
+
' global_package_root="$(pnpm root -g)" && \\',
|
|
911
|
+
' mkdir -p "$global_package_root" /home/openclaw/.openclaw/extensions && \\',
|
|
912
|
+
' ln -sfn "$package_root/@agent-vm" "$global_package_root/@agent-vm" && \\',
|
|
560
913
|
' ln -sfn "$package_root/@agent-vm/openclaw-agent-vm-plugin/dist" /home/openclaw/.openclaw/extensions/gondolin && \\',
|
|
561
914
|
' ln -sfn "$package_root/@agent-vm/openclaw-mcp-portal-plugin/dist" /home/openclaw/.openclaw/extensions/mcp-portal',
|
|
562
915
|
'',
|
|
@@ -565,7 +918,7 @@ export async function useLocalOpenClawGatewayImagePackages(options) {
|
|
|
565
918
|
delete gatewayProfile.source;
|
|
566
919
|
}
|
|
567
920
|
finally {
|
|
568
|
-
await
|
|
921
|
+
await removeE2eLocalPackageTarballs([
|
|
569
922
|
localConfigContractsTarballPath,
|
|
570
923
|
localSecretManagementTarballPath,
|
|
571
924
|
localGondolinAdapterTarballPath,
|
|
@@ -617,9 +970,10 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
617
970
|
sourcePath: localOpenClawAgentVmPluginTarballPath,
|
|
618
971
|
},
|
|
619
972
|
];
|
|
620
|
-
const dockerfilePath = await
|
|
973
|
+
const dockerfilePath = await writeManagedOpenClawE2eDockerfileBase({
|
|
621
974
|
dockerContextDirectory,
|
|
622
975
|
profileName: options.profileName,
|
|
976
|
+
openClawAgentVmPackageInstallMode: 'local-overlay',
|
|
623
977
|
});
|
|
624
978
|
await copyLocalPackageTarballsToDockerContext({
|
|
625
979
|
dockerContextDirectory,
|
|
@@ -636,7 +990,9 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
636
990
|
' rm -f ' +
|
|
637
991
|
localPackageTarballs.map((tarball) => `/tmp/${tarball.archiveName}`).join(' '),
|
|
638
992
|
'RUN package_root="/opt/agent-vm/local-packages/node_modules" && \\',
|
|
639
|
-
'
|
|
993
|
+
' global_package_root="$(pnpm root -g)" && \\',
|
|
994
|
+
' mkdir -p "$global_package_root" /home/openclaw/.openclaw/extensions && \\',
|
|
995
|
+
' ln -sfn "$package_root/@agent-vm" "$global_package_root/@agent-vm" && \\',
|
|
640
996
|
' ln -sfn "$package_root/@agent-vm/openclaw-agent-vm-plugin/dist" /home/openclaw/.openclaw/extensions/gondolin',
|
|
641
997
|
'',
|
|
642
998
|
].join('\n'), 'utf8');
|
|
@@ -644,7 +1000,7 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
644
1000
|
delete gatewayProfile.source;
|
|
645
1001
|
}
|
|
646
1002
|
finally {
|
|
647
|
-
await
|
|
1003
|
+
await removeE2eLocalPackageTarballs([
|
|
648
1004
|
localSecretManagementTarballPath,
|
|
649
1005
|
localGondolinAdapterTarballPath,
|
|
650
1006
|
localGatewayInterfaceTarballPath,
|
|
@@ -652,7 +1008,7 @@ export async function useLocalOpenClawPluginGatewayImage(options) {
|
|
|
652
1008
|
]);
|
|
653
1009
|
}
|
|
654
1010
|
}
|
|
655
|
-
export async function
|
|
1011
|
+
export async function scaffoldOpenClawE2eProject(options) {
|
|
656
1012
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix));
|
|
657
1013
|
const controllerPort = await findAvailablePort();
|
|
658
1014
|
const gatewayPort = await findAvailablePort();
|
|
@@ -667,8 +1023,8 @@ export async function scaffoldOpenClawSmokeProject(options) {
|
|
|
667
1023
|
const systemConfig = await loadSystemConfig(path.join(tempRoot, 'config', 'system.json'));
|
|
668
1024
|
systemConfig.host.controllerPort = controllerPort;
|
|
669
1025
|
systemConfig.host.projectNamespace = 'claw-tests-zone-git';
|
|
670
|
-
systemConfig.cacheDir = path.join(
|
|
671
|
-
const zone =
|
|
1026
|
+
systemConfig.cacheDir = path.join(resolveE2eCacheRoot(), 'openclaw');
|
|
1027
|
+
const zone = getOpenClawE2eZone(systemConfig);
|
|
672
1028
|
zone.gateway.port = gatewayPort;
|
|
673
1029
|
return {
|
|
674
1030
|
controllerPort,
|
|
@@ -679,23 +1035,12 @@ export async function scaffoldOpenClawSmokeProject(options) {
|
|
|
679
1035
|
};
|
|
680
1036
|
}
|
|
681
1037
|
export async function prepareLocalWorkerPackageForGatewayImage(repoRoot) {
|
|
682
|
-
await
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
cwd: path.join(repoRoot, 'packages', 'agent-vm-worker'),
|
|
686
|
-
stdio: 'pipe',
|
|
1038
|
+
return await packLocalPackageTarball({
|
|
1039
|
+
packageDirectory: path.join(repoRoot, 'packages', 'agent-vm-worker'),
|
|
1040
|
+
packageName: 'agent-vm-worker',
|
|
687
1041
|
});
|
|
688
|
-
const packedTarballs = (await fs.readdir(packDirectory)).filter((fileName) => fileName.endsWith('.tgz'));
|
|
689
|
-
const [packedTarballName] = packedTarballs;
|
|
690
|
-
if (packedTarballName === undefined) {
|
|
691
|
-
throw new Error('Failed to pack local agent-vm-worker tarball for smoke image.');
|
|
692
|
-
}
|
|
693
|
-
if (packedTarballs.length > 1) {
|
|
694
|
-
throw new Error('Expected pnpm pack to produce exactly one agent-vm-worker tarball.');
|
|
695
|
-
}
|
|
696
|
-
return path.join(packDirectory, packedTarballName);
|
|
697
1042
|
}
|
|
698
|
-
export async function
|
|
1043
|
+
export async function scaffoldWorkerE2eProject(options) {
|
|
699
1044
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), options.prefix));
|
|
700
1045
|
const controllerPort = await findAvailablePort();
|
|
701
1046
|
const gatewayPort = await findAvailablePort();
|
|
@@ -709,12 +1054,12 @@ export async function scaffoldWorkerSmokeProject(options) {
|
|
|
709
1054
|
const systemConfig = await loadSystemConfig(path.join(tempRoot, 'config', 'system.json'));
|
|
710
1055
|
systemConfig.host.controllerPort = controllerPort;
|
|
711
1056
|
systemConfig.host.projectNamespace = 'claw-tests-worker';
|
|
712
|
-
systemConfig.cacheDir = path.join(
|
|
1057
|
+
systemConfig.cacheDir = path.join(resolveE2eCacheRoot(), 'worker');
|
|
713
1058
|
systemConfig.host.secretsProvider = {
|
|
714
1059
|
type: '1password',
|
|
715
|
-
tokenSource: { type: 'env', envVar: '
|
|
1060
|
+
tokenSource: { type: 'env', envVar: 'AGENT_VM_TEST_OPENAI_API_KEY' },
|
|
716
1061
|
};
|
|
717
|
-
const zone =
|
|
1062
|
+
const zone = getWorkerE2eZone(systemConfig);
|
|
718
1063
|
zone.gateway.port = gatewayPort;
|
|
719
1064
|
return {
|
|
720
1065
|
controllerPort,
|
|
@@ -724,27 +1069,27 @@ export async function scaffoldWorkerSmokeProject(options) {
|
|
|
724
1069
|
zone,
|
|
725
1070
|
};
|
|
726
1071
|
}
|
|
727
|
-
export async function
|
|
1072
|
+
export async function scaffoldGatewayE2eProject(options) {
|
|
728
1073
|
if (options.kind === 'openclaw') {
|
|
729
|
-
return await
|
|
1074
|
+
return await scaffoldOpenClawE2eProject({
|
|
730
1075
|
architecture: options.architecture,
|
|
731
1076
|
prefix: options.prefix,
|
|
732
1077
|
zoneId: options.zoneId,
|
|
733
1078
|
...(options.agents ? { agents: options.agents } : {}),
|
|
734
1079
|
});
|
|
735
1080
|
}
|
|
736
|
-
return await
|
|
1081
|
+
return await scaffoldWorkerE2eProject({
|
|
737
1082
|
architecture: options.architecture,
|
|
738
1083
|
prefix: options.prefix,
|
|
739
1084
|
zoneId: options.zoneId,
|
|
740
1085
|
});
|
|
741
1086
|
}
|
|
742
|
-
export async function
|
|
1087
|
+
export async function writeOpenClawMcpPortalE2eConfigs(options) {
|
|
743
1088
|
await fs.writeFile(path.join(options.configDir, 'mcp.config.jsonc'), `${JSON.stringify({
|
|
744
1089
|
$schema: '../../schemas/mcp.schema.json',
|
|
745
1090
|
providers: {
|
|
746
1091
|
upstreamMock: {
|
|
747
|
-
discovery: { summary: 'Mock upstream MCP server for
|
|
1092
|
+
discovery: { summary: 'Mock upstream MCP server for e2e tests' },
|
|
748
1093
|
kind: 'mcp',
|
|
749
1094
|
namespace: options.namespace,
|
|
750
1095
|
transport: {
|
|
@@ -777,7 +1122,7 @@ export async function writeOpenClawMcpPortalSmokeConfigs(options) {
|
|
|
777
1122
|
schemaVersion: 1,
|
|
778
1123
|
}, null, '\t')}\n`, 'utf8');
|
|
779
1124
|
}
|
|
780
|
-
export async function
|
|
1125
|
+
export async function startE2eControllerRuntime(options) {
|
|
781
1126
|
const restoreEnvironment = applySmokeEnvironment(options.secrets);
|
|
782
1127
|
const secretResolver = createSmokeSecretResolver(options.secrets);
|
|
783
1128
|
const tempRoot = path.dirname(path.dirname(options.startOptions.systemConfig.systemConfigPath));
|
|
@@ -816,7 +1161,7 @@ export async function startSmokeControllerRuntime(options) {
|
|
|
816
1161
|
runtime,
|
|
817
1162
|
systemConfig: options.startOptions.systemConfig,
|
|
818
1163
|
tempRoot,
|
|
819
|
-
close: async () => {
|
|
1164
|
+
close: async (closeOptions = {}) => {
|
|
820
1165
|
const cleanupErrors = [];
|
|
821
1166
|
try {
|
|
822
1167
|
await runtime.close();
|
|
@@ -824,14 +1169,19 @@ export async function startSmokeControllerRuntime(options) {
|
|
|
824
1169
|
catch (error) {
|
|
825
1170
|
cleanupErrors.push(error);
|
|
826
1171
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1172
|
+
if (shouldCleanupE2eDockerImages({
|
|
1173
|
+
...closeOptions,
|
|
1174
|
+
env: process.env,
|
|
1175
|
+
})) {
|
|
1176
|
+
try {
|
|
1177
|
+
await removeE2eDockerImagesForSystemConfig(options.startOptions.systemConfig);
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
cleanupErrors.push(error);
|
|
1181
|
+
}
|
|
832
1182
|
}
|
|
833
1183
|
try {
|
|
834
|
-
await
|
|
1184
|
+
await removeE2eTempRoot(tempRoot);
|
|
835
1185
|
}
|
|
836
1186
|
catch (error) {
|
|
837
1187
|
cleanupErrors.push(error);
|
|
@@ -839,20 +1189,22 @@ export async function startSmokeControllerRuntime(options) {
|
|
|
839
1189
|
finally {
|
|
840
1190
|
restoreEnvironment();
|
|
841
1191
|
}
|
|
842
|
-
|
|
1192
|
+
throwIfE2eHarnessCleanupFailed(cleanupErrors);
|
|
843
1193
|
},
|
|
844
1194
|
};
|
|
845
1195
|
}
|
|
846
1196
|
catch (error) {
|
|
847
1197
|
const cleanupErrors = [error];
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1198
|
+
if (shouldCleanupE2eDockerImages({ env: process.env })) {
|
|
1199
|
+
try {
|
|
1200
|
+
await removeE2eDockerImagesForSystemConfig(options.startOptions.systemConfig);
|
|
1201
|
+
}
|
|
1202
|
+
catch (cleanupError) {
|
|
1203
|
+
cleanupErrors.push(cleanupError);
|
|
1204
|
+
}
|
|
853
1205
|
}
|
|
854
1206
|
try {
|
|
855
|
-
await
|
|
1207
|
+
await removeE2eTempRoot(tempRoot);
|
|
856
1208
|
}
|
|
857
1209
|
catch (cleanupError) {
|
|
858
1210
|
cleanupErrors.push(cleanupError);
|
|
@@ -860,8 +1212,8 @@ export async function startSmokeControllerRuntime(options) {
|
|
|
860
1212
|
finally {
|
|
861
1213
|
restoreEnvironment();
|
|
862
1214
|
}
|
|
863
|
-
|
|
1215
|
+
throwIfE2eHarnessCleanupFailed(cleanupErrors);
|
|
864
1216
|
throw error;
|
|
865
1217
|
}
|
|
866
1218
|
}
|
|
867
|
-
//# sourceMappingURL=
|
|
1219
|
+
//# sourceMappingURL=e2e-harness.js.map
|