mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
fix: clean up post-land CI guards
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export { root, FsSafeError } from "../sdk-security-runtime.js";
|
||||
@@ -1 +0,0 @@
|
||||
export { isNotFoundPathError, isPathInside } from "../sdk-security-runtime.js";
|
||||
@@ -2294,8 +2294,6 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
|
||||
return;
|
||||
case "session.long_running":
|
||||
case "session.stalled":
|
||||
case "session.recovery.completed":
|
||||
case "session.recovery.requested":
|
||||
return;
|
||||
case "session.stuck":
|
||||
recordSessionStuck(evt);
|
||||
|
||||
@@ -3,11 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { VoiceCallRealtimeFastContextConfig } from "./config.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getActiveMemorySearchManager: vi.fn(),
|
||||
resolveRealtimeVoiceFastContextConsult: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/plugins/memory-runtime.js", () => ({
|
||||
getActiveMemorySearchManager: mocks.getActiveMemorySearchManager,
|
||||
vi.mock("openclaw/plugin-sdk/realtime-voice", () => ({
|
||||
resolveRealtimeVoiceFastContextConsult: mocks.resolveRealtimeVoiceFastContextConsult,
|
||||
}));
|
||||
|
||||
import { resolveRealtimeFastContextConsult } from "./realtime-fast-context.js";
|
||||
@@ -36,16 +36,16 @@ function createLogger() {
|
||||
|
||||
describe("resolveRealtimeFastContextConsult", () => {
|
||||
beforeEach(() => {
|
||||
mocks.getActiveMemorySearchManager.mockReset();
|
||||
mocks.resolveRealtimeVoiceFastContextConsult.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("falls back to the full consult when memory manager setup fails", async () => {
|
||||
it("passes voice-call labels into the SDK fast context resolver", async () => {
|
||||
const logger = createLogger();
|
||||
mocks.getActiveMemorySearchManager.mockRejectedValue(new Error("memory misconfigured"));
|
||||
mocks.resolveRealtimeVoiceFastContextConsult.mockResolvedValue({ handled: false });
|
||||
|
||||
await expect(
|
||||
resolveRealtimeFastContextConsult({
|
||||
@@ -58,31 +58,17 @@ describe("resolveRealtimeFastContextConsult", () => {
|
||||
}),
|
||||
).resolves.toEqual({ handled: false });
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("memory misconfigured"));
|
||||
});
|
||||
|
||||
it("returns a bounded miss when memory manager setup exceeds the fast context timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
const logger = createLogger();
|
||||
mocks.getActiveMemorySearchManager.mockReturnValue(new Promise(() => {}));
|
||||
|
||||
const resultPromise = resolveRealtimeFastContextConsult({
|
||||
expect(mocks.resolveRealtimeVoiceFastContextConsult).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
sessionKey: "voice:15550001234",
|
||||
config: createFastContextConfig({ fallbackToConsult: false, timeoutMs: 25 }),
|
||||
config: createFastContextConfig({ fallbackToConsult: true }),
|
||||
args: { question: "What do you remember?" },
|
||||
logger,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(25);
|
||||
|
||||
await expect(resultPromise).resolves.toEqual({
|
||||
handled: true,
|
||||
result: {
|
||||
text: expect.stringContaining("No relevant OpenClaw memory or session context"),
|
||||
labels: {
|
||||
audienceLabel: "caller",
|
||||
contextName: "OpenClaw memory or session context",
|
||||
},
|
||||
});
|
||||
expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("timed out after 25ms"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { __setFsSafeTestHooksForTest } from "@openclaw/fs-safe/test-hooks";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { __setFsSafeTestHooksForTest } from "../infra/fs-safe-test-hooks.js";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
import { __testing, createExecTool } from "./bash-tools.exec.js";
|
||||
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
ArchiveSecurityError,
|
||||
createArchiveSymlinkTraversalError,
|
||||
mergeExtractedTreeIntoDestination,
|
||||
prepareArchiveDestinationDir,
|
||||
prepareArchiveOutputPath,
|
||||
withStagedArchiveDestination,
|
||||
} from "./archive-staging.js";
|
||||
|
||||
const directorySymlinkType = process.platform === "win32" ? "junction" : undefined;
|
||||
|
||||
describe("archive-staging helpers", () => {
|
||||
it("accepts real destination directories and returns their real path", async () => {
|
||||
await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => {
|
||||
const destDir = path.join(rootDir, "dest");
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
|
||||
await expect(prepareArchiveDestinationDir(destDir)).resolves.toBe(await fs.realpath(destDir));
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects symlink and non-directory archive destinations",
|
||||
async () => {
|
||||
await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => {
|
||||
const realDestDir = path.join(rootDir, "real-dest");
|
||||
const symlinkDestDir = path.join(rootDir, "dest-link");
|
||||
const fileDest = path.join(rootDir, "dest.txt");
|
||||
await fs.mkdir(realDestDir, { recursive: true });
|
||||
await fs.symlink(realDestDir, symlinkDestDir, directorySymlinkType);
|
||||
await fs.writeFile(fileDest, "nope", "utf8");
|
||||
|
||||
await expect(prepareArchiveDestinationDir(symlinkDestDir)).rejects.toMatchObject({
|
||||
code: "destination-symlink",
|
||||
} satisfies Partial<ArchiveSecurityError>);
|
||||
await expect(prepareArchiveDestinationDir(fileDest)).rejects.toMatchObject({
|
||||
code: "destination-not-directory",
|
||||
} satisfies Partial<ArchiveSecurityError>);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("creates in-destination parent directories for file outputs", async () => {
|
||||
await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => {
|
||||
const destDir = path.join(rootDir, "dest");
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
const destinationRealDir = await prepareArchiveDestinationDir(destDir);
|
||||
const outPath = path.join(destDir, "nested", "payload.txt");
|
||||
|
||||
await expect(
|
||||
prepareArchiveOutputPath({
|
||||
destinationDir: destDir,
|
||||
destinationRealDir,
|
||||
relPath: "nested/payload.txt",
|
||||
outPath,
|
||||
originalPath: "nested/payload.txt",
|
||||
isDirectory: false,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
await expect(fs.stat(path.dirname(outPath))).resolves.toMatchObject({
|
||||
isDirectory: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects output paths that traverse a destination symlink",
|
||||
async () => {
|
||||
await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => {
|
||||
const destDir = path.join(rootDir, "dest");
|
||||
const outsideDir = path.join(rootDir, "outside");
|
||||
const linkDir = path.join(destDir, "escape");
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
await fs.mkdir(outsideDir, { recursive: true });
|
||||
await fs.symlink(outsideDir, linkDir, directorySymlinkType);
|
||||
const destinationRealDir = await prepareArchiveDestinationDir(destDir);
|
||||
|
||||
await expect(
|
||||
prepareArchiveOutputPath({
|
||||
destinationDir: destDir,
|
||||
destinationRealDir,
|
||||
relPath: "escape/payload.txt",
|
||||
outPath: path.join(linkDir, "payload.txt"),
|
||||
originalPath: "escape/payload.txt",
|
||||
isDirectory: false,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "destination-symlink-traversal",
|
||||
} satisfies Partial<ArchiveSecurityError>);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("cleans up staged archive directories after success and failure", async () => {
|
||||
await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => {
|
||||
const destDir = path.join(rootDir, "dest");
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
const destinationRealDir = await prepareArchiveDestinationDir(destDir);
|
||||
let successStage = "";
|
||||
|
||||
await withStagedArchiveDestination({
|
||||
destinationRealDir,
|
||||
run: async (stagingDir) => {
|
||||
successStage = stagingDir;
|
||||
await fs.writeFile(path.join(stagingDir, "payload.txt"), "ok", "utf8");
|
||||
},
|
||||
});
|
||||
await expect(fs.stat(successStage)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
let failureStage = "";
|
||||
await expect(
|
||||
withStagedArchiveDestination({
|
||||
destinationRealDir,
|
||||
run: async (stagingDir) => {
|
||||
failureStage = stagingDir;
|
||||
throw new Error("boom");
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
await expect(fs.stat(failureStage)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"merges staged trees and rejects symlink entries from the source",
|
||||
async () => {
|
||||
await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => {
|
||||
const sourceDir = path.join(rootDir, "source");
|
||||
const sourceNestedDir = path.join(sourceDir, "nested");
|
||||
const destDir = path.join(rootDir, "dest");
|
||||
const outsideDir = path.join(rootDir, "outside");
|
||||
await fs.mkdir(sourceNestedDir, { recursive: true });
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
await fs.mkdir(outsideDir, { recursive: true });
|
||||
await fs.writeFile(path.join(sourceNestedDir, "payload.txt"), "hi", "utf8");
|
||||
|
||||
const destinationRealDir = await prepareArchiveDestinationDir(destDir);
|
||||
await mergeExtractedTreeIntoDestination({
|
||||
sourceDir,
|
||||
destinationDir: destDir,
|
||||
destinationRealDir,
|
||||
});
|
||||
await expect(
|
||||
fs.readFile(path.join(destDir, "nested", "payload.txt"), "utf8"),
|
||||
).resolves.toBe("hi");
|
||||
|
||||
await fs.symlink(outsideDir, path.join(sourceDir, "escape"), directorySymlinkType);
|
||||
await expect(
|
||||
mergeExtractedTreeIntoDestination({
|
||||
sourceDir,
|
||||
destinationDir: destDir,
|
||||
destinationRealDir,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "destination-symlink-traversal",
|
||||
} satisfies Partial<ArchiveSecurityError>);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("builds a typed archive symlink traversal error", () => {
|
||||
const error = createArchiveSymlinkTraversalError("nested/payload.txt");
|
||||
expect(error).toBeInstanceOf(ArchiveSecurityError);
|
||||
expect(error.code).toBe("destination-symlink-traversal");
|
||||
expect(error.message).toContain("nested/payload.txt");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import "./fs-safe-defaults.js";
|
||||
export {
|
||||
ArchiveSecurityError,
|
||||
createArchiveSymlinkTraversalError,
|
||||
mergeExtractedTreeIntoDestination,
|
||||
prepareArchiveDestinationDir,
|
||||
prepareArchiveOutputPath,
|
||||
withStagedArchiveDestination,
|
||||
type ArchiveSecurityErrorCode,
|
||||
} from "@openclaw/fs-safe/archive";
|
||||
@@ -1,2 +0,0 @@
|
||||
import "./fs-safe-defaults.js";
|
||||
export { __setFsSafeTestHooksForTest, type FsSafeTestHooks } from "@openclaw/fs-safe/test-hooks";
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { __setFsSafeTestHooksForTest } from "@openclaw/fs-safe/test-hooks";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createRebindableDirectoryAlias,
|
||||
withRealpathSymlinkRebindRace,
|
||||
} from "../test-utils/symlink-rebind-race.js";
|
||||
import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
|
||||
import { __setFsSafeTestHooksForTest } from "./fs-safe-test-hooks.js";
|
||||
import {
|
||||
resolveOpenedFileRealPathForHandle,
|
||||
FsSafeError,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as fsSafe from "../infra/fs-safe.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { MediaAttachmentCache } from "./attachments.js";
|
||||
@@ -222,13 +223,11 @@ describe("media understanding attachments SSRF", () => {
|
||||
const cache = new MediaAttachmentCache([{ index: 0, path: attachmentPath }], {
|
||||
localPathRoots: [allowedRoot],
|
||||
});
|
||||
const originalRealpath = fs.realpath.bind(fs);
|
||||
|
||||
vi.spyOn(fs, "realpath").mockImplementation(async (candidatePath) => {
|
||||
if (String(candidatePath) === attachmentPath) {
|
||||
vi.spyOn(fsSafe, "openLocalFileSafely").mockImplementation(async (params) => {
|
||||
if (params.filePath === attachmentPath) {
|
||||
throw new Error("EACCES");
|
||||
}
|
||||
return await originalRealpath(candidatePath);
|
||||
throw new Error(`Unexpected attachment path: ${params.filePath}`);
|
||||
});
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { resolveProcessScopedMap } from "./process-scoped-map.js";
|
||||
|
||||
const MAP_KEY = Symbol("process-scoped-map:test");
|
||||
const OTHER_MAP_KEY = Symbol("process-scoped-map:other");
|
||||
|
||||
afterEach(() => {
|
||||
delete (process as unknown as Record<symbol, unknown>)[MAP_KEY];
|
||||
delete (process as unknown as Record<symbol, unknown>)[OTHER_MAP_KEY];
|
||||
});
|
||||
|
||||
describe("shared/process-scoped-map", () => {
|
||||
it("reuses the same map for the same symbol", () => {
|
||||
const first = resolveProcessScopedMap<number>(MAP_KEY);
|
||||
first.set("a", 1);
|
||||
|
||||
const second = resolveProcessScopedMap<number>(MAP_KEY);
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(second.get("a")).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps distinct maps for distinct symbols", () => {
|
||||
const first = resolveProcessScopedMap<number>(MAP_KEY);
|
||||
const second = resolveProcessScopedMap<number>(OTHER_MAP_KEY);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("reuses a prepopulated process map without replacing it", () => {
|
||||
const existing = new Map<string, number>([["a", 1]]);
|
||||
(process as unknown as Record<symbol, unknown>)[MAP_KEY] = existing;
|
||||
|
||||
const resolved = resolveProcessScopedMap<number>(MAP_KEY);
|
||||
|
||||
expect(resolved).toBe(existing);
|
||||
expect(resolved.get("a")).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
export function resolveProcessScopedMap<T>(key: symbol): Map<string, T> {
|
||||
const proc = process as NodeJS.Process & {
|
||||
[symbolKey: symbol]: Map<string, T> | undefined;
|
||||
};
|
||||
const existing = proc[key];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const created = new Map<string, T>();
|
||||
proc[key] = created;
|
||||
return created;
|
||||
}
|
||||
Reference in New Issue
Block a user