fix: clean up post-land CI guards

This commit is contained in:
Peter Steinberger
2026-05-06 02:51:48 +01:00
parent 8294229592
commit b43efd3793
12 changed files with 17 additions and 271 deletions

View File

@@ -1 +0,0 @@
export { root, FsSafeError } from "../sdk-security-runtime.js";

View File

@@ -1 +0,0 @@
export { isNotFoundPathError, isPathInside } from "../sdk-security-runtime.js";

View File

@@ -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);

View File

@@ -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"));
});
});

View File

@@ -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";

View File

@@ -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");
});
});

View File

@@ -1,10 +0,0 @@
import "./fs-safe-defaults.js";
export {
ArchiveSecurityError,
createArchiveSymlinkTraversalError,
mergeExtractedTreeIntoDestination,
prepareArchiveDestinationDir,
prepareArchiveOutputPath,
withStagedArchiveDestination,
type ArchiveSecurityErrorCode,
} from "@openclaw/fs-safe/archive";

View File

@@ -1,2 +0,0 @@
import "./fs-safe-defaults.js";
export { __setFsSafeTestHooksForTest, type FsSafeTestHooks } from "@openclaw/fs-safe/test-hooks";

View File

@@ -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,

View File

@@ -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(

View File

@@ -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);
});
});

View File

@@ -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;
}