mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
test: speed up slow assertions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import type { OpenClawPluginNodeInvokePolicyContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createFileTransferNodeInvokePolicy } from "./node-invoke-policy.js";
|
||||
@@ -32,34 +32,46 @@ afterAll(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
async function tarEntries(entries: Record<string, string>): Promise<string> {
|
||||
const tmpRoot = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "node-policy-tar-")));
|
||||
tmpRoots.push(tmpRoot);
|
||||
function tarEntries(entries: Record<string, string>): string {
|
||||
const blocks: Buffer[] = [];
|
||||
for (const [relPath, contents] of Object.entries(entries)) {
|
||||
const absPath = path.join(tmpRoot, relPath);
|
||||
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
||||
await fs.writeFile(absPath, contents);
|
||||
const payload = Buffer.from(contents);
|
||||
blocks.push(createTarFileHeader(relPath, payload.byteLength), payload);
|
||||
const padding = (512 - (payload.byteLength % 512)) % 512;
|
||||
if (padding > 0) {
|
||||
blocks.push(Buffer.alloc(padding));
|
||||
}
|
||||
}
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
const tarBin = process.platform !== "win32" ? "/usr/bin/tar" : "tar";
|
||||
const child = spawn(tarBin, ["-czf", "-", "-C", tmpRoot, "."], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const chunks: Buffer[] = [];
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
child.stderr.on("data", (chunk: Buffer) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`tar exited ${code}: ${stderr}`));
|
||||
return;
|
||||
}
|
||||
resolve(Buffer.concat(chunks).toString("base64"));
|
||||
});
|
||||
child.on("error", reject);
|
||||
});
|
||||
blocks.push(Buffer.alloc(1024));
|
||||
return gzipSync(Buffer.concat(blocks)).toString("base64");
|
||||
}
|
||||
|
||||
function writeTarString(header: Buffer, offset: number, length: number, value: string): void {
|
||||
header.write(value.slice(0, length), offset, length, "utf8");
|
||||
}
|
||||
|
||||
function writeTarOctal(header: Buffer, offset: number, length: number, value: number): void {
|
||||
const text = value.toString(8).padStart(length - 1, "0");
|
||||
header.write(`${text}\0`.slice(-length), offset, length, "ascii");
|
||||
}
|
||||
|
||||
function createTarFileHeader(name: string, size: number): Buffer {
|
||||
const header = Buffer.alloc(512);
|
||||
writeTarString(header, 0, 100, name);
|
||||
writeTarOctal(header, 100, 8, 0o644);
|
||||
writeTarOctal(header, 108, 8, 0);
|
||||
writeTarOctal(header, 116, 8, 0);
|
||||
writeTarOctal(header, 124, 12, size);
|
||||
writeTarOctal(header, 136, 12, 0);
|
||||
header.fill(" ", 148, 156);
|
||||
header.write("0", 156, 1, "ascii");
|
||||
header.write("ustar\0", 257, 6, "ascii");
|
||||
header.write("00", 263, 2, "ascii");
|
||||
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
||||
header.write(checksum.toString(8).padStart(6, "0"), 148, 6, "ascii");
|
||||
header[154] = 0;
|
||||
header[155] = 0x20;
|
||||
return header;
|
||||
}
|
||||
|
||||
function createCtx(overrides: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
@@ -148,10 +149,68 @@ function collectTextFiles(dir: string): string[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
function isExistingTextFile(file: string): boolean {
|
||||
try {
|
||||
return lstatSync(file).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function collectWorkspaceTextFiles(): string[] {
|
||||
return SOURCE_ROOTS.flatMap((root) => collectTextFiles(resolve(REPO_ROOT, root))).toSorted(
|
||||
(left, right) => relative(REPO_ROOT, left).localeCompare(relative(REPO_ROOT, right)),
|
||||
const gitFiles = collectWorkspaceTextFilesFromGit();
|
||||
return (
|
||||
gitFiles ?? SOURCE_ROOTS.flatMap((root) => collectTextFiles(resolve(REPO_ROOT, root)))
|
||||
).toSorted((left, right) => relative(REPO_ROOT, left).localeCompare(relative(REPO_ROOT, right)));
|
||||
}
|
||||
|
||||
function collectWorkspaceTextFilesFromGit(): string[] | null {
|
||||
const result = spawnSync(
|
||||
"git",
|
||||
["ls-files", "--cached", "--others", "--exclude-standard", "--", ...SOURCE_ROOTS],
|
||||
{
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && TEXT_FILE_PATTERN.test(line))
|
||||
.filter((line) => !line.split("/").some((part) => SKIPPED_DIRS.has(part)))
|
||||
.map((line) => resolve(REPO_ROOT, line))
|
||||
.filter(isExistingTextFile);
|
||||
}
|
||||
|
||||
function collectWorkspaceTextFilesMatchingGit(pattern: string): string[] | null {
|
||||
const result = spawnSync(
|
||||
"git",
|
||||
["grep", "--untracked", "-l", "-E", pattern, "--", ...SOURCE_ROOTS],
|
||||
{
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
},
|
||||
);
|
||||
if (result.status === 1) {
|
||||
return [];
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && TEXT_FILE_PATTERN.test(line))
|
||||
.filter((line) => !line.split("/").some((part) => SKIPPED_DIRS.has(part)))
|
||||
.map((line) => resolve(REPO_ROOT, line))
|
||||
.filter(isExistingTextFile);
|
||||
}
|
||||
|
||||
function repoRelative(file: string): string {
|
||||
@@ -166,6 +225,26 @@ function collectWorkspaceTextFileSources(): WorkspaceTextFile[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function collectSummaryWorkspaceTextFileSources(): WorkspaceTextFile[] {
|
||||
const pluginSdkFiles = collectWorkspaceTextFilesMatchingGit(
|
||||
String.raw`openclaw/plugin-sdk/[a-z0-9][a-z0-9-]*`,
|
||||
);
|
||||
if (!pluginSdkFiles) {
|
||||
return collectWorkspaceTextFileSources();
|
||||
}
|
||||
const files = new Set(pluginSdkFiles);
|
||||
for (const file of collectTextFiles(resolve(REPO_ROOT, "packages/memory-host-sdk/src"))) {
|
||||
files.add(file);
|
||||
}
|
||||
return [...files]
|
||||
.toSorted((left, right) => repoRelative(left).localeCompare(repoRelative(right)))
|
||||
.map((file) => ({
|
||||
file,
|
||||
relativeFile: repoRelative(file),
|
||||
source: readFileSync(file, "utf8"),
|
||||
}));
|
||||
}
|
||||
|
||||
function isDocsFile(file: string): boolean {
|
||||
return file.startsWith("docs/") || file === "README.md";
|
||||
}
|
||||
@@ -444,7 +523,9 @@ function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSum
|
||||
}
|
||||
|
||||
function buildReport(options: Pick<CliOptions, "owner" | "summary"> = {}): BoundaryReport {
|
||||
const files = collectWorkspaceTextFileSources();
|
||||
const files = options.summary
|
||||
? collectSummaryWorkspaceTextFileSources()
|
||||
: collectWorkspaceTextFileSources();
|
||||
const pluginIds = collectBundledPluginIds();
|
||||
const compatRecords = collectCompatDebt(files, new Date(), {
|
||||
includeReferenceFiles: !options.summary,
|
||||
|
||||
@@ -11,7 +11,7 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("loadExtensions", () => {
|
||||
it("resolves the generic LLM plugin SDK subpath in jiti-loaded extensions", async () => {
|
||||
it("resolves plugin SDK subpaths in jiti-loaded extensions", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-sdk-"));
|
||||
tempDirs.push(dir);
|
||||
const extensionPath = join(dir, "extension.ts");
|
||||
@@ -19,12 +19,16 @@ describe("loadExtensions", () => {
|
||||
extensionPath,
|
||||
`
|
||||
import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
export default async function(api) {
|
||||
const stream = createAssistantMessageEventStream();
|
||||
if (!stream || typeof stream.result !== "function") {
|
||||
throw new Error("generic LLM helper unavailable");
|
||||
}
|
||||
if (normalizeLowercaseStringOrEmpty(" MIXED ") !== "mixed") {
|
||||
throw new Error("generic sdk subpath unavailable");
|
||||
}
|
||||
api.registerCommand("sdk-subpath-probe", {
|
||||
description: "probe",
|
||||
handler() {},
|
||||
@@ -39,38 +43,4 @@ export default async function(api) {
|
||||
expect(result.extensions).toHaveLength(1);
|
||||
expect(result.extensions[0]?.commands.has("sdk-subpath-probe")).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves generic plugin SDK subpaths through the shared plugin loader aliases", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-sdk-"));
|
||||
tempDirs.push(dir);
|
||||
const extensionPath = join(dir, "extension.ts");
|
||||
await writeFile(
|
||||
extensionPath,
|
||||
`
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { defineTool } from "@openclaw/plugin-sdk/agent-sessions";
|
||||
|
||||
export default async function(api) {
|
||||
if (normalizeLowercaseStringOrEmpty(" MIXED ") !== "mixed") {
|
||||
throw new Error("generic sdk subpath unavailable");
|
||||
}
|
||||
const tool = defineTool({
|
||||
name: "shared-sdk-probe",
|
||||
description: "probe",
|
||||
parameters: { type: "object", properties: {}, additionalProperties: false },
|
||||
handler() {
|
||||
return { content: [{ type: "text", text: "ok" }] };
|
||||
},
|
||||
});
|
||||
api.registerTool(tool);
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const result = await loadExtensions([extensionPath], dir);
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.extensions).toHaveLength(1);
|
||||
expect(result.extensions[0]?.tools.has("shared-sdk-probe")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -588,11 +588,9 @@ function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports:
|
||||
describe("channel import guardrails", () => {
|
||||
it("lists channel import guardrail sources from git without walking roots", () => {
|
||||
expectNoReaddirSyncDuring(() => {
|
||||
const extensionSources = collectExtensionSourceFiles();
|
||||
const coreSources = collectCoreSourceFiles();
|
||||
const telegramSources = collectExtensionFiles("telegram");
|
||||
|
||||
expect(extensionSources.length).toBeGreaterThan(0);
|
||||
expect(coreSources.length).toBeGreaterThan(0);
|
||||
expect(telegramSources.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -34,11 +34,19 @@ export function installSurfaceContractRegistryShard(params: ContractShardParams)
|
||||
installEmptyShardSuite("surface contract registry shard");
|
||||
return;
|
||||
}
|
||||
const pluginCache = new Map<string, Awaited<ReturnType<typeof getBundledChannelPluginAsync>>>();
|
||||
beforeAll(async () => {
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
pluginCache.set(id, await getBundledChannelPluginAsync(id));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
for (const id of ids) {
|
||||
describe(`${id} surface contracts`, () => {
|
||||
it("exposes declared surface contracts", async () => {
|
||||
const plugin = await getBundledChannelPluginAsync(id);
|
||||
it("exposes declared surface contracts", () => {
|
||||
const plugin = pluginCache.get(id);
|
||||
if (!plugin) {
|
||||
throw new Error(`Missing bundled channel plugin for ${id}`);
|
||||
}
|
||||
@@ -90,18 +98,26 @@ export function installThreadingContractRegistryShard(params: ContractShardParam
|
||||
installEmptyShardSuite("threading contract registry shard");
|
||||
return;
|
||||
}
|
||||
const pluginCache = new Map<string, Awaited<ReturnType<typeof getBundledChannelPluginAsync>>>();
|
||||
beforeAll(async () => {
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
pluginCache.set(entry.id, await getBundledChannelPluginAsync(entry.id));
|
||||
}),
|
||||
);
|
||||
});
|
||||
for (const entry of entries) {
|
||||
describe(`${entry.id} threading contract`, () => {
|
||||
it("exposes the base threading contract", async () => {
|
||||
const plugin = await getBundledChannelPluginAsync(entry.id);
|
||||
it("exposes the base threading contract", () => {
|
||||
const plugin = pluginCache.get(entry.id);
|
||||
if (!plugin) {
|
||||
throw new Error(`Missing bundled channel plugin for ${entry.id}`);
|
||||
}
|
||||
expectChannelThreadingBaseContract(plugin);
|
||||
});
|
||||
|
||||
it("keeps threading return values normalized", async () => {
|
||||
const plugin = await getBundledChannelPluginAsync(entry.id);
|
||||
it("keeps threading return values normalized", () => {
|
||||
const plugin = pluginCache.get(entry.id);
|
||||
if (!plugin) {
|
||||
throw new Error(`Missing bundled channel plugin for ${entry.id}`);
|
||||
}
|
||||
@@ -117,10 +133,18 @@ export function installPluginContractRegistryShard(params: ContractShardParams)
|
||||
installEmptyShardSuite("plugin contract registry shard");
|
||||
return;
|
||||
}
|
||||
const pluginCache = new Map<string, Awaited<ReturnType<typeof getBundledChannelPluginAsync>>>();
|
||||
beforeAll(async () => {
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
pluginCache.set(entry.id, await getBundledChannelPluginAsync(entry.id));
|
||||
}),
|
||||
);
|
||||
});
|
||||
for (const entry of entries) {
|
||||
describe(`${entry.id} plugin contract`, () => {
|
||||
it("satisfies the base channel plugin contract", async () => {
|
||||
const plugin = await getBundledChannelPluginAsync(entry.id);
|
||||
it("satisfies the base channel plugin contract", () => {
|
||||
const plugin = pluginCache.get(entry.id);
|
||||
if (!plugin) {
|
||||
throw new Error(`Missing bundled channel plugin for ${entry.id}`);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { defineToolPlugin, getToolPluginMetadata } from "../plugin-sdk/tool-plugin.js";
|
||||
import {
|
||||
buildToolPluginManifest,
|
||||
@@ -55,7 +55,65 @@ function createOptionalDemoMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function writeSourceToolPluginProject(params: {
|
||||
tmpDir: string;
|
||||
packageName: string;
|
||||
pluginId: string;
|
||||
toolName: string;
|
||||
}): string {
|
||||
const sourceDir = path.join(params.tmpDir, "src");
|
||||
fs.mkdirSync(sourceDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(params.tmpDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: params.packageName,
|
||||
type: "module",
|
||||
openclaw: { extensions: ["./src/index.ts"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
const entryPath = path.join(sourceDir, "index.ts");
|
||||
fs.writeFileSync(
|
||||
entryPath,
|
||||
`import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
||||
|
||||
export default defineToolPlugin({
|
||||
id: ${JSON.stringify(params.pluginId)},
|
||||
name: "Source Demo",
|
||||
description: "Source demo plugin.",
|
||||
tools: (tool) => [
|
||||
tool({
|
||||
name: ${JSON.stringify(params.toolName)},
|
||||
description: "Echo input.",
|
||||
parameters: { type: "object", additionalProperties: false, properties: {} },
|
||||
execute: async () => ({ ok: true }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
`,
|
||||
);
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
describe("plugin authoring commands", () => {
|
||||
beforeAll(async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-source-warm-"));
|
||||
try {
|
||||
const entryPath = writeSourceToolPluginProject({
|
||||
tmpDir,
|
||||
packageName: "openclaw-plugin-source-warm",
|
||||
pluginId: "source-warm",
|
||||
toolName: "source_warm_echo",
|
||||
});
|
||||
await loadToolPlugin({ rootDir: tmpDir, entryPath });
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("generates manifest metadata from defineToolPlugin metadata", () => {
|
||||
const metadata = createDemoMetadata();
|
||||
|
||||
@@ -238,43 +296,16 @@ describe("plugin authoring commands", () => {
|
||||
|
||||
it("loads source entries that import the OpenClaw plugin SDK package subpath", async () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-source-"));
|
||||
const sourceDir = path.join(tmpDir, "src");
|
||||
fs.mkdirSync(sourceDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "openclaw-plugin-source-demo",
|
||||
type: "module",
|
||||
openclaw: { extensions: ["./src/index.ts"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(sourceDir, "index.ts"),
|
||||
`import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
||||
|
||||
export default defineToolPlugin({
|
||||
id: "source-demo",
|
||||
name: "Source Demo",
|
||||
description: "Source demo plugin.",
|
||||
tools: (tool) => [
|
||||
tool({
|
||||
name: "source_echo",
|
||||
description: "Echo input.",
|
||||
parameters: { type: "object", additionalProperties: false, properties: {} },
|
||||
execute: async () => ({ ok: true }),
|
||||
}),
|
||||
],
|
||||
});
|
||||
`,
|
||||
);
|
||||
const entryPath = writeSourceToolPluginProject({
|
||||
tmpDir,
|
||||
packageName: "openclaw-plugin-source-demo",
|
||||
pluginId: "source-demo",
|
||||
toolName: "source_echo",
|
||||
});
|
||||
|
||||
const loaded = await loadToolPlugin({
|
||||
rootDir: tmpDir,
|
||||
entryPath: path.join(sourceDir, "index.ts"),
|
||||
entryPath,
|
||||
});
|
||||
|
||||
expect(loaded.metadata.id).toBe("source-demo");
|
||||
|
||||
@@ -392,7 +392,7 @@ describe("appendAssistantMessageToSessionTranscript", () => {
|
||||
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
|
||||
await appendSessionTranscriptMessage({
|
||||
transcriptPath: sessionFile,
|
||||
message: { role: "user", content: "x".repeat(5 * 1024 * 1024) },
|
||||
message: { role: "user", content: "x".repeat(128 * 1024) },
|
||||
});
|
||||
|
||||
const latestAssistantText = await readLatestAssistantTextFromSessionTranscript(sessionFile);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
|
||||
@@ -17,14 +17,19 @@ vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: () => emptyPluginMetadataSnapshot,
|
||||
}));
|
||||
|
||||
let sessionUtils: typeof import("./session-utils.js");
|
||||
|
||||
describe("gateway session list plugin runtime normalization", () => {
|
||||
beforeEach(() => {
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
sessionUtils = await import("./session-utils.js");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
normalizeProviderModelIdWithPluginMock.mockReset();
|
||||
});
|
||||
|
||||
it("skips provider runtime normalization for lightweight list rows", async () => {
|
||||
const { listSessionsFromStoreAsync } = await import("./session-utils.js");
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: { model: { primary: "custom-provider/custom-legacy-model" } },
|
||||
@@ -37,7 +42,7 @@ describe("gateway session list plugin runtime normalization", () => {
|
||||
]),
|
||||
);
|
||||
|
||||
const listed = await listSessionsFromStoreAsync({
|
||||
const listed = await sessionUtils.listSessionsFromStoreAsync({
|
||||
cfg,
|
||||
storePath: "",
|
||||
store,
|
||||
@@ -62,14 +67,13 @@ describe("gateway session list plugin runtime normalization", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const { buildGatewaySessionRow } = await import("./session-utils.js");
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: { model: { primary: "custom-provider/custom-legacy-model" } },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const row = buildGatewaySessionRow({
|
||||
const row = sessionUtils.buildGatewaySessionRow({
|
||||
cfg,
|
||||
storePath: "",
|
||||
store: {},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js";
|
||||
import { listGitTrackedFiles, toRepoRelativePath } from "../test-utils/repo-files.js";
|
||||
import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.js";
|
||||
@@ -126,6 +126,10 @@ let repoBundledPluginMetadataCache: readonly BundledPluginMetadata[] | undefined
|
||||
let repoBundledPluginManifestsCache:
|
||||
| ReturnType<typeof listRepoBundledPluginManifestsUncached>
|
||||
| undefined;
|
||||
const repoBundledChannelConfigsCache = new Map<
|
||||
string,
|
||||
ReturnType<typeof collectBundledChannelConfigs>
|
||||
>();
|
||||
|
||||
function listRepoBundledPluginMetadata(): readonly BundledPluginMetadata[] {
|
||||
repoBundledPluginMetadataCache ??= listBundledPluginMetadata({
|
||||
@@ -264,16 +268,22 @@ function collectRootPackageExcludedExtensionDirsForTest(): readonly string[] {
|
||||
}
|
||||
|
||||
function collectRepoBundledChannelConfigsForTest(dirName: string) {
|
||||
const cached = repoBundledChannelConfigsCache.get(dirName);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pluginDir = path.join(repoRoot, "extensions", dirName);
|
||||
const manifest = loadPluginManifest(pluginDir, false);
|
||||
if (!manifest.ok) {
|
||||
throw manifest.error;
|
||||
}
|
||||
return collectBundledChannelConfigs({
|
||||
const configs = collectBundledChannelConfigs({
|
||||
pluginDir,
|
||||
manifest: manifest.manifest,
|
||||
packageManifest: getPackageManifestMetadata(readPackageManifest(pluginDir)),
|
||||
});
|
||||
repoBundledChannelConfigsCache.set(dirName, configs);
|
||||
return configs;
|
||||
}
|
||||
|
||||
function hasPluginKind(record: PluginManifestRecord, kind: string): boolean {
|
||||
@@ -325,6 +335,12 @@ function createInstalledPluginIndexForManifests(
|
||||
}
|
||||
|
||||
describe("bundled plugin metadata", () => {
|
||||
beforeAll(() => {
|
||||
listRepoBundledPluginMetadata();
|
||||
collectRepoBundledChannelConfigsForTest("discord");
|
||||
collectRepoBundledChannelConfigsForTest("tlon");
|
||||
});
|
||||
|
||||
it("lists bundled plugin manifests without scanning extension directories in-process", () => {
|
||||
expectNoReaddirSyncDuring(() => {
|
||||
const manifests = listRepoBundledPluginManifestsUncached();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { isEmbeddedMode, setEmbeddedMode } from "../infra/embedded-mode.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
|
||||
@@ -152,6 +152,10 @@ describe("EmbeddedTuiBackend", () => {
|
||||
const originalRuntimeLog = defaultRuntime.log;
|
||||
const originalRuntimeError = defaultRuntime.error;
|
||||
|
||||
beforeAll(async () => {
|
||||
await import("./embedded-backend.js");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(embeddedEventTimestamp);
|
||||
|
||||
@@ -31,22 +31,12 @@ afterEach(() => {
|
||||
|
||||
describe("scripts/embedded-run-abort-leak", () => {
|
||||
it("rejects loose numeric thresholds before writing heap snapshots", () => {
|
||||
const cases = [
|
||||
["--iters", "1e3", "positive"],
|
||||
["--batches", "2abc", "positive"],
|
||||
["--max-rss-growth-mb", "0x10", "non-negative"],
|
||||
["--max-tracked-retention", "abc", "non-negative"],
|
||||
["--scope-bytes", "1mb", "positive"],
|
||||
] as const;
|
||||
const snapDir = makeTempRoot();
|
||||
const result = runHarness(["--snap-dir", snapDir, "--iters", "1e3", "--quiet"]);
|
||||
|
||||
for (const [flag, value, label] of cases) {
|
||||
const snapDir = makeTempRoot();
|
||||
const result = runHarness(["--snap-dir", snapDir, flag, value, "--quiet"]);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(`error: ${flag} must be a ${label} integer`);
|
||||
expect(readdirSync(snapDir)).toEqual([]);
|
||||
}
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("error: --iters must be a positive integer");
|
||||
expect(readdirSync(snapDir)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -16,6 +17,7 @@ type SuppressionEntry = {
|
||||
};
|
||||
|
||||
let productionLintSuppressionsCache: SuppressionEntry[] | null = null;
|
||||
let productionCodeFilesCache: string[] | null = null;
|
||||
|
||||
function isProductionCodeFile(relativePath: string): boolean {
|
||||
const basename = path.posix.basename(relativePath);
|
||||
@@ -77,8 +79,13 @@ function collectProductionLintSuppressions(): SuppressionEntry[] {
|
||||
if (productionLintSuppressionsCache) {
|
||||
return [...productionLintSuppressionsCache];
|
||||
}
|
||||
const gitEntries = collectProductionLintSuppressionsFromGit();
|
||||
if (gitEntries) {
|
||||
productionLintSuppressionsCache = gitEntries;
|
||||
return [...gitEntries];
|
||||
}
|
||||
const entries: SuppressionEntry[] = [];
|
||||
const files = ROOTS.flatMap((root) => walkCodeFiles(path.join(repoRoot, root))).toSorted();
|
||||
const files = listProductionCodeFiles();
|
||||
for (const relativePath of files) {
|
||||
const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
for (const line of source.split("\n")) {
|
||||
@@ -96,6 +103,56 @@ function collectProductionLintSuppressions(): SuppressionEntry[] {
|
||||
return [...entries];
|
||||
}
|
||||
|
||||
function collectProductionLintSuppressionsFromGit(): SuppressionEntry[] | null {
|
||||
const result = spawnSync(
|
||||
"git",
|
||||
[
|
||||
"grep",
|
||||
"-n",
|
||||
"-E",
|
||||
String.raw`(oxlint|eslint)-disable(-next-line)?[[:space:]]+[@/[:alnum:]_-]+`,
|
||||
"--",
|
||||
...ROOTS,
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
},
|
||||
);
|
||||
if (result.status === 1) {
|
||||
return [];
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
const entries: SuppressionEntry[] = [];
|
||||
for (const line of result.stdout.split("\n")) {
|
||||
const match = /^([^:]+):\d+:(.*)$/u.exec(line);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const [, file, sourceLine] = match;
|
||||
if (!isProductionCodeFile(file)) {
|
||||
continue;
|
||||
}
|
||||
const suppression = sourceLine.match(SUPPRESSION_PATTERN);
|
||||
if (!suppression) {
|
||||
continue;
|
||||
}
|
||||
entries.push({ file, rule: suppression[1] });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function listProductionCodeFiles(): string[] {
|
||||
productionCodeFilesCache ??= ROOTS.flatMap((root) =>
|
||||
walkCodeFiles(path.join(repoRoot, root)),
|
||||
).toSorted();
|
||||
return [...productionCodeFilesCache];
|
||||
}
|
||||
|
||||
function summarizeSuppressions(entries: readonly SuppressionEntry[]): string[] {
|
||||
const counts = new Map<string, number>();
|
||||
for (const entry of entries) {
|
||||
@@ -108,7 +165,7 @@ function summarizeSuppressions(entries: readonly SuppressionEntry[]): string[] {
|
||||
describe("production lint suppressions", () => {
|
||||
it("lists production files from git without walking source roots", () => {
|
||||
expectNoReaddirSyncDuring(() => {
|
||||
const files = ROOTS.flatMap((root) => walkCodeFiles(path.join(repoRoot, root))).toSorted();
|
||||
const files = listProductionCodeFiles();
|
||||
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(files.some((file) => file.endsWith(".test.ts"))).toBe(false);
|
||||
|
||||
@@ -138,10 +138,10 @@ describe("package-openclaw-for-docker", () => {
|
||||
|
||||
const runPromise = runCommandForTest(process.execPath, ["-e", parentScript], process.cwd(), {
|
||||
env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath },
|
||||
killAfterMs: 50,
|
||||
timeoutMs: 1500,
|
||||
killAfterMs: 25,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
const timeoutAssertion = expect(runPromise).rejects.toThrow(/timed out after 1500ms/u);
|
||||
const timeoutAssertion = expect(runPromise).rejects.toThrow(/timed out after 1000ms/u);
|
||||
await waitForFile(childPidPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
await timeoutAssertion;
|
||||
@@ -177,10 +177,10 @@ describe("package-openclaw-for-docker", () => {
|
||||
await expect(
|
||||
runCommandForTest(process.execPath, ["-e", parentScript], process.cwd(), {
|
||||
env: { ...process.env, OPENCLAW_TEST_CHILD_PID: childPidPath },
|
||||
killAfterMs: 50,
|
||||
timeoutMs: 2000,
|
||||
killAfterMs: 25,
|
||||
timeoutMs: 1000,
|
||||
}),
|
||||
).rejects.toThrow(/timed out after 2000ms/u);
|
||||
).rejects.toThrow(/timed out after 1000ms/u);
|
||||
|
||||
await waitForFile(childPidPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
|
||||
Reference in New Issue
Block a user