mirror of https://github.com/openclaw/openclaw.git
test: stabilize targeted harnesses
- reduce module-reset mock churn in auth/acp tests - simplify runtime web mock cleanup - make canvas reload test use in-memory websocket tracking
This commit is contained in:
parent
4680335b2a
commit
0a4c11061d
|
|
@ -1,5 +1,4 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../test/helpers/import-fresh.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
const managerMocks = vi.hoisted(() => ({
|
||||
|
|
@ -32,10 +31,6 @@ vi.mock("./persistent-bindings.resolve.js", () => ({
|
|||
resolveConfiguredAcpBindingSpecBySessionKey:
|
||||
resolveMocks.resolveConfiguredAcpBindingSpecBySessionKey,
|
||||
}));
|
||||
type BindingTargetsModule = typeof import("../channels/plugins/binding-targets.js");
|
||||
let bindingTargets: BindingTargetsModule;
|
||||
let bindingTargetsImportScope = 0;
|
||||
|
||||
const baseCfg = {
|
||||
session: { mainKey: "main", scope: "per-sender" },
|
||||
agents: {
|
||||
|
|
@ -43,13 +38,10 @@ const baseCfg = {
|
|||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
let resetAcpSessionInPlace: typeof import("./persistent-bindings.lifecycle.js").resetAcpSessionInPlace;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
bindingTargetsImportScope += 1;
|
||||
bindingTargets = await importFreshModule<BindingTargetsModule>(
|
||||
import.meta.url,
|
||||
`../channels/plugins/binding-targets.js?scope=${bindingTargetsImportScope}`,
|
||||
);
|
||||
managerMocks.closeSession.mockReset().mockResolvedValue({
|
||||
runtimeClosed: true,
|
||||
metaCleared: false,
|
||||
|
|
@ -58,9 +50,10 @@ beforeEach(async () => {
|
|||
managerMocks.updateSessionRuntimeOptions.mockReset().mockResolvedValue(undefined);
|
||||
sessionMetaMocks.readAcpSessionEntry.mockReset().mockReturnValue(undefined);
|
||||
resolveMocks.resolveConfiguredAcpBindingSpecBySessionKey.mockReset().mockReturnValue(null);
|
||||
({ resetAcpSessionInPlace } = await import("./persistent-bindings.lifecycle.js"));
|
||||
});
|
||||
|
||||
describe("resetConfiguredBindingTargetInPlace", () => {
|
||||
describe("resetAcpSessionInPlace", () => {
|
||||
it("does not resolve configured bindings when ACP metadata already exists", async () => {
|
||||
const sessionKey = "agent:claude:acp:binding:demo-binding:default:9373ab192b2317f4";
|
||||
sessionMetaMocks.readAcpSessionEntry.mockReturnValue({
|
||||
|
|
@ -75,7 +68,7 @@ describe("resetConfiguredBindingTargetInPlace", () => {
|
|||
throw new Error("configured binding resolution should be skipped");
|
||||
});
|
||||
|
||||
const result = await bindingTargets.resetConfiguredBindingTargetInPlace({
|
||||
const result = await resetAcpSessionInPlace({
|
||||
cfg: baseCfg,
|
||||
sessionKey,
|
||||
reason: "reset",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import type { ChannelConfiguredBindingProvider, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
|
@ -376,21 +376,8 @@ function mockReadySession(params: {
|
|||
return sessionKey;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
persistentBindingsResolveModule = await import("./persistent-bindings.resolve.js");
|
||||
lifecycleBindingsModule = await import("./persistent-bindings.lifecycle.js");
|
||||
persistentBindings = {
|
||||
resolveConfiguredAcpBindingRecord:
|
||||
persistentBindingsResolveModule.resolveConfiguredAcpBindingRecord,
|
||||
resolveConfiguredAcpBindingSpecBySessionKey:
|
||||
persistentBindingsResolveModule.resolveConfiguredAcpBindingSpecBySessionKey,
|
||||
ensureConfiguredAcpBindingSession: lifecycleBindingsModule.ensureConfiguredAcpBindingSession,
|
||||
resetAcpSessionInPlace: lifecycleBindingsModule.resetAcpSessionInPlace,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
|
|
@ -418,6 +405,16 @@ beforeEach(() => {
|
|||
managerMocks.initializeSession.mockReset().mockResolvedValue(undefined);
|
||||
managerMocks.updateSessionRuntimeOptions.mockReset().mockResolvedValue(undefined);
|
||||
sessionMetaMocks.readAcpSessionEntry.mockReset().mockReturnValue(undefined);
|
||||
persistentBindingsResolveModule = await import("./persistent-bindings.resolve.js");
|
||||
lifecycleBindingsModule = await import("./persistent-bindings.lifecycle.js");
|
||||
persistentBindings = {
|
||||
resolveConfiguredAcpBindingRecord:
|
||||
persistentBindingsResolveModule.resolveConfiguredAcpBindingRecord,
|
||||
resolveConfiguredAcpBindingSpecBySessionKey:
|
||||
persistentBindingsResolveModule.resolveConfiguredAcpBindingSpecBySessionKey,
|
||||
ensureConfiguredAcpBindingSession: lifecycleBindingsModule.ensureConfiguredAcpBindingSession,
|
||||
resetAcpSessionInPlace: lifecycleBindingsModule.resetAcpSessionInPlace,
|
||||
};
|
||||
});
|
||||
|
||||
describe("resolveConfiguredAcpBindingRecord", () => {
|
||||
|
|
|
|||
|
|
@ -1,64 +1,42 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./auth-profiles.js", () => ({
|
||||
const mocks = vi.hoisted(() => ({
|
||||
ensureAuthProfileStore: vi.fn(),
|
||||
resolveAuthProfileOrder: vi.fn(),
|
||||
resolveAuthProfileDisplayLabel: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./model-auth.js", () => ({
|
||||
resolveUsableCustomProviderApiKey: vi.fn(() => null),
|
||||
resolveEnvApiKey: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
type AuthProfilesModule = typeof import("./auth-profiles.js");
|
||||
type ModelAuthModule = typeof import("./model-auth.js");
|
||||
type ModelAuthLabelModule = typeof import("./model-auth-label.js");
|
||||
vi.mock("./auth-profiles.js", () => ({
|
||||
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
|
||||
resolveAuthProfileOrder: mocks.resolveAuthProfileOrder,
|
||||
resolveAuthProfileDisplayLabel: mocks.resolveAuthProfileDisplayLabel,
|
||||
}));
|
||||
|
||||
let ensureAuthProfileStoreMock: ReturnType<
|
||||
typeof vi.mocked<AuthProfilesModule["ensureAuthProfileStore"]>
|
||||
>;
|
||||
let resolveAuthProfileOrderMock: ReturnType<
|
||||
typeof vi.mocked<AuthProfilesModule["resolveAuthProfileOrder"]>
|
||||
>;
|
||||
let resolveAuthProfileDisplayLabelMock: ReturnType<
|
||||
typeof vi.mocked<AuthProfilesModule["resolveAuthProfileDisplayLabel"]>
|
||||
>;
|
||||
let resolveUsableCustomProviderApiKeyMock: ReturnType<
|
||||
typeof vi.mocked<ModelAuthModule["resolveUsableCustomProviderApiKey"]>
|
||||
>;
|
||||
let resolveEnvApiKeyMock: ReturnType<typeof vi.mocked<ModelAuthModule["resolveEnvApiKey"]>>;
|
||||
let resolveModelAuthLabel: ModelAuthLabelModule["resolveModelAuthLabel"];
|
||||
vi.mock("./model-auth.js", () => ({
|
||||
resolveUsableCustomProviderApiKey: mocks.resolveUsableCustomProviderApiKey,
|
||||
resolveEnvApiKey: mocks.resolveEnvApiKey,
|
||||
}));
|
||||
|
||||
async function loadModelAuthLabelModule() {
|
||||
vi.resetModules();
|
||||
const authProfilesModule = await import("./auth-profiles.js");
|
||||
const modelAuthModule = await import("./model-auth.js");
|
||||
const modelAuthLabelModule = await import("./model-auth-label.js");
|
||||
ensureAuthProfileStoreMock = vi.mocked(authProfilesModule.ensureAuthProfileStore);
|
||||
resolveAuthProfileOrderMock = vi.mocked(authProfilesModule.resolveAuthProfileOrder);
|
||||
resolveAuthProfileDisplayLabelMock = vi.mocked(authProfilesModule.resolveAuthProfileDisplayLabel);
|
||||
resolveUsableCustomProviderApiKeyMock = vi.mocked(
|
||||
modelAuthModule.resolveUsableCustomProviderApiKey,
|
||||
);
|
||||
resolveEnvApiKeyMock = vi.mocked(modelAuthModule.resolveEnvApiKey);
|
||||
resolveModelAuthLabel = modelAuthLabelModule.resolveModelAuthLabel;
|
||||
}
|
||||
let resolveModelAuthLabel: typeof import("./model-auth-label.js").resolveModelAuthLabel;
|
||||
|
||||
describe("resolveModelAuthLabel", () => {
|
||||
beforeEach(async () => {
|
||||
await loadModelAuthLabelModule();
|
||||
ensureAuthProfileStoreMock.mockReset();
|
||||
resolveAuthProfileOrderMock.mockReset();
|
||||
resolveAuthProfileDisplayLabelMock.mockReset();
|
||||
resolveUsableCustomProviderApiKeyMock.mockReset();
|
||||
resolveUsableCustomProviderApiKeyMock.mockReturnValue(null);
|
||||
resolveEnvApiKeyMock.mockReset();
|
||||
resolveEnvApiKeyMock.mockReturnValue(null);
|
||||
if (!resolveModelAuthLabel) {
|
||||
({ resolveModelAuthLabel } = await import("./model-auth-label.js"));
|
||||
}
|
||||
mocks.ensureAuthProfileStore.mockReset();
|
||||
mocks.resolveAuthProfileOrder.mockReset();
|
||||
mocks.resolveAuthProfileDisplayLabel.mockReset();
|
||||
mocks.resolveUsableCustomProviderApiKey.mockReset();
|
||||
mocks.resolveUsableCustomProviderApiKey.mockReturnValue(null);
|
||||
mocks.resolveEnvApiKey.mockReset();
|
||||
mocks.resolveEnvApiKey.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("does not include token value in label for token profiles", () => {
|
||||
ensureAuthProfileStoreMock.mockReturnValue({
|
||||
mocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"github-copilot:default": {
|
||||
|
|
@ -69,8 +47,8 @@ describe("resolveModelAuthLabel", () => {
|
|||
},
|
||||
},
|
||||
} as never);
|
||||
resolveAuthProfileOrderMock.mockReturnValue(["github-copilot:default"]);
|
||||
resolveAuthProfileDisplayLabelMock.mockReturnValue("github-copilot:default");
|
||||
mocks.resolveAuthProfileOrder.mockReturnValue(["github-copilot:default"]);
|
||||
mocks.resolveAuthProfileDisplayLabel.mockReturnValue("github-copilot:default");
|
||||
|
||||
const label = resolveModelAuthLabel({
|
||||
provider: "github-copilot",
|
||||
|
|
@ -85,7 +63,7 @@ describe("resolveModelAuthLabel", () => {
|
|||
|
||||
it("does not include api-key value in label for api-key profiles", () => {
|
||||
const shortSecret = "abc123"; // pragma: allowlist secret
|
||||
ensureAuthProfileStoreMock.mockReturnValue({
|
||||
mocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
|
|
@ -95,8 +73,8 @@ describe("resolveModelAuthLabel", () => {
|
|||
},
|
||||
},
|
||||
} as never);
|
||||
resolveAuthProfileOrderMock.mockReturnValue(["openai:default"]);
|
||||
resolveAuthProfileDisplayLabelMock.mockReturnValue("openai:default");
|
||||
mocks.resolveAuthProfileOrder.mockReturnValue(["openai:default"]);
|
||||
mocks.resolveAuthProfileDisplayLabel.mockReturnValue("openai:default");
|
||||
|
||||
const label = resolveModelAuthLabel({
|
||||
provider: "openai",
|
||||
|
|
@ -110,7 +88,7 @@ describe("resolveModelAuthLabel", () => {
|
|||
});
|
||||
|
||||
it("shows oauth type with profile label", () => {
|
||||
ensureAuthProfileStoreMock.mockReturnValue({
|
||||
mocks.ensureAuthProfileStore.mockReturnValue({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:oauth": {
|
||||
|
|
@ -119,8 +97,8 @@ describe("resolveModelAuthLabel", () => {
|
|||
},
|
||||
},
|
||||
} as never);
|
||||
resolveAuthProfileOrderMock.mockReturnValue(["anthropic:oauth"]);
|
||||
resolveAuthProfileDisplayLabelMock.mockReturnValue("anthropic:oauth");
|
||||
mocks.resolveAuthProfileOrder.mockReturnValue(["anthropic:oauth"]);
|
||||
mocks.resolveAuthProfileDisplayLabel.mockReturnValue("anthropic:oauth");
|
||||
|
||||
const label = resolveModelAuthLabel({
|
||||
provider: "anthropic",
|
||||
|
|
|
|||
|
|
@ -1,31 +1,9 @@
|
|||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { RuntimeWebFetchFirecrawlMetadata } from "../secrets/runtime-web-tools.types.js";
|
||||
import type { RuntimeWebSearchMetadata } from "../secrets/runtime-web-tools.types.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
|
||||
const mockedModuleIds = [
|
||||
"../plugins/tools.js",
|
||||
"../gateway/call.js",
|
||||
"./tools/agents-list-tool.js",
|
||||
"./tools/canvas-tool.js",
|
||||
"./tools/cron-tool.js",
|
||||
"./tools/gateway-tool.js",
|
||||
"./tools/image-generate-tool.js",
|
||||
"./tools/image-tool.js",
|
||||
"./tools/message-tool.js",
|
||||
"./tools/nodes-tool.js",
|
||||
"./tools/pdf-tool.js",
|
||||
"./tools/session-status-tool.js",
|
||||
"./tools/sessions-history-tool.js",
|
||||
"./tools/sessions-list-tool.js",
|
||||
"./tools/sessions-send-tool.js",
|
||||
"./tools/sessions-spawn-tool.js",
|
||||
"./tools/sessions-yield-tool.js",
|
||||
"./tools/subagents-tool.js",
|
||||
"./tools/tts-tool.js",
|
||||
] as const;
|
||||
|
||||
vi.mock("../plugins/tools.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../plugins/tools.js")>("../plugins/tools.js");
|
||||
return {
|
||||
|
|
@ -171,12 +149,6 @@ describe("openclaw tools runtime web metadata wiring", () => {
|
|||
secretsRuntime.clearSecretsRuntimeSnapshot();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
for (const id of mockedModuleIds) {
|
||||
vi.doUnmock(id);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses runtime-selected provider when higher-precedence provider ref is unresolved", async () => {
|
||||
const snapshot = await prepareAndActivate({
|
||||
config: asConfig({
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { createServer, type IncomingMessage } from "node:http";
|
||||
import { createRequire } from "node:module";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Duplex } from "node:stream";
|
||||
import {
|
||||
clearTimeout as clearNativeTimeout,
|
||||
setTimeout as scheduleNativeTimeout,
|
||||
} from "node:timers";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
|
|
@ -19,9 +21,34 @@ type MockWatcher = {
|
|||
__emit: (event: string, ...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
const CANVAS_WS_OPEN_TIMEOUT_MS = 2_000;
|
||||
const CANVAS_RELOAD_TIMEOUT_MS = 4_000;
|
||||
const CANVAS_RELOAD_TEST_TIMEOUT_MS = 12_000;
|
||||
const CANVAS_WS_OPEN_TIMEOUT_MS = 4_000;
|
||||
const CANVAS_RELOAD_TIMEOUT_MS = 10_000;
|
||||
const CANVAS_RELOAD_TEST_TIMEOUT_MS = 20_000;
|
||||
|
||||
type TrackingWebSocketServerCtor = {
|
||||
new (...args: unknown[]): {
|
||||
on: (event: string, cb: (...args: unknown[]) => void) => unknown;
|
||||
emit: (event: string, ...args: unknown[]) => void;
|
||||
handleUpgrade: (
|
||||
req: IncomingMessage,
|
||||
socket: Duplex,
|
||||
head: Buffer,
|
||||
cb: (ws: TrackingWebSocket) => void,
|
||||
) => void;
|
||||
close: (cb?: (err?: Error) => void) => void;
|
||||
connectionCount: number;
|
||||
};
|
||||
latestInstance?: {
|
||||
connectionCount: number;
|
||||
};
|
||||
latestSocket?: TrackingWebSocket;
|
||||
};
|
||||
|
||||
type TrackingWebSocket = {
|
||||
sent: string[];
|
||||
on: (event: string, cb: () => void) => TrackingWebSocket;
|
||||
send: (message: string) => void;
|
||||
};
|
||||
|
||||
function isLoopbackBindDenied(error: unknown) {
|
||||
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
||||
|
|
@ -267,65 +294,117 @@ describe("canvas host", () => {
|
|||
});
|
||||
|
||||
it(
|
||||
"serves HTML with injection and broadcasts reload on file changes",
|
||||
"broadcasts reload on file changes",
|
||||
async () => {
|
||||
const dir = await createCaseDir();
|
||||
const index = path.join(dir, "index.html");
|
||||
await fs.writeFile(index, "<html><body>v1</body></html>", "utf8");
|
||||
|
||||
const watcherStart = watcherState.watchers.length;
|
||||
let server: Awaited<ReturnType<typeof startFixtureCanvasHost>>;
|
||||
try {
|
||||
server = await startFixtureCanvasHost(dir);
|
||||
} catch (error) {
|
||||
if (isLoopbackBindDenied(error)) {
|
||||
return;
|
||||
const TrackingWebSocketServerClass = class TrackingWebSocketServer {
|
||||
static latestInstance: { connectionCount: number } | undefined;
|
||||
static latestSocket: TrackingWebSocket | undefined;
|
||||
connectionCount = 0;
|
||||
readonly handlers = new Map<string, Array<(...args: unknown[]) => void>>();
|
||||
|
||||
on(event: string, cb: (...args: unknown[]) => void) {
|
||||
const list = this.handlers.get(event) ?? [];
|
||||
list.push(cb);
|
||||
this.handlers.set(event, list);
|
||||
return this;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
emit(event: string, ...args: unknown[]) {
|
||||
for (const cb of this.handlers.get(event) ?? []) {
|
||||
cb(...args);
|
||||
}
|
||||
}
|
||||
|
||||
handleUpgrade(
|
||||
req: IncomingMessage,
|
||||
socket: Duplex,
|
||||
head: Buffer,
|
||||
cb: (ws: TrackingWebSocket) => void,
|
||||
) {
|
||||
void req;
|
||||
void socket;
|
||||
void head;
|
||||
const closeHandlers: Array<() => void> = [];
|
||||
const ws: TrackingWebSocket = {
|
||||
sent: [],
|
||||
on: (event, handler) => {
|
||||
if (event === "close") {
|
||||
closeHandlers.push(handler);
|
||||
}
|
||||
return ws;
|
||||
},
|
||||
send: (message: string) => {
|
||||
ws.sent.push(message);
|
||||
},
|
||||
};
|
||||
TrackingWebSocketServerClass.latestSocket = ws;
|
||||
cb(ws);
|
||||
}
|
||||
|
||||
close(cb?: (err?: Error) => void) {
|
||||
cb?.();
|
||||
}
|
||||
|
||||
constructor(..._args: unknown[]) {
|
||||
TrackingWebSocketServerClass.latestInstance = this;
|
||||
this.on("connection", () => {
|
||||
this.connectionCount += 1;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handler = await createCanvasHostHandler({
|
||||
runtime: quietRuntime,
|
||||
rootDir: dir,
|
||||
basePath: CANVAS_HOST_PATH,
|
||||
allowInTests: true,
|
||||
watchFactory: watcherState.watchFactory as unknown as Parameters<
|
||||
typeof createCanvasHostHandler
|
||||
>[0]["watchFactory"],
|
||||
webSocketServerClass:
|
||||
TrackingWebSocketServerClass as unknown as typeof import("ws").WebSocketServer,
|
||||
});
|
||||
|
||||
try {
|
||||
const watcher = watcherState.watchers[watcherStart];
|
||||
expect(watcher).toBeTruthy();
|
||||
|
||||
const { res, html } = await fetchCanvasHtml(server.port);
|
||||
expect(res.status).toBe(200);
|
||||
expect(html).toContain("v1");
|
||||
expect(html).toContain(CANVAS_WS_PATH);
|
||||
|
||||
const ws = new WebSocketClient(`ws://127.0.0.1:${server.port}${CANVAS_WS_PATH}`);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = scheduleNativeTimeout(
|
||||
() => reject(new Error("ws open timeout")),
|
||||
CANVAS_WS_OPEN_TIMEOUT_MS,
|
||||
);
|
||||
ws.on("open", () => {
|
||||
clearNativeTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
ws.on("error", (err) => {
|
||||
clearNativeTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
const upgraded = handler.handleUpgrade(
|
||||
{ url: CANVAS_WS_PATH } as IncomingMessage,
|
||||
{} as Duplex,
|
||||
Buffer.alloc(0),
|
||||
);
|
||||
expect(upgraded).toBe(true);
|
||||
expect(TrackingWebSocketServerClass.latestInstance?.connectionCount).toBe(1);
|
||||
const ws = TrackingWebSocketServerClass.latestSocket;
|
||||
expect(ws).toBeTruthy();
|
||||
|
||||
const msg = new Promise<string>((resolve, reject) => {
|
||||
const timer = scheduleNativeTimeout(
|
||||
() => reject(new Error("reload timeout")),
|
||||
CANVAS_RELOAD_TIMEOUT_MS,
|
||||
);
|
||||
ws.on("message", (data) => {
|
||||
clearNativeTimeout(timer);
|
||||
resolve(rawDataToString(data));
|
||||
});
|
||||
const deadline = Date.now() + CANVAS_RELOAD_TIMEOUT_MS;
|
||||
const poll = () => {
|
||||
const value = ws?.sent[0];
|
||||
if (value) {
|
||||
resolve(value);
|
||||
return;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
reject(new Error("reload timeout"));
|
||||
return;
|
||||
}
|
||||
void sleep(10).then(poll, reject);
|
||||
};
|
||||
void poll();
|
||||
});
|
||||
|
||||
await fs.writeFile(index, "<html><body>v2</body></html>", "utf8");
|
||||
watcher.__emit("all", "change", index);
|
||||
expect(await msg).toBe("reload");
|
||||
ws.terminate();
|
||||
} finally {
|
||||
await server.close();
|
||||
await handler.close();
|
||||
}
|
||||
},
|
||||
CANVAS_RELOAD_TEST_TIMEOUT_MS,
|
||||
|
|
|
|||
|
|
@ -285,7 +285,9 @@ export async function createCanvasHostHandler(
|
|||
debounce = null;
|
||||
broadcastReload();
|
||||
}, reloadDebounceMs);
|
||||
debounce.unref?.();
|
||||
if (!testMode) {
|
||||
debounce.unref?.();
|
||||
}
|
||||
};
|
||||
|
||||
let watcherClosed = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue