perf(test): shorten security audit hotspot tests

This commit is contained in:
Peter Steinberger
2026-04-20 16:22:05 +01:00
parent 705cc97331
commit c4358fb567
5 changed files with 26 additions and 21 deletions

View File

@@ -22,6 +22,7 @@ describe("security audit channel dm policy", () => {
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
inspectAccount: () => ({ enabled: true, configured: true }),
resolveAccount: () => ({}),
isEnabled: () => true,
isConfigured: () => true,

View File

@@ -23,6 +23,7 @@ function stubChannelPlugin(params: {
security: {},
config: {
listAccountIds: () => ["default"],
inspectAccount: () => null,
resolveAccount: (cfg, accountId) => params.resolveAccount(cfg, accountId),
isEnabled: () => true,
isConfigured: () => true,

View File

@@ -89,13 +89,16 @@ export async function collectChannelSecurityFindings(params: {
plugin: (typeof params.plugins)[number],
cfg: OpenClawConfig,
accountId: string,
) =>
plugin.config.inspectAccount?.(cfg, accountId) ??
(await inspectReadOnlyChannelAccount({
) => {
if (plugin.config.inspectAccount) {
return await plugin.config.inspectAccount(cfg, accountId);
}
return await inspectReadOnlyChannelAccount({
channelId: plugin.id,
cfg,
accountId,
}));
});
};
const asAccountRecord = (value: unknown): Record<string, unknown> | null =>
value && typeof value === "object" && !Array.isArray(value)

View File

@@ -57,6 +57,10 @@ type ExecDockerRawFn = (
) => Promise<ExecDockerRawResult>;
type CodeSafetySummaryCache = Map<string, Promise<unknown>>;
type WorkspaceSkillScanLimits = {
maxFiles?: number;
maxDirVisits?: number;
};
const MAX_WORKSPACE_SKILL_SCAN_FILES_PER_WORKSPACE = 2_000;
const MAX_WORKSPACE_SKILL_ESCAPE_DETAIL_ROWS = 12;
@@ -428,6 +432,7 @@ async function getCodeSafetySummary(params: {
async function listWorkspaceSkillMarkdownFiles(
workspaceDir: string,
limits: WorkspaceSkillScanLimits = {},
): Promise<{ skillFilePaths: string[]; truncated: boolean }> {
const skillsRoot = path.join(workspaceDir, "skills");
const rootStat = await safeStat(skillsRoot);
@@ -435,18 +440,14 @@ async function listWorkspaceSkillMarkdownFiles(
return { skillFilePaths: [], truncated: false };
}
const maxFiles = limits.maxFiles ?? MAX_WORKSPACE_SKILL_SCAN_FILES_PER_WORKSPACE;
const maxTotalDirVisits = limits.maxDirVisits ?? maxFiles * 20;
const skillFiles: string[] = [];
const queue: string[] = [skillsRoot];
const visitedDirs = new Set<string>();
// Caps total BFS dequeues, not per-path depth. Named to reflect actual semantics.
const MAX_TOTAL_DIR_VISITS = MAX_WORKSPACE_SKILL_SCAN_FILES_PER_WORKSPACE * 20;
let totalDirVisits = 0;
while (
queue.length > 0 &&
skillFiles.length < MAX_WORKSPACE_SKILL_SCAN_FILES_PER_WORKSPACE &&
totalDirVisits++ < MAX_TOTAL_DIR_VISITS
) {
while (queue.length > 0 && skillFiles.length < maxFiles && totalDirVisits++ < maxTotalDirVisits) {
const dir = queue.shift()!;
// Use the module-level realpathWithTimeout so a hanging network FS doesn't
// block the BFS indefinitely (same 2 s guard as the outer escape-detection loop).
@@ -965,6 +966,7 @@ export async function collectPluginsTrustFindings(params: {
export async function collectWorkspaceSkillSymlinkEscapeFindings(params: {
cfg: OpenClawConfig;
skillScanLimits?: WorkspaceSkillScanLimits;
}): Promise<SecurityAuditFinding[]> {
const findings: SecurityAuditFinding[] = [];
const workspaceDirs = listAgentWorkspaceDirs(params.cfg);
@@ -982,7 +984,10 @@ export async function collectWorkspaceSkillSymlinkEscapeFindings(params: {
for (const workspaceDir of workspaceDirs) {
const workspacePath = path.resolve(workspaceDir);
const workspaceRealPath = (await realpathWithTimeout(workspacePath)) ?? workspacePath;
const { skillFilePaths, truncated } = await listWorkspaceSkillMarkdownFiles(workspacePath);
const { skillFilePaths, truncated } = await listWorkspaceSkillMarkdownFiles(
workspacePath,
params.skillScanLimits,
);
if (truncated) {
// The BFS visit cap was hit before the full skills/ tree was scanned.

View File

@@ -117,15 +117,9 @@ describe("security audit workspace skill path escape findings", () => {
const skillsRoot = path.join(workspaceDir, "skills");
await fs.mkdir(skillsRoot, { recursive: true });
// Strategy: the first readdir (on skillsRoot) returns 41 001 unique subdir
// entries, filling the queue beyond the BFS visit cap
// (MAX_TOTAL_DIR_VISITS = 2000 * 20 = 40 000). All subsequent readdir calls
// return [] so no further queue growth occurs. After 40 000 dequeues the
// loop exits with ~1 001 entries still in queue → truncated = true.
//
// fs.realpath is also mocked to return paths immediately (no real I/O),
// keeping the 40 000 iterations fast (pure microtask overhead, <200 ms).
const FAKE_DIRS = 41_001;
// Use a tiny injected visit cap to exercise the truncation branch without
// forcing the test to await tens of thousands of mocked readdir calls.
const FAKE_DIRS = 3;
const fakeDirEntries = Array.from({ length: FAKE_DIRS }, (_, i) => ({
name: `d${i}`,
isDirectory: () => true,
@@ -150,6 +144,7 @@ describe("security audit workspace skill path escape findings", () => {
try {
const findings = await collectWorkspaceSkillSymlinkEscapeFindings({
cfg: { agents: { defaults: { workspace: workspaceDir } } } satisfies OpenClawConfig,
skillScanLimits: { maxDirVisits: 2 },
});
const truncFinding = findings.find((f) => f.checkId === "skills.workspace.scan_truncated");
expect(truncFinding).toBeDefined();