mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:48:27 +00:00
refactor(channels): dedupe transport and gateway test scaffolds
This commit is contained in:
@@ -84,51 +84,68 @@ const makeContext = (): GatewayRequestContext =>
|
||||
logGateway: { info: vi.fn(), error: vi.fn() },
|
||||
}) as unknown as GatewayRequestContext;
|
||||
|
||||
function mockMainSessionEntry(entry: Record<string, unknown>, cfg: Record<string, unknown> = {}) {
|
||||
mocks.loadSessionEntry.mockReturnValue({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
entry: {
|
||||
sessionId: "existing-session-id",
|
||||
updatedAt: Date.now(),
|
||||
...entry,
|
||||
},
|
||||
canonicalKey: "agent:main:main",
|
||||
});
|
||||
}
|
||||
|
||||
function captureUpdatedMainEntry() {
|
||||
let capturedEntry: Record<string, unknown> | undefined;
|
||||
mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
|
||||
const store: Record<string, unknown> = {};
|
||||
await updater(store);
|
||||
capturedEntry = store["agent:main:main"] as Record<string, unknown>;
|
||||
});
|
||||
return () => capturedEntry;
|
||||
}
|
||||
|
||||
async function runMainAgent(message: string, idempotencyKey: string) {
|
||||
const respond = vi.fn();
|
||||
await agentHandlers.agent({
|
||||
params: {
|
||||
message,
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey,
|
||||
},
|
||||
respond,
|
||||
context: makeContext(),
|
||||
req: { type: "req", id: idempotencyKey, method: "agent" },
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
return respond;
|
||||
}
|
||||
|
||||
describe("gateway agent handler", () => {
|
||||
it("preserves cliSessionIds from existing session entry", async () => {
|
||||
const existingCliSessionIds = { "claude-cli": "abc-123-def" };
|
||||
const existingClaudeCliSessionId = "abc-123-def";
|
||||
|
||||
mocks.loadSessionEntry.mockReturnValue({
|
||||
cfg: {},
|
||||
storePath: "/tmp/sessions.json",
|
||||
entry: {
|
||||
sessionId: "existing-session-id",
|
||||
updatedAt: Date.now(),
|
||||
cliSessionIds: existingCliSessionIds,
|
||||
claudeCliSessionId: existingClaudeCliSessionId,
|
||||
},
|
||||
canonicalKey: "agent:main:main",
|
||||
mockMainSessionEntry({
|
||||
cliSessionIds: existingCliSessionIds,
|
||||
claudeCliSessionId: existingClaudeCliSessionId,
|
||||
});
|
||||
|
||||
let capturedEntry: Record<string, unknown> | undefined;
|
||||
mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
|
||||
const store: Record<string, unknown> = {};
|
||||
await updater(store);
|
||||
capturedEntry = store["agent:main:main"] as Record<string, unknown>;
|
||||
});
|
||||
const getCapturedEntry = captureUpdatedMainEntry();
|
||||
|
||||
mocks.agentCommand.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { durationMs: 100 },
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await agentHandlers.agent({
|
||||
params: {
|
||||
message: "test",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey: "test-idem",
|
||||
},
|
||||
respond,
|
||||
context: makeContext(),
|
||||
req: { type: "req", id: "1", method: "agent" },
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
await runMainAgent("test", "test-idem");
|
||||
|
||||
expect(mocks.updateSessionStore).toHaveBeenCalled();
|
||||
const capturedEntry = getCapturedEntry();
|
||||
expect(capturedEntry).toBeDefined();
|
||||
expect(capturedEntry?.cliSessionIds).toEqual(existingCliSessionIds);
|
||||
expect(capturedEntry?.claudeCliSessionId).toBe(existingClaudeCliSessionId);
|
||||
@@ -188,45 +205,19 @@ describe("gateway agent handler", () => {
|
||||
});
|
||||
|
||||
it("handles missing cliSessionIds gracefully", async () => {
|
||||
mocks.loadSessionEntry.mockReturnValue({
|
||||
cfg: {},
|
||||
storePath: "/tmp/sessions.json",
|
||||
entry: {
|
||||
sessionId: "existing-session-id",
|
||||
updatedAt: Date.now(),
|
||||
// No cliSessionIds or claudeCliSessionId
|
||||
},
|
||||
canonicalKey: "agent:main:main",
|
||||
});
|
||||
mockMainSessionEntry({});
|
||||
|
||||
let capturedEntry: Record<string, unknown> | undefined;
|
||||
mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
|
||||
const store: Record<string, unknown> = {};
|
||||
await updater(store);
|
||||
capturedEntry = store["agent:main:main"] as Record<string, unknown>;
|
||||
});
|
||||
const getCapturedEntry = captureUpdatedMainEntry();
|
||||
|
||||
mocks.agentCommand.mockResolvedValue({
|
||||
payloads: [{ text: "ok" }],
|
||||
meta: { durationMs: 100 },
|
||||
});
|
||||
|
||||
const respond = vi.fn();
|
||||
await agentHandlers.agent({
|
||||
params: {
|
||||
message: "test",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey: "test-idem-2",
|
||||
},
|
||||
respond,
|
||||
context: makeContext(),
|
||||
req: { type: "req", id: "2", method: "agent" },
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
await runMainAgent("test", "test-idem-2");
|
||||
|
||||
expect(mocks.updateSessionStore).toHaveBeenCalled();
|
||||
const capturedEntry = getCapturedEntry();
|
||||
expect(capturedEntry).toBeDefined();
|
||||
// Should be undefined, not cause an error
|
||||
expect(capturedEntry?.cliSessionIds).toBeUndefined();
|
||||
|
||||
@@ -125,6 +125,20 @@ function createErrnoError(code: string) {
|
||||
return err;
|
||||
}
|
||||
|
||||
function mockWorkspaceStateRead(params: { onboardingCompletedAt?: string; errorCode?: string }) {
|
||||
mocks.fsReadFile.mockImplementation(async (filePath: string | URL | number) => {
|
||||
if (String(filePath).endsWith("workspace-state.json")) {
|
||||
if (params.errorCode) {
|
||||
throw createErrnoError(params.errorCode);
|
||||
}
|
||||
return JSON.stringify({
|
||||
onboardingCompletedAt: params.onboardingCompletedAt ?? "2026-02-15T14:00:00.000Z",
|
||||
});
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.fsReadFile.mockImplementation(async () => {
|
||||
throw createEnoentError();
|
||||
@@ -413,14 +427,7 @@ describe("agents.files.list", () => {
|
||||
});
|
||||
|
||||
it("hides BOOTSTRAP.md when workspace onboarding is complete", async () => {
|
||||
mocks.fsReadFile.mockImplementation(async (filePath: string | URL | number) => {
|
||||
if (String(filePath).endsWith("workspace-state.json")) {
|
||||
return JSON.stringify({
|
||||
onboardingCompletedAt: "2026-02-15T14:00:00.000Z",
|
||||
});
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
mockWorkspaceStateRead({ onboardingCompletedAt: "2026-02-15T14:00:00.000Z" });
|
||||
|
||||
const { respond, promise } = makeCall("agents.files.list", { agentId: "main" });
|
||||
await promise;
|
||||
@@ -431,12 +438,7 @@ describe("agents.files.list", () => {
|
||||
});
|
||||
|
||||
it("falls back to showing BOOTSTRAP.md when workspace state cannot be read", async () => {
|
||||
mocks.fsReadFile.mockImplementation(async (filePath: string | URL | number) => {
|
||||
if (String(filePath).endsWith("workspace-state.json")) {
|
||||
throw createErrnoError("EACCES");
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
mockWorkspaceStateRead({ errorCode: "EACCES" });
|
||||
|
||||
const { respond, promise } = makeCall("agents.files.list", { agentId: "main" });
|
||||
await promise;
|
||||
|
||||
@@ -241,6 +241,77 @@ describe("gateway chat transcript writes (guardrail)", () => {
|
||||
|
||||
describe("exec approval handlers", () => {
|
||||
const execApprovalNoop = () => {};
|
||||
type ExecApprovalHandlers = ReturnType<typeof createExecApprovalHandlers>;
|
||||
type ExecApprovalRequestArgs = Parameters<ExecApprovalHandlers["exec.approval.request"]>[0];
|
||||
type ExecApprovalResolveArgs = Parameters<ExecApprovalHandlers["exec.approval.resolve"]>[0];
|
||||
|
||||
const defaultExecApprovalRequestParams = {
|
||||
command: "echo ok",
|
||||
cwd: "/tmp",
|
||||
host: "node",
|
||||
timeoutMs: 2000,
|
||||
} as const;
|
||||
|
||||
function toExecApprovalRequestContext(context: {
|
||||
broadcast: (event: string, payload: unknown) => void;
|
||||
}): ExecApprovalRequestArgs["context"] {
|
||||
return context as unknown as ExecApprovalRequestArgs["context"];
|
||||
}
|
||||
|
||||
function toExecApprovalResolveContext(context: {
|
||||
broadcast: (event: string, payload: unknown) => void;
|
||||
}): ExecApprovalResolveArgs["context"] {
|
||||
return context as unknown as ExecApprovalResolveArgs["context"];
|
||||
}
|
||||
|
||||
async function requestExecApproval(params: {
|
||||
handlers: ExecApprovalHandlers;
|
||||
respond: ReturnType<typeof vi.fn>;
|
||||
context: { broadcast: (event: string, payload: unknown) => void };
|
||||
params?: Record<string, unknown>;
|
||||
}) {
|
||||
const requestParams = {
|
||||
...defaultExecApprovalRequestParams,
|
||||
...params.params,
|
||||
} as unknown as ExecApprovalRequestArgs["params"];
|
||||
return params.handlers["exec.approval.request"]({
|
||||
params: requestParams,
|
||||
respond: params.respond,
|
||||
context: toExecApprovalRequestContext(params.context),
|
||||
client: null,
|
||||
req: { id: "req-1", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveExecApproval(params: {
|
||||
handlers: ExecApprovalHandlers;
|
||||
id: string;
|
||||
respond: ReturnType<typeof vi.fn>;
|
||||
context: { broadcast: (event: string, payload: unknown) => void };
|
||||
}) {
|
||||
return params.handlers["exec.approval.resolve"]({
|
||||
params: { id: params.id, decision: "allow-once" } as ExecApprovalResolveArgs["params"],
|
||||
respond: params.respond,
|
||||
context: toExecApprovalResolveContext(params.context),
|
||||
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
|
||||
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
});
|
||||
}
|
||||
|
||||
function createExecApprovalFixture() {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const broadcasts: Array<{ event: string; payload: unknown }> = [];
|
||||
const respond = vi.fn();
|
||||
const context = {
|
||||
broadcast: (event: string, payload: unknown) => {
|
||||
broadcasts.push({ event, payload });
|
||||
},
|
||||
};
|
||||
return { handlers, broadcasts, respond, context };
|
||||
}
|
||||
|
||||
describe("ExecApprovalRequestParams validation", () => {
|
||||
it("accepts request with resolvedPath omitted", () => {
|
||||
@@ -284,32 +355,13 @@ describe("exec approval handlers", () => {
|
||||
});
|
||||
|
||||
it("broadcasts request + resolve", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const broadcasts: Array<{ event: string; payload: unknown }> = [];
|
||||
const { handlers, broadcasts, respond, context } = createExecApprovalFixture();
|
||||
|
||||
const respond = vi.fn();
|
||||
const context = {
|
||||
broadcast: (event: string, payload: unknown) => {
|
||||
broadcasts.push({ event, payload });
|
||||
},
|
||||
};
|
||||
|
||||
const requestPromise = handlers["exec.approval.request"]({
|
||||
params: {
|
||||
command: "echo ok",
|
||||
cwd: "/tmp",
|
||||
host: "node",
|
||||
timeoutMs: 2000,
|
||||
twoPhase: true,
|
||||
},
|
||||
const requestPromise = requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.request"]
|
||||
>[0]["context"],
|
||||
client: null,
|
||||
req: { id: "req-1", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
context,
|
||||
params: { twoPhase: true },
|
||||
});
|
||||
|
||||
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
|
||||
@@ -324,15 +376,11 @@ describe("exec approval handlers", () => {
|
||||
);
|
||||
|
||||
const resolveRespond = vi.fn();
|
||||
await handlers["exec.approval.resolve"]({
|
||||
params: { id, decision: "allow-once" },
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id,
|
||||
respond: resolveRespond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.resolve"]
|
||||
>[0]["context"],
|
||||
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
|
||||
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
context,
|
||||
});
|
||||
|
||||
await requestPromise;
|
||||
@@ -362,33 +410,19 @@ describe("exec approval handlers", () => {
|
||||
return;
|
||||
}
|
||||
const id = (payload as { id?: string })?.id ?? "";
|
||||
void handlers["exec.approval.resolve"]({
|
||||
params: { id, decision: "allow-once" },
|
||||
void resolveExecApproval({
|
||||
handlers,
|
||||
id,
|
||||
respond: resolveRespond,
|
||||
context: resolveContext as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.resolve"]
|
||||
>[0]["context"],
|
||||
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
|
||||
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
context: resolveContext,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
await handlers["exec.approval.request"]({
|
||||
params: {
|
||||
command: "echo ok",
|
||||
cwd: "/tmp",
|
||||
host: "node",
|
||||
timeoutMs: 2000,
|
||||
},
|
||||
await requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.request"]
|
||||
>[0]["context"],
|
||||
client: null,
|
||||
req: { id: "req-1", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(resolveRespond).toHaveBeenCalledWith(true, { ok: true }, undefined);
|
||||
@@ -400,32 +434,13 @@ describe("exec approval handlers", () => {
|
||||
});
|
||||
|
||||
it("accepts explicit approval ids", async () => {
|
||||
const manager = new ExecApprovalManager();
|
||||
const handlers = createExecApprovalHandlers(manager);
|
||||
const broadcasts: Array<{ event: string; payload: unknown }> = [];
|
||||
const { handlers, broadcasts, respond, context } = createExecApprovalFixture();
|
||||
|
||||
const respond = vi.fn();
|
||||
const context = {
|
||||
broadcast: (event: string, payload: unknown) => {
|
||||
broadcasts.push({ event, payload });
|
||||
},
|
||||
};
|
||||
|
||||
const requestPromise = handlers["exec.approval.request"]({
|
||||
params: {
|
||||
id: "approval-123",
|
||||
command: "echo ok",
|
||||
cwd: "/tmp",
|
||||
host: "gateway",
|
||||
timeoutMs: 2000,
|
||||
},
|
||||
const requestPromise = requestExecApproval({
|
||||
handlers,
|
||||
respond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.request"]
|
||||
>[0]["context"],
|
||||
client: null,
|
||||
req: { id: "req-1", type: "req", method: "exec.approval.request" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
context,
|
||||
params: { id: "approval-123", host: "gateway" },
|
||||
});
|
||||
|
||||
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
|
||||
@@ -433,15 +448,11 @@ describe("exec approval handlers", () => {
|
||||
expect(id).toBe("approval-123");
|
||||
|
||||
const resolveRespond = vi.fn();
|
||||
await handlers["exec.approval.resolve"]({
|
||||
params: { id, decision: "allow-once" },
|
||||
await resolveExecApproval({
|
||||
handlers,
|
||||
id,
|
||||
respond: resolveRespond,
|
||||
context: context as unknown as Parameters<
|
||||
(typeof handlers)["exec.approval.resolve"]
|
||||
>[0]["context"],
|
||||
client: { connect: { client: { id: "cli", displayName: "CLI" } } },
|
||||
req: { id: "req-2", type: "req", method: "exec.approval.resolve" },
|
||||
isWebchatConnect: execApprovalNoop,
|
||||
context,
|
||||
});
|
||||
|
||||
await requestPromise;
|
||||
|
||||
Reference in New Issue
Block a user