mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
test: repair lifecycle fixtures and remove redundant mocks (#138238)
This commit is contained in:
parent
00a7a128dd
commit
e7fffcf7a0
5 changed files with 45 additions and 34 deletions
|
|
@ -1,7 +1,8 @@
|
|||
// Browser tests cover browser request.shared control state plugin behavior.
|
||||
import { createServer } from "node:http";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { getFreePort } from "../browser/test-port.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
|
|
@ -31,6 +32,11 @@ vi.mock("../browser/control-auth.js", () => ({
|
|||
shouldAutoGenerateBrowserAuth: mocks.shouldAutoGenerateBrowserAuth,
|
||||
}));
|
||||
|
||||
// This suite tests shared state; server auth/bind suites cover real HTTP listeners.
|
||||
vi.mock("../browser/http-listen.js", () => ({
|
||||
listenBrowserHttpServer: vi.fn(async () => createServer()),
|
||||
}));
|
||||
|
||||
vi.mock("../browser/server-lifecycle.js", () => ({
|
||||
stopKnownBrowserProfiles: mocks.stopKnownBrowserProfiles,
|
||||
}));
|
||||
|
|
@ -109,6 +115,7 @@ async function browserRequestStatus(): Promise<unknown> {
|
|||
}
|
||||
|
||||
describe("browser.request local control state", () => {
|
||||
const controlPort = 18_791;
|
||||
afterEach(async () => {
|
||||
await stopBrowserControlService();
|
||||
await stopBrowserControlServer();
|
||||
|
|
@ -117,7 +124,6 @@ describe("browser.request local control state", () => {
|
|||
});
|
||||
|
||||
it("uses the same resolved browser config as the HTTP control service", async () => {
|
||||
const controlPort = await getFreePort();
|
||||
const gatewayPort = controlPort - 2;
|
||||
|
||||
mocks.runtimeConfig = browserConfig({
|
||||
|
|
@ -150,7 +156,6 @@ describe("browser.request local control state", () => {
|
|||
});
|
||||
|
||||
it("retains port auth until a failed stop is retried successfully", async () => {
|
||||
const controlPort = await getFreePort();
|
||||
mocks.runtimeConfig = browserConfig({ gatewayPort: controlPort - 2 });
|
||||
mocks.runtimeSourceConfig = mocks.runtimeConfig;
|
||||
mocks.ensureBrowserControlAuth.mockResolvedValueOnce({ auth: { token: "test-token" } });
|
||||
|
|
@ -167,21 +172,20 @@ describe("browser.request local control state", () => {
|
|||
});
|
||||
|
||||
it("clears auth when a stop queues behind cold startup", async () => {
|
||||
const controlPort = await getFreePort();
|
||||
mocks.runtimeConfig = browserConfig({ gatewayPort: controlPort - 2 });
|
||||
mocks.runtimeSourceConfig = mocks.runtimeConfig;
|
||||
let releaseAuth!: () => void;
|
||||
const authGate = new Promise<void>((resolve) => {
|
||||
releaseAuth = resolve;
|
||||
});
|
||||
const authStarted = createDeferred<void>();
|
||||
const authGate = createDeferred<void>();
|
||||
mocks.ensureBrowserControlAuth.mockImplementationOnce(async () => {
|
||||
await authGate;
|
||||
authStarted.resolve();
|
||||
await authGate.promise;
|
||||
return { auth: { token: "test-token" } };
|
||||
});
|
||||
|
||||
const starting = startBrowserControlServerFromConfig();
|
||||
await authStarted.promise;
|
||||
const stopping = stopBrowserControlServer();
|
||||
releaseAuth();
|
||||
authGate.resolve();
|
||||
await expect(starting).resolves.toBeTruthy();
|
||||
await expect(stopping).resolves.toBeUndefined();
|
||||
expect(getBridgeAuthForPort(controlPort)).toBeUndefined();
|
||||
|
|
|
|||
|
|
@ -185,9 +185,12 @@ describe("voice-call diagnostic stream ownership", () => {
|
|||
}
|
||||
const fd = opened.value;
|
||||
expect(fs.fstatSync(fd).isFile()).toBe(true);
|
||||
// The OS can reuse fd before the command continuation resumes; observe the real close.
|
||||
const close = vi.spyOn(fs, "closeSync");
|
||||
process.stdout.emit("error", failed);
|
||||
expect(await running).toBe(failed);
|
||||
expect(() => fs.fstatSync(fd)).toThrow();
|
||||
expect(close).toHaveBeenCalledExactlyOnceWith(fd);
|
||||
expect(close).toHaveReturnedWith(undefined);
|
||||
expect(sleepMock).not.toHaveBeenCalled();
|
||||
expect(process.stdout.listenerCount("drain")).toBe(drainListeners);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ function normalizeMockProviderId(providerId?: string): string {
|
|||
|
||||
type SessionManagerMocks = {
|
||||
getSessionTarget: Mock<() => undefined>;
|
||||
getAppendParentId: Mock<() => string | null>;
|
||||
getLeafId: Mock<() => string | null>;
|
||||
getLeafEntry: UnknownMock;
|
||||
getEntry: UnknownMock;
|
||||
|
|
@ -79,6 +80,7 @@ type SessionManagerMocks = {
|
|||
appendThinkingLevelChange: UnknownMock;
|
||||
appendModelChange: UnknownMock;
|
||||
appendCustomEntry: UnknownMock;
|
||||
appendMessage: UnknownMock;
|
||||
appendSessionInfo: UnknownMock;
|
||||
appendLabelChange: UnknownMock;
|
||||
flushPendingPersistence: UnknownMock;
|
||||
|
|
@ -277,6 +279,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
|
|||
const trajectoryEvents: CapturedTrajectoryEvent[] = [];
|
||||
const sessionManager = {
|
||||
getSessionTarget: vi.fn(() => undefined),
|
||||
getAppendParentId: vi.fn<() => string | null>(() => null),
|
||||
getLeafId: vi.fn<() => string | null>(() => null),
|
||||
getLeafEntry: vi.fn(() => null),
|
||||
getEntry: vi.fn(() => undefined),
|
||||
|
|
@ -287,6 +290,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
|
|||
appendThinkingLevelChange: vi.fn(),
|
||||
appendModelChange: vi.fn(),
|
||||
appendCustomEntry: vi.fn(),
|
||||
appendMessage: vi.fn(),
|
||||
appendSessionInfo: vi.fn(),
|
||||
appendLabelChange: vi.fn(),
|
||||
flushPendingPersistence: vi.fn(),
|
||||
|
|
@ -792,20 +796,12 @@ vi.mock("../../sandbox/runtime-status.js", () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../tool-call-id.js", async (importOriginal) => {
|
||||
return await importOriginal<typeof import("../../tool-call-id.js")>();
|
||||
});
|
||||
|
||||
vi.mock("../../tool-fs-policy.js", () => ({
|
||||
resolveSessionPermissionExecMode: (policy: { mode: string }) =>
|
||||
({ "read-only": "deny", guarded: "ask", workspace: "auto", full: "full" })[policy.mode],
|
||||
resolveEffectiveToolFsWorkspaceOnly: () => false,
|
||||
}));
|
||||
|
||||
vi.mock("../../tool-policy.js", async (importOriginal) => {
|
||||
return await importOriginal<typeof import("../../tool-policy.js")>();
|
||||
});
|
||||
|
||||
vi.mock("../../transcript-policy.js", () => ({
|
||||
resolveTranscriptPolicy: () => ({
|
||||
allowSyntheticToolResults: false,
|
||||
|
|
@ -927,13 +923,6 @@ vi.mock("../thinking.js", async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
vi.mock("../tool-name-allowlist.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../tool-name-allowlist.js")>();
|
||||
return {
|
||||
...actual,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../tool-split.js", () => ({
|
||||
splitSdkTools: ({ tools }: { tools: unknown[] }) => ({
|
||||
customTools: tools,
|
||||
|
|
@ -1152,6 +1141,7 @@ export function resetEmbeddedAttemptHarness(
|
|||
hoisted.embeddedSystemPromptInputs.length = 0;
|
||||
hoisted.trajectoryEvents.length = 0;
|
||||
hoisted.sessionManager.getSessionTarget.mockReset().mockReturnValue(undefined);
|
||||
hoisted.sessionManager.getAppendParentId.mockReset().mockReturnValue(null);
|
||||
hoisted.sessionManager.getLeafId.mockReset().mockReturnValue(null);
|
||||
hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null);
|
||||
hoisted.sessionManager.getEntry.mockReset().mockReturnValue(undefined);
|
||||
|
|
@ -1165,6 +1155,7 @@ export function resetEmbeddedAttemptHarness(
|
|||
hoisted.sessionManager.appendThinkingLevelChange.mockReset();
|
||||
hoisted.sessionManager.appendModelChange.mockReset();
|
||||
hoisted.sessionManager.appendCustomEntry.mockReset();
|
||||
hoisted.sessionManager.appendMessage.mockReset();
|
||||
hoisted.sessionManager.appendSessionInfo.mockReset();
|
||||
hoisted.sessionManager.appendLabelChange.mockReset();
|
||||
hoisted.sessionManager.flushPendingPersistence.mockReset();
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@ import {
|
|||
} from "../../../infra/diagnostic-events.js";
|
||||
import { wrapToolWithBeforeToolCallHook } from "../../agent-tools.before-tool-call.js";
|
||||
import type { createOpenClawCodingTools } from "../../agent-tools.js";
|
||||
import { Agent, type AgentTool } from "../../runtime/index.js";
|
||||
import { Agent, type AgentEvent, type AgentTool } from "../../runtime/index.js";
|
||||
import { getInternalToolExecutionPreparer } from "../../runtime/internal-hooks.js";
|
||||
import { TOOL_EXECUTION_GATED_MESSAGE } from "../../tool-policy-shared.js";
|
||||
import { isToolResultError } from "../../tool-result-error.js";
|
||||
import type { ToolSearchCatalogRef } from "../../tool-search.js";
|
||||
import { createAgentsWaitTool } from "../../tools/agents-wait-tool.js";
|
||||
import { createSessionsSpawnTool } from "../../tools/sessions-spawn-tool.js";
|
||||
|
|
@ -89,7 +91,7 @@ describe("runEmbeddedAttempt tool-search catalog cleanup", () => {
|
|||
},
|
||||
])(
|
||||
"does not enter the original preparer or action through denied $mode",
|
||||
async ({ toolName, code }) => {
|
||||
async ({ mode, toolName, code }) => {
|
||||
const execute = vi.fn(async () => ({ content: [], details: {} }));
|
||||
const prepare = vi.fn(async (args: unknown) => args);
|
||||
const native =
|
||||
|
|
@ -101,8 +103,7 @@ describe("runEmbeddedAttempt tool-search catalog cleanup", () => {
|
|||
const source = wrapToolWithBeforeToolCallHook(native);
|
||||
expect(getInternalToolExecutionPreparer(source)).toBeDefined();
|
||||
hoisted.createOpenClawCodingToolsMock.mockReturnValue([source]);
|
||||
const observed: AssistantMessage["content"][] = [];
|
||||
const outcomes: Array<{ toolName: string; isError: boolean }> = [];
|
||||
const outcomes: Extract<AgentEvent, { type: "tool_execution_end" }>[] = [];
|
||||
await createContextEngineAttemptRunner({
|
||||
contextEngine: createContextEngineBootstrapAndAssemble(),
|
||||
sessionKey: "agent:main:main",
|
||||
|
|
@ -119,6 +120,10 @@ describe("runEmbeddedAttempt tool-search catalog cleanup", () => {
|
|||
let turn = 0;
|
||||
const agent = new Agent({
|
||||
initialState: { model: options.model, tools: allTools },
|
||||
// AgentSession's result middleware normally classifies structured tool failures.
|
||||
afterToolCall: async ({ result, isError }) => ({
|
||||
isError: isError || isToolResultError(result),
|
||||
}),
|
||||
streamFn: () => {
|
||||
const content: AssistantMessage["content"] =
|
||||
turn++ === 0
|
||||
|
|
@ -135,7 +140,6 @@ describe("runEmbeddedAttempt tool-search catalog cleanup", () => {
|
|||
},
|
||||
]
|
||||
: [{ type: "text", text: "Denied as expected." }];
|
||||
observed.push(content);
|
||||
const message: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content,
|
||||
|
|
@ -190,10 +194,18 @@ describe("runEmbeddedAttempt tool-search catalog cleanup", () => {
|
|||
config: { tools: { codeMode: Boolean(code), toolSearch: false } },
|
||||
},
|
||||
});
|
||||
expect(observed.length).toBeGreaterThanOrEqual(1);
|
||||
expect(outcomes).toContainEqual(
|
||||
expect.objectContaining({ toolName: code ? "exec" : toolName, isError: true }),
|
||||
);
|
||||
if (code) {
|
||||
expect(outcomes[0]?.result).toMatchObject({
|
||||
details: {
|
||||
error: expect.stringContaining(
|
||||
mode === "joined Code Mode" ? "agents is not defined" : TOOL_EXECUTION_GATED_MESSAGE,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(prepare).not.toHaveBeenCalled();
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ describe.skipIf(process.platform === "win32")("service-managed child lifecycle",
|
|||
const command = `
|
||||
process.on("SIGTERM", () => {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(termPath)}, String(process.pid));
|
||||
process.exit(0);
|
||||
});
|
||||
require("node:fs").writeFileSync(${JSON.stringify(pidPath)}, String(process.pid));
|
||||
setInterval(() => {}, 1000);
|
||||
|
|
@ -212,8 +213,8 @@ describe.skipIf(process.platform === "win32")("service-managed child lifecycle",
|
|||
await expect(supervisor.waitForScope(runId)).rejects.toThrow("cleanup identity lost");
|
||||
await expect(run.waitForExtinction?.()).rejects.toThrow("cleanup identity lost");
|
||||
await expect(supervisor.shutdown()).rejects.toThrow("cleanup identity lost");
|
||||
// Observe the anchor's TERM before its unchanged grace; neither the timeout
|
||||
// result nor the failed join may disable its independent group cleanup.
|
||||
// TERM must still reach the command after failed cleanup joins. Dedicated
|
||||
// escalation cases cover commands that keep running through the TERM grace.
|
||||
await waitForPidFile(termPath, 5_000, realDelay);
|
||||
await waitFor(() => !isAlive(startedPid));
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue