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
2 changes: 1 addition & 1 deletion ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"test/install-preflight.test.ts": 3934,
"test/nemoclaw-start.test.ts": 4827,
"test/onboard-messaging.test.ts": 2062,
"test/onboard-selection.test.ts": 5624,
"test/onboard-selection.test.ts": 4834,
"test/onboard.test.ts": 4057,
"test/policies.test.ts": 2279
}
Expand Down
89 changes: 89 additions & 0 deletions src/lib/actions/sandbox/rebuild-gateway-drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,95 @@ describe("rebuild gateway drift preflight", () => {
expect(recoverNamedGatewayRuntimeSpy).not.toHaveBeenCalled();
});

it.each([
{
recordedGateway: "nemoclaw",
recordedPort: 8080,
activeGateway: "other-gw",
},
{
recordedGateway: "nemoclaw-9000",
recordedPort: 9000,
activeGateway: "nemoclaw",
},
])("refuses stale recovery when '$activeGateway' is active instead of recorded gateway '$recordedGateway' (#4497)", async ({
recordedGateway,
recordedPort,
activeGateway,
}) => {
detectPreflightIssueSpy.mockReturnValue(null);
vi.mocked(registry.getSandbox).mockReturnValue({
name: "alpha",
provider: "ollama-local",
model: "nvidia/nemotron",
policies: [],
nimContainer: null,
agent: null,
nemoclawVersion: "0.1.0",
dashboardPort: 18789,
gatewayName: recordedGateway,
gatewayPort: recordedPort,
} as never);
const openshellResults: Record<string, { status: number; output: string }> = {
"sandbox list": { status: 0, output: "" },
"sandbox get": {
status: 1,
output: "Error: × Not Found: sandbox not found",
},
};
captureOpenshellSpy.mockImplementation(
(args: string[]) => openshellResults[args.slice(0, 2).join(" ")] ?? { status: 0, output: "" },
);
const getNamedGatewayLifecycleStateSpy = vi
.spyOn(gatewayRuntime, "getNamedGatewayLifecycleState")
.mockReturnValue({
state: "connected_other",
activeGateway,
status: `Gateway: ${activeGateway}\nStatus: Connected`,
} as never);
const backupSandboxStateSpy = vi
.spyOn(requireDist("../../state/sandbox.js"), "backupSandboxState")
.mockImplementation(() => {
throw new Error("unexpected backup");
});
const removeSandboxRegistryEntrySpy = vi
.spyOn(requireDist("./destroy.js"), "removeSandboxRegistryEntryWithReceipt")
.mockImplementation(() => {
throw new Error("unexpected registry removal");
});
const onboardSpy = vi
.spyOn(requireDist("../../onboard.js"), "onboard")
.mockImplementation(async () => {
throw new Error("unexpected onboard");
});
spies.push(
getNamedGatewayLifecycleStateSpy,
backupSandboxStateSpy,
removeSandboxRegistryEntrySpy,
onboardSpy,
);

await expect(rebuildSandbox("alpha", ["--yes"], { throwOnError: true })).rejects.toThrow(
"Could not confirm live state",
);

const output = errorSpy.mock.calls.flat().join("\n");
expect(output).toContain("NOT been removed");
expect(output).toContain(`openshell gateway select ${recordedGateway}`);
expect(getNamedGatewayLifecycleStateSpy).toHaveBeenCalledWith(recordedGateway);
expect(runOpenshellSpy).toHaveBeenCalledWith(
["gateway", "select", recordedGateway],
expect.objectContaining({ ignoreError: true }),
);
expect(backupSandboxStateSpy).not.toHaveBeenCalled();
expect(runOpenshellSpy).not.toHaveBeenCalledWith(
["sandbox", "delete", "alpha"],
expect.anything(),
);
expect(removeSandboxRegistryEntrySpy).not.toHaveBeenCalled();
expect(onboardSpy).not.toHaveBeenCalled();
});

it("recovers the named gateway and retries the liveness query before entering stale recovery", async () => {
detectPreflightIssueSpy.mockReturnValue(null);
// First `sandbox list` fails (gateway down) and triggers recovery; the retry
Expand Down
4 changes: 2 additions & 2 deletions test/generate-openclaw-config-plugin-entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ const BASE_ENV: Record<string, string> = {
};

describe("generate-openclaw-config.mts: default plugin entries", () => {
it("disables the bundled acpx and bonjour plugins by default", () => {
it("omits the stale acpx entry and disables bundled bonjour by default", () => {
const config = buildConfig({ ...BASE_ENV });
expect(config.plugins.entries.acpx).toEqual({ enabled: false });
expect(config.plugins.entries.acpx).toBeUndefined();
expect(config.plugins.entries.bonjour).toEqual({ enabled: false });
});

Expand Down
10 changes: 6 additions & 4 deletions test/helpers/rebuild-flow-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,12 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}):
output: overrides.sandboxListOutput ?? (overrides.staleRecovery ? "" : "alpha Ready"),
},
});
vi.spyOn(gatewayState, "getReconciledSandboxGatewayState").mockResolvedValue({
state: overrides.staleRecovery ? "missing" : "present",
output: "",
});
vi.spyOn(gatewayState, "getReconciledSandboxGatewayState").mockResolvedValue(
overrides.reconciledSandboxGatewayState ?? {
state: overrides.staleRecovery ? "missing" : "present",
output: "",
},
);
const ensureRebuildAgentBaseImageSpy = vi
.spyOn(rebuildFlowHelpers, "ensureRebuildAgentBaseImage")
.mockReturnValue(
Expand Down
2 changes: 2 additions & 0 deletions test/helpers/rebuild-flow-test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { type MockInstance, vi } from "vitest";
import type { SandboxGatewayState } from "../../src/lib/actions/sandbox/gateway-state";
import type { RebuildImagePreflightResult } from "../../src/lib/actions/sandbox/rebuild-custom-image-preflight";
import type { RebuildRecreateOnboardOpts } from "../../src/lib/actions/sandbox/rebuild-gpu-opt-out";
import type { SandboxRemovalReceipt } from "../../src/lib/state/registry";
Expand Down Expand Up @@ -64,6 +65,7 @@ export type RebuildFlowOverrides = {
) => { ok: true; manifest: Record<string, unknown> } | { ok: false; reason: string };
managedImageEvidence?: boolean;
staleRecovery?: boolean;
reconciledSandboxGatewayState?: SandboxGatewayState;
mcpPreparation?: {
entries: Array<Record<string, unknown>>;
detachedProviderEntries: Array<Record<string, unknown>>;
Expand Down
Loading
Loading