mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
improve(tests): tighten async polling across suites (#109919)
* test: tighten signal delivery polling * test: tighten async polling across suites * test: use virtual time for signal ingress drain
This commit is contained in:
committed by
GitHub
parent
ecfcaa07e6
commit
9b5eb04221
@@ -11,6 +11,10 @@ import type { GatewayAccount, GatewayPluginRuntime } from "./types.js";
|
||||
const acknowledgeInteractionMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const sendTextMock = vi.hoisted(() => vi.fn(async () => ({ id: "message-1", timestamp: 1 })));
|
||||
|
||||
function waitForQqInteraction(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
vi.mock("../messaging/sender.js", () => ({
|
||||
accountToCreds: (account: GatewayAccount) => ({
|
||||
appId: account.appId,
|
||||
@@ -139,7 +143,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
@@ -167,7 +171,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
@@ -185,7 +189,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent({ group_member_openid: "OWNER_OPENID" }));
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(resolveApprovalMock).toHaveBeenCalledWith(expectedApprovalResolve),
|
||||
);
|
||||
});
|
||||
@@ -208,7 +212,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(resolveApprovalMock).toHaveBeenCalledWith({
|
||||
approvalId: "exec:looks-like-exec/1",
|
||||
approvalKind: "plugin",
|
||||
@@ -234,7 +238,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
@@ -271,7 +275,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent({ group_member_openid: "OWNER_OPENID" }));
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(log.info).toHaveBeenCalledWith(
|
||||
"Approval already resolved: id=exec:abc12345, status=denied, decision=deny",
|
||||
),
|
||||
@@ -306,7 +310,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent({ group_member_openid: "OWNER_OPENID" }));
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
"interaction-1",
|
||||
@@ -314,11 +318,11 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
{ content: "Approval response received." },
|
||||
),
|
||||
);
|
||||
await vi.waitFor(() => expect(resolveApprovalMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(resolveApprovalMock).toHaveBeenCalled());
|
||||
expect(sendTextMock).not.toHaveBeenCalled();
|
||||
|
||||
releaseResolution(appliedApprovalResult);
|
||||
await vi.waitFor(() => expect(sendTextMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(sendTextMock).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("uses the direct user openid when a group member openid is unavailable", async () => {
|
||||
@@ -335,7 +339,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(resolveApprovalMock).toHaveBeenCalledWith(expectedApprovalResolve),
|
||||
);
|
||||
});
|
||||
@@ -347,7 +351,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(resolveApprovalMock).toHaveBeenCalledWith(expectedApprovalResolve),
|
||||
);
|
||||
});
|
||||
@@ -378,7 +382,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(resolveApprovalMock).toHaveBeenCalledWith(expectedApprovalResolve),
|
||||
);
|
||||
});
|
||||
@@ -403,7 +407,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForQqInteraction(() =>
|
||||
expect(resolveApprovalMock).toHaveBeenCalledWith(expectedApprovalResolve),
|
||||
);
|
||||
});
|
||||
@@ -424,7 +428,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
@@ -468,7 +472,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
@@ -487,7 +491,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent({ group_member_openid: undefined, user_openid: undefined }));
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
@@ -507,7 +511,7 @@ describe("createInteractionHandler approval buttons", () => {
|
||||
|
||||
handler(makeApprovalEvent());
|
||||
|
||||
await vi.waitFor(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
await waitForQqInteraction(() => expect(acknowledgeInteractionMock).toHaveBeenCalled());
|
||||
|
||||
expect(acknowledgeInteractionMock).toHaveBeenCalledWith(
|
||||
{ appId: "app", clientSecret: "secret" },
|
||||
|
||||
+71
-58
@@ -30,6 +30,10 @@ const {
|
||||
const SIGNAL_BASE_URL = "http://127.0.0.1:8080";
|
||||
type MonitorSignalProviderOptions = NonNullable<Parameters<typeof monitorSignalProvider>[0]>;
|
||||
|
||||
function waitForSignalDelivery(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) {
|
||||
return monitorSignalProvider({
|
||||
config: config as OpenClawConfig,
|
||||
@@ -141,7 +145,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[1]).toBe("PFX final reply");
|
||||
@@ -165,7 +169,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
@@ -193,7 +197,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
@@ -233,7 +237,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[0]).toBe("group:signal-group-id");
|
||||
@@ -269,7 +273,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock.mock.calls.length).toBeGreaterThan(1);
|
||||
});
|
||||
for (const call of sendMock.mock.calls) {
|
||||
@@ -306,7 +310,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock.mock.calls.length).toBeGreaterThan(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
@@ -345,7 +349,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
@@ -388,7 +392,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
for (const call of sendMock.mock.calls) {
|
||||
@@ -431,7 +435,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
for (const call of sendMock.mock.calls) {
|
||||
@@ -472,7 +476,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -504,7 +508,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -513,47 +517,56 @@ describe("monitorSignalProvider tool results", () => {
|
||||
});
|
||||
|
||||
it("keeps durable conversation events separate in batched reply mode", async () => {
|
||||
setSignalToolResultTestConfig({
|
||||
...createSignalToolResultConfig({
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
setSignalToolResultTestConfig({
|
||||
...createSignalToolResultConfig({
|
||||
autoStart: false,
|
||||
replyToMode: "batched",
|
||||
}),
|
||||
messages: { inbound: { debounceMs: 10 } },
|
||||
});
|
||||
replyMock.mockResolvedValue({ text: "reply" });
|
||||
const abortController = new AbortController();
|
||||
streamMock.mockImplementation(async ({ onEvent }) => {
|
||||
for (const [timestamp, message] of [
|
||||
[1700000000001, "first message"],
|
||||
[1700000000002, "second message"],
|
||||
] as const) {
|
||||
await onEvent({
|
||||
event: "receive",
|
||||
data: JSON.stringify({
|
||||
envelope: {
|
||||
sourceNumber: "+15550001111",
|
||||
sourceName: "Ada",
|
||||
timestamp,
|
||||
dataMessage: { message },
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
try {
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(replyMock).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
abortController.abort();
|
||||
}
|
||||
});
|
||||
|
||||
await runMonitorWithMocks({
|
||||
autoStart: false,
|
||||
replyToMode: "batched",
|
||||
}),
|
||||
messages: { inbound: { debounceMs: 10 } },
|
||||
});
|
||||
replyMock.mockResolvedValue({ text: "reply" });
|
||||
const abortController = new AbortController();
|
||||
streamMock.mockImplementation(async ({ onEvent }) => {
|
||||
for (const [timestamp, message] of [
|
||||
[1700000000001, "first message"],
|
||||
[1700000000002, "second message"],
|
||||
] as const) {
|
||||
await onEvent({
|
||||
event: "receive",
|
||||
data: JSON.stringify({
|
||||
envelope: {
|
||||
sourceNumber: "+15550001111",
|
||||
sourceName: "Ada",
|
||||
timestamp,
|
||||
dataMessage: { message },
|
||||
},
|
||||
}),
|
||||
});
|
||||
baseUrl: SIGNAL_BASE_URL,
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
for (const call of sendMock.mock.calls) {
|
||||
expect(call[2]).not.toHaveProperty("replyToId");
|
||||
expect(call[2]).not.toHaveProperty("replyToAuthor");
|
||||
expect(call[2]).not.toHaveProperty("replyToBody");
|
||||
}
|
||||
await vi.waitFor(() => expect(replyMock).toHaveBeenCalledTimes(2));
|
||||
abortController.abort();
|
||||
});
|
||||
|
||||
await runMonitorWithMocks({
|
||||
autoStart: false,
|
||||
baseUrl: SIGNAL_BASE_URL,
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
for (const call of sendMock.mock.calls) {
|
||||
expect(call[2]).not.toHaveProperty("replyToId");
|
||||
expect(call[2]).not.toHaveProperty("replyToAuthor");
|
||||
expect(call[2]).not.toHaveProperty("replyToBody");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -575,7 +588,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
@@ -604,7 +617,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -630,7 +643,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -659,7 +672,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -688,7 +701,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
@@ -723,7 +736,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -760,7 +773,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0]?.[2]).not.toHaveProperty("replyToId");
|
||||
@@ -920,7 +933,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForSignalDelivery(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
|
||||
const TELEGRAM_COMMAND_TEXT_LIMIT = 5700;
|
||||
|
||||
function waitForTelegramMenu(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
type SyncMenuOptions = {
|
||||
deleteMyCommands: ReturnType<typeof vi.fn>;
|
||||
setMyCommands: ReturnType<typeof vi.fn>;
|
||||
@@ -289,7 +293,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -314,7 +318,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -348,7 +352,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
@@ -386,7 +390,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
@@ -413,7 +417,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
|
||||
syncMenuCommandsWithMocks({
|
||||
deleteMyCommands,
|
||||
@@ -444,7 +448,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
|
||||
syncMenuCommandsWithMocks({
|
||||
deleteMyCommands,
|
||||
@@ -454,7 +458,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(4));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(4));
|
||||
});
|
||||
|
||||
it("resyncs delimiter-like command lists without hash collisions", async () => {
|
||||
@@ -471,7 +475,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
|
||||
syncMenuCommandsWithMocks({
|
||||
deleteMyCommands,
|
||||
@@ -484,7 +488,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(4));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(4));
|
||||
});
|
||||
|
||||
it("skips sync when command hash is unchanged (#32017)", async () => {
|
||||
@@ -504,7 +508,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
@@ -535,7 +539,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "token-bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(2));
|
||||
|
||||
syncMenuCommandsWithMocks({
|
||||
deleteMyCommands,
|
||||
@@ -545,7 +549,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "token-bot-b",
|
||||
});
|
||||
await vi.waitFor(() => expect(setMyCommands).toHaveBeenCalledTimes(4));
|
||||
await waitForTelegramMenu(() => expect(setMyCommands).toHaveBeenCalledTimes(4));
|
||||
});
|
||||
|
||||
it("does not cache empty-menu hash when deleteMyCommands fails", async () => {
|
||||
@@ -565,7 +569,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(deleteMyCommands).toHaveBeenCalledTimes(2));
|
||||
await waitForTelegramMenu(() => expect(deleteMyCommands).toHaveBeenCalledTimes(2));
|
||||
|
||||
syncMenuCommandsWithMocks({
|
||||
deleteMyCommands,
|
||||
@@ -575,7 +579,7 @@ describe("bot-native-command-menu", () => {
|
||||
accountId,
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
await vi.waitFor(() => expect(deleteMyCommands).toHaveBeenCalledTimes(4));
|
||||
await waitForTelegramMenu(() => expect(deleteMyCommands).toHaveBeenCalledTimes(4));
|
||||
});
|
||||
|
||||
it("retries with fewer commands on BOT_COMMANDS_TOO_MUCH", async () => {
|
||||
@@ -600,7 +604,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
const firstPayload = setMyCommandsPayload(setMyCommands, 0);
|
||||
@@ -640,7 +644,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
expect(setMyCommandsPayload(setMyCommands, 0)).toHaveLength(100);
|
||||
@@ -669,7 +673,7 @@ describe("bot-native-command-menu", () => {
|
||||
botIdentity: "bot-a",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForTelegramMenu(() => {
|
||||
expect(setMyCommands).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
expect(runtimeLog).toHaveBeenCalledWith(
|
||||
|
||||
@@ -119,6 +119,10 @@ const INFERENCE_EVENT: WorkerInferenceEventFrame = {
|
||||
};
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
function waitForWorkerProtocol(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
type InferenceSink = {
|
||||
connectionId: string;
|
||||
send(frame: WorkerInferenceEventFrame | WorkerInferenceTerminalFrame): void;
|
||||
@@ -224,7 +228,7 @@ function attachHarness(
|
||||
|
||||
async function admit(harness: ReturnType<typeof attachHarness>): Promise<void> {
|
||||
harness.sendConnect();
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(1));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(1));
|
||||
}
|
||||
|
||||
describe("dedicated worker websocket protocol", () => {
|
||||
@@ -252,7 +256,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
const harness = attachHarness({ admissionFailure: reason });
|
||||
harness.sendConnect();
|
||||
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, reason));
|
||||
await waitForWorkerProtocol(() => expect(harness.close).toHaveBeenCalledWith(1008, reason));
|
||||
expect(harness.responses[0]).toMatchObject({ ok: false, error: { details: { reason } } });
|
||||
expect(harness.logWsControl.warn).toHaveBeenCalledWith(
|
||||
`worker admission rejected reason=${reason}`,
|
||||
@@ -269,7 +273,9 @@ describe("dedicated worker websocket protocol", () => {
|
||||
await admit(harness);
|
||||
harness.sendRequest(method, params);
|
||||
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, "method-not-allowed"));
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1008, "method-not-allowed"),
|
||||
);
|
||||
expect(harness.logGateway.warn).toHaveBeenCalledWith(
|
||||
"worker protocol request rejected reason=method-not-allowed",
|
||||
);
|
||||
@@ -279,7 +285,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
const valid = attachHarness();
|
||||
await admit(valid);
|
||||
valid.sendRequest("worker.heartbeat", { sentAtMs: 1, status: "busy" });
|
||||
await vi.waitFor(() => expect(valid.responses).toHaveLength(2));
|
||||
await waitForWorkerProtocol(() => expect(valid.responses).toHaveLength(2));
|
||||
expect(valid.responses[1]).toMatchObject({
|
||||
ok: true,
|
||||
payload: { status: "ok", ownerEpoch: 1 },
|
||||
@@ -297,7 +303,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
});
|
||||
await admit(unsupported);
|
||||
unsupported.sendRequest("worker.inference.start", INFERENCE_START);
|
||||
await vi.waitFor(() =>
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(unsupported.close).toHaveBeenCalledWith(1008, "method-not-allowed"),
|
||||
);
|
||||
expect(unsupported.service.startInference).not.toHaveBeenCalled();
|
||||
@@ -311,7 +317,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
await admit(harness);
|
||||
harness.sendRequest("worker.inference.start", INFERENCE_START);
|
||||
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(3));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(3));
|
||||
expect(harness.responses[1]).toMatchObject({
|
||||
ok: true,
|
||||
payload: { status: "accepted" },
|
||||
@@ -320,7 +326,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
expect(harness.service.startInference).toHaveBeenCalledOnce();
|
||||
|
||||
harness.sendRequest("worker.inference.cancel", INFERENCE_IDS, "cancel-1");
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(4));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(4));
|
||||
expect(harness.responses[3]).toMatchObject({
|
||||
ok: true,
|
||||
payload: { status: "cancelled" },
|
||||
@@ -333,7 +339,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
await admit(harness);
|
||||
harness.sendRequest("worker.transcript.commit", TRANSCRIPT_COMMIT);
|
||||
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(2));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(2));
|
||||
expect(harness.responses[1]).toMatchObject({
|
||||
ok: true,
|
||||
payload: { entryIds: ["entry-1"], newLeafId: "entry-1" },
|
||||
@@ -357,7 +363,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
});
|
||||
await admit(unsupported);
|
||||
unsupported.sendRequest("worker.live-event", LIVE_EVENT);
|
||||
await vi.waitFor(() => expect(unsupported.close).toHaveBeenCalled());
|
||||
await waitForWorkerProtocol(() => expect(unsupported.close).toHaveBeenCalled());
|
||||
expect(unsupported.service.pushLiveEvent).not.toHaveBeenCalled();
|
||||
|
||||
const resync = attachHarness({
|
||||
@@ -365,7 +371,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
});
|
||||
await admit(resync);
|
||||
resync.sendRequest("worker.live-event", { ...LIVE_EVENT, seq: 7 });
|
||||
await vi.waitFor(() =>
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(resync.responses[1]).toMatchObject({
|
||||
error: { details: { reason: "resync-required" } },
|
||||
}),
|
||||
@@ -378,7 +384,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
...LIVE_EVENT,
|
||||
event: { kind: "assistant", payload: { delta: "x" } },
|
||||
});
|
||||
await vi.waitFor(() =>
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(invalid.responses[1]).toMatchObject({
|
||||
error: { details: { reason: "invalid-event" } },
|
||||
}),
|
||||
@@ -393,7 +399,9 @@ describe("dedicated worker websocket protocol", () => {
|
||||
await admit(harness);
|
||||
harness.sendRequest("worker.transcript.commit", TRANSCRIPT_COMMIT);
|
||||
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, "method-not-allowed"));
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1008, "method-not-allowed"),
|
||||
);
|
||||
expect(harness.service.commitTranscript).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -402,7 +410,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
await admit(harness);
|
||||
harness.sendRequest("worker.transcript.commit", TRANSCRIPT_COMMIT);
|
||||
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(2));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(2));
|
||||
expect(harness.responses[1]).toMatchObject({
|
||||
ok: false,
|
||||
error: { details: { reason: "stale-base-leaf" } },
|
||||
@@ -418,7 +426,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
sessionId: "foreign-session",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(2));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(2));
|
||||
expect(harness.responses[1]).toMatchObject({
|
||||
ok: false,
|
||||
error: { details: { reason: "invalid-batch" } },
|
||||
@@ -436,12 +444,14 @@ describe("dedicated worker websocket protocol", () => {
|
||||
sessionId: "foreign-session",
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(harness.responses).toHaveLength(2));
|
||||
await waitForWorkerProtocol(() => expect(harness.responses).toHaveLength(2));
|
||||
expect(harness.responses[1]).toMatchObject({
|
||||
ok: false,
|
||||
error: { details: { reason: "credential-replaced" } },
|
||||
});
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"));
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"),
|
||||
);
|
||||
expect(harness.service.commitTranscript).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -449,7 +459,9 @@ describe("dedicated worker websocket protocol", () => {
|
||||
const harness = attachHarness({ validationFailure: "credential-replaced" });
|
||||
harness.sendConnect();
|
||||
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"));
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"),
|
||||
);
|
||||
expect(harness.setClient).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -459,7 +471,9 @@ describe("dedicated worker websocket protocol", () => {
|
||||
vi.mocked(harness.service.validateWorkerConnection).mockReturnValue("credential-replaced");
|
||||
harness.sendRequest("worker.heartbeat", { sentAtMs: 1, status: "ready" });
|
||||
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"));
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"),
|
||||
);
|
||||
});
|
||||
|
||||
it("fences a replaced connection before dispatch", async () => {
|
||||
@@ -468,7 +482,9 @@ describe("dedicated worker websocket protocol", () => {
|
||||
harness.client()!.invalidated = true;
|
||||
harness.sendRequest("worker.heartbeat", { sentAtMs: 1, status: "ready" });
|
||||
|
||||
await vi.waitFor(() => expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"));
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1008, "credential-replaced"),
|
||||
);
|
||||
expect(harness.service.validateWorkerConnection).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -479,7 +495,7 @@ describe("dedicated worker websocket protocol", () => {
|
||||
expect(suspension).not.toBeNull();
|
||||
try {
|
||||
harness.sendRequest("worker.heartbeat", { sentAtMs: 1, status: "ready" });
|
||||
await vi.waitFor(() =>
|
||||
await waitForWorkerProtocol(() =>
|
||||
expect(harness.close).toHaveBeenCalledWith(1013, "gateway-unavailable"),
|
||||
);
|
||||
expect(harness.service.validateWorkerConnection).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -12,6 +12,10 @@ type OnboardingMemoryImportElement = HTMLElement & {
|
||||
updateComplete: Promise<boolean>;
|
||||
};
|
||||
|
||||
function waitForOnboardingMemoryImport(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
const guardKey = "openclaw.onboarding.memory-import";
|
||||
|
||||
function createProvider(providerId: string, fingerprint: string) {
|
||||
@@ -163,7 +167,9 @@ describe("OnboardingMemoryImport", () => {
|
||||
const context = createContext(request, { agentsLoaded: false });
|
||||
await mount(context);
|
||||
|
||||
await vi.waitFor(() => expect(context.agents.ensureList).toHaveBeenCalledTimes(1));
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(context.agents.ensureList).toHaveBeenCalledTimes(1),
|
||||
);
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -171,7 +177,9 @@ describe("OnboardingMemoryImport", () => {
|
||||
const request = vi.fn(async () => createPlan([]));
|
||||
const element = await mount(createContext(request));
|
||||
|
||||
await vi.waitFor(() => expect(sessionStorage.getItem(guardKey)).toBe("done"));
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(sessionStorage.getItem(guardKey)).toBe("done"),
|
||||
);
|
||||
expect(request).toHaveBeenCalledWith("migrations.memory.plan", {
|
||||
agentId: "research",
|
||||
overwrite: false,
|
||||
@@ -185,7 +193,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
});
|
||||
const element = await mount(createContext(request));
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await waitForOnboardingMemoryImport(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await element.updateComplete;
|
||||
expect(element.querySelector("openclaw-modal-dialog")).toBeNull();
|
||||
expect(sessionStorage.getItem(guardKey)).toBeNull();
|
||||
@@ -207,7 +215,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
}));
|
||||
const element = await mount(createContext(request));
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await waitForOnboardingMemoryImport(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await element.updateComplete;
|
||||
expect(element.querySelector("openclaw-modal-dialog")).toBeNull();
|
||||
expect(sessionStorage.getItem(guardKey)).toBeNull();
|
||||
@@ -223,7 +231,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
);
|
||||
const context = createContext(request);
|
||||
const element = await mount(context);
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await waitForOnboardingMemoryImport(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
|
||||
context.gateway.snapshot.client = createContext(vi.fn()).gateway.snapshot.client;
|
||||
resolvePlan(createPlan());
|
||||
@@ -246,7 +254,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
.mockReturnValueOnce("00000000-0000-4000-8000-000000000001")
|
||||
.mockReturnValueOnce("00000000-0000-4000-8000-000000000002");
|
||||
const element = await mount(createContext(request));
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>(
|
||||
"[data-test-id='onboarding-memory-import-import']",
|
||||
@@ -258,7 +266,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='onboarding-memory-import-import']")
|
||||
?.click();
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await waitForOnboardingMemoryImport(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
const applyCalls = request.mock.calls.filter(
|
||||
([method]) => method === "migrations.memory.apply",
|
||||
);
|
||||
@@ -297,7 +305,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
return createApplyResult("codex");
|
||||
});
|
||||
const element = await mount(createContext(originalRequest));
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>(
|
||||
"[data-test-id='onboarding-memory-import-import']",
|
||||
@@ -309,11 +317,11 @@ describe("OnboardingMemoryImport", () => {
|
||||
// never be applied through the old binding. The offer replans instead.
|
||||
element.context = createContext(replacementRequest);
|
||||
await element.updateComplete;
|
||||
await vi.waitFor(() => expect(replacementRequest).toHaveBeenCalled());
|
||||
await waitForOnboardingMemoryImport(() => expect(replacementRequest).toHaveBeenCalled());
|
||||
expect(replacementRequest.mock.calls[0]?.[0]).toBe("migrations.memory.plan");
|
||||
expect(originalRequest).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>(
|
||||
"[data-test-id='onboarding-memory-import-import']",
|
||||
@@ -323,7 +331,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
element
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='onboarding-memory-import-import']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
replacementRequest.mock.calls.filter((call) => call[0] === "migrations.memory.apply"),
|
||||
).toHaveLength(1),
|
||||
@@ -344,7 +352,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
return result;
|
||||
});
|
||||
const element = await mount(createContext(request));
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>(
|
||||
"[data-test-id='onboarding-memory-import-import']",
|
||||
@@ -356,14 +364,14 @@ describe("OnboardingMemoryImport", () => {
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='onboarding-memory-import-import']")
|
||||
?.click();
|
||||
|
||||
await vi.waitFor(() => expect(element.textContent).toContain("1 failed"));
|
||||
await waitForOnboardingMemoryImport(() => expect(element.textContent).toContain("1 failed"));
|
||||
expect(element.textContent).toContain("Migrated 1");
|
||||
});
|
||||
|
||||
it("sets the guard when skipped", async () => {
|
||||
const request = vi.fn(async () => createPlan());
|
||||
const element = await mount(createContext(request));
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>("[data-test-id='onboarding-memory-import-skip']"),
|
||||
).not.toBeNull(),
|
||||
@@ -389,7 +397,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
return createApplyResult("claude", 1, 0);
|
||||
});
|
||||
const element = await mount(createContext(request));
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>(
|
||||
"[data-test-id='onboarding-memory-import-import']",
|
||||
@@ -401,7 +409,7 @@ describe("OnboardingMemoryImport", () => {
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='onboarding-memory-import-import']")
|
||||
?.click();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForOnboardingMemoryImport(() =>
|
||||
expect(
|
||||
element.querySelector<HTMLButtonElement>(
|
||||
"[data-test-id='onboarding-memory-import-continue']",
|
||||
|
||||
@@ -6,6 +6,10 @@ type WidgetBridgeDeps = Parameters<typeof createWidgetBridge>[0];
|
||||
|
||||
let testWidgetSequence = 0;
|
||||
|
||||
function waitForWidgetBridge(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testWidgetSequence += 1;
|
||||
});
|
||||
@@ -59,7 +63,7 @@ describe("getData binding gating", () => {
|
||||
resolveBinding,
|
||||
});
|
||||
bridge.handleMessage({ v: 1, type: "workspace:getData", requestId: "r1", bindingId: "value" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({ type: "workspace:error", code: "capability_denied" });
|
||||
expect(resolveBinding).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -67,7 +71,7 @@ describe("getData binding gating", () => {
|
||||
it("resolves a declared binding and posts data", async () => {
|
||||
const { bridge, posted } = makeBridge({ resolveBinding: async () => ({ revenue: 42 }) });
|
||||
bridge.handleMessage({ v: 1, type: "workspace:getData", requestId: "r1", bindingId: "value" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toEqual({
|
||||
v: 1,
|
||||
type: "workspace:data",
|
||||
@@ -81,7 +85,7 @@ describe("getData binding gating", () => {
|
||||
const resolveBinding = vi.fn(async () => ({ ok: true }));
|
||||
const { bridge, posted } = makeBridge({ resolveBinding });
|
||||
bridge.handleMessage({ v: 1, type: "workspace:getData", requestId: "r1", bindingId: "secret" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({
|
||||
type: "workspace:error",
|
||||
code: "binding_denied",
|
||||
@@ -138,7 +142,7 @@ describe("sendPrompt capability + confirm + rate limit", () => {
|
||||
sendPrompt,
|
||||
});
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "p1", text: "hi" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({ type: "workspace:error", code: "capability_denied" });
|
||||
expect(confirmPrompt).not.toHaveBeenCalled();
|
||||
expect(sendPrompt).not.toHaveBeenCalled();
|
||||
@@ -152,7 +156,7 @@ describe("sendPrompt capability + confirm + rate limit", () => {
|
||||
sendPrompt,
|
||||
});
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "p1", text: "hi" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({ type: "workspace:error", code: "prompt_declined" });
|
||||
expect(sendPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -166,7 +170,7 @@ describe("sendPrompt capability + confirm + rate limit", () => {
|
||||
sendPrompt,
|
||||
});
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "p1", text: "do it" });
|
||||
await vi.waitFor(() => expect(sendPrompt).toHaveBeenCalledTimes(1));
|
||||
await waitForWidgetBridge(() => expect(sendPrompt).toHaveBeenCalledTimes(1));
|
||||
expect(confirmPrompt).toHaveBeenCalledWith("do it");
|
||||
expect(sendPrompt).toHaveBeenCalledWith("do it");
|
||||
});
|
||||
@@ -183,7 +187,7 @@ describe("sendPrompt capability + confirm + rate limit", () => {
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "p1", text: "one" });
|
||||
// Second request while the first confirm is still pending → rate_limited.
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "p2", text: "two" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({
|
||||
type: "workspace:error",
|
||||
code: "rate_limited",
|
||||
@@ -207,12 +211,12 @@ describe("sendPrompt capability + confirm + rate limit", () => {
|
||||
// next (the in-flight limit is exercised by a separate test).
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: `p${i}`, text: "x" });
|
||||
await vi.waitFor(() => expect(sent).toBe(i + 1));
|
||||
await waitForWidgetBridge(() => expect(sent).toBe(i + 1));
|
||||
}
|
||||
expect(posted).toHaveLength(0);
|
||||
// The 11th within the same rolling minute is rejected.
|
||||
bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "p10", text: "x" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({ type: "workspace:error", code: "rate_limited" });
|
||||
expect(sent).toBe(10);
|
||||
});
|
||||
@@ -282,13 +286,13 @@ describe("rate-limit state persists across bridge re-instantiation (remount)", (
|
||||
requestId: `a${i}`,
|
||||
text: "x",
|
||||
});
|
||||
await vi.waitFor(() => expect(sent).toBe(i + 1));
|
||||
await waitForWidgetBridge(() => expect(sent).toBe(i + 1));
|
||||
}
|
||||
// Remount: dispose the exhausted bridge and build a fresh one for the same name.
|
||||
first.bridge.dispose();
|
||||
const second = makeNamed();
|
||||
second.bridge.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "b0", text: "x" });
|
||||
await vi.waitFor(() => expect(second.posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(second.posted).toHaveLength(1));
|
||||
// Budget survived the remount → still rate_limited, no extra send.
|
||||
expect(second.posted[0]).toMatchObject({ type: "workspace:error", code: "rate_limited" });
|
||||
expect(sent).toBe(10);
|
||||
@@ -324,11 +328,11 @@ describe("rate-limit state persists across bridge re-instantiation (remount)", (
|
||||
// Exhaust widget-a's budget.
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
bridgeA.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: `a${i}`, text: "x" });
|
||||
await vi.waitFor(() => expect(sentA).toBe(i + 1));
|
||||
await waitForWidgetBridge(() => expect(sentA).toBe(i + 1));
|
||||
}
|
||||
// widget-b is unaffected — its first send succeeds.
|
||||
bridgeB.handleMessage({ v: 1, type: "workspace:sendPrompt", requestId: "b0", text: "x" });
|
||||
await vi.waitFor(() => expect(sentB).toBe(1));
|
||||
await waitForWidgetBridge(() => expect(sentB).toBe(1));
|
||||
expect(postedB).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -352,7 +356,7 @@ describe("resolve-time binding re-check", () => {
|
||||
assertBindingAllowed: () => null,
|
||||
});
|
||||
bridge.handleMessage({ v: 1, type: "workspace:getData", requestId: "r1", bindingId: "value" });
|
||||
await vi.waitFor(() => expect(posted).toHaveLength(1));
|
||||
await waitForWidgetBridge(() => expect(posted).toHaveLength(1));
|
||||
expect(posted[0]).toMatchObject({ type: "workspace:data", data: 42 });
|
||||
expect(resolveBinding).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -14,6 +14,10 @@ type CronTestPage = HTMLElement & {
|
||||
cronModelSuggestions: string[];
|
||||
};
|
||||
|
||||
function waitForCronPage(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
type TestGateway = ApplicationContext["gateway"] & {
|
||||
emitSnapshot: (patch: Partial<ApplicationGatewaySnapshot>) => void;
|
||||
emitRetiredEvent: (event: Parameters<GatewayEventListener>[0]) => void;
|
||||
@@ -159,7 +163,7 @@ describe("CronPage editor state sync", () => {
|
||||
const gateway = createGateway({ request } as unknown as GatewayBrowserClient, true);
|
||||
createPage(createContext(gateway, "writer"));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForCronPage(() => {
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"cron.list",
|
||||
expect.objectContaining({ agentId: "writer" }),
|
||||
@@ -191,21 +195,21 @@ describe("CronPage editor state sync", () => {
|
||||
const gateway = createGateway(client, true);
|
||||
const page = createPage(createContext(gateway), { render: true });
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForCronPage(() =>
|
||||
expect(page.querySelector('[data-test-id="cron-new-task"]')).not.toBeNull(),
|
||||
);
|
||||
(page.querySelector('[data-suggestion="repoPulse"]') as HTMLButtonElement).click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForCronPage(() =>
|
||||
expect(page.querySelector('[data-test-id="cron-submit-run"]')).not.toBeNull(),
|
||||
);
|
||||
(page.querySelector('[data-test-id="cron-submit-run"]') as HTMLButtonElement).click();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
await waitForCronPage(() => {
|
||||
const methods = request.mock.calls.map((call) => call[0]);
|
||||
expect(methods.indexOf("cron.run")).toBeGreaterThan(methods.indexOf("cron.add"));
|
||||
});
|
||||
expect(request).toHaveBeenCalledWith("cron.run", { id: "job-fresh", mode: "force" });
|
||||
await vi.waitFor(() => expect(page.cron.cronCreateOpen).toBe(false));
|
||||
await waitForCronPage(() => expect(page.cron.cronCreateOpen).toBe(false));
|
||||
});
|
||||
|
||||
it("drills from the failing stat into run history filtered to errors", async () => {
|
||||
@@ -214,12 +218,12 @@ describe("CronPage editor state sync", () => {
|
||||
const gateway = createGateway(client, true);
|
||||
const page = createPage(createContext(gateway), { render: true });
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForCronPage(() =>
|
||||
expect(page.querySelector('[data-test-id="cron-stat-failing"]')).not.toBeNull(),
|
||||
);
|
||||
(page.querySelector('[data-test-id="cron-stat-failing"]') as HTMLButtonElement).click();
|
||||
|
||||
await vi.waitFor(() => expect(page.querySelector(".cron-activity")).not.toBeNull());
|
||||
await waitForCronPage(() => expect(page.querySelector(".cron-activity")).not.toBeNull());
|
||||
expect(page.cron.cronRunsStatuses).toEqual(["error"]);
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"cron.runs",
|
||||
@@ -273,13 +277,13 @@ describe("CronPage editor state sync", () => {
|
||||
const gateway = createGateway(client, true);
|
||||
const page = createPage(createContext(gateway), { render: true });
|
||||
|
||||
await vi.waitFor(() => expect(page.querySelector(".cron-table__row")).not.toBeNull());
|
||||
await waitForCronPage(() => expect(page.querySelector(".cron-table__row")).not.toBeNull());
|
||||
(page.querySelector(".cron-table__row") as HTMLElement).click();
|
||||
await vi.waitFor(() => expect(page.cron.cronEditingJobId).toBe("job-1"));
|
||||
await waitForCronPage(() => expect(page.cron.cronEditingJobId).toBe("job-1"));
|
||||
expect(page.cron.cronRunsScope).toBe("job");
|
||||
expect(page.cron.cronForm.enabled).toBe(true);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForCronPage(() =>
|
||||
expect(page.querySelector('[data-test-id="cron-toggle-enabled"] wa-switch')).not.toBeNull(),
|
||||
);
|
||||
const enabledToggle = page.querySelector(
|
||||
@@ -287,15 +291,15 @@ describe("CronPage editor state sync", () => {
|
||||
) as HTMLElement & { checked: boolean };
|
||||
enabledToggle.checked = false;
|
||||
enabledToggle.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await vi.waitFor(() => expect(page.cron.cronForm.enabled).toBe(false));
|
||||
await waitForCronPage(() => expect(page.cron.cronForm.enabled).toBe(false));
|
||||
expect(serverEnabled).toBe(false);
|
||||
|
||||
const removeButton = Array.from(page.querySelectorAll(".cron-job-menu__item")).find(
|
||||
(item) => item.textContent?.trim() === "Remove",
|
||||
) as HTMLButtonElement;
|
||||
removeButton.click();
|
||||
await vi.waitFor(() => expect(page.cron.cronEditingJobId).toBeNull());
|
||||
await vi.waitFor(() => expect(page.cron.cronRunsScope).toBe("all"));
|
||||
await waitForCronPage(() => expect(page.cron.cronEditingJobId).toBeNull());
|
||||
await waitForCronPage(() => expect(page.cron.cronRunsScope).toBe("all"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,10 +364,10 @@ describe("CronPage lifecycle", () => {
|
||||
await page.updateComplete;
|
||||
|
||||
gateway.emitSnapshot({ connected: true });
|
||||
await vi.waitFor(() => expect(modelRequestCount).toBe(1));
|
||||
await waitForCronPage(() => expect(modelRequestCount).toBe(1));
|
||||
gateway.emitSnapshot({ connected: false });
|
||||
gateway.emitSnapshot({ connected: true });
|
||||
await vi.waitFor(() => expect(page.cronModelSuggestions).toEqual(["fresh/model"]));
|
||||
await waitForCronPage(() => expect(page.cronModelSuggestions).toEqual(["fresh/model"]));
|
||||
|
||||
staleModels.resolve({ models: [{ id: "stale/model" }] });
|
||||
await Promise.resolve();
|
||||
@@ -380,12 +384,12 @@ describe("CronPage lifecycle", () => {
|
||||
const firstContext = createContext(firstGateway);
|
||||
const secondContext = createContext(secondGateway);
|
||||
const page = createPage(firstContext);
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalled());
|
||||
await waitForCronPage(() => expect(request).toHaveBeenCalled());
|
||||
|
||||
page.context = secondContext;
|
||||
page.requestUpdate();
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(page.cron.client).toBe(client));
|
||||
await waitForCronPage(() => expect(page.cron.client).toBe(client));
|
||||
request.mockClear();
|
||||
vi.mocked(secondContext.channels.refresh).mockClear();
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ type MemoryImportPageElement = HTMLElement & {
|
||||
requestUpdate(): void;
|
||||
};
|
||||
|
||||
function waitForMemoryImport(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
function createPlan(agentId = "research") {
|
||||
const workspace = `/tmp/openclaw-${agentId}`;
|
||||
return {
|
||||
@@ -110,7 +114,7 @@ describe("MemoryImportPage", () => {
|
||||
});
|
||||
const page = await mountPage(createContext(request));
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(1));
|
||||
await page.updateComplete;
|
||||
await Promise.resolve();
|
||||
await page.updateComplete;
|
||||
@@ -124,7 +128,7 @@ describe("MemoryImportPage", () => {
|
||||
throw new Error("expected Refresh button");
|
||||
}
|
||||
refresh.click();
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it("keeps apply recovery results visible when the follow-up plan fails", async () => {
|
||||
@@ -167,7 +171,7 @@ describe("MemoryImportPage", () => {
|
||||
});
|
||||
const page = await mountPage(createContext(request));
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']"),
|
||||
).not.toBeNull(),
|
||||
@@ -175,14 +179,14 @@ describe("MemoryImportPage", () => {
|
||||
page
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']"),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']")?.click();
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await page.updateComplete;
|
||||
expect(page.textContent).toContain("post-apply planning unavailable");
|
||||
expect(page.textContent).toContain("replacement interrupted");
|
||||
@@ -219,7 +223,7 @@ describe("MemoryImportPage", () => {
|
||||
});
|
||||
const page = await mountPage(createContext(request));
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']"),
|
||||
).not.toBeNull(),
|
||||
@@ -227,15 +231,15 @@ describe("MemoryImportPage", () => {
|
||||
page
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']"),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']")?.click();
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.disabled,
|
||||
@@ -273,7 +277,7 @@ describe("MemoryImportPage", () => {
|
||||
});
|
||||
const page = await mountPage(createContext(request));
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']"),
|
||||
).not.toBeNull(),
|
||||
@@ -281,16 +285,16 @@ describe("MemoryImportPage", () => {
|
||||
page
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']"),
|
||||
).not.toBeNull(),
|
||||
);
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']")?.click();
|
||||
await vi.waitFor(() => expect(page.textContent).toContain("response lost"));
|
||||
await waitForMemoryImport(() => expect(page.textContent).toContain("response lost"));
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']")?.click();
|
||||
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(4));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(4));
|
||||
const firstApply = request.mock.calls[1]?.[1] as { idempotencyKey?: string } | undefined;
|
||||
const retryApply = request.mock.calls[2]?.[1] as { idempotencyKey?: string } | undefined;
|
||||
expect(firstApply?.idempotencyKey).toMatch(/\S/u);
|
||||
@@ -307,7 +311,7 @@ describe("MemoryImportPage", () => {
|
||||
const context = createContext(request);
|
||||
const page = await mountPage(context);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']"),
|
||||
).not.toBeNull(),
|
||||
@@ -315,7 +319,7 @@ describe("MemoryImportPage", () => {
|
||||
page
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(page.querySelector("[data-test-id='memory-import-confirm']")).not.toBeNull(),
|
||||
);
|
||||
|
||||
@@ -330,7 +334,7 @@ describe("MemoryImportPage", () => {
|
||||
context.gateway.snapshot.client = replacementClient;
|
||||
context.gateway.snapshot.connected = true;
|
||||
page.requestUpdate();
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
expect(page.querySelector("[data-test-id='memory-import-confirm']")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -369,7 +373,7 @@ describe("MemoryImportPage", () => {
|
||||
const context = createContext(request);
|
||||
const page = await mountPage(context);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']"),
|
||||
).not.toBeNull(),
|
||||
@@ -377,11 +381,11 @@ describe("MemoryImportPage", () => {
|
||||
page
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(page.querySelector("[data-test-id='memory-import-confirm']")).not.toBeNull(),
|
||||
);
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']")?.click();
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
const firstApply = request.mock.calls[1]?.[1] as { idempotencyKey?: string } | undefined;
|
||||
|
||||
context.gateway.snapshot.connected = false;
|
||||
@@ -396,12 +400,12 @@ describe("MemoryImportPage", () => {
|
||||
context.gateway.snapshot.client = replacementClient;
|
||||
context.gateway.snapshot.connected = true;
|
||||
page.requestUpdate();
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(3));
|
||||
await waitForMemoryImport(() =>
|
||||
expect(page.querySelector("[data-test-id='memory-import-confirm']")).not.toBeNull(),
|
||||
);
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']")?.click();
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(4));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(4));
|
||||
|
||||
const retryApply = request.mock.calls[3]?.[1] as { idempotencyKey?: string } | undefined;
|
||||
expect(firstApply?.idempotencyKey).toMatch(/\S/u);
|
||||
@@ -430,7 +434,7 @@ describe("MemoryImportPage", () => {
|
||||
mutableContext.agents.state.agentsList.agents.push({ id: "writer", name: "Writer" });
|
||||
const page = await mountPage(context);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']"),
|
||||
).not.toBeNull(),
|
||||
@@ -438,7 +442,7 @@ describe("MemoryImportPage", () => {
|
||||
page
|
||||
.querySelector<HTMLButtonElement>("[data-test-id='memory-import-provider-button']")
|
||||
?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForMemoryImport(() =>
|
||||
expect(
|
||||
page.querySelector<HTMLButtonElement>("[data-test-id='memory-import-confirm']"),
|
||||
).not.toBeNull(),
|
||||
@@ -449,7 +453,7 @@ describe("MemoryImportPage", () => {
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
|
||||
page.requestUpdate();
|
||||
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
await waitForMemoryImport(() => expect(request).toHaveBeenCalledTimes(2));
|
||||
await page.updateComplete;
|
||||
expect(request.mock.calls[1]?.[1]).toMatchObject({ agentId: "writer" });
|
||||
expect(page.querySelector("[data-test-id='memory-import-confirm']")).toBeNull();
|
||||
|
||||
@@ -20,6 +20,10 @@ type SkillWorkshopPageTestElement = HTMLElement & {
|
||||
requestUpdate: () => void;
|
||||
};
|
||||
|
||||
function waitForSkillWorkshop(assertion: () => void) {
|
||||
return vi.waitFor(assertion, { interval: 1 });
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((next) => {
|
||||
@@ -201,7 +205,7 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
page.requestUpdate();
|
||||
await page.updateComplete;
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(secondRequest).toHaveBeenCalledWith("skills.proposals.list", {
|
||||
agentId: "research",
|
||||
}),
|
||||
@@ -232,7 +236,7 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
updatedAt: "2026-07-08T00:00:00.000Z",
|
||||
proposals: [],
|
||||
});
|
||||
await vi.waitFor(() => expect(page.state?.skillWorkshopLoaded).toBe(true));
|
||||
await waitForSkillWorkshop(() => expect(page.state?.skillWorkshopLoaded).toBe(true));
|
||||
expect(callsFor(request, "skills.proposals.list")).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -246,7 +250,9 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
page.context = createContext(request);
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(page.state?.skillWorkshopError).toContain("gateway offline"));
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(page.state?.skillWorkshopError).toContain("gateway offline"),
|
||||
);
|
||||
|
||||
page.requestUpdate();
|
||||
await page.updateComplete;
|
||||
@@ -273,7 +279,9 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
await page.updateComplete;
|
||||
page.requestUpdate();
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(callsFor(request, "skills.proposals.list")).toHaveLength(1));
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(request, "skills.proposals.list")).toHaveLength(1),
|
||||
);
|
||||
const loadingState = page.state;
|
||||
|
||||
gatewayListener?.({ ...context.gateway.snapshot, connected: false });
|
||||
@@ -337,7 +345,7 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
await page.updateComplete;
|
||||
|
||||
const revision = page.handleRevisionRequest("revise it", proposal, "research");
|
||||
await vi.waitFor(() => expect(oldSessions.list).toHaveBeenCalledTimes(1));
|
||||
await waitForSkillWorkshop(() => expect(oldSessions.list).toHaveBeenCalledTimes(1));
|
||||
|
||||
const newContext = createContext(vi.fn(async () => ({})));
|
||||
page.context = newContext;
|
||||
@@ -391,14 +399,18 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
page.context = createContext(oldRequest);
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(callsFor(oldRequest, "skills.proposals.list")).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(oldRequest, "skills.proposals.list")).toHaveLength(1),
|
||||
);
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(oldRequest, "skills.proposals.historyStatus")).toHaveLength(1),
|
||||
);
|
||||
await vi.waitFor(() => expect(page.state?.skillWorkshopHistoryScan.loaded).toBe(true));
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(page.state?.skillWorkshopHistoryScan.loaded).toBe(true),
|
||||
);
|
||||
|
||||
page.querySelector<HTMLButtonElement>(".sw-history__action button")?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(oldRequest).toHaveBeenCalledWith("skills.proposals.historyScan", {
|
||||
agentId: "research",
|
||||
direction: "older",
|
||||
@@ -457,10 +469,12 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
page.context = createContext(firstRequest);
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(page.state?.skillWorkshopHistoryScan.loaded).toBe(true));
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(page.state?.skillWorkshopHistoryScan.loaded).toBe(true),
|
||||
);
|
||||
|
||||
page.querySelector<HTMLButtonElement>(".sw-history__action button")?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(firstRequest).toHaveBeenCalledWith("skills.proposals.historyScan", {
|
||||
agentId: "research",
|
||||
direction: "older",
|
||||
@@ -491,14 +505,14 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
page.context = createContext(returnedRequest);
|
||||
page.requestUpdate();
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(returnedRequest, "skills.proposals.historyStatus")).toHaveLength(1),
|
||||
);
|
||||
|
||||
scan.resolve({ ...scanStatus, hasScanned: true, reviewedSessions: 8 });
|
||||
await Promise.resolve();
|
||||
firstReturnedStatus.resolve(scanStatus);
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(returnedRequest, "skills.proposals.historyStatus")).toHaveLength(2),
|
||||
);
|
||||
expect(page.state?.skillWorkshopHistoryScan.result?.reviewedSessions).toBe(8);
|
||||
@@ -533,24 +547,30 @@ describe("SkillWorkshopPage lifecycle", () => {
|
||||
page.context = createContext(request);
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
await vi.waitFor(() => expect(callsFor(request, "skills.proposals.list")).toHaveLength(1));
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(request, "skills.proposals.list")).toHaveLength(1),
|
||||
);
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(request, "skills.proposals.historyStatus")).toHaveLength(1),
|
||||
);
|
||||
await vi.waitFor(() => expect(page.state?.skillWorkshopHistoryScan.loaded).toBe(true));
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(page.state?.skillWorkshopHistoryScan.loaded).toBe(true),
|
||||
);
|
||||
|
||||
page.querySelector<HTMLButtonElement>(".sw-history__action button")?.click();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(request).toHaveBeenCalledWith("skills.proposals.historyScan", {
|
||||
agentId: "research",
|
||||
direction: "older",
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(request, "skills.proposals.historyStatus")).toHaveLength(2),
|
||||
);
|
||||
await vi.waitFor(() => expect(callsFor(request, "skills.proposals.list")).toHaveLength(2));
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(callsFor(request, "skills.proposals.list")).toHaveLength(2),
|
||||
);
|
||||
expect(page.state?.skillWorkshopHistoryScan.error).toBe("late review failure");
|
||||
});
|
||||
});
|
||||
@@ -604,13 +624,13 @@ describe("SkillWorkshopPage self-learning toggle", () => {
|
||||
expect(button).not.toBeNull();
|
||||
button?.click();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(patch).toHaveBeenCalledWith({
|
||||
raw: { skills: { workshop: { autonomous: { enabled: true } } } },
|
||||
note: "Enable Skill Workshop self-learning",
|
||||
}),
|
||||
);
|
||||
await vi.waitFor(() => expect(runtimeConfig.refresh).toHaveBeenCalledTimes(1));
|
||||
await waitForSkillWorkshop(() => expect(runtimeConfig.refresh).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("refreshes a stale config snapshot and retries the self-learning toggle", async () => {
|
||||
@@ -640,8 +660,8 @@ describe("SkillWorkshopPage self-learning toggle", () => {
|
||||
|
||||
page.querySelector<HTMLButtonElement>(".sw-empty-state__selflearn button")?.click();
|
||||
|
||||
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledTimes(2));
|
||||
await vi.waitFor(() => expect(runtimeConfig.refresh).toHaveBeenCalledTimes(2));
|
||||
await waitForSkillWorkshop(() => expect(runtimeConfig.patch).toHaveBeenCalledTimes(2));
|
||||
await waitForSkillWorkshop(() => expect(runtimeConfig.refresh).toHaveBeenCalledTimes(2));
|
||||
await page.updateComplete;
|
||||
expect(page.querySelector(".sw-error")).toBeNull();
|
||||
expect(
|
||||
@@ -658,7 +678,7 @@ describe("SkillWorkshopPage self-learning toggle", () => {
|
||||
await page.updateComplete;
|
||||
|
||||
page.querySelector<HTMLButtonElement>(".sw-empty-state__selflearn button")?.click();
|
||||
await vi.waitFor(() =>
|
||||
await waitForSkillWorkshop(() =>
|
||||
expect(page.querySelector(".sw-error")?.textContent).toContain(
|
||||
"Could not update the self-learning setting.",
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user