diff --git a/src/cli/gateway-cli/dev.integration.test.ts b/src/cli/gateway-cli/dev.integration.test.ts new file mode 100644 index 000000000000..ad21300f03a2 --- /dev/null +++ b/src/cli/gateway-cli/dev.integration.test.ts @@ -0,0 +1,44 @@ +// Proves a fresh dev gateway can replace the synthetic implicit roster through real config IO. +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { resetConfigRuntimeState } from "../../config/config.js"; +import { withEnvAsync } from "../../test-utils/env.js"; +import { ensureDevGatewayConfig } from "./dev.js"; + +describe("ensureDevGatewayConfig integration", () => { + const tempDirs: string[] = []; + + afterEach(async () => { + resetConfigRuntimeState(); + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); + }); + + it("writes the dedicated dev roster into a fresh state directory", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "openclaw-dev-config-integration-")); + tempDirs.push(root); + const stateDir = path.join(root, "state"); + const configPath = path.join(stateDir, "openclaw.json"); + const workspace = path.join(root, "workspace"); + + await withEnvAsync( + { + OPENCLAW_CONFIG_PATH: configPath, + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_WORKSPACE_DIR: workspace, + }, + async () => { + resetConfigRuntimeState(); + await ensureDevGatewayConfig({}); + }, + ); + + const config = JSON.parse(await readFile(configPath, "utf8")) as { + agents?: { entries?: Record }; + }; + expect(config.agents?.entries).toEqual({ + dev: { default: true, workspace: `${workspace}-dev`, identity: expect.any(Object) }, + }); + }); +}); diff --git a/src/cli/gateway-cli/dev.test.ts b/src/cli/gateway-cli/dev.test.ts index 3e22c71703f9..c20caa2a873d 100644 --- a/src/cli/gateway-cli/dev.test.ts +++ b/src/cli/gateway-cli/dev.test.ts @@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({ configPath: "", workspace: "", nextConfig: undefined as unknown, + writeOptions: undefined as unknown, replaceConfigFile: vi.fn(), })); @@ -48,9 +49,12 @@ describe("ensureDevGatewayConfig", () => { mocks.configPath = path.join(tempDir, "openclaw.json"); mocks.workspace = path.join(tempDir, "workspace"); mocks.nextConfig = undefined; + mocks.writeOptions = undefined; mocks.replaceConfigFile.mockReset(); mocks.replaceConfigFile.mockImplementation(async (options: unknown) => { - mocks.nextConfig = (options as { nextConfig: unknown }).nextConfig; + const configOptions = options as { nextConfig: unknown; writeOptions?: unknown }; + mocks.nextConfig = configOptions.nextConfig; + mocks.writeOptions = configOptions.writeOptions; }); }); @@ -76,5 +80,6 @@ describe("ensureDevGatewayConfig", () => { }, }); expect(OpenClawSchema.safeParse(mocks.nextConfig).success).toBe(true); + expect(mocks.writeOptions).toEqual({ allowedAgentRosterRemovals: ["main"] }); }); }); diff --git a/src/cli/gateway-cli/dev.ts b/src/cli/gateway-cli/dev.ts index db7e970f5f8f..8751217a1668 100644 --- a/src/cli/gateway-cli/dev.ts +++ b/src/cli/gateway-cli/dev.ts @@ -8,6 +8,7 @@ import { resolveWorkspaceTemplateSearchDirs } from "../../agents/workspace-templ import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js"; import { handleReset } from "../../commands/onboard-helpers.js"; import { createConfigIO, replaceConfigFile } from "../../config/config.js"; +import { LEGACY_IMPLICIT_AGENT_ID } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveUserPath, shortenHomePath } from "../../utils.js"; @@ -128,6 +129,9 @@ export async function ensureDevGatewayConfig(opts: { reset?: boolean }) { }, }, afterWrite: { mode: "auto" }, + // An absent config resolves to the implicit legacy agent before this full + // replacement. Declare only that synthetic deletion; authored rosters stay protected. + writeOptions: { allowedAgentRosterRemovals: [LEGACY_IMPLICIT_AGENT_ID] }, }); await ensureDevWorkspace(workspace); defaultRuntime.log(`Dev config ready: ${shortenHomePath(configPath)}`);