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
38 changes: 38 additions & 0 deletions test/e2e/live/mcp-bridge-hermes-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ import type { HostCliClient } from "../fixtures/clients/host.ts";
import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts";
import { expect } from "../fixtures/e2e-test.ts";
import { MCP_BRIDGE_TEST_CREDENTIALS } from "../fixtures/mcp-bridge-credentials.ts";
import type { ShellProbeResult } from "../fixtures/shell-probe.ts";

const SERVER_NAME = "fake";
const HOST_SECRET = MCP_BRIDGE_TEST_CREDENTIALS.host;
const ROTATED_HOST_SECRET = MCP_BRIDGE_TEST_CREDENTIALS.rotatedHost;
const INSPECTION_CONTROL_MARKER = "MCP_INSPECT_FORGED_CONTROL_LINE";
const REGISTRY_FILE = path.join(process.env.HOME ?? os.homedir(), ".nemoclaw", "sandboxes.json");

function targetSandboxDoesNotExist(result: ShellProbeResult, sandboxName: string): boolean {
const expected = `Sandbox '${sandboxName}' does not exist.`;
return resultText(result)
.split(/\r?\n/u)
.some((line) => line.trim() === expected);
}

export async function assertHermesConfig(
sandbox: SandboxClient,
sandboxName: string,
Expand Down Expand Up @@ -227,6 +235,36 @@ export async function reopenHermesMcpMaintenanceWindow(
expectExitZero(shieldsDown, "open a fresh Hermes MCP maintenance window after rebuild");
}

export async function lowerHermesShieldsForCleanup(
host: HostCliClient,
sandboxName: string,
): Promise<void> {
const shieldsDown = await host.nemoclaw(
[sandboxName, "shields", "down", "--timeout", "5m", "--reason", "E2E cleanup"],
{
artifactName: "cleanup-hermes-shields-down",
env: buildAvailabilityProbeEnv(),
timeoutMs: 3 * 60_000,
},
);
if (shieldsDown.exitCode === 0 || targetSandboxDoesNotExist(shieldsDown, sandboxName)) {
return;
}

const shieldsStatus = await host.nemoclaw([sandboxName, "shields", "status"], {
artifactName: "cleanup-hermes-shields-status-after-down-error",
env: buildAvailabilityProbeEnv(),
timeoutMs: 60_000,
});
if (targetSandboxDoesNotExist(shieldsStatus, sandboxName)) {
return;
}
expect(
shieldsStatus.exitCode === 0 && shieldsStatus.stdout.includes("Shields: DOWN"),
`Hermes Shields cleanup could not confirm DOWN posture\n${resultText(shieldsDown)}\n${resultText(shieldsStatus)}`,
).toBe(true);
}

/**
* Inject a first-reload failure around the packaged transaction helper, then
* require its real rollback reload to restore the prior config, both integrity
Expand Down
12 changes: 2 additions & 10 deletions test/e2e/live/mcp-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
assertHermesManagedAddSurvivesLockedGatewayRestartAndStateLayout,
assertHermesReloadRollback,
assertHermesRemovalSurvivesGatewayRestart,
lowerHermesShieldsForCleanup,
reopenHermesMcpMaintenanceWindow,
} from "./mcp-bridge-hermes-lifecycle.ts";
import {
Expand Down Expand Up @@ -1168,16 +1169,7 @@ mcpBridgeShardTest("hermes")(
await rebuildWithoutMcpHostSecret(host, HERMES_SANDBOX_NAME, "hermes");
await captureHermesGatewayIdentity(sandbox, "hermes-gateway-identity-after-mcp-restore");
cleanup.add("lower Hermes Shields before teardown", async () => {
const result = await host.nemoclaw(
[HERMES_SANDBOX_NAME, "shields", "down", "--timeout", "5m", "--reason", "E2E cleanup"],
{
artifactName: "cleanup-hermes-shields-down",
env: buildAvailabilityProbeEnv(),
timeoutMs: 3 * 60_000,
},
);
const sandboxMissing = /not found|does not exist/iu.test(resultText(result));
expect(result.exitCode === 0 || sandboxMissing).toBe(true);
await lowerHermesShieldsForCleanup(host, HERMES_SANDBOX_NAME);
});
cleanup.add("capture Hermes post-rebuild MCP evidence", async () => {
await artifacts.writeJson(
Expand Down
65 changes: 62 additions & 3 deletions test/e2e/support/mcp-bridge-hermes-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from "../fixtures/shell-probe.ts";
import {
assertHermesReloadRollback,
lowerHermesShieldsForCleanup,
reopenHermesMcpMaintenanceWindow,
} from "../live/mcp-bridge-hermes-lifecycle.ts";

Expand All @@ -20,14 +21,14 @@ interface RunnerCall {
options?: ShellProbeRunOptions;
}

function shellResult(exitCode = 0): ShellProbeResult {
function shellResult(exitCode = 0, stdout = "", stderr = ""): ShellProbeResult {
return {
command: [],
exitCode,
signal: null,
timedOut: false,
stdout: "",
stderr: "",
stdout,
stderr,
artifacts: {
stdout: "/tmp/stdout",
stderr: "/tmp/stderr",
Expand Down Expand Up @@ -147,3 +148,61 @@ describe("Hermes MCP post-rebuild maintenance", () => {
]);
});
});

describe("Hermes MCP cleanup posture", () => {
it("accepts an already-down Shields posture", async () => {
const runner = new RecordingRunner([
shellResult(1, "", "Config is already unlocked for hermes-e2e"),
shellResult(0, " Shields: DOWN (temporarily unlocked)\n"),
]);
const host = new HostCliClient(runner, { cliPath: "nemoclaw" });

await lowerHermesShieldsForCleanup(host, "hermes-e2e");

expect(runner.calls).toEqual([
expect.objectContaining({
command: "nemoclaw",
args: ["hermes-e2e", "shields", "down", "--timeout", "5m", "--reason", "E2E cleanup"],
}),
expect.objectContaining({
command: "nemoclaw",
args: ["hermes-e2e", "shields", "status"],
}),
]);
});

it("rejects cleanup when Shields remain up", async () => {
const runner = new RecordingRunner([
shellResult(1, "", "required executable does not exist"),
shellResult(0, " Shields: UP\n"),
]);
const host = new HostCliClient(runner, { cliPath: "nemoclaw" });

await expect(lowerHermesShieldsForCleanup(host, "hermes-e2e")).rejects.toThrow(
"Hermes Shields cleanup could not confirm DOWN posture",
);
});

it("rejects unrelated absence output from Shields status", async () => {
const runner = new RecordingRunner([
shellResult(1, "", "Config transition failed"),
shellResult(1, "", "provider configuration not found"),
]);
const host = new HostCliClient(runner, { cliPath: "nemoclaw" });

await expect(lowerHermesShieldsForCleanup(host, "hermes-e2e")).rejects.toThrow(
"Hermes Shields cleanup could not confirm DOWN posture",
);
});

it("accepts cleanup after the sandbox is removed", async () => {
const runner = new RecordingRunner([
shellResult(1, "", " Sandbox 'hermes-e2e' does not exist.\n"),
]);
const host = new HostCliClient(runner, { cliPath: "nemoclaw" });

await lowerHermesShieldsForCleanup(host, "hermes-e2e");

expect(runner.calls).toHaveLength(1);
});
});
Loading