mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
test: micro-optimize heavy gateway/browser/telegram suites
This commit is contained in:
@@ -11,13 +11,28 @@ import {
|
||||
import { AcpxRuntime, decodeAcpxRuntimeHandleState } from "./runtime.js";
|
||||
|
||||
let sharedFixture: Awaited<ReturnType<typeof createMockRuntimeFixture>> | null = null;
|
||||
let missingCommandRuntime: AcpxRuntime | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
sharedFixture = await createMockRuntimeFixture();
|
||||
missingCommandRuntime = new AcpxRuntime(
|
||||
{
|
||||
command: "/definitely/missing/acpx",
|
||||
allowPluginLocalInstall: false,
|
||||
installCommand: "n/a",
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "approve-reads",
|
||||
nonInteractivePermissions: "fail",
|
||||
strictWindowsCmdWrapper: true,
|
||||
queueOwnerTtlSeconds: 0.1,
|
||||
},
|
||||
{ logger: NOOP_LOGGER },
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
sharedFixture = null;
|
||||
missingCommandRuntime = null;
|
||||
await cleanupMockRuntimeFixtures();
|
||||
});
|
||||
|
||||
@@ -319,22 +334,12 @@ describe("AcpxRuntime", () => {
|
||||
});
|
||||
|
||||
it("marks runtime unhealthy when command is missing", async () => {
|
||||
const runtime = new AcpxRuntime(
|
||||
{
|
||||
command: "/definitely/missing/acpx",
|
||||
allowPluginLocalInstall: false,
|
||||
installCommand: "n/a",
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "approve-reads",
|
||||
nonInteractivePermissions: "fail",
|
||||
strictWindowsCmdWrapper: true,
|
||||
queueOwnerTtlSeconds: 0.1,
|
||||
},
|
||||
{ logger: NOOP_LOGGER },
|
||||
);
|
||||
|
||||
await runtime.probeAvailability();
|
||||
expect(runtime.isHealthy()).toBe(false);
|
||||
expect(missingCommandRuntime).toBeDefined();
|
||||
if (!missingCommandRuntime) {
|
||||
throw new Error("missing-command runtime fixture missing");
|
||||
}
|
||||
await missingCommandRuntime.probeAvailability();
|
||||
expect(missingCommandRuntime.isHealthy()).toBe(false);
|
||||
});
|
||||
|
||||
it("logs ACPX spawn resolution once per command policy", async () => {
|
||||
@@ -363,21 +368,11 @@ describe("AcpxRuntime", () => {
|
||||
});
|
||||
|
||||
it("returns doctor report for missing command", async () => {
|
||||
const runtime = new AcpxRuntime(
|
||||
{
|
||||
command: "/definitely/missing/acpx",
|
||||
allowPluginLocalInstall: false,
|
||||
installCommand: "n/a",
|
||||
cwd: process.cwd(),
|
||||
permissionMode: "approve-reads",
|
||||
nonInteractivePermissions: "fail",
|
||||
strictWindowsCmdWrapper: true,
|
||||
queueOwnerTtlSeconds: 0.1,
|
||||
},
|
||||
{ logger: NOOP_LOGGER },
|
||||
);
|
||||
|
||||
const report = await runtime.doctor();
|
||||
expect(missingCommandRuntime).toBeDefined();
|
||||
if (!missingCommandRuntime) {
|
||||
throw new Error("missing-command runtime fixture missing");
|
||||
}
|
||||
const report = await missingCommandRuntime.doctor();
|
||||
expect(report.ok).toBe(false);
|
||||
expect(report.code).toBe("ACP_BACKEND_UNAVAILABLE");
|
||||
expect(report.installCommand).toContain("acpx");
|
||||
|
||||
@@ -17,6 +17,7 @@ async function pathExists(filePath: string): Promise<boolean> {
|
||||
|
||||
let fixtureRoot = "";
|
||||
let fixtureCount = 0;
|
||||
let syncSourceTemplateDir = "";
|
||||
|
||||
async function createCaseDir(prefix: string): Promise<string> {
|
||||
const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`);
|
||||
@@ -26,6 +27,27 @@ async function createCaseDir(prefix: string): Promise<string> {
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skills-sync-suite-"));
|
||||
syncSourceTemplateDir = await createCaseDir("source-template");
|
||||
await writeSkill({
|
||||
dir: path.join(syncSourceTemplateDir, ".extra", "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Extra version",
|
||||
});
|
||||
await writeSkill({
|
||||
dir: path.join(syncSourceTemplateDir, ".bundled", "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Bundled version",
|
||||
});
|
||||
await writeSkill({
|
||||
dir: path.join(syncSourceTemplateDir, ".managed", "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Managed version",
|
||||
});
|
||||
await writeSkill({
|
||||
dir: path.join(syncSourceTemplateDir, "skills", "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Workspace version",
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -39,34 +61,19 @@ describe("buildWorkspaceSkillsPrompt", () => {
|
||||
) =>
|
||||
withEnv({ HOME: workspaceDir, PATH: "" }, () => buildWorkspaceSkillsPrompt(workspaceDir, opts));
|
||||
|
||||
it("syncs merged skills into a target workspace", async () => {
|
||||
const cloneSourceTemplate = async () => {
|
||||
const sourceWorkspace = await createCaseDir("source");
|
||||
await fs.cp(syncSourceTemplateDir, sourceWorkspace, { recursive: true });
|
||||
return sourceWorkspace;
|
||||
};
|
||||
|
||||
it("syncs merged skills into a target workspace", async () => {
|
||||
const sourceWorkspace = await cloneSourceTemplate();
|
||||
const targetWorkspace = await createCaseDir("target");
|
||||
const extraDir = path.join(sourceWorkspace, ".extra");
|
||||
const bundledDir = path.join(sourceWorkspace, ".bundled");
|
||||
const managedDir = path.join(sourceWorkspace, ".managed");
|
||||
|
||||
await writeSkill({
|
||||
dir: path.join(extraDir, "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Extra version",
|
||||
});
|
||||
await writeSkill({
|
||||
dir: path.join(bundledDir, "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Bundled version",
|
||||
});
|
||||
await writeSkill({
|
||||
dir: path.join(managedDir, "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Managed version",
|
||||
});
|
||||
await writeSkill({
|
||||
dir: path.join(sourceWorkspace, "skills", "demo-skill"),
|
||||
name: "demo-skill",
|
||||
description: "Workspace version",
|
||||
});
|
||||
|
||||
await withEnv({ HOME: sourceWorkspace, PATH: "" }, () =>
|
||||
syncSkillsToWorkspace({
|
||||
sourceWorkspaceDir: sourceWorkspace,
|
||||
|
||||
@@ -7,9 +7,32 @@ import { writeSkill } from "./skills.e2e-test-helpers.js";
|
||||
import { buildWorkspaceSkillSnapshot, buildWorkspaceSkillsPrompt } from "./skills.js";
|
||||
|
||||
const fixtureSuite = createFixtureSuite("openclaw-skills-snapshot-suite-");
|
||||
let truncationWorkspaceTemplateDir = "";
|
||||
let nestedRepoTemplateDir = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
await fixtureSuite.setup();
|
||||
truncationWorkspaceTemplateDir = await fixtureSuite.createCaseDir(
|
||||
"template-truncation-workspace",
|
||||
);
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const name = `skill-${String(i).padStart(2, "0")}`;
|
||||
await writeSkill({
|
||||
dir: path.join(truncationWorkspaceTemplateDir, "skills", name),
|
||||
name,
|
||||
description: "x".repeat(800),
|
||||
});
|
||||
}
|
||||
|
||||
nestedRepoTemplateDir = await fixtureSuite.createCaseDir("template-skills-repo");
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const name = `repo-skill-${String(i).padStart(2, "0")}`;
|
||||
await writeSkill({
|
||||
dir: path.join(nestedRepoTemplateDir, "skills", name),
|
||||
name,
|
||||
description: `Desc ${i}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -20,6 +43,12 @@ function withWorkspaceHome<T>(workspaceDir: string, cb: () => T): T {
|
||||
return withEnv({ HOME: workspaceDir, PATH: "" }, cb);
|
||||
}
|
||||
|
||||
async function cloneTemplateDir(templateDir: string, prefix: string): Promise<string> {
|
||||
const cloned = await fixtureSuite.createCaseDir(prefix);
|
||||
await fs.cp(templateDir, cloned, { recursive: true });
|
||||
return cloned;
|
||||
}
|
||||
|
||||
describe("buildWorkspaceSkillSnapshot", () => {
|
||||
it("returns an empty snapshot when skills dirs are missing", async () => {
|
||||
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
|
||||
@@ -110,17 +139,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("truncates the skills prompt when it exceeds the configured char budget", async () => {
|
||||
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
|
||||
|
||||
// Keep fixture size modest while still forcing truncation logic.
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const name = `skill-${String(i).padStart(2, "0")}`;
|
||||
await writeSkill({
|
||||
dir: path.join(workspaceDir, "skills", name),
|
||||
name,
|
||||
description: "x".repeat(800),
|
||||
});
|
||||
}
|
||||
const workspaceDir = await cloneTemplateDir(truncationWorkspaceTemplateDir, "workspace");
|
||||
|
||||
const snapshot = withWorkspaceHome(workspaceDir, () =>
|
||||
buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
@@ -143,16 +162,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
|
||||
it("limits discovery for nested repo-style skills roots (dir/skills/*)", async () => {
|
||||
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
|
||||
const repoDir = await fixtureSuite.createCaseDir("skills-repo");
|
||||
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const name = `repo-skill-${String(i).padStart(2, "0")}`;
|
||||
await writeSkill({
|
||||
dir: path.join(repoDir, "skills", name),
|
||||
name,
|
||||
description: `Desc ${i}`,
|
||||
});
|
||||
}
|
||||
const repoDir = await cloneTemplateDir(nestedRepoTemplateDir, "skills-repo");
|
||||
|
||||
const snapshot = withWorkspaceHome(workspaceDir, () =>
|
||||
buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createServer } from "node:http";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import WebSocket from "ws";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import {
|
||||
@@ -141,6 +141,7 @@ async function waitForListMatch<T>(
|
||||
describe("chrome extension relay server", () => {
|
||||
const TEST_GATEWAY_TOKEN = "test-gateway-token";
|
||||
let cdpUrl = "";
|
||||
let sharedCdpUrl = "";
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -162,6 +163,24 @@ describe("chrome extension relay server", () => {
|
||||
envSnapshot.restore();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!sharedCdpUrl) {
|
||||
return;
|
||||
}
|
||||
await stopChromeExtensionRelayServer({ cdpUrl: sharedCdpUrl }).catch(() => {});
|
||||
sharedCdpUrl = "";
|
||||
});
|
||||
|
||||
async function ensureSharedRelayServer() {
|
||||
if (sharedCdpUrl) {
|
||||
return sharedCdpUrl;
|
||||
}
|
||||
const port = await getFreePort();
|
||||
sharedCdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl: sharedCdpUrl });
|
||||
return sharedCdpUrl;
|
||||
}
|
||||
|
||||
async function startRelayWithExtension() {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
@@ -205,57 +224,51 @@ describe("chrome extension relay server", () => {
|
||||
const unknown = getChromeExtensionRelayAuthHeaders(`http://127.0.0.1:${port}`);
|
||||
expect(unknown).toEqual({});
|
||||
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
|
||||
const headers = getChromeExtensionRelayAuthHeaders(cdpUrl);
|
||||
const headers = getChromeExtensionRelayAuthHeaders(sharedUrl);
|
||||
expect(Object.keys(headers)).toContain("x-openclaw-relay-token");
|
||||
expect(headers["x-openclaw-relay-token"]).not.toBe(TEST_GATEWAY_TOKEN);
|
||||
});
|
||||
|
||||
it("rejects CDP access without relay auth token", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
const sharedPort = new URL(sharedUrl).port;
|
||||
|
||||
const res = await fetch(`${cdpUrl}/json/version`);
|
||||
const res = await fetch(`${sharedUrl}/json/version`);
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`);
|
||||
const cdp = new WebSocket(`ws://127.0.0.1:${sharedPort}/cdp`);
|
||||
const err = await waitForError(cdp);
|
||||
expect(err.message).toContain("401");
|
||||
});
|
||||
|
||||
it("returns 400 for malformed percent-encoding in target action routes", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
|
||||
const res = await fetch(`${cdpUrl}/json/activate/%E0%A4%A`, {
|
||||
headers: relayAuthHeaders(cdpUrl),
|
||||
const res = await fetch(`${sharedUrl}/json/activate/%E0%A4%A`, {
|
||||
headers: relayAuthHeaders(sharedUrl),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.text()).toContain("invalid targetId encoding");
|
||||
});
|
||||
|
||||
it("deduplicates concurrent relay starts for the same requested port", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
const port = Number(new URL(sharedUrl).port);
|
||||
const [first, second] = await Promise.all([
|
||||
ensureChromeExtensionRelayServer({ cdpUrl }),
|
||||
ensureChromeExtensionRelayServer({ cdpUrl }),
|
||||
ensureChromeExtensionRelayServer({ cdpUrl: sharedUrl }),
|
||||
ensureChromeExtensionRelayServer({ cdpUrl: sharedUrl }),
|
||||
]);
|
||||
expect(first).toBe(second);
|
||||
expect(first.port).toBe(port);
|
||||
});
|
||||
|
||||
it("allows CORS preflight from chrome-extension origins", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
|
||||
const origin = "chrome-extension://abcdefghijklmnop";
|
||||
const res = await fetch(`${cdpUrl}/json/version`, {
|
||||
const res = await fetch(`${sharedUrl}/json/version`, {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
Origin: origin,
|
||||
@@ -272,11 +285,9 @@ describe("chrome extension relay server", () => {
|
||||
});
|
||||
|
||||
it("rejects CORS preflight from non-extension origins", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
|
||||
const res = await fetch(`${cdpUrl}/json/version`, {
|
||||
const res = await fetch(`${sharedUrl}/json/version`, {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
Origin: "https://example.com",
|
||||
@@ -288,15 +299,13 @@ describe("chrome extension relay server", () => {
|
||||
});
|
||||
|
||||
it("returns CORS headers on JSON responses for extension origins", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
|
||||
const origin = "chrome-extension://abcdefghijklmnop";
|
||||
const res = await fetch(`${cdpUrl}/json/version`, {
|
||||
const res = await fetch(`${sharedUrl}/json/version`, {
|
||||
headers: {
|
||||
Origin: origin,
|
||||
...relayAuthHeaders(cdpUrl),
|
||||
...relayAuthHeaders(sharedUrl),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -305,11 +314,10 @@ describe("chrome extension relay server", () => {
|
||||
});
|
||||
|
||||
it("rejects extension websocket access without relay auth token", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
const sharedPort = new URL(sharedUrl).port;
|
||||
|
||||
const ext = new WebSocket(`ws://127.0.0.1:${port}/extension`);
|
||||
const ext = new WebSocket(`ws://127.0.0.1:${sharedPort}/extension`);
|
||||
const err = await waitForError(ext);
|
||||
expect(err.message).toContain("401");
|
||||
});
|
||||
@@ -566,44 +574,42 @@ describe("chrome extension relay server", () => {
|
||||
});
|
||||
|
||||
it("accepts extension websocket access with relay token query param", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
const sharedPort = new URL(sharedUrl).port;
|
||||
|
||||
const token = relayAuthHeaders(`ws://127.0.0.1:${port}/extension`)["x-openclaw-relay-token"];
|
||||
const token = relayAuthHeaders(`ws://127.0.0.1:${sharedPort}/extension`)[
|
||||
"x-openclaw-relay-token"
|
||||
];
|
||||
expect(token).toBeTruthy();
|
||||
const ext = new WebSocket(
|
||||
`ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(String(token))}`,
|
||||
`ws://127.0.0.1:${sharedPort}/extension?token=${encodeURIComponent(String(token))}`,
|
||||
);
|
||||
await waitForOpen(ext);
|
||||
ext.close();
|
||||
});
|
||||
|
||||
it("accepts /json endpoints with relay token query param", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
|
||||
const token = relayAuthHeaders(cdpUrl)["x-openclaw-relay-token"];
|
||||
const token = relayAuthHeaders(sharedUrl)["x-openclaw-relay-token"];
|
||||
expect(token).toBeTruthy();
|
||||
const versionRes = await fetch(
|
||||
`${cdpUrl}/json/version?token=${encodeURIComponent(String(token))}`,
|
||||
`${sharedUrl}/json/version?token=${encodeURIComponent(String(token))}`,
|
||||
);
|
||||
expect(versionRes.status).toBe(200);
|
||||
});
|
||||
|
||||
it("accepts raw gateway token for relay auth compatibility", async () => {
|
||||
const port = await getFreePort();
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
const sharedUrl = await ensureSharedRelayServer();
|
||||
const sharedPort = new URL(sharedUrl).port;
|
||||
|
||||
const versionRes = await fetch(`${cdpUrl}/json/version`, {
|
||||
const versionRes = await fetch(`${sharedUrl}/json/version`, {
|
||||
headers: { "x-openclaw-relay-token": TEST_GATEWAY_TOKEN },
|
||||
});
|
||||
expect(versionRes.status).toBe(200);
|
||||
|
||||
const ext = new WebSocket(
|
||||
`ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(TEST_GATEWAY_TOKEN)}`,
|
||||
`ws://127.0.0.1:${sharedPort}/extension?token=${encodeURIComponent(TEST_GATEWAY_TOKEN)}`,
|
||||
);
|
||||
await waitForOpen(ext);
|
||||
ext.close();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-cha
|
||||
import { buildDeviceAuthPayload } from "./device-auth.js";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
import {
|
||||
createGatewaySuiteHarness,
|
||||
connectReq,
|
||||
getTrackedConnectChallengeNonce,
|
||||
getFreePort,
|
||||
@@ -360,6 +361,7 @@ export {
|
||||
connectReq,
|
||||
CONTROL_UI_CLIENT,
|
||||
createSignedDevice,
|
||||
createGatewaySuiteHarness,
|
||||
ensurePairedDeviceTokenForCurrentIdentity,
|
||||
expectHelloOkServerVersion,
|
||||
getFreePort,
|
||||
|
||||
@@ -115,12 +115,11 @@ installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
let harness: GatewayServerHarness;
|
||||
let sharedSessionStoreDir: string;
|
||||
let sharedSessionStorePath: string;
|
||||
let sessionStoreCaseSeq = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await startGatewayServerHarness();
|
||||
sharedSessionStoreDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
|
||||
sharedSessionStorePath = path.join(sharedSessionStoreDir, "sessions.json");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -131,10 +130,11 @@ afterAll(async () => {
|
||||
const openClient = async (opts?: Parameters<typeof connectOk>[1]) => await harness.openClient(opts);
|
||||
|
||||
async function createSessionStoreDir() {
|
||||
await fs.rm(sharedSessionStoreDir, { recursive: true, force: true });
|
||||
await fs.mkdir(sharedSessionStoreDir, { recursive: true });
|
||||
testState.sessionStorePath = sharedSessionStorePath;
|
||||
return { dir: sharedSessionStoreDir, storePath: sharedSessionStorePath };
|
||||
const dir = path.join(sharedSessionStoreDir, `case-${sessionStoreCaseSeq++}`);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
testState.sessionStorePath = storePath;
|
||||
return { dir, storePath };
|
||||
}
|
||||
|
||||
async function writeSingleLineSession(dir: string, sessionId: string, content: string) {
|
||||
|
||||
@@ -354,6 +354,57 @@ export async function withGatewayServer<T>(
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGatewaySuiteHarness(opts?: {
|
||||
port?: number;
|
||||
serverOptions?: GatewayServerOptions;
|
||||
}): Promise<{
|
||||
port: number;
|
||||
server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||
openWs: (headers?: Record<string, string>) => Promise<WebSocket>;
|
||||
close: () => Promise<void>;
|
||||
}> {
|
||||
const started = await startGatewayServerWithRetries({
|
||||
port: opts?.port ?? (await getFreePort()),
|
||||
opts: opts?.serverOptions,
|
||||
});
|
||||
return {
|
||||
port: started.port,
|
||||
server: started.server,
|
||||
openWs: async (headers?: Record<string, string>) => {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${started.port}`, headers ? { headers } : undefined);
|
||||
trackConnectChallengeNonce(ws);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("timeout waiting for ws open")), 10_000);
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
ws.off("open", onOpen);
|
||||
ws.off("error", onError);
|
||||
ws.off("close", onClose);
|
||||
};
|
||||
const onOpen = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = (err: unknown) => {
|
||||
cleanup();
|
||||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
};
|
||||
const onClose = (code: number, reason: Buffer) => {
|
||||
cleanup();
|
||||
reject(new Error(`closed ${code}: ${reason.toString()}`));
|
||||
};
|
||||
ws.once("open", onOpen);
|
||||
ws.once("error", onError);
|
||||
ws.once("close", onClose);
|
||||
});
|
||||
return ws;
|
||||
},
|
||||
close: async () => {
|
||||
await started.server.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function startServerWithClient(
|
||||
token?: string,
|
||||
opts?: GatewayServerOptions & { wsHeaders?: Record<string, string> },
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Chat, Message } from "@grammyjs/types";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
@@ -52,10 +52,10 @@ const TELEGRAM_TEST_TIMINGS = {
|
||||
} as const;
|
||||
|
||||
describe("createTelegramBot", () => {
|
||||
beforeEach(() => {
|
||||
beforeAll(() => {
|
||||
process.env.TZ = "UTC";
|
||||
});
|
||||
afterEach(() => {
|
||||
afterAll(() => {
|
||||
process.env.TZ = ORIGINAL_TZ;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { escapeRegExp, formatEnvelopeTimestamp } from "../../test/helpers/envelope-timestamp.js";
|
||||
import { expectInboundContextContract } from "../../test/helpers/inbound-contract.js";
|
||||
import {
|
||||
@@ -36,8 +36,14 @@ function resolveSkillCommands(config: Parameters<typeof listNativeCommandSpecsFo
|
||||
|
||||
const ORIGINAL_TZ = process.env.TZ;
|
||||
describe("createTelegramBot", () => {
|
||||
beforeEach(() => {
|
||||
beforeAll(() => {
|
||||
process.env.TZ = "UTC";
|
||||
});
|
||||
afterAll(() => {
|
||||
process.env.TZ = ORIGINAL_TZ;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
loadConfig.mockReturnValue({
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -49,11 +55,8 @@ describe("createTelegramBot", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env.TZ = ORIGINAL_TZ;
|
||||
});
|
||||
|
||||
it("merges custom commands with native commands", () => {
|
||||
it("merges custom commands with native commands", async () => {
|
||||
const config = {
|
||||
channels: {
|
||||
telegram: {
|
||||
@@ -68,6 +71,10 @@ describe("createTelegramBot", () => {
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(setMyCommandsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
|
||||
command: string;
|
||||
description: string;
|
||||
@@ -84,7 +91,7 @@ describe("createTelegramBot", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores custom commands that collide with native commands", () => {
|
||||
it("ignores custom commands that collide with native commands", async () => {
|
||||
const errorSpy = vi.fn();
|
||||
const config = {
|
||||
channels: {
|
||||
@@ -109,6 +116,10 @@ describe("createTelegramBot", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(setMyCommandsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
|
||||
command: string;
|
||||
description: string;
|
||||
@@ -126,7 +137,7 @@ describe("createTelegramBot", () => {
|
||||
expect(errorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registers custom commands when native commands are disabled", () => {
|
||||
it("registers custom commands when native commands are disabled", async () => {
|
||||
const config = {
|
||||
commands: { native: false },
|
||||
channels: {
|
||||
@@ -142,6 +153,10 @@ describe("createTelegramBot", () => {
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(setMyCommandsSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const registered = setMyCommandsSpy.mock.calls[0]?.[0] as Array<{
|
||||
command: string;
|
||||
description: string;
|
||||
|
||||
@@ -15,6 +15,7 @@ let sharedBinDir = "";
|
||||
let sharedHomeDir = "";
|
||||
let sharedHomeBinDir = "";
|
||||
let sharedFakePythonPath = "";
|
||||
const runScriptCache = new Map<string, { ok: boolean; stdout: string; stderr: string }>();
|
||||
|
||||
async function writeExecutable(filePath: string, body: string): Promise<void> {
|
||||
await writeFile(filePath, body, "utf8");
|
||||
@@ -29,6 +30,14 @@ function runScript(
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
} {
|
||||
const cacheKey = JSON.stringify({
|
||||
homeDir,
|
||||
extraEnv: Object.entries(extraEnv).toSorted(([a], [b]) => a.localeCompare(b)),
|
||||
});
|
||||
const cached = runScriptCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const binDir = path.join(homeDir, "bin");
|
||||
const env = {
|
||||
HOME: homeDir,
|
||||
@@ -42,7 +51,9 @@ function runScript(
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return { ok: true, stdout: stdout.trim(), stderr: "" };
|
||||
const result = { ok: true, stdout: stdout.trim(), stderr: "" };
|
||||
runScriptCache.set(cacheKey, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const e = error as {
|
||||
stdout?: string | Buffer;
|
||||
@@ -50,7 +61,9 @@ function runScript(
|
||||
};
|
||||
const stdout = typeof e.stdout === "string" ? e.stdout : (e.stdout?.toString("utf8") ?? "");
|
||||
const stderr = typeof e.stderr === "string" ? e.stderr : (e.stderr?.toString("utf8") ?? "");
|
||||
return { ok: false, stdout: stdout.trim(), stderr: stderr.trim() };
|
||||
const result = { ok: false, stdout: stdout.trim(), stderr: stderr.trim() };
|
||||
runScriptCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user