Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
564e248
fix(e2e): model portable Podman service activation
senthilr-nv Aug 15, 2026
35f0e75
test(e2e): keep portable service cleanup linear
senthilr-nv Aug 15, 2026
0bbca0b
test(e2e): exercise portable shim installation
senthilr-nv Aug 15, 2026
1a7d6aa
merge: reconcile current main
senthilr-nv Aug 15, 2026
f5d2667
test(e2e): model cold portable socket activation
senthilr-nv Aug 15, 2026
3853552
test(e2e): keep portable activation handoff linear
senthilr-nv Aug 15, 2026
209cece
test(e2e): preserve portable socket authority
senthilr-nv Aug 15, 2026
96c1a3b
merge: refresh portable profile fixture base
senthilr-nv Aug 15, 2026
eeb329d
test(e2e): clean portable fixture processes
senthilr-nv Aug 15, 2026
90fbff5
test(e2e): fail closed on fixture cleanup
senthilr-nv Aug 15, 2026
d566677
merge: refresh portable profile fixture base
senthilr-nv Aug 15, 2026
3f3d2ab
ci(e2e): clean portable workflow fixture
senthilr-nv Aug 15, 2026
6c1dcfd
fix(e2e): serialize portable fixture refresh
senthilr-nv Aug 15, 2026
f164955
merge: refresh portable profile fixture base
senthilr-nv Aug 15, 2026
758679e
merge(main): reconcile portable profile tests with current main
senthilr-nv Aug 15, 2026
6de3e4d
test(e2e): bind portable fixture process identity
senthilr-nv Aug 15, 2026
bd7e9ba
merge(main): reconcile portable profile tests with current main
senthilr-nv Aug 15, 2026
ab3613a
test(e2e): keep portable fixture teardown linear
senthilr-nv Aug 15, 2026
89f5a04
merge(main): reconcile portable profile tests with current main
senthilr-nv Aug 15, 2026
4e252fb
fix(e2e): stabilize portable process identity checks
senthilr-nv Aug 15, 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
41 changes: 3 additions & 38 deletions .github/workflows/portable-profile-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ on:
- "src/lib/domain/sandbox/image-tag.ts"
- "src/lib/sandbox/**"
- "test/e2e/fixtures/availability-env.ts"
- "test/e2e/fixtures/portable-profile-systemctl-shim.sh"
- "test/e2e/live/full-e2e.test.ts"
- "test/e2e/live/launch-agent-turn.ts"
- "test/e2e/live/portable-profile-gateway-proof.ts"
Expand Down Expand Up @@ -139,47 +140,11 @@ jobs:

shim_dir="${RUNNER_TEMP}/nemoclaw-portable-bin"
install -d -m 700 "$shim_dir"
cat >"$shim_dir/systemctl" <<'SHIM'
#!/usr/bin/env bash
set -euo pipefail
runtime_dir="${XDG_RUNTIME_DIR:?}"
service_dir="${runtime_dir}/podman"
socket_path="${service_dir}/podman.sock"
pid_file="${runtime_dir}/nemoclaw-podman-service.pid"
log_file="${runtime_dir}/nemoclaw-podman-service.log"
case "$*" in
"--user set-environment "*) exit 0 ;;
"--user try-restart podman.service")
if [[ -f "$pid_file" ]]; then
kill "$(<"$pid_file")" 2>/dev/null || true
rm -f "$pid_file" "$socket_path"
fi
;;
"--user enable --now podman.socket")
install -d -m 755 "$service_dir"
nohup podman system service --time=0 "unix://$socket_path" >"$log_file" 2>&1 &
echo $! >"$pid_file"
for _ in $(seq 1 100); do
if [[ -S "$socket_path" ]]; then
chmod 660 "$socket_path"
exit 0
fi
sleep 0.1
done
cat "$log_file" >&2 || true
exit 1
;;
*)
echo "unexpected user-service command: $*" >&2
exit 64
;;
esac
SHIM
chmod 700 "$shim_dir/systemctl"
install -m 700 test/e2e/fixtures/portable-profile-systemctl-shim.sh "$shim_dir/systemctl"

export PATH="$shim_dir:$PATH"
export XDG_RUNTIME_DIR="$runtime_dir"
systemctl --user enable --now podman.socket
systemctl --user start podman.socket
printf '%s\n' "$shim_dir" >>"$GITHUB_PATH"
printf 'XDG_RUNTIME_DIR=%s\n' "$runtime_dir" >>"$GITHUB_ENV"
printf 'DOCKER_HOST=unix://%s/podman/podman.sock\n' "$runtime_dir" >>"$GITHUB_ENV"
Expand Down
81 changes: 81 additions & 0 deletions test/e2e/fixtures/portable-profile-systemctl-shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

runtime_dir="${XDG_RUNTIME_DIR:?}"
service_dir="${runtime_dir}/podman"
socket_path="${service_dir}/podman.sock"
pid_file="${runtime_dir}/nemoclaw-podman-service.pid"
log_file="${runtime_dir}/nemoclaw-podman-service.log"

service_is_active() {
[[ -f "$pid_file" ]] || return 1
local pid
pid="$(<"$pid_file")"
[[ "$pid" =~ ^[1-9][0-9]*$ ]] || return 1
kill -0 "$pid" 2>/dev/null && [[ -S "$socket_path" ]]
}

stop_service() {
if [[ -f "$pid_file" ]]; then
local pid
pid="$(<"$pid_file")"
if [[ "$pid" =~ ^[1-9][0-9]*$ ]]; then
kill "$pid" 2>/dev/null || true
fi
fi
rm -f "$pid_file" "$socket_path"
}

start_socket() {
if service_is_active; then
return 0
fi

stop_service
install -d -m 700 "$service_dir"
nohup podman system service --time=0 "unix://$socket_path" >"$log_file" 2>&1 &
echo $! >"$pid_file"

for ((attempt = 0; attempt < 100; attempt += 1)); do
if service_is_active; then
chmod 660 "$socket_path"
return 0
fi
if ! kill -0 "$(<"$pid_file")" 2>/dev/null; then
break
fi
sleep 0.1
done

stop_service
cat "$log_file" >&2 || true
return 1
}

case "$*" in
"--user set-environment NETAVARK_FW=iptables CONTAINERS_CONF="*)
exit 0
;;
"--user try-restart podman.service")
if service_is_active; then
stop_service
start_socket
fi
;;
"--user is-active --quiet podman.service")
if service_is_active; then
exit 0
fi
exit 3
;;
"--user start podman.socket")
start_socket
;;
*)
echo "unexpected user-service command: $*" >&2
exit 64
;;
esac
60 changes: 13 additions & 47 deletions test/e2e/live/portable-profile-rootless-linux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,49 +98,11 @@ async function waitForRegistry(attempt = 0): Promise<void> {

function writeSystemctlShim(binDir: string): void {
const shim = path.join(binDir, "systemctl");
fs.writeFileSync(
fs.copyFileSync(
path.join(process.cwd(), "test/e2e/fixtures/portable-profile-systemctl-shim.sh"),
shim,
`#!/usr/bin/env bash
set -euo pipefail
runtime_dir="\${XDG_RUNTIME_DIR:?}"
service_dir="\${runtime_dir}/podman"
socket_path="\${service_dir}/podman.sock"
pid_file="\${runtime_dir}/nemoclaw-podman-service.pid"
log_file="\${runtime_dir}/nemoclaw-podman-service.log"

case "$*" in
"--user set-environment NETAVARK_FW=iptables CONTAINERS_CONF="*)
exit 0
;;
"--user try-restart podman.service")
if [[ -f "\${pid_file}" ]]; then
kill "$(<"\${pid_file}")" 2>/dev/null || true
rm -f "\${pid_file}" "\${socket_path}"
fi
exit 0
;;
"--user enable --now podman.socket")
install -d -m 755 "\${service_dir}"
nohup podman system service --time=0 "unix://\${socket_path}" >"\${log_file}" 2>&1 &
echo $! >"\${pid_file}"
for _ in $(seq 1 100); do
if [[ -S "\${socket_path}" ]]; then
chmod 660 "\${socket_path}"
exit 0
fi
sleep 0.1
done
cat "\${log_file}" >&2 || true
exit 1
;;
*)
echo "unexpected user-service command: $*" >&2
exit 64
;;
esac
`,
{ encoding: "utf-8", mode: 0o700 },
);
fs.chmodSync(shim, 0o700);
}

function selectInstallerPodmanRuntime(repoRoot: string): string {
Expand Down Expand Up @@ -321,9 +283,13 @@ async function main(progress: TestProgress): Promise<void> {
}
}

test("portable profile rootless environment completes the local image and fixed-host route contracts", {
meta: { e2ePhases: PORTABLE_PROFILE_E2E_PHASES },
timeout: 120_000,
}, async ({ progress }) => {
await main(progress);
});
test(
"portable profile rootless environment completes the local image and fixed-host route contracts",
{
meta: { e2ePhases: PORTABLE_PROFILE_E2E_PHASES },
timeout: 120_000,
},
async ({ progress }) => {
await main(progress);
},
);
119 changes: 119 additions & 0 deletions test/e2e/support/portable-profile-systemctl-shim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { describe, expect, it } from "vitest";

import { readYaml, type Workflow, type WorkflowStep } from "../../helpers/e2e-workflow-contract.ts";

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const SHIM_SOURCE = path.join(REPO_ROOT, "test/e2e/fixtures/portable-profile-systemctl-shim.sh");

function writeExecutable(filePath: string, source: string): void {
fs.writeFileSync(filePath, source, { encoding: "utf8", mode: 0o700 });
}

function portableLaunchProvisionStep(): WorkflowStep {
const workflow = readYaml<Workflow>(".github/workflows/portable-profile-e2e.yaml");
const step = workflow.jobs["portable-launch"]?.steps?.find(
(candidate) => candidate.name === "Provision restricted rootless Linux runtime",
);
expect(step).toBeDefined();
return step!;
}

describe("portable profile systemctl fixture", () => {
it(
"reports inactive status, activates the socket, and reports active status (#9006)",
{
timeout: 15_000,
},
() => {
const directory = fs.mkdtempSync("/tmp/portable-systemctl-shim-");
const binDir = path.join(directory, "bin");
const runtimeDir = path.join(directory, "runtime");
const shim = path.join(binDir, "systemctl");
fs.mkdirSync(binDir);
fs.mkdirSync(runtimeDir);
fs.copyFileSync(SHIM_SOURCE, shim);
fs.chmodSync(shim, 0o700);
writeExecutable(
path.join(binDir, "podman"),
`#!${process.execPath}
const net = require("node:net");
const socketPath = process.argv.at(-1).replace("unix://", "");
const server = net.createServer();
server.listen(socketPath);
const stop = () => server.close(() => process.exit(0));
process.on("SIGINT", stop);
process.on("SIGTERM", stop);
`,
);
const env = {
...process.env,
PATH: `${binDir}:${process.env.PATH ?? ""}`,
XDG_RUNTIME_DIR: runtimeDir,
};
const systemctl = (args: string[]) =>
spawnSync(shim, args, { encoding: "utf8", env, timeout: 15_000 });

try {
expect(systemctl(["--user", "is-active", "--quiet", "podman.service"]).status).toBe(3);
expect(
systemctl([
"--user",
"set-environment",
"NETAVARK_FW=iptables",
`CONTAINERS_CONF=${path.join(directory, "containers.conf")}`,
]).status,
).toBe(0);
const activation = systemctl(["--user", "start", "podman.socket"]);
expect(activation.status, activation.stderr).toBe(0);
expect(systemctl(["--user", "is-active", "--quiet", "podman.service"]).status).toBe(0);
} finally {
const pidFile = path.join(runtimeDir, "nemoclaw-podman-service.pid");
if (fs.existsSync(pidFile)) {
const pid = Number(fs.readFileSync(pidFile, "utf8").trim());
if (Number.isInteger(pid)) {
process.kill(pid, "SIGTERM");
}
}
fs.rmSync(directory, { force: true, recursive: true });
Comment thread
senthilr-nv marked this conversation as resolved.
Outdated
}
},
);

it("rejects an unexpected user-service command (#9006)", () => {
const runtimeDir = fs.mkdtempSync("/tmp/portable-systemctl-shim-");
try {
const result = spawnSync(SHIM_SOURCE, ["--user", "restart", "podman.socket"], {
encoding: "utf8",
env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir },
});
expect(result.status).toBe(64);
expect(result.stderr).toContain(
"unexpected user-service command: --user restart podman.socket",
);
} finally {
fs.rmSync(runtimeDir, { force: true, recursive: true });
}
});

it("binds both portable profile lanes to the same systemctl fixture (#9006)", () => {
const provision = portableLaunchProvisionStep().run ?? "";
expect(provision).toContain(
'install -m 700 test/e2e/fixtures/portable-profile-systemctl-shim.sh "$shim_dir/systemctl"',
);
expect(provision).toContain("systemctl --user start podman.socket");
expect(
fs.readFileSync(
path.join(REPO_ROOT, "test/e2e/live/portable-profile-rootless-linux.test.ts"),
"utf8",
),
).toContain('"test/e2e/fixtures/portable-profile-systemctl-shim.sh"');
});
});
8 changes: 6 additions & 2 deletions test/helpers/vitest-watch-triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [
pattern: /(?:^|\/)\.github\/workflows\/e2e-standard-profile\.yaml$/,
testsToRun: runTests("test/e2e/support/standard-profile-workflow-boundary.test.ts"),
},
{
pattern:
/(?:^|\/)(?:\.github\/workflows\/portable-profile-e2e\.yaml|test\/e2e\/fixtures\/portable-profile-systemctl-shim\.sh)$/,
testsToRun: runTests("test/e2e/support/portable-profile-systemctl-shim.test.ts"),
},
{
pattern:
/(?:^|\/)\.github\/(?:actions\/docker-auth-(?:cleanup|setup)\/action\.yaml|scripts\/docker-auth-(?:cleanup|setup)\.sh)$/,
Expand Down Expand Up @@ -184,8 +189,7 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [
testsToRun: runTests("test/e2e-main-retry-workflow.test.ts"),
},
{
pattern:
/(?:^|\/)\.github\/workflows\/(?:hosted-runner-recovery|platform-vitest-main)\.yaml$/,
pattern: /(?:^|\/)\.github\/workflows\/(?:hosted-runner-recovery|platform-vitest-main)\.yaml$/,
testsToRun: runTests("test/hosted-runner-recovery-workflow.test.ts"),
},
{
Expand Down
10 changes: 10 additions & 0 deletions test/vitest-watch-triggers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const OPAQUE_INPUTS = [
"test/e2e/docs/parity-inventory.generated.json",
".github/workflows/e2e.yaml",
".github/workflows/e2e-standard-profile.yaml",
".github/workflows/portable-profile-e2e.yaml",
"test/e2e/fixtures/portable-profile-systemctl-shim.sh",
".github/actions/docker-auth-setup/action.yaml",
".github/actions/docker-auth-cleanup/action.yaml",
".github/scripts/docker-auth-setup.sh",
Expand Down Expand Up @@ -166,6 +168,14 @@ describe("Vitest opaque-input watch triggers", () => {
expect(triggeredBy(".github/workflows/e2e-standard-profile.yaml")).toEqual([
"test/e2e/support/standard-profile-workflow-boundary.test.ts",
]);
for (const portableProfilePath of [
".github/workflows/portable-profile-e2e.yaml",
"test/e2e/fixtures/portable-profile-systemctl-shim.sh",
]) {
expect(triggeredBy(portableProfilePath)).toEqual([
"test/e2e/support/portable-profile-systemctl-shim.test.ts",
]);
}
for (const authPath of [
".github/actions/docker-auth-setup/action.yaml",
".github/actions/docker-auth-cleanup/action.yaml",
Expand Down
Loading