// Bootstrap extra files hook tests cover extra file context injection. import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { makeTempWorkspace, writeWorkspaceFile } from "../../../test-helpers/workspace.js"; import { type AgentBootstrapHookContext, createInternalHookEvent as createHookEvent, } from "../../internal-hooks.js"; import handler from "./handler.js"; function createBootstrapExtraConfig(paths: string[]): OpenClawConfig { return { hooks: { internal: { entries: { "bootstrap-extra-files": { enabled: true, paths, }, }, }, }, }; } async function createBootstrapContext(params: { workspaceDir: string; cfg: OpenClawConfig; sessionKey: string; rootFiles: Array<{ name: string; content: string }>; }): Promise { const bootstrapFiles = (await Promise.all( params.rootFiles.map(async (file) => ({ name: file.name, path: await writeWorkspaceFile({ dir: params.workspaceDir, name: file.name, content: file.content, }), content: file.content, missing: false, })), )) as AgentBootstrapHookContext["bootstrapFiles"]; return { workspaceDir: params.workspaceDir, bootstrapFiles, cfg: params.cfg, sessionKey: params.sessionKey, }; } describe("bootstrap-extra-files hook", () => { it("appends extra bootstrap files from configured patterns", async () => { const tempDir = await makeTempWorkspace("openclaw-bootstrap-extra-"); const extraDir = path.join(tempDir, "packages", "core"); await fs.mkdir(extraDir, { recursive: true }); await fs.writeFile(path.join(extraDir, "AGENTS.md"), "extra agents", "utf-8"); const cfg = createBootstrapExtraConfig(["packages/*/AGENTS.md"]); const context = await createBootstrapContext({ workspaceDir: tempDir, cfg, sessionKey: "agent:main:main", rootFiles: [{ name: "AGENTS.md", content: "root agents" }], }); const event = createHookEvent("agent", "bootstrap", "agent:main:main", context); await handler(event); const injected = context.bootstrapFiles.filter((f) => f.name === "AGENTS.md"); expect(injected).toHaveLength(2); expect(injected.map((f) => path.relative(tempDir, f.path))).toContain( path.join("packages", "core", "AGENTS.md"), ); }); it("appends configured nested memory without applying session policy", async () => { const tempDir = await makeTempWorkspace("openclaw-bootstrap-extra-memory-"); const extraDir = path.join(tempDir, "packages", "core"); const sessionKey = "agent:main:slack:channel:c1"; await fs.mkdir(extraDir, { recursive: true }); await fs.writeFile(path.join(extraDir, "MEMORY.md"), "nested memory", "utf-8"); const cfg = createBootstrapExtraConfig(["packages/*/MEMORY.md"]); const context = await createBootstrapContext({ workspaceDir: tempDir, cfg, sessionKey, rootFiles: [{ name: "MEMORY.md", content: "private root memory" }], }); const event = createHookEvent("agent", "bootstrap", sessionKey, context); await handler(event); const relativePaths = context.bootstrapFiles.map((file) => path.relative(tempDir, file.path)); expect(relativePaths).toContain("MEMORY.md"); expect(relativePaths).toContain(path.join("packages", "core", "MEMORY.md")); }); it("leaves subagent allowlist enforcement to the final resolver", async () => { const tempDir = await makeTempWorkspace("openclaw-bootstrap-extra-subagent-"); const extraDir = path.join(tempDir, "packages", "persona"); await fs.mkdir(extraDir, { recursive: true }); await fs.writeFile(path.join(extraDir, "SOUL.md"), "extra persona", "utf-8"); const cfg = createBootstrapExtraConfig(["packages/*/SOUL.md"]); const context = await createBootstrapContext({ workspaceDir: tempDir, cfg, sessionKey: "agent:main:subagent:abc", rootFiles: [{ name: "AGENTS.md", content: "root agents" }], }); const event = createHookEvent("agent", "bootstrap", "agent:main:subagent:abc", context); await handler(event); expect(context.bootstrapFiles.map((f) => f.name).toSorted()).toEqual(["AGENTS.md", "SOUL.md"]); }); });