Skip to content

Commit 957cc81

Browse files
authored
test: speed up slow unit and browser coverage (#108563)
1 parent 7f9d519 commit 957cc81

10 files changed

Lines changed: 168 additions & 57 deletions

src/agents/auth-profiles.doctor.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22
* Auth-profile doctor copy tests.
33
* Covers provider-specific repair hints without invoking real auth flows.
44
*/
5-
import { describe, expect, it } from "vitest";
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
7+
const buildProviderAuthDoctorHintWithPluginMock = vi.hoisted(() => vi.fn());
8+
9+
vi.mock("../plugins/provider-runtime.runtime.js", () => ({
10+
buildProviderAuthDoctorHintWithPlugin: buildProviderAuthDoctorHintWithPluginMock,
11+
}));
12+
613
import { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
714

815
describe("formatAuthDoctorHint", () => {
16+
beforeEach(() => {
17+
buildProviderAuthDoctorHintWithPluginMock.mockReset();
18+
buildProviderAuthDoctorHintWithPluginMock.mockResolvedValue(undefined);
19+
});
20+
921
it("guides legacy qwen portal oauth profiles to re-authenticate", async () => {
1022
const hint = await formatAuthDoctorHint({
1123
store: {
@@ -27,6 +39,7 @@ describe("formatAuthDoctorHint", () => {
2739
expect(hint).toBe(
2840
"Legacy Qwen Portal OAuth profiles are not refreshable. Re-authenticate with a current portal token: openclaw onboard --auth-choice qwen-oauth.",
2941
);
42+
expect(buildProviderAuthDoctorHintWithPluginMock).not.toHaveBeenCalled();
3043
});
3144

3245
it("guides an unsupported github-copilot enterprise profile to login again", async () => {
@@ -50,6 +63,7 @@ describe("formatAuthDoctorHint", () => {
5063

5164
expect(hint).toContain("unsupported enterprise domain");
5265
expect(hint).toContain("openclaw models auth login --provider github-copilot --force");
66+
expect(buildProviderAuthDoctorHintWithPluginMock).not.toHaveBeenCalled();
5367
});
5468

5569
it("accepts a github-copilot profile on a ghe.com tenant", async () => {
@@ -72,6 +86,7 @@ describe("formatAuthDoctorHint", () => {
7286
});
7387

7488
expect(hint).not.toContain("unsupported enterprise domain");
89+
expect(buildProviderAuthDoctorHintWithPluginMock).toHaveBeenCalledOnce();
7590
});
7691

7792
it("accepts a public github.com profile", async () => {
@@ -93,5 +108,6 @@ describe("formatAuthDoctorHint", () => {
93108
});
94109

95110
expect(hint).not.toContain("unsupported enterprise domain");
111+
expect(buildProviderAuthDoctorHintWithPluginMock).toHaveBeenCalledOnce();
96112
});
97113
});

src/agents/embedded-agent-subscribe.callback-fatal.test.ts

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
11
// Child-process proof uses OpenClaw's real fatal unhandled-rejection handler so
2-
// an accidentally detached callback rejection terminates the negative control.
2+
// a detached callback rejection would terminate the process.
33
import { describe, expect, it } from "vitest";
44
import { spawnNodeEvalSync } from "../test-utils/node-process.js";
55

6-
const fatalHandlerImport = `
7-
import { installUnhandledRejectionHandler } from "./src/infra/unhandled-rejections.ts";
8-
installUnhandledRejectionHandler();
9-
`;
10-
116
describe("embedded agent callback rejection containment", () => {
12-
it("proves the real process handler terminates an uncontained rejection", () => {
13-
const result = spawnNodeEvalSync(
14-
`${fatalHandlerImport}
15-
void Promise.reject(new Error("negative-control-rejection"));
16-
setTimeout(() => console.log("negative control survived"), 50);`,
17-
{ imports: ["tsx"], timeout: 20_000 },
18-
);
19-
20-
expect(result.status).toBe(1);
21-
expect(result.stderr).toContain("Unhandled promise rejection");
22-
expect(result.stderr).toContain("negative-control-rejection");
23-
expect(result.stdout).not.toContain("negative control survived");
24-
});
25-
267
it("keeps the production assistant progress path alive when its callback rejects", () => {
278
const result = spawnNodeEvalSync(
28-
`${fatalHandlerImport}
9+
`import { installUnhandledRejectionHandler } from "./src/infra/unhandled-rejections.ts";
2910
import { subscribeEmbeddedAgentSession } from "./src/agents/embedded-agent-subscribe.ts";
11+
installUnhandledRejectionHandler();
3012
let emit = () => {};
3113
let callbackCalls = 0;
3214
const session = {
@@ -48,13 +30,12 @@ describe("embedded agent callback rejection containment", () => {
4830
message: { role: "assistant" },
4931
assistantMessageEvent: { type: "text_delta", delta: "hello" },
5032
});
51-
setTimeout(() => {
52-
if (callbackCalls !== 1) {
53-
console.error("unexpected callback count: " + callbackCalls);
54-
process.exit(2);
55-
}
56-
console.log("assistant callback rejection contained");
57-
}, 50);`,
33+
await new Promise((resolve) => setImmediate(resolve));
34+
if (callbackCalls !== 1) {
35+
console.error("unexpected callback count: " + callbackCalls);
36+
process.exit(2);
37+
}
38+
console.log("assistant callback rejection contained");`,
5839
{ imports: ["tsx"], timeout: 20_000 },
5940
);
6041

src/process/exec.no-output-timer.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ describe("runCommandWithTimeout no-output timer", () => {
99
"let timer",
1010
"const emit = () => {",
1111
" process.stdout.write('.')",
12-
" if (++count === 21) { clearInterval(timer); process.exit(0) }",
12+
" if (++count === 31) { clearInterval(timer); process.exit(0) }",
1313
"}",
1414
"emit()",
15-
"timer = setInterval(emit, 100)",
15+
"timer = setInterval(emit, 25)",
1616
].join(";");
1717
const result = await runCommandWithTimeout([process.execPath, "-e", script], {
1818
timeoutMs: 10_000,
1919
// Leave ample process-startup margin while keeping total runtime above
2020
// this threshold, so only output-driven resets let the child finish.
21-
noOutputTimeoutMs: 1_500,
21+
noOutputTimeoutMs: 500,
2222
});
2323

2424
expect(result).toMatchObject({
2525
code: 0,
2626
noOutputTimedOut: false,
27-
stdout: ".....................",
27+
stdout: ".".repeat(31),
2828
termination: "exit",
2929
});
3030
});

ui/src/e2e/chat-composer-redesign.e2e.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ describeControlUiE2e("Control UI chat composer redesign", () => {
796796
code: "UNAVAILABLE",
797797
message: "metadata unavailable",
798798
});
799+
const agentsRequestsBeforeStartup = (await gateway.getRequests("agents.list")).length;
799800
await gateway.resolveDeferred("chat.startup", {
800801
agentsList: {
801802
agents: [
@@ -814,19 +815,31 @@ describeControlUiE2e("Control UI chat composer redesign", () => {
814815
sessionId: "control-ui-e2e-session",
815816
thinkingLevel: null,
816817
});
817-
await page.waitForTimeout(150);
818-
819-
const metadataRequests = await gateway.getRequests("chat.metadata");
820-
expect(metadataRequests).toHaveLength(1);
821-
expect((metadataRequests[0]?.params as { agentId?: string } | undefined)?.agentId).toBe(
822-
"work",
823-
);
818+
await expect
819+
.poll(async () => (await gateway.getRequests("agents.list")).length)
820+
.toBeGreaterThan(agentsRequestsBeforeStartup);
821+
await page.waitForFunction(() => {
822+
const pane = document.querySelector("openclaw-chat-pane") as
823+
| (HTMLElement & {
824+
state?: { agentsList?: { defaultId?: string; agents?: Array<{ id?: string }> } };
825+
})
826+
| null;
827+
return (
828+
pane?.state?.agentsList?.defaultId === "main" &&
829+
pane.state.agentsList.agents?.some((agent) => agent.id === "main") === true
830+
);
831+
});
824832
const composer = page.locator(".agent-chat__input");
825833
await expect
826834
.poll(async () =>
827835
(await composer.locator("[data-chat-model-option]").allTextContents()).join(" "),
828836
)
829837
.not.toContain("GPT Default");
838+
const metadataRequests = await gateway.getRequests("chat.metadata");
839+
expect(metadataRequests).toHaveLength(1);
840+
expect((metadataRequests[0]?.params as { agentId?: string } | undefined)?.agentId).toBe(
841+
"work",
842+
);
830843
expect(await gateway.getRequests("models.list")).toHaveLength(0);
831844
} finally {
832845
await context.close();

ui/src/e2e/chat-quota-pill-93041.e2e.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,14 @@ describeE2e("Control UI #93041 desktop chat quota popover (mocked Gateway E2E)",
206206
const { page } = fixture;
207207
try {
208208
await page.locator(".agent-chat__composer-controls").first().waitFor({ state: "visible" });
209-
await page.waitForTimeout(500);
209+
await page.waitForFunction(() => {
210+
const pane = document.querySelector("openclaw-chat-pane") as
211+
| (HTMLElement & {
212+
state?: { modelAuthStatusResult?: { providers?: unknown[] } | null };
213+
})
214+
| null;
215+
return Array.isArray(pane?.state?.modelAuthStatusResult?.providers);
216+
});
210217
expect(await page.locator('[data-chat-provider-usage="true"]').count()).toBe(0);
211218
} finally {
212219
await closeChat(fixture);

ui/src/e2e/lobster-pet.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describeControlUiE2e("Control UI lobster pet", () => {
8181
const sprite = page.locator(".lobster-pet");
8282
await expect.poll(() => sprite.count()).toBe(0);
8383

84-
await page.clock.runFor(600_500);
84+
await page.clock.fastForward(600_500);
8585
await settlePet();
8686
expect(await page.locator(".lobster-pet--vigil").count()).toBe(1);
8787
await page.evaluate(async () => {

ui/src/e2e/mantis-chat-proof.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describeMantisWebUiChat("Mantis Control UI web chat proof", () => {
116116
.locator(".chat-working-indicator__status > span:not(.agent-chat__sr-only)")
117117
.count(),
118118
).toBe(0);
119-
await page.clock.runFor(177_000);
119+
await page.clock.fastForward(177_000);
120120
await expect
121121
.poll(() => page.locator(".chat-working-indicator__elapsed").textContent())
122122
.toBe("2m 57s");

ui/src/e2e/session-transcript-search.e2e.test.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ async function captureUiProof(fileName: string) {
3636
await page.screenshot({ fullPage: true, path: path.join(artifactDir, fileName) });
3737
}
3838

39+
async function resolveDeferredAndDrain(
40+
browserPage: Page,
41+
method: string,
42+
payload: unknown,
43+
): Promise<void> {
44+
await browserPage.evaluate(
45+
async ({ targetMethod, responsePayload }) => {
46+
const gateway = (
47+
window as Window & {
48+
openclawControlUiE2eGateway?: {
49+
resolveDeferred: (method: string, payload?: unknown) => void;
50+
};
51+
}
52+
).openclawControlUiE2eGateway;
53+
if (!gateway) {
54+
throw new Error("Mock Gateway is not installed");
55+
}
56+
gateway.resolveDeferred(targetMethod, responsePayload);
57+
await new Promise<void>((resolve) => {
58+
setTimeout(resolve, 0);
59+
});
60+
},
61+
{ targetMethod: method, responsePayload: payload },
62+
);
63+
}
64+
3965
describeControlUiE2e("Control UI session transcript search", () => {
4066
beforeAll(async () => {
4167
if (captureProof) {
@@ -206,7 +232,7 @@ describeControlUiE2e("Control UI session transcript search", () => {
206232
await input.press("Enter");
207233
await expect.poll(async () => gateway.getRequests("sessions.search")).toHaveLength(1);
208234
await input.fill("new phrase");
209-
await gateway.resolveDeferred("sessions.search", {
235+
await resolveDeferredAndDrain(page, "sessions.search", {
210236
results: [
211237
{
212238
messageId: "message-stale",
@@ -219,15 +245,13 @@ describeControlUiE2e("Control UI session transcript search", () => {
219245
},
220246
],
221247
});
222-
await page.waitForTimeout(50);
223248
expect(await page.getByText("stale result must stay hidden", { exact: true }).count()).toBe(0);
224-
225249
await input.press("Enter");
226250
await page
227251
.getByText("The transcript index is still updating. Retry to include recent messages.")
228252
.waitFor({ state: "visible", timeout: 10_000 });
229-
expect(await page.getByText("No transcript messages match that search.").count()).toBe(0);
230253
await expect.poll(async () => gateway.getRequests("sessions.search")).toHaveLength(2);
254+
expect(await page.getByText("No transcript messages match that search.").count()).toBe(0);
231255

232256
await gateway.setMethodResponse("sessions.search", { results: [] });
233257
await page.getByRole("button", { name: "Retry" }).click();

ui/src/e2e/workspace-custom-widget.e2e.test.ts

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,14 +370,86 @@ describeControlUiE2e("Control UI custom-widget host mocked Gateway E2E", () => {
370370
// Post a spoofed data message from the PARENT window (event.source !== the
371371
// iframe's contentWindow). The bridge must ignore it: #value stays the
372372
// legitimate value and never shows the injected marker.
373-
await page.evaluate(() => {
374-
window.postMessage(
375-
{ v: 1, type: "workspace:data", requestId: "x", bindingId: "value", data: "SPOOFED" },
376-
"*",
377-
);
373+
await frame.locator("#value").evaluate((node) => {
374+
type SpoofObservationWindow = Window & {
375+
openclawSawSpoofed?: boolean;
376+
openclawSpoofObserver?: MutationObserver;
377+
};
378+
const targetWindow = window as SpoofObservationWindow;
379+
const valueNode = node as HTMLElement;
380+
const recordSpoof = () => {
381+
if (valueNode.textContent?.includes("SPOOFED")) {
382+
targetWindow.openclawSawSpoofed = true;
383+
}
384+
};
385+
targetWindow.openclawSawSpoofed = false;
386+
targetWindow.openclawSpoofObserver?.disconnect();
387+
targetWindow.openclawSpoofObserver = new MutationObserver(recordSpoof);
388+
targetWindow.openclawSpoofObserver.observe(valueNode, {
389+
characterData: true,
390+
childList: true,
391+
subtree: true,
392+
});
393+
recordSpoof();
378394
});
379-
await page.waitForTimeout(300);
380-
expect(await frame.locator("#value").textContent()).not.toContain("SPOOFED");
395+
await page.evaluate(async () => {
396+
await new Promise<void>((resolve) => {
397+
const onMessage = (event: MessageEvent) => {
398+
const data = event.data as { requestId?: string } | undefined;
399+
if (event.source !== window || data?.requestId !== "x") {
400+
return;
401+
}
402+
window.removeEventListener("message", onMessage);
403+
resolve();
404+
};
405+
window.addEventListener("message", onMessage);
406+
window.postMessage(
407+
{ v: 1, type: "workspace:data", requestId: "x", bindingId: "value", data: "SPOOFED" },
408+
"*",
409+
);
410+
});
411+
});
412+
await frame.locator("#value").evaluate(async () => {
413+
type WidgetBridge = {
414+
postMessage: (message: unknown) => void;
415+
addEventListener: (type: "message", listener: (event: MessageEvent) => void) => void;
416+
removeEventListener: (type: "message", listener: (event: MessageEvent) => void) => void;
417+
};
418+
const bridge = (window as Window & { openclawWorkspaceBridge?: WidgetBridge })
419+
.openclawWorkspaceBridge;
420+
if (!bridge) {
421+
throw new Error("workspace bridge unavailable");
422+
}
423+
await new Promise<void>((resolve) => {
424+
const onMessage = (event: MessageEvent) => {
425+
const data = event.data as { requestId?: string } | undefined;
426+
if (data?.requestId !== "after-spoof") {
427+
return;
428+
}
429+
bridge.removeEventListener("message", onMessage);
430+
resolve();
431+
};
432+
bridge.addEventListener("message", onMessage);
433+
const barrierRequest = {
434+
v: 1,
435+
type: "workspace:getData",
436+
requestId: "after-spoof",
437+
bindingId: "value",
438+
};
439+
// oxlint-disable-next-line unicorn/require-post-message-target-origin -- This is a MessagePort-style widget bridge, not Window.postMessage.
440+
bridge.postMessage(barrierRequest);
441+
});
442+
});
443+
const sawSpoofed = await frame.locator("#value").evaluate((node) => {
444+
type SpoofObservationWindow = Window & {
445+
openclawSawSpoofed?: boolean;
446+
openclawSpoofObserver?: MutationObserver;
447+
};
448+
const targetWindow = window as SpoofObservationWindow;
449+
targetWindow.openclawSpoofObserver?.disconnect();
450+
return targetWindow.openclawSawSpoofed === true || node.textContent?.includes("SPOOFED");
451+
});
452+
expect(sawSpoofed).toBe(false);
381453
} finally {
382454
await page.context().close();
383455
}

ui/src/pages/plugins/plugins.e2e.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,8 @@ async function captureScreenshot(page: Page, name: string): Promise<void> {
221221
return;
222222
}
223223
await mkdir(artifactDir, { recursive: true });
224-
// UI transitions top out at 180ms; capture only after Chromium has painted
225-
// the settled catalog grid rather than a partially composited transition.
226-
await page.waitForTimeout(250);
227224
await page.locator(".content").screenshot({
225+
animations: "disabled",
228226
caret: "hide",
229227
path: path.join(artifactDir, name),
230228
});

0 commit comments

Comments
 (0)