test: share runtime scan filters and cached test scans
This commit is contained in:
@@ -289,40 +289,10 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
});
|
||||
|
||||
it("sessions_spawn reports timed out when agent.wait returns timeout", async () => {
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
let agentCallCount = 0;
|
||||
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string; params?: unknown };
|
||||
calls.push(request);
|
||||
if (request.method === "agent") {
|
||||
agentCallCount += 1;
|
||||
return {
|
||||
runId: `run-${agentCallCount}`,
|
||||
status: "accepted",
|
||||
acceptedAt: 5000 + agentCallCount,
|
||||
};
|
||||
}
|
||||
if (request.method === "agent.wait") {
|
||||
const params = request.params as { runId?: string } | undefined;
|
||||
return {
|
||||
runId: params?.runId ?? "run-1",
|
||||
status: "timeout",
|
||||
startedAt: 6000,
|
||||
endedAt: 7000,
|
||||
};
|
||||
}
|
||||
if (request.method === "chat.history") {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "still working" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
const ctx = setupSessionsSpawnGatewayMock({
|
||||
includeChatHistory: true,
|
||||
chatHistoryText: "still working",
|
||||
agentWaitResult: { status: "timeout", startedAt: 6000, endedAt: 7000 },
|
||||
});
|
||||
|
||||
const tool = await getSessionsSpawnTool({
|
||||
@@ -340,9 +310,9 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
await waitFor(() => calls.filter((call) => call.method === "agent").length >= 2);
|
||||
await waitFor(() => ctx.calls.filter((call) => call.method === "agent").length >= 2);
|
||||
|
||||
const mainAgentCall = calls
|
||||
const mainAgentCall = ctx.calls
|
||||
.filter((call) => call.method === "agent")
|
||||
.find((call) => {
|
||||
const params = call.params as { lane?: string } | undefined;
|
||||
@@ -355,40 +325,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
});
|
||||
|
||||
it("sessions_spawn announces with requester accountId", async () => {
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
let agentCallCount = 0;
|
||||
let childRunId: string | undefined;
|
||||
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string; params?: unknown };
|
||||
calls.push(request);
|
||||
if (request.method === "agent") {
|
||||
agentCallCount += 1;
|
||||
const runId = `run-${agentCallCount}`;
|
||||
const params = request.params as { lane?: string; sessionKey?: string } | undefined;
|
||||
if (params?.lane === "subagent") {
|
||||
childRunId = runId;
|
||||
}
|
||||
return {
|
||||
runId,
|
||||
status: "accepted",
|
||||
acceptedAt: 4000 + agentCallCount,
|
||||
};
|
||||
}
|
||||
if (request.method === "agent.wait") {
|
||||
const params = request.params as { runId?: string; timeoutMs?: number } | undefined;
|
||||
return {
|
||||
runId: params?.runId ?? "run-1",
|
||||
status: "ok",
|
||||
startedAt: 1000,
|
||||
endedAt: 2000,
|
||||
};
|
||||
}
|
||||
if (request.method === "sessions.delete" || request.method === "sessions.patch") {
|
||||
return { ok: true };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const ctx = setupSessionsSpawnGatewayMock({});
|
||||
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
@@ -406,13 +343,14 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
if (!childRunId) {
|
||||
const child = ctx.getChild();
|
||||
if (!child.runId) {
|
||||
throw new Error("missing child runId");
|
||||
}
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
emitAgentEvent({
|
||||
runId: childRunId,
|
||||
runId: child.runId,
|
||||
stream: "lifecycle",
|
||||
data: {
|
||||
phase: "end",
|
||||
@@ -426,7 +364,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
const agentCalls = calls.filter((call) => call.method === "agent");
|
||||
const agentCalls = ctx.calls.filter((call) => call.method === "agent");
|
||||
expect(agentCalls).toHaveLength(2);
|
||||
const announceParams = agentCalls[1]?.params as
|
||||
| { accountId?: string; channel?: string; deliver?: boolean }
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AgentWaitCall = { runId?: string; timeoutMs?: number };
|
||||
type SessionsSpawnGatewayMockOptions = {
|
||||
includeSessionsList?: boolean;
|
||||
includeChatHistory?: boolean;
|
||||
chatHistoryText?: string;
|
||||
onAgentSubagentSpawn?: (params: unknown) => void;
|
||||
onSessionsPatch?: (params: unknown) => void;
|
||||
onSessionsDelete?: (params: unknown) => void;
|
||||
@@ -137,7 +138,7 @@ export function setupSessionsSpawnGatewayMock(setupOpts: SessionsSpawnGatewayMoc
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "done" }],
|
||||
content: [{ type: "text", text: setupOpts.chatHistoryText ?? "done" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -2,24 +2,11 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listRepoFiles } from "../test-utils/repo-scan.js";
|
||||
import { listRuntimeSourceFiles, shouldSkipRuntimeSourcePath } from "../test-utils/repo-scan.js";
|
||||
|
||||
const RUNTIME_ROOTS = ["src", "extensions"] as const;
|
||||
const SKIP_PATTERNS = [
|
||||
/\.test\.tsx?$/,
|
||||
/\.test-helpers\.tsx?$/,
|
||||
/\.test-utils\.tsx?$/,
|
||||
/\.e2e\.tsx?$/,
|
||||
/\.d\.ts$/,
|
||||
/[\\/](?:__tests__|tests)[\\/]/,
|
||||
/[\\/][^\\/]*test-helpers(?:\.[^\\/]+)?\.ts$/,
|
||||
];
|
||||
const QUICK_TMPDIR_JOIN_PATTERN = /\bpath\.join\s*\(\s*os\.tmpdir\s*\(\s*\)/;
|
||||
|
||||
function shouldSkip(relativePath: string): boolean {
|
||||
return SKIP_PATTERNS.some((pattern) => pattern.test(relativePath));
|
||||
}
|
||||
|
||||
function isIdentifierNamed(node: ts.Node, name: string): node is ts.Identifier {
|
||||
return ts.isIdentifier(node) && node.text === name;
|
||||
}
|
||||
@@ -86,9 +73,9 @@ function hasDynamicTmpdirJoin(source: string, filePath = "fixture.ts"): boolean
|
||||
|
||||
describe("temp path guard", () => {
|
||||
it("skips test helper filename variants", () => {
|
||||
expect(shouldSkip("src/commands/test-helpers.ts")).toBe(true);
|
||||
expect(shouldSkip("src/commands/sessions.test-helpers.ts")).toBe(true);
|
||||
expect(shouldSkip("src\\commands\\sessions.test-helpers.ts")).toBe(true);
|
||||
expect(shouldSkipRuntimeSourcePath("src/commands/test-helpers.ts")).toBe(true);
|
||||
expect(shouldSkipRuntimeSourcePath("src/commands/sessions.test-helpers.ts")).toBe(true);
|
||||
expect(shouldSkipRuntimeSourcePath("src\\commands\\sessions.test-helpers.ts")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects dynamic and ignores static fixtures", () => {
|
||||
@@ -117,16 +104,12 @@ describe("temp path guard", () => {
|
||||
const repoRoot = process.cwd();
|
||||
const offenders: string[] = [];
|
||||
|
||||
const files = await listRepoFiles(repoRoot, {
|
||||
const files = await listRuntimeSourceFiles(repoRoot, {
|
||||
roots: RUNTIME_ROOTS,
|
||||
extensions: [".ts", ".tsx"],
|
||||
skipHiddenDirectories: true,
|
||||
});
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(repoRoot, file);
|
||||
if (shouldSkip(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
const source = await fs.readFile(file, "utf-8");
|
||||
if (!QUICK_TMPDIR_JOIN_PATTERN.test(source)) {
|
||||
continue;
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listRepoFiles } from "../test-utils/repo-scan.js";
|
||||
import { listRuntimeSourceFiles } from "../test-utils/repo-scan.js";
|
||||
|
||||
const SCAN_ROOTS = ["src", "extensions"] as const;
|
||||
|
||||
function isRuntimeTypeScriptFile(relativePath: string): boolean {
|
||||
return !relativePath.endsWith(".test.ts") && !relativePath.endsWith(".d.ts");
|
||||
}
|
||||
|
||||
async function findWeakRandomPatternMatches(repoRoot: string): Promise<string[]> {
|
||||
const matches: string[] = [];
|
||||
const files = await listRepoFiles(repoRoot, {
|
||||
const files = await listRuntimeSourceFiles(repoRoot, {
|
||||
roots: SCAN_ROOTS,
|
||||
extensions: [".ts"],
|
||||
shouldIncludeFile: isRuntimeTypeScriptFile,
|
||||
});
|
||||
for (const filePath of files) {
|
||||
const lines = (await fs.readFile(filePath, "utf8")).split(/\r?\n/);
|
||||
|
||||
@@ -2,6 +2,18 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export const DEFAULT_REPO_SCAN_SKIP_DIR_NAMES = new Set([".git", "dist", "node_modules"]);
|
||||
export const DEFAULT_RUNTIME_SOURCE_ROOTS = ["src", "extensions"] as const;
|
||||
export const DEFAULT_RUNTIME_SOURCE_EXTENSIONS = [".ts", ".tsx"] as const;
|
||||
export const RUNTIME_SOURCE_SKIP_PATTERNS = [
|
||||
/\.test\.tsx?$/,
|
||||
/\.test-helpers\.tsx?$/,
|
||||
/\.test-utils\.tsx?$/,
|
||||
/\.e2e\.tsx?$/,
|
||||
/\.d\.ts$/,
|
||||
/\/(?:__tests__|tests)\//,
|
||||
/\/[^/]*test-helpers(?:\.[^/]+)?\.tsx?$/,
|
||||
/\/[^/]*test-utils(?:\.[^/]+)?\.tsx?$/,
|
||||
] as const;
|
||||
|
||||
export type RepoFileScanOptions = {
|
||||
roots: readonly string[];
|
||||
@@ -10,10 +22,15 @@ export type RepoFileScanOptions = {
|
||||
skipHiddenDirectories?: boolean;
|
||||
shouldIncludeFile?: (relativePath: string) => boolean;
|
||||
};
|
||||
export type RuntimeSourceScanOptions = {
|
||||
roots?: readonly string[];
|
||||
extensions?: readonly string[];
|
||||
};
|
||||
|
||||
type PendingDir = {
|
||||
absolutePath: string;
|
||||
};
|
||||
const runtimeSourceScanCache = new Map<string, Promise<Array<string>>>();
|
||||
|
||||
function shouldSkipDirectory(
|
||||
name: string,
|
||||
@@ -29,6 +46,18 @@ function hasAllowedExtension(fileName: string, extensions: readonly string[]): b
|
||||
return extensions.some((extension) => fileName.endsWith(extension));
|
||||
}
|
||||
|
||||
function normalizeRelativePath(relativePath: string): string {
|
||||
return relativePath.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function toSortedUnique(values: readonly string[]): Array<string> {
|
||||
return [...new Set(values)].toSorted();
|
||||
}
|
||||
|
||||
function getRuntimeScanCacheKey(repoRoot: string, roots: readonly string[]): string {
|
||||
return `${repoRoot}::${toSortedUnique(roots).join(",")}`;
|
||||
}
|
||||
|
||||
export async function listRepoFiles(
|
||||
repoRoot: string,
|
||||
options: RepoFileScanOptions,
|
||||
@@ -76,3 +105,34 @@ export async function listRepoFiles(
|
||||
files.sort((a, b) => a.localeCompare(b));
|
||||
return files;
|
||||
}
|
||||
|
||||
export function shouldSkipRuntimeSourcePath(relativePath: string): boolean {
|
||||
const normalizedPath = normalizeRelativePath(relativePath);
|
||||
return RUNTIME_SOURCE_SKIP_PATTERNS.some((pattern) => pattern.test(normalizedPath));
|
||||
}
|
||||
|
||||
export async function listRuntimeSourceFiles(
|
||||
repoRoot: string,
|
||||
options: RuntimeSourceScanOptions = {},
|
||||
): Promise<Array<string>> {
|
||||
const roots = options.roots ?? DEFAULT_RUNTIME_SOURCE_ROOTS;
|
||||
const requestedExtensions = toSortedUnique(
|
||||
options.extensions ?? DEFAULT_RUNTIME_SOURCE_EXTENSIONS,
|
||||
);
|
||||
const cacheKey = getRuntimeScanCacheKey(repoRoot, roots);
|
||||
|
||||
let pending = runtimeSourceScanCache.get(cacheKey);
|
||||
if (!pending) {
|
||||
pending = listRepoFiles(repoRoot, {
|
||||
roots,
|
||||
extensions: DEFAULT_RUNTIME_SOURCE_EXTENSIONS,
|
||||
skipHiddenDirectories: true,
|
||||
shouldIncludeFile: (relativePath) => !shouldSkipRuntimeSourcePath(relativePath),
|
||||
});
|
||||
runtimeSourceScanCache.set(cacheKey, pending);
|
||||
}
|
||||
const files = await pending;
|
||||
return files.filter((filePath) =>
|
||||
requestedExtensions.some((extension) => filePath.endsWith(extension)),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user