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
30 changes: 29 additions & 1 deletion .github/workflows/platform-vitest-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,42 @@ jobs:
node-version: "22"
cache: npm

- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.14"
cache: pip
cache-dependency-path: ci/platform-vitest-macos-requirements.lock

- name: Install macOS test dependencies
run: |
set -euo pipefail
brew install bash coreutils gawk ripgrep
printf '%s\n' \
"$(brew --prefix bash)/bin" \
"$(brew --prefix coreutils)/libexec/gnubin" \
"$(brew --prefix gawk)/libexec/gnubin" \
>>"$GITHUB_PATH"
python -m pip install \
--only-binary=:all: \
--require-hashes \
--requirement ci/platform-vitest-macos-requirements.lock

- name: Show environment
run: |
set -euo pipefail
echo "Runner: $(uname -a)"
echo "Arch: $(uname -m)"
sw_vers
bash --version | head -n 1
node --version
npm --version
python --version
python -c 'import setuptools, yaml; print(f"setuptools={setuptools.__version__} pyyaml={yaml.__version__}")'
rg --version | head -n 1
timeout --version | head -n 1
stat --version | head -n 1
awk --version | head -n 1

- name: Install dependencies
run: |
Expand Down Expand Up @@ -194,7 +222,7 @@ jobs:
'Acquire::Retries "5";' \
>/etc/apt/apt.conf.d/99github-actions-network
apt-get update
apt-get install -y bash ca-certificates curl git jq lsb-release make python3 python3-pip python3-venv rsync tar unzip xz-utils
apt-get install -y bash ca-certificates curl git jq lsb-release make python3 python3-pip python3-venv ripgrep rsync tar unzip xz-utils
if ! id -u "$test_user" >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash "$test_user"
fi
Expand Down
10 changes: 8 additions & 2 deletions agents/hermes/generate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@
// - Base environment entries used by Hermes inside OpenShell
// - Agent defaults (terminal, memory, skills, display)

import { realpathSync } from "node:fs";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { fileURLToPath } from "node:url";
import { generateHermesConfig } from "./config/generate.ts";

export function main(): void {
generateHermesConfig({ env: process.env, scriptDir: import.meta.dirname });
}

function isMainModule(): boolean {
return process.argv[1] ? import.meta.url === pathToFileURL(resolve(process.argv[1])).href : false;
if (!process.argv[1]) return false;
try {
return realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}

if (isMainModule()) main();
9 changes: 9 additions & 0 deletions ci/platform-vitest-macos-requirements.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Test-only dependencies for the macOS platform Vitest lane. Keep these hashes
# aligned with agents/langchain-deepagents-code/requirements.lock.
pyyaml==6.0.3 \
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310
setuptools==82.0.1 \
--hash=sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb
10 changes: 10 additions & 0 deletions ci/source-shape-test-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,16 @@
"test": "cross-checks the allowlist against every production archive install boundary",
"category": "security"
},
{
"file": "test/platform-vitest-main-workflow.test.ts",
"test": "keeps the WSL suite unprivileged with explicit root-only contracts",
"category": "security"
},
{
"file": "test/platform-vitest-main-workflow.test.ts",
"test": "provisions the pinned macOS test runtime before running the full suite",
"category": "compatibility"
},
{
"file": "test/plugin-vitest-project.test.ts",
"test": "defines one canonical plugin project for root and standalone runs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,10 @@ describe("docker-driver-gateway compatibility container", () => {
});

it("fails closed when the configured Unix socket does not answer as a Docker daemon", async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-daemon-probe-"));
// Darwin limits AF_UNIX socket paths to 104 bytes. Vitest's nested temp root
// can consume most of that budget before the fixture suffix is appended.
const socketTempRoot = process.platform === "darwin" ? "/tmp" : os.tmpdir();
const dir = fs.mkdtempSync(path.join(socketTempRoot, "nemoclaw-docker-daemon-probe-"));
const socketPath = path.join(dir, "docker.sock");
const server = createServer();
await new Promise<void>((resolve, reject) => {
Expand Down
11 changes: 10 additions & 1 deletion src/lib/tunnel/gateway-stop-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { GATEWAY_STOP_SCRIPT } from "./gateway-stop-script";

// Linux-only: execute the production shell script against real processes while
Expand Down Expand Up @@ -94,6 +94,14 @@ ${script}`;
return stat.replace(/^[^)]*\) /, "").split(" ")[19];
}

async function waitForArgv0(pid: number, expected: string): Promise<void> {
await vi.waitFor(
() =>
expect(readFileSync(`/proc/${pid}/cmdline`, "utf-8").split("\0")[0] ?? "").toBe(expected),
{ timeout: 5_000, interval: 10 },
);
}

function identityFixture(
pid: number,
mode = 0o600,
Expand Down Expand Up @@ -143,6 +151,7 @@ ${script}`;
"finds and kills a gateway whose argv was rewritten to bare 'openclaw' (#4951)",
async () => {
const pid = spawnWithArgv0("openclaw");
await waitForArgv0(pid, "openclaw");
expect(runStopScript(stopScriptWithGatewayIdentity(pid))).toBe(0);
await new Promise((r) => setTimeout(r, 300));
expect(isAlive(pid)).toBe(false);
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/support/e2e-fixture-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ describe("E2E fixture primitives", () => {
const artifacts = createArtifactSink(targetId, tmp);
await artifacts.ensureRoot();

expect(artifacts.rootDir).toBe(path.resolve(artifactParent, targetId));
expect(fs.realpathSync(artifacts.rootDir)).toBe(
fs.realpathSync(path.resolve(artifactParent, targetId)),
);
for (const file of allowlistedFiles) {
await artifacts.writeJson(file, { targetId, file });
}
Expand Down
11 changes: 8 additions & 3 deletions test/gateway-supervisor-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,14 @@ describe("gateway supervisor tracked PID handling", () => {
);

expect(result.status).toBe(0);
// macOS bash 3.2 reports SIGTERM job-control notifications to stderr
// (e.g. "Terminated: 15 sleep 30") despite set +m; filter them out.
expect(result.stderr.replace(/^(?:Terminated|Killed): \d+[^\n]*\n?/gm, "")).toBe("");
// Bash can report SIGTERM job-control notifications to stderr despite
// set +m, with an additional "bash: line N: PID" prefix on macOS.
expect(
result.stderr.replace(
/^(?:bash: line \d+: \d+\s+)?(?:Terminated|Killed): \d+[^\n]*\n?/gm,
"",
),
).toBe("");
expect(result.stdout).toMatch(/^\d+$/);
});

Expand Down
5 changes: 5 additions & 0 deletions test/helpers/vitest-watch-triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [
pattern: /(?:^|\/)\.github\/workflows\/e2e\.yaml$/,
testsToRun: runTests(...E2E_WORKFLOW_CONTRACTS),
},
{
pattern:
/(?:^|\/)(?:\.github\/workflows\/platform-vitest-main\.yaml|ci\/platform-vitest-macos-requirements\.lock)$/,
testsToRun: runTests("test/platform-vitest-main-workflow.test.ts"),
},
];

export function resolveVitestWatchTests(file: string): string[] {
Expand Down
52 changes: 32 additions & 20 deletions test/langchain-deepagents-code-managed-mcp-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ function runManagedHelper(source: string) {
}

describe("Deep Agents managed MCP runtime hardening", () => {
it("treats only the exact empty managed projection as an absent snapshot", () => {
const result = runManagedHelper(String.raw`
it.runIf(process.platform === "linux")(
"treats only the exact empty managed projection as an absent snapshot",
() => {
const result = runManagedHelper(String.raw`
import importlib.util
import sys

Expand Down Expand Up @@ -66,12 +68,15 @@ for raw in invalid:
print("strict-tombstone-ok")
`);

expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("strict-tombstone-ok");
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("strict-tombstone-ok");
},
);

it("rejects a same-sized fully sealed descriptor not created by this process state", () => {
const result = runManagedHelper(String.raw`
it.runIf(process.platform === "linux")(
"rejects a same-sized fully sealed descriptor not created by this process state",
() => {
const result = runManagedHelper(String.raw`
import fcntl
import importlib.util
import os
Expand Down Expand Up @@ -110,12 +115,15 @@ finally:
print("descriptor-provenance-ok")
`);

expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("descriptor-provenance-ok");
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("descriptor-provenance-ok");
},
);

it("falls back on blocked memfd with repeatable digest-bound child reads", () => {
const result = runManagedHelper(String.raw`
it.runIf(process.platform === "linux")(
"falls back on blocked memfd with repeatable digest-bound child reads",
() => {
const result = runManagedHelper(String.raw`
import errno
import fcntl
import importlib.util
Expand Down Expand Up @@ -223,12 +231,15 @@ print(child.managed_mcp_config_bytes(sys.argv[2]).decode(), end="")
print("anonymous-fallback-ok")
`);

expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("anonymous-fallback-ok");
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("anonymous-fallback-ok");
},
);

it("fails closed without O_TMPFILE and does not mask unrelated memfd errors", () => {
const result = runManagedHelper(String.raw`
it.runIf(process.platform === "linux")(
"fails closed without O_TMPFILE and does not mask unrelated memfd errors",
() => {
const result = runManagedHelper(String.raw`
import errno
import importlib.util
import os
Expand Down Expand Up @@ -329,7 +340,8 @@ with tempfile.TemporaryDirectory() as tempdir:
print("fallback-fail-closed-ok")
`);

expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("fallback-fail-closed-ok");
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe("fallback-fail-closed-ok");
},
);
});
2 changes: 1 addition & 1 deletion test/local-credential-helper-suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ export function registerLocalCredentialHelperTests(group: LocalCredentialHelperT
USERPROFILE: accountHome,
}
: { HOME: accountHome, PWD: commandCwd };
expect(observed.cwd).toBe(commandCwd);
expect(fs.realpathSync(observed.cwd)).toBe(fs.realpathSync(commandCwd));
expect(
Object.fromEntries(Object.entries(observed.environment).filter(([, value]) => value)),
).toEqual(expectedEnvironment);
Expand Down
8 changes: 4 additions & 4 deletions test/onboard-model-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ describe("onboard Model Router setup", () => {
}).trim();
assert.match(sourceHead, /^[0-9a-f]{40}$/i);
assert.equal(
runCapture(["git", "-C", routerDir, "rev-parse", "--show-toplevel"]).trim(),
routerDir,
fs.realpathSync(runCapture(["git", "-C", routerDir, "rev-parse", "--show-toplevel"]).trim()),
fs.realpathSync(routerDir),
);
fs.mkdirSync(path.dirname(managedCommand), { recursive: true });
fs.writeFileSync(managedCommand, "#!/usr/bin/env sh\nexit 0\n", { mode: 0o755 });
Expand Down Expand Up @@ -349,7 +349,7 @@ describe("onboard Model Router setup", () => {
"--output",
litellmConfigPath,
]);
assert.equal(proxyConfig.cwd, blueprintDir);
assert.equal(fs.realpathSync(proxyConfig.cwd), fs.realpathSync(blueprintDir));
assert.deepEqual(proxy.args, [
"proxy",
"--litellm-config",
Expand All @@ -361,7 +361,7 @@ describe("onboard Model Router setup", () => {
"--port",
String(port),
]);
assert.equal(proxy.cwd, blueprintDir);
assert.equal(fs.realpathSync(proxy.cwd), fs.realpathSync(blueprintDir));
assert.deepEqual(proxy.env, {
ROUTER_API_KEY: "router-secret",
OPENAI_API_KEY: "router-secret",
Expand Down
Loading
Loading