fix(ui): deduplicate gateway-native session hosts

This commit is contained in:
Vincent Koc
2026-07-30 15:22:31 +08:00
parent ad1dc60259
commit bb68389a66
20 changed files with 453 additions and 38 deletions
+68
View File
@@ -0,0 +1,68 @@
import fs from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import {
createOpenClawTestState,
type OpenClawTestState,
} from "../test-utils/openclaw-test-state.js";
import { configureNodeHost } from "./config.js";
import { resolveLocalNodeId } from "./local-id.js";
const states: OpenClawTestState[] = [];
afterEach(async () => {
closeOpenClawStateDatabaseForTest();
while (states.length > 0) {
await states.pop()?.cleanup();
}
});
describe("resolveLocalNodeId", () => {
it("reads and process-caches the canonical same-install node id", async () => {
const state = await createOpenClawTestState({
label: "local-node-id",
layout: "state-only",
});
states.push(state);
await configureNodeHost({
candidateNodeId: "gateway-local-node",
fallbackDisplayName: "Gateway node",
gateway: {},
env: state.env,
nowMs: 1,
});
await expect(resolveLocalNodeId(state.env)).resolves.toBe("gateway-local-node");
await configureNodeHost({
candidateNodeId: "replacement-node",
fallbackDisplayName: "Replacement node",
gateway: {},
env: state.env,
nowMs: 2,
});
await expect(resolveLocalNodeId(state.env)).resolves.toBe("gateway-local-node");
});
it("retries after a failed canonical config read", async () => {
const state = await createOpenClawTestState({
label: "local-node-id-retry",
layout: "state-only",
});
states.push(state);
const legacyPath = state.statePath("node.json");
await fs.writeFile(legacyPath, "{}\n", "utf8");
await expect(resolveLocalNodeId(state.env)).rejects.toThrow("openclaw doctor --fix");
await fs.rm(legacyPath);
await configureNodeHost({
candidateNodeId: "recovered-local-node",
fallbackDisplayName: "Recovered node",
gateway: {},
env: state.env,
nowMs: 1,
});
await expect(resolveLocalNodeId(state.env)).resolves.toBe("recovered-local-node");
});
});
+27
View File
@@ -0,0 +1,27 @@
import { resolveStateDir } from "../config/paths.js";
import { loadNodeHostConfig } from "./config.js";
const localNodeIdByStateDir = new Map<string, Promise<string | null>>();
/**
* Resolve the same-install node host from canonical shared SQLite state.
* Node-host config changes require restart, so this fact stays process-stable.
*/
export async function resolveLocalNodeId(
env: NodeJS.ProcessEnv = process.env,
): Promise<string | null> {
const stateDir = resolveStateDir(env);
let pending = localNodeIdByStateDir.get(stateDir);
if (!pending) {
pending = loadNodeHostConfig(env).then((config) => config?.nodeId ?? null);
localNodeIdByStateDir.set(stateDir, pending);
}
try {
return await pending;
} catch (error) {
if (localNodeIdByStateDir.get(stateDir) === pending) {
localNodeIdByStateDir.delete(stateDir);
}
throw error;
}
}