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
106 changes: 106 additions & 0 deletions src/lib/actions/sandbox/rebuild-post-restore-phase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as agentDefs from "../../agent/defs";
import * as agentRuntime from "../../agent/runtime";
import * as shields from "../../shields";
import * as registry from "../../state/registry";
import * as messagingHostForward from "./messaging-host-forward-lifecycle";
import * as processRecovery from "./process-recovery";
import * as rebuildConfigHash from "./rebuild-config-hash";
import * as rebuildMcp from "./rebuild-mcp-phase";
import * as rebuildMessaging from "./rebuild-messaging-phase";
import { runRebuildPostRestorePhase } from "./rebuild-post-restore-phase";
import * as sessionModels from "./reconcile-session-models";

describe("rebuild post-restore session model reconciliation (#7102)", () => {
let agentName: "openclaw" | "hermes";
let order: string[];

beforeEach(() => {
agentName = "openclaw";
order = [];
vi.spyOn(console, "log").mockImplementation(() => undefined);
vi.spyOn(console, "error").mockImplementation(() => undefined);
vi.spyOn(agentRuntime, "getSessionAgent").mockImplementation(
() => ({ name: agentName }) as never,
);
vi.spyOn(agentRuntime, "getAgentDisplayName").mockReturnValue("test agent");
vi.spyOn(agentDefs, "loadAgent").mockImplementation(
() => ({ name: agentName, expectedVersion: null }) as never,
);
vi.spyOn(processRecovery, "executeSandboxCommand").mockImplementation(() => {
order.push("doctor");
return { status: 0, stdout: "", stderr: "" };
});
vi.spyOn(sessionModels, "reconcileStalePinnedSessionModelsAfterRebuild").mockImplementation(
() => {
order.push("reconcile");
},
);
vi.spyOn(rebuildMessaging, "reapplyMessagingManifestAfterOpenClawDoctor").mockImplementation(
async () => {
order.push("messaging");
},
);
vi.spyOn(
rebuildConfigHash,
"refreshMutableOpenClawConfigHashAfterPostRestoreWrites",
).mockImplementation(() => {
order.push("config-hash");
return true;
});
vi.spyOn(shields, "repairMutableConfigPerms").mockReturnValue({
applied: false,
reason: "not needed",
skipReason: "not-needed",
} as never);
vi.spyOn(rebuildMcp, "restoreMcpAfterRebuild").mockResolvedValue(true);
vi.spyOn(registry, "updateSandbox").mockReturnValue(true);
vi.spyOn(messagingHostForward, "ensureMessagingHostForwardAfterRebuild").mockReturnValue(true);
});

afterEach(() => {
vi.restoreAllMocks();
});

function input() {
return {
sandboxName: "alpha",
sandboxEntry: {} as never,
messagingPlan: null,
backupManifest: null,
mcpEntries: [],
restoreSucceeded: true,
backupWasForceSkipped: false,
failedPresets: [],
finalBuiltinPresets: [],
failedPresetRemovals: [],
policyPresetReconciliationVerified: true,
staleRecovery: false,
recoveryRecreate: false,
preparedBackupRecovery: false,
staleSandboxWasLocked: false,
versionCheck: { expectedVersion: null } as never,
relockShieldsIfNeeded: vi.fn(() => true),
log: vi.fn(),
bail: vi.fn() as never,
};
}

it("reconciles OpenClaw sessions after doctor and before later config writes", async () => {
await runRebuildPostRestorePhase(input());

expect(order).toEqual(["doctor", "reconcile", "messaging", "config-hash"]);
});

it("does not run OpenClaw session reconciliation for another agent", async () => {
agentName = "hermes";

await runRebuildPostRestorePhase(input());

expect(sessionModels.reconcileStalePinnedSessionModelsAfterRebuild).not.toHaveBeenCalled();
expect(processRecovery.executeSandboxCommand).not.toHaveBeenCalled();
});
});
5 changes: 5 additions & 0 deletions src/lib/actions/sandbox/rebuild-post-restore-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
restoreMcpAfterRebuild,
} from "./rebuild-mcp-phase";
import { reapplyMessagingManifestAfterOpenClawDoctor } from "./rebuild-messaging-phase";
import { reconcileStalePinnedSessionModelsAfterRebuild } from "./reconcile-session-models";

export interface RebuildPostRestorePhaseInput {
sandboxName: string;
Expand Down Expand Up @@ -145,6 +146,10 @@ export async function runRebuildPostRestorePhase(
);
}

// #7102: clear stale per-session pinned models left over from an
// `inference set` before this rebuild, while the gateway is still down.
reconcileStalePinnedSessionModelsAfterRebuild(sandboxName, log);

await reapplyMessagingManifestAfterOpenClawDoctor(sandboxName, messagingPlan, log);
log("Refreshing mutable OpenClaw config hash after post-restore config writes");
if (!refreshMutableOpenClawConfigHashAfterPostRestoreWrites(sandboxName, log)) {
Expand Down
Loading