Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion test/e2e/fixtures/clients/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

import { isAbsolute } from "node:path";

import {
buildForwardServiceArgs,
createForwardServiceTarget,
} from "../../../../src/lib/adapters/openshell/forward-service.ts";
import { buildAvailabilityProbeEnv } from "../availability-env.ts";
import {
assertStockManagedImageReceipt,
Expand All @@ -26,6 +30,7 @@ export interface HostClientOptions {

export interface ForwardListenerEvidence {
valid: boolean;
pid?: number;
identity: string;
output: string;
}
Expand Down Expand Up @@ -233,7 +238,17 @@ export class HostCliClient {
artifactName: `${artifactName}-listener-after`,
}),
]);
const expectedCommandLine = `${commandPath} --gateway nemoclaw --workspace default forward service ${sandboxName} --target-port ${port} --target-host 127.0.0.1 --local 127.0.0.1:${port}`;
const target = createForwardServiceTarget(
{
executable: commandPath,
gatewayName: "nemoclaw",
localHost: "127.0.0.1",
sandboxName,
workspace: "default",
},
Number(port),
);
const expectedCommandLine = [commandPath, ...buildForwardServiceArgs(target)].join(" ");
const afterPids = [
...new Set(
after.stdout
Expand All @@ -252,6 +267,7 @@ export class HostCliClient {
afterPids[0] === pid;
return {
valid,
...(valid ? { pid: Number(pid) } : {}),
identity,
output: probes.map(resultText).filter(Boolean).join("\n"),
};
Expand Down
40 changes: 36 additions & 4 deletions test/e2e/live/openclaw-plugin-runtime-exdev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import os from "node:os";
import path from "node:path";

import { resolveOpenshell } from "../../../src/lib/adapters/openshell/resolve.ts";
import { isLocalForwardReachable } from "../../../src/lib/actions/sandbox/forward-health.ts";
import { DASHBOARD_PORT } from "../../../src/lib/core/ports.ts";
import { waitUntil } from "../../../src/lib/core/wait.ts";
import { pullAndResolveBaseImageDigest } from "../../../src/lib/onboard/base-image.ts";
import { execTimeout, testTimeout } from "../../helpers/timeouts.ts";
import type { ArtifactSink } from "../fixtures/artifacts.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import type { CleanupRegistry } from "../fixtures/cleanup.ts";
import { terminateProcessIfRunning } from "../fixtures/cleanup-resources.ts";
import { resultText } from "../fixtures/clients/command.ts";
import type { HostCliClient } from "../fixtures/clients/host.ts";
import {
Expand Down Expand Up @@ -86,6 +90,7 @@ function liveEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
...extra,
NEMOCLAW_NON_INTERACTIVE: "1",
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1",
NEMOCLAW_DASHBOARD_PORT: String(DASHBOARD_PORT),
};
}

Expand Down Expand Up @@ -371,6 +376,7 @@ test(
`test-only driver config mounts tmpfs at ${EXDEV_TMPFS_MOUNT}`,
`sandbox proves ${EXDEV_TMPFS_SOURCE} and the OpenClaw extension target are distinct devices`,
"OpenClaw installs the weather plugin across that boundary before restart",
"the restarted dashboard forward is owned by canonical OpenShell, not the test wrapper",
],
selector: "current-lifecycle",
nemoclawSource: "current-checkout",
Expand Down Expand Up @@ -433,6 +439,11 @@ test(
host,
path.join(REPO_ROOT, "scripts", "install-openshell.sh"),
);
await host.resolveOpenShellCommandPath({
artifactName: "resolve-canonical-openshell-for-exdev-listener",
env: liveEnv(),
timeoutMs: PROBE_TIMEOUT_MS,
});
const openshellWrapper = createOpenShellTrustedImageWrapper({
driverConfigJson: EXDEV_TMPFS_DRIVER_CONFIG,
realOpenshellPath: openshell.cli,
Expand Down Expand Up @@ -527,22 +538,38 @@ test(
timeoutMs: PROBE_TIMEOUT_MS,
});
const crossDeviceInstallText = resultText(crossDeviceInstall);
expect(crossDeviceInstall.exitCode, crossDeviceInstallText).toBe(0);
const [, sourceDevice, targetDevice] =
/source_device=(\d+) target_device=(\d+)/.exec(crossDeviceInstallText) ?? [];
expect(sourceDevice, crossDeviceInstallText).not.toBe(targetDevice);
expect(
crossDeviceInstall.exitCode === 0 &&
sourceDevice !== undefined &&
targetDevice !== undefined &&
sourceDevice !== targetDevice,
crossDeviceInstallText,
).toBe(true);

progress.phase("restart the gateway and confirm the installed payload");
const restart = await host.command(
"node",
[CLI_ENTRYPOINT, SANDBOX_NAME, "gateway", "restart"],
{
artifactName: "openclaw-weather-plugin-gateway-restart",
env: sandboxEnv,
env: { ...sandboxEnv, NEMOCLAW_OPENSHELL_BIN: openshell.cli },
timeoutMs: 180_000,
},
);
expect(restart.exitCode, resultText(restart)).toBe(0);
const listenerAfterRestart = await host.inspectOpenShellForwardListener(
String(DASHBOARD_PORT),
SANDBOX_NAME,
{
artifactName: "openclaw-weather-plugin-listener-after-restart",
env: liveEnv(),
},
);
expect(
restart.exitCode === 0 && listenerAfterRestart.valid,
`${resultText(restart)}\n${listenerAfterRestart.output}`,
).toBe(true);
const weatherAfterRestart = await assertWeatherPluginRuntime(
sandbox,
"after-restart",
Expand All @@ -565,6 +592,11 @@ test(
version: "v2",
});
openshellWrapper.selectImage(pluginImageV2);
terminateProcessIfRunning(listenerAfterRestart.pid!, "SIGKILL");
expect(
waitUntil(() => !isLocalForwardReachable(DASHBOARD_PORT, 100), 5, 50),
`verified dashboard listener still owns port ${DASHBOARD_PORT} after termination`,
).toBe(true);
const recreate = await runOpenClawPluginWithFailureEvidence({
operation: "openclaw-plugin-runtime-exdev.recreate-pairing",
captureDiagnostics: capturePairingDiagnostics,
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/support/e2e-clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,28 @@ describe("E2E fixture clients", () => {

await expect(host.inspectOpenShellForwardListener("18789", "alpha")).resolves.toMatchObject({
valid: expected,
...(expected ? { pid: 4321 } : {}),
});
},
);

it("rejects a wrapper as the owner of a canonical OpenShell listener", async () => {
const runner = new FakeRunner();
runner.enqueue({ stdout: "4321\n" });
runner.enqueue({ stdout: "/tmp/openshell-wrapper\n" });
runner.enqueue({ stdout: "/opt/openshell\n" });
runner.enqueue({ stdout: "/tmp/openshell-wrapper\n" });
runner.enqueue({
stdout:
"/tmp/openshell-wrapper --gateway nemoclaw --workspace default forward service alpha --target-port 18789 --target-host 127.0.0.1 --local 127.0.0.1:18789\n",
});
runner.enqueue({ stdout: "4321\n" });

await expect(
new HostCliClient(runner).inspectOpenShellForwardListener("18789", "alpha"),
).resolves.toMatchObject({ valid: false });
});

it("composes installation, OpenShell resolution, and launch in authority order", async () => {
const runner = new FakeRunner();
runner.enqueue({ stdout: "installation complete\n" });
Expand Down
Loading