import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { buildBootstrapContextFiles, DEFAULT_BOOTSTRAP_MAX_CHARS, DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS, resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars, } from "./pi-embedded-helpers.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; const makeFile = (overrides: Partial): WorkspaceBootstrapFile => ({ name: DEFAULT_AGENTS_FILENAME, path: "/tmp/AGENTS.md", content: "", missing: false, ...overrides, }); describe("buildBootstrapContextFiles", () => { it("keeps missing markers", () => { const files = [makeFile({ missing: true, content: undefined })]; expect(buildBootstrapContextFiles(files)).toEqual([ { path: "/tmp/AGENTS.md", content: "[MISSING] Expected at: /tmp/AGENTS.md", }, ]); }); it("skips empty or whitespace-only content", () => { const files = [makeFile({ content: " \n " })]; expect(buildBootstrapContextFiles(files)).toEqual([]); }); it("truncates large bootstrap content", () => { const head = `HEAD-${"a".repeat(600)}`; const tail = `${"b".repeat(300)}-TAIL`; const long = `${head}${tail}`; const files = [makeFile({ name: "TOOLS.md", content: long })]; const warnings: string[] = []; const maxChars = 200; const expectedTailChars = Math.floor(maxChars * 0.2); const [result] = buildBootstrapContextFiles(files, { maxChars, warn: (message) => warnings.push(message), }); expect(result?.content).toContain("[...truncated, read TOOLS.md for full content...]"); expect(result?.content.length).toBeLessThan(long.length); expect(result?.content.startsWith(long.slice(0, 120))).toBe(true); expect(result?.content.endsWith(long.slice(-expectedTailChars))).toBe(true); expect(warnings).toHaveLength(1); expect(warnings[0]).toContain("TOOLS.md"); expect(warnings[0]).toContain("limit 200"); }); it("keeps content under the default limit", () => { const long = "a".repeat(DEFAULT_BOOTSTRAP_MAX_CHARS - 10); const files = [makeFile({ content: long })]; const [result] = buildBootstrapContextFiles(files); expect(result?.content).toBe(long); expect(result?.content).not.toContain("[...truncated, read AGENTS.md for full content...]"); }); it("caps total injected bootstrap characters across files", () => { const files = [ makeFile({ name: "AGENTS.md", content: "a".repeat(10_000) }), makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(10_000) }), makeFile({ name: "USER.md", path: "/tmp/USER.md", content: "c".repeat(10_000) }), ]; const result = buildBootstrapContextFiles(files); const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0); expect(totalChars).toBeLessThanOrEqual(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS); expect(result).toHaveLength(3); expect(result[2]?.content).toContain("[...truncated, read USER.md for full content...]"); }); it("enforces strict total cap even when truncation markers are present", () => { const files = [ makeFile({ name: "AGENTS.md", content: "a".repeat(1_000) }), makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(1_000) }), ]; const result = buildBootstrapContextFiles(files, { maxChars: 100, totalMaxChars: 150, }); const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0); expect(totalChars).toBeLessThanOrEqual(150); }); it("skips bootstrap injection when remaining total budget is too small", () => { const files = [makeFile({ name: "AGENTS.md", content: "a".repeat(1_000) })]; const result = buildBootstrapContextFiles(files, { maxChars: 200, totalMaxChars: 40, }); expect(result).toEqual([]); }); it("keeps missing markers under small total budgets", () => { const files = [makeFile({ missing: true, content: undefined })]; const result = buildBootstrapContextFiles(files, { totalMaxChars: 20, }); expect(result).toHaveLength(1); expect(result[0]?.content.length).toBeLessThanOrEqual(20); expect(result[0]?.content.startsWith("[MISSING]")).toBe(true); }); }); describe("resolveBootstrapMaxChars", () => { it("returns default when unset", () => { expect(resolveBootstrapMaxChars()).toBe(DEFAULT_BOOTSTRAP_MAX_CHARS); }); it("uses configured value when valid", () => { const cfg = { agents: { defaults: { bootstrapMaxChars: 12345 } }, } as OpenClawConfig; expect(resolveBootstrapMaxChars(cfg)).toBe(12345); }); it("falls back when invalid", () => { const cfg = { agents: { defaults: { bootstrapMaxChars: -1 } }, } as OpenClawConfig; expect(resolveBootstrapMaxChars(cfg)).toBe(DEFAULT_BOOTSTRAP_MAX_CHARS); }); }); describe("resolveBootstrapTotalMaxChars", () => { it("returns default when unset", () => { expect(resolveBootstrapTotalMaxChars()).toBe(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS); }); it("uses configured value when valid", () => { const cfg = { agents: { defaults: { bootstrapTotalMaxChars: 12345 } }, } as OpenClawConfig; expect(resolveBootstrapTotalMaxChars(cfg)).toBe(12345); }); it("falls back when invalid", () => { const cfg = { agents: { defaults: { bootstrapTotalMaxChars: -1 } }, } as OpenClawConfig; expect(resolveBootstrapTotalMaxChars(cfg)).toBe(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS); }); });