Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c55480c
fix(ci): isolate platform compatibility tests
rsliter Sep 8, 2026
17f4d04
merge: refresh platform CI fix from main
rsliter Sep 8, 2026
e8cc4d9
fix(ci): scope platform package credentials
rsliter Sep 8, 2026
8682e61
fix(ci): declare platform test prerequisites
rsliter Sep 8, 2026
b408fb8
fix(ci): align gateway TLS runtime fixture
rsliter Sep 8, 2026
d85798f
Merge branch 'main' into codex/fix-11208-platform-ci
rsliter Sep 8, 2026
191a5bb
fix(ci): address platform review findings
rsliter Sep 8, 2026
b45a32d
fix(ci): preserve trusted gateway runtime template
rsliter Sep 8, 2026
d1ed448
fix(ci): verify runner GNU tar before Homebrew
rsliter Sep 8, 2026
169fc3c
fix(onboard): defer prepared runtime lookup
rsliter Sep 8, 2026
281705d
fix(ci): isolate WSL platform fixtures
rsliter Sep 8, 2026
acc7f1e
test(ci): require WSL suite ordering target
rsliter Sep 8, 2026
a8ced84
test(ci): harden platform compatibility fixtures
rsliter Sep 9, 2026
4b60472
Merge remote-tracking branch 'origin/main' into codex/fix-11208-platf…
rsliter Sep 9, 2026
08dfe7e
fix(ci): restrict package token to main
rsliter Sep 9, 2026
cd0c40b
fix(ci): stabilize platform compatibility suites
rsliter Sep 9, 2026
be0dc84
test(ci): isolate Station Docker inventory fixtures
rsliter Sep 9, 2026
585a61b
ci: decouple macOS live compatibility evidence
rsliter Sep 9, 2026
5dc2314
docs(ci): align platform E2E ownership
rsliter Sep 9, 2026
5ec51d7
test(ci): prove platform runtime handoffs
rsliter Sep 9, 2026
ff3b2a2
fix(ci): make runtime handoffs explicit
rsliter Sep 9, 2026
d84c45a
test(ci): update gateway preparation fixture
rsliter Sep 9, 2026
9a09765
merge: refresh platform compatibility fixes from main
rsliter Sep 9, 2026
4f67bfe
fix(ci): integrate trusted runtime and isolate platform fixtures
rsliter Sep 10, 2026
c65fc2d
fix(ci): repair platform fixtures and stage reviewed SDK
rsliter Sep 10, 2026
9c0930a
test(platform): isolate health fixture hostname lookup
rsliter Sep 10, 2026
39a49c7
merge: resolve gateway preparation conflict with main
rsliter Sep 10, 2026
032e3cc
test(platform): use writable home for component fixtures
rsliter Sep 10, 2026
c26f11b
merge: consume canonical validation dependencies
rsliter Sep 10, 2026
aea0cfe
test(platform): isolate preflight and model host observations
rsliter Sep 10, 2026
27eaf05
test(platform): await backend process reaping
rsliter Sep 10, 2026
bde214a
merge: consume canonical workflow validation dependency
rsliter Sep 10, 2026
f5e7133
test(platform): wait for descendant readiness before deadline
rsliter Sep 11, 2026
c9f6bd0
merge: consume canonical publication validation dependency
rsliter Sep 11, 2026
d24182d
merge: consume current semantic validator dependency
rsliter Sep 11, 2026
d65aa11
test(platform): isolate inherited fixture host assumptions
rsliter Sep 11, 2026
5e9e8cc
merge: consume provider fence fixes and resolve main conflict
rsliter Sep 11, 2026
58eaf7f
merge: resolve installer fixture conflicts with main
rsliter Sep 11, 2026
b98aece
test(platform): keep component fixtures in the protected user home
rsliter Sep 11, 2026
869f35b
merge: refresh main after passing platform validation
rsliter Sep 11, 2026
75161b5
merge(ci): reconcile platform fixtures with main
rsliter Sep 11, 2026
b156d8a
fix(ci): preserve diagnostics after SSH readiness expires
rsliter Sep 11, 2026
c40483e
merge(ci): consume current test-size validation budget
rsliter Sep 11, 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
111 changes: 84 additions & 27 deletions .github/workflows/platform-vitest-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:

permissions:
contents: read
packages: read

concurrency:
group: platform-evidence-${{ github.ref }}
Expand Down Expand Up @@ -120,7 +121,11 @@ jobs:
run: |
set -euo pipefail
brew install bash coreutils fd gawk ripgrep
test -x "$(command -v gtar)"
mkdir -p "$RUNNER_TEMP/nemoclaw-bin"
ln -s "$(command -v gtar)" "$RUNNER_TEMP/nemoclaw-bin/tar"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
printf '%s\n' \
"$RUNNER_TEMP/nemoclaw-bin" \
"$(brew --prefix bash)/bin" \
"$(brew --prefix coreutils)/libexec/gnubin" \
"$(brew --prefix gawk)/libexec/gnubin" \
Expand All @@ -147,16 +152,16 @@ jobs:
stat --version | head -n 1
awk --version | head -n 1

- name: Install pinned OpenShell
env:
NEMOCLAW_NON_INTERACTIVE: "1"
run: env -u GH_TOKEN -u GITHUB_TOKEN bash scripts/install-openshell.sh

- name: Install dependencies
env:
NODE_AUTH_TOKEN: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.token || '' }}
run: |
npm ci --ignore-scripts
cd nemoclaw
npm ci --ignore-scripts
if [[ -n "$NODE_AUTH_TOKEN" ]]; then
bash .github/actions/ci-install-dependencies.sh
else
npm ci --ignore-scripts
npm --prefix nemoclaw ci --ignore-scripts
fi

- name: Build CLI and plugin
run: |
Expand All @@ -167,9 +172,42 @@ jobs:
- name: Run full Vitest suite on macOS
run: npx vitest run --testTimeout 60000 --shard="${{ matrix.shard }}/4"

macos-live-e2e:
name: macOS live E2E
if: ${{ github.ref == 'refs/heads/main' }}
needs: macos-vitest
runs-on: macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: npm

- name: Install dependencies
env:
NODE_AUTH_TOKEN: ${{ github.token }}
run: bash .github/actions/ci-install-dependencies.sh

- name: Build CLI and plugin
run: |
npm run build:cli
npm --prefix nemoclaw run build

- name: Install pinned OpenShell
env:
NEMOCLAW_NON_INTERACTIVE: "1"
run: env -u GH_TOKEN -u GITHUB_TOKEN bash scripts/install-openshell.sh

- id: macos_docker
name: Detect Docker availability for macOS E2E
if: ${{ matrix.shard == 1 }}
shell: bash
run: |
set -euo pipefail
Expand All @@ -182,7 +220,7 @@ jobs:
fi

- name: Run macOS live E2E
if: ${{ matrix.shard == 1 && steps.macos_docker.outputs.docker_ok == 'true' && github.ref == 'refs/heads/main' }}
if: ${{ steps.macos_docker.outputs.docker_ok == 'true' }}
env:
GITHUB_TOKEN: ${{ github.token }}
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
Expand All @@ -197,19 +235,13 @@ jobs:
--silent=false --reporter=default

- name: Explain skipped macOS live E2E
if: ${{ matrix.shard == 1 && (steps.macos_docker.outputs.docker_ok != 'true' || github.ref != 'refs/heads/main') }}
shell: bash
if: ${{ steps.macos_docker.outputs.docker_ok != 'true' }}
run: |
set -euo pipefail
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "The macOS live E2E did not run because this workflow does not test main."
else
echo "The macOS live E2E did not run because Docker is unavailable."
fi
echo "The macOS build and Vitest shard still provide platform evidence."
echo "The macOS live E2E did not run because Docker is unavailable."
echo "The macOS Vitest shards still provide platform evidence."

- name: Upload macOS E2E logs on failure
if: ${{ matrix.shard == 1 && failure() }}
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-platform-evidence-logs
Expand Down Expand Up @@ -239,10 +271,6 @@ jobs:
WSL_DISTRO: Ubuntu
WSL_TEST_USER: nemoclaw-ci
TRUSTED_WSL_HELPER: ${{ github.workspace }}\trusted-wsl-ci\tools\wsl\ci-helper.ps1
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-wsl"
steps:
- name: Force LF line endings for checkout
shell: powershell
Expand Down Expand Up @@ -288,10 +316,26 @@ jobs:
. "$env:TRUSTED_WSL_HELPER"
$packages = @(
'bash', 'ca-certificates', 'curl', 'git', 'jq', 'lsb-release', 'make',
'python3', 'python3-pip', 'python3-venv', 'ripgrep', 'rsync', 'tar', 'unzip', 'xz-utils'
'docker.io', 'iproute2', 'podman', 'procps', 'python3', 'python3-pip', 'python3-venv',
'ripgrep', 'rsync', 'tar', 'unzip', 'xz-utils'
)
Install-WslUbuntuDependencies -Distro $env:WSL_DISTRO -Packages $packages -TestUser $env:WSL_TEST_USER

- name: Start the WSL container runtime
shell: powershell
run: |
. "$env:TRUSTED_WSL_HELPER"
$testUser = ConvertTo-BashLiteral -Value $env:WSL_TEST_USER
$script = @"
set -euo pipefail
usermod -aG docker $testUser
service docker start
docker info
podman --version
ip -Version
"@
Invoke-WslScript -Distro $env:WSL_DISTRO -User root -Script $script

- name: Install Node.js 22 in WSL
shell: powershell
run: |
Expand All @@ -310,18 +354,27 @@ jobs:

- name: Install dependencies and build in WSL
shell: powershell
env:
NODE_AUTH_TOKEN: ${{ github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.token || '' }}
run: |
. "$env:TRUSTED_WSL_HELPER"
$testUser = ConvertTo-BashLiteral -Value $env:WSL_TEST_USER
$workdir = ConvertTo-BashLiteral -Value $env:WSL_WORKDIR
$nodeAuthToken = ConvertTo-BashLiteral -Value ([string]$env:NODE_AUTH_TOKEN)
$script = @"
set -euo pipefail
id -un | grep -Fxq $testUser
cd $workdir
npm ci --ignore-scripts
export NODE_AUTH_TOKEN=$nodeAuthToken
if [ -n "`$NODE_AUTH_TOKEN" ]; then
bash .github/actions/ci-install-dependencies.sh
else
npm ci --ignore-scripts
npm --prefix nemoclaw ci --ignore-scripts
fi
unset NODE_AUTH_TOKEN
npm run build:cli
cd nemoclaw
npm ci --ignore-scripts
npm run build
"@
Invoke-WslScript -Distro $env:WSL_DISTRO -User $env:WSL_TEST_USER -Script $script
Expand Down Expand Up @@ -401,6 +454,10 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_RECREATE_SANDBOX: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-wsl"
run: |
. "$env:TRUSTED_WSL_HELPER"
$workdir = ConvertTo-BashLiteral -Value $env:WSL_WORKDIR
Expand Down
46 changes: 30 additions & 16 deletions src/commands/config/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,36 @@ describe("config export command", () => {
expect(mocks.observeLiveExportSource).not.toHaveBeenCalled();
});

it("composes live observation through file publication and JSON result (#10938)", async () => {
await expect(
ConfigExportCommand.run(["alpha", "--output", "/tmp/alpha.yaml", "--json"], process.cwd()),
).resolves.toMatchObject({
status: "succeeded",
sourceSandbox: "alpha",
outputPath: "/tmp/alpha.yaml",
documentDigest: "sha256:document",
specDigest: "sha256:spec",
});
expect(mocks.publishExportFile).toHaveBeenCalledWith(
"/tmp/alpha.yaml",
"kind: NemoClawConfig\n",
false,
);
});
it.runIf(process.platform === "linux")(
"composes live observation through file publication and JSON result (#10938)",
async () => {
await expect(
ConfigExportCommand.run(["alpha", "--output", "/tmp/alpha.yaml", "--json"], process.cwd()),
).resolves.toMatchObject({
status: "succeeded",
sourceSandbox: "alpha",
outputPath: "/tmp/alpha.yaml",
documentDigest: "sha256:document",
specDigest: "sha256:spec",
});
expect(mocks.publishExportFile).toHaveBeenCalledWith(
"/tmp/alpha.yaml",
"kind: NemoClawConfig\n",
false,
);
},
);

it.runIf(process.platform !== "linux")(
"rejects file publication before reading source state (#10938)",
async () => {
await expect(
ConfigExportCommand.run(["alpha", "--output", "/tmp/alpha.yaml", "--json"], process.cwd()),
).resolves.toBeUndefined();
expect(mocks.observeLiveExportSource).not.toHaveBeenCalled();
expect(mocks.publishExportFile).not.toHaveBeenCalled();
},
);

it("declares the required output and safe replacement flags (#10938)", () => {
expect(ConfigExportCommand.flags).toMatchObject({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ describe("preflightRebuildImage", () => {
}
});

it.runIf(process.platform !== "win32")(
it.runIf(process.platform === "linux")(
"uses the isolated config across the WSL helper and Docker subprocess boundary (#7111)",
async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-wsl-rebuild-process-"));
Expand Down
17 changes: 16 additions & 1 deletion src/lib/config/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});

describe("publishExportFile", () => {
describe.runIf(process.platform === "linux")("publishExportFile", () => {
it("publishes all YAML bytes through a mode-0600 regular file (#10938)", () => {
const root = temporaryRoot();
const outputPath = path.join(root, "selected.yaml");
Expand Down Expand Up @@ -247,3 +247,18 @@ describe("publishExportFile", () => {
expect(calls).toEqual(["fsync", "publish", "fsync"]);
});
});

describe.runIf(process.platform !== "linux")("publishExportFile platform boundary", () => {
it("rejects file publication without Linux retained-directory descriptors (#10938)", () => {
const root = temporaryRoot();
const outputPath = path.join(root, "selected.yaml");

expect(() => publishExportFile(outputPath, "content")).toThrowError(
expect.objectContaining<Partial<YamlExportOutputError>>({
category: "unsafe-output",
outputPath,
}),
);
expect(fs.existsSync(outputPath)).toBe(false);
});
});
4 changes: 2 additions & 2 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,6 @@ import type { Session, SessionUpdates } from "./state/onboard-session";
import type { SandboxEntry } from "./state/registry";
import type { BackupResult } from "./state/sandbox";
import type { ProbeRecovery } from "./validation-recovery";

const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1";
const USE_COLOR = !process.env.NO_COLOR && !!process.stdout.isTTY;
const DIM = USE_COLOR ? "\x1b[2m" : "";
Expand All @@ -582,6 +581,7 @@ const {
getDockerDriverGatewayRuntimeDrift,
getDockerDriverGatewayRuntimeDriftFromSnapshot,
getDockerDriverGatewayStateDir,
prepareDockerDriverGatewayHostRuntime,
getGatewayPortListenerRawScan,
isDockerDriverGatewayPortListener,
isDockerDriverGatewayProcess,
Expand Down Expand Up @@ -1387,7 +1387,6 @@ const gatewayRegistration = createGatewayRegistration({
runOpenshell,
runQuietOpenshell,
});

const dockerDriverGatewayStart = createDockerDriverGatewayStart({
SUPPORTED_OPENSHELL_FALLBACK_VERSION,
checkGatewayPortAvailable,
Expand All @@ -1404,6 +1403,7 @@ const dockerDriverGatewayStart = createDockerDriverGatewayStart({
getDockerDriverGatewayPortListenerScan,
getDockerDriverGatewayRuntimeDrift,
getDockerDriverGatewayStateDir,
prepareDockerDriverGatewayHostRuntime,
getInstalledOpenshellVersion,
isDockerDriverGatewayHttpReady,
isDockerDriverGatewayProcessAlive,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/onboard/docker-driver-gateway-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
writeDockerGatewayDebEnvOverride,
} from "./docker-driver-gateway-env";
import { PORTABLE_HOST_GATEWAY_IP } from "./experimental/portable-profile";
import { prepareNativePodmanGatewayHostRuntime } from "./runtime-provider/podman-runtime-surfaces";

function homeEnv(home: string, xdgConfigHome = ""): NodeJS.ProcessEnv {
return { HOME: home, XDG_CONFIG_HOME: xdgConfigHome } as NodeJS.ProcessEnv;
Expand Down Expand Up @@ -183,6 +184,10 @@ describe("buildDockerDriverGatewayEnv", () => {
const env = buildDockerDriverGatewayEnv({
platform: "linux",
stateDir,
gatewayHostRuntime: prepareNativePodmanGatewayHostRuntime({
environment: process.env,
platform: "linux",
}),
getDockerSupervisorImage: () => "supervisor:test",
resolveSandboxBin: () => "/usr/bin/openshell-sandbox",
});
Expand Down
Loading
Loading