Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
055f2bc
fix(e2e): build changed sandbox images locally
sandl99 Aug 25, 2026
723a32b
fix(e2e): complete local image qualification
sandl99 Aug 25, 2026
6a55a5f
refactor(e2e): share image input matching
sandl99 Aug 25, 2026
72e7af8
fix(e2e): route Pi to its candidate image
sandl99 Aug 25, 2026
f151510
fix(onboard): report pairing settlement failures
sandl99 Aug 25, 2026
c1e3570
fix(e2e): preserve local DCode base resolution
sandl99 Aug 25, 2026
f340c61
fix(onboard): consume pairing producer receipts
sandl99 Aug 25, 2026
c0d08cd
test(e2e): use valid vLLM storage GPU UUID
sandl99 Aug 25, 2026
5acda20
Merge remote-tracking branch 'origin/main' into fix/e2e-local-build-f…
sandl99 Aug 25, 2026
51d0933
fix(e2e): use private managed-image catalog writer
sandl99 Aug 25, 2026
b8c4387
test(e2e): remove duplicate managed-image cases
sandl99 Aug 25, 2026
a1b9a0a
test(e2e): cover renamed managed-image inputs
sandl99 Aug 25, 2026
88ab371
refactor(e2e): remove unrelated pairing changes
sandl99 Aug 25, 2026
4d2df93
test(e2e): cover PR revision guards
sandl99 Aug 25, 2026
d73cd20
fix(e2e): qualify local images on arm64
sandl99 Aug 25, 2026
80f0aba
test(e2e): set multiarch workload source
sandl99 Aug 25, 2026
95cd106
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 26, 2026
6b980cc
fix(onboard): restore local build fallback
sandl99 Aug 26, 2026
65ed831
fix(e2e): harden managed image selection
sandl99 Aug 26, 2026
39d60e4
fix(e2e): qualify local images on protected runtime
sandl99 Aug 26, 2026
b188ad6
fix(mcp): wait for final provider revision
sandl99 Aug 26, 2026
12fe0e8
fix(e2e): resolve local Dockerfile from agent
sandl99 Aug 26, 2026
087f361
fix(onboard): wait for canonical pairing before warmup
sandl99 Aug 26, 2026
b49077a
docs(e2e): qualify GitHub rate-limit retries
sandl99 Aug 26, 2026
82206e4
chore(mcp): leave provider revision fix to owning PR
sandl99 Aug 26, 2026
66fd3ed
chore(onboard): leave pairing settlement unchanged
sandl99 Aug 26, 2026
2513729
merge(main): synchronize current base
sandl99 Aug 26, 2026
b6f45b2
fix(e2e): preserve local base identity on GPU
sandl99 Aug 26, 2026
272b372
fix(e2e): keep workload selection acyclic
sandl99 Aug 26, 2026
f560f03
fix(ci): resolve managed image verification failures
sandl99 Aug 26, 2026
b425a07
Merge remote-tracking branch 'origin/main' into fix/e2e-local-build-f…
sandl99 Aug 26, 2026
6b57594
test(ci): remove duplicate advisor watch trigger
sandl99 Aug 26, 2026
993c194
chore(mcp): remove unrelated credential revision fix
sandl99 Aug 26, 2026
9259f23
chore(ci): leave advisor trigger fix to owner
sandl99 Aug 26, 2026
44026cd
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 26, 2026
649968b
fix(e2e): bind workload selection to commit trees
sandl99 Aug 26, 2026
1effafb
merge(main): synchronize current base
sandl99 Aug 26, 2026
7dee76e
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
8803497
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
577fe69
Merge branch 'main' into fix/e2e-local-build-for-image-changes
prekshivyas Aug 26, 2026
4f2407a
merge(main): resolve PR #10263 conflicts
sandl99 Aug 28, 2026
4661092
Merge remote-tracking branch 'origin/main' into fix/e2e-local-build-f…
sandl99 Aug 28, 2026
e1b8531
refactor(e2e): remove unused PR image catalog path
sandl99 Aug 28, 2026
a2b575e
fix(e2e): retain managed image catalog assembler
sandl99 Aug 28, 2026
0847dd9
refactor(e2e): remove obsolete DCode publication path
sandl99 Aug 28, 2026
20a39f1
fix(e2e): retain trusted base for local builds
sandl99 Aug 28, 2026
fecee10
refactor(e2e): remove unused publication outputs
sandl99 Aug 28, 2026
cdf63ce
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 28, 2026
86bcb5b
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 28, 2026
5994ff0
fix(e2e): bind image selection to commit trees
sandl99 Aug 29, 2026
f39859c
merge(main): synchronize current base
sandl99 Aug 29, 2026
6f7f097
test(e2e): map live helpers to existing coverage
sandl99 Aug 29, 2026
d9331e3
test(onboard): remove duplicate activation case
sandl99 Aug 29, 2026
363bbea
merge: resolve conflicts with main
github-actions[bot] Aug 30, 2026
f977478
merge: resolve conflicts with main
github-actions[bot] Aug 31, 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
46 changes: 4 additions & 42 deletions .github/workflows/e2e-standard-profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ on:
cli_artifact_provenance:
required: true
type: string
managed_image_catalog:
required: true
type: string
managed_image_revision:
required: true
type: string
managed_image_receipt:
required: true
type: string
workload_source:
required: true
type: string
credential_boundary:
required: true
type: string
Expand Down Expand Up @@ -113,6 +113,7 @@ jobs:
env:
E2E_JOB: "1"
E2E_MANAGED_IMAGE_REVISION: ${{ inputs.managed_image_revision }}
E2E_WORKLOAD_SOURCE: ${{ inputs.workload_source }}
E2E_TARGET_ID: ${{ inputs.target_id }}
NEMOCLAW_RUN_LIVE_E2E: "1"
NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.candidate_sha }}
Expand Down Expand Up @@ -400,45 +401,6 @@ jobs:
with:
provenance-json: ${{ inputs.cli_artifact_provenance }}

- name: Materialize temporary managed-image catalog
if: ${{ inputs.managed_image_catalog != '' }}
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
env:
CANDIDATE_SHA: ${{ inputs.candidate_sha }}
MANAGED_IMAGE_CATALOG: ${{ inputs.managed_image_catalog }}
RESTORE_CLI: ${{ inputs.restore_cli && 'true' || 'false' }}
run: |
set -euo pipefail
catalog_path="${RUNNER_TEMP}/e2e-managed-image-catalog.json"
jq -e --arg revision "$CANDIDATE_SHA" '
type == "object" and length > 0 and
all(.[];
.source.revision == $revision and
(.source.release | type == "string" and length > 0) and
(.source.cohort | type == "string" and length > 0)
) and
([.[].source.release] | unique | length) == 1 and
([.[].source.cohort] | unique | length) == 1
' <<<"$MANAGED_IMAGE_CATALOG" >/dev/null || {
echo "::error::managed-image catalog source identity does not match the candidate" >&2
exit 1
}
if [[ "$RESTORE_CLI" == "true" ]]; then
candidate_release="v$(jq -r '.nemoclawVersion' dist/build-identity.json)"
jq -e --arg release "$candidate_release" '
all(.[]; .source.release == $release)
' <<<"$MANAGED_IMAGE_CATALOG" >/dev/null || {
echo "::error::managed-image catalog release does not match the restored CLI" >&2
exit 1
}
fi
jq -c . <<<"$MANAGED_IMAGE_CATALOG" >"$catalog_path"
[[ -s "$catalog_path" && ! -L "$catalog_path" ]] || {
echo "::error::temporary managed-image catalog is invalid" >&2
exit 1
}
printf 'NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG=%s\n' "$catalog_path" >>"$GITHUB_ENV"

- name: Install reviewed cloudflared
if: ${{ inputs.cloudflared }}
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
Expand Down
16 changes: 4 additions & 12 deletions src/lib/onboard/managed-workload/onboard-orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,15 @@ describe("managed workload onboard orchestration", () => {
).toBe(false);
});

it("keeps stock managed images required during providerless interceptor creation (#9833)", () => {
expect(
shouldActivateStockManagedRuntime({
portableLifecycle: false,
hermesPortableLifecycle: false,
agentName: "openclaw",
}),
).toBe(true);
});

it("rejects an unavailable catalog for stock managed-image onboarding", async () => {
it("uses the Dockerfile when the stock managed-image catalog is unavailable", async () => {
const { runtime } = createFreshOnboardingRuntime(
{},
{ stockManagedRuntime: true, unavailableCatalog: true },
);

await expect(runtime.ensurePreparedWorkload()).rejects.toThrow("registry offline");
await expect(runtime.ensurePreparedWorkload()).resolves.toMatchObject({
source: { kind: "legacy-dockerfile" },
});
});

it("rejects an unavailable catalog for explicit temporary managed-image onboarding", async () => {
Expand Down
5 changes: 4 additions & 1 deletion test/e2e/fixtures/managed-image-receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,11 @@ export function assertStockManagedImageReceipt(options: {
readonly environment?: NodeJS.ProcessEnv;
readonly expectedAgent?: string;
readonly sandboxName: string;
}): StockManagedImageReceiptEvidence {
}): StockManagedImageReceiptEvidence | null {
const environment = options.environment ?? process.env;
const workloadSource =
environment.E2E_WORKLOAD_SOURCE?.trim() ?? process.env.E2E_WORKLOAD_SOURCE?.trim();
if (workloadSource === "local-dockerfile") return null;
const revision = selectedManagedImageRevision(environment);
const home = environment.HOME?.trim() || os.homedir();
const registryPath = path.join(
Expand Down
17 changes: 11 additions & 6 deletions test/e2e/fixtures/phases/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,16 @@ export class OnboardingPhaseFixture {
);
}
const sandboxName = sandboxNameFromOptions(environment.onboarding, options);
const baseImageReference = requireDcodeBaseImageReference(
options.dcodeBaseImageReference === undefined
? process.env
: { [DCODE_BASE_IMAGE_ENV]: options.dcodeBaseImageReference },
);
const localDockerfile =
options.dcodeBaseImageReference === undefined &&
process.env.E2E_WORKLOAD_SOURCE === "local-dockerfile";
const baseImageReference = localDockerfile
? undefined
: requireDcodeBaseImageReference(
options.dcodeBaseImageReference === undefined
? process.env
: { [DCODE_BASE_IMAGE_ENV]: options.dcodeBaseImageReference },
);
const apiKey = this.secrets.required("NVIDIA_INFERENCE_API_KEY");
this.registerSandboxCleanup(sandboxName);
const result = await this.host.nemoclaw([...ONBOARD_ARGS, "--observability"], {
Expand All @@ -282,7 +287,7 @@ export class OnboardingPhaseFixture {
NEMOCLAW_PREFERRED_API: process.env.NEMOCLAW_PREFERRED_API || "openai-completions",
NVIDIA_INFERENCE_API_KEY: apiKey,
[HOSTED_INFERENCE_CREDENTIAL_ENV]: apiKey,
[DCODE_BASE_IMAGE_ENV]: baseImageReference,
...(baseImageReference ? { [DCODE_BASE_IMAGE_ENV]: baseImageReference } : {}),
}),
redactionValues: [apiKey],
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
Expand Down
30 changes: 28 additions & 2 deletions test/e2e/fixtures/shell-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import type { ArtifactSink } from "./artifacts.ts";
import { loadAgent } from "../../../src/lib/agent/defs.ts";
import {
CANDIDATE_AGENT_FEATURE_ENV,
CANDIDATE_QUALIFICATION_RECEIPT_ENV,
} from "../../../src/lib/agent/candidate.ts";
import { CUA_FEATURE_ENV } from "../../../src/lib/cua/feature.ts";
import { type ChildProcessProgress, spawnObservedChild } from "./observed-child-process.ts";
import { superviseChild } from "./shell/supervisor.ts";
import type { TrustedShellCommand } from "./shell/trusted-command.ts";
Expand Down Expand Up @@ -41,6 +47,26 @@ export interface ShellProbeOutputEvent {
export type { TrustedShellCommand, TrustedShellCommandInput } from "./shell/trusted-command.ts";
export { trustedShellCommand } from "./shell/trusted-command.ts";

export function resolveLiveE2eWorkloadSourceEnv(input: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const targetId = input.E2E_TARGET_ID ?? process.env.E2E_TARGET_ID;
const source = input.E2E_WORKLOAD_SOURCE ?? process.env.E2E_WORKLOAD_SOURCE;
if (!targetId || source !== "local-dockerfile" || input.NEMOCLAW_FROM_DOCKERFILE) return input;
const agentName = input.NEMOCLAW_AGENT ?? process.env.NEMOCLAW_AGENT ?? "openclaw";
const agent = loadAgent(agentName, {
[CANDIDATE_AGENT_FEATURE_ENV]:
input[CANDIDATE_AGENT_FEATURE_ENV] ?? process.env[CANDIDATE_AGENT_FEATURE_ENV],
[CANDIDATE_QUALIFICATION_RECEIPT_ENV]:
input[CANDIDATE_QUALIFICATION_RECEIPT_ENV] ??
process.env[CANDIDATE_QUALIFICATION_RECEIPT_ENV],
[CUA_FEATURE_ENV]: input[CUA_FEATURE_ENV] ?? process.env[CUA_FEATURE_ENV],
});
const dockerfilePath = agent.dockerfilePath ?? agent.legacyPaths?.dockerfile;
if (!dockerfilePath) {
throw new Error(`Agent '${agent.name}' has no Dockerfile for local E2E workload source.`);
}
return { ...input, NEMOCLAW_FROM_DOCKERFILE: dockerfilePath };
}

export interface ShellProbeResult {
command: string[];
/** Wall-clock command duration, persisted for CI bottleneck analysis. */
Expand Down Expand Up @@ -216,10 +242,10 @@ export class ShellProbe {
const startedAtMs = Date.now();
const commandOutputObserver =
options.onOutput === this.progress.onOutput ? undefined : options.onOutput;
const commandEnv: NodeJS.ProcessEnv = {
const commandEnv = resolveLiveE2eWorkloadSourceEnv({
...(process.env.PATH === undefined ? {} : { PATH: process.env.PATH }),
...(options.env ?? {}),
};
});
const child = spawnObservedChild(command, args, {
activityLabel: `command: ${activityName}`,
progress: this.progress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { spawnObservedChild } from "../fixtures/observed-child-process.ts";
import { REPO_ROOT } from "../fixtures/paths.ts";
import type { TestProgress, TestProgressCapability } from "../fixtures/progress.ts";
import { redactString } from "../fixtures/redaction.ts";
import { resolveLiveE2eWorkloadSourceEnv } from "../fixtures/shell-probe.ts";
import {
projectRawOutputForArtifact,
type RawArtifactOutputMode,
Expand Down Expand Up @@ -95,7 +96,7 @@ export async function runRawCommand(
spawn: {
cwd: options.cwd ?? REPO_ROOT,
detached: true,
env: { ...(options.env ?? {}) },
env: resolveLiveE2eWorkloadSourceEnv({ ...(options.env ?? {}) }),
stdio: ["ignore", "pipe", "pipe"],
},
});
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/live/dashboard-connect-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
spawnObservedChild,
} from "../fixtures/observed-child-process.ts";
import { REPO_ROOT } from "../fixtures/paths.ts";
import { resolveLiveE2eWorkloadSourceEnv } from "../fixtures/shell-probe.ts";
import { dashboardRemoteBindConnectStarted } from "./dashboard-remote-bind-env.ts";

const CONNECT_CAPTURE_LIMIT_BYTES = 1024 * 1024;
Expand Down Expand Up @@ -87,7 +88,7 @@ export async function runDashboardConnectUntilForwardHandoff(
spawn: {
cwd: REPO_ROOT,
detached: true,
env: { ...options.env },
env: resolveLiveE2eWorkloadSourceEnv({ ...options.env }),
stdio: ["ignore", "pipe", "pipe"],
},
});
Expand Down
4 changes: 4 additions & 0 deletions test/e2e/live/full-e2e-workload-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export function readFullE2eColdWorkloadEvidence(
usedBuildKitPrebuild: boolean,
environment: NodeJS.ProcessEnv = process.env,
) {
if (environment.E2E_WORKLOAD_SOURCE === "local-dockerfile") {
return { kind: "legacy-dockerfile" } as const;
}
if (usedBuildKitPrebuild) {
throw new Error("managed-image cold onboarding must not use a local BuildKit prebuild");
}
Expand All @@ -16,6 +19,7 @@ export function readFullE2eColdWorkloadEvidence(
expectedAgent: "openclaw",
sandboxName,
});
if (!receipt) throw new Error("managed-image cold onboarding receipt is missing");
return {
kind: "managed-image",
reference: receipt.reference,
Expand Down
6 changes: 5 additions & 1 deletion test/e2e/mock-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
{
"live": "test/e2e/live/full-e2e.test.ts",
"liveSources": [
"test/e2e/live/full-e2e-workload-evidence.ts",
"test/e2e/live/agent-turn-latency-helpers.ts",
"test/e2e/live/full-e2e-inference-probe.ts"
],
Expand All @@ -151,6 +152,7 @@
"test/e2e/support/openclaw-agent-output.test.ts",
"test/e2e/support/e2e-cleanup-resources.test.ts",
"test/e2e/support/e2e-clients.test.ts",
"test/e2e/support/managed-image-receipt.test.ts",
"test/helpers/e2e-answer-assertions.test.ts"
]
},
Expand Down Expand Up @@ -255,7 +257,8 @@
},
{
"live": "test/e2e/live/dashboard-remote-bind.test.ts",
"liveOnlyReason": "Requires a real OpenShell dashboard forward on a remote Brev host to prove all-interface bind behavior after background connect."
"liveSources": ["test/e2e/live/dashboard-connect-handoff.ts"],
"fast": ["test/e2e/support/dashboard-connect-handoff.test.ts"]
},
{
"live": "test/e2e/live/sandbox-rlimits-connect.test.ts",
Expand Down Expand Up @@ -328,6 +331,7 @@
},
{
"live": "test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts",
"liveSources": ["test/e2e/live/bedrock-runtime-compatible-anthropic-raw-command.ts"],
"fast": [
"test/e2e/support/bedrock-runtime-compatible-anthropic-progress.test.ts",
"test/e2e/support/e2e-cleanup-resources.test.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ afterEach(async () => {
});

describe("Bedrock raw-command progress", () => {
it("reports timestamp-only output activity without forwarding child payloads", async () => {
it("applies the local Dockerfile and reports timestamp-only output activity", async () => {
const secret = "opaque-bedrock-progress-secret";
const expectedDockerfile = path.resolve("Dockerfile");
const expectedOutput = `${secret}:${expectedDockerfile}`;
const artifacts = await artifactSink("bedrock-progress-output");
const observation = progressProbe();
const { progress } = observation;
Expand All @@ -64,19 +66,25 @@ describe("Bedrock raw-command progress", () => {
process.execPath,
[
"-e",
"process.stdout.write(process.env.BEDROCK_TEST_SECRET); process.stderr.write('stderr-ready')",
"process.stdout.write(`${process.env.BEDROCK_TEST_SECRET}:${process.env.NEMOCLAW_FROM_DOCKERFILE}`); process.stderr.write('stderr-ready')",
],
{
artifactName: "bedrock-progress-output",
artifacts,
env: { ...process.env, BEDROCK_TEST_SECRET: secret },
env: {
...process.env,
BEDROCK_TEST_SECRET: secret,
E2E_TARGET_ID: "bedrock-runtime-compatible-anthropic",
E2E_WORKLOAD_SOURCE: "local-dockerfile",
NEMOCLAW_AGENT: "openclaw",
},
progress,
redactionValues: [secret],
},
);

expect(result.exitCode).toBe(0);
expect(result.stdout).toBe(secret);
expect(result.stdout).toBe(expectedOutput);
observation.timers[0]?.();
expect(observation.lines.at(-1)).toContain("no active command");
expect(observation.lines).toEqual(
Expand All @@ -95,7 +103,7 @@ describe("Bedrock raw-command progress", () => {
path.join(artifacts.rootDir, "raw-shell/bedrock-progress-output.stdout.txt"),
"utf8",
),
).resolves.toBe("[REDACTED]");
).resolves.toBe(`[REDACTED]:${expectedDockerfile}`);
});

it("emits an immediate content-free timeout event and closes command activity", async () => {
Expand Down
16 changes: 13 additions & 3 deletions test/e2e/support/dashboard-connect-handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,25 @@ async function stopFixtureProcess(pid: number): Promise<void> {
expect(processExists(pid)).toBe(false);
}

test("accepts a normally completed connect when the forward is already healthy", async ({
test("accepts a normally completed connect with the local Dockerfile workload", async ({
artifacts,
progress,
}) => {
const result = await runDashboardConnectUntilForwardHandoff({
artifacts,
command: [process.execPath, "-e", "process.exit(0)"],
command: [
process.execPath,
"-e",
"process.exit(process.env.NEMOCLAW_FROM_DOCKERFILE === process.argv[1] ? 0 : 1)",
path.resolve("Dockerfile"),
],
dashboardPort: DASHBOARD_PORT,
env: process.env,
env: {
...process.env,
E2E_TARGET_ID: "dashboard-remote-bind",
E2E_WORKLOAD_SOURCE: "local-dockerfile",
NEMOCLAW_AGENT: "openclaw",
},
progress,
sandboxName: SANDBOX_NAME,
timeoutMs: 2_000,
Expand Down
9 changes: 6 additions & 3 deletions test/e2e/support/managed-image-receipt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,15 @@ describe("stock E2E managed-image receipt assertion", () => {
).toMatchObject({ agent: "openclaw", sourceRevision: REVISION });
});

it("uses the trusted candidate catalog for full E2E workload evidence", () => {
it("records local Dockerfile evidence without a managed-image receipt", () => {
const home = writeRegistry(managedReceipt());

expect(
readFullE2eColdWorkloadEvidence(SANDBOX_NAME, false, candidateCatalogEnvironment(home)),
).toMatchObject({ kind: "managed-image", sourceRevision: REVISION });
readFullE2eColdWorkloadEvidence(SANDBOX_NAME, false, {
...candidateCatalogEnvironment(home),
E2E_WORKLOAD_SOURCE: "local-dockerfile",
}),
).toMatchObject({ kind: "legacy-dockerfile" });
});

it("rejects a candidate catalog whose source revision differs from the exact candidate revision", () => {
Expand Down
Loading