Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
2e935c7
test(e2e): fix EXDEV fixture permissions
rsliter Sep 1, 2026
b745aae
test(e2e): cover EXDEV fixture permissions
rsliter Sep 1, 2026
1bc9355
test(e2e): resume delayed EXDEV pairing
rsliter Sep 1, 2026
1b2cef9
test(e2e): reconcile delayed pairing resume
rsliter Sep 1, 2026
44df48c
test(e2e): expose fixture through policy
rsliter Sep 1, 2026
a2589be
merge: refresh main for typecheck
rsliter Sep 1, 2026
601c616
test(e2e): harden EXDEV retry handoff
rsliter Sep 2, 2026
4e2efeb
merge: refresh main for advisor fixes
rsliter Sep 2, 2026
626ee1e
merge: refresh main for CI fixes
rsliter Sep 2, 2026
7111aca
test(e2e): prove EXDEV install mutation
rsliter Sep 2, 2026
f3cd488
test(e2e): verify cross-device identity
rsliter Sep 2, 2026
858916b
test(e2e): bind EXDEV fixture identities
rsliter Sep 2, 2026
7ed4e18
docs(e2e): align EXDEV lifecycle evidence
rsliter Sep 2, 2026
7754238
test(e2e): remove redundant runtime version alias
rsliter Sep 2, 2026
6333774
test(e2e): validate EXDEV runtime anchor
rsliter Sep 2, 2026
d1bce9e
Merge remote-tracking branch 'origin/main' into codex/fix-exdev-fixtu…
rsliter Sep 2, 2026
e2996f6
Merge remote-tracking branch 'origin/main' into codex/fix-exdev-fixtu…
rsliter Sep 2, 2026
5fa7fb0
test(e2e): follow retry evidence move
rsliter Sep 2, 2026
eeb318d
Merge remote-tracking branch 'origin/main' into codex/fix-exdev-fixtu…
rsliter Sep 2, 2026
c288bc7
test(e2e): recover pairing during recreation
rsliter Sep 2, 2026
cfbfe1a
test(e2e): cover recreation retry wiring
rsliter Sep 2, 2026
6202b18
Merge remote-tracking branch 'origin/main' into codex/fix-exdev-fixtu…
rsliter Sep 2, 2026
82c5ba2
test(e2e): fail closed without diagnostics
rsliter Sep 2, 2026
ce3b1d8
Merge remote-tracking branch 'origin/main' into codex/fix-exdev-fixtu…
rsliter Sep 2, 2026
6cf5378
test(e2e): validate canonical OpenShell features
rsliter Sep 3, 2026
518f40d
Merge remote-tracking branch 'origin/main' into codex/fix-exdev-fixtu…
rsliter Sep 3, 2026
197c750
merge: refresh main into EXDEV fixture repair
rsliter Sep 3, 2026
cfd48df
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
fea5eba
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
17ebfad
test(e2e): reconcile EXDEV assertion budget
rsliter Sep 3, 2026
5cd3827
test(e2e): fail closed on EXDEV pairing gaps
rsliter Sep 3, 2026
2f25385
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
29b1421
docs(e2e): qualify EXDEV diagnostic evidence
rsliter Sep 3, 2026
74ed4ab
test(e2e): restore EXDEV fixture runtime user
rsliter Sep 3, 2026
f376db7
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
36463bd
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
9f7328e
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
334c110
test(e2e): address EXDEV review feedback
rsliter Sep 3, 2026
7259bf6
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
526ac45
merge: refresh PR #10808 from main
rsliter Sep 3, 2026
813e827
merge: refresh PR #10808 from main
rsliter Sep 4, 2026
f517e9b
merge: refresh PR #10808 from main
rsliter Sep 4, 2026
70f9154
test(e2e): simplify EXDEV failure evidence
rsliter Sep 4, 2026
ba598a2
merge: refresh PR #10808 from main
rsliter Sep 4, 2026
81ac604
test(e2e): address EXDEV advisor findings
rsliter Sep 4, 2026
a8ed89e
merge: refresh PR #10808 from main
rsliter Sep 4, 2026
3f85ed9
merge: refresh PR #10808 from main
rsliter Sep 4, 2026
d84b032
fix(e2e): bound trusted image inspection
rsliter Sep 4, 2026
4b33434
fix(e2e): retain EXDEV retry evidence
rsliter Sep 4, 2026
4a93960
merge: refresh PR #10808 from main
rsliter Sep 4, 2026
debee6f
test(e2e): simplify pairing diagnostics artifacts
rsliter Sep 4, 2026
660c45f
Merge remote-tracking branch 'origin/main' into codex/pr10808-signed-…
rsliter Sep 4, 2026
6e3dc32
test(e2e): remove duplicate EXDEV fixture payload
rsliter Sep 4, 2026
e4ded2d
Merge remote-tracking branch 'origin/main' into codex/pr10808-signed-…
rsliter Sep 4, 2026
c7fae6e
test(e2e): distinguish image inspection failures
rsliter Sep 4, 2026
89f3bc4
Merge remote-tracking branch 'origin/main' into codex/pr10808-signed-…
rsliter Sep 4, 2026
bdf5064
test(e2e): clarify EXDEV support titles
rsliter Sep 4, 2026
30c9a54
test: simplify EXDEV failure coverage
rsliter Sep 4, 2026
4037387
docs: clarify EXDEV fixture handoff
rsliter Sep 5, 2026
f902276
Merge remote-tracking branch 'origin/main' into codex/pr10808-signed-…
rsliter Sep 5, 2026
4af10ac
merge: refresh PR #10808 after final-handoff fix
rsliter Sep 9, 2026
2fd9797
docs(e2e): clarify EXDEV diagnostics trigger
rsliter Sep 9, 2026
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
1 change: 0 additions & 1 deletion src/lib/onboard/sandbox-readiness-tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ describe("createSandboxReadyWaiter", () => {
isLinuxDockerDriverGatewayEnabled: () => true,
now: () => 0,
sleep,
now: () => 0,
});

await expect(waitForSandboxReady(NAME, 2, 3)).resolves.toEqual({
Expand Down
1 change: 1 addition & 0 deletions test/e2e/RETRY_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
| Operation | Owner | Limit | Retry condition | Safety basis | Evidence |
| --- | --- | --- | --- | --- | --- |
| `external-gateway-health.tcp-readiness` | `openshell-gateway` | 10 attempts, one second apart | The newly started gateway listener rejects a TCP connection with `ECONNREFUSED` | The probe is read-only. Other errors stop without retry. The Blueprint Runner health operation runs once after the listener opens. | `external-gateway-readiness-retry.json` |
| `openclaw-plugin-runtime-exdev.onboard-pairing` | `openclaw-plugin-runtime-exdev` | Two attempts | Fresh onboarding reports that canonical CLI device pairing did not appear. | The retry uses `onboard --resume` only when OpenShell reports the sandbox as Ready, the registry resolves its current runtime identity, and the saved in-progress OpenClaw session is paused at `post_verify` for the same sandbox, gateway, and custom Dockerfile. The saved session retains mutation authority. Other errors stop without retry. | `openclaw-plugin-exdev-onboard-retry.json` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
104 changes: 104 additions & 0 deletions test/e2e/live/openclaw-plugin-runtime-exdev-onboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { type CommandExitResult, outputContainsReadySandbox } from "../fixtures/clients/command.ts";
import {
runBoundedRetry,
type BoundedRetryResult,
type RetryEvidence,
} from "../fixtures/retry-policy.ts";

const OPERATION = "openclaw-plugin-runtime-exdev.onboard-pairing";
const OWNER = "openclaw-plugin-runtime-exdev";

type OnboardPairingRetryOptions<T extends CommandExitResult> = {
sandboxName: string;
run(attempt: number): Promise<T>;
reconcile(value: T | undefined, error: unknown, attempt: number): Promise<boolean>;
onEvidence(evidence: RetryEvidence): Promise<void> | void;
};

type PairingTarget = {
gatewayName: string;
stateDirectory: string;
version: string;
};

type OnboardPairingSession = {
agent: string | null;
failure: unknown;
machine: { state: string };
metadata: { fromDockerfile: string | null; gatewayName: string };
resumable: boolean;
sandboxName: string | null;
status: string;
};

type OnboardPairingReconciliationOptions<T extends CommandExitResult> = {
expectedFromDockerfile: string;
sandboxName: string;
captureDiagnostics(): Promise<void>;
listSandbox(): Promise<T>;
loadSession(): OnboardPairingSession | null;
resolveTarget(): PairingTarget | null;
};

export function classifyOpenClawPluginOnboard<T extends CommandExitResult>(
value: T | undefined,
error: unknown,
sandboxName: string,
):
| { outcome: "passed" }
| { outcome: "failed"; failureClass: "deterministic" | "transient-external" } {
if (error === undefined && value?.exitCode === 0) return { outcome: "passed" };
const message = `OpenClaw onboarding for '${sandboxName}' is incomplete because its canonical CLI device pairing did not appear. Resume or rerun onboarding.`;
const output = value ? `${value.stdout}\n${value.stderr}` : "";
return {
outcome: "failed",
failureClass:
error === undefined && output.includes(message) ? "transient-external" : "deterministic",
};
}

/** Resume once when the startup watcher has not yet published canonical pairing. */
export function runOpenClawPluginOnboardWithPairingResume<T extends CommandExitResult>(
options: OnboardPairingRetryOptions<T>,
): Promise<BoundedRetryResult<T>> {
return runBoundedRetry({
operation: OPERATION,
owner: OWNER,
idempotence: "reconciled-mutation",
maxAttempts: 2,
run: options.run,
classify: (value, error) => classifyOpenClawPluginOnboard(value, error, options.sandboxName),
reconcile: options.reconcile,
onEvidence: options.onEvidence,
});
}

/** Require the paused finalization session and its live runtime before resume can continue. */
export async function reconcileOpenClawPluginOnboardPairing<T extends CommandExitResult>(
options: OnboardPairingReconciliationOptions<T>,
): Promise<boolean> {
await options.captureDiagnostics();
const list = await options.listSandbox();
if (list.exitCode !== 0 || !outputContainsReadySandbox(list, options.sandboxName)) return false;
try {
const target = options.resolveTarget();
if (!target) return false;
const session = options.loadSession();
return (
session !== null &&
session.status === "in_progress" &&
session.resumable === true &&
session.failure === null &&
session.sandboxName === options.sandboxName &&
session.agent === "openclaw" &&
session.machine.state === "post_verify" &&
session.metadata.gatewayName === target.gatewayName &&
session.metadata.fromDockerfile === options.expectedFromDockerfile
);
} catch {
return false;
}
}
58 changes: 58 additions & 0 deletions test/e2e/live/openclaw-plugin-runtime-exdev-trusted-prebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,64 @@ import {

export const DELEGATED_CAPABILITY_COMMENT_PREFIX =
"# TEST-ONLY delegated-capability marker from validated canonical OpenShell: ";
export const TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR =
"/usr/local/share/nemoclaw-e2e/weather-plugin";

export function createTrustedPluginFixtureDockerfile(options: {
pluginDirName: string;
source: string;
versionSourceName: string;
}): string {
const runtimeAnchor = "FROM ${BASE_IMAGE}\n";
const runtime = options.source.replace(
runtimeAnchor,
"FROM ${BASE_IMAGE} AS nemoclaw-runtime\n",
);
const extension = String.raw`

# Build the deterministic custom-plugin fixture used by this live contract.
FROM builder AS weather-plugin-builder
WORKDIR /opt/weather
COPY ${options.pluginDirName}/package.json ${options.pluginDirName}/package-lock.json ${options.pluginDirName}/tsconfig.json ./
RUN npm ci --ignore-scripts --no-audit --no-fund
COPY ${options.pluginDirName}/openclaw.plugin.json ./
COPY ${options.pluginDirName}/src/ ./src/
COPY ${options.versionSourceName} ./src/version.ts
RUN npm run build \
&& npm prune --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund

# Extend the completed managed runtime so its entrypoint, health check, config
# generation, and permissions remain the source of truth.
FROM nemoclaw-runtime AS weather-runtime
ARG NEMOCLAW_TOOL_DISCLOSURE=progressive
ENV NEMOCLAW_TOOL_DISCLOSURE=${"${NEMOCLAW_TOOL_DISCLOSURE}"}
COPY --from=weather-plugin-builder --chown=sandbox:sandbox \
/opt/weather/package.json \
/opt/weather/package-lock.json \
/opt/weather/openclaw.plugin.json \
${TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR}/
COPY --from=weather-plugin-builder --chown=sandbox:sandbox \
/opt/weather/dist/ ${TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR}/dist/
COPY --from=weather-plugin-builder --chown=sandbox:sandbox \
/opt/weather/node_modules/ ${TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR}/node_modules/

USER sandbox
RUN HOME=/sandbox openclaw plugins install ${TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR} \
&& HOME=/sandbox openclaw plugins enable weather

# Enabling the plugin changes openclaw.json after the managed runtime hashes it.
# The runtime test copies this fixture from OpenShell's read-only /usr policy tree
# into tmpfs after the sandbox starts.
# hadolint ignore=DL3002
USER root
RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \
&& chmod 660 /sandbox/.openclaw/openclaw.json \
&& sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \
&& chown sandbox:sandbox /sandbox/.openclaw/.config-hash \
&& chmod 660 /sandbox/.openclaw/.config-hash
`;
return runtime.trimEnd() + extension;
}

const TRUSTED_EXDEV_IMAGE_REF_PATTERN = new RegExp(
`^${LOCAL_SANDBOX_IMAGE_REPO}:[a-z0-9_][a-z0-9_.-]{0,127}$`,
Expand Down
146 changes: 74 additions & 72 deletions test/e2e/live/openclaw-plugin-runtime-exdev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import os from "node:os";
import path from "node:path";

import { resolveOpenshell } from "../../../src/lib/adapters/openshell/resolve.ts";
import { resolveOrdinaryOpenClawPairingTarget } from "../../../src/lib/actions/sandbox/launch-readiness.ts";
import { pullAndResolveBaseImageDigest } from "../../../src/lib/onboard/base-image.ts";
import { loadSession } from "../../../src/lib/state/onboard-session.ts";
import type { ArtifactSink } from "../fixtures/artifacts.ts";
import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import type { CleanupRegistry } from "../fixtures/cleanup.ts";
Expand All @@ -21,13 +23,20 @@ import {
} from "../fixtures/clients/sandbox.ts";
import { expect, test } from "../fixtures/e2e-test.ts";
import { startFakeOpenAiCompatibleServer } from "../fixtures/fake-openai-compatible.ts";
import { captureIssue4462FailureDiagnostics } from "../fixtures/issue-4462-diagnostics.ts";
import { CLI_ENTRYPOINT, REPO_ROOT } from "../fixtures/paths.ts";
import type { TestProgress } from "../fixtures/progress.ts";
import { parseJsonFromText } from "./json-envelope.ts";
import {
reconcileOpenClawPluginOnboardPairing,
runOpenClawPluginOnboardWithPairingResume,
} from "./openclaw-plugin-runtime-exdev-onboard.ts";
import {
buildTrustedPluginFixtureImage,
createOpenShellTrustedImageWrapper,
createTrustedPluginFixtureDockerfile,
registerTrustedPluginFixtureImageCleanup,
TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR,
} from "./openclaw-plugin-runtime-exdev-trusted-prebuild.ts";
import {
type OpenShellComponents,
Expand All @@ -40,7 +49,6 @@ import {
// boundary, and prove it survives restart and recreation.

const WEATHER_FIXTURE_DIR = path.join(REPO_ROOT, "test/e2e/fixtures/plugins/weather");
const TOOL_DISCLOSURE_ENV_REFERENCE = "${NEMOCLAW_TOOL_DISCLOSURE}";
const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-oc-exdev";
const ONBOARD_TIMEOUT_MS = 25 * 60_000;
const PROBE_TIMEOUT_MS = 60_000;
Expand Down Expand Up @@ -185,57 +193,20 @@ function writeCustomPluginVersion(
function createCustomPluginDockerfile(context: CustomPluginBuildContext): void {
const sourceDockerfile = path.join(context.sourceRoot, "Dockerfile");
const source = fs.readFileSync(sourceDockerfile, "utf8");
const runtimeAnchor = "FROM ${BASE_IMAGE}\n";
const runtime = source.replace(runtimeAnchor, "FROM ${BASE_IMAGE} AS nemoclaw-runtime\n");
const pluginDirName = path.basename(context.pluginDirPath);
const versionSourceName = path.basename(context.versionSourcePath);
const extension = String.raw`

# Build the deterministic custom-plugin fixture used by this live contract.
FROM builder AS weather-plugin-builder
WORKDIR /opt/weather
COPY ${pluginDirName}/package.json ${pluginDirName}/package-lock.json ${pluginDirName}/tsconfig.json ./
RUN npm ci --ignore-scripts --no-audit --no-fund
COPY ${pluginDirName}/openclaw.plugin.json ./
COPY ${pluginDirName}/src/ ./src/
COPY ${versionSourceName} ./src/version.ts
RUN npm run build \
&& npm prune --omit=dev --omit=peer --ignore-scripts --no-audit --no-fund

# Extend the completed managed runtime so its entrypoint, health check, config
# generation, and permissions remain the source of truth.
FROM nemoclaw-runtime AS weather-runtime
ARG NEMOCLAW_TOOL_DISCLOSURE=progressive
ENV NEMOCLAW_TOOL_DISCLOSURE=${TOOL_DISCLOSURE_ENV_REFERENCE}
COPY --from=weather-plugin-builder --chown=sandbox:sandbox \
/opt/weather/package.json \
/opt/weather/package-lock.json \
/opt/weather/openclaw.plugin.json \
/opt/weather-plugin/
COPY --from=weather-plugin-builder --chown=sandbox:sandbox \
/opt/weather/dist/ /opt/weather-plugin/dist/
COPY --from=weather-plugin-builder --chown=sandbox:sandbox \
/opt/weather/node_modules/ /opt/weather-plugin/node_modules/

USER sandbox
RUN HOME=/sandbox openclaw plugins install /opt/weather-plugin \
&& HOME=/sandbox openclaw plugins enable weather

# Enabling the plugin changes openclaw.json after the managed runtime hashes it.
# hadolint ignore=DL3002
USER root
RUN chown sandbox:sandbox /sandbox/.openclaw/openclaw.json \
&& chmod 660 /sandbox/.openclaw/openclaw.json \
&& sha256sum /sandbox/.openclaw/openclaw.json > /sandbox/.openclaw/.config-hash \
&& chown sandbox:sandbox /sandbox/.openclaw/.config-hash \
&& chmod 660 /sandbox/.openclaw/.config-hash
`;
stageWeatherPluginFixture(context);
writeCustomPluginVersion(context.versionSourcePath, "v1", true);
fs.writeFileSync(context.dockerfilePath, runtime.trimEnd() + extension, {
encoding: "utf8",
flag: "wx",
});
fs.writeFileSync(
context.dockerfilePath,
createTrustedPluginFixtureDockerfile({
pluginDirName: path.basename(context.pluginDirPath),
source,
versionSourceName: path.basename(context.versionSourcePath),
}),
{
encoding: "utf8",
flag: "wx",
},
);
}

type GatewayToolInvocation = {
Expand Down Expand Up @@ -288,7 +259,7 @@ async function assertWeatherPluginRuntime(
const crossDevicePluginInstallSource = `set -eu
rm -rf ${EXDEV_TMPFS_SOURCE}
mkdir -p ${EXDEV_TMPFS_SOURCE} /sandbox/.openclaw/extensions
cp -R /opt/weather-plugin/. ${EXDEV_TMPFS_SOURCE}/
cp -R ${TRUSTED_PLUGIN_FIXTURE_IMAGE_DIR}/. ${EXDEV_TMPFS_SOURCE}/
source_device=$(stat -c '%d' ${EXDEV_TMPFS_SOURCE})
target_device=$(stat -c '%d' /sandbox/.openclaw/extensions)
printf 'source_device=%s target_device=%s\n' "$source_device" "$target_device"
Expand Down Expand Up @@ -515,27 +486,58 @@ test(
version: "v1",
});
openshellWrapper.selectImage(pluginImageV1);
const onboard = await host.command(
"node",
[
CLI_ENTRYPOINT,
"onboard",
"--fresh",
"--non-interactive",
"--yes-i-accept-third-party-software",
"--agent",
"openclaw",
"--from",
customPluginContext.dockerfilePath,
],
{
artifactName: "openclaw-plugin-exdev-onboard",
env: sandboxEnv,
timeoutMs: ONBOARD_TIMEOUT_MS,
const onboard = await runOpenClawPluginOnboardWithPairingResume({
sandboxName: SANDBOX_NAME,
run: (attempt) =>
host.command(
"node",
attempt === 1
? [
CLI_ENTRYPOINT,
"onboard",
"--fresh",
"--non-interactive",
"--yes-i-accept-third-party-software",
"--agent",
"openclaw",
"--from",
customPluginContext.dockerfilePath,
]
: [CLI_ENTRYPOINT, "onboard", "--resume", "--non-interactive"],
{
artifactName:
attempt === 1
? "openclaw-plugin-exdev-onboard"
: "openclaw-plugin-exdev-onboard-pairing-resume",
env: sandboxEnv,
timeoutMs: ONBOARD_TIMEOUT_MS,
},
),
reconcile: () =>
reconcileOpenClawPluginOnboardPairing({
expectedFromDockerfile: customPluginContext.dockerfilePath,
sandboxName: SANDBOX_NAME,
captureDiagnostics: () =>
captureIssue4462FailureDiagnostics(sandbox, {
env: sandboxEnv,
redactionValues: ["nemoclaw-exdev-dummy-key"],
sandboxName: SANDBOX_NAME,
}),
listSandbox: () =>
sandbox.list({
artifactName: "openclaw-plugin-exdev-onboard-pairing-reconcile",
env: sandboxEnv,
timeoutMs: PROBE_TIMEOUT_MS,
}),
loadSession,
resolveTarget: () => resolveOrdinaryOpenClawPairingTarget(SANDBOX_NAME),
}),
onEvidence: async (evidence) => {
await artifacts.writeJson("openclaw-plugin-exdev-onboard-retry.json", evidence);
Comment thread
rsliter marked this conversation as resolved.
Resolved
},
);
const onboardText = resultText(onboard);
expect(onboard.exitCode, onboardText).toBe(0);
});
const onboardText = onboard.value ? resultText(onboard.value) : "onboard returned no result";
expect(onboard.outcome, onboardText).toBe("passed");
const weatherAfterOnboard = await assertWeatherPluginRuntime(sandbox, "after-onboard", "v1");

progress.phase("install plugin v1 across filesystems");
Expand Down Expand Up @@ -605,7 +607,7 @@ test(

await artifacts.target.complete({
id: "openclaw-plugin-runtime-exdev",
onboardExitCode: onboard.exitCode,
onboardExitCode: onboard.value!.exitCode,
crossDeviceInstallExitCode: crossDeviceInstall.exitCode,
restartExitCode: restart.exitCode,
recreateExitCode: recreate.exitCode,
Expand Down
Loading
Loading