Download src/node-host/config.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/node-host/config.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/node-host/config.test.ts
-
curl -L -o config.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/node-host/config.test.ts
12.1 kB
| import type { ChildProcess } from "node:child_process"; | |
| import fs from "node:fs/promises"; | |
| import path from "node:path"; | |
| import { afterEach, describe, expect, it, vi } from "vitest"; | |
| import { runManagedCommand } from "../../scripts/lib/managed-child-process.mts"; | |
| import { createBoundedChildOutput } from "../../test/helpers/bounded-child-output.js"; | |
| import { createFixtureLifetime } from "../../test/helpers/fixture-lifetime.js"; | |
| import { createDeferred, withTestTimeout } from "../../test/helpers/promise.js"; | |
| import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "../infra/runtime-worker-url.js"; | |
| import { logInfo } from "../logger.js"; | |
| import { writeConfigMachineState } from "../state/config-machine-state-write.js"; | |
| import { | |
| readConfigMachineState, | |
| readConfigMachineStateWithMetadata, | |
| } from "../state/config-machine-state.js"; | |
| import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; | |
| import { nodeHostConfigRuntimeEntrypoint } from "./config-runtime.test-support.js"; | |
| import { | |
| configureNodeHost, | |
| loadNodeHostConfig, | |
| NODE_HOST_CONFIG_KEY, | |
| type NodeHostConfig, | |
| } from "./config.js"; | |
| const fixtureDigest = ["fixture", "digest"].join("-"); | |
| const fixture = createFixtureLifetime(); | |
| vi.mock("../logger.js", () => ({ logInfo: vi.fn() })); | |
| async function runConcurrentImplicitConfigures( | |
| stateDir: string, | |
| signal: AbortSignal, | |
| ): Promise<[NodeHostConfig, NodeHostConfig]> { | |
| const workerUrl = resolveRuntimeWorkerUrl(nodeHostConfigRuntimeEntrypoint); | |
| const cancellation = new AbortController(); | |
| const childSignal = AbortSignal.any([signal, cancellation.signal]); | |
| const workers = ["candidate-a", "candidate-b"].map((candidate, index) => { | |
| const ready = createDeferred<ChildProcess>(); | |
| const stderr = createBoundedChildOutput(); | |
| let config: NodeHostConfig | undefined; | |
| const outcome = fixture.track( | |
| runManagedCommand({ | |
| bin: process.execPath, | |
| args: [...resolveRuntimeWorkerArgv(workerUrl), candidate, String(index + 1)], | |
| env: { ...process.env, OPENCLAW_STATE_DIR: stateDir }, | |
| shell: false, | |
| stdio: ["ignore", "ignore", "pipe", "ipc"], | |
| signal: childSignal, | |
| requireProcessTreeExit: process.platform !== "win32", | |
| onReady(child) { | |
| child.stderr!.on("data", stderr.append); | |
| child.on("message", (message: "ready" | NodeHostConfig) => { | |
| if (message === "ready") { | |
| ready.resolve(child); | |
| } else { | |
| config = message; | |
| } | |
| }); | |
| }, | |
| }).then((code) => { | |
| if (code !== 0) { | |
| throw new Error(`configure worker failed (${code}): ${stderr.text()}`); | |
| } | |
| if (!config) { | |
| throw new Error("configure worker produced no result"); | |
| } | |
| return config; | |
| }), | |
| ); | |
| return { | |
| ready: Promise.race([ | |
| ready.promise, | |
| outcome.then(() => { | |
| throw new Error("configure worker exited before the start barrier"); | |
| }), | |
| ]), | |
| outcome, | |
| }; | |
| }); | |
| try { | |
| // Both real processes must finish imports before either can choose its implicit id. | |
| const children = await withTestTimeout( | |
| Promise.all(workers.map(({ ready }) => ready)), | |
| 15_000, | |
| "timed out waiting for concurrent configure workers", | |
| ); | |
| for (const child of children) { | |
| child.send("start"); | |
| } | |
| const outcomes = await Promise.all(workers.map(({ outcome }) => outcome)); | |
| const first = outcomes[0]; | |
| const second = outcomes[1]; | |
| if (!first || !second) { | |
| throw new Error("expected two concurrent configure results"); | |
| } | |
| return [first, second]; | |
| } finally { | |
| cancellation.abort(); | |
| await Promise.allSettled(workers.map(({ outcome }) => outcome)); | |
| } | |
| } | |
| describe("node-host SQLite config", () => { | |
| afterEach(async () => { | |
| closeOpenClawStateDatabaseForTest(); | |
| await fixture.cleanup(); | |
| }); | |
| function makeTestEnv(): { env: NodeJS.ProcessEnv; stateDir: string } { | |
| const stateDir = fixture.createTempDir("openclaw-node-host-config-"); | |
| return { env: { ...process.env, OPENCLAW_STATE_DIR: stateDir }, stateDir }; | |
| } | |
| it("round-trips the complete gateway snapshot across database reopen", async () => { | |
| const { env, stateDir } = makeTestEnv(); | |
| const configured = await configureNodeHost({ | |
| nodeId: "node-custom", | |
| displayName: "Build Node", | |
| fallbackDisplayName: "fallback", | |
| gateway: { | |
| host: "gateway.local", | |
| port: 18443, | |
| tls: false, | |
| tlsFingerprint: fixtureDigest, | |
| contextPath: "/openclaw-gw", | |
| cloudflareAccess: { | |
| clientId: { source: "env", provider: "default", id: "CF_ACCESS_CLIENT_ID" }, | |
| clientSecret: { | |
| source: "env", | |
| provider: "default", | |
| id: "CF_ACCESS_CLIENT_SECRET", | |
| }, | |
| }, | |
| }, | |
| env, | |
| nowMs: 1_234, | |
| }); | |
| expect(configured).toEqual({ | |
| version: 1, | |
| nodeId: "node-custom", | |
| displayName: "Build Node", | |
| installedAppsSharing: false, | |
| gateway: { | |
| host: "gateway.local", | |
| port: 18443, | |
| tls: false, | |
| tlsFingerprint: fixtureDigest, | |
| contextPath: "/openclaw-gw", | |
| cloudflareAccess: { | |
| clientId: { source: "env", provider: "default", id: "CF_ACCESS_CLIENT_ID" }, | |
| clientSecret: { | |
| source: "env", | |
| provider: "default", | |
| id: "CF_ACCESS_CLIENT_SECRET", | |
| }, | |
| }, | |
| }, | |
| }); | |
| expect(readConfigMachineState<NodeHostConfig>(NODE_HOST_CONFIG_KEY, { env })).toEqual( | |
| configured, | |
| ); | |
| expect(readConfigMachineStateWithMetadata(NODE_HOST_CONFIG_KEY, { env })?.updatedAtMs).toBe( | |
| 1_234, | |
| ); | |
| expect(readConfigMachineState(NODE_HOST_CONFIG_KEY, { env })).not.toHaveProperty("token"); | |
| closeOpenClawStateDatabaseForTest(); | |
| await expect(loadNodeHostConfig(env)).resolves.toEqual(configured); | |
| await expect(fs.stat(path.join(stateDir, "node.json"))).rejects.toMatchObject({ | |
| code: "ENOENT", | |
| }); | |
| }); | |
| it("keeps installed-app sharing disabled by default and persists an explicit enable", async () => { | |
| const { env } = makeTestEnv(); | |
| const initial = await configureNodeHost({ | |
| fallbackDisplayName: "node", | |
| gateway: {}, | |
| env, | |
| nowMs: 1, | |
| }); | |
| expect(initial.installedAppsSharing).toBe(false); | |
| const enabled = await configureNodeHost({ | |
| fallbackDisplayName: "node", | |
| gateway: {}, | |
| installedAppsSharing: true, | |
| env, | |
| nowMs: 2, | |
| }); | |
| expect(enabled.installedAppsSharing).toBe(true); | |
| closeOpenClawStateDatabaseForTest(); | |
| await expect(loadNodeHostConfig(env)).resolves.toMatchObject({ installedAppsSharing: true }); | |
| }); | |
| it("normalizes command restrictions and preserves them across reopen and endpoint updates", async () => { | |
| const { env } = makeTestEnv(); | |
| await configureNodeHost({ | |
| fallbackDisplayName: "node", | |
| gateway: {}, | |
| commands: [" fixture.read ", "fixture.list", "fixture.read"], | |
| env, | |
| }); | |
| closeOpenClawStateDatabaseForTest(); | |
| await expect(loadNodeHostConfig(env)).resolves.toMatchObject({ | |
| commands: ["fixture.list", "fixture.read"], | |
| }); | |
| await expect( | |
| configureNodeHost({ fallbackDisplayName: "node", gateway: { host: "new.example" }, env }), | |
| ).resolves.toMatchObject({ commands: ["fixture.list", "fixture.read"] }); | |
| await configureNodeHost({ fallbackDisplayName: "node", gateway: {}, commands: [], env }); | |
| closeOpenClawStateDatabaseForTest(); | |
| await expect(loadNodeHostConfig(env)).resolves.toMatchObject({ commands: [] }); | |
| }); | |
| it("removes a saved command allowlist durably when all commands are selected", async () => { | |
| vi.mocked(logInfo).mockClear(); | |
| const { env } = makeTestEnv(); | |
| const params = { fallbackDisplayName: "node", gateway: {}, env }; | |
| const restricted = await configureNodeHost({ ...params, commands: ["fixture.read"] }); | |
| const cleared = await configureNodeHost({ ...params, allCommands: true }); | |
| expect(cleared).not.toHaveProperty("commands"); | |
| expect(cleared.nodeId).toBe(restricted.nodeId); | |
| closeOpenClawStateDatabaseForTest(); | |
| expect(readConfigMachineState(NODE_HOST_CONFIG_KEY, { env })).not.toHaveProperty("commands"); | |
| await expect(loadNodeHostConfig(env)).resolves.not.toHaveProperty("commands"); | |
| await expect(configureNodeHost(params)).resolves.not.toHaveProperty("commands"); | |
| await configureNodeHost({ ...params, allCommands: true }); | |
| expect(logInfo).toHaveBeenCalledExactlyOnceWith( | |
| "node-host: cleared saved command allowlist; advertising the full default command surface", | |
| ); | |
| }); | |
| it.each([{ commands: "fixture.read" }, { commands: [""] }, { commands: [42] }])( | |
| "rejects corrupt command restrictions rather than exposing the full host: $commands", | |
| async ({ commands }) => { | |
| const { env } = makeTestEnv(); | |
| writeConfigMachineState( | |
| NODE_HOST_CONFIG_KEY, | |
| { version: 1, nodeId: "node", commands }, | |
| { env }, | |
| ); | |
| await expect(loadNodeHostConfig(env)).rejects.toThrow("invalid node-host commands"); | |
| }, | |
| ); | |
| it("keeps the first committed implicit node id across processes", async ({ signal }) => { | |
| const { env, stateDir } = makeTestEnv(); | |
| const [first, second] = await fixture.run(() => | |
| runConcurrentImplicitConfigures(stateDir, signal), | |
| ); | |
| expect(["candidate-a", "candidate-b"]).toContain(first.nodeId); | |
| expect(second.nodeId).toBe(first.nodeId); | |
| expect(first.gateway).toBeUndefined(); | |
| expect(second.gateway).toBeUndefined(); | |
| await expect(loadNodeHostConfig(env)).resolves.toEqual(second); | |
| }, 30_000); | |
| it("preserves explicit custom ids and atomically clears omitted gateway fields", async () => { | |
| const { env } = makeTestEnv(); | |
| await configureNodeHost({ | |
| nodeId: "first-custom-id", | |
| fallbackDisplayName: "node", | |
| gateway: { | |
| host: "old.example", | |
| port: 443, | |
| tls: true, | |
| tlsFingerprint: fixtureDigest, | |
| contextPath: "/old", | |
| }, | |
| env, | |
| nowMs: 20, | |
| }); | |
| const configured = await configureNodeHost({ | |
| nodeId: "custom id with spaces inside", | |
| fallbackDisplayName: "node", | |
| gateway: { host: "new.example", port: 18789, tls: false }, | |
| env, | |
| nowMs: 21, | |
| }); | |
| expect(configured).toMatchObject({ | |
| nodeId: "custom id with spaces inside", | |
| gateway: { host: "new.example", port: 18789, tls: false }, | |
| }); | |
| expect(configured.gateway?.tlsFingerprint).toBeUndefined(); | |
| expect(configured.gateway?.contextPath).toBeUndefined(); | |
| await expect(loadNodeHostConfig(env)).resolves.toEqual(configured); | |
| }); | |
| it("rejects corrupt canonical rows instead of rotating identity", async () => { | |
| const { env } = makeTestEnv(); | |
| writeConfigMachineState(NODE_HOST_CONFIG_KEY, { version: 2, nodeId: "stale-node" }, { env }); | |
| await expect(loadNodeHostConfig(env)).rejects.toThrow("unsupported version 2"); | |
| await expect( | |
| configureNodeHost({ fallbackDisplayName: "node", gateway: {}, env }), | |
| ).rejects.toThrow("unsupported version 2"); | |
| }); | |
| it.each(["source", "claim", "dangling-source-symlink"] as const)( | |
| "blocks runtime while retired state remains: %s", | |
| async (kind) => { | |
| const { env, stateDir } = makeTestEnv(); | |
| const sourcePath = path.join(stateDir, "node.json"); | |
| const claimPath = `${sourcePath}.doctor-importing`; | |
| if (kind === "source") { | |
| await fs.writeFile(sourcePath, "{}\n", "utf8"); | |
| } else if (kind === "claim") { | |
| await fs.writeFile(claimPath, "{}\n", "utf8"); | |
| } else { | |
| await fs.symlink(path.join(stateDir, "missing-node.json"), sourcePath); | |
| } | |
| await expect(loadNodeHostConfig(env)).rejects.toThrow("openclaw doctor --fix"); | |
| await expect( | |
| configureNodeHost({ fallbackDisplayName: "node", gateway: {}, env }), | |
| ).rejects.toThrow("openclaw doctor --fix"); | |
| }, | |
| ); | |
| }); | |