Files
openclaw/src/agents/bootstrap-cache.test.ts
T
Vincent Koc d06fc25a0c fix(agents): protect private memory in shared chats (#119198)
* fix(agents): protect private memory in shared chats

Fixes #84466

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* fix(agents): propagate shared chat type across runtimes

* fix(agents): enforce bootstrap privacy after hooks

* test(agents): cover final bootstrap filtering

* fix(agents): filter private memory before allowlists

* fix(agents): preserve bootstrap source identity

* refactor(agents): simplify bootstrap source identity

* fix(agents): keep bootstrap source identity private

* fix(agents): unify CLI runtime chat type

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-04 23:01:38 +08:00

155 lines
5.3 KiB
TypeScript

/**
* Regression coverage for per-session workspace bootstrap caching.
* Verifies reuse, refresh, pruning, and explicit cache clears.
*/
import { randomUUID } from "node:crypto";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { WorkspaceBootstrapFile } from "./workspace.js";
vi.mock("./workspace.js", () => ({
loadWorkspaceBootstrapFiles: vi.fn(),
workspaceFileSourceIdentitiesMatch: vi.fn(() => true),
}));
import { clearBootstrapSnapshot, getOrLoadBootstrapFiles } from "./bootstrap-cache.js";
import { loadWorkspaceBootstrapFiles, workspaceFileSourceIdentitiesMatch } from "./workspace.js";
let workspaceDir = "";
beforeEach(() => {
workspaceDir = `/ws/${randomUUID()}`;
});
function makeFile(name: string, content: string): WorkspaceBootstrapFile {
return {
name: name as WorkspaceBootstrapFile["name"],
path: `/ws/${name}`,
content,
missing: false,
};
}
describe("getOrLoadBootstrapFiles", () => {
const files = [makeFile("AGENTS.md", "# Agent"), makeFile("SOUL.md", "# Soul")];
const mockLoad = () => vi.mocked(loadWorkspaceBootstrapFiles);
beforeEach(() => {
mockLoad().mockResolvedValue(files);
vi.mocked(workspaceFileSourceIdentitiesMatch).mockReturnValue(true);
});
afterEach(() => {
vi.clearAllMocks();
});
it("loads from disk on first call and caches", async () => {
const result = await getOrLoadBootstrapFiles({
workspaceDir,
sessionKey: "session-1",
});
expect(result).toBe(files);
expect(mockLoad()).toHaveBeenCalledTimes(1);
});
it("refreshes from disk on second call while preserving unchanged object identity", async () => {
const refreshedFiles = [makeFile("AGENTS.md", "# Agent"), makeFile("SOUL.md", "# Soul")];
mockLoad().mockResolvedValueOnce(files).mockResolvedValueOnce(refreshedFiles);
const first = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
const result = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
expect(first).toBe(files);
expect(result).toBe(first);
expect(result).not.toBe(refreshedFiles);
expect(mockLoad()).toHaveBeenCalledTimes(2);
});
it("replaces cached result when workspace bootstrap contents change", async () => {
const updatedFiles = [makeFile("AGENTS.md", "# Agent v2"), makeFile("SOUL.md", "# Soul")];
mockLoad().mockResolvedValueOnce(files).mockResolvedValueOnce(updatedFiles);
const first = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
const result = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
expect(first).toBe(files);
expect(result).toBe(updatedFiles);
expect(mockLoad()).toHaveBeenCalledTimes(2);
});
it("replaces cached result when loader source identity changes", async () => {
const refreshedFiles = [makeFile("AGENTS.md", "# Agent"), makeFile("SOUL.md", "# Soul")];
mockLoad().mockResolvedValueOnce(files).mockResolvedValueOnce(refreshedFiles);
vi.mocked(workspaceFileSourceIdentitiesMatch).mockReturnValue(false);
const first = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
const result = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
expect(first).toBe(files);
expect(result).toBe(refreshedFiles);
});
it("different session keys get independent caches", async () => {
const files2 = [makeFile("AGENTS.md", "# Agent v2")];
mockLoad().mockResolvedValueOnce(files).mockResolvedValueOnce(files2);
const r1 = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-1" });
const r2 = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "session-2" });
expect(r1).toBe(files);
expect(r2).toBe(files2);
expect(mockLoad()).toHaveBeenCalledTimes(2);
});
it("evicts the oldest snapshot once the cache exceeds its cap", async () => {
for (let index = 0; index <= 64; index += 1) {
await getOrLoadBootstrapFiles({
workspaceDir,
sessionKey: `session-${index}`,
});
}
expect(mockLoad()).toHaveBeenCalledTimes(65);
await getOrLoadBootstrapFiles({
workspaceDir,
sessionKey: "session-0",
});
expect(mockLoad()).toHaveBeenCalledTimes(66);
});
});
describe("clearBootstrapSnapshot", () => {
const mockLoad = () => vi.mocked(loadWorkspaceBootstrapFiles);
beforeEach(() => {
mockLoad().mockResolvedValue([makeFile("AGENTS.md", "content")]);
});
afterEach(() => {
vi.clearAllMocks();
});
it("clears a single session entry", async () => {
await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "sk" });
clearBootstrapSnapshot("sk");
// Next call should hit disk again.
await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "sk" });
expect(mockLoad()).toHaveBeenCalledTimes(2);
});
it("does not affect other sessions", async () => {
await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "sk1" });
const first = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "sk2" });
clearBootstrapSnapshot("sk1");
// sk2 should still preserve its cached snapshot identity after refresh.
const second = await getOrLoadBootstrapFiles({ workspaceDir, sessionKey: "sk2" });
expect(second).toBe(first);
expect(mockLoad()).toHaveBeenCalledTimes(3); // sk1 x1, sk2 x2
});
});