test(gateway): stabilize suite session-store config (#52193)

* test(gateway): stabilize suite session-store config

* test(gateway): preserve seeded config semantics

* test(gateway): update seeded session store overrides
This commit is contained in:
Luke
2026-03-22 22:18:44 +11:00
committed by GitHub
parent c70ae1c96e
commit ad24fccff5
4 changed files with 99 additions and 26 deletions

View File

@@ -6,8 +6,8 @@ const GATEWAY_CLIENT_CONSTRUCTOR_PATTERN = /new\s+GatewayClient\s*\(/;
const ALLOWED_GATEWAY_CLIENT_CALLSITES = new Set([
"src/acp/server.ts",
"extensions/discord/src/monitor/exec-approvals.ts",
"src/gateway/call.ts",
"src/gateway/operator-approvals-client.ts",
"src/gateway/probe.ts",
"src/node-host/runner.ts",
"src/tui/gateway-chat.ts",

View File

@@ -109,6 +109,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
"chat.abort",
"sessions.create",
"sessions.send",
"sessions.steer",
"sessions.abort",
"browser.request",
"push.test",

View File

@@ -185,6 +185,26 @@ const hoisted = vi.hoisted(() => ({
testTailscaleWhois: { value: null as TailscaleWhoisIdentity | null },
getReplyFromConfig: vi.fn<GetReplyFromConfigFn>().mockResolvedValue(undefined),
sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }),
testState: {
agentConfig: undefined as Record<string, unknown> | undefined,
agentsConfig: undefined as Record<string, unknown> | undefined,
bindingsConfig: undefined as AgentBinding[] | undefined,
channelsConfig: undefined as Record<string, unknown> | undefined,
sessionStorePath: undefined as string | undefined,
sessionConfig: undefined as Record<string, unknown> | undefined,
allowFrom: undefined as string[] | undefined,
cronStorePath: undefined as string | undefined,
cronEnabled: false as boolean | undefined,
gatewayBind: undefined as "auto" | "lan" | "tailnet" | "loopback" | undefined,
gatewayAuth: undefined as Record<string, unknown> | undefined,
gatewayControlUi: undefined as Record<string, unknown> | undefined,
hooksConfig: undefined as HooksConfig | undefined,
canvasHostPort: undefined as number | undefined,
legacyIssues: [] as Array<{ path: string; message: string }>,
legacyParsed: {} as Record<string, unknown>,
migrationConfig: null as Record<string, unknown> | null,
migrationChanges: [] as string[],
},
}));
const pluginRegistryState = {
@@ -218,26 +238,7 @@ export const cronIsolatedRun = hoisted.cronIsolatedRun;
export const agentCommand: Mock<() => void> = hoisted.agentCommand;
export const getReplyFromConfig: Mock<GetReplyFromConfigFn> = hoisted.getReplyFromConfig;
export const testState = {
agentConfig: undefined as Record<string, unknown> | undefined,
agentsConfig: undefined as Record<string, unknown> | undefined,
bindingsConfig: undefined as AgentBinding[] | undefined,
channelsConfig: undefined as Record<string, unknown> | undefined,
sessionStorePath: undefined as string | undefined,
sessionConfig: undefined as Record<string, unknown> | undefined,
allowFrom: undefined as string[] | undefined,
cronStorePath: undefined as string | undefined,
cronEnabled: false as boolean | undefined,
gatewayBind: undefined as "auto" | "lan" | "tailnet" | "loopback" | undefined,
gatewayAuth: undefined as Record<string, unknown> | undefined,
gatewayControlUi: undefined as Record<string, unknown> | undefined,
hooksConfig: undefined as HooksConfig | undefined,
canvasHostPort: undefined as number | undefined,
legacyIssues: [] as Array<{ path: string; message: string }>,
legacyParsed: {} as Record<string, unknown>,
migrationConfig: null as Record<string, unknown> | null,
migrationChanges: [] as string[],
};
export const testState = hoisted.testState;
export const testIsNixMode = hoisted.testIsNixMode;
export const sessionStoreSaveDelayMs = hoisted.sessionStoreSaveDelayMs;
@@ -530,8 +531,11 @@ vi.mock("../config/config.js", async () => {
} catch {
fileConfig = {};
}
return applyPluginAutoEnable({ config: composeTestConfig(fileConfig), env: process.env })
.config;
const config = applyPluginAutoEnable({
config: composeTestConfig(fileConfig),
env: process.env,
}).config;
return config;
},
parseConfigJson5: (raw: string) => {
try {

View File

@@ -4,7 +4,16 @@ import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from "vitest";
import { WebSocket } from "ws";
import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js";
import {
clearConfigCache,
clearRuntimeConfigSnapshot,
parseConfigJson5,
} from "../config/config.js";
import {
clearSessionStoreCacheForTest,
resolveMainSessionKeyFromConfig,
type SessionEntry,
} from "../config/sessions.js";
import { resetAgentRunContextForTest } from "../infra/agent-events.js";
import {
loadOrCreateDeviceIdentity,
@@ -63,6 +72,58 @@ let tempHome: string | undefined;
let tempConfigRoot: string | undefined;
let suiteConfigRootSeq = 0;
async function persistTestSessionStorePath(storePath: string): Promise<void> {
const configPaths = new Set<string>();
if (process.env.OPENCLAW_CONFIG_PATH) {
configPaths.add(process.env.OPENCLAW_CONFIG_PATH);
}
if (process.env.OPENCLAW_STATE_DIR) {
configPaths.add(path.join(process.env.OPENCLAW_STATE_DIR, "openclaw.json"));
}
const parsedConfigs = new Map<string, Record<string, unknown>>();
let preservedTemplateStore: string | undefined;
for (const configPath of configPaths) {
let config: Record<string, unknown> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
const parsed = parseConfigJson5(raw);
if (
parsed.ok &&
parsed.parsed &&
typeof parsed.parsed === "object" &&
!Array.isArray(parsed.parsed)
) {
config = parsed.parsed as Record<string, unknown>;
}
} catch {
config = {};
}
parsedConfigs.set(configPath, config);
const session =
config.session && typeof config.session === "object" && !Array.isArray(config.session)
? (config.session as Record<string, unknown>)
: undefined;
const existingStore = typeof session?.store === "string" ? session.store.trim() : "";
if (!preservedTemplateStore && existingStore.includes("{agentId}")) {
preservedTemplateStore = existingStore;
}
}
const nextStoreValue = preservedTemplateStore || storePath;
for (const configPath of configPaths) {
const config = { ...parsedConfigs.get(configPath) };
const session =
config.session && typeof config.session === "object" && !Array.isArray(config.session)
? { ...(config.session as Record<string, unknown>) }
: {};
session.store = nextStoreValue;
config.session = session;
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
}
clearRuntimeConfigSnapshot();
clearConfigCache();
}
export async function writeSessionStore(params: {
entries: Record<string, Partial<SessionEntry>>;
storePath?: string;
@@ -87,8 +148,13 @@ export async function writeSessionStore(params: {
});
store[storeKey] = entry;
}
// Gateway suites often reuse the same store path across tests while writing the
// file directly; clear the in-process cache so handlers reload the seeded state.
clearSessionStoreCacheForTest();
await persistTestSessionStorePath(storePath);
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(store, null, 2), "utf-8");
clearSessionStoreCacheForTest();
}
async function setupGatewayTestHome() {
@@ -133,6 +199,8 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) {
await fs.mkdir(tempConfigRoot, { recursive: true });
}
setTestConfigRoot(tempConfigRoot);
clearRuntimeConfigSnapshot();
clearConfigCache();
sessionStoreSaveDelayMs.value = 0;
testTailnetIPv4.value = undefined;
testTailscaleWhois.value = null;
@@ -197,10 +265,10 @@ export function installGatewayTestHooks(options?: { scope?: "test" | "suite" })
if (scope === "suite") {
beforeAll(async () => {
await setupGatewayTestHome();
await resetGatewayTestState({ uniqueConfigRoot: true });
await resetGatewayTestState({ uniqueConfigRoot: false });
});
beforeEach(async () => {
await resetGatewayTestState({ uniqueConfigRoot: true });
await resetGatewayTestState({ uniqueConfigRoot: false });
}, 60_000);
afterEach(async () => {
await cleanupGatewayTestHome({ restoreEnv: false });