import { describe, expect, it } from "vitest"; import { appendBootstrapPromptWarning, analyzeBootstrapBudget, buildBootstrapInjectionStats, buildBootstrapPromptWarning, buildBootstrapTruncationReportMeta, buildBootstrapTruncationSignature, formatBootstrapTruncationWarningLines, resolveBootstrapWarningSignaturesSeen, } from "./bootstrap-budget.js"; import { buildAgentSystemPrompt } from "./system-prompt.js"; import type { WorkspaceBootstrapFile } from "./workspace.js"; describe("buildBootstrapInjectionStats", () => { it("maps raw and injected sizes and marks truncation", () => { const bootstrapFiles: WorkspaceBootstrapFile[] = [ { name: "AGENTS.md", path: "/tmp/AGENTS.md", content: "a".repeat(100), missing: false, }, { name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(50), missing: false, }, ]; const injectedFiles = [ { path: "/tmp/AGENTS.md", content: "a".repeat(100) }, { path: "/tmp/SOUL.md", content: "b".repeat(20) }, ]; const stats = buildBootstrapInjectionStats({ bootstrapFiles, injectedFiles, }); expect(stats).toHaveLength(2); expect(stats[0]).toMatchObject({ name: "AGENTS.md", rawChars: 100, injectedChars: 100, truncated: false, }); expect(stats[1]).toMatchObject({ name: "SOUL.md", rawChars: 50, injectedChars: 20, truncated: true, }); }); }); describe("analyzeBootstrapBudget", () => { it("reports per-file and total-limit causes", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/AGENTS.md", missing: false, rawChars: 150, injectedChars: 120, truncated: true, }, { name: "SOUL.md", path: "/tmp/SOUL.md", missing: false, rawChars: 90, injectedChars: 80, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); expect(analysis.hasTruncation).toBe(true); expect(analysis.totalNearLimit).toBe(true); expect(analysis.truncatedFiles).toHaveLength(2); const agents = analysis.truncatedFiles.find((file) => file.name === "AGENTS.md"); const soul = analysis.truncatedFiles.find((file) => file.name === "SOUL.md"); expect(agents?.causes).toContain("per-file-limit"); expect(agents?.causes).toContain("total-limit"); expect(soul?.causes).toContain("total-limit"); }); it("does not force a total-limit cause when totals are within limits", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/AGENTS.md", missing: false, rawChars: 90, injectedChars: 40, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); expect(analysis.truncatedFiles[0]?.causes).toEqual([]); }); }); describe("bootstrap prompt warnings", () => { it("appends warning details to the turn prompt instead of mutating the system prompt", () => { const prompt = appendBootstrapPromptWarning("Please continue.", [ "AGENTS.md: 200 raw -> 0 injected", ]); expect(prompt.startsWith("Please continue.")).toBe(true); expect(prompt).toContain("[Bootstrap truncation warning]"); expect(prompt).toContain("Treat Project Context as partial"); expect(prompt).toContain("- AGENTS.md: 200 raw -> 0 injected"); expect(prompt.endsWith("- AGENTS.md: 200 raw -> 0 injected")).toBe(true); }); it("preserves raw prompt whitespace when appending warning details", () => { const prompt = appendBootstrapPromptWarning(" indented\nkeep tail ", [ "AGENTS.md: 200 raw -> 0 injected", ]); expect(prompt).toContain(" indented\nkeep tail "); expect(prompt.indexOf(" indented\nkeep tail ")).toBe(0); }); it("preserves exact heartbeat prompts without warning suffixes", () => { const heartbeatPrompt = "Read HEARTBEAT.md. Reply HEARTBEAT_OK."; expect( appendBootstrapPromptWarning(heartbeatPrompt, ["AGENTS.md: 200 raw -> 0 injected"], { preserveExactPrompt: heartbeatPrompt, }), ).toBe(heartbeatPrompt); }); it("resolves seen signatures from report history or legacy single signature", () => { expect( resolveBootstrapWarningSignaturesSeen({ bootstrapTruncation: { warningSignaturesSeen: ["sig-a", " ", "sig-b", "sig-a"], promptWarningSignature: "legacy-ignored", }, }), ).toEqual(["sig-a", "sig-b"]); expect( resolveBootstrapWarningSignaturesSeen({ bootstrapTruncation: { promptWarningSignature: "legacy-only", }, }), ).toEqual(["legacy-only"]); expect(resolveBootstrapWarningSignaturesSeen(undefined)).toEqual([]); }); it("ignores single-signature fallback when warning mode is off", () => { expect( resolveBootstrapWarningSignaturesSeen({ bootstrapTruncation: { warningMode: "off", promptWarningSignature: "off-mode-signature", }, }), ).toEqual([]); expect( resolveBootstrapWarningSignaturesSeen({ bootstrapTruncation: { warningMode: "off", warningSignaturesSeen: ["prior-once-signature"], promptWarningSignature: "off-mode-signature", }, }), ).toEqual(["prior-once-signature"]); }); it("dedupes warnings in once mode by signature", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/AGENTS.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); const first = buildBootstrapPromptWarning({ analysis, mode: "once", }); expect(first.warningShown).toBe(true); expect(first.signature).toBeTruthy(); expect(first.lines.join("\n")).toContain("AGENTS.md"); const second = buildBootstrapPromptWarning({ analysis, mode: "once", seenSignatures: first.warningSignaturesSeen, }); expect(second.warningShown).toBe(false); expect(second.lines).toEqual([]); }); it("dedupes once mode across non-consecutive repeated signatures", () => { const analysisA = analyzeBootstrapBudget({ files: [ { name: "A.md", path: "/tmp/A.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); const analysisB = analyzeBootstrapBudget({ files: [ { name: "B.md", path: "/tmp/B.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); const firstA = buildBootstrapPromptWarning({ analysis: analysisA, mode: "once", }); expect(firstA.warningShown).toBe(true); const firstB = buildBootstrapPromptWarning({ analysis: analysisB, mode: "once", seenSignatures: firstA.warningSignaturesSeen, }); expect(firstB.warningShown).toBe(true); const secondA = buildBootstrapPromptWarning({ analysis: analysisA, mode: "once", seenSignatures: firstB.warningSignaturesSeen, }); expect(secondA.warningShown).toBe(false); }); it("includes overflow line when more files are truncated than shown", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "A.md", path: "/tmp/A.md", missing: false, rawChars: 10, injectedChars: 1, truncated: true, }, { name: "B.md", path: "/tmp/B.md", missing: false, rawChars: 10, injectedChars: 1, truncated: true, }, { name: "C.md", path: "/tmp/C.md", missing: false, rawChars: 10, injectedChars: 1, truncated: true, }, ], bootstrapMaxChars: 20, bootstrapTotalMaxChars: 10, }); const lines = formatBootstrapTruncationWarningLines({ analysis, maxFiles: 2, }); expect(lines).toContain("+1 more truncated file(s)."); }); it("disambiguates duplicate file names in warning lines", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/a/AGENTS.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, { name: "AGENTS.md", path: "/tmp/b/AGENTS.md", missing: false, rawChars: 140, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 300, }); const lines = formatBootstrapTruncationWarningLines({ analysis, }); expect(lines.join("\n")).toContain("AGENTS.md (/tmp/a/AGENTS.md)"); expect(lines.join("\n")).toContain("AGENTS.md (/tmp/b/AGENTS.md)"); }); it("respects off/always warning modes", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/AGENTS.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); const signature = buildBootstrapTruncationSignature(analysis); const off = buildBootstrapPromptWarning({ analysis, mode: "off", seenSignatures: [signature ?? ""], previousSignature: signature, }); expect(off.warningShown).toBe(false); expect(off.lines).toEqual([]); const always = buildBootstrapPromptWarning({ analysis, mode: "always", seenSignatures: [signature ?? ""], previousSignature: signature, }); expect(always.warningShown).toBe(true); expect(always.lines.length).toBeGreaterThan(0); }); it("uses file path in signature to avoid collisions for duplicate names", () => { const left = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/a/AGENTS.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); const right = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/b/AGENTS.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); expect(buildBootstrapTruncationSignature(left)).not.toBe( buildBootstrapTruncationSignature(right), ); }); it("builds truncation report metadata from analysis + warning decision", () => { const analysis = analyzeBootstrapBudget({ files: [ { name: "AGENTS.md", path: "/tmp/AGENTS.md", missing: false, rawChars: 150, injectedChars: 100, truncated: true, }, ], bootstrapMaxChars: 120, bootstrapTotalMaxChars: 200, }); const warning = buildBootstrapPromptWarning({ analysis, mode: "once", }); const meta = buildBootstrapTruncationReportMeta({ analysis, warningMode: "once", warning, }); expect(meta.warningMode).toBe("once"); expect(meta.warningShown).toBe(true); expect(meta.truncatedFiles).toBe(1); expect(meta.nearLimitFiles).toBeGreaterThanOrEqual(1); expect(meta.promptWarningSignature).toBeTruthy(); expect(meta.warningSignaturesSeen?.length).toBeGreaterThan(0); }); it("improves cache-relevant system prompt stability versus legacy warning injection", () => { const contextFiles = [{ path: "AGENTS.md", content: "Follow AGENTS guidance." }]; const warningLines = ["AGENTS.md: 200 raw -> 0 injected"]; const stableSystemPrompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", contextFiles, }); const optimizedTurns = [stableSystemPrompt, stableSystemPrompt, stableSystemPrompt]; const injectLegacyWarning = (prompt: string, lines: string[]) => { const warningBlock = [ "⚠ Bootstrap truncation warning:", ...lines.map((line) => `- ${line}`), "", ].join("\n"); return prompt.replace("## AGENTS.md", `${warningBlock}## AGENTS.md`); }; const legacyTurns = [ injectLegacyWarning(optimizedTurns[0] ?? "", warningLines), optimizedTurns[1] ?? "", injectLegacyWarning(optimizedTurns[2] ?? "", warningLines), ]; const cacheHitRate = (turns: string[]) => { const hits = turns.slice(1).filter((turn, index) => turn === turns[index]).length; return hits / Math.max(1, turns.length - 1); }; expect(cacheHitRate(legacyTurns)).toBe(0); expect(cacheHitRate(optimizedTurns)).toBe(1); expect(optimizedTurns[0]).not.toContain("⚠ Bootstrap truncation warning:"); }); });